diff options
76 files changed, 1027 insertions, 1008 deletions
diff --git a/models/repo/repo.go b/models/repo/repo.go index 5aae02c6d8..34d1bf55f6 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -653,7 +653,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool { // CanEnableEditor returns true if repository meets the requirements of web editor. func (repo *Repository) CanEnableEditor() bool { - return !repo.IsMirror + return !repo.IsMirror && !repo.IsArchived } // DescriptionHTML does special handles to description and return HTML string. diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 9288be3b28..0fbf0f0b24 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -4,6 +4,7 @@ package markup import ( + "html/template" "io" "net/url" "regexp" @@ -92,9 +93,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { return policy } -// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist. -func Sanitize(s string) string { - return GetDefaultSanitizer().defaultPolicy.Sanitize(s) +// Sanitize use default sanitizer policy to sanitize a string +func Sanitize(s string) template.HTML { + return template.HTML(GetDefaultSanitizer().defaultPolicy.Sanitize(s)) } // SanitizeReader sanitizes a Reader diff --git a/modules/markup/sanitizer_default_test.go b/modules/markup/sanitizer_default_test.go index 5282916944..e5ba018e1b 100644 --- a/modules/markup/sanitizer_default_test.go +++ b/modules/markup/sanitizer_default_test.go @@ -69,6 +69,6 @@ func TestSanitizer(t *testing.T) { } for i := 0; i < len(testCases); i += 2 { - assert.Equal(t, testCases[i+1], Sanitize(testCases[i])) + assert.Equal(t, testCases[i+1], string(Sanitize(testCases[i]))) } } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index d55d4f87c5..052f9c47ab 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -176,9 +176,9 @@ func safeHTML(s any) template.HTML { panic(fmt.Sprintf("unexpected type %T", s)) } -// SanitizeHTML sanitizes the input by pre-defined markdown rules +// SanitizeHTML sanitizes the input by default sanitization rules. func SanitizeHTML(s string) template.HTML { - return template.HTML(markup.Sanitize(s)) + return markup.Sanitize(s) } func htmlEscape(s any) template.HTML { diff --git a/modules/web/router_path.go b/modules/web/router_path.go index 1531ccd01c..ce041eedab 100644 --- a/modules/web/router_path.go +++ b/modules/web/router_path.go @@ -6,6 +6,7 @@ package web import ( "net/http" "regexp" + "slices" "strings" "code.gitea.io/gitea/modules/container" @@ -36,11 +37,21 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req) } +type RouterPathGroupPattern struct { + re *regexp.Regexp + params []routerPathParam + middlewares []any +} + // MatchPath matches the request method, and uses regexp to match the path. -// The pattern uses "<...>" to define path parameters, for example: "/<name>" (different from chi router) -// It is only designed to resolve some special cases which chi router can't handle. +// The pattern uses "<...>" to define path parameters, for example, "/<name>" (different from chi router) +// It is only designed to resolve some special cases that chi router can't handle. // For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) { + g.MatchPattern(methods, g.PatternRegexp(pattern), h...) +} + +func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupPattern, h ...any) { g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...)) } @@ -96,8 +107,8 @@ func isValidMethod(name string) bool { return false } -func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { - middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) +func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, h ...any) *routerPathMatcher { + middlewares, handlerFunc := wrapMiddlewareAndHandler(patternRegexp.middlewares, h) p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} for method := range strings.SplitSeq(methods, ",") { method = strings.TrimSpace(method) @@ -106,19 +117,25 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher } p.methods.Add(method) } + p.re, p.params = patternRegexp.re, patternRegexp.params + return p +} + +func patternRegexp(pattern string, h ...any) *RouterPathGroupPattern { + p := &RouterPathGroupPattern{middlewares: slices.Clone(h)} re := []byte{'^'} lastEnd := 0 for lastEnd < len(pattern) { start := strings.IndexByte(pattern[lastEnd:], '<') if start == -1 { - re = append(re, pattern[lastEnd:]...) + re = append(re, regexp.QuoteMeta(pattern[lastEnd:])...) break } end := strings.IndexByte(pattern[lastEnd+start:], '>') if end == -1 { panic("invalid pattern: " + pattern) } - re = append(re, pattern[lastEnd:lastEnd+start]...) + re = append(re, regexp.QuoteMeta(pattern[lastEnd:lastEnd+start])...) partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") lastEnd += start + end + 1 @@ -140,7 +157,10 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher p.params = append(p.params, param) } re = append(re, '$') - reStr := string(re) - p.re = regexp.MustCompile(reStr) + p.re = regexp.MustCompile(string(re)) return p } + +func (g *RouterPathGroup) PatternRegexp(pattern string, h ...any) *RouterPathGroupPattern { + return patternRegexp(pattern, h...) +} diff --git a/modules/web/router_test.go b/modules/web/router_test.go index 21619012ea..1cee2b879b 100644 --- a/modules/web/router_test.go +++ b/modules/web/router_test.go @@ -34,7 +34,7 @@ func TestPathProcessor(t *testing.T) { testProcess := func(pattern, uri string, expectedPathParams map[string]string) { chiCtx := chi.NewRouteContext() chiCtx.RouteMethod = "GET" - p := newRouterPathMatcher("GET", pattern, http.NotFound) + p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound) assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri) } @@ -56,18 +56,20 @@ func TestRouter(t *testing.T) { recorder.Body = buff type resultStruct struct { - method string - pathParams map[string]string - handlerMark string + method string + pathParams map[string]string + handlerMarks []string } - var res resultStruct + var res resultStruct h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { mark := util.OptionalArg(optMark, "") return func(resp http.ResponseWriter, req *http.Request) { res.method = req.Method res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context())) - res.handlerMark = mark + if mark != "" { + res.handlerMarks = append(res.handlerMarks, mark) + } } } @@ -77,6 +79,8 @@ func TestRouter(t *testing.T) { if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { h(stop)(resp, req) resp.WriteHeader(http.StatusOK) + } else if mark != "" { + res.handlerMarks = append(res.handlerMarks, mark) } } } @@ -108,7 +112,7 @@ func TestRouter(t *testing.T) { m.Delete("", h()) }) m.PathGroup("/*", func(g *RouterPathGroup) { - g.MatchPath("GET", `/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2"), h("match-path")) + g.MatchPattern("GET", g.PatternRegexp(`/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2")), stopMark("s3"), h("match-path")) }, stopMark("s1")) }) }) @@ -126,31 +130,31 @@ func TestRouter(t *testing.T) { } t.Run("RootRouter", func(t *testing.T) { - testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"}) + testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/"}}) testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, - handlerMark: "list-issues-b", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, + handlerMarks: []string{"list-issues-b"}, }) testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, - handlerMark: "view-issue", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, + handlerMarks: []string{"view-issue"}, }) testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, - handlerMark: "hijack", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, + handlerMarks: []string{"hijack"}, }) testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{ - method: "POST", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, - handlerMark: "update-issue", + method: "POST", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, + handlerMarks: []string{"update-issue"}, }) }) t.Run("Sub Router", func(t *testing.T) { - testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"}) + testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/api/v1"}}) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, @@ -179,31 +183,37 @@ func TestRouter(t *testing.T) { t.Run("MatchPath", func(t *testing.T) { testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, - handlerMark: "match-path", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3", "match-path"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, - handlerMark: "match-path", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3", "match-path"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{ - method: "GET", - pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, - handlerMark: "not-found:/api/v1", + method: "GET", + pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, + handlerMarks: []string{"s1", "not-found:/api/v1"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, - handlerMark: "s1", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, + handlerMarks: []string{"s1"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, - handlerMark: "s2", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2"}, + }) + + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s3", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3"}, }) }) } 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 4d1f7e7bf4..dc50671335 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1399,6 +1399,13 @@ editor.revert = Revert %s onto: editor.failed_to_commit = Failed to commit changes. editor.failed_to_commit_summary = Error Message: +editor.fork_create = Fork Repository to Propose Changes +editor.fork_create_description = You can not edit this repository directly. Instead you can create a fork, make edits and create a pull request. +editor.fork_edit_description = You can not edit this repository directly. The changes will be written to your fork <b>%s</b>, so you can create a pull request. +editor.fork_not_editable = You have forked this repository but your fork is not editable. +editor.fork_failed_to_push_branch = Failed to push branch %s to your repository. +editor.fork_branch_exists = Branch "%s" already exists in your fork, please choose a new branch name. + commits.desc = Browse source code change history. commits.commits = Commits commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories. 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 ac6a089e13..b563350b55 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -1320,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=Додати «Підписано комітером» в кінці повідомлення коміту. @@ -1337,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=Повне повідомлення про відмову: @@ -2611,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 7d37661725..cb9c2acff5 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1354,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=在提交日志消息末尾添加签署人信息。 @@ -1374,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 不匹配。请提交到补丁分支然后合并。 @@ -1383,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=详细拒绝信息: @@ -2831,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/packages/api.go b/routers/api/packages/api.go index df5897e45e..f65c4b99ff 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -5,8 +5,6 @@ package packages import ( "net/http" - "regexp" - "strings" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" @@ -282,42 +280,10 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/conda", func() { - var ( - downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`) - uploadPattern = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`) - ) - - r.Get("/*", func(ctx *context.Context) { - m := downloadPattern.FindStringSubmatch(ctx.PathParam("*")) - if len(m) == 0 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/")) - ctx.SetPathParam("architecture", m[2]) - ctx.SetPathParam("filename", m[3]) - - switch m[3] { - case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": - conda.EnumeratePackages(ctx) - default: - conda.DownloadPackageFile(ctx) - } - }) - r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) { - m := uploadPattern.FindStringSubmatch(ctx.PathParam("*")) - if len(m) == 0 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/")) - ctx.SetPathParam("filename", m[2]) - - conda.UploadPackageFile(ctx) - }) + r.PathGroup("/conda/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<architecture>/<filename>", conda.ListOrGetPackages) + g.MatchPath("GET", "/<channel:*>/<architecture>/<filename>", conda.ListOrGetPackages) + g.MatchPath("PUT", "/<channel:*>/<filename>", reqPackageAccess(perm.AccessModeWrite), conda.UploadPackageFile) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cran", func() { r.Group("/src", func() { @@ -358,60 +324,15 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/go", func() { r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage) - r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) { - ctx.Status(http.StatusNotFound) - }) + r.Get("/sumdb/sum.golang.org/supported", http.NotFound) - // Manual mapping of routes because the package name contains slashes which chi does not support // https://go.dev/ref/mod#goproxy-protocol - r.Get("/*", func(ctx *context.Context) { - path := ctx.PathParam("*") - - if strings.HasSuffix(path, "/@latest") { - ctx.SetPathParam("name", path[:len(path)-len("/@latest")]) - ctx.SetPathParam("version", "latest") - - goproxy.PackageVersionMetadata(ctx) - return - } - - parts := strings.SplitN(path, "/@v/", 2) - if len(parts) != 2 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("name", parts[0]) - - // <package/name>/@v/list - if parts[1] == "list" { - goproxy.EnumeratePackageVersions(ctx) - return - } - - // <package/name>/@v/<version>.zip - if strings.HasSuffix(parts[1], ".zip") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".zip")]) - - goproxy.DownloadPackageFile(ctx) - return - } - // <package/name>/@v/<version>.info - if strings.HasSuffix(parts[1], ".info") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".info")]) - - goproxy.PackageVersionMetadata(ctx) - return - } - // <package/name>/@v/<version>.mod - if strings.HasSuffix(parts[1], ".mod") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".mod")]) - - goproxy.PackageVersionGoModContent(ctx) - return - } - - ctx.Status(http.StatusNotFound) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<name:*>/@<version:latest>", goproxy.PackageVersionMetadata) + g.MatchPath("GET", "/<name:*>/@v/list", goproxy.EnumeratePackageVersions) + g.MatchPath("GET", "/<name:*>/@v/<version>.zip", goproxy.DownloadPackageFile) + g.MatchPath("GET", "/<name:*>/@v/<version>.info", goproxy.PackageVersionMetadata) + g.MatchPath("GET", "/<name:*>/@v/<version>.mod", goproxy.PackageVersionGoModContent) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/generic", func() { @@ -532,82 +453,24 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/pypi", func() { r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile) r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) r.Get("/simple/{id}", pypi.PackageMetadata) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/rpm", func() { - r.Group("/repository.key", func() { - r.Head("", rpm.GetRepositoryKey) - r.Get("", rpm.GetRepositoryKey) - }) - var ( - repoPattern = regexp.MustCompile(`\A(.*?)\.repo\z`) - uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`) - filePattern = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`) - repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`) - ) - - r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { - path := ctx.PathParam("*") - isHead := ctx.Req.Method == http.MethodHead - isGetHead := ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet - isPut := ctx.Req.Method == http.MethodPut - isDelete := ctx.Req.Method == http.MethodDelete - - m := repoPattern.FindStringSubmatch(path) - if len(m) == 2 && isGetHead { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - rpm.GetRepositoryConfig(ctx) - return - } - - m = repoFilePattern.FindStringSubmatch(path) - if len(m) == 3 && isGetHead { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - ctx.SetPathParam("filename", m[2]) - if isHead { - rpm.CheckRepositoryFileExistence(ctx) - } else { - rpm.GetRepositoryFile(ctx) - } - return - } - - m = uploadPattern.FindStringSubmatch(path) - if len(m) == 2 && isPut { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - rpm.UploadPackageFile(ctx) - return - } - - m = filePattern.FindStringSubmatch(path) - if len(m) == 6 && (isGetHead || isDelete) { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - ctx.SetPathParam("name", m[2]) - ctx.SetPathParam("version", m[3]) - ctx.SetPathParam("architecture", m[4]) - if isGetHead { - rpm.DownloadPackageFile(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - rpm.DeletePackageFile(ctx) - } - return - } - - ctx.Status(http.StatusNotFound) - }) + r.Methods("HEAD,GET", "/rpm.repo", reqPackageAccess(perm.AccessModeRead), rpm.GetRepositoryConfig) + r.PathGroup("/rpm/*", func(g *web.RouterPathGroup) { + g.MatchPath("HEAD,GET", "/repository.key", rpm.GetRepositoryKey) + g.MatchPath("HEAD,GET", "/<group:*>.repo", rpm.GetRepositoryConfig) + g.MatchPath("HEAD", "/<group:*>/repodata/<filename>", rpm.CheckRepositoryFileExistence) + g.MatchPath("GET", "/<group:*>/repodata/<filename>", rpm.GetRepositoryFile) + g.MatchPath("PUT", "/<group:*>/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile) + g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile) + g.MatchPath("DELETE", "/<group:*>/package/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/rubygems", func() { r.Get("/specs.4.8.gz", rubygems.EnumeratePackages) r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest) @@ -621,6 +484,7 @@ func CommonRoutes() *web.Router { r.Delete("/yank", rubygems.DeletePackage) }, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/swift", func() { r.Group("", func() { // Needs to be unauthenticated. r.Post("", swift.CheckAuthenticate) @@ -632,31 +496,12 @@ func CommonRoutes() *web.Router { r.Get("", swift.EnumeratePackageVersions) r.Get(".json", swift.EnumeratePackageVersions) }, swift.CheckAcceptMediaType(swift.AcceptJSON)) - r.Group("/{version}", func() { - r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) - r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) - r.Get("", func(ctx *context.Context) { - // Can't use normal routes here: https://github.com/go-chi/chi/issues/781 - - version := ctx.PathParam("version") - if strings.HasSuffix(version, ".zip") { - swift.CheckAcceptMediaType(swift.AcceptZip)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("version", version[:len(version)-4]) - swift.DownloadPackageFile(ctx) - } else { - swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx) - if ctx.Written() { - return - } - if strings.HasSuffix(version, ".json") { - ctx.SetPathParam("version", version[:len(version)-5]) - } - swift.PackageVersionMetadata(ctx) - } - }) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<version>.json", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata) + g.MatchPath("GET", "/<version>.zip", swift.CheckAcceptMediaType(swift.AcceptZip), swift.DownloadPackageFile) + g.MatchPath("GET", "/<version>/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) + g.MatchPath("GET", "/<version>", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata) + g.MatchPath("PUT", "/<version>", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) }) }) r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers) @@ -705,18 +550,13 @@ func ContainerRoutes() *web.Router { r.PathGroup("/*", func(g *web.RouterPathGroup) { g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads) g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagsList) - g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { - switch ctx.Req.Method { - case http.MethodGet: - container.GetBlobsUpload(ctx) - case http.MethodPatch: - container.PatchBlobsUpload(ctx) - case http.MethodPut: - container.PutBlobsUpload(ctx) - default: /* DELETE */ - container.DeleteBlobsUpload(ctx) - } - }) + + patternBlobsUploadsUUID := g.PatternRegexp(`/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName) + g.MatchPattern("GET", patternBlobsUploadsUUID, container.GetBlobsUpload) + g.MatchPattern("PATCH", patternBlobsUploadsUUID, container.PatchBlobsUpload) + g.MatchPattern("PUT", patternBlobsUploadsUUID, container.PutBlobsUpload) + g.MatchPattern("DELETE", patternBlobsUploadsUUID, container.DeleteBlobsUpload) + g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob) g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob) g.MatchPath("DELETE", `/<image:*>/blobs/<digest>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go index fe7542dd18..cfe069d6db 100644 --- a/routers/api/packages/conda/conda.go +++ b/routers/api/packages/conda/conda.go @@ -36,6 +36,24 @@ func apiError(ctx *context.Context, status int, obj any) { }) } +func isCondaPackageFileName(filename string) bool { + return strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".conda") +} + +func ListOrGetPackages(ctx *context.Context) { + filename := ctx.PathParam("filename") + switch filename { + case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": + EnumeratePackages(ctx) + return + } + if isCondaPackageFileName(filename) { + DownloadPackageFile(ctx) + return + } + ctx.NotFound(nil) +} + func EnumeratePackages(ctx *context.Context) { type Info struct { Subdir string `json:"subdir"` @@ -174,6 +192,12 @@ func EnumeratePackages(ctx *context.Context) { } func UploadPackageFile(ctx *context.Context) { + filename := ctx.PathParam("filename") + if !isCondaPackageFileName(filename) { + apiError(ctx, http.StatusBadRequest, nil) + return + } + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -191,7 +215,7 @@ func UploadPackageFile(ctx *context.Context) { defer buf.Close() var pck *conda_module.Package - if strings.HasSuffix(strings.ToLower(ctx.PathParam("filename")), ".tar.bz2") { + if strings.HasSuffix(filename, ".tar.bz2") { pck, err = conda_module.ParsePackageBZ2(buf) } else { pck, err = conda_module.ParsePackageConda(buf, buf.Size()) diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 2ea9b3839c..abfc21f95a 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -90,14 +90,14 @@ func mountBlob(ctx context.Context, pi *packages_service.PackageInfo, pb *packag }) } -func containerPkgName(piOwnerID int64, piName string) string { - return fmt.Sprintf("pkg_%d_container_%s", piOwnerID, strings.ToLower(piName)) +func containerGlobalLockKey(piOwnerID int64, piName, usage string) string { + return fmt.Sprintf("pkg_%d_container_%s_%s", piOwnerID, strings.ToLower(piName), usage) } func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) { var uploadVersion *packages_model.PackageVersion - releaser, err := globallock.Lock(ctx, containerPkgName(pi.Owner.ID, pi.Name)) + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(pi.Owner.ID, pi.Name, "package")) if err != nil { return nil, err } @@ -178,7 +178,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p } func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error { - releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image)) + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(ownerID, image, "blob")) if err != nil { return err } diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index d1b80daccf..aeec16be4b 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -32,7 +32,7 @@ import ( packages_service "code.gitea.io/gitea/services/packages" container_service "code.gitea.io/gitea/services/packages/container" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" ) // maximum size of a container manifest diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index b69b7af3f7..22ea11c8ce 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -16,6 +16,7 @@ import ( packages_model "code.gitea.io/gitea/models/packages" container_model "code.gitea.io/gitea/models/packages/container" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" @@ -61,6 +62,13 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag } } + // .../container/manifest.go:453:createManifestBlob() [E] Error inserting package blob: Error 1062 (23000): Duplicate entry '..........' for key 'package_blob.UQE_package_blob_md5' + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(mci.Owner.ID, mci.Image, "manifest")) + if err != nil { + return "", err + } + defer releaser() + if container_module.IsMediaTypeImageManifest(mci.MediaType) { return processOciImageManifest(ctx, mci, buf) } else if container_module.IsMediaTypeImageIndex(mci.MediaType) { diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index e8ad3cceb5..ae0b74b019 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" @@ -39,7 +40,7 @@ const ( editorCommitChoiceNewBranch string = "commit-to-new-branch" ) -func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) { +func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions { 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)) @@ -47,18 +48,28 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) { redirectTo += "?" + ctx.Req.URL.RawQuery } ctx.Redirect(redirectTo) - return + return nil } - commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer) + commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName) if err != nil { - ctx.ServerError("PrepareCommitFormBehaviors", err) - return + ctx.ServerError("PrepareCommitFormOptions", err) + return nil + } + + if commitFormOptions.NeedFork { + ForkToEdit(ctx) + return nil + } + + if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() { + ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable") + ctx.NotFound(nil) } ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["TreePath"] = ctx.Repo.TreePath - ctx.Data["CommitFormBehaviors"] = commitFormBehaviors + ctx.Data["CommitFormOptions"] = commitFormOptions // for online editor ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") @@ -69,25 +80,27 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) { // 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["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch) + ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo) ctx.Data["last_commit"] = ctx.Repo.CommitID + return commitFormOptions } 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) + ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/")) } -type parsedEditorCommitForm[T any] struct { - form T - commonForm *forms.CommitCommonForm - CommitFormBehaviors *context.CommitFormBehaviors - TargetBranchName string - GitCommitter *files_service.IdentityOptions +type preparedEditorCommitForm[T any] struct { + form T + commonForm *forms.CommitCommonForm + CommitFormOptions *context.CommitFormOptions + OldBranchName string + NewBranchName string + GitCommitter *files_service.IdentityOptions } -func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string { +func (f *preparedEditorCommitForm[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 @@ -95,7 +108,7 @@ func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string return commitMessage } -func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *parsedEditorCommitForm[T] { +func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] { form := web.GetForm(ctx).(T) if ctx.HasError() { ctx.JSONError(ctx.GetErrMsg()) @@ -105,15 +118,22 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont commonForm := form.GetCommitCommonForm() commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath) - commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer) + commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName) if err != nil { - ctx.ServerError("PrepareCommitFormBehaviors", err) + ctx.ServerError("PrepareCommitFormOptions", err) + return nil + } + if commitFormOptions.NeedFork { + // It shouldn't happen, because we should have done the checks in the "GET" request. But just in case. + ctx.JSONError(ctx.Locale.TrString("error.not_found")) return nil } // check commit behavior - targetBranchName := util.Iif(commonForm.CommitChoice == editorCommitChoiceNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName) - if targetBranchName == ctx.Repo.BranchName && !commitFormBehaviors.CanCommitToBranch { + fromBaseBranch := ctx.FormString("from_base_branch") + commitToNewBranch := commonForm.CommitChoice == editorCommitChoiceNewBranch || fromBaseBranch != "" + targetBranchName := util.Iif(commitToNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName) + if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch { ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName)) return nil } @@ -125,28 +145,63 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont return nil } - return &parsedEditorCommitForm[T]{ - form: form, - commonForm: commonForm, - CommitFormBehaviors: commitFormBehaviors, - TargetBranchName: targetBranchName, - GitCommitter: gitCommitter, + if commitToNewBranch { + // if target branch exists, we should stop + targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName) + if err != nil { + ctx.ServerError("IsBranchExist", err) + return nil + } else if targetBranchExists { + if fromBaseBranch != "" { + ctx.JSONError(ctx.Tr("repo.editor.fork_branch_exists", targetBranchName)) + } else { + ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", targetBranchName)) + } + return nil + } + } + + oldBranchName := ctx.Repo.BranchName + if fromBaseBranch != "" { + err = editorPushBranchToForkedRepository(ctx, ctx.Doer, ctx.Repo.Repository.BaseRepo, fromBaseBranch, commitFormOptions.TargetRepo, targetBranchName) + if err != nil { + log.Error("Unable to editorPushBranchToForkedRepository: %v", err) + ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName)) + return nil + } + // we have pushed the base branch as the new branch, now we need to commit the changes directly to the new branch + oldBranchName = targetBranchName + } + + return &preparedEditorCommitForm[T]{ + form: form, + commonForm: commonForm, + CommitFormOptions: commitFormOptions, + OldBranchName: oldBranchName, + NewBranchName: targetBranchName, + GitCommitter: gitCommitter, } } // redirectForCommitChoice redirects after committing the edit to a branch -func redirectForCommitChoice[T any](ctx *context.Context, parsed *parsedEditorCommitForm[T], treePath string) { +func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) { + // when editing a file in a PR, it should return to the origin location + if returnURI := ctx.FormString("return_uri"); returnURI != "" && httplib.IsCurrentGiteaSiteURL(ctx, returnURI) { + ctx.JSONRedirect(returnURI) + return + } + if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch { // Redirect to a pull request when possible redirectToPullRequest := false - repo, baseBranch, headBranch := ctx.Repo.Repository, ctx.Repo.BranchName, parsed.TargetBranchName - if repo.UnitEnabled(ctx, unit.TypePullRequests) { - redirectToPullRequest = true - } else if parsed.CommitFormBehaviors.CanCreateBasePullRequest { + repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName + if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest { redirectToPullRequest = true baseBranch = repo.BaseRepo.DefaultBranch headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch repo = repo.BaseRepo + } else if repo.UnitEnabled(ctx, unit.TypePullRequests) { + redirectToPullRequest = true } if redirectToPullRequest { ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch)) @@ -154,11 +209,9 @@ func redirectForCommitChoice[T any](ctx *context.Context, parsed *parsedEditorCo } } - returnURI := ctx.FormString("return_uri") - if returnURI == "" || !httplib.IsCurrentGiteaSiteURL(ctx, returnURI) { - returnURI = util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.TargetBranchName), util.PathEscapeSegments(treePath)) - } - ctx.JSONRedirect(returnURI) + // redirect to the newly updated file + redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath)) + ctx.JSONRedirect(redirectTo) } func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) { @@ -268,7 +321,7 @@ func EditFile(ctx *context.Context) { func EditFilePost(ctx *context.Context) { editorAction := ctx.PathParam("editor_action") isNewFile := editorAction == "_new" - parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) + parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) if ctx.Written() { return } @@ -292,8 +345,8 @@ func EditFilePost(ctx *context.Context) { _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: parsed.form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: parsed.TargetBranchName, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, Message: parsed.GetCommitMessage(defaultCommitMessage), Files: []*files_service.ChangeRepoFile{ { @@ -308,7 +361,7 @@ func EditFilePost(ctx *context.Context) { Committer: parsed.GitCommitter, }) if err != nil { - editorHandleFileOperationError(ctx, parsed.TargetBranchName, err) + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } @@ -327,7 +380,7 @@ func DeleteFile(ctx *context.Context) { // DeleteFilePost response for deleting file func DeleteFilePost(ctx *context.Context) { - parsed := parseEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx) + parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx) if ctx.Written() { return } @@ -335,8 +388,8 @@ func DeleteFilePost(ctx *context.Context) { 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: parsed.TargetBranchName, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, Files: []*files_service.ChangeRepoFile{ { Operation: "delete", @@ -349,29 +402,29 @@ func DeleteFilePost(ctx *context.Context) { Committer: parsed.GitCommitter, }) if err != nil { - editorHandleFileOperationError(ctx, parsed.TargetBranchName, err) + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) - redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.TargetBranchName, treePath) + redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath) redirectForCommitChoice(ctx, parsed, redirectTreePath) } func UploadFile(ctx *context.Context) { ctx.Data["PageIsUpload"] = true - upload.AddUploadContext(ctx, "repo") prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) - - prepareEditorCommitFormOptions(ctx, "_upload") + opts := prepareEditorCommitFormOptions(ctx, "_upload") if ctx.Written() { return } + upload.AddUploadContextForRepo(ctx, opts.TargetRepo) + ctx.HTML(http.StatusOK, tplUploadFile) } func UploadFilePost(ctx *context.Context) { - parsed := parseEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx) + parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx) if ctx.Written() { return } @@ -379,8 +432,8 @@ func UploadFilePost(ctx *context.Context) { 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, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, TreePath: parsed.form.TreePath, Message: parsed.GetCommitMessage(defaultCommitMessage), Files: parsed.form.Files, @@ -389,7 +442,7 @@ func UploadFilePost(ctx *context.Context) { Committer: parsed.GitCommitter, }) if err != nil { - editorHandleFileOperationError(ctx, parsed.TargetBranchName, err) + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go index 9fd7a9468b..bd2811cc5f 100644 --- a/routers/web/repo/editor_apply_patch.go +++ b/routers/web/repo/editor_apply_patch.go @@ -25,7 +25,7 @@ func NewDiffPatch(ctx *context.Context) { // NewDiffPatchPost response for sending patch page func NewDiffPatchPost(ctx *context.Context) { - parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) + parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) if ctx.Written() { return } @@ -33,8 +33,8 @@ func NewDiffPatchPost(ctx *context.Context) { 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, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, Message: parsed.GetCommitMessage(defaultCommitMessage), Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"), Author: parsed.GitCommitter, @@ -44,7 +44,7 @@ func NewDiffPatchPost(ctx *context.Context) { err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch") } if err != nil { - editorHandleFileOperationError(ctx, parsed.TargetBranchName, err) + editorHandleFileOperationError(ctx, parsed.NewBranchName, 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 index 4c93d610cc..10c2741b1c 100644 --- a/routers/web/repo/editor_cherry_pick.go +++ b/routers/web/repo/editor_cherry_pick.go @@ -45,7 +45,7 @@ func CherryPick(ctx *context.Context) { func CherryPickPost(ctx *context.Context) { fromCommitID := ctx.PathParam("sha") - parsed := parseEditorCommitSubmittedForm[*forms.CherryPickForm](ctx) + parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx) if ctx.Written() { return } @@ -53,8 +53,8 @@ func CherryPickPost(ctx *context.Context) { 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, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, Message: parsed.GetCommitMessage(defaultCommitMessage), Author: parsed.GitCommitter, Committer: parsed.GitCommitter, @@ -78,7 +78,7 @@ func CherryPickPost(ctx *context.Context) { } } if err != nil { - editorHandleFileOperationError(ctx, parsed.TargetBranchName, err) + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } } diff --git a/routers/web/repo/editor_fork.go b/routers/web/repo/editor_fork.go new file mode 100644 index 0000000000..b78a634c00 --- /dev/null +++ b/routers/web/repo/editor_fork.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" +) + +const tplEditorFork templates.TplName = "repo/editor/fork" + +func ForkToEdit(ctx *context.Context) { + ctx.HTML(http.StatusOK, tplEditorFork) +} + +func ForkToEditPost(ctx *context.Context) { + ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{ + BaseRepo: ctx.Repo.Repository, + Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name), + Description: ctx.Repo.Repository.Description, + SingleBranch: ctx.Repo.Repository.DefaultBranch, // maybe we only need the default branch in the fork? + }) + if ctx.Written() { + return + } + ctx.JSONRedirect("") // reload the page, the new fork should be editable now +} diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go index 8744b4479e..f910f0bd40 100644 --- a/routers/web/repo/editor_util.go +++ b/routers/web/repo/editor_util.go @@ -11,9 +11,11 @@ import ( 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/json" "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" context_service "code.gitea.io/gitea/services/context" ) @@ -83,3 +85,26 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) { } return treeNames, treePaths } + +// getUniqueRepositoryName Gets a unique repository name for a user +// It will append a -<num> postfix if the name is already taken +func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string { + uniqueName := name + for i := 1; i < 1000; i++ { + _, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName) + if err != nil || repo_model.IsErrRepoNotExist(err) { + return uniqueName + } + uniqueName = fmt.Sprintf("%s-%d", name, i) + i++ + } + return "" +} + +func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error { + return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ + Remote: targetRepo.RepoPath(), + Branch: baseBranchName + ":" + targetBranchName, + Env: repo_module.PushingEnvironment(doer, targetRepo), + }) +} diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 9f5cda10c2..c2694e540f 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -189,17 +189,25 @@ func ForkPost(ctx *context.Context) { } } - repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{ + repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{ BaseRepo: forkRepo, Name: form.RepoName, Description: form.Description, SingleBranch: form.ForkSingleBranch, }) + if ctx.Written() { + return + } + ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) +} + +func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository { + repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts) if err != nil { ctx.Data["Err_RepoName"] = true switch { case repo_model.IsErrReachLimitOfRepo(err): - maxCreationLimit := ctxUser.MaxCreationLimit() + maxCreationLimit := owner.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) ctx.JSONError(msg) case repo_model.IsErrRepoAlreadyExist(err): @@ -224,9 +232,7 @@ func ForkPost(ctx *context.Context) { default: ctx.ServerError("ForkPost", err) } - return + return nil } - - log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name) - ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) + return repo } diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index ec0ad02828..5606a8e6ec 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -290,7 +290,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) { // archived or mirror repository, the buttons should not be shown - if ctx.Repo.Repository.IsArchived || !ctx.Repo.Repository.CanEnableEditor() { + if !ctx.Repo.Repository.CanEnableEditor() { return } @@ -302,7 +302,9 @@ func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) { } if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + ctx.Data["CanEditFile"] = true ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") + ctx.Data["CanDeleteFile"] = true ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") return } diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 7af6ad450e..4ce22d79db 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -212,7 +212,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) } - if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { + if !fInfo.isLFSFile && ctx.Repo.Repository.CanEnableEditor() { ctx.Data["CanEditReadmeFile"] = true } } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index a1e10c380d..69858c9692 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -7,6 +7,7 @@ package repo import ( "bytes" gocontext "context" + "html/template" "io" "net/http" "net/url" @@ -61,9 +62,9 @@ func MustEnableWiki(ctx *context.Context) { return } - unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki) + repoUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki) if err == nil { - ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL) + ctx.Redirect(repoUnit.ExternalWikiConfig().ExternalWikiURL) return } } @@ -95,7 +96,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) } func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) { - wikiGitRepo, errGitRepo := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) + wikiGitRepo, errGitRepo := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository.WikiStorageRepo()) if errGitRepo != nil { ctx.ServerError("OpenRepository", errGitRepo) return nil, nil, errGitRepo @@ -178,23 +179,17 @@ func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_ } func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } return nil, nil } - // Get page list. + // get the wiki pages list. entries, err := commit.ListEntries() if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("ListEntries", err) return nil, nil } @@ -208,9 +203,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { if repo_model.IsErrWikiInvalidFileName(err) { continue } - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("WikiFilenameToName", err) return nil, nil } else if wikiName == "_Sidebar" || wikiName == "_Footer" { @@ -249,58 +241,26 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName))) } if entry == nil || ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - // get filecontent + // get page content data := wikiContentsByEntry(ctx, entry) if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - var sidebarContent []byte - if !isSideBar { - sidebarContent, _, _, _ = wikiContentsByName(ctx, commit, "_Sidebar") - if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } - return nil, nil - } - } else { - sidebarContent = data - } - - var footerContent []byte - if !isFooter { - footerContent, _, _, _ = wikiContentsByName(ctx, commit, "_Footer") - if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } - return nil, nil - } - } else { - footerContent = data - } - rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository) - buf := &strings.Builder{} - renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) { + renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) { + buf := &strings.Builder{} markupRd, markupWr := io.Pipe() defer markupWr.Close() done := make(chan struct{}) go func() { // We allow NBSP here this is rendered escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP) - output = buf.String() + output = template.HTML(buf.String()) buf.Reset() close(done) }() @@ -311,75 +271,61 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { return escaped, output, err } - ctx.Data["EscapeStatus"], ctx.Data["content"], err = renderFn(data) + ctx.Data["EscapeStatus"], ctx.Data["WikiContentHTML"], err = renderFn(data) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } if rctx.SidebarTocNode != nil { - sb := &strings.Builder{} - err = markdown.SpecializedMarkdown(rctx).Renderer().Render(sb, nil, rctx.SidebarTocNode) - if err != nil { + sb := strings.Builder{} + if err = markdown.SpecializedMarkdown(rctx).Renderer().Render(&sb, nil, rctx.SidebarTocNode); err != nil { log.Error("Failed to render wiki sidebar TOC: %v", err) - } else { - ctx.Data["sidebarTocContent"] = sb.String() } + ctx.Data["WikiSidebarTocHTML"] = templates.SanitizeHTML(sb.String()) } if !isSideBar { - buf.Reset() - ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent) + sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar") + if ctx.Written() { + return nil, nil + } + ctx.Data["WikiSidebarEscapeStatus"], ctx.Data["WikiSidebarHTML"], err = renderFn(sidebarContent) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } - ctx.Data["sidebarPresent"] = sidebarContent != nil - } else { - ctx.Data["sidebarPresent"] = false } if !isFooter { - buf.Reset() - ctx.Data["footerEscapeStatus"], ctx.Data["footerContent"], err = renderFn(footerContent) + footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer") + if ctx.Written() { + return nil, nil + } + ctx.Data["WikiFooterEscapeStatus"], ctx.Data["WikiFooterHTML"], err = renderFn(footerContent) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } - ctx.Data["footerPresent"] = footerContent != nil - } else { - ctx.Data["footerPresent"] = false } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount - return wikiRepo, entry + return wikiGitRepo, entry } func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } return nil, nil } - // get requested pagename + // get requested page name pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*")) if len(pageName) == 0 { pageName = "Home" @@ -394,50 +340,35 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name - // lookup filename in wiki - get filecontent, gitTree entry , real filename - data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) + // lookup filename in wiki - get page content, gitTree entry , real filename + _, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) if noEntry { ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } if entry == nil || ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - ctx.Data["content"] = string(data) - ctx.Data["sidebarPresent"] = false - ctx.Data["sidebarContent"] = "" - ctx.Data["footerPresent"] = false - ctx.Data["footerContent"] = "" - // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount // get page page := max(ctx.FormInt("page"), 1) // get Commit Count - commitsHistory, err := wikiRepo.CommitsByFileAndRange( + commitsHistory, err := wikiGitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, Page: page, }) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("CommitsByFileAndRange", err) return nil, nil } ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("ConvertFromGitCommit", err) return nil, nil } @@ -446,16 +377,11 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager - return wikiRepo, entry + return wikiGitRepo, entry } func renderEditPage(ctx *context.Context) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - _ = wikiRepo.Close() - } - }() + _, commit, err := findWikiRepoCommit(ctx) if err != nil { if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) @@ -463,7 +389,7 @@ func renderEditPage(ctx *context.Context) { return } - // get requested pagename + // get requested page name pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*")) if len(pageName) == 0 { pageName = "Home" @@ -487,17 +413,13 @@ func renderEditPage(ctx *context.Context) { return } - // get filecontent + // get wiki page content data := wikiContentsByEntry(ctx, entry) if ctx.Written() { return } - ctx.Data["content"] = string(data) - ctx.Data["sidebarPresent"] = false - ctx.Data["sidebarContent"] = "" - ctx.Data["footerPresent"] = false - ctx.Data["footerContent"] = "" + ctx.Data["WikiEditContent"] = string(data) } // WikiPost renders post of wiki page @@ -559,12 +481,7 @@ func Wiki(ctx *context.Context) { return } - wikiRepo, entry := renderViewPage(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() + wikiGitRepo, entry := renderViewPage(ctx) if ctx.Written() { return } @@ -580,7 +497,7 @@ func Wiki(ctx *context.Context) { ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown." } // Get last change information. - lastCommit, err := wikiRepo.GetCommitByPath(wikiPath) + lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath) if err != nil { ctx.ServerError("GetCommitByPath", err) return @@ -600,13 +517,7 @@ func WikiRevision(ctx *context.Context) { return } - wikiRepo, entry := renderRevisionPage(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() - + wikiGitRepo, entry := renderRevisionPage(ctx) if ctx.Written() { return } @@ -618,7 +529,7 @@ func WikiRevision(ctx *context.Context) { // Get last change information. wikiPath := entry.Name() - lastCommit, err := wikiRepo.GetCommitByPath(wikiPath) + lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath) if err != nil { ctx.ServerError("GetCommitByPath", err) return @@ -638,12 +549,7 @@ func WikiPages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.wiki.pages") ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - _ = wikiRepo.Close() - } - }() + _, commit, err := findWikiRepoCommit(ctx) if err != nil { ctx.Redirect(ctx.Repo.RepoLink + "/wiki") return @@ -697,13 +603,7 @@ func WikiPages(ctx *context.Context) { // WikiRaw outputs raw blob requested by user (image for example) func WikiRaw(ctx *context.Context) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() - + _, commit, err := findWikiRepoCommit(ctx) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound(nil) diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 73f9970a07..59bf6ed79b 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -164,7 +164,7 @@ func TestEditWiki(t *testing.T) { EditWiki(ctx) assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, "Home", ctx.Data["Title"]) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"]) + assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["WikiEditContent"]) ctx, _ = contexttest.MockContext(t, "user2/repo1/wiki/jpeg.jpg?action=_edit") ctx.SetPathParam("*", "jpeg.jpg") diff --git a/routers/web/web.go b/routers/web/web.go index 3040375def..4b5d68b260 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1313,23 +1313,35 @@ func registerWebRoutes(m *web.Router) { }, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) // end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones - m.Group("/{username}/{reponame}", func() { // repo code + m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader") m.Group("", func() { m.Group("", func() { - 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("/_delete/*").Get(repo.DeleteFile). - Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost) - m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile). - Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) - m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). - Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) - m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick). - Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) - }, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData) + // "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission. + // Because reader can "fork and edit" + canWriteToBranch := context.CanWriteToBranch() + m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader" + m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader" + + // the path params are used in PrepareCommitFormOptions to construct the correct form action URL + m.Combo("/{editor_action:_edit}/*"). + Get(repo.EditFile). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost) + m.Combo("/{editor_action:_new}/*"). + Get(repo.EditFile). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost) + m.Combo("/{editor_action:_delete}/*"). + Get(repo.DeleteFile). + Post(web.Bind(forms.DeleteRepoFileForm{}), canWriteToBranch, repo.DeleteFilePost) + m.Combo("/{editor_action:_upload}/*", repo.MustBeAbleToUpload). + Get(repo.UploadFile). + Post(web.Bind(forms.UploadRepoFileForm{}), canWriteToBranch, repo.UploadFilePost) + m.Combo("/{editor_action:_diffpatch}/*"). + Get(repo.NewDiffPatch). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.NewDiffPatchPost) + m.Combo("/{editor_action:_cherrypick}/{sha:([a-f0-9]{7,64})}/*"). + Get(repo.CherryPick). + Post(web.Bind(forms.CherryPickForm{}), canWriteToBranch, repo.CherryPickPost) + }, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData) m.Group("", func() { m.Post("/upload-file", repo.UploadFileToServer) m.Post("/upload-remove", repo.RemoveUploadFileFromServer) diff --git a/services/context/repo.go b/services/context/repo.go index c28ae7e8fd..572211712b 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -71,11 +71,6 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user) } -// CanEnableEditor returns true if repository is editable and user has proper access level. -func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool { - return r.RefFullName.IsBranch() && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived -} - // CanCreateBranch returns true if repository is editable and user has proper access level. func (r *Repository) CanCreateBranch() bool { return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch() @@ -94,9 +89,13 @@ func RepoMustNotBeArchived() func(ctx *Context) { } } -type CommitFormBehaviors struct { +type CommitFormOptions struct { + NeedFork bool + + TargetRepo *repo_model.Repository + TargetFormAction string + WillSubmitToFork bool CanCommitToBranch bool - EditorEnabled bool UserCanPush bool RequireSigned bool WillSign bool @@ -106,51 +105,84 @@ type CommitFormBehaviors struct { CanCreateBasePullRequest bool } -func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) { - protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName) +func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) { + if !refName.IsBranch() { + // it shouldn't happen because middleware already checks + return nil, util.NewInvalidArgumentErrorf("ref %q is not a branch", refName) + } + + originRepo := targetRepo + branchName := refName.ShortName() + // TODO: CanMaintainerWriteToBranch is a bad name, but it really does what "CanWriteToBranch" does + if !issues_model.CanMaintainerWriteToBranch(ctx, doerRepoPerm, branchName, doer) { + targetRepo = repo_model.GetForkedRepo(ctx, doer.ID, targetRepo.ID) + if targetRepo == nil { + return &CommitFormOptions{NeedFork: true}, nil + } + // now, we get our own forked repo; it must be writable by us. + } + submitToForkedRepo := targetRepo.ID != originRepo.ID + err := targetRepo.GetBaseRepo(ctx) if err != nil { return nil, err } - userCanPush := true - requireSigned := false + + protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName) + if err != nil { + return nil, err + } + canPushWithProtection := true + protectionRequireSigned := false if protectedBranch != nil { - protectedBranch.Repo = r.Repository - userCanPush = protectedBranch.CanUserPush(ctx, doer) - requireSigned = protectedBranch.RequireSignedCommits + protectedBranch.Repo = targetRepo + canPushWithProtection = protectedBranch.CanUserPush(ctx, doer) + protectionRequireSigned = protectedBranch.RequireSignedCommits } - sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) - - canEnableEditor := r.CanEnableEditor(ctx, doer) - canCommit := canEnableEditor && userCanPush - if requireSigned { - canCommit = canCommit && sign - } + willSign, signKeyID, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String()) wontSignReason := "" - if err != nil { - if asymkey_service.IsErrWontSign(err) { - wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) - err = nil - } else { - wontSignReason = "error" - } + if asymkey_service.IsErrWontSign(err) { + wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) + } else if err != nil { + return nil, err + } + + canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection + if protectionRequireSigned { + canCommitToBranch = canCommitToBranch && willSign } - 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 + canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) + canCreatePullRequest := targetRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest - return &CommitFormBehaviors{ - CanCommitToBranch: canCommit, - EditorEnabled: canEnableEditor, - UserCanPush: userCanPush, - RequireSigned: requireSigned, - WillSign: sign, - SigningKey: keyID, + opts := &CommitFormOptions{ + TargetRepo: targetRepo, + WillSubmitToFork: submitToForkedRepo, + CanCommitToBranch: canCommitToBranch, + UserCanPush: canPushWithProtection, + RequireSigned: protectionRequireSigned, + WillSign: willSign, + SigningKey: signKeyID, WontSignReason: wontSignReason, CanCreatePullRequest: canCreatePullRequest, CanCreateBasePullRequest: canCreateBasePullRequest, - }, err + } + editorAction := ctx.PathParam("editor_action") + editorPathParamRemaining := util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + if submitToForkedRepo { + // there is only "default branch" in forked repo, we will use "from_base_branch" to get a new branch from base repo + editorPathParamRemaining = util.PathEscapeSegments(targetRepo.DefaultBranch) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + "?from_base_branch=" + url.QueryEscape(branchName) + } + if editorAction == "_cherrypick" { + opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + ctx.PathParam("sha") + "/" + editorPathParamRemaining + } else { + opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + editorPathParamRemaining + } + if ctx.Req.URL.RawQuery != "" { + opts.TargetFormAction += util.Iif(strings.Contains(opts.TargetFormAction, "?"), "&", "?") + ctx.Req.URL.RawQuery + } + return opts, nil } // CanUseTimetracker returns whether a user can use the timetracker. diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go index 303e7da38b..23707950d4 100644 --- a/services/context/upload/upload.go +++ b/services/context/upload/upload.go @@ -11,7 +11,9 @@ import ( "regexp" "strings" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" ) @@ -106,14 +108,17 @@ func AddUploadContext(ctx *context.Context, uploadType string) { ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",") ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize - case "repo": - ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file" - ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove" - ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file" - 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) } } + +func AddUploadContextForRepo(ctx reqctx.RequestContext, repo *repo_model.Repository) { + ctxData, repoLink := ctx.GetData(), repo.Link() + ctxData["UploadUrl"] = repoLink + "/upload-file" + ctxData["UploadRemoveUrl"] = repoLink + "/upload-remove" + ctxData["UploadLinkUrl"] = repoLink + "/upload-file" + ctxData["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",") + ctxData["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles + ctxData["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize +} diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go index d15d6b6c84..263562a396 100644 --- a/services/packages/container/cleanup.go +++ b/services/packages/container/cleanup.go @@ -13,7 +13,7 @@ import ( container_module "code.gitea.io/gitea/modules/packages/container" packages_service "code.gitea.io/gitea/services/packages" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" ) // Cleanup removes expired container data diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index ae195758b9..fdd428b45c 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -195,7 +195,7 @@ func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload func createTelegramPayloadHTML(msgHTML string) TelegramPayload { // https://core.telegram.org/bots/api#formatting-options return TelegramPayload{ - Message: strings.TrimSpace(markup.Sanitize(msgHTML)), + Message: strings.TrimSpace(string(markup.Sanitize(msgHTML))), ParseMode: "HTML", DisableWebPreview: true, } diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl index f850ebf916..7981fd0761 100644 --- a/templates/repo/editor/cherry_pick.tmpl +++ b/templates/repo/editor/cherry_pick.tmpl @@ -3,8 +3,9 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_cherrypick/{{.FromCommitID}}/{{.BranchName | PathEscapeSegments}}"> + <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"> {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} <input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}"> <div class="repo-editor-header"> <div class="breadcrumb"> diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl index df1b9ac554..7067614444 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 .CommitFormBehaviors.WillSign}} - <span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span> + <h3>{{- if .CommitFormOptions.WillSign}} + <span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormOptions.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" .CommitFormBehaviors.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span> + <span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormOptions.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 .CommitFormBehaviors.CanCommitToBranch}}disabled{{end}}"> + <div class="ui radio checkbox {{if not .CommitFormOptions.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 .CommitFormBehaviors.CanCommitToBranch}} + {{if not .CommitFormOptions.CanCommitToBranch}} <div class="ui visible small warning message"> {{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}} <ul> - {{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}} + {{if not .CommitFormOptions.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}} + {{if and .CommitFormOptions.RequireSigned (not .CommitFormOptions.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 .CommitFormBehaviors.CanCreatePullRequest}} + {{if .CommitFormOptions.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 .CommitFormBehaviors.CanCreatePullRequest}} + {{if .CommitFormOptions.CanCreatePullRequest}} {{ctx.Locale.Tr "repo.editor.create_new_branch"}} {{else}} {{ctx.Locale.Tr "repo.editor.create_new_branch_np"}} diff --git a/templates/repo/editor/common_top.tmpl b/templates/repo/editor/common_top.tmpl new file mode 100644 index 0000000000..23280ed565 --- /dev/null +++ b/templates/repo/editor/common_top.tmpl @@ -0,0 +1,6 @@ +{{if .CommitFormOptions.WillSubmitToFork}} +<div class="ui blue message"> + {{$repoLinkHTML := HTMLFormat `<a href="%s">%s</a>` .CommitFormOptions.TargetRepo.Link .CommitFormOptions.TargetRepo.FullName}} + {{ctx.Locale.Tr "repo.editor.fork_edit_description" $repoLinkHTML}} +</div> +{{end}} diff --git a/templates/repo/editor/delete.tmpl b/templates/repo/editor/delete.tmpl index 6f9963a6bd..bf6143f1cb 100644 --- a/templates/repo/editor/delete.tmpl +++ b/templates/repo/editor/delete.tmpl @@ -3,8 +3,9 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui form form-fetch-action" method="post"> + <form class="ui form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"> {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} {{template "repo/editor/commit_form" .}} </form> </div> diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index 536ed07ca7..0911d02e1f 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -3,11 +3,12 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui edit form form-fetch-action" method="post" + <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}" 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}} + {{template "repo/editor/common_top" .}} <div class="repo-editor-header"> {{template "repo/editor/common_breadcrumb" .}} </div> diff --git a/templates/repo/editor/fork.tmpl b/templates/repo/editor/fork.tmpl new file mode 100644 index 0000000000..e28b2ba7a2 --- /dev/null +++ b/templates/repo/editor/fork.tmpl @@ -0,0 +1,18 @@ +{{template "base/head" .}} +<div role="main" aria-label="{{.Title}}" class="page-content repository"> + {{template "repo/header" .}} + <div class="ui container"> + {{template "base/alert" .}} + <form class="ui form form-fetch-action" method="post" action="{{.RepoLink}}/_fork/{{.BranchName | PathEscapeSegments}}"> + {{.CsrfTokenHtml}} + <div class="tw-text-center"> + <div class="tw-my-[40px]"> + <h3>{{ctx.Locale.Tr "repo.editor.fork_create"}}</h3> + <p>{{ctx.Locale.Tr "repo.editor.fork_create_description"}}</p> + </div> + <button class="ui primary button">{{ctx.Locale.Tr "repo.fork_repo"}}</button> + </div> + </form> + </div> +</div> +{{template "base/footer" .}} diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl index 7f9571b4ae..fa00edd92e 100644 --- a/templates/repo/editor/patch.tmpl +++ b/templates/repo/editor/patch.tmpl @@ -3,11 +3,12 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}" + <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}" 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}} + {{template "repo/editor/common_top" .}} <div class="repo-editor-header"> <div class="breadcrumb"> {{ctx.Locale.Tr "repo.editor.patching"}} diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl index 2e9280e9cd..3e36c77b3b 100644 --- a/templates/repo/editor/upload.tmpl +++ b/templates/repo/editor/upload.tmpl @@ -3,8 +3,9 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui comment form form-fetch-action" method="post"> + <form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"> {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} <div class="repo-editor-header"> {{template "repo/editor/common_breadcrumb" .}} </div> diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl index 8bceafa8bb..f8226ec728 100644 --- a/templates/repo/unicode_escape_prompt.tmpl +++ b/templates/repo/unicode_escape_prompt.tmpl @@ -1,22 +1,22 @@ {{if .EscapeStatus}} {{if .EscapeStatus.HasInvisible}} - <div class="ui warning message unicode-escape-prompt tw-text-left"> + <div class="ui warning message unicode-escape-prompt"> <button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button> <div class="header"> {{ctx.Locale.Tr "repo.invisible_runes_header"}} </div> - <p>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</p> + <div>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</div> {{if .EscapeStatus.HasAmbiguous}} - <p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p> + <div>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</div> {{end}} </div> {{else if .EscapeStatus.HasAmbiguous}} - <div class="ui warning message unicode-escape-prompt tw-text-left"> + <div class="ui warning message unicode-escape-prompt"> <button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button> <div class="header"> {{ctx.Locale.Tr "repo.ambiguous_runes_header"}} </div> - <p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p> + <div>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</div> </div> {{end}} {{end}} diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 2418f24d89..3ba04a9974 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -41,8 +41,8 @@ <a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> {{end}} - {{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} - <button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}> + {{if and .RefFullName.IsBranch (not .IsViewFile)}} + <button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}> {{ctx.Locale.Tr "repo.editor.add_file"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl index 5ebccc69e9..12f0983904 100644 --- a/templates/repo/wiki/new.tmpl +++ b/templates/repo/wiki/new.tmpl @@ -18,7 +18,7 @@ {{ctx.Locale.Tr "repo.wiki.page_name_desc"}} </div> - {{$content := .content}} + {{$content := .WikiEditContent}} {{if not .PageIsWikiEdit}} {{$content = ctx.Locale.Tr "repo.wiki.welcome"}} {{end}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 89befcd7c5..f6f82fb52d 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -62,36 +62,34 @@ {{end}} <div class="wiki-content-parts"> - {{if .sidebarTocContent}} + {{if .WikiSidebarTocHTML}} <div class="render-content markup wiki-content-sidebar wiki-content-toc"> - {{.sidebarTocContent | SafeHTML}} + {{.WikiSidebarTocHTML}} </div> {{end}} - <div class="render-content markup wiki-content-main {{if or .sidebarTocContent .sidebarPresent}}with-sidebar{{end}}"> - {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} - {{.content | SafeHTML}} + <div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML}}with-sidebar{{end}}"> + {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}} + {{.WikiContentHTML}} </div> - {{if .sidebarPresent}} + {{if .WikiSidebarHTML}} <div class="render-content markup wiki-content-sidebar"> {{if and .CanWriteWiki (not .Repository.IsMirror)}} <a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> {{end}} - {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}} - {{.sidebarContent | SafeHTML}} + {{.WikiSidebarHTML}} </div> {{end}} <div class="tw-clear-both"></div> - {{if .footerPresent}} + {{if .WikiFooterHTML}} <div class="render-content markup wiki-content-footer"> {{if and .CanWriteWiki (not .Repository.IsMirror)}} <a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> {{end}} - {{template "repo/unicode_escape_prompt" dict "footerEscapeStatus" .sidebarEscapeStatus "root" $}} - {{.footerContent | SafeHTML}} + {{.WikiFooterHTML}} </div> {{end}} </div> diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index 9e4ebb573f..ac47ed0094 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -7,6 +7,7 @@ import ( "bytes" "fmt" "io" + "maps" "mime/multipart" "net/http" "net/http/httptest" @@ -19,292 +20,278 @@ import ( "code.gitea.io/gitea/models/unittest" 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/modules/util" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestCreateFile(t *testing.T) { +func TestEditor(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content") + sessionUser2 := loginUser(t, "user2") + t.Run("EditFileNotAllowed", testEditFileNotAllowed) + t.Run("DiffPreview", testEditorDiffPreview) + t.Run("CreateFile", testEditorCreateFile) + t.Run("EditFile", func(t *testing.T) { + testEditFile(t, sessionUser2, "user2", "repo1", "master", "README.md", "Hello, World (direct)\n") + testEditFileToNewBranch(t, sessionUser2, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (commit-to-new-branch)\n") + }) + t.Run("PatchFile", testEditorPatchFile) + t.Run("DeleteFile", func(t *testing.T) { + viewLink := "/user2/repo1/src/branch/branch2/README.md" + sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusOK) + testEditorActionPostRequest(t, sessionUser2, "/user2/repo1/_delete/branch2/README.md", map[string]string{"commit_choice": "direct"}) + sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusNotFound) + }) + t.Run("ForkToEditFile", func(t *testing.T) { + testForkToEditFile(t, loginUser(t, "user4"), "user4", "user2", "repo1", "master", "README.md") + }) + t.Run("WebGitCommitEmail", testEditorWebGitCommitEmail) + t.Run("ProtectedBranch", testEditorProtectedBranch) }) } -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) - resp := session.MakeRequest(t, req, http.StatusOK) - - doc := NewHTMLParser(t, resp.Body) - lastCommit := doc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) +func testEditorCreateFile(t *testing.T) { + session := loginUser(t, "user2") + testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content") + testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{ + "tree_path": "test.txt", + "commit_choice": "direct", + "new_branch_name": "master", + }, `A file named "test.txt" already exists in this repository.`) + testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{ + "tree_path": "test.txt", + "commit_choice": "commit-to-new-branch", + "new_branch_name": "master", + }, `Branch "master" already exists in this repository.`) +} - // Save new file to master branch - req = NewRequestWithValues(t, "POST", newURL, map[string]string{ - "_csrf": doc.GetCSRF(), - "last_commit": lastCommit, +func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) { + testEditorActionEdit(t, session, user, repo, "_new", branch, "", map[string]string{ "tree_path": filePath, "content": content, "commit_choice": "direct", }) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEmpty(t, test.RedirectURL(resp)) } -func TestCreateFileOnProtectedBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - - csrf := GetUserCSRFToken(t, session) - // Change master branch to protected - req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ - "_csrf": csrf, - "rule_name": "master", - "enable_push": "true", - }) - session.MakeRequest(t, req, http.StatusSeeOther) - // Check if master branch has been locked successfully - flashMsg := session.GetCookieFlashMessage() - assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg) - - // Request editor page - req = NewRequest(t, "GET", "/user2/repo1/_new/master/") - resp := session.MakeRequest(t, req, http.StatusOK) - - doc := NewHTMLParser(t, resp.Body) - lastCommit := doc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) - - // Save new file to master branch - req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{ - "_csrf": doc.GetCSRF(), - "last_commit": lastCommit, - "tree_path": "test.txt", - "content": "Content", - "commit_choice": "direct", - }) - - 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) - - // Change master branch to protected - req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/1/delete", map[string]string{ - "_csrf": csrf, - }) - - resp = session.MakeRequest(t, req, http.StatusOK) - - res := make(map[string]string) - assert.NoError(t, json.NewDecoder(resp.Body).Decode(&res)) - assert.Equal(t, "/user2/repo1/settings/branches", res["redirect"]) - - // Check if master branch has been locked successfully - flashMsg = session.GetCookieFlashMessage() - assert.Equal(t, `Removing branch protection rule "1" failed.`, flashMsg.ErrorMsg) +func testEditorProtectedBranch(t *testing.T) { + session := loginUser(t, "user2") + // Change the "master" branch to "protected" + req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "rule_name": "master", + "enable_push": "true", }) + session.MakeRequest(t, req, http.StatusSeeOther) + flashMsg := session.GetCookieFlashMessage() + assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg) + + // Try to commit a file to the "master" branch and it should fail + resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"}) + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) } -func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder { - // Get to the 'edit this file' page - req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath)) +func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder { + req := NewRequest(t, "GET", requestPath) resp := session.MakeRequest(t, req, http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - lastCommit := htmlDoc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) - - // Submit the edits - req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath), - map[string]string{ - "_csrf": htmlDoc.GetCSRF(), - "last_commit": lastCommit, - "tree_path": filePath, - "content": newContent, - "commit_choice": "direct", - }, - ) - 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)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, newContent, resp.Body.String()) - - return resp + form := map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "last_commit": htmlDoc.GetInputValueByName("last_commit"), + } + maps.Copy(form, params) + req = NewRequestWithValues(t, "POST", requestPath, form) + return session.MakeRequest(t, req, NoExpectedStatus) } -func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) *httptest.ResponseRecorder { - // Get to the 'edit this file' page - req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath)) - resp := session.MakeRequest(t, req, http.StatusOK) +func testEditorActionPostRequestError(t *testing.T, session *TestSession, requestPath string, params map[string]string, errorMessage string) { + resp := testEditorActionPostRequest(t, session, requestPath, params) + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Equal(t, errorMessage, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) +} - htmlDoc := NewHTMLParser(t, resp.Body) - lastCommit := htmlDoc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) - - // Submit the edits - req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath), - map[string]string{ - "_csrf": htmlDoc.GetCSRF(), - "last_commit": lastCommit, - "tree_path": filePath, - "content": newContent, - "commit_choice": "commit-to-new-branch", - "new_branch_name": targetBranch, - }, - ) - resp = session.MakeRequest(t, req, http.StatusOK) +func testEditorActionEdit(t *testing.T, session *TestSession, user, repo, editorAction, branch, filePath string, params map[string]string) *httptest.ResponseRecorder { + params["tree_path"] = util.IfZero(params["tree_path"], filePath) + newBranchName := util.Iif(params["commit_choice"] == "direct", branch, params["new_branch_name"]) + resp := testEditorActionPostRequest(t, session, fmt.Sprintf("/%s/%s/%s/%s/%s", user, repo, editorAction, branch, filePath), params) + assert.Equal(t, http.StatusOK, resp.Code) assert.NotEmpty(t, test.RedirectURL(resp)) - - // Verify the change - req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath)) + req := NewRequest(t, "GET", path.Join(user, repo, "raw/branch", newBranchName, params["tree_path"])) resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, newContent, resp.Body.String()) - + assert.Equal(t, params["content"], resp.Body.String()) return resp } -func TestEditFile(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - testEditFile(t, session, "user2", "repo1", "master", "README.md", "Hello, World (Edited)\n") +func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) { + testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{ + "content": newContent, + "commit_choice": "direct", }) } -func TestEditFileToNewBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n") +func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) { + testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{ + "content": newContent, + "commit_choice": "commit-to-new-branch", + "new_branch_name": targetBranch, }) } -func TestWebGitCommitEmail(t *testing.T) { - onGiteaRun(t, func(t *testing.T, _ *url.URL) { - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - require.True(t, user.KeepEmailPrivate) - - repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath()) - defer gitRepo.Close() - getLastCommit := func(t *testing.T) *git.Commit { - c, err := gitRepo.GetBranchCommit("master") - require.NoError(t, err) - return c - } +func testEditorDiffPreview(t *testing.T) { + session := loginUser(t, "user2") + req := NewRequestWithValues(t, "POST", "/user2/repo1/_preview/master/README.md", map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "content": "Hello, World (Edited)\n", + }) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), `<span class="added-code">Hello, World (Edited)</span>`) +} - session := loginUser(t, user.Name) - - makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder { - lastCommit := getLastCommit(t) - params["_csrf"] = GetUserCSRFToken(t, session) - params["last_commit"] = lastCommit.ID.String() - params["commit_choice"] = "direct" - req := NewRequestWithValues(t, "POST", link, params) - resp := session.MakeRequest(t, req, NoExpectedStatus) - newCommit := getLastCommit(t) - if expectedUserName == "" { - require.Equal(t, lastCommit.ID.String(), newCommit.ID.String()) - 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) - assert.Equal(t, expectedEmail, newCommit.Author.Email) - assert.Equal(t, expectedUserName, newCommit.Committer.Name) - assert.Equal(t, expectedEmail, newCommit.Committer.Email) - } - return resp - } +func testEditorPatchFile(t *testing.T) { + session := loginUser(t, "user2") + pathContentCommon := `diff --git a/patch-file-1.txt b/patch-file-1.txt +new file mode 100644 +index 0000000000..aaaaaaaaaa +--- /dev/null ++++ b/patch-file-1.txt +@@ -0,0 +1 @@ ++` + testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/master/", map[string]string{ + "content": pathContentCommon + "patched content\n", + "commit_choice": "commit-to-new-branch", + "new_branch_name": "patched-branch", + }) + resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/raw/branch/patched-branch/patch-file-1.txt"), http.StatusOK) + assert.Equal(t, "patched content\n", resp.Body.String()) + + // patch again, it should fail + resp = testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/patched-branch/", map[string]string{ + "content": pathContentCommon + "another patched content\n", + "commit_choice": "commit-to-new-branch", + "new_branch_name": "patched-branch-1", + }) + assert.Equal(t, "Unable to apply patch", test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) +} - uploadFile := func(t *testing.T, name, content string) string { - body := &bytes.Buffer{} - uploadForm := multipart.NewWriter(body) - file, _ := uploadForm.CreateFormFile("file", name) - _, _ = io.Copy(file, strings.NewReader(content)) - _ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session)) - _ = uploadForm.Close() - - req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body) - req.Header.Add("Content-Type", uploadForm.FormDataContentType()) - resp := session.MakeRequest(t, req, http.StatusOK) - - respMap := map[string]string{} - DecodeJSON(t, resp, &respMap) - return respMap["uuid"] +func testEditorWebGitCommitEmail(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + require.True(t, user.KeepEmailPrivate) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath()) + defer gitRepo.Close() + getLastCommit := func(t *testing.T) *git.Commit { + c, err := gitRepo.GetBranchCommit("master") + require.NoError(t, err) + return c + } + + session := loginUser(t, user.Name) + + makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder { + lastCommit := getLastCommit(t) + params["_csrf"] = GetUserCSRFToken(t, session) + params["last_commit"] = lastCommit.ID.String() + params["commit_choice"] = "direct" + req := NewRequestWithValues(t, "POST", link, params) + resp := session.MakeRequest(t, req, NoExpectedStatus) + newCommit := getLastCommit(t) + if expectedUserName == "" { + require.Equal(t, lastCommit.ID.String(), newCommit.ID.String()) + 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) + assert.Equal(t, expectedEmail, newCommit.Author.Email) + assert.Equal(t, expectedUserName, newCommit.Committer.Name) + assert.Equal(t, expectedEmail, newCommit.Committer.Email) } + return resp + } + + uploadFile := func(t *testing.T, name, content string) string { + body := &bytes.Buffer{} + uploadForm := multipart.NewWriter(body) + file, _ := uploadForm.CreateFormFile("file", name) + _, _ = io.Copy(file, strings.NewReader(content)) + _ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session)) + _ = uploadForm.Close() + + req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body) + req.Header.Add("Content-Type", uploadForm.FormDataContentType()) + resp := session.MakeRequest(t, req, http.StatusOK) - t.Run("EmailInactive", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID}) - require.False(t, email.IsActivated) - makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ - "tree_path": "README.md", - "content": "test content", - "commit_email": email.Email, - }, "", "") - }) + respMap := map[string]string{} + DecodeJSON(t, resp, &respMap) + return respMap["uuid"] + } + + t.Run("EmailInactive", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID}) + require.False(t, email.IsActivated) + makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ + "tree_path": "README.md", + "content": "test content", + "commit_email": email.Email, + }, "", "") + }) + + t.Run("EmailInvalid", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true}) + require.NotEqual(t, email.UID, user.ID) + makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ + "tree_path": "README.md", + "content": "test content", + "commit_email": email.Email, + }, "", "") + }) - t.Run("EmailInvalid", func(t *testing.T) { + testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) { + t.Run("DefaultEmailKeepPrivate", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true}) - require.NotEqual(t, email.UID, user.ID) - makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ - "tree_path": "README.md", - "content": "test content", - "commit_email": email.Email, - }, "", "") + paramsForKeepPrivate["commit_email"] = "" + resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org") }) - - testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) { - t.Run("DefaultEmailKeepPrivate", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - paramsForKeepPrivate["commit_email"] = "" - resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org") - }) - t.Run("ChooseEmail", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - paramsForChosenEmail["commit_email"] = "user2@example.com" - resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com") - }) - return resp1, resp2 - } - - t.Run("Edit", func(t *testing.T) { - testWebGit(t, - "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"}, - "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"}, - ) + t.Run("ChooseEmail", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + paramsForChosenEmail["commit_email"] = "user2@example.com" + resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com") }) + return resp1, resp2 + } + + t.Run("Edit", func(t *testing.T) { + testWebGit(t, + "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"}, + "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"}, + ) + }) - t.Run("UploadDelete", func(t *testing.T) { - file1UUID := uploadFile(t, "file1", "File 1") - file2UUID := uploadFile(t, "file2", "File 2") - testWebGit(t, - "/user2/repo1/_upload/master", map[string]string{"files": file1UUID}, - "/user2/repo1/_upload/master", map[string]string{"files": file2UUID}, - ) - testWebGit(t, - "/user2/repo1/_delete/master/file1", map[string]string{}, - "/user2/repo1/_delete/master/file2", map[string]string{}, - ) - }) + t.Run("UploadDelete", func(t *testing.T) { + file1UUID := uploadFile(t, "file1", "File 1") + file2UUID := uploadFile(t, "file2", "File 2") + testWebGit(t, + "/user2/repo1/_upload/master", map[string]string{"files": file1UUID}, + "/user2/repo1/_upload/master", map[string]string{"files": file2UUID}, + ) + testWebGit(t, + "/user2/repo1/_delete/master/file1", map[string]string{}, + "/user2/repo1/_delete/master/file2", map[string]string{}, + ) + }) - t.Run("ApplyPatchCherryPick", func(t *testing.T) { - testWebGit(t, - "/user2/repo1/_diffpatch/master", map[string]string{ - "tree_path": "__dummy__", - "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt + t.Run("ApplyPatchCherryPick", func(t *testing.T) { + testWebGit(t, + "/user2/repo1/_diffpatch/master", map[string]string{ + "tree_path": "__dummy__", + "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt new file mode 100644 index 0000000000..aaaaaaaaaa --- /dev/null @@ -312,10 +299,10 @@ index 0000000000..aaaaaaaaaa @@ -0,0 +1 @@ +File 1 `, - }, - "/user2/repo1/_diffpatch/master", map[string]string{ - "tree_path": "__dummy__", - "content": `diff --git a/patch-file-2.txt b/patch-file-2.txt + }, + "/user2/repo1/_diffpatch/master", map[string]string{ + "tree_path": "__dummy__", + "content": `diff --git a/patch-file-2.txt b/patch-file-2.txt new file mode 100644 index 0000000000..bbbbbbbbbb --- /dev/null @@ -323,20 +310,146 @@ index 0000000000..bbbbbbbbbb @@ -0,0 +1 @@ +File 2 `, - }, - ) - - commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt") - require.NoError(t, err) - commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt") - require.NoError(t, err) - resp1, _ := testWebGit(t, - "/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"}, - "/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"}, - ) - - // By the way, test the "cherrypick" page: a successful revert redirects to the main branch - assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1)) - }) + }, + ) + + commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt") + require.NoError(t, err) + commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt") + require.NoError(t, err) + resp1, _ := testWebGit(t, + "/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"}, + "/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"}, + ) + + // By the way, test the "cherrypick" page: a successful revert redirects to the main branch + assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1)) + }) +} + +func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) { + forkToEdit := func(t *testing.T, session *TestSession, owner, repo, operation, branch, filePath string) { + // visit the base repo, see the "Add File" button + req := NewRequest(t, "GET", path.Join(owner, repo)) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + AssertHTMLElement(t, htmlDoc, ".repo-add-file", 1) + + // attempt to edit a file, see the guideline page + req = NewRequest(t, "GET", path.Join(owner, repo, operation, branch, filePath)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "Fork Repository to Propose Changes") + + // fork the repository + req = NewRequestWithValues(t, "POST", path.Join(owner, repo, "_fork", branch), map[string]string{"_csrf": GetUserCSRFToken(t, session)}) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.JSONEq(t, `{"redirect":""}`, resp.Body.String()) + } + + t.Run("ForkButArchived", func(t *testing.T) { + // Fork repository because we can't edit it + forkToEdit(t, session, owner, repo, "_edit", branch, filePath) + + // Archive the repository + req := NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"), + map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "repo_name": repo, + "action": "archive", + }, + ) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Check editing archived repository is disabled + req = NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)).SetHeader("Accept", "text/html") + resp := session.MakeRequest(t, req, http.StatusNotFound) + assert.Contains(t, resp.Body.String(), "You have forked this repository but your fork is not editable.") + + // Unfork the repository + req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"), + map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "repo_name": repo, + "action": "convert_fork", + }, + ) + session.MakeRequest(t, req, http.StatusSeeOther) }) + + // Fork repository again, and check the existence of the forked repo with unique name + forkToEdit(t, session, owner, repo, "_edit", branch, filePath) + session.MakeRequest(t, NewRequestf(t, "GET", "/%s/%s-1", user, repo), http.StatusOK) + + t.Run("CheckBaseRepoForm", func(t *testing.T) { + // the base repo's edit form should have the correct action and upload links (pointing to the forked repo) + req := NewRequest(t, "GET", path.Join(owner, repo, "_upload", branch, filePath)+"?foo=bar") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + uploadForm := htmlDoc.doc.Find(".form-fetch-action") + formAction := uploadForm.AttrOr("action", "") + assert.Equal(t, fmt.Sprintf("/%s/%s-1/_upload/%s/%s?from_base_branch=%s&foo=bar", user, repo, branch, filePath, branch), formAction) + uploadLink := uploadForm.Find(".dropzone").AttrOr("data-link-url", "") + assert.Equal(t, fmt.Sprintf("/%s/%s-1/upload-file", user, repo), uploadLink) + newBranchName := uploadForm.Find("input[name=new_branch_name]").AttrOr("value", "") + assert.Equal(t, user+"-patch-1", newBranchName) + commitChoice := uploadForm.Find("input[name=commit_choice][checked]").AttrOr("value", "") + assert.Equal(t, "commit-to-new-branch", commitChoice) + lastCommit := uploadForm.Find("input[name=last_commit]").AttrOr("value", "") + assert.NotEmpty(t, lastCommit) + }) + + t.Run("ViewBaseEditFormAndCommitToFork", func(t *testing.T) { + req := NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + editRequestForm := map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "last_commit": htmlDoc.GetInputValueByName("last_commit"), + "tree_path": filePath, + "content": "new content in fork", + "commit_choice": "commit-to-new-branch", + } + // change a file in the forked repo with existing branch name (should fail) + editRequestForm["new_branch_name"] = "master" + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm) + resp = session.MakeRequest(t, req, http.StatusBadRequest) + respJSON := test.ParseJSONError(resp.Body.Bytes()) + assert.Equal(t, `Branch "master" already exists in your fork, please choose a new branch name.`, respJSON.ErrorMessage) + + // change a file in the forked repo (should succeed) + newBranchName := htmlDoc.GetInputValueByName("new_branch_name") + editRequestForm["new_branch_name"] = newBranchName + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, fmt.Sprintf("/%s/%s/compare/%s...%s/%s-1:%s", owner, repo, branch, user, repo, newBranchName), test.RedirectURL(resp)) + + // check the file in the fork's branch is changed + req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s-1/src/branch/%s/%s", user, repo, newBranchName, filePath)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "new content in fork") + }) +} + +func testEditFileNotAllowed(t *testing.T) { + sessionUser1 := loginUser(t, "user1") // admin, all access + sessionUser4 := loginUser(t, "user4") + // "_cherrypick" has a different route pattern, so skip its test + operations := []string{"_new", "_edit", "_delete", "_upload", "_diffpatch"} + for _, operation := range operations { + t.Run(operation, func(t *testing.T) { + // Branch does not exist + targetLink := path.Join("user2", "repo1", operation, "missing", "README.md") + sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound) + + // Private repository + targetLink = path.Join("user2", "repo2", operation, "master", "Home.md") + sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusOK) + sessionUser4.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound) + + // Empty repository + targetLink = path.Join("org41", "repo61", operation, "master", "README.md") + sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound) + }) + } } diff --git a/tests/integration/html_helper.go b/tests/integration/html_helper.go index 874fc32228..4d589b32e7 100644 --- a/tests/integration/html_helper.go +++ b/tests/integration/html_helper.go @@ -42,7 +42,7 @@ func (doc *HTMLDoc) GetCSRF() string { return doc.GetInputValueByName("_csrf") } -// AssertHTMLElement check if element by selector exists or does not exist depending on checkExists +// AssertHTMLElement check if the element by selector exists or does not exist depending on checkExists func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) { sel := doc.doc.Find(selector) switch v := any(checkExists).(type) { diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index bfcb97b082..49326a594a 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -129,7 +129,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") - testEditFileToNewBranch(t, session, "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1") + testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1") url := path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", url, diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 8f92a51749..c6a89edf25 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -134,7 +134,9 @@ margin-bottom: 16px; } -/* override p:last-child from base.css */ +/* override p:last-child from base.css. +Fomantic assumes that <p>/<hX> elements only have margins between elements, but not for the first's top or last's bottom. +In markup content, we always use bottom margin for all elements */ .markup p:last-child { margin-bottom: 16px; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 85522a0a69..1a05b68dd4 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1834,6 +1834,7 @@ tbody.commit-list { border-radius: 0; display: flex; flex-direction: column; + gap: 0.5em; } /* fomantic's last-child selector does not work with hidden last child */ diff --git a/web_src/css/repo/wiki.css b/web_src/css/repo/wiki.css index ca59dadb9c..144cb1206c 100644 --- a/web_src/css/repo/wiki.css +++ b/web_src/css/repo/wiki.css @@ -39,10 +39,6 @@ min-width: 150px; } -.repository.wiki .wiki-content-sidebar .ui.message.unicode-escape-prompt p { - display: none; -} - .repository.wiki .wiki-content-footer { margin-top: 1em; } diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index 9359713454..a372216ae6 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -5,7 +5,7 @@ import {confirmModal} from './comp/ConfirmModal.ts'; import type {RequestOpts} from '../types.ts'; import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts'; -const {appSubUrl, i18n} = window.config; +const {appSubUrl} = window.config; // fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location" // more details are in the backend's fetch-redirect handler @@ -23,11 +23,20 @@ function fetchActionDoRedirect(redirect: string) { } async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) { + const showErrorForResponse = (code: number, message: string) => { + showErrorToast(`Error ${code || 'request'}: ${message}`); + }; + + let respStatus = 0; + let respText = ''; try { hideToastsAll(); const resp = await request(url, opt); - if (resp.status === 200) { - let {redirect} = await resp.json(); + respStatus = resp.status; + respText = await resp.text(); + const respJson = JSON.parse(respText); + if (respStatus === 200) { + let {redirect} = respJson; redirect = redirect || actionElem.getAttribute('data-redirect'); ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading if (redirect) { @@ -38,22 +47,19 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R return; } - if (resp.status >= 400 && resp.status < 500) { - const data = await resp.json(); + if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) { // 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. - if (data.errorMessage) { - showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'}); - } else { - showErrorToast(`server error: ${resp.status}`); - } + showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'}); } else { - showErrorToast(`server error: ${resp.status}`); + showErrorForResponse(respStatus, respText); } } catch (e) { - if (e.name !== 'AbortError') { - console.error('error when doRequest', e); - showErrorToast(`${i18n.network_error} ${e}`); + if (e.name === 'SyntaxError') { + showErrorForResponse(respStatus, (respText || '').substring(0, 100)); + } else if (e.name !== 'AbortError') { + console.error('fetchActionDoRequest error', e); + showErrorForResponse(respStatus, `${e}`); } } actionElem.classList.remove('is-loading', 'loading-icon-2px'); |