aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--models/git/lfs.go2
-rw-r--r--models/user/user.go14
-rw-r--r--modules/markup/html_node.go10
-rw-r--r--modules/markup/markdown/markdown_test.go19
-rw-r--r--modules/markup/sanitizer_default.go2
-rw-r--r--modules/structs/repo_file.go2
-rw-r--r--options/locale/locale_cs-CZ.ini11
-rw-r--r--options/locale/locale_de-DE.ini11
-rw-r--r--options/locale/locale_el-GR.ini11
-rw-r--r--options/locale/locale_en-US.ini32
-rw-r--r--options/locale/locale_es-ES.ini11
-rw-r--r--options/locale/locale_fa-IR.ini6
-rw-r--r--options/locale/locale_fi-FI.ini3
-rw-r--r--options/locale/locale_fr-FR.ini12
-rw-r--r--options/locale/locale_ga-IE.ini13
-rw-r--r--options/locale/locale_hu-HU.ini4
-rw-r--r--options/locale/locale_id-ID.ini3
-rw-r--r--options/locale/locale_is-IS.ini3
-rw-r--r--options/locale/locale_it-IT.ini6
-rw-r--r--options/locale/locale_ja-JP.ini11
-rw-r--r--options/locale/locale_ko-KR.ini4
-rw-r--r--options/locale/locale_lv-LV.ini11
-rw-r--r--options/locale/locale_nl-NL.ini5
-rw-r--r--options/locale/locale_pl-PL.ini6
-rw-r--r--options/locale/locale_pt-BR.ini10
-rw-r--r--options/locale/locale_pt-PT.ini15
-rw-r--r--options/locale/locale_ru-RU.ini11
-rw-r--r--options/locale/locale_si-LK.ini6
-rw-r--r--options/locale/locale_sk-SK.ini2
-rw-r--r--options/locale/locale_sv-SE.ini5
-rw-r--r--options/locale/locale_tr-TR.ini11
-rw-r--r--options/locale/locale_uk-UA.ini13
-rw-r--r--options/locale/locale_zh-CN.ini13
-rw-r--r--options/locale/locale_zh-HK.ini3
-rw-r--r--options/locale/locale_zh-TW.ini11
-rw-r--r--package-lock.json8
-rw-r--r--package.json2
-rw-r--r--routers/api/v1/repo/file.go3
-rw-r--r--routers/install/install.go2
-rw-r--r--routers/web/auth/auth.go12
-rw-r--r--routers/web/explore/repo.go1
-rw-r--r--routers/web/org/setting.go105
-rw-r--r--routers/web/repo/cherry_pick.go193
-rw-r--r--routers/web/repo/editor.go980
-rw-r--r--routers/web/repo/editor_apply_patch.go51
-rw-r--r--routers/web/repo/editor_cherry_pick.go86
-rw-r--r--routers/web/repo/editor_error.go82
-rw-r--r--routers/web/repo/editor_preview.go41
-rw-r--r--routers/web/repo/editor_test.go79
-rw-r--r--routers/web/repo/editor_uploader.go61
-rw-r--r--routers/web/repo/editor_util.go85
-rw-r--r--routers/web/repo/patch.go126
-rw-r--r--routers/web/repo/view.go5
-rw-r--r--routers/web/user/profile.go1
-rw-r--r--routers/web/web.go13
-rw-r--r--services/context/repo.go34
-rw-r--r--services/context/upload/upload.go2
-rw-r--r--services/forms/org.go6
-rw-r--r--services/forms/repo_form.go124
-rw-r--r--services/forms/repo_form_editor.go57
-rw-r--r--services/mailer/mail_test.go2
-rw-r--r--services/repository/files/content.go4
-rw-r--r--services/repository/files/file.go8
-rw-r--r--services/repository/files/file_test.go18
-rw-r--r--services/repository/files/update.go118
-rw-r--r--services/repository/files/upload.go238
-rw-r--r--services/webhook/feishu.go36
-rw-r--r--services/webhook/feishu_test.go6
-rw-r--r--services/webhook/payloader.go3
-rw-r--r--templates/admin/user/view.tmpl2
-rw-r--r--templates/explore/repos.tmpl4
-rw-r--r--templates/org/home.tmpl4
-rw-r--r--templates/org/settings/delete.tmpl35
-rw-r--r--templates/org/settings/navbar.tmpl3
-rw-r--r--templates/org/settings/options.tmpl178
-rw-r--r--templates/org/settings/options_dangerzone.tmpl93
-rw-r--r--templates/post-install.tmpl2
-rw-r--r--templates/repo/editor/cherry_pick.tmpl10
-rw-r--r--templates/repo/editor/commit_form.tmpl25
-rw-r--r--templates/repo/editor/common_breadcrumb.tmpl16
-rw-r--r--templates/repo/editor/delete.tmpl3
-rw-r--r--templates/repo/editor/edit.tmpl21
-rw-r--r--templates/repo/editor/patch.tmpl6
-rw-r--r--templates/repo/editor/upload.tmpl19
-rw-r--r--templates/repo/forks.tmpl14
-rw-r--r--templates/repo/issue/card.tmpl2
-rw-r--r--templates/repo/issue/view_content/attachments.tmpl2
-rw-r--r--templates/repo/issue/view_content/comments.tmpl2
-rw-r--r--templates/repo/settings/lfs_file.tmpl2
-rw-r--r--templates/repo/view_content.tmpl2
-rw-r--r--templates/shared/repo/list.tmpl (renamed from templates/explore/repo_list.tmpl)8
-rw-r--r--templates/shared/repo/search.tmpl (renamed from templates/shared/repo_search.tmpl)0
-rw-r--r--templates/shared/user/profile_big_avatar.tmpl2
-rw-r--r--templates/user/auth/signup_inner.tmpl3
-rw-r--r--templates/user/dashboard/feeds.tmpl2
-rw-r--r--templates/user/notification/notification_subscriptions.tmpl4
-rw-r--r--templates/user/profile.tmpl8
-rw-r--r--tests/integration/api_repo_languages_test.go5
-rw-r--r--tests/integration/api_repo_license_test.go6
-rw-r--r--tests/integration/editor_test.go25
-rw-r--r--tests/integration/empty_repo_test.go12
-rw-r--r--tests/integration/pull_compare_test.go3
-rw-r--r--tests/integration/repo_fork_test.go2
-rw-r--r--web_src/css/base.css4
-rw-r--r--web_src/css/modules/breadcrumb.css6
-rw-r--r--web_src/css/modules/dimmer.css2
-rw-r--r--web_src/css/modules/toast.css2
-rw-r--r--web_src/css/repo.css5
-rw-r--r--web_src/fomantic/build/components/dropdown.js6
-rw-r--r--web_src/fomantic/build/components/modal.js8
-rw-r--r--web_src/js/components/RepoContributors.vue2
-rw-r--r--web_src/js/features/common-fetch-action.ts9
-rw-r--r--web_src/js/features/install.ts2
-rw-r--r--web_src/js/features/repo-editor.ts28
-rw-r--r--web_src/js/modules/fomantic/dropdown.ts4
-rw-r--r--web_src/js/modules/fomantic/modal.ts10
-rw-r--r--web_src/js/modules/toast.ts29
117 files changed, 1480 insertions, 2066 deletions
diff --git a/models/git/lfs.go b/models/git/lfs.go
index bb6361050a..e4fa2b446a 100644
--- a/models/git/lfs.go
+++ b/models/git/lfs.go
@@ -112,7 +112,6 @@ type LFSMetaObject struct {
ID int64 `xorm:"pk autoincr"`
lfs.Pointer `xorm:"extends"`
RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
- Existing bool `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
@@ -146,7 +145,6 @@ func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMet
if err != nil {
return nil, err
} else if exist {
- m.Existing = true
return m, committer.Commit()
}
diff --git a/models/user/user.go b/models/user/user.go
index 86a3549345..7c871bf575 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -831,6 +831,20 @@ type CountUserFilter struct {
IsActive optional.Option[bool]
}
+// HasUsers checks whether there are any users in the database, or only one user exists.
+func HasUsers(ctx context.Context) (ret struct {
+ HasAnyUser, HasOnlyOneUser bool
+}, err error,
+) {
+ res, err := db.GetEngine(ctx).Table(&User{}).Cols("id").Limit(2).Query()
+ if err != nil {
+ return ret, fmt.Errorf("error checking user existence: %w", err)
+ }
+ ret.HasAnyUser = len(res) != 0
+ ret.HasOnlyOneUser = len(res) == 1
+ return ret, nil
+}
+
// CountUsers returns number of users.
func CountUsers(ctx context.Context, opts *CountUserFilter) int64 {
return countUsers(ctx, opts)
diff --git a/modules/markup/html_node.go b/modules/markup/html_node.go
index f67437465c..4eb78fdd2b 100644
--- a/modules/markup/html_node.go
+++ b/modules/markup/html_node.go
@@ -63,8 +63,11 @@ func processNodeA(ctx *RenderContext, node *html.Node) {
func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
next = img.NextSibling
+ attrSrc, hasLazy := "", false
for i, imgAttr := range img.Attr {
+ hasLazy = hasLazy || imgAttr.Key == "loading" && imgAttr.Val == "lazy"
if imgAttr.Key != "src" {
+ attrSrc = imgAttr.Val
continue
}
@@ -72,8 +75,8 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
isLinkable := imgSrcOrigin != "" && !strings.HasPrefix(imgSrcOrigin, "data:")
// By default, the "<img>" tag should also be clickable,
- // because frontend use `<img>` to paste the re-scaled image into the markdown,
- // so it must match the default markdown image behavior.
+ // because frontend uses `<img>` to paste the re-scaled image into the Markdown,
+ // so it must match the default Markdown image behavior.
cnt := 0
for p := img.Parent; isLinkable && p != nil && cnt < 2; p = p.Parent {
if hasParentAnchor := p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
@@ -98,6 +101,9 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
imgAttr.Val = camoHandleLink(imgAttr.Val)
img.Attr[i] = imgAttr
}
+ if !RenderBehaviorForTesting.DisableAdditionalAttributes && !hasLazy && !strings.HasPrefix(attrSrc, "data:") {
+ img.Attr = append(img.Attr, html.Attribute{Key: "loading", Val: "lazy"})
+ }
return next
}
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index 76434ac8b3..4eb01bcc2d 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -47,7 +47,7 @@ func TestRender_StandardLinks(t *testing.T) {
func TestRender_Images(t *testing.T) {
setting.AppURL = AppURL
- test := func(input, expected string) {
+ render := func(input, expected string) {
buffer, err := markdown.RenderString(markup.NewTestRenderContext(FullURL), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
@@ -59,27 +59,32 @@ func TestRender_Images(t *testing.T) {
result := util.URLJoin(FullURL, url)
// hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now
- test(
+ render(
"!["+title+"]("+url+")",
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
- test(
+ render(
"[["+title+"|"+url+"]]",
`<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" title="`+title+`" alt="`+title+`"/></a></p>`)
- test(
+ render(
"[!["+title+"]("+url+")]("+href+")",
`<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`)
- test(
+ render(
"!["+title+"]("+url+")",
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
- test(
+ render(
"[["+title+"|"+url+"]]",
`<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" title="`+title+`" alt="`+title+`"/></a></p>`)
- test(
+ render(
"[!["+title+"]("+url+")]("+href+")",
`<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`)
+
+ defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, false)()
+ render(
+ "<a><img src='a.jpg'></a>", // by the way, empty "a" tag will be removed
+ `<p dir="auto"><img src="http://localhost:3000/user13/repo11/a.jpg" loading="lazy"/></p>`)
}
func TestTotal_RenderString(t *testing.T) {
diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go
index 14161eb533..9288be3b28 100644
--- a/modules/markup/sanitizer_default.go
+++ b/modules/markup/sanitizer_default.go
@@ -52,6 +52,8 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
+ policy.AllowAttrs("loading").OnElements("img")
+
// Allow generally safe attributes (reference: https://github.com/jch/html-pipeline)
generalSafeAttrs := []string{
"abbr", "accept", "accept-charset",
diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go
index b0e0bd979e..a281620a3b 100644
--- a/modules/structs/repo_file.go
+++ b/modules/structs/repo_file.go
@@ -66,6 +66,8 @@ func (o *UpdateFileOptions) Branch() string {
return o.FileOptions.BranchName
}
+// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options.
+
// ChangeFileOperation for creating, updating or deleting a file
type ChangeFileOperation struct {
// indicates what to do with the file
diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 2a3bd3e743..53a286d9bd 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -1330,7 +1330,6 @@ editor.update=Aktualizovat %s
editor.delete=Odstranit %s
editor.patch=Použít záplatu
editor.patching=Záplatování:
-editor.fail_to_apply_patch=Nelze použít záplatu „%s“
editor.new_patch=Nová záplata
editor.commit_message_desc=Přidat volitelný rozšířený popis…
editor.signoff_desc=Přidat Signed-off-by podpis přispěvatele na konec zprávy o commitu.
@@ -1348,8 +1347,6 @@ editor.branch_already_exists=Větev „%s“ již existuje v tomto repozitáři.
editor.directory_is_a_file=Jméno adresáře „%s“ je již použito jako jméno souboru v tomto repozitáři.
editor.file_is_a_symlink=`„%s“ je symbolický odkaz. Symbolické odkazy nemohou být upravovány ve webovém editoru`
editor.filename_is_a_directory=Jméno souboru „%s“ je již použito jako jméno adresáře v tomto repozitáři.
-editor.file_editing_no_longer_exists=Upravovaný soubor „%s“ již není souÄástí tohoto repozitáře.
-editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již není souÄástí tohoto repozitáře.
editor.file_changed_while_editing=Obsah souboru byl zmÄ›nÄ›n od doby, kdy jste zaÄaly s úpravou. <a target="_blank" rel="noopener noreferrer" href="%s">KliknÄ›te zde</a>, abyste je zobrazili, nebo <strong>potvrÄte zmÄ›ny jeÅ¡tÄ› jednou</strong> pro jejich pÅ™epsání.
editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři.
editor.commit_id_not_matching=ID commitu se neshoduje s ID, když jsi zaÄal/a s úpravami. Odevzdat do záplatové vÄ›tve a poté slouÄit.
@@ -1357,8 +1354,6 @@ editor.push_out_of_date=Nahrání se zdá být zastaralé.
editor.commit_empty_file_header=Odevzdat prázdný soubor
editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. PokraÄovat?
editor.no_changes_to_show=Žádné změny k zobrazení.
-editor.fail_to_update_file=Nepodařilo se aktualizovat/vytvořit soubor „%s“.
-editor.fail_to_update_file_summary=Chybové hlášení:
editor.push_rejected_no_message=ZmÄ›na byla serverem zamítnuta bez zprávy. Prosím, zkontrolujte háÄky Gitu.
editor.push_rejected=ZmÄ›na byla serverem zamítnuta. Prosím, zkontrolujte háÄky Gitu.
editor.push_rejected_summary=Úplná zpráva o odmítnutí:
@@ -2780,15 +2775,13 @@ settings.visibility.private_shortname=Soukromý
settings.update_settings=Upravit nastavení
settings.update_setting_success=Nastavení organizace bylo upraveno.
-settings.change_orgname_prompt=Poznámka: Změna názvu organizace také změní adresu URL vaší organizace a uvolní staré jméno této organizace.
-settings.change_orgname_redirect_prompt=Staré jméno bude přesměrovávat, dokud nebude znovu obsazeno.
+
+
settings.update_avatar_success=Avatar organizace byl aktualizován.
settings.delete=Smazat organizaci
settings.delete_account=Smazat tuto organizaci
settings.delete_prompt=Organizace bude trvale odstraněna. Tato změna <strong>NEMŮŽE</strong> být vrácena!
settings.confirm_delete_account=Potvrdit smazání
-settings.delete_org_title=Smazat organizaci
-settings.delete_org_desc=Tato organizace bude trvale smazána. PokraÄovat?
settings.hooks_desc=PÅ™idat webové háÄky, které budou spouÅ¡tÄ›ny pro <strong>vÅ¡echny repozitáře</strong> v této organizaci.
settings.labels_desc=Přidejte štítky, které mohou být použity pro úkoly <strong>všech repositářů</strong> v rámci této organizace.
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index 56dcadd451..3157fae8ca 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -1352,7 +1352,6 @@ editor.update=%s aktualisiert
editor.delete=%s gelöscht
editor.patch=Patch anwenden
editor.patching=Patche:
-editor.fail_to_apply_patch=Patch "%s" nicht anwendbar
editor.new_patch=Neuer Patch
editor.commit_message_desc=Eine ausführlichere (optionale) Beschreibung hinzufügen…
editor.signoff_desc=Am Ende der Commit Nachricht einen Signed-off-by Anhang vom Committer hinzufügen.
@@ -1372,8 +1371,6 @@ editor.branch_already_exists=Branch "%s" existiert bereits in diesem Repository.
editor.directory_is_a_file=Der Verzeichnisname "%s" wird bereits als Dateiname in diesem Repository verwendet.
editor.file_is_a_symlink=`"%s" ist ein symbolischer Link. Symbolische Links können mit dem Web-Editor nicht bearbeitet werden`
editor.filename_is_a_directory=Der Dateiname "%s" wird bereits als Verzeichnisname in diesem Repository verwendet.
-editor.file_editing_no_longer_exists=Die bearbeitete Datei "%s" existiert nicht mehr in diesem Repository.
-editor.file_deleting_no_longer_exists=Die zu löschende Datei "%s" existiert nicht mehr in diesem Repository.
editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. <a target="_blank" rel="noopener noreferrer" href="%s">Hier klicken</a>, um die Änderungen anzusehen, oder <strong>Änderungen erneut comitten</strong>, um sie zu überschreiben.
editor.file_already_exists=Eine Datei mit dem Namen '%s' existiert bereits in diesem Repository.
editor.commit_id_not_matching=Die Commit-ID stimmt nicht mit der ID überein, bei welcher du mit der Bearbeitung begonnen hast. Commite in einen Patch-Branch und merge daraufhin.
@@ -1381,8 +1378,6 @@ editor.push_out_of_date=Der Push scheint veraltet zu sein.
editor.commit_empty_file_header=Leere Datei committen
editor.commit_empty_file_text=Die Datei, die du commiten willst, ist leer. Fortfahren?
editor.no_changes_to_show=Keine Änderungen vorhanden.
-editor.fail_to_update_file=Fehler beim Aktualisieren/Erstellen der Datei "%s".
-editor.fail_to_update_file_summary=Fehlermeldung:
editor.push_rejected_no_message=Die Änderung wurde vom Server ohne Nachricht abgelehnt. Bitte überprüfe die Git Hooks.
editor.push_rejected=Die Änderung wurde vom Server abgelehnt. Bitte überprüfe die Git Hooks.
editor.push_rejected_summary=Vollständige Ablehnungsmeldung:
@@ -2829,15 +2824,13 @@ settings.visibility.private_shortname=Privat
settings.update_settings=Einstellungen speichern
settings.update_setting_success=Organisationseinstellungen wurden aktualisiert.
-settings.change_orgname_prompt=Hinweis: Das Ändern des Organisationsnamens wird auch die URL deiner Organisation ändern und den alten Namen freigeben.
-settings.change_orgname_redirect_prompt=Der alte Name wird weiterleiten, bis er wieder beansprucht wird.
+
+
settings.update_avatar_success=Der Organisationsavatar wurde aktualisiert.
settings.delete=Organisation löschen
settings.delete_account=Diese Organisation löschen
settings.delete_prompt=Die Organisation wird dauerhaft gelöscht. Dies <strong>KANN NICHT</strong> rückgängig gemacht werden!
settings.confirm_delete_account=Löschen bestätigen
-settings.delete_org_title=Organisation löschen
-settings.delete_org_desc=Diese Organisation wird dauerhaft gelöscht. Fortfahren?
settings.hooks_desc=Webhooks hinzufügen, die für <strong>alle</strong> Repositories dieser Organisation ausgelöst werden.
settings.labels_desc=Labels hinzufügen, die für <strong>alle Repositories</strong> dieser Organisation genutzt werden können.
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 444fbd26c9..02b7296fe2 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -1190,7 +1190,6 @@ editor.update=ΕνημέÏωση %s
editor.delete=ΔιαγÏαφή %s
editor.patch=ΕφαÏμογή ΔιόÏθωσης
editor.patching=ΕπιδιόÏθωση:
-editor.fail_to_apply_patch=`Αδυναμία εφαÏμογής της επιδιόÏθωσης "%s"`
editor.new_patch=Îέα ΔιόÏθωση
editor.commit_message_desc=ΠÏοσθήκη Ï€ÏοαιÏετικής εκτενοÏÏ‚ πεÏιγÏαφής…
editor.signoff_desc=ΠÏοσθέστε ένα Ï€Ïόσθετο Signed-off-by στο τέλος του μηνÏματος καταγÏαφής της υποβολής.
@@ -1208,15 +1207,11 @@ editor.branch_already_exists=Ο κλάδος "%s" υπάÏχει ήδη σε αÏ
editor.directory_is_a_file=Το όνομα φακέλου "%s" χÏησιμοποιείται ήδη ως όνομα αÏχείου σε αυτό το αποθετήÏιο.
editor.file_is_a_symlink=`Το "%s" είναι συμβολικός σÏνδεσμος. Οι συμβολικοί σÏνδεσμοι δεν μποÏοÏν να επεξεÏγαστοÏν στην ενσωματωμένη εφαÏμογή`
editor.filename_is_a_directory=Το όνομα αÏχείου "%s" χÏησιμοποιείται ήδη ως όνομα φακέλου σε αυτό το αποθετήÏιο.
-editor.file_editing_no_longer_exists=Το αÏχείο "%s" που επεξεÏγάζεται, δεν υπάÏχει πλέον σε αυτό το αποθετήÏιο.
-editor.file_deleting_no_longer_exists=Το αÏχείο "%s" που διαγÏάφεται, δεν υπάÏχει πλέον σε αυτό το αποθετήÏιο.
editor.file_changed_while_editing=Τα πεÏιεχόμενα του αÏχείου άλλαξαν από τότε που ξεκίνησε η επεξεÏγασία. <a target="_blank" rel="noopener noreferrer" href="%s">Κάντε κλικ εδώ</a> για να τα δείτε ή <strong>Υποβολή Αλλαγών ξανά</strong> για να τα αντικαταστήσετε.
editor.file_already_exists=Ένα αÏχείο με το όνομα "%s" υπάÏχει ήδη σε αυτό το αποθετήÏιο.
editor.commit_empty_file_header=Υποβολή ενός ÎºÎµÎ½Î¿Ï Î±Ïχείου
editor.commit_empty_file_text=Το αÏχείο που Ï€Ïόκειται να υποβληθεί είναι κενό. Συνέχεια;
editor.no_changes_to_show=Δεν υπάÏχουν αλλαγές για εμφάνιση.
-editor.fail_to_update_file=Αποτυχία ενημέÏωσης/δημιουÏγίας του αÏχείου "%s".
-editor.fail_to_update_file_summary=Μήνυμα Σφάλματος:
editor.push_rejected_no_message=Η αλλαγή αποÏÏίφθηκε από το διακομιστή χωÏίς κάποιο μήνυμα. ΠαÏακαλώ ελέγξτε τα ΆγκιστÏα Git.
editor.push_rejected=Η αλλαγή αποÏÏίφθηκε από τον διακομιστή. ΠαÏακαλώ ελέγξτε τα ΆγκιστÏα Git.
editor.push_rejected_summary=Μήνυμα ΠλήÏους ΑπόÏÏιψης:
@@ -2505,15 +2500,13 @@ settings.visibility.private_shortname=Ιδιωτικός
settings.update_settings=ΕνημέÏωση Ρυθμίσεων
settings.update_setting_success=Οι Ïυθμίσεις του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï Î­Ï‡Î¿Ï…Î½ ενημεÏωθεί.
-settings.change_orgname_prompt=Σημείωση: Η αλλαγή του ονόματος του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï Î¸Î± αλλάξει επίσης τη διεÏθυνση URL του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï ÏƒÎ±Ï‚ και θα απελευθεÏώσει το παλιό όνομα.
-settings.change_orgname_redirect_prompt=Το παλιό όνομα θα ανακατευθÏνει μέχÏι να διεκδικηθεί.
+
+
settings.update_avatar_success=Η εικόνα του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï Î­Ï‡ÎµÎ¹ ενημεÏωθεί.
settings.delete=ΔιαγÏαφή ΟÏγανισμοÏ
settings.delete_account=ΔιαγÏαφή Î‘Ï…Ï„Î¿Ï Î¤Î¿Ï… ΟÏγανισμοÏ
settings.delete_prompt=Ο οÏγανισμός θα αφαιÏεθεί οÏιστικά. Αυτό το <strong>ΔΕΠΜΠΟΡΕΙ</strong> να αναιÏεθεί!
settings.confirm_delete_account=Επιβεβαίωση ΔιαγÏαφής
-settings.delete_org_title=ΔιαγÏαφή ΟÏγανισμοÏ
-settings.delete_org_desc=Αυτός ο οÏγανισμός θα διαγÏαφεί οÏιστικά. Συνέχεια;
settings.hooks_desc=ΠÏοσθήκη webhooks που θα ενεÏγοποιοÏνται για <strong>όλα τα αποθετήÏια</strong> κάτω από αυτό τον οÏγανισμό.
settings.labels_desc=ΠÏοσθήκη σημάτων που μποÏοÏν να χÏησιμοποιηθοÏν σε ζητήματα για <strong>όλα τα αποθετήÏια</strong> κάτω από αυτό τον οÏγανισμό.
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 8a0e0abf20..4d1f7e7bf4 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -421,6 +421,7 @@ remember_me.compromised = The login token is not valid anymore which may indicat
forgot_password_title= Forgot Password
forgot_password = Forgot password?
need_account = Need an account?
+sign_up_tip = You are registering the first account in the system, which has administrator privileges. Please carefully remember your username and password. If you forget the username or password, please refer to the Gitea documentation to recover the account.
sign_up_now = Register now.
sign_up_successful = Account was successfully created. Welcome!
confirmation_mail_sent_prompt_ex = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it.
@@ -1354,7 +1355,7 @@ editor.update = Update %s
editor.delete = Delete %s
editor.patch = Apply Patch
editor.patching = Patching:
-editor.fail_to_apply_patch = Unable to apply patch "%s"
+editor.fail_to_apply_patch = Unable to apply patch
editor.new_patch = New Patch
editor.commit_message_desc = Add an optional extended description…
editor.signoff_desc = Add a Signed-off-by trailer by the committer at the end of the commit log message.
@@ -1374,8 +1375,7 @@ editor.branch_already_exists = Branch "%s" already exists in this repository.
editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository.
editor.file_is_a_symlink = `"%s" is a symbolic link. Symbolic links cannot be edited in the web editor`
editor.filename_is_a_directory = Filename "%s" is already used as a directory name in this repository.
-editor.file_editing_no_longer_exists = The file being edited, "%s", no longer exists in this repository.
-editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository.
+editor.file_modifying_no_longer_exists = The file being modified, "%s", no longer exists in this repository.
editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them.
editor.file_already_exists = A file named "%s" already exists in this repository.
editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing. Commit into a patch branch and then merge.
@@ -1383,8 +1383,6 @@ editor.push_out_of_date = The push appears to be out of date.
editor.commit_empty_file_header = Commit an empty file
editor.commit_empty_file_text = The file you're about to commit is empty. Proceed?
editor.no_changes_to_show = There are no changes to show.
-editor.fail_to_update_file = Failed to update/create file "%s".
-editor.fail_to_update_file_summary = Error Message:
editor.push_rejected_no_message = The change was rejected by the server without a message. Please check Git Hooks.
editor.push_rejected = The change was rejected by the server. Please check Git Hooks.
editor.push_rejected_summary = Full Rejection Message:
@@ -1398,6 +1396,8 @@ editor.user_no_push_to_branch = User cannot push to branch
editor.require_signed_commit = Branch requires a signed commit
editor.cherry_pick = Cherry-pick %s onto:
editor.revert = Revert %s onto:
+editor.failed_to_commit = Failed to commit changes.
+editor.failed_to_commit_summary = Error Message:
commits.desc = Browse source code change history.
commits.commits = Commits
@@ -2812,6 +2812,7 @@ team_permission_desc = Permission
team_unit_desc = Allow Access to Repository Sections
team_unit_disabled = (Disabled)
+form.name_been_taken = The organisation name "%s" has already been taken.
form.name_reserved = The organization name "%s" is reserved.
form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name.
form.create_org_not_allowed = You are not allowed to create an organization.
@@ -2833,15 +2834,28 @@ settings.visibility.private_shortname = Private
settings.update_settings = Update Settings
settings.update_setting_success = Organization settings have been updated.
-settings.change_orgname_prompt = Note: Changing the organization name will also change your organization's URL and free the old name.
-settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed.
+
+settings.rename = Rename Organization
+settings.rename_desc = Changing the organization name will also change your organization's URL and free the old name.
+settings.rename_success = Organization %[1]s have been renamed to %[2]s successfully.
+settings.rename_no_change = Organization name is no change.
+settings.rename_new_org_name = New Organization Name
+settings.rename_failed = Rename Organization failed because of internal error
+settings.rename_notices_1 = This operation <strong>CANNOT</strong> be undone.
+settings.rename_notices_2 = The old name will redirect until it is claimed.
+
settings.update_avatar_success = The organization's avatar has been updated.
settings.delete = Delete Organization
settings.delete_account = Delete This Organization
settings.delete_prompt = The organization will be permanently removed. This <strong>CANNOT</strong> be undone!
+settings.name_confirm = Enter the organization name as confirmation:
+settings.delete_notices_1 = This operation <strong>CANNOT</strong> be undone.
+settings.delete_notices_2 = This operation will permanently delete all the <strong>repositories</strong> of <strong>%s</strong> including code, issues, comments, wiki data and collaborator settings.
+settings.delete_notices_3 = This operation will permanently delete all the <strong>packages</strong> of <strong>%s</strong>.
+settings.delete_notices_4 = This operation will permanently delete all the <strong>projects</strong> of <strong>%s</strong>.
settings.confirm_delete_account = Confirm Deletion
-settings.delete_org_title = Delete Organization
-settings.delete_org_desc = This organization will be deleted permanently. Continue?
+settings.delete_failed = Delete Organization failed because of internal error
+settings.delete_successful = Organization <b>%s</b> has been deleted successfully.
settings.hooks_desc = Add webhooks which will be triggered for <strong>all repositories</strong> under this organization.
settings.labels_desc = Add labels which can be used on issues for <strong>all repositories</strong> under this organization.
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index 521583395e..9f50eb27a0 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -1180,7 +1180,6 @@ editor.update=Actualizar %s
editor.delete=Eliminar %s
editor.patch=Aplicar parche
editor.patching=Parcheando:
-editor.fail_to_apply_patch=`No se puede aplicar el parche "%s"`
editor.new_patch=Nuevo parche
editor.commit_message_desc=Añadir una descripción extendida opcional…
editor.signoff_desc=Añadir un trailer firmado por el committer al final del mensaje de registro de confirmación.
@@ -1198,15 +1197,11 @@ editor.branch_already_exists=La rama "%s" ya existe en este repositorio.
editor.directory_is_a_file=Nombre del directorio "%s" ya se utiliza como nombre de archivo en este repositorio.
editor.file_is_a_symlink=`"%s" es un enlace simbólico. Los enlaces simbólicos no se pueden editar en el editor web`
editor.filename_is_a_directory=Nombre de archivo "%s" ya se utiliza como nombre de directorio en este repositorio.
-editor.file_editing_no_longer_exists=El archivo que se está editando, "%s", ya no existe en este repositorio.
-editor.file_deleting_no_longer_exists=El archivo que se está eliminando, "%s", ya no existe en este repositorio.
editor.file_changed_while_editing=Desde que comenzó a editar, el contenido del archivo ha sido cambiado. <a target="_blank" rel="noopener noreferrer" href="%s">Haga clic aquí</a> para ver qué ha cambiado o <strong>presione confirmar de nuevo</strong> para sobrescribir los cambios.
editor.file_already_exists=Ya existe un archivo llamado "%s" en este repositorio.
editor.commit_empty_file_header=Commit un archivo vacío
editor.commit_empty_file_text=El archivo que estás tratando de commit está vacío. ¿Proceder?
editor.no_changes_to_show=No existen cambios para mostrar.
-editor.fail_to_update_file=Error al actualizar/crear el archivo "%s".
-editor.fail_to_update_file_summary=Mensaje de error
editor.push_rejected_no_message=El cambio fue rechazado por el servidor sin un mensaje. Por favor, compruebe Git Hooks.
editor.push_rejected=El cambio fue rechazado por el servidor. Por favor, comprueba los Git Hooks.
editor.push_rejected_summary=Mensaje completo de rechazo
@@ -2487,15 +2482,13 @@ settings.visibility.private_shortname=Privado
settings.update_settings=Actualizar configuración
settings.update_setting_success=Configuración de la organización se han actualizado.
-settings.change_orgname_prompt=Nota: Cambiar el nombre de la organización también cambiará la URL de su organización y liberará el nombre antiguo.
-settings.change_orgname_redirect_prompt=El nombre antiguo se redirigirá hasta que se reclame.
+
+
settings.update_avatar_success=Se ha actualizado el avatar de la organización.
settings.delete=Eliminar organización
settings.delete_account=Eliminar esta organización
settings.delete_prompt=La organización será eliminada permanentemente. ¡Esta acción <strong>NO PUEDE</strong> deshacerse!
settings.confirm_delete_account=Confirmar eliminación
-settings.delete_org_title=Eliminar organización
-settings.delete_org_desc=Esta organización se eliminará permanentemente. ¿Continuar?
settings.hooks_desc=Añadir webhooks que serán ejecutados para <strong>todos los repositorios</strong> de esta organización.
settings.labels_desc=Añadir etiquetas que pueden ser utilizadas en problemas para <strong>todos los repositorios</strong> bajo esta organización.
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index 18abc0f401..ce1a0ef9ef 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -943,7 +943,6 @@ editor.file_changed_while_editing=محتوای پرونده تغییر میکنØ
editor.commit_empty_file_header=کامیت کردن یک پرونده خالی
editor.commit_empty_file_text=ÙØ§ÛŒÙ„ÛŒ Ú©Ù‡ درخواست ارسال دارید خالی است. ادامه بدم?
editor.no_changes_to_show=تغییری برای نمایش وجود ندارد.
-editor.fail_to_update_file_summary=متن خطا:
editor.push_rejected_summary=متن کامل پیام دلیل رد شدن:
editor.add_subdir=Ø§ÙØ²ÙˆØ¯Ù† پوشه…
editor.no_commit_to_branch=نمی‌توان به طور مستقیم درمورد شاخه نطر داد زیرا:
@@ -1920,14 +1919,13 @@ settings.visibility.private_shortname=پوشیده
settings.update_settings=به‌ روزرسانی تنظیمات
settings.update_setting_success=تنظیمات این سازمان به‌روز شد.
-settings.change_orgname_redirect_prompt=نام قدیمی تا زمانی که ادعا شود تغییر مسیر می دهد.
+
+
settings.update_avatar_success=آواتار این سازمان به‌روز شد.
settings.delete=حذ٠سازمان
settings.delete_account=حذ٠این سازمان
settings.delete_prompt=سازمان برای همیشه حذ٠خواهد شد. این قابل برگشت <strong>نخواهد بود</strong>!
settings.confirm_delete_account=تاییدیه حذÙ
-settings.delete_org_title=حذ٠سازمان
-settings.delete_org_desc=سازمان برای همیشه حذ٠خواهد شد. آیا همچنان ادامه می‌دهید؟
settings.hooks_desc=Ø§ÙØ²ÙˆØ¯Ù† webhook های Ú©Ù‡ برای<strong> تمام مخازن</strong> این سازمان اجرا میشود.
settings.labels_desc=تگ هایی را اضاÙÙ‡ کنید Ú©Ù‡ می‌توانند برای مشکلات <strong>همه مخازن</strong> تحت این سازمان Ø§Ø³ØªÙØ§Ø¯Ù‡ شوند.
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index b925d6f43a..ca4bcce2f7 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -1318,11 +1318,12 @@ settings.visibility.private=Yksityinen (näkyvä vain organisaation jäsenille)
settings.visibility.private_shortname=Yksityinen
settings.update_settings=Päivitä asetukset
+
+
settings.delete=Poista organisaatio
settings.delete_account=Poista tämä organisaatio
settings.delete_prompt=Organisaatio poistetaan pysyvästi, ja tätä <strong>EI VOI</strong> peruuttaa myöhemmin!
settings.confirm_delete_account=Vahvista poisto
-settings.delete_org_title=Poista organisaatio
settings.hooks_desc=Lisää webkoukkuja, jotka suoritetaan <strong>kaikissa repoissa</strong> tässä organisaatiossa.
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 5dd918fb91..76d4a635d0 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -1354,7 +1354,6 @@ editor.update=Actualiser %s
editor.delete=Supprimer %s
editor.patch=Appliquer le correctif
editor.patching=Correction:
-editor.fail_to_apply_patch=`Impossible d'appliquer le correctif "%s"`
editor.new_patch=Nouveau correctif
editor.commit_message_desc=Ajouter une description détaillée facultative…
editor.signoff_desc=Créditer l'auteur "Signed-off-by:" en pied de révision.
@@ -1374,8 +1373,6 @@ editor.branch_already_exists=La branche "%s" existe déjà dans ce dépôt.
editor.directory_is_a_file=Le nom de dossier "%s" est déjà utilisé comme nom de fichier dans ce dépôt.
editor.file_is_a_symlink=`« %s » est un lien symbolique. Ce type de fichiers ne peut être modifié dans l'éditeur web.`
editor.filename_is_a_directory=« %s » est déjà utilisé comme nom de dossier dans ce dépôt.
-editor.file_editing_no_longer_exists=Impossible de modifier le fichier « %s » car il n’existe plus dans ce dépôt.
-editor.file_deleting_no_longer_exists=Impossible de supprimer le fichier « %s » car il n’existe plus dans ce dépôt.
editor.file_changed_while_editing=Le contenu du fichier a changé depuis que vous avez commencé à éditer. <a target="_blank" rel="noopener noreferrer" href="%s">Cliquez ici</a> pour voir les changements ou <strong>soumettez de nouveau</strong> pour les écraser.
editor.file_already_exists=Un fichier nommé "%s" existe déjà dans ce dépôt.
editor.commit_id_not_matching=L’ID de la révision ne correspond pas à l’ID lorsque vous avez commencé à éditer. Faites une révision dans une branche de correctif puis fusionnez.
@@ -1383,8 +1380,6 @@ editor.push_out_of_date=Cet envoi semble être obsolète.
editor.commit_empty_file_header=Réviser un fichier vide
editor.commit_empty_file_text=Le fichier que vous allez réviser est vide. Continuer ?
editor.no_changes_to_show=Il n’y a aucune modification à afficher.
-editor.fail_to_update_file=Impossible de mettre à jour/créer le fichier "%s".
-editor.fail_to_update_file_summary=Message d'erreur :
editor.push_rejected_no_message=La modification a été rejetée par le serveur sans message. Veuillez vérifier les Git Hooks.
editor.push_rejected=La modification a été rejetée par le serveur. Veuillez vérifier vos Git Hooks.
editor.push_rejected_summary=Message de rejet complet :
@@ -1654,6 +1649,7 @@ issues.save=Enregistrer
issues.label_title=Nom du label
issues.label_description=Description du label
issues.label_color=Couleur du label
+issues.label_color_invalid=Couleur invalide
issues.label_exclusive=Exclusif
issues.label_archive=Archivé
issues.label_archived_filter=Afficher les labels archivés
@@ -2830,15 +2826,13 @@ settings.visibility.private_shortname=Privé
settings.update_settings=Appliquer les paramètres
settings.update_setting_success=Les paramètres de l'organisation ont été mis à jour.
-settings.change_orgname_prompt=Remarque : Changer le nom de l'organisation changera également l'URL de votre organisation et libèrera l'ancien nom.
-settings.change_orgname_redirect_prompt=L'ancien nom d'utilisateur redirigera jusqu'à ce qu'il soit réclamé.
+
+
settings.update_avatar_success=L'avatar de l'organisation a été mis à jour.
settings.delete=Supprimer l'organisation
settings.delete_account=Supprimer cette organisation
settings.delete_prompt=Cette organisation sera supprimée définitivement. Cette action est <strong>IRRÉVERSIBLE</strong> !
settings.confirm_delete_account=Confirmer la suppression
-settings.delete_org_title=Supprimer l'organisation
-settings.delete_org_desc=Cette organisation sera supprimée définitivement. Voulez-vous continuer ?
settings.hooks_desc=Vous pouvez ajouter des webhooks qui seront activés pour <strong>tous les dépôts</strong> de cette organisation.
settings.labels_desc=Ajoute des labels qui peuvent être utilisés sur les tickets pour <strong>tous les dépôts</strong> de cette organisation.
diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini
index 64f893f03c..35a2513351 100644
--- a/options/locale/locale_ga-IE.ini
+++ b/options/locale/locale_ga-IE.ini
@@ -1354,7 +1354,6 @@ editor.update=Nuashonraigh %s
editor.delete=Scrios %s
editor.patch=Cuir paiste i bhfeidh
editor.patching=Paisteáil:
-editor.fail_to_apply_patch=Ní féidir paiste "%s" a chur i bhfeidhm
editor.new_patch=Paiste Nua
editor.commit_message_desc=Cuir cur síos leathnaithe roghnach leis…
editor.signoff_desc=Cuir leantóir sínithe ag an gcoiteoir ag deireadh na teachtaireachta logála tiomanta.
@@ -1374,8 +1373,6 @@ editor.branch_already_exists=Tá brainse "%s" ann cheana féin sa stóras seo.
editor.directory_is_a_file=Úsáidtear ainm eolaire "%s" cheana féin mar ainm comhaid sa stóras seo.
editor.file_is_a_symlink=Is nasc siombalach é "%s". Ní féidir naisc shiombalacha a chur in eagar san eagarthóir gréasáin
editor.filename_is_a_directory=Úsáidtear ainm comhaid "%s" cheana féin mar ainm eolaire sa stóras seo.
-editor.file_editing_no_longer_exists=Níl an comhad atá á chur in eagar, "%s", ann sa stóras seo a thuilleadh.
-editor.file_deleting_no_longer_exists=Níl an comhad atá á scriosadh, "%s", ann sa stóras seo a thuilleadh.
editor.file_changed_while_editing=Tá athrú tagtha ar ábhar an chomhad ó thosaigh tú ag eagarthóireacht <a target="_blank" rel="noopener noreferrer" href="%s">Cliceáil anseo</a> chun iad a fheiceáil nó Athru <strong>ithe a Tiomantas arís</strong> chun iad a fhorscríobh.
editor.file_already_exists=Tá comhad darb ainm "%s" ann cheana féin sa stóras seo.
editor.commit_id_not_matching=Ní mheaitseálann an ID Tiomanta leis an ID nuair a thosaigh tú ag eagarthóireacht. Tiomanta isteach i mbrainse paiste agus ansin cumaisc.
@@ -1383,8 +1380,6 @@ editor.push_out_of_date=Is cosúil go bhfuil an brú as dáta.
editor.commit_empty_file_header=Tiomantas comhad folamh
editor.commit_empty_file_text=Tá an comhad atá tú ar tí tiomantas folamh. Ar aghaidh?
editor.no_changes_to_show=Níl aon athruithe le taispeáint.
-editor.fail_to_update_file=Theip ar nuashonrú/cruthú comhad "%s".
-editor.fail_to_update_file_summary=Teachtaireacht Earráide:
editor.push_rejected_no_message=Dhiúltaigh an freastalaí an t-athrú gan teachtaireacht. Seiceáil Git Hooks le do thoil.
editor.push_rejected=Dhiúltaigh an freastalaí an t-athrú. Seiceáil Git Hooks le do thoil.
editor.push_rejected_summary=Teachtaireacht Diúltaithe Iomlán:
@@ -2403,6 +2398,8 @@ settings.event_pull_request_review_request_desc=Tarraing athbhreithniú iarratai
settings.event_pull_request_approvals=Ceaduithe Iarratais Tarraing
settings.event_pull_request_merge=Cumaisc Iarratas Tarraing
settings.event_header_workflow=Imeachtaí Sreabhadh Oibre
+settings.event_workflow_run=Rith Sreabhadh Oibre
+settings.event_workflow_run_desc=Tá rith Sreabhadh Oibre Gníomhartha Gitea sa scuaine, ag fanacht, ar siúl, nó críochnaithe.
settings.event_workflow_job=Poist Sreabhadh Oibre
settings.event_workflow_job_desc=Gitea Actions Sreabhadh oibre post ciúáilte, ag fanacht, ar siúl, nó críochnaithe.
settings.event_package=Pacáiste
@@ -2831,15 +2828,13 @@ settings.visibility.private_shortname=Príobháideach
settings.update_settings=Nuashonrú Socruithe
settings.update_setting_success=Nuashonraíodh socruithe eagraíochta.
-settings.change_orgname_prompt=Nóta: Athróidh ainm na heagraíochta ag athrú URL d'eagraíochta agus saorfar an sean-ainm.
-settings.change_orgname_redirect_prompt=Déanfaidh an sean-ainm a atreorú go dtí go n-éilítear é.
+
+
settings.update_avatar_success=Nuashonraíodh avatar na heagraíochta.
settings.delete=Scrios Eagraíocht
settings.delete_account=Scrios an Eagraíocht seo
settings.delete_prompt=Bainfear an eagraíocht go buan. <strong>Nà FÉIDIR</strong> é seo a chealú!
settings.confirm_delete_account=Deimhnigh scriosadh
-settings.delete_org_title=Scrios Eagraíocht
-settings.delete_org_desc=Scriosfar an eagraíocht seo go buan. Lean ar aghaidh?
settings.hooks_desc=Cuir crúcaí gréasán in leis a spreagfar do <strong>gach stóras</strong> faoin eagraíocht seo.
settings.labels_desc=Cuir lipéid leis ar féidir iad a úsáid ar shaincheisteanna do <strong>gach stóras</strong> faoin eagraíocht seo.
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index ebc6d5c801..ae5c64123d 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -1174,13 +1174,13 @@ settings.visibility.private_shortname=Privát
settings.update_settings=Beállítások frissítése
settings.update_setting_success=A szervezet beállításai frissültek.
+
+
settings.update_avatar_success=A szervezet avatarja frissítve.
settings.delete=Szervezet törlése
settings.delete_account=A szervezet törlése
settings.delete_prompt=A szervezet véglegesen el lesz távolítva. <strong>NEM</strong> vonható vissza!
settings.confirm_delete_account=Törlés megerősítése
-settings.delete_org_title=Szervezet törlése
-settings.delete_org_desc=Ez a szervezet véglegesen törölve lesz. Folytatható?
settings.hooks_desc=Webhook hozzáadása a szervezet <strong>összes tárolójához</strong>.
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index 54b0499d96..a37c8c437c 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -1055,10 +1055,11 @@ settings.visibility.private_shortname=Pribadi
settings.update_settings=Perbarui Setelan
settings.update_setting_success=Pengaturan organisasi telah diperbarui.
+
+
settings.delete=Menghapus Organisasi
settings.delete_account=Menghapus Organisasi Ini
settings.confirm_delete_account=Konfirmasi Penghapusan
-settings.delete_org_title=Menghapus Organisasi
settings.hooks_desc=Tambahkan webhooks yang akan dipicu untuk <strong>semua repositori</strong> di bawah organisasi ini.
diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini
index 42ecfabe22..665f16de83 100644
--- a/options/locale/locale_is-IS.ini
+++ b/options/locale/locale_is-IS.ini
@@ -689,7 +689,6 @@ editor.create_new_branch=Búðu til <strong>nýja grein</strong> og sameiningarb
editor.create_new_branch_np=Búðu til <strong>nýja grein</strong> fyrir þetta framlag.
editor.new_branch_name_desc=Heiti nýjar greinar…
editor.cancel=Hætta við
-editor.fail_to_update_file_summary=Villuskilaboð:
commits.commits=Framlög
commits.author=Höfundur
@@ -1118,6 +1117,8 @@ settings.visibility.private_shortname=Einka
settings.update_settings=Uppfæra Stillingar
+
+
members.private=Faldir
members.owner=Eigandi
members.member=Meðlimur
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index 9cc257029b..248af39df1 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -1014,7 +1014,6 @@ editor.file_changed_while_editing=I contenuti di questo file hanno subito dei ca
editor.commit_empty_file_header=Commit di un file vuoto
editor.commit_empty_file_text=Il file che stai per effettuare il commit è vuoto. Procedere?
editor.no_changes_to_show=Non ci sono cambiamenti da mostrare.
-editor.fail_to_update_file_summary=Messaggio d'errore:
editor.push_rejected_no_message=La modifica è stata rifiutata dal server senza un messaggio. Controlla Git Hooks.
editor.push_rejected=La modifica è stata rifiutata dal server. Controlla Git Hooks.
editor.push_rejected_summary=Messaggio Di Rifiuto Completo:
@@ -2078,14 +2077,13 @@ settings.visibility.private_shortname=Privato
settings.update_settings=Aggiorna Impostazioni
settings.update_setting_success=Le impostazioni dell'organizzazione sono state aggiornate.
-settings.change_orgname_redirect_prompt=Il vecchio nome reindirizzerà fino a quando non sarà richiesto.
+
+
settings.update_avatar_success=L'avatar dell'organizzazione è stato aggiornato.
settings.delete=Elimina organizzazione
settings.delete_account=Elimina questa organizzazione
settings.delete_prompt=L'organizzazione verrà rimossa definitivamente. Questa operazione <strong>NON PUÒ</strong> essere annullata!
settings.confirm_delete_account=Conferma Eliminazione
-settings.delete_org_title=Elimina organizzazione
-settings.delete_org_desc=Questa organizzazione verrà eliminata definitivamente. Continuare?
settings.hooks_desc=Aggiungi i webhooks che verranno attivati per <strong>tutti i repository</strong> sotto questa organizzazione.
settings.labels_desc=Aggiungi i webhooks che verranno attivati per <strong>tutti i repository</strong> sotto questa organizzazione.
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 2934384052..d9f9e70826 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -1352,7 +1352,6 @@ editor.update=%s ã‚’æ›´æ–°
editor.delete=%s を削除
editor.patch=パッãƒã®é©ç”¨
editor.patching=パッãƒ:
-editor.fail_to_apply_patch=`パッãƒã‚’é©ç”¨ã§ãã¾ã›ã‚“ "%s"`
editor.new_patch=æ–°ã—ã„パッãƒ
editor.commit_message_desc=詳細ãªèª¬æ˜Žã‚’追加…
editor.signoff_desc=ã‚³ãƒŸãƒƒãƒˆãƒ­ã‚°ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®æœ€å¾Œã«ã‚³ãƒŸãƒƒã‚¿ãƒ¼ã® Signed-off-by 行を追加
@@ -1372,8 +1371,6 @@ editor.branch_already_exists=ブランム"%s" ã¯ã€ã“ã®ãƒªãƒã‚¸ãƒˆãƒªã«æ—¢
editor.directory_is_a_file=ディレクトリå "%s" ã¯ã™ã§ã«ãƒªãƒã‚¸ãƒˆãƒªå†…ã®ãƒ•ァイルã§ä½¿ç”¨ã•れã¦ã„ã¾ã™ã€‚
editor.file_is_a_symlink=`"%s" ã¯ã‚·ãƒ³ãƒœãƒªãƒƒã‚¯ãƒªãƒ³ã‚¯ã§ã™ã€‚ シンボリックリンクã¯Webエディターã§ç·¨é›†ã§ãã¾ã›ã‚“。`
editor.filename_is_a_directory=ファイルå "%s" ã¯ã€ã“ã®ãƒªãƒã‚¸ãƒˆãƒªä¸Šã§ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªåã¨ã—ã¦ã™ã§ã«ä½¿ç”¨ã•れã¦ã„ã¾ã™ã€‚
-editor.file_editing_no_longer_exists=編集中ã®ãƒ•ァイル "%s" ãŒã€ã‚‚ã†ãƒªãƒã‚¸ãƒˆãƒªå†…ã«ã‚りã¾ã›ã‚“。
-editor.file_deleting_no_longer_exists=削除ã—よã†ã¨ã—ãŸãƒ•ァイル "%s" ãŒã€ã™ã§ã«ãƒªãƒã‚¸ãƒˆãƒªå†…ã«ã‚りã¾ã›ã‚“。
editor.file_changed_while_editing=ã‚ãªãŸãŒç·¨é›†ã‚’é–‹å§‹ã—ãŸã‚ã¨ã€ãƒ•ァイルã®å†…容ãŒå¤‰æ›´ã•れã¾ã—ãŸã€‚ <a target="_blank" rel="noopener noreferrer" href="%s">ã“ã“をクリック</a>ã—ã¦ä½•ãŒå¤‰æ›´ã•れãŸã‹ç¢ºèªã™ã‚‹ã‹ã€<strong>ã‚‚ã†ä¸€åº¦"変更をコミット"をクリック</strong>ã—ã¦ä¸Šæ›¸ãã—ã¾ã™ã€‚
editor.file_already_exists=ファイル "%s" ã¯ã€ã“ã®ãƒªãƒã‚¸ãƒˆãƒªã«æ—¢ã«å­˜åœ¨ã—ã¾ã™ã€‚
editor.commit_id_not_matching=コミットIDãŒç·¨é›†ã‚’é–‹å§‹ã—ãŸã¨ãã®IDã¨ä¸€è‡´ã—ã¾ã›ã‚“。 パッãƒç”¨ã®ãƒ–ランãƒã«ã‚³ãƒŸãƒƒãƒˆã—ãŸã‚ã¨ãƒžãƒ¼ã‚¸ã—ã¦ãã ã•ã„。
@@ -1381,8 +1378,6 @@ editor.push_out_of_date=ã“ã®ãƒ—ãƒƒã‚·ãƒ¥ã¯æœ€æ–°ã§ã¯ãªã„よã†ã§ã™ã€‚
editor.commit_empty_file_header=空ファイルã®ã‚³ãƒŸãƒƒãƒˆ
editor.commit_empty_file_text=コミットã—よã†ã¨ã—ã¦ã„るファイルã¯ç©ºã§ã™ã€‚ 続行ã—ã¾ã™ã‹ï¼Ÿ
editor.no_changes_to_show=表示ã™ã‚‹å¤‰æ›´ç®‡æ‰€ã¯ã‚りã¾ã›ã‚“。
-editor.fail_to_update_file=ファイル "%s" を作æˆã¾ãŸã¯å¤‰æ›´ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚
-editor.fail_to_update_file_summary=エラーメッセージ:
editor.push_rejected_no_message=サーãƒãƒ¼ãŒãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’出ã•ãšã«å¤‰æ›´ã‚’æ‹’å¦ã—ã¾ã—ãŸã€‚ Git フックを確èªã—ã¦ãã ã•ã„。
editor.push_rejected=サーãƒãƒ¼ãŒå¤‰æ›´ã‚’æ‹’å¦ã—ã¾ã—ãŸã€‚ Gitフックを確èªã—ã¦ãã ã•ã„。
editor.push_rejected_summary=æ‹’å¦ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸å…¨ä½“:
@@ -2827,15 +2822,13 @@ settings.visibility.private_shortname=プライベート
settings.update_settings=è¨­å®šã®æ›´æ–°
settings.update_setting_success=組織ã®è¨­å®šã‚’æ›´æ–°ã—ã¾ã—ãŸã€‚
-settings.change_orgname_prompt=注æ„: 組織åを変更ã™ã‚‹ã¨çµ„ç¹”ã®URLも変更ã•れã€å¤ã„åå‰ã¯è§£æ”¾ã•れã¾ã™ã€‚
-settings.change_orgname_redirect_prompt=å¤ã„åå‰ã¯ã€å†ä½¿ç”¨ã•れã¦ã„ãªã„é™ã‚Šãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã—ã¾ã™ã€‚
+
+
settings.update_avatar_success=組織ã®ã‚¢ãƒã‚¿ãƒ¼ã‚’æ›´æ–°ã—ã¾ã—ãŸã€‚
settings.delete=組織を削除
settings.delete_account=ã“ã®çµ„織を削除
settings.delete_prompt=çµ„ç¹”ã¯æ’ä¹…çš„ã«å‰Šé™¤ã•れã¾ã™ã€‚ å…ƒã«æˆ»ã™ã“ã¨ã¯<strong>ã§ãã¾ã›ã‚“</strong>ï¼
settings.confirm_delete_account=削除を確èª
-settings.delete_org_title=組織ã®å‰Šé™¤
-settings.delete_org_desc=組織をæ’ä¹…çš„ã«å‰Šé™¤ã—ã¾ã™ã€‚ 続行ã—ã¾ã™ã‹ï¼Ÿ
settings.hooks_desc=ã“ã®çµ„ç¹”ã®<strong>ã™ã¹ã¦ã®ãƒªãƒã‚¸ãƒˆãƒª</strong>ã§ãƒˆãƒªã‚¬ãƒ¼ã•れるWebhookを追加ã—ã¾ã™ã€‚
settings.labels_desc=ã“ã®çµ„ç¹”ã®<strong>ã™ã¹ã¦ã®ãƒªãƒã‚¸ãƒˆãƒª</strong>ã§ä½¿ç”¨å¯èƒ½ãªã‚¤ã‚·ãƒ¥ãƒ¼ãƒ©ãƒ™ãƒ«ã‚’追加ã—ã¾ã™ã€‚
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index 22bf3e1641..61f1c2535e 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -1151,12 +1151,12 @@ settings.visibility.private_shortname=비공개
settings.update_settings=설정 ì—…ë°ì´íЏ
settings.update_setting_success=ì¡°ì§ ì„¤ì •ì´ ë³€ê²½ë˜ì—ˆìŠµë‹ˆë‹¤.
+
+
settings.update_avatar_success=ì¡°ì§ì˜ 아바타가 갱신ë˜ì—ˆìŠµë‹ˆë‹¤.
settings.delete=ì¡°ì§ ì‚­ì œ
settings.delete_account=ì´ ì¡°ì§ì„ 삭제합니다.
settings.confirm_delete_account=ì‚­ì œ 승ì¸
-settings.delete_org_title=ì¡°ì§ ì‚­ì œ
-settings.delete_org_desc=ì´ ì¡°ì§ì´ ì˜êµ¬ížˆ ì‚­ì œë©ë‹ˆë‹¤. ê³„ì† í•˜ì‹œê² ìŠµë‹ˆê¹Œ?
members.membership_visibility=íšŒì› í‘œì‹œ:
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index a746f8738c..8e2671f43c 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -1196,7 +1196,6 @@ editor.update=Atjaunot %s
editor.delete=Dzēst %s
editor.patch=Pielietot ielÄpu
editor.patching=Pielieto ielÄpu:
-editor.fail_to_apply_patch=`NeizdevÄs pielietot ielÄpu "%s"`
editor.new_patch=Jauns ielÄps
editor.commit_message_desc=Pievienot neobligÄtu paplaÅ¡inÄtu aprakstu…
editor.signoff_desc=Pievienot revÄ«zijas žurnÄla ziņojuma beigÄs Signed-off-by ar revÄ«zijas autoru.
@@ -1214,15 +1213,11 @@ editor.branch_already_exists=Atzars "%s" Å¡ajÄ repozitorijÄ jau eksistÄ“.
editor.directory_is_a_file=Direktorijas nosaukums "%s" vecÄka ceÄ¼Ä ir fails nevis direktorija Å¡ajÄ repozitorijÄ.
editor.file_is_a_symlink=Fails "%s" ir norÄde, kuru nav iespÄ“jams labot no tÄ«mekļa redaktora
editor.filename_is_a_directory=Faila nosaukums "%s" sakrÄ«t ar direktorijas nosaukumu Å¡ajÄ repozitorijÄ.
-editor.file_editing_no_longer_exists=Fails "%s", ko labojat, vairs neeksistÄ“ Å¡ajÄ repozitorijÄ.
-editor.file_deleting_no_longer_exists=Fails "%s", ko dzēšat, vairs neeksistÄ“ Å¡ajÄ repozitorijÄ.
editor.file_changed_while_editing=Faila saturs ir mainÄ«jies kopÅ¡ sÄkÄt to labot. Noklikšķiniet <a target="_blank" rel="noopener noreferrer" href="%s">Å¡eit</a>, lai apskatÄ«tu, vai <strong>NosÅ«tiet izmaiņas atkÄrtoti</strong>, lai pÄrrakstÄ«tu.
editor.file_already_exists=Fails ar nosaukumu "%s" Å¡ajÄ repozitorijÄ jau eksistÄ“.
editor.commit_empty_file_header=Iesūtīt tukšu failu
editor.commit_empty_file_text=Fails, ko vÄ“laties iesÅ«tÄ«t, ir tukÅ¡s. Vai turpinÄt?
editor.no_changes_to_show=Nav izmaiņu, ko rÄdÄ«t.
-editor.fail_to_update_file=NeizdevÄs atjaunot/izveidot failu "%s".
-editor.fail_to_update_file_summary=Kļūdas ziņojums:
editor.push_rejected_no_message=Izmaiņu iesÅ«tīšana tika noraidÄ«ta, bet serveris neatgrieza paziņojumu. PÄrbaudiet git ÄÄ·us Å¡im repozitorijam.
editor.push_rejected=Serveris noraidÄ«ja Å¡o izmaiņu. PÄrbaudiet git ÄÄ·us.
editor.push_rejected_summary=Pilns noraidīšanas ziņojums:
@@ -2509,15 +2504,13 @@ settings.visibility.private_shortname=PrivÄta
settings.update_settings=Mainīt iestatījumus
settings.update_setting_success=OrganizÄcijas iestatÄ«jumi tika saglabÄti.
-settings.change_orgname_prompt=PiezÄ«me: organizÄcijas nosaukuma maiņa izmainÄ«s arÄ« organizÄcijas URL un atbrÄ«vos veco nosaukumu.
-settings.change_orgname_redirect_prompt=Vecais vÄrds pÄrsÅ«tÄ«s uz jauno, kamÄ“r vien tas nebÅ«s izmantots.
+
+
settings.update_avatar_success=OrganizÄcijas attÄ“ls tika saglabÄts.
settings.delete=DzÄ“st organizÄciju
settings.delete_account=DzÄ“st Å¡o organizÄciju
settings.delete_prompt=Å Ä« darbÄ«ba pilnÄ«bÄ dzÄ“sÄ«s Å¡o organizÄciju, kÄ arÄ« tÄ ir <strong>NEATGRIEZENISKA</strong>!
settings.confirm_delete_account=ApstiprinÄt dzēšanu
-settings.delete_org_title=DzÄ“st organizÄciju
-settings.delete_org_desc=OrganizÄcija tiks dzÄ“sta neatgriezeniski. Vai turpinÄt?
settings.labels_desc=Pievienojiet iezÄ«mes, kas var tikt izmantotas <strong>visos</strong> šīs organizÄcijas repozitorijos.
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index b6887ee9e0..12d79a1979 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -1012,7 +1012,6 @@ editor.file_changed_while_editing=De bestandsinhoud is veranderd sinds je bent b
editor.commit_empty_file_header=Commit een leeg bestand
editor.commit_empty_file_text=Het bestand dat u wilt committen is leeg. Doorgaan?
editor.no_changes_to_show=Er zijn geen wijzigingen om weer te geven.
-editor.fail_to_update_file_summary=Foutmelding:
editor.push_rejected_no_message=De wijziging is afgewezen door de server zonder bericht. Controleer de Git Hooks alsjeblieft.
editor.push_rejected=De wijziging is afgewezen door de server. Controleer Controleer de Git Hooks alsjeblieft.
editor.push_rejected_summary=Volledig afwijzingsbericht:
@@ -1989,13 +1988,13 @@ settings.visibility.private=Privé (alleen zichtbaar voor organisatieleden)
settings.visibility.private_shortname=Privé
settings.update_settings=Instellingen bijwerken
+
+
settings.update_avatar_success=De avatar van de organisatie is aangepast.
settings.delete=Verwijder organisatie
settings.delete_account=Verwijder deze organisatie
settings.delete_prompt=Deze organisatie zal permanent worden verwijderd. U kunt dit <strong>NIET</strong> ongedaan maken!
settings.confirm_delete_account=Bevestig verwijdering
-settings.delete_org_title=Verwijder organisatie
-settings.delete_org_desc=Deze organisatie zal permanent verwijderd worden. Doorgaan?
settings.hooks_desc=Een webhook toevoegen die door <strong>alle repositories</strong> in deze organisatie getriggerd kan worden.
settings.labels_desc=Voeg labels toe die kunnen worden gebruikt bij problemen voor <strong>alle repositories</strong> in deze organisatie.
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index 42a33f9ce4..9ae87d7f7b 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -942,7 +942,6 @@ editor.file_changed_while_editing=Zawartość pliku zmieniła się, odkąd rozpo
editor.commit_empty_file_header=Commituj pusty plik
editor.commit_empty_file_text=Plik, który zamierzasz commitować, jest pusty. Kontynuować?
editor.no_changes_to_show=Brak zmian do pokazania.
-editor.fail_to_update_file_summary=Komunikat błędu:
editor.push_rejected_summary=Pełny komunikat odrzucenia:
editor.add_subdir=Dodaj katalog…
editor.no_commit_to_branch=Zatwierdzanie bezpośrednio do tej gałęzi nie jest możliwe, ponieważ:
@@ -1862,14 +1861,13 @@ settings.visibility.private_shortname=Prywatny
settings.update_settings=Aktualizuj ustawienia
settings.update_setting_success=Ustawienia organizacji zostały zaktualizowane.
-settings.change_orgname_redirect_prompt=Stara nazwa będzie przekierowywała dopóki ktoś jej nie zajmie.
+
+
settings.update_avatar_success=Awatar organizacji został zaktualizowany.
settings.delete=Usuń organizację
settings.delete_account=Usuń tą organizację
settings.delete_prompt=Organizacja zostanie trwale usunięta. Tej akcji <strong>NIE MOŻNA</strong> cofnąć!
settings.confirm_delete_account=Potwierdź usunięcie
-settings.delete_org_title=Usuń organizację
-settings.delete_org_desc=Ta organizacja zostanie trwale usunięta. Kontynuować?
settings.hooks_desc=Dodaj webhooki, uruchamiane dla <strong>wszystkich repozytoriów</strong> w tej organizacji.
settings.labels_desc=Dodaj etykiety, które mogą być używane w zgłoszeniach dla <strong>wszystkich repozytoriów</strong> w tej organizacji.
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index 8ee675e6e0..491190d987 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -1191,7 +1191,6 @@ editor.update=Atualizar %s
editor.delete=Excluir %s
editor.patch=Aplicar Correção
editor.patching=Corrigindo:
-editor.fail_to_apply_patch=`Não foi possível aplicar a correção "%s"`
editor.new_patch=Nova correção
editor.commit_message_desc=Adicione uma descrição detalhada (opcional)...
editor.signoff_desc=Adicione um assinado-por-committer no final do log do commit.
@@ -1209,15 +1208,11 @@ editor.branch_already_exists=Branch "%s" já existe neste repositório.
editor.directory_is_a_file=O nome do diretório "%s" já é usado como um nome de arquivo neste repositório.
editor.file_is_a_symlink=`"%s" é um link simbólico. Links simbólicos não podem ser editados no editor da web`
editor.filename_is_a_directory=O nome do arquivo "%s" já é usado como um nome de diretório neste repositório.
-editor.file_editing_no_longer_exists=O arquivo que está sendo editado, "%s", não existe mais neste repositório.
-editor.file_deleting_no_longer_exists=O arquivo a ser excluído, "%s", não existe mais neste repositório.
editor.file_changed_while_editing=O conteúdo do arquivo mudou desde que você começou a editar. <a target="_blank" rel="noopener noreferrer" href="%s">Clique aqui</a> para ver o que foi editado ou <strong>clique em Aplicar commit das alterações novamemente</strong> para sobreescrever estas alterações.
editor.file_already_exists=Um arquivo com nome "%s" já existe neste repositório.
editor.commit_empty_file_header=Fazer commit de um arquivo vazio
editor.commit_empty_file_text=O arquivo que você está prestes fazer commit está vazio. Continuar?
editor.no_changes_to_show=Nenhuma alteração a mostrar.
-editor.fail_to_update_file=Falha ao atualizar/criar arquivo "%s".
-editor.fail_to_update_file_summary=Mensagem de erro:
editor.push_rejected_no_message=A alteração foi rejeitada pelo servidor sem uma mensagem. Por favor, verifique os Hooks Git.
editor.push_rejected=A alteração foi rejeitada pelo servidor. Por favor, verifique os Hooks Git.
editor.push_rejected_summary=Mensagem completa de rejeição:
@@ -2468,14 +2463,13 @@ settings.visibility.private_shortname=Privado
settings.update_settings=Atualizar Configurações
settings.update_setting_success=Configurações da organização foram atualizadas.
-settings.change_orgname_redirect_prompt=O nome antigo irá redirecionar até que seja reivindicado.
+
+
settings.update_avatar_success=O avatar da organização foi atualizado.
settings.delete=Excluir organização
settings.delete_account=Excluir esta organização
settings.delete_prompt=A organização será excluída permanentemente. Isto <strong>NÃO PODERÃ</strong> ser desfeito!
settings.confirm_delete_account=Confirmar exclusão
-settings.delete_org_title=Excluir organização
-settings.delete_org_desc=Essa organização será excluída permanentemente. Continuar?
settings.hooks_desc=Adicionar Webhooks que serão acionados para <strong>todos os repositórios</strong> desta organização.
settings.labels_desc=Adicionar rótulos que possam ser usadas em issues para <strong>todos os repositórios</strong> desta organização.
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index a62a24ff1d..18da7f5587 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -1353,7 +1353,6 @@ editor.update=Modificar %s
editor.delete=Eliminar %s
editor.patch=Aplicar remendo (patch)
editor.patching=Remendando (patching):
-editor.fail_to_apply_patch=`Não foi possível aplicar o remendo (patch) "%s"`
editor.new_patch=Novo remendo (patch)
editor.commit_message_desc=Adicionar uma descrição alargada opcional…
editor.signoff_desc=Adicionar "Assinado-por" seguido do autor do cometimento no fim da mensagem do registo de cometimentos.
@@ -1373,8 +1372,6 @@ editor.branch_already_exists=O ramo "%s" já existe neste repositório.
editor.directory_is_a_file=O nome da pasta "%s" já é usado como um nome de ficheiro neste repositório.
editor.file_is_a_symlink=`"%s" é uma ligação simbólica. Ligações simbólicas não podem ser editadas no editor web`
editor.filename_is_a_directory=O nome de ficheiro "%s" já está a ser usado como um nome de pasta neste repositório.
-editor.file_editing_no_longer_exists=O ficheiro que está a ser editado, "%s", já não existe neste repositório.
-editor.file_deleting_no_longer_exists=O ficheiro que está a ser eliminado, "%s", já não existe neste repositório.
editor.file_changed_while_editing=O conteúdo do ficheiro mudou desde que começou a editar. <a target="_blank" rel="noopener noreferrer" href="%s">Clique aqui</a> para ver as modificações ou clique em <strong>Cometer novamente</strong> para escrever por cima.
editor.file_already_exists=Já existe um ficheiro com o nome "%s" neste repositório.
editor.commit_id_not_matching=O ID do cometimento não corresponde ao ID de quando começou a editar. Faça o cometimento para um ramo de remendo (patch) e depois faça a integração.
@@ -1382,8 +1379,6 @@ editor.push_out_of_date=O envio parece estar obsoleto.
editor.commit_empty_file_header=Cometer um ficheiro vazio
editor.commit_empty_file_text=O ficheiro que está prestes a cometer está vazio. Quer continuar?
editor.no_changes_to_show=Não existem modificações para mostrar.
-editor.fail_to_update_file=Falhou ao modificar/criar o ficheiro "%s".
-editor.fail_to_update_file_summary=Mensagem de erro:
editor.push_rejected_no_message=A modificação foi rejeitada pelo servidor sem qualquer mensagem. Verifique os Automatismos do Git.
editor.push_rejected=A modificação foi rejeitada pelo servidor. Verifique os Automatismos do Git.
editor.push_rejected_summary=Mensagem completa de rejeição:
@@ -2402,8 +2397,10 @@ settings.event_pull_request_review_request_desc=A revisão do pedido de integraÃ
settings.event_pull_request_approvals=Aprovações do pedido de integração
settings.event_pull_request_merge=Integração constante no pedido
settings.event_header_workflow=Eventos da sequência de trabalho
+settings.event_workflow_run=Execução da sequência de trabalho
+settings.event_workflow_run_desc=A execução da sequência de trabalho das operações do Gitea foi colocada em fila, está em espera, em andamento ou concluída.
settings.event_workflow_job=Trabalhos da sequência de trabalho
-settings.event_workflow_job_desc=O trabalho da sequência de trabalho das operações do Gitea foi colocado em fila, está em espera, em andamento ou concluída.
+settings.event_workflow_job_desc=O trabalho da sequência de trabalho das operações do Gitea foi colocado em fila, está em espera, em andamento ou concluído.
settings.event_package=Pacote
settings.event_package_desc=Pacote criado ou eliminado num repositório.
settings.branch_filter=Filtro de ramos
@@ -2830,15 +2827,13 @@ settings.visibility.private_shortname=Privado
settings.update_settings=Modificar configurações
settings.update_setting_success=As configurações da organização foram modificadas.
-settings.change_orgname_prompt=Nota: Mudar o nome da organização também irá mudar o URL da organização e libertar o nome antigo.
-settings.change_orgname_redirect_prompt=O nome antigo, enquanto não for reivindicado, irá reencaminhar para o novo.
+
+
settings.update_avatar_success=O avatar da organização foi modificado.
settings.delete=Eliminar organização
settings.delete_account=Eliminar esta organização
settings.delete_prompt=A organização será removida permanentemente. Essa operação <strong>NÃO PODERÃ</strong> ser revertida!
settings.confirm_delete_account=Confirme a eliminação
-settings.delete_org_title=Eliminar organização
-settings.delete_org_desc=Esta organização será eliminada permanentemente. Quer continuar?
settings.hooks_desc=Adicionar automatismos web que serão despoletados para <strong>todos os repositórios</strong> desta organização.
settings.labels_desc=Adicionar rótulos que possam ser usados em questões para <strong>todos os repositórios</strong> desta organização.
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index c65d08a4cf..81e9ff5dbc 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -1169,7 +1169,6 @@ editor.update=Обновить %s
editor.delete=Удалить %s
editor.patch=Применить патч
editor.patching=ИÑправление:
-editor.fail_to_apply_patch=Ðевозможно применить патч «%s»
editor.new_patch=Ðовый патч
editor.commit_message_desc=Добавьте необÑзательное раÑширенное опиÑание…
editor.signoff_desc=Добавить трейлер Signed-off-by Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¾Ð¼ коммита в конце ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ ÐºÐ¾Ð¼Ð¼Ð¸Ñ‚Ð°.
@@ -1187,15 +1186,11 @@ editor.branch_already_exists=Ветка «%s» уже ÑущеÑтвует в Ñ
editor.directory_is_a_file=Ð˜Ð¼Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ð° «%s» уже иÑпользуетÑÑ Ð² качеÑтве имени файла в Ñтом репозитории.
editor.file_is_a_symlink=`«%s» ÑвлÑетÑÑ ÑимволичеÑкой ÑÑылкой. СимволичеÑкие ÑÑылки невозможно отредактировать в веб-редакторе`
editor.filename_is_a_directory=Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð° «%s» уже иÑпользуетÑÑ Ð² качеÑтве каталога в Ñтом репозитории.
-editor.file_editing_no_longer_exists=Редактируемый файл «%s» больше не ÑущеÑтвует в Ñтом репозитории.
-editor.file_deleting_no_longer_exists=УдалÑемый файл «%s» больше не ÑущеÑтвует в Ñтом репозитории.
editor.file_changed_while_editing=Содержимое файла изменилоÑÑŒ Ñ Ð¼Ð¾Ð¼ÐµÐ½Ñ‚Ð° начала редактированиÑ. <a target="_blank" rel="noopener noreferrer" href="%s">Ðажмите здеÑÑŒ</a>, чтобы увидеть, что было изменено, или <strong>ЗафикÑировать Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñнова</strong>, чтобы заменить их.
editor.file_already_exists=Файл Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ «%s» уже ÑущеÑтвует в репозитории.
editor.commit_empty_file_header=Закоммитить пуÑтой файл
editor.commit_empty_file_text=Файл, который вы ÑобираетеÑÑŒ зафикÑировать, пуÑÑ‚. Продолжить?
editor.no_changes_to_show=Ðет изменений.
-editor.fail_to_update_file=Ðе удалоÑÑŒ обновить/Ñоздать файл «%s».
-editor.fail_to_update_file_summary=Ошибка:
editor.push_rejected_no_message=Изменение отклонено Ñервером без ÑообщениÑ. ПожалуйÑта, проверьте хуки Git.
editor.push_rejected=Изменение отклонено Ñервером. ПожалуйÑта, проверьте хуки Git.
editor.push_rejected_summary=Полное Ñообщение об отклонении:
@@ -2455,15 +2450,13 @@ settings.visibility.private_shortname=Приватный
settings.update_settings=Обновить наÑтройки
settings.update_setting_success=ÐаÑтройки организации обновлены.
-settings.change_orgname_prompt=Обратите внимание: изменение Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ð¸ также изменит URL вашей организации и оÑвободит Ñтарое имÑ.
-settings.change_orgname_redirect_prompt=Старое Ð¸Ð¼Ñ Ð±ÑƒÐ´ÐµÑ‚ перенаправлено до тех пор, пока оно не будет введено.
+
+
settings.update_avatar_success=Ðватар организации обновлён.
settings.delete=Удалить организацию
settings.delete_account=Удалить Ñту организацию
settings.delete_prompt=Это дейÑтвие <strong>БЕЗВОЗВРÐТÐО</strong> удалит Ñту организацию навÑегда!
settings.confirm_delete_account=Подтвердить удаление
-settings.delete_org_title=Удалить организацию
-settings.delete_org_desc=Эта Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð±ÑƒÐ´ÐµÑ‚ безвозвратно удалена. Продолжить?
settings.hooks_desc=Добавьте веб-хуки, которые будет вызыватьÑÑ Ð´Ð»Ñ <strong>вÑех репозиториев</strong> под Ñтой организации.
settings.labels_desc=Добавьте метки, которые могут быть иÑпользованы в задачах Ð´Ð»Ñ <strong>вÑех репозиториев</strong> Ñтой организации.
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index a209187aff..cc0f83cae4 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -917,7 +917,6 @@ editor.file_changed_while_editing=ඔබ සංස්කරණය කිරීà¶
editor.commit_empty_file_header=හිස් ගොනුවක් à¶šà·à¶´ කරන්න
editor.commit_empty_file_text=ඔබ à¶šà·à¶´ කිරීමට යන ගොනුව හිස් ය. ඉදිරියට?
editor.no_changes_to_show=පෙන්වීමට කිසිදු වෙනසක් à¶±à·à¶­.
-editor.fail_to_update_file_summary=දà·à·‚ පණිවිඩය:
editor.push_rejected_summary=පූර්ණ ප්රතික්ෂේප පණිවිඩය:
editor.add_subdir=ඩිරෙක්ටරියක් එක් කරන්න…
editor.no_commit_to_branch=à·à·à¶›à·à·€à¶§ කෙලින්ම à¶šà·à¶´à·€à·’ය නොහà·à¶šà·’ නිසà·:
@@ -1882,14 +1881,13 @@ settings.visibility.private_shortname=පෞද්ගලික
settings.update_settings=à·ƒà·à¶šà·ƒà·”ම් යà·à·€à¶­à·Šà¶šà·à¶½ කරන්න
settings.update_setting_success=සංවිධà·à¶±à¶ºà·š à·ƒà·à¶šà·ƒà·”ම් යà·à·€à¶­à·Šà¶šà·à¶½ à¶šà¶» ඇත.
-settings.change_orgname_redirect_prompt=à¶´à·à¶»à¶«à·’ නම ඉල්ල෠සිටින තුරු à¶±à·à·€à¶­ හරව෠යවයි.
+
+
settings.update_avatar_success=සංවිධà·à¶±à¶ºà·š අවතà·à¶»à¶º යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà¶» ඇත.
settings.delete=සංවිධà·à¶±à¶º මකන්න
settings.delete_account=මෙම සංවිධà·à¶±à¶º මකන්න
settings.delete_prompt=සංවිධà·à¶±à¶º ස්ථිරවම ඉවත් කරනු à¶½à·à¶¶à·š. මෙම <strong></strong> à¶…à·„à·à·ƒà·’ à¶šà·… නොහà·à¶š!
settings.confirm_delete_account=මකà·à¶¯à·à¶¸à·“ම තහවුරු කරන්න
-settings.delete_org_title=සංවිධà·à¶±à¶º මකන්න
-settings.delete_org_desc=මෙම සංවිධà·à¶±à¶º ස්ථිරවම මක෠දමනු ඇත. දිගටම?
settings.hooks_desc=මෙම සංවිධà·à¶±à¶º යටතේ <strong>සියලුම ගබඩà·à·€à¶±à·Š</strong> සඳහ෠මුලපුරනු ලබන වෙබ් කොකු à¶‘à¶šà¶­à·” කරන්න.
settings.labels_desc=මෙම සංවිධà·à¶±à¶º යටතේ <strong>සියලුම ගබඩà·à·€à¶½à¶¯à·“</strong> සඳහ෠ගà·à¶§à·…à·” සඳහ෠භà·à·€à·’à¶­à· à¶šà·… à·„à·à¶šà·’ ලේබල් à¶‘à¶šà¶­à·” කරන්න.
diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini
index e461075e53..af364fd69b 100644
--- a/options/locale/locale_sk-SK.ini
+++ b/options/locale/locale_sk-SK.ini
@@ -1219,6 +1219,8 @@ lower_repositories=repozitáre
settings.visibility.private=Súkromná ​​(viditeľné iba pre Älenov organizácie)
settings.visibility.private_shortname=Súkromný
+
+
settings.hooks_desc=Pridajte webhooky, ktoré sa spustia nad <strong>všetkými repozitármi</strong> v rámci tejto organizácie.
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index 04428aeab2..4522c2363d 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -777,7 +777,6 @@ editor.file_changed_while_editing=Filens innehåll har ändrats sedan du påbör
editor.commit_empty_file_header=Committa en tom fil
editor.commit_empty_file_text=Filen du vill committa är tom. Vill du fortsätta?
editor.no_changes_to_show=Det finns inga ändringar att visa.
-editor.fail_to_update_file_summary=Felmeddelande:
editor.add_subdir=Lägga till en katalog…
editor.no_commit_to_branch=Det gick inte att committa direkt till branchen för:
editor.user_no_push_to_branch=Användaren kan inte pusha till branchen
@@ -1524,13 +1523,13 @@ settings.visibility.private=Privat (synlig endast för organisationens medlemmar
settings.visibility.private_shortname=Privat
settings.update_settings=Uppdatera inställningar
+
+
settings.update_avatar_success=Organisationens avatar har uppdateras.
settings.delete=Tag bort organisation
settings.delete_account=Tag bort denna organisation
settings.delete_prompt=Organisationen kommer tas bort permanent, och det går <strong>INTE</strong> att ångra detta!
settings.confirm_delete_account=Bekräfta borttagning
-settings.delete_org_title=Ta bort organisation
-settings.delete_org_desc=Denna organisation kommer tas bort permanent. Vill du fortsätta?
settings.hooks_desc=Lägg till webbhook som triggas för <strong>alla utvecklingskataloger</strong> under denna organisationen.
settings.labels_desc=Lägg till etiketter som kan användas till ärenden för <strong>alla utvecklingskataloger</strong> under denna organisation.
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index 1f46369fe0..8d4c0f3f79 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -1345,7 +1345,6 @@ editor.update=%s Güncelle
editor.delete=%s Sil
editor.patch=Yama Uygula
editor.patching=Yamalanıyor:
-editor.fail_to_apply_patch=`"%s" yaması uygulanamıyor`
editor.new_patch=Yeni Yama
editor.commit_message_desc=İsteğe bağlı uzun bir açıklama ekleyin…
editor.signoff_desc=İşleme günlüğü mesajının sonuna işleyen tarafından imzalanan bir fragman ekleyin.
@@ -1365,8 +1364,6 @@ editor.branch_already_exists=Bu depoda "%s" dalı zaten var.
editor.directory_is_a_file=Dizin adı "%s" zaten bu depoda bir dosya adı olarak kullanılmaktadır.
editor.file_is_a_symlink=`"%s" sembolik bir bağlantıdır. Sembolik bağlantılar web düzenleyicisinde düzenlenemez`
editor.filename_is_a_directory=Dosya adı "%s" zaten bu depoda bir dizin adı olarak kullanılmaktadır.
-editor.file_editing_no_longer_exists=Düzenlenmekte olan "%s" dosyası artık bu depoda yer almıyor.
-editor.file_deleting_no_longer_exists=Silinen "%s" dosyası artık bu depoda yer almıyor.
editor.file_changed_while_editing=Düzenlemeye başladığınızdan beri dosya içeriği değişti. Görmek için <a target="_blank" rel="noopener noreferrer" href="%s">burayı tıklayın</a> veya üzerine yazmak için <strong>değişiklikleri yine de işleyin</strong>.
editor.file_already_exists=Bu depoda "%s" isimli bir dosya zaten var.
editor.commit_id_not_matching=İşleme ID'si, düzenlemeye başladığınız ID ile uyuşmuyor, bir yama dalına işleme yapın ve sonra birleştirin.
@@ -1374,8 +1371,6 @@ editor.push_out_of_date=İtme eskimiş.
editor.commit_empty_file_header=BoÅŸ bir dosya iÅŸle
editor.commit_empty_file_text=İşlemek üzere olduğunuz dosya boş. Devam edilsin mi?
editor.no_changes_to_show=Gösterilecek değişiklik yok.
-editor.fail_to_update_file=`"%s" dosyası güncellenemedi/oluşturulamadı.`
-editor.fail_to_update_file_summary=Hata Mesajı:
editor.push_rejected_no_message=Değişiklik, bir ileti olmadan sunucu tarafından reddedildi. Git Hooks'u kontrol edin.
editor.push_rejected=Değişiklik sunucu tarafından reddedildi. Lütfen Git Hooks'u kontrol edin.
editor.push_rejected_summary=Tam Red Mesajı:
@@ -2735,15 +2730,13 @@ settings.visibility.private_shortname=Özel
settings.update_settings=Ayarları Güncelle
settings.update_setting_success=Organizasyon ayarları güncellendi.
-settings.change_orgname_prompt=Not: Organizasyon adını değiştirmek organizasyonunuzun URL'sini de değiştirecek ve eski ismi serbest bıracaktır.
-settings.change_orgname_redirect_prompt=Eski ad, talep edilene kadar yeniden yönlendirilecektir.
+
+
settings.update_avatar_success=Organizasyonun resmi güncellendi.
settings.delete=Organizasyonu Sil
settings.delete_account=Bu Organizasyonu Sil
settings.delete_prompt=Organizasyon kalıcı olarak kaldırılacaktır. Bu işlem <strong>GERİ ALINAMAZ</strong>!
settings.confirm_delete_account=Silmeyi Onaylıyorum
-settings.delete_org_title=Organizasyonu Sil
-settings.delete_org_desc=Bu organizasyon kalıcı olarak silinecektir. Devam edilsin mi?
settings.hooks_desc=Bu organizasyon altındaki <strong>tüm depolar</strong> için tetiklenecek webhook'lar ekle.
settings.labels_desc=Bu organizasyonun altındaki <strong>tüm depolar</strong> ile ilgili konularda kullanılabilecek etiketler ekleyin.
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index fab4a06ecb..b563350b55 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -425,8 +425,7 @@ account_activated=Обліковий Ð·Ð°Ð¿Ð¸Ñ Ð°ÐºÑ‚Ð¸Ð²Ð¾Ð²Ð°Ð½Ð¾
prohibit_login=Вхід заборонено
prohibit_login_desc=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð½Ð¾ Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ, звернітьÑÑ Ð´Ð¾ адмініÑтратора Ñайту.
resent_limit_prompt=Ви вже надÑилали запит на активацію нещодавно. Зачекайте 3 хвилини Ñ– Ñпробуйте ще раз.
-has_unconfirmed_mail=
-Привіт %s, у Ð²Ð°Ñ Ð½ÐµÐ¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð° адреÑа електронної пошти (<b>%s</b>). Якщо ви не отримали лиÑта з підтвердженнÑм або вам потрібно надіÑлати новий, будь лаÑка, натиÑніть кнопку нижче.
+has_unconfirmed_mail=Привіт %s, у Ð²Ð°Ñ Ð½ÐµÐ¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð° адреÑа електронної пошти (<b>%s</b>). Якщо ви не отримали лиÑта з підтвердженнÑм або вам потрібно надіÑлати новий, будь лаÑка, натиÑніть кнопку нижче.
change_unconfirmed_mail_address=Якщо ваша адреÑа електронної пошти Ð´Ð»Ñ Ñ€ÐµÑ”Ñтрації невірна, ви можете змінити Ñ—Ñ— тут Ñ– надіÑлати новий лиÑÑ‚ з підтвердженнÑм.
resend_mail=ÐатиÑніть тут, щоб повторно надіÑлати лиÑÑ‚ з активацією
email_not_associate=Ð¦Ñ Ð°Ð´Ñ€ÐµÑа електронної пошти не пов'Ñзана з жодним обліковим запиÑом.
@@ -1321,7 +1320,6 @@ editor.update=Оновити %s
editor.delete=Видалити %s
editor.patch=ЗаÑтоÑувати патч
editor.patching=ЗаÑтоÑÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½ÑŒ:
-editor.fail_to_apply_patch=`Ðе вдалоÑÑ Ð·Ð°ÑтоÑувати патч "%s"`
editor.new_patch=Ðовий патч
editor.commit_message_desc=Додати необов'Ñзковий розширений опиÑ…
editor.signoff_desc=Додати «ПідпиÑано комітером» в кінці Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ.
@@ -1338,13 +1336,10 @@ editor.commit_email=Електронна пошта коміту
editor.invalid_commit_email=ÐдреÑа електронної пошти Ð´Ð»Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ недійÑна.
editor.file_is_a_symlink=`"%s" - це Ñимволічне поÑиланнÑ. Символічні поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ðµ можна редагувати у веб-редакторі`
editor.filename_is_a_directory=Ðазва файлу '%s' вже викориÑтовуєтьÑÑ Ñк назва каталогу у цьому Ñховищі.
-editor.file_deleting_no_longer_exists=Видалений файл '%s' більше не Ñ–Ñнує в цьому Ñховищі.
editor.file_changed_while_editing=ЗміÑÑ‚ файлу змінивÑÑ Ð· моменту початку редагуваннÑ. <a target="_blank" rel="noopener" href="%s"> ÐатиÑніть тут </a>, щоб переглÑнути що було змінено, або <strong>закомітьте зміни ще раз</strong>, щоб перепиÑати Ñ—Ñ….
editor.commit_empty_file_header=Закомітити порожній файл
editor.commit_empty_file_text=Файл, Ñкий ви збираєтеÑÑ Ð·Ð°ÐºÐ¾Ð¼Ñ–Ñ‚Ð¸Ñ‚Ð¸, порожній. Продовжувати?
editor.no_changes_to_show=Ðемає змін.
-editor.fail_to_update_file=Ðе вдалоÑÑ Ð¾Ð½Ð¾Ð²Ð¸Ñ‚Ð¸/Ñтворити файл "%s".
-editor.fail_to_update_file_summary=Помилка:
editor.push_rejected_no_message=Зміну відхилено Ñервером без повідомленнÑ. Будь лаÑка, перевірте Git-хуки.
editor.push_rejected=Зміну відхилено Ñервером. Будь лаÑка, перевірте Git-хуки.
editor.push_rejected_summary=Повне Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ відмову:
@@ -2612,15 +2607,13 @@ settings.visibility.private_shortname=Приватний
settings.update_settings=Оновити налаштуваннÑ
settings.update_setting_success=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ— оновлені.
-settings.change_orgname_prompt=Примітка: Зміна назви організації також змінить URL-адреÑу вашої організації та звільнить Ñтару назву.
-settings.change_orgname_redirect_prompt=Стара назва буде перенаправлÑтиÑÑ Ð´Ð¾ тих пір, поки не буде заброньована.
+
+
settings.update_avatar_success=Ðватар організації оновлений.
settings.delete=Видалити організацію
settings.delete_account=Видалити цю організацію
settings.delete_prompt=Організацію буде оÑтаточно видалено. Це <strong>ÐЕМОЖЛИВО</strong> ÑкаÑувати!
settings.confirm_delete_account=Підтвердити видаленнÑ
-settings.delete_org_title=Видалити організацію
-settings.delete_org_desc=Ð¦Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð±ÑƒÐ´Ðµ безповоротно видалена. Продовжити?
settings.hooks_desc=Додайте веб-хуки, Ñкі Ñпрацьовуватимуть Ð´Ð»Ñ <strong>вÑÑ–Ñ… Ñховищ</strong> у цій організації.
settings.labels_desc=Додайте мітки, Ñкі можна викориÑтовувати у задачах Ð´Ð»Ñ <strong>уÑÑ–Ñ… Ñховищ</strong> у цій організації.
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index b88377cce8..cb9c2acff5 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -1332,7 +1332,9 @@ editor.upload_file=上传文件
editor.edit_file=编辑文件
editor.preview_changes=é¢„è§ˆå˜æ›´
editor.cannot_edit_lfs_files=无法在 web 界é¢ä¸­ç¼–辑 lfs 文件。
+editor.cannot_edit_too_large_file=文件过大,无法编辑。
editor.cannot_edit_non_text_files=网页ä¸èƒ½ç¼–辑二进制文件。
+editor.file_not_editable_hint=但您ä»ç„¶å¯ä»¥é‡å‘½å或移动它。
editor.edit_this_file=编辑文件
editor.this_file_locked=文件已é”定
editor.must_be_on_a_branch=您必须在æŸä¸ªåˆ†æ”¯ä¸Šæ‰èƒ½å¯¹æ­¤æ–‡ä»¶è¿›è¡Œä¿®æ”¹æ“作。
@@ -1352,7 +1354,6 @@ editor.update=æ›´æ–° %s
editor.delete=删除 %s
editor.patch=应用补ä¸
editor.patching=打补ä¸ï¼š
-editor.fail_to_apply_patch=无法应用补ä¸ã€Œ%sã€
editor.new_patch=æ–°è¡¥ä¸
editor.commit_message_desc=添加一个å¯é€‰çš„æ‰©å±•æè¿°...
editor.signoff_desc=在æäº¤æ—¥å¿—æ¶ˆæ¯æœ«å°¾æ·»åŠ ç­¾ç½²äººä¿¡æ¯ã€‚
@@ -1372,8 +1373,6 @@ editor.branch_already_exists=此仓库已存在å为「%sã€çš„分支。
editor.directory_is_a_file=目录å「%sã€å·²ä½œä¸ºæ–‡ä»¶å在此仓库中存在。
editor.file_is_a_symlink=`「%sã€æ˜¯ä¸€ä¸ªç¬¦å·é“¾æŽ¥ï¼Œæ— æ³•在 Web 编辑器中编辑`
editor.filename_is_a_directory=文件å「%sã€å·²ä½œä¸ºç›®å½•å在此仓库中存在。
-editor.file_editing_no_longer_exists=正在编辑的文件「%sã€å·²ä¸å­˜åœ¨äºŽæ­¤ä»“库。
-editor.file_deleting_no_longer_exists=正在删除的文件「%sã€å·²ä¸å­˜åœ¨äºŽæ­¤ä»“库。
editor.file_changed_while_editing=文件内容在您进行编辑时已ç»å‘生å˜åŠ¨ã€‚<a target="_blank" rel="noopener noreferrer" href="%s">å•击此处</a> 查看å˜åŠ¨çš„å…·ä½“å†…å®¹ï¼Œæˆ–è€… <strong>冿¬¡æäº¤</strong> 覆盖已å‘生的å˜åŠ¨ã€‚
editor.file_already_exists=此仓库已ç»å­˜åœ¨å为「%sã€çš„æ–‡ä»¶ã€‚
editor.commit_id_not_matching=æäº¤ ID 与您开始编辑时的 ID ä¸åŒ¹é…。请æäº¤åˆ°è¡¥ä¸åˆ†æ”¯ç„¶åŽåˆå¹¶ã€‚
@@ -1381,8 +1380,6 @@ editor.push_out_of_date=推é€ä¼¼ä¹Žå·²ç»è¿‡æ—¶ã€‚
editor.commit_empty_file_header=æäº¤ä¸€ä¸ªç©ºæ–‡ä»¶
editor.commit_empty_file_text=æ‚¨è¦æäº¤çš„æ–‡ä»¶æ˜¯ç©ºçš„ï¼Œç»§ç»­å—?
editor.no_changes_to_show=没有å¯ä»¥æ˜¾ç¤ºçš„å˜æ›´ã€‚
-editor.fail_to_update_file=æ›´æ–°/创建文件「%sã€å¤±è´¥ã€‚
-editor.fail_to_update_file_summary=错误信æ¯ï¼š
editor.push_rejected_no_message=此修改被æœåŠ¡å™¨æ‹’ç»å¹¶ä¸”没有å馈消æ¯ã€‚请检查 Git é’©å­ã€‚
editor.push_rejected=此修改被æœåŠ¡å™¨æ‹’ç»ã€‚请检查 Git é’©å­ã€‚
editor.push_rejected_summary=详细拒ç»ä¿¡æ¯ï¼š
@@ -2829,15 +2826,13 @@ settings.visibility.private_shortname=ç§æœ‰
settings.update_settings=更新组织设置
settings.update_setting_success=组织设置已更新。
-settings.change_orgname_prompt=注æ„:更改组织åç§°åŒæ—¶ä¼šæ›´æ”¹ç»„织的 URL 地å€å¹¶é‡Šæ”¾æ—§çš„å称。
-settings.change_orgname_redirect_prompt=在被人使用å‰ï¼Œæ—§ç”¨æˆ·å将会被é‡å®šå‘。
+
+
settings.update_avatar_success=组织头åƒå·²ç»æ›´æ–°ã€‚
settings.delete=删除组织
settings.delete_account=删除当å‰ç»„织
settings.delete_prompt=删除æ“作会永久清除该组织的信æ¯ï¼Œå¹¶ä¸” <strong>ä¸å¯æ¢å¤</strong>ï¼
settings.confirm_delete_account=确认删除组织
-settings.delete_org_title=删除组织
-settings.delete_org_desc=此组织将会永久删除,确认继续å—?
settings.hooks_desc=在此处添加的 Web é’©å­å°†ä¼šåº”用到该组织下的 <strong>所有仓库</strong>。
settings.labels_desc=添加能够被该组织下的 <strong>所有仓库</strong> 的工å•使用的标签。
diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini
index 2874da3170..10c89c8cbe 100644
--- a/options/locale/locale_zh-HK.ini
+++ b/options/locale/locale_zh-HK.ini
@@ -656,10 +656,11 @@ settings.visibility.private_shortname=ç§æœ‰åº«
settings.update_settings=更新組織設定
settings.update_setting_success=組織設定已更新。
+
+
settings.delete=刪除組織
settings.delete_account=刪除當å‰çµ„ç¹”
settings.confirm_delete_account=確èªåˆªé™¤çµ„ç¹”
-settings.delete_org_title=刪除組織
settings.hooks_desc=新增 webhooks 將觸發在這個組織下 <strong>全部的儲存庫</strong> 。
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index a52e147415..30900b74b8 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -1318,7 +1318,6 @@ editor.update=æ›´æ–° %s
editor.delete=刪除 %s
editor.patch=套用 Patch
editor.patching=正在 Patch:
-editor.fail_to_apply_patch=無法套用 Patch「%sã€
editor.new_patch=新增 Patch
editor.commit_message_desc=(é¸ç”¨) 加入詳細說明...
editor.signoff_desc=在æäº¤è¨Šæ¯åº•部加入æäº¤è€…的「Signed-off-byã€è³‡è¨Šã€‚
@@ -1336,8 +1335,6 @@ editor.branch_already_exists=此儲存庫已有å為「%sã€çš„分支。
editor.directory_is_a_file=目錄å稱「%sã€å·²è¢«æ­¤å„²å­˜åº«çš„æª”案使用。
editor.file_is_a_symlink=`"%s" 是一個符號連çµã€‚符號連çµç„¡æ³•在網é ç·¨è¼¯å™¨ä¸­ç·¨è¼¯`
editor.filename_is_a_directory=檔å「%sã€å·²è¢«æ­¤å„²å­˜åº«çš„目錄å稱使用。
-editor.file_editing_no_longer_exists=æ­£è¦ç·¨è¼¯çš„æª”案「%sã€å·²ä¸å­˜åœ¨æ­¤å„²å­˜åº«ä¸­ã€‚
-editor.file_deleting_no_longer_exists=æ­£è¦åˆªé™¤çš„æª”案「%sã€å·²ä¸å­˜åœ¨æ­¤å„²å­˜åº«ä¸­ã€‚
editor.file_changed_while_editing=檔案內容在您編輯的途中已被變更。<a target="_blank" rel="noopener noreferrer" href="%s">按一下此處</a>查看更動的地方或<strong>冿¬¡æäº¤</strong>以覆蓋這些變更。
editor.file_already_exists=此儲存庫已有å為「%sã€çš„æª”案。
editor.commit_id_not_matching=æäº¤ ID 與您開始編輯時的 ID ä¸åŒ¹é…。請æäº¤åˆ°ä¸€å€‹è£œä¸åˆ†æ”¯ç„¶å¾Œåˆä½µã€‚
@@ -1345,8 +1342,6 @@ editor.push_out_of_date=推é€ä¼¼ä¹Žå·²éŽæ™‚。
editor.commit_empty_file_header=æäº¤ç©ºç™½æª”案
editor.commit_empty_file_text=你準備æäº¤çš„æª”案是空白的,是å¦ç¹¼çºŒï¼Ÿ
editor.no_changes_to_show=沒有å¯ä»¥é¡¯ç¤ºçš„變更。
-editor.fail_to_update_file=æ›´æ–°/建立檔案「%sã€å¤±æ•—。
-editor.fail_to_update_file_summary=錯誤訊æ¯:
editor.push_rejected_no_message=該變更被伺æœå™¨æ‹’絕但未æä¾›å…¶ä»–資訊。請檢查 Git Hook。
editor.push_rejected=該變更被伺æœå™¨æ‹’絕。請檢查 Git Hook。
editor.push_rejected_summary=完整的拒絕訊æ¯:
@@ -2756,15 +2751,13 @@ settings.visibility.private_shortname=ç§æœ‰
settings.update_settings=更新設定
settings.update_setting_success=組織設定已更新。
-settings.change_orgname_prompt=注æ„:更改組織åç¨±å°‡åŒæ™‚更改組織的 URL 並釋放舊å稱。
-settings.change_orgname_redirect_prompt=舊的å稱被領用å‰ï¼Œæœƒé‡æ–°å°Žå‘æ–°å稱。
+
+
settings.update_avatar_success=已更新組織的大頭貼。
settings.delete=刪除組織
settings.delete_account=刪除這個組織
settings.delete_prompt=該組織將被永久刪除。此動作<strong>ä¸å¯</strong>還原ï¼
settings.confirm_delete_account=確èªåˆªé™¤çµ„ç¹”
-settings.delete_org_title=刪除組織
-settings.delete_org_desc=å³å°‡æ°¸ä¹…刪除這個組織,是å¦ç¹¼çºŒï¼Ÿ
settings.hooks_desc=此組織下的<strong>所有存儲庫</strong>都會觸發在此新增的 Webhook。
settings.labels_desc=在此處新增的標籤å¯ç”¨æ–¼æ­¤çµ„織下的<strong>所有儲存庫</strong>。
diff --git a/package-lock.json b/package-lock.json
index 59ce5b33e0..d48702d31e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,7 @@
"esbuild-loader": "4.3.0",
"escape-goat": "4.0.0",
"fast-glob": "3.3.3",
- "htmx.org": "2.0.4",
+ "htmx.org": "2.0.5",
"idiomorph": "0.7.3",
"jquery": "3.7.1",
"katex": "0.16.22",
@@ -8233,9 +8233,9 @@
}
},
"node_modules/htmx.org": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz",
- "integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.5.tgz",
+ "integrity": "sha512-ocgvtHCShWFW0DvSV1NbJC7Y5EzUMy2eo5zeWvGj2Ac4LOr7sv9YKg4jzCZJdXN21fXACmCViwKSy+cm6i2dWQ==",
"license": "0BSD"
},
"node_modules/iconv-lite": {
diff --git a/package.json b/package.json
index 4faf34900a..3dab385c6e 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
"esbuild-loader": "4.3.0",
"escape-goat": "4.0.0",
"fast-glob": "3.3.3",
- "htmx.org": "2.0.4",
+ "htmx.org": "2.0.5",
"idiomorph": "0.7.3",
"jquery": "3.7.1",
"katex": "0.16.22",
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index f40d39a251..1c7b57b922 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -470,6 +470,9 @@ func ChangeFiles(ctx *context.APIContext) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
+ // FIXME: actually now we support more operations like "rename", "upload"
+ // FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options.
+ // Need to fully fix them in API
changeRepoFile := &files_service.ChangeRepoFile{
Operation: file.Operation,
TreePath: file.Path,
diff --git a/routers/install/install.go b/routers/install/install.go
index b9bc41dfcf..c1da79454a 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -601,5 +601,7 @@ func SubmitInstall(ctx *context.Context) {
// InstallDone shows the "post-install" page, makes it easier to develop the page.
// The name is not called as "PostInstall" to avoid misinterpretation as a handler for "POST /install"
func InstallDone(ctx *context.Context) { //nolint
+ hasUsers, _ := user_model.HasUsers(ctx)
+ ctx.Data["IsAccountCreated"] = hasUsers.HasAnyUser
ctx.HTML(http.StatusOK, tplPostInstall)
}
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 87edbc357b..94f75f69ff 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -421,9 +421,11 @@ func SignOut(ctx *context.Context) {
// SignUp render the register page
func SignUp(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_up")
-
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
+ hasUsers, _ := user_model.HasUsers(ctx)
+ ctx.Data["IsFirstTimeRegistration"] = !hasUsers.HasAnyUser
+
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignUp", err)
@@ -610,7 +612,13 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
// sends a confirmation email if required.
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
// Auto-set admin for the only user.
- if user_model.CountUsers(ctx, nil) == 1 {
+ hasUsers, err := user_model.HasUsers(ctx)
+ if err != nil {
+ ctx.ServerError("HasUsers", err)
+ return false
+ }
+ if hasUsers.HasOnlyOneUser {
+ // the only user is the one just created, will set it as admin
opts := &user_service.UpdateOptions{
IsActive: optional.Some(true),
IsAdmin: user_service.UpdateOptionFieldFromValue(true),
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index 855c2a50db..f0d7d0ce7d 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -151,6 +151,7 @@ func Repos(ctx *context.Context) {
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
ctx.Data["Title"] = ctx.Tr("explore")
ctx.Data["PageIsExplore"] = true
+ ctx.Data["ShowRepoOwnerOnList"] = true
ctx.Data["PageIsExploreRepositories"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index 9dd0a98160..2bc1e8bc43 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -18,6 +18,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
@@ -31,8 +32,6 @@ import (
const (
// tplSettingsOptions template path for render settings
tplSettingsOptions templates.TplName = "org/settings/options"
- // tplSettingsDelete template path for render delete repository
- tplSettingsDelete templates.TplName = "org/settings/delete"
// tplSettingsHooks template path for render hook settings
tplSettingsHooks templates.TplName = "org/settings/hooks"
// tplSettingsLabels template path for render labels settings
@@ -71,26 +70,6 @@ func SettingsPost(ctx *context.Context) {
org := ctx.Org.Organization
- if org.Name != form.Name {
- if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil {
- if user_model.IsErrUserAlreadyExist(err) {
- ctx.Data["Err_Name"] = true
- ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
- } else if db.IsErrNameReserved(err) {
- ctx.Data["Err_Name"] = true
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
- } else if db.IsErrNamePatternNotAllowed(err) {
- ctx.Data["Err_Name"] = true
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
- } else {
- ctx.ServerError("RenameUser", err)
- }
- return
- }
-
- ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name)
- }
-
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
ctx.Data["Err_Email"] = true
@@ -163,42 +142,27 @@ func SettingsDeleteAvatar(ctx *context.Context) {
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings")
}
-// SettingsDelete response for deleting an organization
-func SettingsDelete(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("org.settings")
- ctx.Data["PageIsOrgSettings"] = true
- ctx.Data["PageIsSettingsDelete"] = true
-
- if ctx.Req.Method == http.MethodPost {
- if ctx.Org.Organization.Name != ctx.FormString("org_name") {
- ctx.Data["Err_OrgName"] = true
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil)
- return
- }
-
- if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
- if repo_model.IsErrUserOwnRepos(err) {
- ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
- ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
- } else if packages_model.IsErrUserOwnPackages(err) {
- ctx.Flash.Error(ctx.Tr("form.org_still_own_packages"))
- ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
- } else {
- ctx.ServerError("DeleteOrganization", err)
- }
- } else {
- log.Trace("Organization deleted: %s", ctx.Org.Organization.Name)
- ctx.Redirect(setting.AppSubURL + "/")
- }
+// SettingsDeleteOrgPost response for deleting an organization
+func SettingsDeleteOrgPost(ctx *context.Context) {
+ if ctx.Org.Organization.Name != ctx.FormString("org_name") {
+ ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
return
}
- if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
- ctx.ServerError("RenderUserOrgHeader", err)
+ if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false /* no purge */); err != nil {
+ if repo_model.IsErrUserOwnRepos(err) {
+ ctx.JSONError(ctx.Tr("form.org_still_own_repo"))
+ } else if packages_model.IsErrUserOwnPackages(err) {
+ ctx.JSONError(ctx.Tr("form.org_still_own_packages"))
+ } else {
+ log.Error("DeleteOrganization: %v", err)
+ ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.delete_failed"))))
+ }
return
}
- ctx.HTML(http.StatusOK, tplSettingsDelete)
+ ctx.Flash.Success(ctx.Tr("org.settings.delete_successful", ctx.Org.Organization.Name))
+ ctx.JSONRedirect(setting.AppSubURL + "/")
}
// Webhooks render webhook list page
@@ -250,3 +214,40 @@ func Labels(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplSettingsLabels)
}
+
+// SettingsRenamePost response for renaming organization
+func SettingsRenamePost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RenameOrgForm)
+ if ctx.HasError() {
+ ctx.JSONError(ctx.GetErrMsg())
+ return
+ }
+
+ oldOrgName, newOrgName := ctx.Org.Organization.Name, form.NewOrgName
+
+ if form.OrgName != oldOrgName {
+ ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
+ return
+ }
+ if newOrgName == oldOrgName {
+ ctx.JSONError(ctx.Tr("org.settings.rename_no_change"))
+ return
+ }
+
+ if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName); err != nil {
+ if user_model.IsErrUserAlreadyExist(err) {
+ ctx.JSONError(ctx.Tr("org.form.name_been_taken", newOrgName))
+ } else if db.IsErrNameReserved(err) {
+ ctx.JSONError(ctx.Tr("org.form.name_reserved", newOrgName))
+ } else if db.IsErrNamePatternNotAllowed(err) {
+ ctx.JSONError(ctx.Tr("org.form.name_pattern_not_allowed", newOrgName))
+ } else {
+ log.Error("RenameOrganization: %v", err)
+ ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.rename_failed"))))
+ }
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("org.settings.rename_success", oldOrgName, newOrgName))
+ ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(newOrgName) + "/settings")
+}
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
deleted file mode 100644
index 690b830bc2..0000000000
--- a/routers/web/repo/cherry_pick.go
+++ /dev/null
@@ -1,193 +0,0 @@
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repo
-
-import (
- "bytes"
- "errors"
- "net/http"
- "strings"
-
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/repository/files"
-)
-
-var tplCherryPick templates.TplName = "repo/editor/cherry_pick"
-
-// CherryPick handles cherrypick GETs
-func CherryPick(ctx *context.Context) {
- ctx.Data["SHA"] = ctx.PathParam("sha")
- cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha"))
- if err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(err)
- return
- }
- ctx.ServerError("GetCommit", err)
- return
- }
-
- if ctx.FormString("cherry-pick-type") == "revert" {
- ctx.Data["CherryPickType"] = "revert"
- ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
- ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
- } else {
- ctx.Data["CherryPickType"] = "cherry-pick"
- splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
- ctx.Data["commit_summary"] = splits[0]
- ctx.Data["commit_message"] = splits[1]
- }
-
- canCommit := renderCommitRights(ctx)
- ctx.Data["TreePath"] = ""
-
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- ctx.HTML(http.StatusOK, tplCherryPick)
-}
-
-// CherryPickPost handles cherrypick POSTs
-func CherryPickPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CherryPickForm)
-
- sha := ctx.PathParam("sha")
- ctx.Data["SHA"] = sha
- if form.Revert {
- ctx.Data["CherryPickType"] = "revert"
- } else {
- ctx.Data["CherryPickType"] = "cherry-pick"
- }
-
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplCherryPick)
- return
- }
-
- // Cannot commit to a an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplCherryPick, &form)
- return
- }
-
- message := strings.TrimSpace(form.CommitSummary)
- if message == "" {
- if form.Revert {
- message = ctx.Locale.TrString("repo.commit.revert-header", sha)
- } else {
- message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
- }
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplCherryPick, &form)
- return
- }
- opts := &files.ApplyDiffPatchOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- Author: gitCommitter,
- Committer: gitCommitter,
- }
-
- // First lets try the simple plain read-tree -m approach
- opts.Content = sha
- if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- // Drop through to the apply technique
-
- buf := &bytes.Buffer{}
- if form.Revert {
- if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist."))
- return
- }
- ctx.ServerError("GetRawDiff", err)
- return
- }
- } else {
- if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist."))
- return
- }
- ctx.ServerError("GetRawDiff", err)
- return
- }
- }
-
- opts.Content = buf.String()
- ctx.Data["FileContent"] = opts.Content
-
- if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
- return
- }
- }
-
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
- ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
- } else {
- ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
- }
-}
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 1a090c9437..e8ad3cceb5 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -4,6 +4,7 @@
package repo
import (
+ "bytes"
"fmt"
"io"
"net/http"
@@ -11,18 +12,15 @@ import (
"strings"
git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
@@ -34,141 +32,209 @@ const (
tplEditDiffPreview templates.TplName = "repo/editor/diff_preview"
tplDeleteFile templates.TplName = "repo/editor/delete"
tplUploadFile templates.TplName = "repo/editor/upload"
+ tplPatchFile templates.TplName = "repo/editor/patch"
+ tplCherryPick templates.TplName = "repo/editor/cherry_pick"
- frmCommitChoiceDirect string = "direct"
- frmCommitChoiceNewBranch string = "commit-to-new-branch"
+ editorCommitChoiceDirect string = "direct"
+ editorCommitChoiceNewBranch string = "commit-to-new-branch"
)
-func canCreateBasePullRequest(ctx *context.Context) bool {
- baseRepo := ctx.Repo.Repository.BaseRepo
- return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests)
+func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
+ cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
+ if cleanedTreePath != ctx.Repo.TreePath {
+ redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
+ if ctx.Req.URL.RawQuery != "" {
+ redirectTo += "?" + ctx.Req.URL.RawQuery
+ }
+ ctx.Redirect(redirectTo)
+ return
+ }
+
+ commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("PrepareCommitFormBehaviors", err)
+ return
+ }
+
+ ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
+ ctx.Data["TreePath"] = ctx.Repo.TreePath
+ ctx.Data["CommitFormBehaviors"] = commitFormBehaviors
+
+ // for online editor
+ ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
+ ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
+ ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
+ ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
+
+ // form fields
+ ctx.Data["commit_summary"] = ""
+ ctx.Data["commit_message"] = ""
+ ctx.Data["commit_choice"] = util.Iif(commitFormBehaviors.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
+ ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, ctx.Repo.Repository)
+ ctx.Data["last_commit"] = ctx.Repo.CommitID
}
-func renderCommitRights(ctx *context.Context) bool {
- canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer)
+func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
+ // show the tree path fields in the "breadcrumb" and help users to edit the target tree path
+ ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(treePath)
+}
+
+type parsedEditorCommitForm[T any] struct {
+ form T
+ commonForm *forms.CommitCommonForm
+ CommitFormBehaviors *context.CommitFormBehaviors
+ TargetBranchName string
+ GitCommitter *files_service.IdentityOptions
+}
+
+func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
+ commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage)
+ if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
+ commitMessage += "\n\n" + body
+ }
+ return commitMessage
+}
+
+func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *parsedEditorCommitForm[T] {
+ form := web.GetForm(ctx).(T)
+ if ctx.HasError() {
+ ctx.JSONError(ctx.GetErrMsg())
+ return nil
+ }
+
+ commonForm := form.GetCommitCommonForm()
+ commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
+
+ commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer)
if err != nil {
- log.Error("CanCommitToBranch: %v", err)
+ ctx.ServerError("PrepareCommitFormBehaviors", err)
+ return nil
+ }
+
+ // check commit behavior
+ targetBranchName := util.Iif(commonForm.CommitChoice == editorCommitChoiceNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
+ if targetBranchName == ctx.Repo.BranchName && !commitFormBehaviors.CanCommitToBranch {
+ ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName))
+ return nil
}
- ctx.Data["CanCommitToBranch"] = canCommitToBranch
- ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx)
- return canCommitToBranch.CanCommitToBranch
+ // Committer user info
+ gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, commonForm.CommitEmail)
+ if !valid {
+ ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email"))
+ return nil
+ }
+
+ return &parsedEditorCommitForm[T]{
+ form: form,
+ commonForm: commonForm,
+ CommitFormBehaviors: commitFormBehaviors,
+ TargetBranchName: targetBranchName,
+ GitCommitter: gitCommitter,
+ }
}
// redirectForCommitChoice redirects after committing the edit to a branch
-func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) {
- if commitChoice == frmCommitChoiceNewBranch {
+func redirectForCommitChoice[T any](ctx *context.Context, parsed *parsedEditorCommitForm[T], treePath string) {
+ if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch {
// Redirect to a pull request when possible
redirectToPullRequest := false
- repo := ctx.Repo.Repository
- baseBranch := ctx.Repo.BranchName
- headBranch := newBranchName
+ repo, baseBranch, headBranch := ctx.Repo.Repository, ctx.Repo.BranchName, parsed.TargetBranchName
if repo.UnitEnabled(ctx, unit.TypePullRequests) {
redirectToPullRequest = true
- } else if canCreateBasePullRequest(ctx) {
+ } else if parsed.CommitFormBehaviors.CanCreateBasePullRequest {
redirectToPullRequest = true
baseBranch = repo.BaseRepo.DefaultBranch
headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
repo = repo.BaseRepo
}
-
if redirectToPullRequest {
- ctx.Redirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
+ ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
return
}
}
returnURI := ctx.FormString("return_uri")
-
- ctx.RedirectToCurrentSite(
- returnURI,
- ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath),
- )
+ if returnURI == "" || !httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
+ returnURI = util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.TargetBranchName), util.PathEscapeSegments(treePath))
+ }
+ ctx.JSONRedirect(returnURI)
}
-// getParentTreeFields returns list of parent tree names and corresponding tree paths
-// based on given tree path.
-func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
- if len(treePath) == 0 {
- return treeNames, treePaths
+func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) {
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
+ if err != nil {
+ HandleGitError(ctx, "GetTreeEntryByPath", err)
+ return nil, nil, nil
}
- treeNames = strings.Split(treePath, "/")
- treePaths = make([]string, len(treeNames))
- for i := range treeNames {
- treePaths[i] = strings.Join(treeNames[:i+1], "/")
+ // No way to edit a directory online.
+ if entry.IsDir() {
+ ctx.NotFound(nil)
+ return nil, nil, nil
}
- return treeNames, treePaths
-}
-func editFileCommon(ctx *context.Context, isNewFile bool) {
- ctx.Data["PageIsEdit"] = true
- ctx.Data["IsNewFile"] = isNewFile
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
- ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
-}
-
-func editFile(ctx *context.Context, isNewFile bool) {
- editFileCommon(ctx, isNewFile)
- canCommit := renderCommitRights(ctx)
-
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if treePath != ctx.Repo.TreePath {
- if isNewFile {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
+ blob := entry.Blob()
+ buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
+ if err != nil {
+ if git.IsErrNotExist(err) {
+ ctx.NotFound(err)
} else {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
+ ctx.ServerError("getFileReader", err)
}
- return
+ return nil, nil, nil
}
- // Check if the filename (and additional path) is specified in the querystring
- // (filename is a misnomer, but kept for compatibility with GitHub)
- filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename"))
- filePath = strings.Trim(filePath, "/")
- treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath))
-
- if !isNewFile {
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
+ if fInfo.isLFSFile {
+ lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
if err != nil {
- HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
- return
- }
-
- // No way to edit a directory online.
- if entry.IsDir() {
+ _ = dataRc.Close()
+ ctx.ServerError("GetTreePathLock", err)
+ return nil, nil, nil
+ } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
+ _ = dataRc.Close()
ctx.NotFound(nil)
- return
+ return nil, nil, nil
}
+ }
- blob := entry.Blob()
+ return buf, dataRc, fInfo
+}
- buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
- if err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(err)
- } else {
- ctx.ServerError("getFileReader", err)
- }
- return
+func EditFile(ctx *context.Context) {
+ editorAction := ctx.PathParam("editor_action")
+ isNewFile := editorAction == "_new"
+ ctx.Data["IsNewFile"] = isNewFile
+
+ // Check if the filename (and additional path) is specified in the querystring
+ // (filename is a misnomer, but kept for compatibility with GitHub)
+ urlQuery := ctx.Req.URL.Query()
+ queryFilename := urlQuery.Get("filename")
+ if queryFilename != "" {
+ newTreePath := path.Join(ctx.Repo.TreePath, queryFilename)
+ redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(newTreePath))
+ urlQuery.Del("filename")
+ if newQueryParams := urlQuery.Encode(); newQueryParams != "" {
+ redirectTo += "?" + newQueryParams
}
+ ctx.Redirect(redirectTo)
+ return
+ }
- defer dataRc.Close()
+ // on the "New File" page, we should add an empty path field to make end users could input a new name
+ prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath))
- if fInfo.isLFSFile {
- lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
- if err != nil {
- ctx.ServerError("GetTreePathLock", err)
- return
- }
- if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
- ctx.NotFound(nil)
- return
- }
+ prepareEditorCommitFormOptions(ctx, editorAction)
+ if ctx.Written() {
+ return
+ }
+
+ if !isNewFile {
+ prefetch, dataRc, fInfo := editFileOpenExisting(ctx)
+ if ctx.Written() {
+ return
}
+ defer dataRc.Close()
ctx.Data["FileSize"] = fInfo.fileSize
@@ -179,740 +245,152 @@ func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
} else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file")
- } else {
- d, _ := io.ReadAll(dataRc)
+ }
- buf = append(buf, d...)
+ if ctx.Data["NotEditableReason"] == nil {
+ buf, err := io.ReadAll(io.MultiReader(bytes.NewReader(prefetch), dataRc))
+ if err != nil {
+ ctx.ServerError("ReadAll", err)
+ return
+ }
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
- log.Error("ToUTF8: %v", err)
ctx.Data["FileContent"] = string(buf)
} else {
ctx.Data["FileContent"] = content
}
}
- } else {
- // Append filename from query, or empty string to allow username the new file.
- treeNames = append(treeNames, fileName)
}
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch)
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.Data["last_commit"] = ctx.Repo.CommitID
-
- ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
-
+ ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath)
ctx.HTML(http.StatusOK, tplEditFile)
}
-// GetEditorConfig returns a editorconfig JSON string for given treePath or "null"
-func GetEditorConfig(ctx *context.Context, treePath string) string {
- ec, _, err := ctx.Repo.GetEditorconfig()
- if err == nil {
- def, err := ec.GetDefinitionForFilename(treePath)
- if err == nil {
- jsonStr, _ := json.Marshal(def)
- return string(jsonStr)
- }
- }
- return "null"
-}
-
-// EditFile render edit file page
-func EditFile(ctx *context.Context) {
- editFile(ctx, false)
-}
-
-// NewFile render create file page
-func NewFile(ctx *context.Context) {
- editFile(ctx, true)
-}
-
-func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
- editFileCommon(ctx, isNewFile)
- ctx.Data["PageHasPosted"] = true
-
- canCommit := renderCommitRights(ctx)
- treeNames, treePaths := getParentTreeFields(form.TreePath)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- ctx.Data["TreePath"] = form.TreePath
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["FileContent"] = form.Content
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplEditFile)
- return
- }
-
- // Cannot commit to an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
+func EditFilePost(ctx *context.Context) {
+ editorAction := ctx.PathParam("editor_action")
+ isNewFile := editorAction == "_new"
+ parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
+ if ctx.Written() {
return
}
- // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
- // `message` will be both the summary and message combined
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- if isNewFile {
- message = ctx.Locale.TrString("repo.editor.add", form.TreePath)
- } else {
- message = ctx.Locale.TrString("repo.editor.update", form.TreePath)
- }
- }
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form)
- return
- }
+ defaultCommitMessage := util.Iif(isNewFile, ctx.Locale.TrString("repo.editor.add", parsed.form.TreePath), ctx.Locale.TrString("repo.editor.update", parsed.form.TreePath))
var operation string
if isNewFile {
operation = "create"
- } else if form.Content.Has() {
+ } else if parsed.form.Content.Has() {
// The form content only has data if the file is representable as text, is not too large and not in lfs.
operation = "update"
- } else if ctx.Repo.TreePath != form.TreePath {
+ } else if ctx.Repo.TreePath != parsed.form.TreePath {
// If it doesn't have data, the only possible operation is a "rename"
operation = "rename"
} else {
// It should never happen, just in case
- ctx.Flash.Error(ctx.Tr("error.occurred"))
- ctx.HTML(http.StatusOK, tplEditFile)
+ ctx.JSONError(ctx.Tr("error.occurred"))
return
}
- if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
- LastCommitID: form.LastCommit,
+ _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
+ LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
+ NewBranch: parsed.TargetBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
Files: []*files_service.ChangeRepoFile{
{
Operation: operation,
FromTreePath: ctx.Repo.TreePath,
- TreePath: form.TreePath,
- ContentReader: strings.NewReader(strings.ReplaceAll(form.Content.Value(), "\r", "")),
+ TreePath: parsed.form.TreePath,
+ ContentReader: strings.NewReader(strings.ReplaceAll(parsed.form.Content.Value(), "\r", "")),
},
},
- Signoff: form.Signoff,
- Author: gitCommitter,
- Committer: gitCommitter,
- }); err != nil {
- // This is where we handle all the errors thrown by files_service.ChangeRepoFiles
- if git.IsErrNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
- } else if git_model.IsErrLFSFileLocked(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok {
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
- default:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", fileErr.Path), tplEditFile, &form)
- }
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrRepoFileAlreadyExists(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
- } else if git.IsErrBranchNotExist(err) {
- // For when a user adds/updates a file to a branch that no longer exists
- if branchErr, ok := err.(git.ErrBranchNotExist); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- ctx.Data["Err_NewBranchName"] = true
- if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form)
- } else if git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("editFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplEditFile, &form)
- }
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
- "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
- "Details": utils.SanitizeFlashErrorString(err.Error()),
- })
- if err != nil {
- ctx.ServerError("editFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplEditFile, &form)
- }
- }
-
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
-}
-
-// EditFilePost response for editing file
-func EditFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
- editFilePost(ctx, *form, false)
-}
-
-// NewFilePost response for creating file
-func NewFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
- editFilePost(ctx, *form, true)
-}
-
-// DiffPreviewPost render preview diff page
-func DiffPreviewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditPreviewDiffForm)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if len(treePath) == 0 {
- ctx.HTTPError(http.StatusInternalServerError, "file name to diff is invalid")
- return
- }
-
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error())
- return
- } else if entry.IsDir() {
- ctx.HTTPError(http.StatusUnprocessableEntity)
- return
- }
-
- diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "GetDiffPreview: "+err.Error())
+ editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
return
}
- if len(diff.Files) != 0 {
- ctx.Data["File"] = diff.Files[0]
- }
-
- ctx.HTML(http.StatusOK, tplEditDiffPreview)
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
}
// DeleteFile render delete file page
func DeleteFile(ctx *context.Context) {
- ctx.Data["PageIsDelete"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
-
- if treePath != ctx.Repo.TreePath {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
+ prepareEditorCommitFormOptions(ctx, "_delete")
+ if ctx.Written() {
return
}
-
- ctx.Data["TreePath"] = treePath
- canCommit := renderCommitRights(ctx)
-
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
-
+ ctx.Data["PageIsDelete"] = true
ctx.HTML(http.StatusOK, tplDeleteFile)
}
// DeleteFilePost response for deleting file
func DeleteFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.DeleteRepoFileForm)
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- ctx.Data["PageIsDelete"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["TreePath"] = ctx.Repo.TreePath
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplDeleteFile)
+ parsed := parseEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
+ if ctx.Written() {
return
}
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
- return
- }
-
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
- }
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplDeleteFile, &form)
- return
- }
-
- if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
- LastCommitID: form.LastCommit,
+ treePath := ctx.Repo.TreePath
+ _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
+ LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
+ NewBranch: parsed.TargetBranchName,
Files: []*files_service.ChangeRepoFile{
{
Operation: "delete",
- TreePath: ctx.Repo.TreePath,
+ TreePath: treePath,
},
},
- Message: message,
- Signoff: form.Signoff,
- Author: gitCommitter,
- Committer: gitCommitter,
- }); err != nil {
- // This is where we handle all the errors thrown by repofiles.DeleteRepoFile
- if git.IsErrNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok {
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form)
- default:
- ctx.ServerError("DeleteRepoFile", err)
- }
- } else {
- ctx.ServerError("DeleteRepoFile", err)
- }
- } else if git.IsErrBranchNotExist(err) {
- // For when a user deletes a file to a branch that no longer exists
- if branchErr, ok := err.(git.ErrBranchNotExist); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("DeleteFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplDeleteFile, &form)
- }
- } else {
- ctx.ServerError("DeleteRepoFile", err)
- }
+ Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
return
}
- ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
- treePath := path.Dir(ctx.Repo.TreePath)
- if treePath == "." {
- treePath = "" // the file deleted was in the root, so we return the user to the root directory
- }
- if len(treePath) > 0 {
- // Need to get the latest commit since it changed
- commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
- if err == nil && commit != nil {
- // We have the comment, now find what directory we can return the user to
- // (must have entries)
- treePath = GetClosestParentWithFiles(treePath, commit)
- } else {
- treePath = "" // otherwise return them to the root of the repo
- }
- }
-
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath)
+ ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
+ redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.TargetBranchName, treePath)
+ redirectForCommitChoice(ctx, parsed, redirectTreePath)
}
-// UploadFile render upload file page
func UploadFile(ctx *context.Context) {
ctx.Data["PageIsUpload"] = true
upload.AddUploadContext(ctx, "repo")
- canCommit := renderCommitRights(ctx)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if treePath != ctx.Repo.TreePath {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
- return
- }
- ctx.Repo.TreePath = treePath
-
- treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
- if len(treeNames) == 0 {
- // We must at least have one element for user to input.
- treeNames = []string{""}
- }
+ prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
+ prepareEditorCommitFormOptions(ctx, "_upload")
+ if ctx.Written() {
+ return
}
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
-
ctx.HTML(http.StatusOK, tplUploadFile)
}
-// UploadFilePost response for uploading file
func UploadFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
- ctx.Data["PageIsUpload"] = true
- upload.AddUploadContext(ctx, "repo")
- canCommit := renderCommitRights(ctx)
-
- oldBranchName := ctx.Repo.BranchName
- branchName := oldBranchName
-
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- form.TreePath = cleanUploadFileName(form.TreePath)
-
- treeNames, treePaths := getParentTreeFields(form.TreePath)
- if len(treeNames) == 0 {
- // We must at least have one element for user to input.
- treeNames = []string{""}
- }
-
- ctx.Data["TreePath"] = form.TreePath
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = branchName
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplUploadFile)
- return
- }
-
- if oldBranchName != branchName {
- if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err == nil && exist {
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form)
- return
- }
- } else if !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form)
- return
- }
-
- if !ctx.Repo.Repository.IsEmpty {
- var newTreePath string
- for _, part := range treeNames {
- newTreePath = path.Join(newTreePath, part)
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
- if err != nil {
- if git.IsErrNotExist(err) {
- break // Means there is no item with that name, so we're good
- }
- ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
- return
- }
-
- // User can only upload files to a directory, the directory name shouldn't be an existing file.
- if !entry.IsDir() {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form)
- return
- }
- }
- }
-
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- dir := form.TreePath
- if dir == "" {
- dir = "/"
- }
- message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplUploadFile, &form)
+ parsed := parseEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
+ if ctx.Written() {
return
}
- if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
- LastCommitID: ctx.Repo.CommitID,
- OldBranch: oldBranchName,
- NewBranch: branchName,
- TreePath: form.TreePath,
- Message: message,
- Files: form.Files,
- Signoff: form.Signoff,
- Author: gitCommitter,
- Committer: gitCommitter,
- }); err != nil {
- if git_model.IsErrLFSFileLocked(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- fileErr := err.(files_service.ErrFilePathInvalid)
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form)
- default:
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrRepoFileAlreadyExists(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form)
- } else if git.IsErrBranchNotExist(err) {
- branchErr := err.(git.ErrBranchNotExist)
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form)
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- ctx.Data["Err_NewBranchName"] = true
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
- } else if git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("UploadFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplUploadFile, &form)
- }
- } else {
- // os.ErrNotExist - upload file missing in the intervening time?!
- log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err)
- ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form)
- }
- return
- }
-
- if ctx.Repo.Repository.IsEmpty {
- if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
- _ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
- }
- }
-
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
-}
-
-func cleanUploadFileName(name string) string {
- // Rebase the filename
- name = util.PathJoinRel(name)
- // Git disallows any filenames to have a .git directory in them.
- for part := range strings.SplitSeq(name, "/") {
- if strings.ToLower(part) == ".git" {
- return ""
- }
- }
- return name
-}
-
-// UploadFileToServer upload file to server file dir not git
-func UploadFileToServer(ctx *context.Context) {
- file, header, err := ctx.Req.FormFile("file")
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
- return
- }
- defer file.Close()
-
- buf := make([]byte, 1024)
- n, _ := util.ReadAtMost(file, buf)
- if n > 0 {
- buf = buf[:n]
- }
-
- err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
- if err != nil {
- ctx.HTTPError(http.StatusBadRequest, err.Error())
- return
- }
-
- name := cleanUploadFileName(header.Filename)
- if len(name) == 0 {
- ctx.HTTPError(http.StatusInternalServerError, "Upload file name is invalid")
- return
- }
-
- upload, err := repo_model.NewUpload(ctx, name, buf, file)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
- return
- }
-
- log.Trace("New file uploaded: %s", upload.UUID)
- ctx.JSON(http.StatusOK, map[string]string{
- "uuid": upload.UUID,
+ defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/"))
+ err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: ctx.Repo.BranchName,
+ NewBranch: parsed.TargetBranchName,
+ TreePath: parsed.form.TreePath,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Files: parsed.form.Files,
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
})
-}
-
-// RemoveUploadFileFromServer remove file from server file dir
-func RemoveUploadFileFromServer(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
- if len(form.File) == 0 {
- ctx.Status(http.StatusNoContent)
- return
- }
-
- if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
return
}
-
- log.Trace("Upload file removed: %s", form.File)
- ctx.Status(http.StatusNoContent)
-}
-
-// GetUniquePatchBranchName Gets a unique branch name for a new patch branch
-// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
-// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
-// type in the branch name themselves (will be an empty field)
-func GetUniquePatchBranchName(ctx *context.Context) string {
- prefix := ctx.Doer.LowerName + "-patch-"
- for i := 1; i <= 1000; i++ {
- branchName := fmt.Sprintf("%s%d", prefix, i)
- if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err != nil {
- log.Error("GetUniquePatchBranchName: %v", err)
- return ""
- } else if !exist {
- return branchName
- }
- }
- return ""
-}
-
-// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is
-// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a
-// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing.
-func GetClosestParentWithFiles(treePath string, commit *git.Commit) string {
- if len(treePath) == 0 || treePath == "." {
- return ""
- }
- // see if the tree has entries
- if tree, err := commit.SubTree(treePath); err != nil {
- // failed to get tree, going up a dir
- return GetClosestParentWithFiles(path.Dir(treePath), commit)
- } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
- // no files in this dir, going up a dir
- return GetClosestParentWithFiles(path.Dir(treePath), commit)
- }
- return treePath
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
}
diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go
new file mode 100644
index 0000000000..9fd7a9468b
--- /dev/null
+++ b/routers/web/repo/editor_apply_patch.go
@@ -0,0 +1,51 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func NewDiffPatch(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_diffpatch")
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Data["PageIsPatch"] = true
+ ctx.HTML(http.StatusOK, tplPatchFile)
+}
+
+// NewDiffPatchPost response for sending patch page
+func NewDiffPatchPost(ctx *context.Context) {
+ parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
+ if ctx.Written() {
+ return
+ }
+
+ defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
+ _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: ctx.Repo.BranchName,
+ NewBranch: parsed.TargetBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
+ if err != nil {
+ err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
+ }
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
+ return
+ }
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
+}
diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go
new file mode 100644
index 0000000000..4c93d610cc
--- /dev/null
+++ b/routers/web/repo/editor_cherry_pick.go
@@ -0,0 +1,86 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "bytes"
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func CherryPick(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_cherrypick")
+ if ctx.Written() {
+ return
+ }
+
+ fromCommitID := ctx.PathParam("sha")
+ ctx.Data["FromCommitID"] = fromCommitID
+ cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID)
+ if err != nil {
+ HandleGitError(ctx, "GetCommit", err)
+ return
+ }
+
+ if ctx.FormString("cherry-pick-type") == "revert" {
+ ctx.Data["CherryPickType"] = "revert"
+ ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
+ ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
+ } else {
+ ctx.Data["CherryPickType"] = "cherry-pick"
+ splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
+ ctx.Data["commit_summary"] = splits[0]
+ ctx.Data["commit_message"] = splits[1]
+ }
+
+ ctx.HTML(http.StatusOK, tplCherryPick)
+}
+
+func CherryPickPost(ctx *context.Context) {
+ fromCommitID := ctx.PathParam("sha")
+ parsed := parseEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
+ if ctx.Written() {
+ return
+ }
+
+ defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID))
+ opts := &files.ApplyDiffPatchOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: ctx.Repo.BranchName,
+ NewBranch: parsed.TargetBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ }
+
+ // First try the simple plain read-tree -m approach
+ opts.Content = fromCommitID
+ if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, parsed.form.Revert, opts); err != nil {
+ // Drop through to the "apply" method
+ buf := &bytes.Buffer{}
+ if parsed.form.Revert {
+ err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf)
+ } else {
+ err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf)
+ }
+ if err == nil {
+ opts.Content = buf.String()
+ _, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
+ if err != nil {
+ err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
+ }
+ }
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
+ return
+ }
+ }
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
+}
diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go
new file mode 100644
index 0000000000..245226a039
--- /dev/null
+++ b/routers/web/repo/editor_error.go
@@ -0,0 +1,82 @@
+// Copyright 2025 Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+
+ git_model "code.gitea.io/gitea/models/git"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/utils"
+ context_service "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+func errorAs[T error](v error) (e T, ok bool) {
+ if errors.As(v, &e) {
+ return e, true
+ }
+ return e, false
+}
+
+func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, summary, details string) {
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
+ "Message": message,
+ "Summary": summary,
+ "Details": utils.SanitizeFlashErrorString(details),
+ })
+ if err == nil {
+ ctx.JSONError(flashError)
+ } else {
+ log.Error("RenderToHTML: %v", err)
+ ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details))
+ }
+}
+
+func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
+ if errAs := util.ErrorAsLocale(err); errAs != nil {
+ ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
+ } else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))
+ } else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", errAs.Path, errAs.UserName))
+ } else if errAs, ok := errorAs[files_service.ErrFilenameInvalid](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
+ } else if errAs, ok := errorAs[files_service.ErrFilePathInvalid](err); ok {
+ switch errAs.Type {
+ case git.EntryModeSymlink:
+ ctx.JSONError(ctx.Tr("repo.editor.file_is_a_symlink", errAs.Path))
+ case git.EntryModeTree:
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_a_directory", errAs.Path))
+ case git.EntryModeBlob:
+ ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", errAs.Path))
+ default:
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
+ }
+ } else if errAs, ok := errorAs[files_service.ErrRepoFileAlreadyExists](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path))
+ } else if errAs, ok := errorAs[git.ErrBranchNotExist](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name))
+ } else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName))
+ } else if files_service.IsErrCommitIDDoesNotMatch(err) {
+ ctx.JSONError(ctx.Tr("repo.editor.commit_id_not_matching"))
+ } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
+ ctx.JSONError(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(targetBranchName)))
+ } else if errAs, ok := errorAs[*git.ErrPushRejected](err); ok {
+ if errAs.Message == "" {
+ ctx.JSONError(ctx.Tr("repo.editor.push_rejected_no_message"))
+ } else {
+ editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.push_rejected"), ctx.Locale.TrString("repo.editor.push_rejected_summary"), errAs.Message)
+ }
+ } else if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONError(ctx.Tr("error.not_found"))
+ } else {
+ setting.PanicInDevOrTesting("unclear err %T: %v", err, err)
+ editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error())
+ }
+}
diff --git a/routers/web/repo/editor_preview.go b/routers/web/repo/editor_preview.go
new file mode 100644
index 0000000000..14be5b72b6
--- /dev/null
+++ b/routers/web/repo/editor_preview.go
@@ -0,0 +1,41 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+func DiffPreviewPost(ctx *context.Context) {
+ content := ctx.FormString("content")
+ treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
+ if treePath == "" {
+ ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid")
+ return
+ }
+
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
+ if err != nil {
+ ctx.ServerError("GetTreeEntryByPath", err)
+ return
+ } else if entry.IsDir() {
+ ctx.HTTPError(http.StatusUnprocessableEntity)
+ return
+ }
+
+ diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content)
+ if err != nil {
+ ctx.ServerError("GetDiffPreview", err)
+ return
+ }
+
+ if len(diff.Files) != 0 {
+ ctx.Data["File"] = diff.Files[0]
+ }
+
+ ctx.HTML(http.StatusOK, tplEditDiffPreview)
+}
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index 89bf8f309c..6e2c1d6219 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -6,76 +6,27 @@ package repo
import (
"testing"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
-func TestCleanUploadName(t *testing.T) {
+func TestEditorUtils(t *testing.T) {
unittest.PrepareTestEnv(t)
-
- kases := map[string]string{
- ".git/refs/master": "",
- "/root/abc": "root/abc",
- "./../../abc": "abc",
- "a/../.git": "",
- "a/../../../abc": "abc",
- "../../../acd": "acd",
- "../../.git/abc": "",
- "..\\..\\.git/abc": "..\\..\\.git/abc",
- "..\\../.git/abc": "",
- "..\\../.git": "",
- "abc/../def": "def",
- ".drone.yml": ".drone.yml",
- ".abc/def/.drone.yml": ".abc/def/.drone.yml",
- "..drone.yml.": "..drone.yml.",
- "..a.dotty...name...": "..a.dotty...name...",
- "..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
- }
- for k, v := range kases {
- assert.Equal(t, cleanUploadFileName(k), v)
- }
-}
-
-func TestGetUniquePatchBranchName(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- expectedBranchName := "user2-patch-1"
- branchName := GetUniquePatchBranchName(ctx)
- assert.Equal(t, expectedBranchName, branchName)
-}
-
-func TestGetClosestParentWithFiles(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- repo := ctx.Repo.Repository
- branch := repo.DefaultBranch
- gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
- defer gitRepo.Close()
- commit, _ := gitRepo.GetBranchCommit(branch)
- var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo
- for _, deletedFile := range []string{
- "dir1/dir2/dir3/file.txt",
- "file.txt",
- } {
- treePath := GetClosestParentWithFiles(deletedFile, commit)
- assert.Equal(t, expectedTreePath, treePath)
- }
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ t.Run("getUniquePatchBranchName", func(t *testing.T) {
+ branchName := getUniquePatchBranchName(t.Context(), "user2", repo)
+ assert.Equal(t, "user2-patch-1", branchName)
+ })
+ t.Run("getClosestParentWithFiles", func(t *testing.T) {
+ gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
+ defer gitRepo.Close()
+ treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar")
+ assert.Equal(t, "docs", treePath)
+ treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other")
+ assert.Empty(t, treePath)
+ })
}
diff --git a/routers/web/repo/editor_uploader.go b/routers/web/repo/editor_uploader.go
new file mode 100644
index 0000000000..1ce9a1aca4
--- /dev/null
+++ b/routers/web/repo/editor_uploader.go
@@ -0,0 +1,61 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+// UploadFileToServer upload file to server file dir not git
+func UploadFileToServer(ctx *context.Context) {
+ file, header, err := ctx.Req.FormFile("file")
+ if err != nil {
+ ctx.ServerError("FormFile", err)
+ return
+ }
+ defer file.Close()
+
+ buf := make([]byte, 1024)
+ n, _ := util.ReadAtMost(file, buf)
+ if n > 0 {
+ buf = buf[:n]
+ }
+
+ err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
+ if err != nil {
+ ctx.HTTPError(http.StatusBadRequest, err.Error())
+ return
+ }
+
+ name := files_service.CleanGitTreePath(header.Filename)
+ if len(name) == 0 {
+ ctx.HTTPError(http.StatusBadRequest, "Upload file name is invalid")
+ return
+ }
+
+ uploaded, err := repo_model.NewUpload(ctx, name, buf, file)
+ if err != nil {
+ ctx.ServerError("NewUpload", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]string{"uuid": uploaded.UUID})
+}
+
+// RemoveUploadFileFromServer remove file from server file dir
+func RemoveUploadFileFromServer(ctx *context.Context) {
+ fileUUID := ctx.FormString("file")
+ if err := repo_model.DeleteUploadByUUID(ctx, fileUUID); err != nil {
+ ctx.ServerError("DeleteUploadByUUID", err)
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go
new file mode 100644
index 0000000000..8744b4479e
--- /dev/null
+++ b/routers/web/repo/editor_util.go
@@ -0,0 +1,85 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "context"
+ "fmt"
+ "path"
+ "strings"
+
+ git_model "code.gitea.io/gitea/models/git"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ context_service "code.gitea.io/gitea/services/context"
+)
+
+// getUniquePatchBranchName Gets a unique branch name for a new patch branch
+// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
+// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
+// type in the branch name themselves (will be an empty field)
+func getUniquePatchBranchName(ctx context.Context, prefixName string, repo *repo_model.Repository) string {
+ prefix := prefixName + "-patch-"
+ for i := 1; i <= 1000; i++ {
+ branchName := fmt.Sprintf("%s%d", prefix, i)
+ if exist, err := git_model.IsBranchExist(ctx, repo.ID, branchName); err != nil {
+ log.Error("getUniquePatchBranchName: %v", err)
+ return ""
+ } else if !exist {
+ return branchName
+ }
+ }
+ return ""
+}
+
+// getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is
+// deleted. It returns "" for the tree root if no parents other than the root have files.
+func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string {
+ var f func(treePath string, commit *git.Commit) string
+ f = func(treePath string, commit *git.Commit) string {
+ if treePath == "" || treePath == "." {
+ return ""
+ }
+ // see if the tree has entries
+ if tree, err := commit.SubTree(treePath); err != nil {
+ return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir
+ } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
+ return f(path.Dir(treePath), commit) // no files in this dir, going up a dir
+ }
+ return treePath
+ }
+ commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change
+ if err != nil {
+ log.Error("GetBranchCommit: %v", err)
+ return ""
+ }
+ return f(originTreePath, commit)
+}
+
+// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null"
+func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string {
+ ec, _, err := ctx.Repo.GetEditorconfig()
+ if err == nil {
+ def, err := ec.GetDefinitionForFilename(treePath)
+ if err == nil {
+ jsonStr, _ := json.Marshal(def)
+ return string(jsonStr)
+ }
+ }
+ return "null"
+}
+
+// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath.
+// eg: []{"a", "b", "c"}, []{"a", "a/b", "a/b/c"}
+// or: []{""}, []{""} for the root treePath
+func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
+ treeNames = strings.Split(treePath, "/")
+ treePaths = make([]string, len(treeNames))
+ for i := range treeNames {
+ treePaths[i] = strings.Join(treeNames[:i+1], "/")
+ }
+ return treeNames, treePaths
+}
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
deleted file mode 100644
index 3ffd8f89c4..0000000000
--- a/routers/web/repo/patch.go
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repo
-
-import (
- "net/http"
- "strings"
-
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/repository/files"
-)
-
-const (
- tplPatchFile templates.TplName = "repo/editor/patch"
-)
-
-// NewDiffPatch render create patch page
-func NewDiffPatch(ctx *context.Context) {
- canCommit := renderCommitRights(ctx)
-
- ctx.Data["PageIsPatch"] = true
-
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- ctx.HTML(http.StatusOK, tplPatchFile)
-}
-
-// NewDiffPatchPost response for sending patch page
-func NewDiffPatchPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
-
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
- ctx.Data["PageIsPatch"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["FileContent"] = form.Content
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplPatchFile)
- return
- }
-
- // Cannot commit to an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
- return
- }
-
- // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
- // `message` will be both the summary and message combined
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- message = ctx.Locale.TrString("repo.editor.patch")
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form)
- return
- }
-
- fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- Content: strings.ReplaceAll(form.Content.Value(), "\r", ""),
- Author: gitCommitter,
- Committer: gitCommitter,
- })
- if err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
- return
- }
-
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
- ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
- } else {
- ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA)
- }
-}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 68fa17bf07..f0d90f9533 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -394,9 +394,10 @@ func Forks(ctx *context.Context) {
}
pager := context.NewPagination(int(total), pageSize, page, 5)
+ ctx.Data["ShowRepoOwnerAvatar"] = true
+ ctx.Data["ShowRepoOwnerOnList"] = true
ctx.Data["Page"] = pager
-
- ctx.Data["Forks"] = forks
+ ctx.Data["Repos"] = forks
ctx.HTML(http.StatusOK, tplForks)
}
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index f6d50cf5fe..d7052914b6 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -197,6 +197,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
total = int(count)
case "stars":
ctx.Data["PageIsProfileStarList"] = true
+ ctx.Data["ShowRepoOwnerOnList"] = true
repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: pagingNum,
diff --git a/routers/web/web.go b/routers/web/web.go
index a54f96ec68..3040375def 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -964,7 +964,8 @@ func registerWebRoutes(m *web.Router) {
addSettingsVariablesRoutes()
}, actions.MustEnableActions)
- m.Methods("GET,POST", "/delete", org.SettingsDelete)
+ m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost)
+ m.Post("/delete", org.SettingsDeleteOrgPost)
m.Group("/packages", func() {
m.Get("", org.Packages)
@@ -1315,11 +1316,11 @@ func registerWebRoutes(m *web.Router) {
m.Group("/{username}/{reponame}", func() { // repo code
m.Group("", func() {
m.Group("", func() {
- m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
- m.Combo("/_edit/*").Get(repo.EditFile).
+ m.Post("/_preview/*", repo.DiffPreviewPost)
+ m.Combo("/{editor_action:_edit}/*").Get(repo.EditFile).
+ Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
+ m.Combo("/{editor_action:_new}/*").Get(repo.EditFile).
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
- m.Combo("/_new/*").Get(repo.NewFile).
- Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost)
m.Combo("/_delete/*").Get(repo.DeleteFile).
Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile).
@@ -1331,7 +1332,7 @@ func registerWebRoutes(m *web.Router) {
}, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData)
m.Group("", func() {
m.Post("/upload-file", repo.UploadFileToServer)
- m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
+ m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
}, repo.MustBeAbleToUpload, reqRepoCodeWriter)
}, repo.MustBeEditable, context.RepoMustNotBeArchived())
diff --git a/services/context/repo.go b/services/context/repo.go
index 32d54c88ff..c28ae7e8fd 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -94,24 +94,22 @@ func RepoMustNotBeArchived() func(ctx *Context) {
}
}
-// CanCommitToBranchResults represents the results of CanCommitToBranch
-type CanCommitToBranchResults struct {
- CanCommitToBranch bool
- EditorEnabled bool
- UserCanPush bool
- RequireSigned bool
- WillSign bool
- SigningKey *git.SigningKey
- WontSignReason string
+type CommitFormBehaviors struct {
+ CanCommitToBranch bool
+ EditorEnabled bool
+ UserCanPush bool
+ RequireSigned bool
+ WillSign bool
+ SigningKey *git.SigningKey
+ WontSignReason string
+ CanCreatePullRequest bool
+ CanCreateBasePullRequest bool
}
-// CanCommitToBranch returns true if repository is editable and user has proper access level
-//
-// and branch is not protected for push
-func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) {
+func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) {
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
if err != nil {
- return CanCommitToBranchResults{}, err
+ return nil, err
}
userCanPush := true
requireSigned := false
@@ -138,7 +136,10 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use
}
}
- return CanCommitToBranchResults{
+ canCreateBasePullRequest := ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
+ canCreatePullRequest := ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
+
+ return &CommitFormBehaviors{
CanCommitToBranch: canCommit,
EditorEnabled: canEnableEditor,
UserCanPush: userCanPush,
@@ -146,6 +147,9 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use
WillSign: sign,
SigningKey: keyID,
WontSignReason: wontSignReason,
+
+ CanCreatePullRequest: canCreatePullRequest,
+ CanCreateBasePullRequest: canCreateBasePullRequest,
}, err
}
diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go
index 5edddc6f27..303e7da38b 100644
--- a/services/context/upload/upload.go
+++ b/services/context/upload/upload.go
@@ -113,5 +113,7 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
+ default:
+ setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType)
}
}
diff --git a/services/forms/org.go b/services/forms/org.go
index db182f7e96..2ac18ef25c 100644
--- a/services/forms/org.go
+++ b/services/forms/org.go
@@ -36,7 +36,6 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding
// UpdateOrgSettingForm form for updating organization settings
type UpdateOrgSettingForm struct {
- Name string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"`
FullName string `binding:"MaxSize(100)"`
Email string `binding:"MaxSize(255)"`
Description string `binding:"MaxSize(255)"`
@@ -53,6 +52,11 @@ func (f *UpdateOrgSettingForm) Validate(req *http.Request, errs binding.Errors)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
+type RenameOrgForm struct {
+ OrgName string `binding:"Required"`
+ NewOrgName string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"`
+}
+
// ___________
// \__ ___/___ _____ _____
// | |_/ __ \\__ \ / \
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index c79d3b95e7..d116bb9f11 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -10,7 +10,6 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
- "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context"
@@ -681,129 +680,6 @@ func (f *NewWikiForm) Validate(req *http.Request, errs binding.Errors) binding.E
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
-// ___________ .___.__ __
-// \_ _____/ __| _/|__|/ |_
-// | __)_ / __ | | \ __\
-// | \/ /_/ | | || |
-// /_______ /\____ | |__||__|
-// \/ \/
-
-// EditRepoFileForm form for changing repository file
-type EditRepoFileForm struct {
- TreePath string `binding:"Required;MaxSize(500)"`
- Content optional.Option[string]
- CommitSummary string `binding:"MaxSize(100)"`
- CommitMessage string
- CommitChoice string `binding:"Required;MaxSize(50)"`
- NewBranchName string `binding:"GitRefName;MaxSize(100)"`
- LastCommit string
- Signoff bool
- CommitEmail string
-}
-
-// Validate validates the fields
-func (f *EditRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetValidateContext(req)
- return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
-}
-
-// EditPreviewDiffForm form for changing preview diff
-type EditPreviewDiffForm struct {
- Content string
-}
-
-// Validate validates the fields
-func (f *EditPreviewDiffForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetValidateContext(req)
- return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
-}
-
-// _________ .__ __________.__ __
-// \_ ___ \| |__ __________________ ___.__. \______ \__| ____ | | __
-// / \ \/| | \_/ __ \_ __ \_ __ < | | | ___/ |/ ___\| |/ /
-// \ \___| Y \ ___/| | \/| | \/\___ | | | | \ \___| <
-// \______ /___| /\___ >__| |__| / ____| |____| |__|\___ >__|_ \
-// \/ \/ \/ \/ \/ \/
-
-// CherryPickForm form for changing repository file
-type CherryPickForm struct {
- CommitSummary string `binding:"MaxSize(100)"`
- CommitMessage string
- CommitChoice string `binding:"Required;MaxSize(50)"`
- NewBranchName string `binding:"GitRefName;MaxSize(100)"`
- LastCommit string
- Revert bool
- Signoff bool
- CommitEmail string
-}
-
-// Validate validates the fields
-func (f *CherryPickForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetValidateContext(req)
- return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
-}
-
-// ____ ___ .__ .___
-// | | \______ | | _________ __| _/
-// | | /\____ \| | / _ \__ \ / __ |
-// | | / | |_> > |_( <_> ) __ \_/ /_/ |
-// |______/ | __/|____/\____(____ /\____ |
-// |__| \/ \/
-//
-
-// UploadRepoFileForm form for uploading repository file
-type UploadRepoFileForm struct {
- TreePath string `binding:"MaxSize(500)"`
- CommitSummary string `binding:"MaxSize(100)"`
- CommitMessage string
- CommitChoice string `binding:"Required;MaxSize(50)"`
- NewBranchName string `binding:"GitRefName;MaxSize(100)"`
- Files []string
- Signoff bool
- CommitEmail string
-}
-
-// Validate validates the fields
-func (f *UploadRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetValidateContext(req)
- return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
-}
-
-// RemoveUploadFileForm form for removing uploaded file
-type RemoveUploadFileForm struct {
- File string `binding:"Required;MaxSize(50)"`
-}
-
-// Validate validates the fields
-func (f *RemoveUploadFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetValidateContext(req)
- return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
-}
-
-// ________ .__ __
-// \______ \ ____ | | _____/ |_ ____
-// | | \_/ __ \| | _/ __ \ __\/ __ \
-// | ` \ ___/| |_\ ___/| | \ ___/
-// /_______ /\___ >____/\___ >__| \___ >
-// \/ \/ \/ \/
-
-// DeleteRepoFileForm form for deleting repository file
-type DeleteRepoFileForm struct {
- CommitSummary string `binding:"MaxSize(100)"`
- CommitMessage string
- CommitChoice string `binding:"Required;MaxSize(50)"`
- NewBranchName string `binding:"GitRefName;MaxSize(100)"`
- LastCommit string
- Signoff bool
- CommitEmail string
-}
-
-// Validate validates the fields
-func (f *DeleteRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetValidateContext(req)
- return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
-}
-
// ___________.__ ___________ __
// \__ ___/|__| _____ ____ \__ ___/___________ ____ | | __ ___________
// | | | |/ \_/ __ \ | | \_ __ \__ \ _/ ___\| |/ // __ \_ __ \
diff --git a/services/forms/repo_form_editor.go b/services/forms/repo_form_editor.go
new file mode 100644
index 0000000000..3ad2eae75d
--- /dev/null
+++ b/services/forms/repo_form_editor.go
@@ -0,0 +1,57 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forms
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
+
+ "gitea.com/go-chi/binding"
+)
+
+type CommitCommonForm struct {
+ TreePath string `binding:"MaxSize(500)"`
+ CommitSummary string `binding:"MaxSize(100)"`
+ CommitMessage string
+ CommitChoice string `binding:"Required;MaxSize(50)"`
+ NewBranchName string `binding:"GitRefName;MaxSize(100)"`
+ LastCommit string
+ Signoff bool
+ CommitEmail string
+}
+
+func (f *CommitCommonForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+ ctx := context.GetValidateContext(req)
+ return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
+
+type CommitCommonFormInterface interface {
+ GetCommitCommonForm() *CommitCommonForm
+}
+
+func (f *CommitCommonForm) GetCommitCommonForm() *CommitCommonForm {
+ return f
+}
+
+type EditRepoFileForm struct {
+ CommitCommonForm
+ Content optional.Option[string]
+}
+
+type DeleteRepoFileForm struct {
+ CommitCommonForm
+}
+
+type UploadRepoFileForm struct {
+ CommitCommonForm
+ Files []string
+}
+
+type CherryPickForm struct {
+ CommitCommonForm
+ Revert bool
+}
diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go
index 7a47cf3876..b15949f352 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -528,7 +528,7 @@ func TestEmbedBase64Images(t *testing.T) {
require.NoError(t, err)
mailBody := msgs[0].Body
- assert.Regexp(t, `MSG-BEFORE <a[^>]+><img src="data:image/png;base64,iVBORw0KGgo="/></a> MSG-AFTER`, mailBody)
+ assert.Regexp(t, `MSG-BEFORE <a[^>]+><img src="data:image/png;base64,iVBORw0KGgo=".*/></a> MSG-AFTER`, mailBody)
})
t.Run("EmbedInstanceImageSkipExternalImage", func(t *testing.T) {
diff --git a/services/repository/files/content.go b/services/repository/files/content.go
index 7a07a0ddca..ccba3b7594 100644
--- a/services/repository/files/content.go
+++ b/services/repository/files/content.go
@@ -42,7 +42,7 @@ func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, refComm
}
// Check that the path given in opts.treePath is valid (not a git path)
- cleanTreePath := CleanUploadFileName(treePath)
+ cleanTreePath := CleanGitTreePath(treePath)
if cleanTreePath == "" && treePath != "" {
return nil, ErrFilenameInvalid{
Path: treePath,
@@ -103,7 +103,7 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
// GetContents gets the metadata on a file's contents. Ref can be a branch, commit or tag
func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePath string, forList bool) (*api.ContentsResponse, error) {
// Check that the path given in opts.treePath is valid (not a git path)
- cleanTreePath := CleanUploadFileName(treePath)
+ cleanTreePath := CleanGitTreePath(treePath)
if cleanTreePath == "" && treePath != "" {
return nil, ErrFilenameInvalid{
Path: treePath,
diff --git a/services/repository/files/file.go b/services/repository/files/file.go
index 0e1100a098..855dc5c8ed 100644
--- a/services/repository/files/file.go
+++ b/services/repository/files/file.go
@@ -134,9 +134,8 @@ func (err ErrFilenameInvalid) Unwrap() error {
return util.ErrInvalidArgument
}
-// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
-func CleanUploadFileName(name string) string {
- // Rebase the filename
+// CleanGitTreePath cleans a tree path for git, it returns an empty string the path is invalid (e.g.: contains ".git" part)
+func CleanGitTreePath(name string) string {
name = util.PathJoinRel(name)
// Git disallows any filenames to have a .git directory in them.
for part := range strings.SplitSeq(name, "/") {
@@ -144,5 +143,8 @@ func CleanUploadFileName(name string) string {
return ""
}
}
+ if name == "." {
+ name = ""
+ }
return name
}
diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go
index 169cafba0d..894c184472 100644
--- a/services/repository/files/file_test.go
+++ b/services/repository/files/file_test.go
@@ -10,17 +10,9 @@ import (
)
func TestCleanUploadFileName(t *testing.T) {
- t.Run("Clean regular file", func(t *testing.T) {
- name := "this/is/test"
- cleanName := CleanUploadFileName(name)
- expectedCleanName := name
- assert.Equal(t, expectedCleanName, cleanName)
- })
-
- t.Run("Clean a .git path", func(t *testing.T) {
- name := "this/is/test/.git"
- cleanName := CleanUploadFileName(name)
- expectedCleanName := ""
- assert.Equal(t, expectedCleanName, cleanName)
- })
+ assert.Equal(t, "", CleanGitTreePath("")) //nolint
+ assert.Equal(t, "", CleanGitTreePath(".")) //nolint
+ assert.Equal(t, "a/b", CleanGitTreePath("a/b"))
+ assert.Equal(t, "", CleanGitTreePath(".git/b")) //nolint
+ assert.Equal(t, "", CleanGitTreePath("a/.git")) //nolint
}
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index 99c1215c9f..5aaa394e9a 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -88,8 +88,26 @@ func (err ErrRepoFileDoesNotExist) Unwrap() error {
return util.ErrNotExist
}
+type LazyReadSeeker interface {
+ io.ReadSeeker
+ io.Closer
+ OpenLazyReader() error
+}
+
// ChangeRepoFiles adds, updates or removes multiple files in the given repository
-func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (*structs.FilesResponse, error) {
+func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (_ *structs.FilesResponse, errRet error) {
+ var addedLfsPointers []lfs.Pointer
+ defer func() {
+ if errRet != nil {
+ for _, lfsPointer := range addedLfsPointers {
+ _, err := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, lfsPointer.Oid)
+ if err != nil {
+ log.Error("ChangeRepoFiles: RemoveLFSMetaObjectByOid failed: %v", err)
+ }
+ }
+ }
+ }()
+
err := repo.MustNotBeArchived()
if err != nil {
return nil, err
@@ -127,14 +145,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
// Check that the path given in opts.treePath is valid (not a git path)
- treePath := CleanUploadFileName(file.TreePath)
+ treePath := CleanGitTreePath(file.TreePath)
if treePath == "" {
return nil, ErrFilenameInvalid{
Path: file.TreePath,
}
}
// If there is a fromTreePath (we are copying it), also clean it up
- fromTreePath := CleanUploadFileName(file.FromTreePath)
+ fromTreePath := CleanGitTreePath(file.FromTreePath)
if fromTreePath == "" && file.FromTreePath != "" {
return nil, ErrFilenameInvalid{
Path: file.FromTreePath,
@@ -241,10 +259,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
lfsContentStore := lfs.NewContentStore()
for _, file := range opts.Files {
switch file.Operation {
- case "create", "update", "rename":
- if err = CreateUpdateRenameFile(ctx, t, file, lfsContentStore, repo.ID, hasOldBranch); err != nil {
+ case "create", "update", "rename", "upload":
+ addedLfsPointer, err := modifyFile(ctx, t, file, lfsContentStore, repo.ID)
+ if err != nil {
return nil, err
}
+ if addedLfsPointer != nil {
+ addedLfsPointers = append(addedLfsPointers, *addedLfsPointer)
+ }
case "delete":
if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil {
return nil, err
@@ -366,18 +388,29 @@ func (err ErrSHAOrCommitIDNotProvided) Error() string {
// handles the check for various issues for ChangeRepoFiles
func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error {
- if file.Operation == "update" || file.Operation == "delete" || file.Operation == "rename" {
- fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
- if err != nil {
- return err
+ // check old entry (fromTreePath/fromEntry)
+ if file.Operation == "update" || file.Operation == "upload" || file.Operation == "delete" || file.Operation == "rename" {
+ var fromEntryIDString string
+ {
+ fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
+ if file.Operation == "upload" && git.IsErrNotExist(err) {
+ fromEntry = nil
+ } else if err != nil {
+ return err
+ }
+ if fromEntry != nil {
+ fromEntryIDString = fromEntry.ID.String()
+ file.Options.executable = fromEntry.IsExecutable() // FIXME: legacy hacky approach, it shouldn't prepare the "Options" in the "check" function
+ }
}
+
if file.SHA != "" {
// If the SHA given doesn't match the SHA of the fromTreePath, throw error
- if file.SHA != fromEntry.ID.String() {
+ if file.SHA != fromEntryIDString {
return pull_service.ErrSHADoesNotMatch{
Path: file.Options.treePath,
GivenSHA: file.SHA,
- CurrentSHA: fromEntry.ID.String(),
+ CurrentSHA: fromEntryIDString,
}
}
} else if opts.LastCommitID != "" {
@@ -399,11 +432,10 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
// haven't been made. We throw an error if one wasn't provided.
return ErrSHAOrCommitIDNotProvided{}
}
- // FIXME: legacy hacky approach, it shouldn't prepare the "Options" in the "check" function
- file.Options.executable = fromEntry.IsExecutable()
}
- if file.Operation == "create" || file.Operation == "update" || file.Operation == "rename" {
+ // check new entry (treePath/treeEntry)
+ if file.Operation == "create" || file.Operation == "update" || file.Operation == "upload" || file.Operation == "rename" {
// For operation's target path, we need to make sure no parts of the path are existing files or links
// except for the last item in the path (which is the file name).
// And that shouldn't exist IF it is a new file OR is being moved to a new path.
@@ -454,18 +486,23 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
return nil
}
-func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error {
+func modifyFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64) (addedLfsPointer *lfs.Pointer, _ error) {
+ if rd, ok := file.ContentReader.(LazyReadSeeker); ok {
+ if err := rd.OpenLazyReader(); err != nil {
+ return nil, fmt.Errorf("OpenLazyReader: %w", err)
+ }
+ defer rd.Close()
+ }
+
// Get the two paths (might be the same if not moving) from the index if they exist
filesInIndex, err := t.LsFiles(ctx, file.TreePath, file.FromTreePath)
if err != nil {
- return fmt.Errorf("UpdateRepoFile: %w", err)
+ return nil, fmt.Errorf("LsFiles: %w", err)
}
// If is a new file (not updating) then the given path shouldn't exist
if file.Operation == "create" {
if slices.Contains(filesInIndex, file.TreePath) {
- return ErrRepoFileAlreadyExists{
- Path: file.TreePath,
- }
+ return nil, ErrRepoFileAlreadyExists{Path: file.TreePath}
}
}
@@ -474,7 +511,7 @@ func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, f
for _, indexFile := range filesInIndex {
if indexFile == file.Options.fromTreePath {
if err = t.RemoveFilesFromIndex(ctx, file.FromTreePath); err != nil {
- return err
+ return nil, err
}
}
}
@@ -482,45 +519,46 @@ func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, f
var writeObjectRet *writeRepoObjectRet
switch file.Operation {
- case "create", "update":
- writeObjectRet, err = writeRepoObjectForCreateOrUpdate(ctx, t, file)
+ case "create", "update", "upload":
+ writeObjectRet, err = writeRepoObjectForModify(ctx, t, file)
case "rename":
writeObjectRet, err = writeRepoObjectForRename(ctx, t, file)
default:
- return util.NewInvalidArgumentErrorf("unknown file modification operation: '%s'", file.Operation)
+ return nil, util.NewInvalidArgumentErrorf("unknown file modification operation: '%s'", file.Operation)
}
if err != nil {
- return err
+ return nil, err
}
// Add the object to the index, the "file.Options.executable" is set in handleCheckErrors by the caller (legacy hacky approach)
if err = t.AddObjectToIndex(ctx, util.Iif(file.Options.executable, "100755", "100644"), writeObjectRet.ObjectHash, file.Options.treePath); err != nil {
- return err
+ return nil, err
}
if writeObjectRet.LfsContent == nil {
- return nil // No LFS pointer, so nothing to do
+ return nil, nil // No LFS pointer, so nothing to do
}
defer writeObjectRet.LfsContent.Close()
// Now we must store the content into an LFS object
lfsMetaObject, err := git_model.NewLFSMetaObject(ctx, repoID, writeObjectRet.LfsPointer)
if err != nil {
- return err
- }
- if exist, err := contentStore.Exists(lfsMetaObject.Pointer); err != nil {
- return err
- } else if exist {
- return nil
+ return nil, err
}
-
- err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent)
+ exist, err := contentStore.Exists(lfsMetaObject.Pointer)
if err != nil {
- if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil {
- return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, err)
+ return nil, err
+ }
+ if !exist {
+ err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent)
+ if err != nil {
+ if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil {
+ return nil, fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, err)
+ }
+ return nil, err
}
}
- return err
+ return &lfsMetaObject.Pointer, nil
}
func checkIsLfsFileInGitAttributes(ctx context.Context, t *TemporaryUploadRepository, paths []string) (ret []bool, err error) {
@@ -544,8 +582,8 @@ type writeRepoObjectRet struct {
LfsPointer lfs.Pointer
}
-// writeRepoObjectForCreateOrUpdate hashes the git object for create or update operations
-func writeRepoObjectForCreateOrUpdate(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
+// writeRepoObjectForModify hashes the git object for create or update operations
+func writeRepoObjectForModify(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
ret = &writeRepoObjectRet{}
treeObjectContentReader := file.ContentReader
if setting.LFS.StartServer {
@@ -574,7 +612,7 @@ func writeRepoObjectForCreateOrUpdate(ctx context.Context, t *TemporaryUploadRep
return ret, nil
}
-// writeRepoObjectForRename the same as writeRepoObjectForCreateOrUpdate buf for "rename"
+// writeRepoObjectForRename the same as writeRepoObjectForModify buf for "rename"
func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
lastCommitID, err := t.GetLastCommit(ctx)
if err != nil {
diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go
index b004e3cc4c..b783cbd01d 100644
--- a/services/repository/files/upload.go
+++ b/services/repository/files/upload.go
@@ -8,15 +8,11 @@ import (
"fmt"
"os"
"path"
- "strings"
+ "sync"
- git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/git/attribute"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/log"
)
// UploadRepoFileOptions contains the uploaded repository file options
@@ -32,208 +28,84 @@ type UploadRepoFileOptions struct {
Committer *IdentityOptions
}
-type uploadInfo struct {
- upload *repo_model.Upload
- lfsMetaObject *git_model.LFSMetaObject
+type lazyLocalFileReader struct {
+ *os.File
+ localFilename string
+ counter int
+ mu sync.Mutex
}
-func cleanUpAfterFailure(ctx context.Context, infos *[]uploadInfo, t *TemporaryUploadRepository, original error) error {
- for _, info := range *infos {
- if info.lfsMetaObject == nil {
- continue
- }
- if !info.lfsMetaObject.Existing {
- if _, err := git_model.RemoveLFSMetaObjectByOid(ctx, t.repo.ID, info.lfsMetaObject.Oid); err != nil {
- original = fmt.Errorf("%w, %v", original, err) // We wrap the original error - as this is the underlying error that required the fallback
- }
- }
- }
- return original
-}
-
-// UploadRepoFiles uploads files to the given repository
-func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UploadRepoFileOptions) error {
- if len(opts.Files) == 0 {
- return nil
- }
+var _ LazyReadSeeker = (*lazyLocalFileReader)(nil)
- uploads, err := repo_model.GetUploadsByUUIDs(ctx, opts.Files)
- if err != nil {
- return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err)
- }
+func (l *lazyLocalFileReader) Close() error {
+ l.mu.Lock()
+ defer l.mu.Unlock()
- names := make([]string, len(uploads))
- infos := make([]uploadInfo, len(uploads))
- for i, upload := range uploads {
- // Check file is not lfs locked, will return nil if lock setting not enabled
- filepath := path.Join(opts.TreePath, upload.Name)
- lfsLock, err := git_model.GetTreePathLock(ctx, repo.ID, filepath)
- if err != nil {
- return err
- }
- if lfsLock != nil && lfsLock.OwnerID != doer.ID {
- u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
- if err != nil {
- return err
+ if l.counter > 0 {
+ l.counter--
+ if l.counter == 0 {
+ if err := l.File.Close(); err != nil {
+ return fmt.Errorf("close file %s: %w", l.localFilename, err)
}
- return git_model.ErrLFSFileLocked{RepoID: repo.ID, Path: filepath, UserName: u.Name}
- }
-
- names[i] = upload.Name
- infos[i] = uploadInfo{upload: upload}
- }
-
- t, err := NewTemporaryUploadRepository(repo)
- if err != nil {
- return err
- }
- defer t.Close()
-
- hasOldBranch := true
- if err = t.Clone(ctx, opts.OldBranch, true); err != nil {
- if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
- return err
- }
- if err = t.Init(ctx, repo.ObjectFormatName); err != nil {
- return err
- }
- hasOldBranch = false
- opts.LastCommitID = ""
- }
- if hasOldBranch {
- if err = t.SetDefaultIndex(ctx); err != nil {
- return err
- }
- }
-
- var attributesMap map[string]*attribute.Attributes
- // when uploading to an empty repo, the old branch doesn't exist, but some "global gitattributes" or "info/attributes" may exist
- if setting.LFS.StartServer {
- attributesMap, err = attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{
- Attributes: []string{attribute.Filter},
- Filenames: names,
- })
- if err != nil {
- return err
+ l.File = nil
}
+ return nil
}
+ return fmt.Errorf("file %s already closed", l.localFilename)
+}
- // Copy uploaded files into repository.
- // TODO: there is a small problem: when uploading LFS files with ".gitattributes", the "check-attr" runs before this loop,
- // so LFS files are not able to be added as LFS objects. Ideally we need to do in 3 steps in the future:
- // 1. Add ".gitattributes" to git index
- // 2. Run "check-attr" (the previous attribute.CheckAttributes call)
- // 3. Add files to git index (this loop)
- // This problem is trivial so maybe no need to spend too much time on it at the moment.
- for i := range infos {
- if err := copyUploadedLFSFileIntoRepository(ctx, &infos[i], attributesMap, t, opts.TreePath); err != nil {
- return err
- }
- }
+func (l *lazyLocalFileReader) OpenLazyReader() error {
+ l.mu.Lock()
+ defer l.mu.Unlock()
- // Now write the tree
- treeHash, err := t.WriteTree(ctx)
- if err != nil {
- return err
+ if l.File != nil {
+ l.counter++
+ return nil
}
- // Now commit the tree
- commitOpts := &CommitTreeUserOptions{
- ParentCommitID: opts.LastCommitID,
- TreeHash: treeHash,
- CommitMessage: opts.Message,
- SignOff: opts.Signoff,
- DoerUser: doer,
- AuthorIdentity: opts.Author,
- CommitterIdentity: opts.Committer,
- }
- commitHash, err := t.CommitTree(ctx, commitOpts)
+ file, err := os.Open(l.localFilename)
if err != nil {
return err
}
+ l.File = file
+ l.counter = 1
+ return nil
+}
- // Now deal with LFS objects
- for i := range infos {
- if infos[i].lfsMetaObject == nil {
- continue
- }
- infos[i].lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, infos[i].lfsMetaObject.RepositoryID, infos[i].lfsMetaObject.Pointer)
- if err != nil {
- // OK Now we need to cleanup
- return cleanUpAfterFailure(ctx, &infos, t, err)
- }
- // Don't move the files yet - we need to ensure that
- // everything can be inserted first
- }
-
- // OK now we can insert the data into the store - there's no way to clean up the store
- // once it's in there, it's in there.
- contentStore := lfs.NewContentStore()
- for _, info := range infos {
- if err := uploadToLFSContentStore(info, contentStore); err != nil {
- return cleanUpAfterFailure(ctx, &infos, t, err)
- }
- }
-
- // Then push this tree to NewBranch
- if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
- return err
+// UploadRepoFiles uploads files to the given repository
+func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UploadRepoFileOptions) error {
+ if len(opts.Files) == 0 {
+ return nil
}
- return repo_model.DeleteUploads(ctx, uploads...)
-}
-
-func copyUploadedLFSFileIntoRepository(ctx context.Context, info *uploadInfo, attributesMap map[string]*attribute.Attributes, t *TemporaryUploadRepository, treePath string) error {
- file, err := os.Open(info.upload.LocalPath())
+ uploads, err := repo_model.GetUploadsByUUIDs(ctx, opts.Files)
if err != nil {
- return err
+ return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err)
}
- defer file.Close()
- var objectHash string
- if setting.LFS.StartServer && attributesMap[info.upload.Name] != nil && attributesMap[info.upload.Name].Get(attribute.Filter).ToString().Value() == "lfs" {
- // Handle LFS
- // FIXME: Inefficient! this should probably happen in models.Upload
- pointer, err := lfs.GeneratePointer(file)
- if err != nil {
- return err
- }
-
- info.lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: t.repo.ID}
-
- if objectHash, err = t.HashObjectAndWrite(ctx, strings.NewReader(pointer.StringContent())); err != nil {
- return err
- }
- } else if objectHash, err = t.HashObjectAndWrite(ctx, file); err != nil {
- return err
+ changeOpts := &ChangeRepoFilesOptions{
+ LastCommitID: opts.LastCommitID,
+ OldBranch: opts.OldBranch,
+ NewBranch: opts.NewBranch,
+ Message: opts.Message,
+ Signoff: opts.Signoff,
+ Author: opts.Author,
+ Committer: opts.Committer,
+ }
+ for _, upload := range uploads {
+ changeOpts.Files = append(changeOpts.Files, &ChangeRepoFile{
+ Operation: "upload",
+ TreePath: path.Join(opts.TreePath, upload.Name),
+ ContentReader: &lazyLocalFileReader{localFilename: upload.LocalPath()},
+ })
}
- // Add the object to the index
- return t.AddObjectToIndex(ctx, "100644", objectHash, path.Join(treePath, info.upload.Name))
-}
-
-func uploadToLFSContentStore(info uploadInfo, contentStore *lfs.ContentStore) error {
- if info.lfsMetaObject == nil {
- return nil
- }
- exist, err := contentStore.Exists(info.lfsMetaObject.Pointer)
+ _, err = ChangeRepoFiles(ctx, repo, doer, changeOpts)
if err != nil {
return err
}
- if !exist {
- file, err := os.Open(info.upload.LocalPath())
- if err != nil {
- return err
- }
-
- defer file.Close()
- // FIXME: Put regenerates the hash and copies the file over.
- // I guess this strictly ensures the soundness of the store but this is inefficient.
- if err := contentStore.Put(info.lfsMetaObject.Pointer, file); err != nil {
- // OK Now we need to cleanup
- // Can't clean up the store, once uploaded there they're there.
- return err
- }
+ if err := repo_model.DeleteUploads(ctx, uploads...); err != nil {
+ log.Error("DeleteUploads: %v", err)
}
return nil
}
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index c7d2309ac4..b6ee80c44c 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -5,9 +5,13 @@ package webhook
import (
"context"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
"fmt"
"net/http"
"strings"
+ "time"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
@@ -16,10 +20,12 @@ import (
)
type (
- // FeishuPayload represents
+ // FeishuPayload represents the payload for Feishu webhook
FeishuPayload struct {
- MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
- Content struct {
+ Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp for signature verification
+ Sign string `json:"sign,omitempty"` // Signature for verification
+ MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
+ Content struct {
Text string `json:"text"`
} `json:"content"`
}
@@ -184,9 +190,29 @@ func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, er
return newFeishuTextPayload(text), nil
}
+// feishuGenSign generates a signature for Feishu webhook
+// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
+func feishuGenSign(secret string, timestamp int64) string {
+ // key="{timestamp}\n{secret}", then hmac-sha256, then base64 encode
+ stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
+ h := hmac.New(sha256.New, []byte(stringToSign))
+ return base64.StdEncoding.EncodeToString(h.Sum(nil))
+}
+
func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
- var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
- return newJSONRequest(pc, w, t, true)
+ payload, err := newPayload(feishuConvertor{}, []byte(t.PayloadContent), t.EventType)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Add timestamp and signature if secret is provided
+ if w.Secret != "" {
+ timestamp := time.Now().Unix()
+ payload.Timestamp = timestamp
+ payload.Sign = feishuGenSign(w.Secret, timestamp)
+ }
+
+ return prepareJSONRequest(payload, w, t, false /* no default headers */)
}
func init() {
diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go
index c4249bdb30..7e200ea132 100644
--- a/services/webhook/feishu_test.go
+++ b/services/webhook/feishu_test.go
@@ -168,6 +168,7 @@ func TestFeishuJSONPayload(t *testing.T) {
URL: "https://feishu.example.com/",
Meta: `{}`,
HTTPMethod: "POST",
+ Secret: "secret",
}
task := &webhook_model.HookTask{
HookID: hook.ID,
@@ -183,10 +184,13 @@ func TestFeishuJSONPayload(t *testing.T) {
assert.Equal(t, "POST", req.Method)
assert.Equal(t, "https://feishu.example.com/", req.URL.String())
- assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
var body FeishuPayload
err = json.NewDecoder(req.Body).Decode(&body)
assert.NoError(t, err)
assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text)
+ assert.Equal(t, feishuGenSign(hook.Secret, body.Timestamp), body.Sign)
+
+ // a separate sign test, the result is generated by official python code, so the algo must be correct
+ assert.Equal(t, "rWZ84lcag1x9aBFhn1gtV4ZN+4gme3pilfQNMk86vKg=", feishuGenSign("a", 1))
}
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index c25d700c23..b607bf3250 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -95,7 +95,10 @@ func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *
if err != nil {
return nil, nil, err
}
+ return prepareJSONRequest(payload, w, t, withDefaultHeaders)
+}
+func prepareJSONRequest[T any](payload T, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
body, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return nil, nil, err
diff --git a/templates/admin/user/view.tmpl b/templates/admin/user/view.tmpl
index 31616ffbf9..67f9148e64 100644
--- a/templates/admin/user/view.tmpl
+++ b/templates/admin/user/view.tmpl
@@ -26,7 +26,7 @@
{{ctx.Locale.Tr "admin.repositories"}} ({{ctx.Locale.Tr "admin.total" .ReposTotal}})
</h4>
<div class="ui attached segment">
- {{template "explore/repo_list" .}}
+ {{template "shared/repo/list" .}}
</div>
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.organization"}} ({{ctx.Locale.Tr "admin.total" .OrgsTotal}})
diff --git a/templates/explore/repos.tmpl b/templates/explore/repos.tmpl
index 53742bf0d9..68da398306 100644
--- a/templates/explore/repos.tmpl
+++ b/templates/explore/repos.tmpl
@@ -2,8 +2,8 @@
<div role="main" aria-label="{{.Title}}" class="page-content explore repositories">
{{template "explore/navbar" .}}
<div class="ui container">
- {{template "shared/repo_search" .}}
- {{template "explore/repo_list" .}}
+ {{template "shared/repo/search" .}}
+ {{template "shared/repo/list" .}}
{{template "base/paginate" .}}
</div>
</div>
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index cffdfabfaa..3cde3554c9 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -8,8 +8,8 @@
{{if .ProfileReadmeContent}}
<div id="readme_profile" class="render-content markup" data-profile-view-as-member="{{.IsViewingOrgAsMember}}">{{.ProfileReadmeContent}}</div>
{{end}}
- {{template "shared/repo_search" .}}
- {{template "explore/repo_list" .}}
+ {{template "shared/repo/search" .}}
+ {{template "shared/repo/list" .}}
{{template "base/paginate" .}}
</div>
diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl
deleted file mode 100644
index e1ef471e34..0000000000
--- a/templates/org/settings/delete.tmpl
+++ /dev/null
@@ -1,35 +0,0 @@
-{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings delete")}}
-
- <div class="org-setting-content">
- <h4 class="ui top attached error header">
- {{ctx.Locale.Tr "org.settings.delete_account"}}
- </h4>
- <div class="ui attached error segment">
- <div class="ui red message">
- <p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt"}}</p>
- </div>
- <form class="ui form ignore-dirty" id="delete-form" action="{{.Link}}" method="post">
- {{.CsrfTokenHtml}}
- <div class="inline required field {{if .Err_OrgName}}error{{end}}">
- <label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}}</label>
- <input id="org_name" name="org_name" value="" autocomplete="off" autofocus required>
- </div>
- <button class="ui red button delete-button" data-type="form" data-form="#delete-form">
- {{ctx.Locale.Tr "org.settings.confirm_delete_account"}}
- </button>
- </form>
- </div>
- </div>
-
-<div class="ui g-modal-confirm delete modal">
- <div class="header">
- {{svg "octicon-trash"}}
- {{ctx.Locale.Tr "org.settings.delete_org_title"}}
- </div>
- <div class="content">
- <p>{{ctx.Locale.Tr "org.settings.delete_org_desc"}}</p>
- </div>
- {{template "base/modal_actions_confirm" .}}
-</div>
-
-{{template "org/settings/layout_footer" .}}
diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl
index ce792f667c..58475de7e7 100644
--- a/templates/org/settings/navbar.tmpl
+++ b/templates/org/settings/navbar.tmpl
@@ -41,8 +41,5 @@
</div>
</details>
{{end}}
- <a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete">
- {{ctx.Locale.Tr "org.settings.delete"}}
- </a>
</div>
</div>
diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl
index f4583bbe36..d94bb4c62b 100644
--- a/templates/org/settings/options.tmpl
+++ b/templates/org/settings/options.tmpl
@@ -1,101 +1,97 @@
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings options")}}
- <div class="org-setting-content">
- <h4 class="ui top attached header">
- {{ctx.Locale.Tr "org.settings.options"}}
- </h4>
- <div class="ui attached segment">
- <form class="ui form" action="{{.Link}}" method="post">
- {{.CsrfTokenHtml}}
- <div class="required field {{if .Err_Name}}error{{end}}">
- <label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}}
- <span class="text red tw-hidden" id="org-name-change-prompt">
- <br>{{ctx.Locale.Tr "org.settings.change_orgname_prompt"}}<br>{{ctx.Locale.Tr "org.settings.change_orgname_redirect_prompt"}}
- </span>
- </label>
- <input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" required maxlength="40">
- </div>
- <div class="field {{if .Err_FullName}}error{{end}}">
- <label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label>
- <input id="full_name" name="full_name" value="{{.Org.FullName}}" maxlength="100">
- </div>
- <div class="field {{if .Err_Email}}error{{end}}">
- <label for="email">{{ctx.Locale.Tr "org.settings.email"}}</label>
- <input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255">
- </div>
- <div class="field {{if .Err_Description}}error{{end}}">
- {{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}}
- <label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label>
- <textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea>
- </div>
- <div class="field {{if .Err_Website}}error{{end}}">
- <label for="website">{{ctx.Locale.Tr "org.settings.website"}}</label>
- <input id="website" name="website" type="url" value="{{.Org.Website}}" maxlength="255">
- </div>
- <div class="field">
- <label for="location">{{ctx.Locale.Tr "org.settings.location"}}</label>
- <input id="location" name="location" value="{{.Org.Location}}" maxlength="50">
- </div>
- <div class="divider"></div>
- <div class="field" id="visibility_box">
- <label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label>
- <div class="field">
- <div class="ui radio checkbox">
- <input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}>
- <label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label>
- </div>
- </div>
- <div class="field">
- <div class="ui radio checkbox">
- <input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}>
- <label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label>
- </div>
- </div>
- <div class="field">
- <div class="ui radio checkbox">
- <input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}>
- <label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label>
- </div>
- </div>
- </div>
+<div class="ui segments org-setting-content">
+ <h4 class="ui top attached header">
+ {{ctx.Locale.Tr "org.settings.options"}}
+ </h4>
+ <div class="ui attached segment">
+ <form class="ui form" action="{{.Link}}" method="post">
+ {{.CsrfTokenHtml}}
+ <div class="field {{if .Err_FullName}}error{{end}}">
+ <label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label>
+ <input id="full_name" name="full_name" value="{{.Org.FullName}}" maxlength="100">
+ </div>
+ <div class="field {{if .Err_Email}}error{{end}}">
+ <label for="email">{{ctx.Locale.Tr "org.settings.email"}}</label>
+ <input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255">
+ </div>
+ <div class="field {{if .Err_Description}}error{{end}}">
+ {{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}}
+ <label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label>
+ <textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea>
+ </div>
+ <div class="field {{if .Err_Website}}error{{end}}">
+ <label for="website">{{ctx.Locale.Tr "org.settings.website"}}</label>
+ <input id="website" name="website" type="url" value="{{.Org.Website}}" maxlength="255">
+ </div>
+ <div class="field">
+ <label for="location">{{ctx.Locale.Tr "org.settings.location"}}</label>
+ <input id="location" name="location" value="{{.Org.Location}}" maxlength="50">
+ </div>
+
+ <div class="divider"></div>
+ <div class="field" id="visibility_box">
+ <label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label>
+ <div class="field">
+ <div class="ui radio checkbox">
+ <input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}>
+ <label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label>
+ </div>
+ </div>
+ <div class="field">
+ <div class="ui radio checkbox">
+ <input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}>
+ <label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label>
+ </div>
+ </div>
+ <div class="field">
+ <div class="ui radio checkbox">
+ <input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}>
+ <label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label>
+ </div>
+ </div>
+ </div>
- <div class="field" id="permission_box">
- <label>{{ctx.Locale.Tr "org.settings.permission"}}</label>
- <div class="field">
- <div class="ui checkbox">
- <input type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}>
- <label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label>
- </div>
- </div>
- </div>
+ <div class="field" id="permission_box">
+ <label>{{ctx.Locale.Tr "org.settings.permission"}}</label>
+ <div class="field">
+ <div class="ui checkbox">
+ <input type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}>
+ <label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label>
+ </div>
+ </div>
+ </div>
- {{if .SignedUser.IsAdmin}}
- <div class="divider"></div>
+ {{if .SignedUser.IsAdmin}}
+ <div class="divider"></div>
- <div class="inline field {{if .Err_MaxRepoCreation}}error{{end}}">
- <label for="max_repo_creation">{{ctx.Locale.Tr "admin.users.max_repo_creation"}}</label>
- <input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}">
- <p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p>
- </div>
- {{end}}
+ <div class="inline field {{if .Err_MaxRepoCreation}}error{{end}}">
+ <label for="max_repo_creation">{{ctx.Locale.Tr "admin.users.max_repo_creation"}}</label>
+ <input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}">
+ <p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p>
+ </div>
+ {{end}}
- <div class="field">
- <button class="ui primary button">{{ctx.Locale.Tr "org.settings.update_settings"}}</button>
- </div>
- </form>
+ <div class="field">
+ <button class="ui primary button">{{ctx.Locale.Tr "org.settings.update_settings"}}</button>
+ </div>
+ </form>
- <div class="divider"></div>
+ <div class="divider"></div>
- <form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data">
- {{.CsrfTokenHtml}}
- <div class="inline field">
- {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}}
- </div>
- <div class="field">
- <button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button>
- <button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button>
- </div>
- </form>
- </div>
+ <form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data">
+ {{.CsrfTokenHtml}}
+ <div class="inline field">
+ {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}}
+ </div>
+ <div class="field">
+ <button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button>
+ <button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button>
</div>
+ </form>
+ </div>
+</div>
+
+{{template "org/settings/options_dangerzone" .}}
+
{{template "org/settings/layout_footer" .}}
diff --git a/templates/org/settings/options_dangerzone.tmpl b/templates/org/settings/options_dangerzone.tmpl
new file mode 100644
index 0000000000..01cf3fd405
--- /dev/null
+++ b/templates/org/settings/options_dangerzone.tmpl
@@ -0,0 +1,93 @@
+<h4 class="ui top attached error header">
+ {{ctx.Locale.Tr "repo.settings.danger_zone"}}
+</h4>
+<div class="ui attached error danger segment">
+ <div class="flex-list">
+ <div class="flex-item tw-items-center">
+ <div class="flex-item-main">
+ <div class="flex-item-title">{{ctx.Locale.Tr "org.settings.rename"}}</div>
+ <div class="flex-item-body">{{ctx.Locale.Tr "org.settings.rename_desc"}}</div>
+ </div>
+ <div class="flex-item-trailing">
+ <button class="ui basic red show-modal button" data-modal="#rename-org-modal">{{ctx.Locale.Tr "org.settings.rename"}}</button>
+ </div>
+ </div>
+
+ <div class="flex-item">
+ <div class="flex-item-main">
+ <div class="flex-item-title">{{ctx.Locale.Tr "org.settings.delete_account"}}</div>
+ <div class="flex-item-body">{{ctx.Locale.Tr "org.settings.delete_prompt"}}</div>
+ </div>
+ <div class="flex-item-trailing">
+ <button class="ui basic red show-modal button" data-modal="#delete-org-modal">{{ctx.Locale.Tr "org.settings.delete_account"}}</button>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="ui small modal" id="rename-org-modal">
+ <div class="header">
+ {{ctx.Locale.Tr "org.settings.rename"}}
+ </div>
+ <div class="content">
+ <ul class="ui warning message">
+ <li>{{ctx.Locale.Tr "org.settings.rename_notices_1"}}</li>
+ <li>{{ctx.Locale.Tr "org.settings.rename_notices_2"}}</li>
+ </ul>
+ <form class="ui form form-fetch-action" action="{{.Link}}/rename" method="post">
+ {{.CsrfTokenHtml}}
+ <div class="field">
+ <label>
+ {{ctx.Locale.Tr "org.settings.name_confirm"}}
+ <span class="text red">{{.Org.Name}}</span>
+ </label>
+ </div>
+ <div class="required field">
+ <label for="org_name_to_rename">{{ctx.Locale.Tr "org.org_name_holder"}}</label>
+ <input id="org_name_to_rename" name="org_name" required>
+ </div>
+
+ <div class="required field">
+ <label>{{ctx.Locale.Tr "org.settings.rename_new_org_name"}}</label>
+ <input name="new_org_name" required>
+ </div>
+
+ <div class="actions">
+ <button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
+ <button class="ui red button">{{ctx.Locale.Tr "org.settings.rename"}}</button>
+ </div>
+ </form>
+ </div>
+</div>
+
+<div class="ui small modal" id="delete-org-modal">
+ <div class="header">
+ {{ctx.Locale.Tr "org.settings.delete_account"}}
+ </div>
+ <div class="content">
+ <ul class="ui warning message">
+ <li>{{ctx.Locale.Tr "org.settings.delete_notices_1"}}</li>
+ <li>{{ctx.Locale.Tr "org.settings.delete_notices_2" .Org.Name}}</li>
+ <li>{{ctx.Locale.Tr "org.settings.delete_notices_3" .Org.Name}}</li>
+ <li>{{ctx.Locale.Tr "org.settings.delete_notices_4" .Org.Name}}</li>
+ </ul>
+ <form class="ui form form-fetch-action" action="{{.Link}}/delete" method="post">
+ {{.CsrfTokenHtml}}
+ <div class="field">
+ <label>
+ {{ctx.Locale.Tr "org.settings.name_confirm"}}
+ <span class="text red">{{.Org.Name}}</span>
+ </label>
+ </div>
+ <div class="required field">
+ <label>{{ctx.Locale.Tr "org.org_name_holder"}}</label>
+ <input name="org_name" required>
+ </div>
+
+ <div class="actions">
+ <button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
+ <button class="ui red button">{{ctx.Locale.Tr "org.settings.delete_account"}}</button>
+ </div>
+ </form>
+ </div>
+</div>
diff --git a/templates/post-install.tmpl b/templates/post-install.tmpl
index 0c9aa35c90..9baac4f84c 100644
--- a/templates/post-install.tmpl
+++ b/templates/post-install.tmpl
@@ -4,7 +4,7 @@
<!-- the "cup" has a handler, so move it a little leftward to make it visually in the center -->
<div class="tw-ml-[-30px]"><img width="160" src="{{AssetUrlPrefix}}/img/loading.png" alt aria-hidden="true"></div>
<div class="tw-my-[2em] tw-text-[18px]">
- <a id="goto-user-login" href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "install.installing_desc"}}</a>
+ <a id="goto-after-install" href="{{AppSubUrl}}{{Iif .IsAccountCreated "/user/login" "/user/sign_up"}}">{{ctx.Locale.Tr "install.installing_desc"}}</a>
</div>
</div>
</div>
diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl
index f9c9eef5aa..f850ebf916 100644
--- a/templates/repo/editor/cherry_pick.tmpl
+++ b/templates/repo/editor/cherry_pick.tmpl
@@ -3,15 +3,13 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
- <form class="ui edit form" method="post" action="{{.RepoLink}}/_cherrypick/{{.SHA}}/{{.BranchName | PathEscapeSegments}}">
+ <form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_cherrypick/{{.FromCommitID}}/{{.BranchName | PathEscapeSegments}}">
{{.CsrfTokenHtml}}
- <input type="hidden" name="last_commit" value="{{.last_commit}}">
- <input type="hidden" name="page_has_posted" value="true">
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}">
<div class="repo-editor-header">
- <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
- {{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}}
- {{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .SHA)}}
+ <div class="breadcrumb">
+ {{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .FromCommitID)}}
+ {{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .FromCommitID)}}
{{if eq .CherryPickType "revert"}}
{{ctx.Locale.Tr "repo.editor.revert" $shalink}}
{{else}}
diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl
index 7efed77349..df1b9ac554 100644
--- a/templates/repo/editor/commit_form.tmpl
+++ b/templates/repo/editor/commit_form.tmpl
@@ -1,11 +1,11 @@
<div class="commit-form-wrapper">
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
<div class="commit-form">
- <h3>{{- if .CanCommitToBranch.WillSign}}
- <span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CanCommitToBranch.SigningKey}}">{{svg "octicon-lock" 24}}</span>
+ <h3>{{- if .CommitFormBehaviors.WillSign}}
+ <span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span>
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
{{- else}}
- <span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CanCommitToBranch.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
+ <span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormBehaviors.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
{{ctx.Locale.Tr "repo.editor.commit_changes"}}
{{- end}}</h3>
<div class="field">
@@ -22,17 +22,17 @@
</div>
<div class="quick-pull-choice js-quick-pull-choice">
<div class="field">
- <div class="ui radio checkbox {{if not .CanCommitToBranch.CanCommitToBranch}}disabled{{end}}">
+ <div class="ui radio checkbox {{if not .CommitFormBehaviors.CanCommitToBranch}}disabled{{end}}">
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}>
<label>
{{svg "octicon-git-commit"}}
{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}}
- {{if not .CanCommitToBranch.CanCommitToBranch}}
+ {{if not .CommitFormBehaviors.CanCommitToBranch}}
<div class="ui visible small warning message">
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
<ul>
- {{if not .CanCommitToBranch.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
- {{if and .CanCommitToBranch.RequireSigned (not .CanCommitToBranch.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
+ {{if not .CommitFormBehaviors.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
+ {{if and .CommitFormBehaviors.RequireSigned (not .CommitFormBehaviors.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
</ul>
</div>
{{end}}
@@ -42,14 +42,14 @@
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
<div class="field">
<div class="ui radio checkbox">
- {{if .CanCreatePullRequest}}
+ {{if .CommitFormBehaviors.CanCreatePullRequest}}
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
{{else}}
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
{{end}}
<label>
{{svg "octicon-git-pull-request"}}
- {{if .CanCreatePullRequest}}
+ {{if .CommitFormBehaviors.CanCreatePullRequest}}
{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
{{else}}
{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
@@ -58,7 +58,7 @@
</div>
</div>
<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}">
- <div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
+ <div class="new-branch-name-input field">
{{svg "octicon-git-branch"}}
<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
<span class="text-muted js-quick-pull-normalization-info"></span>
@@ -67,7 +67,7 @@
{{end}}
</div>
{{if and .CommitCandidateEmails (gt (len .CommitCandidateEmails) 1)}}
- <div class="field {{if .Err_CommitEmail}}error{{end}}">
+ <div class="field">
<label>{{ctx.Locale.Tr "repo.editor.commit_email"}}</label>
<select class="ui selection dropdown" name="commit_email">
{{- range $email := .CommitCandidateEmails -}}
@@ -77,7 +77,8 @@
</div>
{{end}}
</div>
- <button id="commit-button" type="submit" class="ui primary button" {{if .PageIsEdit}}disabled{{end}}>
+ <input type="hidden" name="last_commit" value="{{.last_commit}}">
+ <button id="commit-button" type="submit" class="ui primary button">
{{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}}
</button>
<a class="ui button red" href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a>
diff --git a/templates/repo/editor/common_breadcrumb.tmpl b/templates/repo/editor/common_breadcrumb.tmpl
new file mode 100644
index 0000000000..df36f00504
--- /dev/null
+++ b/templates/repo/editor/common_breadcrumb.tmpl
@@ -0,0 +1,16 @@
+<div class="breadcrumb">
+ <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
+ {{$n := len .TreeNames}}
+ {{$l := Eval $n "-" 1}}
+ {{range $i, $v := .TreeNames}}
+ <div class="breadcrumb-divider">/</div>
+ {{if eq $i $l}}
+ <input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr (Iif $.PageIsUpload "repo.editor.add_subdir" "repo.editor.name_your_file")}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
+ <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
+ {{else}}
+ <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
+ {{end}}
+ {{end}}
+ <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{or .ReturnURI (print $.BranchLink "/" (PathEscapeSegments .TreePath))}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
+ <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}">
+</div>
diff --git a/templates/repo/editor/delete.tmpl b/templates/repo/editor/delete.tmpl
index 2c0c2fc792..6f9963a6bd 100644
--- a/templates/repo/editor/delete.tmpl
+++ b/templates/repo/editor/delete.tmpl
@@ -3,9 +3,8 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
- <form class="ui form" method="post">
+ <form class="ui form form-fetch-action" method="post">
{{.CsrfTokenHtml}}
- <input type="hidden" name="last_commit" value="{{.last_commit}}">
{{template "repo/editor/commit_form" .}}
</form>
</div>
diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl
index e1bf46d53d..536ed07ca7 100644
--- a/templates/repo/editor/edit.tmpl
+++ b/templates/repo/editor/edit.tmpl
@@ -3,30 +3,13 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
- <form class="ui edit form" method="post"
+ <form class="ui edit form form-fetch-action" method="post"
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
>
{{.CsrfTokenHtml}}
- <input type="hidden" name="last_commit" value="{{.last_commit}}">
- <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}">
<div class="repo-editor-header">
- <div class="ui breadcrumb field{{if .Err_TreePath}} error{{end}}">
- <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
- {{$n := len .TreeNames}}
- {{$l := Eval $n "-" 1}}
- {{range $i, $v := .TreeNames}}
- <div class="breadcrumb-divider">/</div>
- {{if eq $i $l}}
- <input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
- <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
- {{else}}
- <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
- {{end}}
- {{end}}
- <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
- <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required>
- </div>
+ {{template "repo/editor/common_breadcrumb" .}}
</div>
{{if not .NotEditableReason}}
<div class="field">
diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl
index 33a7c2a89d..7f9571b4ae 100644
--- a/templates/repo/editor/patch.tmpl
+++ b/templates/repo/editor/patch.tmpl
@@ -3,15 +3,13 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
- <form class="ui edit form" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
+ <form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
>
{{.CsrfTokenHtml}}
- <input type="hidden" name="last_commit" value="{{.last_commit}}">
- <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}">
<div class="repo-editor-header">
- <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
+ <div class="breadcrumb">
{{ctx.Locale.Tr "repo.editor.patching"}}
<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
<div class="breadcrumb-divider">:</div>
diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl
index 5725020406..2e9280e9cd 100644
--- a/templates/repo/editor/upload.tmpl
+++ b/templates/repo/editor/upload.tmpl
@@ -3,25 +3,10 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
- <form class="ui comment form" method="post">
+ <form class="ui comment form form-fetch-action" method="post">
{{.CsrfTokenHtml}}
<div class="repo-editor-header">
- <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
- <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
- {{$n := len .TreeNames}}
- {{$l := Eval $n "-" 1}}
- {{range $i, $v := .TreeNames}}
- <div class="breadcrumb-divider">/</div>
- {{if eq $i $l}}
- <input type="text" id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus>
- <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
- {{else}}
- <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
- {{end}}
- {{end}}
- <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}{{if not .IsNewFile}}/{{.TreePath | PathEscapeSegments}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
- <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required>
- </div>
+ {{template "repo/editor/common_breadcrumb" .}}
</div>
<div class="field">
{{template "repo/upload" .}}
diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl
index 725b67c651..6e46457893 100644
--- a/templates/repo/forks.tmpl
+++ b/templates/repo/forks.tmpl
@@ -1,20 +1,12 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository forks">
{{template "repo/header" .}}
- <div class="ui container">
+ <div class="ui container fork-list">
<h2 class="ui dividing header">
{{ctx.Locale.Tr "repo.forks"}}
</h2>
- <div class="flex-list">
- {{range .Forks}}
- <div class="flex-item tw-border-0 repo-fork-item">
- <span>{{ctx.AvatarUtils.Avatar .Owner}}</span>
- <span><a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="{{.Link}}">{{.Name}}</a></span>
- </div>
- {{end}}
- </div>
+ {{template "shared/repo/list" .}}
+ {{template "base/paginate" .}}
</div>
-
- {{template "base/paginate" .}}
</div>
{{template "base/footer" .}}
diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index 41fe6cea8f..9a8341dcca 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -4,7 +4,7 @@
{{if $attachments}}
<div class="card-attachment-images">
{{range $attachments}}
- <img src="{{.DownloadURL}}" alt="{{.Name}}" />
+ <img loading="lazy" src="{{.DownloadURL}}" alt="{{.Name}}" />
{{end}}
</div>
{{end}}
diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl
index 2155f78656..e865050559 100644
--- a/templates/repo/issue/view_content/attachments.tmpl
+++ b/templates/repo/issue/view_content/attachments.tmpl
@@ -31,7 +31,7 @@
{{if FilenameIsImage .Name}}
{{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}}
<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}">
- <img alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
+ <img loading="lazy" alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
</a>
{{end}}
{{end}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 8f49bcf07e..e02111fd8e 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -615,7 +615,7 @@
<div class="timeline-item-group">
<div class="timeline-item event" id="{{.HashTag}}">
<a class="timeline-avatar"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
- <img alt src="{{.Poster.AvatarLink ctx}}" width="40" height="40">
+ <img loading="lazy" alt src="{{.Poster.AvatarLink ctx}}" width="40" height="40">
</a>
<span class="badge grey">{{svg "octicon-x" 16}}</span>
<span class="text grey muted-links">
diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl
index 7698f77b2a..1a8014e218 100644
--- a/templates/repo/settings/lfs_file.tmpl
+++ b/templates/repo/settings/lfs_file.tmpl
@@ -21,7 +21,7 @@
{{else if not .IsTextFile}}
<div class="view-raw">
{{if .IsImageFile}}
- <img alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}">
+ <img loading="lazy" alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}">
{{else if .IsVideoFile}}
<video controls src="{{$.RawFileLink}}">
<strong>{{ctx.Locale.Tr "repo.video_not_supported_in_browser"}}</strong>
diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl
index 292a2f878c..2418f24d89 100644
--- a/templates/repo/view_content.tmpl
+++ b/templates/repo/view_content.tmpl
@@ -69,7 +69,7 @@
{{if not $isTreePathRoot}}
{{$treeNameIdxLast := Eval (len .TreeNames) "-" 1}}
- <span class="breadcrumb repo-path tw-ml-1">
+ <span class="breadcrumb">
<a class="section" href="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
{{- range $i, $v := .TreeNames -}}
<span class="breadcrumb-divider">/</span>
diff --git a/templates/explore/repo_list.tmpl b/templates/shared/repo/list.tmpl
index ad190b89b2..2c8af14f9c 100644
--- a/templates/explore/repo_list.tmpl
+++ b/templates/shared/repo/list.tmpl
@@ -2,12 +2,16 @@
{{range .Repos}}
<div class="flex-item">
<div class="flex-item-leading">
- {{template "repo/icon" .}}
+ {{if $.ShowRepoOwnerAvatar}}
+ {{ctx.AvatarUtils.Avatar .Owner 24}}
+ {{else}}
+ {{template "repo/icon" .}}
+ {{end}}
</div>
<div class="flex-item-main">
<div class="flex-item-header">
<div class="flex-item-title">
- {{if and (or $.PageIsExplore $.PageIsProfileStarList) .Owner}}
+ {{if and $.ShowRepoOwnerOnList .Owner}}
<a class="text primary name" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/
{{end}}
<a class="text primary name" href="{{.Link}}">{{.Name}}</a>
diff --git a/templates/shared/repo_search.tmpl b/templates/shared/repo/search.tmpl
index a909061184..a909061184 100644
--- a/templates/shared/repo_search.tmpl
+++ b/templates/shared/repo/search.tmpl
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index 91f04e0b53..1190dc54ec 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -103,7 +103,7 @@
<ul class="user-badges">
{{range .Badges}}
<li>
- <img width="64" height="64" src="{{.ImageURL}}" alt="{{.Description}}" data-tooltip-content="{{.Description}}">
+ <img loading="lazy" width="64" height="64" src="{{.ImageURL}}" alt="{{.Description}}" data-tooltip-content="{{.Description}}">
</li>
{{end}}
</ul>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index d66568199d..a3f6e1471f 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -7,6 +7,9 @@
{{end}}
</h4>
<div class="ui attached segment">
+ {{if .IsFirstTimeRegistration}}
+ <p>{{ctx.Locale.Tr "auth.sign_up_tip"}}</p>
+ {{end}}
<form class="ui form" action="{{.SignUpLink}}" method="post">
{{.CsrfTokenHtml}}
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}}
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index 4ee12fa783..37dede7af6 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -91,7 +91,7 @@
{{range $push.Commits}}
{{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}}
<div class="flex-text-block">
- <img alt class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16">
+ <img loading="lazy" alt class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16">
<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
<span class="text truncate">
{{ctx.RenderUtils.RenderCommitMessage .Message $repo}}
diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl
index 6ef53ab654..e4dd27e63b 100644
--- a/templates/user/notification/notification_subscriptions.tmpl
+++ b/templates/user/notification/notification_subscriptions.tmpl
@@ -69,8 +69,8 @@
{{template "shared/issuelist" dict "." . "listType" "dashboard"}}
{{end}}
{{else}}
- {{template "shared/repo_search" .}}
- {{template "explore/repo_list" .}}
+ {{template "shared/repo/search" .}}
+ {{template "shared/repo/list" .}}
{{template "base/paginate" .}}
{{end}}
</div>
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index e5c3412ddd..74a53b937d 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -17,8 +17,8 @@
{{template "user/dashboard/feeds" .}}
{{else if eq .TabName "stars"}}
<div class="stars">
- {{template "shared/repo_search" .}}
- {{template "explore/repo_list" .}}
+ {{template "shared/repo/search" .}}
+ {{template "shared/repo/list" .}}
{{template "base/paginate" .}}
</div>
{{else if eq .TabName "following"}}
@@ -30,8 +30,8 @@
{{else if eq .TabName "organizations"}}
{{template "repo/user_cards" .}}
{{else}}
- {{template "shared/repo_search" .}}
- {{template "explore/repo_list" .}}
+ {{template "shared/repo/search" .}}
+ {{template "shared/repo/list" .}}
{{template "base/paginate" .}}
{{end}}
</div>
diff --git a/tests/integration/api_repo_languages_test.go b/tests/integration/api_repo_languages_test.go
index 1045aef57d..6347a43b4e 100644
--- a/tests/integration/api_repo_languages_test.go
+++ b/tests/integration/api_repo_languages_test.go
@@ -9,6 +9,8 @@ import (
"testing"
"time"
+ "code.gitea.io/gitea/modules/test"
+
"github.com/stretchr/testify/assert"
)
@@ -32,7 +34,8 @@ func TestRepoLanguages(t *testing.T) {
"content": "package main",
"commit_choice": "direct",
})
- session.MakeRequest(t, req, http.StatusSeeOther)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ assert.NotEmpty(t, test.RedirectURL(resp))
// let gitea calculate language stats
time.Sleep(time.Second)
diff --git a/tests/integration/api_repo_license_test.go b/tests/integration/api_repo_license_test.go
index 52d3085694..fb4450a2bd 100644
--- a/tests/integration/api_repo_license_test.go
+++ b/tests/integration/api_repo_license_test.go
@@ -12,12 +12,13 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
var testLicenseContent = `
-Copyright (c) 2024 Gitea
+Copyright (c) 2024 Gitea
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
@@ -48,7 +49,8 @@ func TestAPIRepoLicense(t *testing.T) {
"content": testLicenseContent,
"commit_choice": "direct",
})
- session.MakeRequest(t, req, http.StatusSeeOther)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ assert.NotEmpty(t, test.RedirectURL(resp))
// let gitea update repo license
time.Sleep(time.Second)
diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go
index a5936d86de..9e4ebb573f 100644
--- a/tests/integration/editor_test.go
+++ b/tests/integration/editor_test.go
@@ -20,6 +20,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/tests"
@@ -34,7 +35,7 @@ func TestCreateFile(t *testing.T) {
})
}
-func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) *httptest.ResponseRecorder {
+func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) {
// Request editor page
newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch)
req := NewRequest(t, "GET", newURL)
@@ -52,7 +53,8 @@ func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, file
"content": content,
"commit_choice": "direct",
})
- return session.MakeRequest(t, req, http.StatusSeeOther)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ assert.NotEmpty(t, test.RedirectURL(resp))
}
func TestCreateFileOnProtectedBranch(t *testing.T) {
@@ -88,9 +90,9 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
"commit_choice": "direct",
})
- resp = session.MakeRequest(t, req, http.StatusOK)
- // Check body for error message
- assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch &#34;master&#34;.")
+ resp = session.MakeRequest(t, req, http.StatusBadRequest)
+ respErr := test.ParseJSONError(resp.Body.Bytes())
+ assert.Equal(t, `Cannot commit to protected branch "master".`, respErr.ErrorMessage)
// remove the protected branch
csrf = GetUserCSRFToken(t, session)
@@ -131,7 +133,8 @@ func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePa
"commit_choice": "direct",
},
)
- session.MakeRequest(t, req, http.StatusSeeOther)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ assert.NotEmpty(t, test.RedirectURL(resp))
// Verify the change
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
@@ -161,7 +164,8 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra
"new_branch_name": targetBranch,
},
)
- session.MakeRequest(t, req, http.StatusSeeOther)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ assert.NotEmpty(t, test.RedirectURL(resp))
// Verify the change
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
@@ -211,9 +215,8 @@ func TestWebGitCommitEmail(t *testing.T) {
newCommit := getLastCommit(t)
if expectedUserName == "" {
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
- htmlDoc := NewHTMLParser(t, resp.Body)
- errMsg := htmlDoc.doc.Find(".ui.negative.message").Text()
- assert.Contains(t, errMsg, translation.NewLocale("en-US").Tr("repo.editor.invalid_commit_email"))
+ respErr := test.ParseJSONError(resp.Body.Bytes())
+ assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
} else {
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
assert.Equal(t, expectedUserName, newCommit.Author.Name)
@@ -333,7 +336,7 @@ index 0000000000..bbbbbbbbbb
)
// By the way, test the "cherrypick" page: a successful revert redirects to the main branch
- assert.Equal(t, "/user2/repo1/src/branch/master", resp1.Header().Get("Location"))
+ assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1))
})
})
}
diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go
index 8cebfaf32a..6a8c70f12f 100644
--- a/tests/integration/empty_repo_test.go
+++ b/tests/integration/empty_repo_test.go
@@ -10,7 +10,6 @@ import (
"io"
"mime/multipart"
"net/http"
- "net/http/httptest"
"strings"
"testing"
@@ -30,7 +29,7 @@ import (
"github.com/stretchr/testify/require"
)
-func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder {
+func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) {
url := fmt.Sprintf("/%s/%s/_new/%s", user, repo, branch)
req := NewRequestWithValues(t, "POST", url, map[string]string{
"_csrf": GetUserCSRFToken(t, session),
@@ -38,7 +37,8 @@ func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, tree
"tree_path": treePath,
"content": content,
})
- return session.MakeRequest(t, req, http.StatusSeeOther)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ assert.NotEmpty(t, test.RedirectURL(resp))
}
func TestEmptyRepo(t *testing.T) {
@@ -87,7 +87,7 @@ func TestEmptyRepoAddFile(t *testing.T) {
"content": "newly-added-test-file",
})
- resp = session.MakeRequest(t, req, http.StatusSeeOther)
+ resp = session.MakeRequest(t, req, http.StatusOK)
redirect := test.RedirectURL(resp)
assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/test-file.md", redirect)
@@ -154,9 +154,9 @@ func TestEmptyRepoUploadFile(t *testing.T) {
"files": respMap["uuid"],
"tree_path": "",
})
- resp = session.MakeRequest(t, req, http.StatusSeeOther)
+ resp = session.MakeRequest(t, req, http.StatusOK)
redirect := test.RedirectURL(resp)
- assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/", redirect)
+ assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch, redirect)
req = NewRequest(t, "GET", redirect)
resp = session.MakeRequest(t, req, http.StatusOK)
diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go
index ba4cc82992..f95a2f1690 100644
--- a/tests/integration/pull_compare_test.go
+++ b/tests/integration/pull_compare_test.go
@@ -159,7 +159,8 @@ func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) {
"commit_summary": "user2 updated the file",
"commit_choice": "direct",
})
- user2Session.MakeRequest(t, req, http.StatusSeeOther)
+ resp = user2Session.MakeRequest(t, req, http.StatusOK)
+ assert.NotEmpty(t, test.RedirectURL(resp))
}
}
})
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index db2caaf6ca..95325eefeb 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -84,7 +84,7 @@ func TestRepoForkToOrg(t *testing.T) {
func TestForkListLimitedAndPrivateRepos(t *testing.T) {
defer tests.PrepareTestEnv(t)()
- forkItemSelector := ".repo-fork-item"
+ forkItemSelector := ".fork-list .flex-item"
user1Sess := loginUser(t, "user1")
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
diff --git a/web_src/css/base.css b/web_src/css/base.css
index b50abf79f1..dc58fb850a 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -30,6 +30,10 @@
--page-spacing: 16px; /* space between page elements */
--page-margin-x: 32px; /* minimum space on left and right side of page */
--page-space-bottom: 64px; /* space between last page element and footer */
+
+ /* z-index */
+ --z-index-modal: 1001; /* modal dialog, hard-coded from Fomantic modal.css */
+ --z-index-toast: 1002; /* should be larger than modal */
}
@media (min-width: 768px) and (max-width: 1200px) {
diff --git a/web_src/css/modules/breadcrumb.css b/web_src/css/modules/breadcrumb.css
index ca488c2150..77e31ef627 100644
--- a/web_src/css/modules/breadcrumb.css
+++ b/web_src/css/modules/breadcrumb.css
@@ -1,14 +1,10 @@
.breadcrumb {
display: flex;
- flex-wrap: wrap;
align-items: center;
gap: 3px;
+ overflow-wrap: anywhere;
}
.breadcrumb .breadcrumb-divider {
color: var(--color-text-light-2);
}
-
-.breadcrumb > * {
- display: inline;
-}
diff --git a/web_src/css/modules/dimmer.css b/web_src/css/modules/dimmer.css
index 8924821370..7d1ca6171a 100644
--- a/web_src/css/modules/dimmer.css
+++ b/web_src/css/modules/dimmer.css
@@ -20,7 +20,7 @@
opacity: 1;
}
-.ui.dimmer > * {
+.ui.dimmer > .ui.modal {
position: static;
margin-top: auto !important;
margin-bottom: auto !important;
diff --git a/web_src/css/modules/toast.css b/web_src/css/modules/toast.css
index 1145f3b1b5..330d3b176e 100644
--- a/web_src/css/modules/toast.css
+++ b/web_src/css/modules/toast.css
@@ -3,7 +3,7 @@
position: fixed;
opacity: 0;
transition: all .2s ease;
- z-index: 500;
+ z-index: var(--z-index-toast);
border-radius: var(--border-radius);
box-shadow: 0 8px 24px var(--color-shadow);
display: flex;
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index cbc890e356..85522a0a69 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -139,11 +139,6 @@ td .commit-summary {
}
}
-.repo-path {
- display: flex;
- overflow-wrap: anywhere;
-}
-
.repository.file.list .non-diff-file-content .header .icon {
font-size: 1em;
}
diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js
index 85530c7991..3ad0984865 100644
--- a/web_src/fomantic/build/components/dropdown.js
+++ b/web_src/fomantic/build/components/dropdown.js
@@ -525,7 +525,7 @@ $.fn.dropdown = function(parameters) {
return true;
}
if(settings.onShow.call(element) !== false) {
- settings.onAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items
+ $module.fomanticExt.onDropdownAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items
module.animate.show(function() {
if( module.can.click() ) {
module.bind.intent();
@@ -753,7 +753,7 @@ $.fn.dropdown = function(parameters) {
if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
module.show();
}
- settings.onAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items
+ $module.fomanticExt.onDropdownAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items
}
;
if(settings.useLabels && module.has.maxSelections()) {
@@ -3994,8 +3994,6 @@ $.fn.dropdown.settings = {
onShow : function(){},
onHide : function(){},
- onAfterFiltered: function(){}, // GITEA-PATCH: callback to correctly handle the filtered items
-
/* Component */
name : 'Dropdown',
namespace : 'dropdown',
diff --git a/web_src/fomantic/build/components/modal.js b/web_src/fomantic/build/components/modal.js
index 420ecc250b..3f578ccfcc 100644
--- a/web_src/fomantic/build/components/modal.js
+++ b/web_src/fomantic/build/components/modal.js
@@ -467,7 +467,7 @@ $.fn.modal = function(parameters) {
ignoreRepeatedEvents = false;
return false;
}
-
+ $module.fomanticExt.onModalBeforeHidden.call(element); // GITEA-PATCH: handle more UI updates before hidden
if( module.is.animating() || module.is.active() ) {
if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
module.remove.active();
@@ -641,7 +641,7 @@ $.fn.modal = function(parameters) {
$module
.off('mousedown' + elementEventNamespace)
;
- }
+ }
$dimmer
.off('mousedown' + elementEventNamespace)
;
@@ -877,7 +877,7 @@ $.fn.modal = function(parameters) {
? $(document).scrollTop() + settings.padding
: $(document).scrollTop() + (module.cache.contextHeight - module.cache.height - settings.padding),
marginLeft: -(module.cache.width / 2)
- })
+ })
;
} else {
$module
@@ -886,7 +886,7 @@ $.fn.modal = function(parameters) {
? -(module.cache.height / 2)
: settings.padding / 2,
marginLeft: -(module.cache.width / 2)
- })
+ })
;
}
module.verbose('Setting modal offset for legacy mode');
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index 1006ea30bb..754acb997d 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -397,7 +397,7 @@ export default defineComponent({
<div class="ui top attached header tw-flex tw-flex-1">
<b class="ui right">#{{ index + 1 }}</b>
<a :href="contributor.home_link">
- <img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link" alt="">
+ <img loading="lazy" class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link" alt="">
</a>
<div class="tw-ml-2">
<a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a>
diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts
index a4a69540a8..9359713454 100644
--- a/web_src/js/features/common-fetch-action.ts
+++ b/web_src/js/features/common-fetch-action.ts
@@ -1,5 +1,5 @@
import {request} from '../modules/fetch.ts';
-import {showErrorToast} from '../modules/toast.ts';
+import {hideToastsAll, showErrorToast} from '../modules/toast.ts';
import {addDelegatedEventListener, submitEventSubmitter} from '../utils/dom.ts';
import {confirmModal} from './comp/ConfirmModal.ts';
import type {RequestOpts} from '../types.ts';
@@ -24,6 +24,7 @@ function fetchActionDoRedirect(redirect: string) {
async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) {
try {
+ hideToastsAll();
const resp = await request(url, opt);
if (resp.status === 200) {
let {redirect} = await resp.json();
@@ -35,7 +36,9 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R
window.location.reload();
}
return;
- } else if (resp.status >= 400 && resp.status < 500) {
+ }
+
+ if (resp.status >= 400 && resp.status < 500) {
const data = await resp.json();
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
@@ -70,7 +73,7 @@ export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitt
}
const formMethod = formEl.getAttribute('method') || 'get';
- const formActionUrl = formEl.getAttribute('action');
+ const formActionUrl = formEl.getAttribute('action') || window.location.href;
const formData = new FormData(formEl);
const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')];
if (submitterName) {
diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts
index 34df4757f9..ca4bcce881 100644
--- a/web_src/js/features/install.ts
+++ b/web_src/js/features/install.ts
@@ -104,7 +104,7 @@ function initPreInstall() {
}
function initPostInstall() {
- const el = document.querySelector('#goto-user-login');
+ const el = document.querySelector('#goto-after-install');
if (!el) return;
const targetUrl = el.getAttribute('href');
diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts
index acf4127399..c6b5cccd54 100644
--- a/web_src/js/features/repo-editor.ts
+++ b/web_src/js/features/repo-editor.ts
@@ -7,6 +7,7 @@ import {initDropzone} from './dropzone.ts';
import {confirmModal} from './comp/ConfirmModal.ts';
import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
+import {submitFormFetchAction} from './common-fetch-action.ts';
function initEditPreviewTab(elForm: HTMLFormElement) {
const elTabMenu = elForm.querySelector('.repo-editor-menu');
@@ -143,31 +144,28 @@ export function initRepoEditor() {
const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form');
+ // on the upload page, there is no editor(textarea)
+ const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
+ if (!editArea) return;
+
// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
// to enable or disable the commit button
const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
const dirtyFileClass = 'dirty-file';
- // Enabling the button at the start if the page has posted
- if (document.querySelector<HTMLInputElement>('input[name="page_has_posted"]')?.value === 'true') {
- commitButton.disabled = false;
- }
-
+ const syncCommitButtonState = () => {
+ const dirty = elForm.classList.contains(dirtyFileClass);
+ commitButton.disabled = !dirty;
+ };
// Registering a custom listener for the file path and the file content
// FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added
applyAreYouSure(elForm, {
silent: true,
dirtyClass: dirtyFileClass,
fieldSelector: ':input:not(.commit-form-wrapper :input)',
- change($form: any) {
- const dirty = $form[0]?.classList.contains(dirtyFileClass);
- commitButton.disabled = !dirty;
- },
+ change: syncCommitButtonState,
});
-
- // on the upload page, there is no editor(textarea)
- const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
- if (!editArea) return;
+ syncCommitButtonState(); // disable the "commit" button when no content changes
initEditPreviewTab(elForm);
@@ -182,7 +180,7 @@ export function initRepoEditor() {
editor.setValue(value);
}
- commitButton?.addEventListener('click', async (e) => {
+ commitButton.addEventListener('click', async (e) => {
// A modal which asks if an empty file should be committed
if (!editArea.value) {
e.preventDefault();
@@ -191,7 +189,7 @@ export function initRepoEditor() {
content: elForm.getAttribute('data-text-empty-confirm-content'),
})) {
ignoreAreYouSure(elForm);
- elForm.submit();
+ submitFormFetchAction(elForm);
}
}
});
diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts
index 02fee5a267..ccc22073d7 100644
--- a/web_src/js/modules/fomantic/dropdown.ts
+++ b/web_src/js/modules/fomantic/dropdown.ts
@@ -9,9 +9,9 @@ const fomanticDropdownFn = $.fn.dropdown;
// use our own `$().dropdown` function to patch Fomantic's dropdown module
export function initAriaDropdownPatch() {
if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once');
- $.fn.dropdown.settings.onAfterFiltered = onAfterFiltered;
$.fn.dropdown = ariaDropdownFn;
$.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem;
+ $.fn.fomanticExt.onDropdownAfterFiltered = onDropdownAfterFiltered;
(ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings;
}
@@ -71,7 +71,7 @@ function updateSelectionLabel(label: HTMLElement) {
}
}
-function onAfterFiltered(this: any) {
+function onDropdownAfterFiltered(this: any) {
const $dropdown = $(this).closest('.ui.dropdown'); // "this" can be the "ui dropdown" or "<select>"
const hideEmptyDividers = $dropdown.dropdown('setting', 'hideDividers') === 'empty';
const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu');
diff --git a/web_src/js/modules/fomantic/modal.ts b/web_src/js/modules/fomantic/modal.ts
index 6a2c558890..b07b941590 100644
--- a/web_src/js/modules/fomantic/modal.ts
+++ b/web_src/js/modules/fomantic/modal.ts
@@ -1,5 +1,7 @@
import $ from 'jquery';
import type {FomanticInitFunction} from '../../types.ts';
+import {queryElems} from '../../utils/dom.ts';
+import {hideToastsFrom} from '../toast.ts';
const fomanticModalFn = $.fn.modal;
@@ -7,6 +9,7 @@ const fomanticModalFn = $.fn.modal;
export function initAriaModalPatch() {
if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once');
$.fn.modal = ariaModalFn;
+ $.fn.fomanticExt.onModalBeforeHidden = onModalBeforeHidden;
(ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings;
}
@@ -27,3 +30,10 @@ function ariaModalFn(this: any, ...args: Parameters<FomanticInitFunction>) {
}
return ret;
}
+
+function onModalBeforeHidden(this: any) {
+ const $modal = $(this);
+ const elModal = $modal[0];
+ queryElems(elModal, 'form', (form: HTMLFormElement) => form.reset());
+ hideToastsFrom(elModal.closest('.ui.dimmer') ?? document.body);
+}
diff --git a/web_src/js/modules/toast.ts b/web_src/js/modules/toast.ts
index 36e2321743..b0afc343c3 100644
--- a/web_src/js/modules/toast.ts
+++ b/web_src/js/modules/toast.ts
@@ -1,6 +1,6 @@
import {htmlEscape} from 'escape-goat';
import {svg} from '../svg.ts';
-import {animateOnce, showElem} from '../utils/dom.ts';
+import {animateOnce, queryElems, showElem} from '../utils/dom.ts';
import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown
import type {Intent} from '../types.ts';
import type {SvgName} from '../svg.ts';
@@ -37,17 +37,20 @@ const levels: ToastLevels = {
type ToastOpts = {
useHtmlBody?: boolean,
- preventDuplicates?: boolean,
+ preventDuplicates?: boolean | string,
} & Options;
+type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast };
+
// See https://github.com/apvarun/toastify-js#api for options
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast {
const body = useHtmlBody ? String(message) : htmlEscape(message);
- const key = `${level}-${body}`;
+ const parent = document.querySelector('.ui.dimmer.active') ?? document.body;
+ const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${body}` : preventDuplicates) : '';
- // prevent showing duplicate toasts with same level and message, and give a visual feedback for end users
+ // prevent showing duplicate toasts with the same level and message, and give visual feedback for end users
if (preventDuplicates) {
- const toastEl = document.querySelector(`.toastify[data-toast-unique-key="${CSS.escape(key)}"]`);
+ const toastEl = parent.querySelector(`:scope > .toastify.on[data-toast-unique-key="${CSS.escape(duplicateKey)}"]`);
if (toastEl) {
const toastDupNumEl = toastEl.querySelector('.toast-duplicate-number');
showElem(toastDupNumEl);
@@ -59,6 +62,7 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
const toast = Toastify({
+ selector: parent,
text: `
<div class='toast-icon'>${svg(icon)}</div>
<div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${body}</div>
@@ -74,7 +78,8 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
toast.showToast();
toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
- toast.toastElement.setAttribute('data-toast-unique-key', key);
+ toast.toastElement.setAttribute('data-toast-unique-key', duplicateKey);
+ (toast.toastElement as ToastifyElement)._giteaToastifyInstance = toast;
return toast;
}
@@ -89,3 +94,15 @@ export function showWarningToast(message: string, opts?: ToastOpts): Toast {
export function showErrorToast(message: string, opts?: ToastOpts): Toast {
return showToast(message, 'error', opts);
}
+
+function hideToastByElement(el: Element): void {
+ (el as ToastifyElement)?._giteaToastifyInstance?.hideToast();
+}
+
+export function hideToastsFrom(parent: Element): void {
+ queryElems(parent, ':scope > .toastify.on', hideToastByElement);
+}
+
+export function hideToastsAll(): void {
+ queryElems(document, '.toastify.on', hideToastByElement);
+}