diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index b7365e40c4..85d28836f1 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -29,8 +29,10 @@ func TestShortSha(t *testing.T) { func TestVerifyTimeLimitCode(t *testing.T) { defer test.MockVariableValue(&setting.InstallLock, true)() initGeneralSecret := func(secret string) { - setting.InstallLock = true setting.CfgProvider, _ = setting.NewConfigProviderFromData(fmt.Sprintf(` +[security] +INTERNAL_TOKEN = dummy +INSTALL_LOCK = true [oauth2] JWT_SECRET = %s `, secret)) diff --git a/modules/git/gitcmd/command_test.go b/modules/git/gitcmd/command_test.go index 662356bc3f..6e4214d995 100644 --- a/modules/git/gitcmd/command_test.go +++ b/modules/git/gitcmd/command_test.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "os" + "strings" "testing" "time" @@ -49,9 +50,11 @@ func TestRunWithContextStd(t *testing.T) { stdout, stderr, err := cmd.RunStdString(t.Context()) if assert.Error(t, err) { assert.Equal(t, stderr, err.Stderr()) - assert.Equal(t, "fatal: Not a valid object name no-such\n", err.Stderr()) + stderrLower := strings.ToLower(stderr) // see: IsStdErrorNotValidObjectName + assert.Equal(t, "fatal: not a valid object name no-such\n", stderrLower) // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message - assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such", err.Error()) + errLower := strings.ToLower(err.Error()) + assert.Equal(t, "exit status 128 - fatal: not a valid object name no-such", errLower) assert.Empty(t, stdout) } } @@ -61,9 +64,11 @@ func TestRunWithContextStd(t *testing.T) { stdout, stderr, err := cmd.RunStdBytes(t.Context()) if assert.Error(t, err) { assert.Equal(t, string(stderr), err.Stderr()) - assert.Equal(t, "fatal: Not a valid object name no-such\n", err.Stderr()) + stderrLower := strings.ToLower(err.Stderr()) // see: IsStdErrorNotValidObjectName + assert.Equal(t, "fatal: not a valid object name no-such\n", stderrLower) // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message - assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such", err.Error()) + errLower := strings.ToLower(err.Error()) + assert.Equal(t, "exit status 128 - fatal: not a valid object name no-such", errLower) assert.Empty(t, stdout) } } diff --git a/modules/git/gitcmd/error.go b/modules/git/gitcmd/error.go index 066b37f10d..b674068c40 100644 --- a/modules/git/gitcmd/error.go +++ b/modules/git/gitcmd/error.go @@ -77,6 +77,13 @@ func IsErrorCanceledOrKilled(err error) bool { return errors.Is(err, context.Canceled) || IsErrorSignalKilled(err) } +func IsStdErrorNotValidObjectName(err error) bool { + stderr, ok := ErrorAsStderr(err) + // Git is lowercasing the "fatal: Not a valid object name" error message + // ref: https://lore.kernel.org/git/pull.2052.git.1771836302101.gitgitgadget@gmail.com + return ok && strings.Contains(strings.ToLower(stderr), "fatal: not a valid object name") +} + type pipelineError struct { error } diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index d50c1ad629..5d951dad0d 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -65,7 +65,7 @@ func (t *Tree) ListEntries() (Entries, error) { stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(t.repo.Path).RunStdBytes(t.repo.Ctx) if runErr != nil { - if strings.Contains(runErr.Error(), "fatal: Not a valid object name") || strings.Contains(runErr.Error(), "fatal: not a tree object") { + if gitcmd.IsStdErrorNotValidObjectName(runErr) || strings.Contains(runErr.Error(), "fatal: not a tree object") { return nil, ErrNotExist{ ID: t.ID.String(), } diff --git a/modules/setting/security.go b/modules/setting/security.go index 743df61681..a1fd0bce2e 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -109,7 +109,6 @@ func generateSaveInternalToken(rootCfg ConfigProvider) { func loadSecurityFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("security") - InstallLock = HasInstallLock(rootCfg) LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(31) SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY") if SecretKey == "" { diff --git a/modules/setting/setting.go b/modules/setting/setting.go index dc60d99bd6..f2b6274edc 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -108,6 +108,9 @@ func LoadCommonSettings() { // loadCommonSettingsFrom loads common configurations from a configuration provider. func loadCommonSettingsFrom(cfg ConfigProvider) error { + // a lot of logic depends on InstallLock value, so it must be loaded before any other settings + InstallLock = HasInstallLock(cfg) + // WARNING: don't change the sequence except you know what you are doing. loadRunModeFrom(cfg) loadLogGlobalFrom(cfg) diff --git a/modules/util/path.go b/modules/util/path.go index 0cb8ab7ece..48447d7b90 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -64,7 +64,7 @@ func PathJoinRelX(elem ...string) string { return PathJoinRel(elems...) } -const pathSeparator = string(os.PathSeparator) +const filepathSeparator = string(os.PathSeparator) // FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately. // All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators. @@ -82,7 +82,7 @@ func FilePathJoinAbs(base string, sub ...string) string { if isOSWindows() { elems[0] = filepath.Clean(base) } else { - elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator)) + elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", filepathSeparator)) } if !filepath.IsAbs(elems[0]) { // This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead @@ -93,9 +93,9 @@ func FilePathJoinAbs(base string, sub ...string) string { continue } if isOSWindows() { - elems = append(elems, filepath.Clean(pathSeparator+s)) + elems = append(elems, filepath.Clean(filepathSeparator+s)) } else { - elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator))) + elems = append(elems, filepath.Clean(filepathSeparator+strings.ReplaceAll(s, "\\", filepathSeparator))) } } // the elems[0] must be an absolute path, just join them together @@ -115,12 +115,72 @@ func IsDir(dir string) (bool, error) { return false, err } -func IsRegularFile(filePath string) (bool, error) { - f, err := os.Lstat(filePath) - if err == nil { - return f.Mode().IsRegular(), nil +var ErrNotRegularPathFile = errors.New("not a regular file") + +// ReadRegularPathFile reads a file with given sub path in root dir. +// It returns error when the path is not a regular file, or any parent path is not a regular directory. +func ReadRegularPathFile(root, filePathIn string, limit int) ([]byte, error) { + pathFields := strings.Split(PathJoinRelX(filePathIn), "/") + + targetPathBuilder := strings.Builder{} + targetPathBuilder.Grow(len(root) + len(filePathIn) + 2) + targetPathBuilder.WriteString(root) + targetPathString := root + for i, subPath := range pathFields { + targetPathBuilder.WriteByte(filepath.Separator) + targetPathBuilder.WriteString(subPath) + targetPathString = targetPathBuilder.String() + + expectFile := i == len(pathFields)-1 + st, err := os.Lstat(targetPathString) + if err != nil { + return nil, err + } + if expectFile && !st.Mode().IsRegular() || !expectFile && !st.Mode().IsDir() { + return nil, fmt.Errorf("%w: %s", ErrNotRegularPathFile, filePathIn) + } } - return false, err + f, err := os.Open(targetPathString) + if err != nil { + return nil, err + } + defer f.Close() + return ReadWithLimit(f, limit) +} + +// WriteRegularPathFile writes data to a file with given sub path in root dir, it creates parent directories if necessary. +// The file is created with fileMode, and the directories are created with dirMode. +// It returns error when the path already exists but is not a regular file, or any parent path is not a regular directory. +func WriteRegularPathFile(root, filePathIn string, data []byte, dirMode, fileMode os.FileMode) error { + pathFields := strings.Split(PathJoinRelX(filePathIn), "/") + + targetPathBuilder := strings.Builder{} + targetPathBuilder.Grow(len(root) + len(filePathIn) + 2) + targetPathBuilder.WriteString(root) + targetPathString := root + for i, subPath := range pathFields { + targetPathBuilder.WriteByte(filepath.Separator) + targetPathBuilder.WriteString(subPath) + targetPathString = targetPathBuilder.String() + + expectFile := i == len(pathFields)-1 + st, err := os.Lstat(targetPathString) + if err == nil { + if expectFile && !st.Mode().IsRegular() || !expectFile && !st.Mode().IsDir() { + return fmt.Errorf("%w: %s", ErrNotRegularPathFile, filePathIn) + } + continue + } + if !os.IsNotExist(err) { + return err + } + if !expectFile { + if err = os.Mkdir(targetPathString, dirMode); err != nil { + return err + } + } + } + return os.WriteFile(targetPathString, data, fileMode) } // IsExist checks whether a file or directory exists. diff --git a/modules/util/path_test.go b/modules/util/path_test.go index 79c37e55f7..2469088b3a 100644 --- a/modules/util/path_test.go +++ b/modules/util/path_test.go @@ -6,6 +6,7 @@ package util import ( "net/url" "os" + "path/filepath" "runtime" "testing" @@ -230,3 +231,70 @@ func TestListDirRecursively(t *testing.T) { require.NoError(t, err) assert.ElementsMatch(t, []string{"d1/f-d1", "d1/s1/f-d1s1"}, res) } + +func TestReadWriteRegularPathFile(t *testing.T) { + const readLimit = 10000 + tmpDir := t.TempDir() + rootDir := tmpDir + "/root" + _ = os.Mkdir(rootDir, 0o755) + _ = os.WriteFile(tmpDir+"/other-file", []byte("other-content"), 0o755) + _ = os.Mkdir(rootDir+"/real-dir", 0o755) + _ = os.WriteFile(rootDir+"/real-dir/real-file", []byte("dummy-content"), 0o644) + _ = os.Symlink(rootDir+"/real-dir", rootDir+"/link-dir") + _ = os.Symlink(rootDir+"/real-dir/real-file", rootDir+"/real-dir/link-file") + + t.Run("Read", func(t *testing.T) { + content, err := os.ReadFile(filepath.Join(rootDir, "../other-file")) + require.NoError(t, err) + assert.Equal(t, "other-content", string(content)) + + content, err = ReadRegularPathFile(rootDir, "../other-file", readLimit) + require.ErrorIs(t, err, os.ErrNotExist) + assert.Empty(t, string(content)) + + content, err = ReadRegularPathFile(rootDir, "real-dir/real-file", readLimit) + require.NoError(t, err) + assert.Equal(t, "dummy-content", string(content)) + + _, err = ReadRegularPathFile(rootDir, "link-dir/real-file", readLimit) + require.ErrorIs(t, err, ErrNotRegularPathFile) + _, err = ReadRegularPathFile(rootDir, "real-dir/link-file", readLimit) + require.ErrorIs(t, err, ErrNotRegularPathFile) + _, err = ReadRegularPathFile(rootDir, "link-dir/link-file", readLimit) + require.ErrorIs(t, err, ErrNotRegularPathFile) + }) + + t.Run("Write", func(t *testing.T) { + assertFileContent := func(path, expected string) { + data, err := os.ReadFile(path) + if expected == "" { + assert.ErrorIs(t, err, os.ErrNotExist) + return + } + require.NoError(t, err) + assert.Equal(t, expected, string(data), "file content mismatch for %s", path) + } + + err := WriteRegularPathFile(rootDir, "new-dir/new-file", []byte("new-content"), 0o755, 0o644) + require.NoError(t, err) + assertFileContent(rootDir+"/new-dir/new-file", "new-content") + + err = WriteRegularPathFile(rootDir, "link-dir/real-file", []byte("new-content"), 0o755, 0o644) + require.ErrorIs(t, err, ErrNotRegularPathFile) + err = WriteRegularPathFile(rootDir, "link-dir/link-file", []byte("new-content"), 0o755, 0o644) + require.ErrorIs(t, err, ErrNotRegularPathFile) + err = WriteRegularPathFile(rootDir, "link-dir/new-file", []byte("new-content"), 0o755, 0o644) + require.ErrorIs(t, err, ErrNotRegularPathFile) + err = WriteRegularPathFile(rootDir, "real-dir/link-file", []byte("new-content"), 0o755, 0o644) + require.ErrorIs(t, err, ErrNotRegularPathFile) + + err = WriteRegularPathFile(rootDir, "../other-file", []byte("new-content"), 0o755, 0o644) + require.NoError(t, err) + assertFileContent(rootDir+"/../other-file", "other-content") + assertFileContent(rootDir+"/other-file", "new-content") + + err = WriteRegularPathFile(rootDir, "real-dir/real-file", []byte("changed-content"), 0o755, 0o644) + require.NoError(t, err) + assertFileContent(rootDir+"/real-dir/real-file", "changed-content") + }) +} diff --git a/options/locale/locale_fr-FR.json b/options/locale/locale_fr-FR.json index 22be98d2ff..caa1b7dbf3 100644 --- a/options/locale/locale_fr-FR.json +++ b/options/locale/locale_fr-FR.json @@ -148,6 +148,13 @@ "filter.private": "Privé", "no_results_found": "Aucun résultat trouvé.", "internal_error_skipped": "Une erreur interne est survenue, mais ignorée : %s", + "characters_spaces": "Espaces", + "characters_tabs": "Tabulations", + "text_indent_style": "Style d’indentation", + "text_indent_size": "Taille de l’indentation", + "text_line_wrap": "Retour à la ligne", + "text_line_nowrap": "Pas de retour à la ligne", + "text_line_wrap_mode": "Mode de retour automatique à la ligne", "search.search": "Rechercher…", "search.type_tooltip": "Type de recherche", "search.fuzzy": "Approximative", @@ -751,6 +758,7 @@ "settings.add_email": "Ajouter un courriel", "settings.add_openid": "Ajouter une URI OpenID", "settings.add_email_confirmation_sent": "Un courriel de confirmation a été envoyé à « %s ». Veuillez vérifier votre boîte de réception dans les %s suivants pour confirmer votre adresse.", + "settings.email_primary_not_found": "L’adresse courriel sélectionnée est introuvable.", "settings.add_email_success": "La nouvelle adresse a été ajoutée.", "settings.email_preference_set_success": "Le courriel de préférence a été défini avec succès.", "settings.add_openid_success": "La nouvelle adresse OpenID a été ajoutée.", @@ -1490,6 +1498,7 @@ "repo.issues.filter_sort.feweststars": "Favoris (croissant)", "repo.issues.filter_sort.mostforks": "Bifurcations (décroissant)", "repo.issues.filter_sort.fewestforks": "Bifurcations (croissant)", + "repo.issues.quick_goto": "Allez au ticket", "repo.issues.action_open": "Ouvrir", "repo.issues.action_close": "Fermer", "repo.issues.action_label": "Label", @@ -1777,6 +1786,8 @@ "repo.pulls.title_desc": "souhaite fusionner %[1]d révision(s) depuis %[2]s vers %[3]s", "repo.pulls.merged_title_desc": "a fusionné %[1]d révision(s) à partir de %[2]s vers %[3]s %[4]s", "repo.pulls.change_target_branch_at": "a remplacée la branche cible %s par %s %s.", + "repo.pulls.marked_as_work_in_progress_at": "a marqué la demande d’ajout comme travail en cours %s", + "repo.pulls.marked_as_ready_for_review_at": "a marqué la demande d’ajout comme prête pour relecture %s", "repo.pulls.tab_conversation": "Discussion", "repo.pulls.tab_commits": "Révisions", "repo.pulls.tab_files": "Fichiers Modifiés", @@ -1795,6 +1806,7 @@ "repo.pulls.remove_prefix": "Enlever le préfixe %s", "repo.pulls.data_broken": "Cette demande d’ajout est impossible par manque d'informations de bifurcation.", "repo.pulls.files_conflicted": "Cette demande d'ajout contient des modifications en conflit avec la branche ciblée.", + "repo.pulls.files_conflicted_no_listed_files": "(Aucun fichier en conflit répertorié)", "repo.pulls.is_checking": "Recherche de conflits de fusion…", "repo.pulls.is_ancestor": "Cette branche est déjà présente dans la branche ciblée. Il n'y a rien à fusionner.", "repo.pulls.is_empty": "Les changements sur cette branche sont déjà sur la branche cible. Cette révision sera vide.", @@ -1865,6 +1877,7 @@ "repo.pulls.update_not_allowed": "Vous n'êtes pas autorisé à mettre à jour la branche", "repo.pulls.outdated_with_base_branch": "Cette branche est désynchronisée avec la branche de base", "repo.pulls.close": "Fermer la demande d’ajout", + "repo.pulls.reopen": "Rouvrir la demande d’ajout", "repo.pulls.closed_at": "a fermé cette demande d'ajout %[2]s.", "repo.pulls.reopened_at": "a rouvert cette demande d'ajout %[2]s.", "repo.pulls.cmd_instruction_hint": "Voir les instructions en ligne de commande", @@ -2120,6 +2133,8 @@ "repo.settings.pulls.ignore_whitespace": "Ignorer les espaces lors des conflits", "repo.settings.pulls.enable_autodetect_manual_merge": "Activer la détection automatique de la fusion manuelle (Remarque : dans certains cas particuliers, des erreurs de détection peuvent se produire)", "repo.settings.pulls.allow_rebase_update": "Activer la mise à jour de demande d'ajout par rebase", + "repo.settings.pulls.default_target_branch": "Branche cible par défaut pour les nouvelles demandes d’ajout", + "repo.settings.pulls.default_target_branch_default": "Branche par défaut (%s)", "repo.settings.pulls.default_delete_branch_after_merge": "Supprimer la branche après la fusion par default", "repo.settings.pulls.default_allow_edits_from_maintainers": "Autoriser les modifications par les mainteneurs par défaut", "repo.settings.releases_desc": "Activer les publications du dépôt", @@ -2432,7 +2447,8 @@ "repo.settings.block_outdated_branch_desc": "La fusion ne sera pas possible lorsque la branche principale est derrière la branche de base.", "repo.settings.block_admin_merge_override": "Les administrateurs doivent respecter les règles de protection des branches", "repo.settings.block_admin_merge_override_desc": "Les administrateurs doivent respecter les règles de protection des branches et ne peuvent pas les contourner.", - "repo.settings.default_branch_desc": "Sélectionnez une branche par défaut pour les demandes de fusion et les révisions :", + "repo.settings.default_branch_desc": "Sélectionnez une branche par défaut pour les révisions.", + "repo.settings.default_target_branch_desc": "Les demandes d’ajout peuvent utiliser une branche cible différente, telle que définie dans la section Demandes d’ajouts des Paramètres avancés du dépôt.", "repo.settings.merge_style_desc": "Styles de fusion", "repo.settings.default_merge_style_desc": "Méthode de fusion par défaut", "repo.settings.choose_branch": "Choisissez une branche…", @@ -2646,7 +2662,7 @@ "repo.branch.restore_success": "La branche \"%s\" a été restaurée.", "repo.branch.restore_failed": "Impossible de restaurer la branche \"%s\".", "repo.branch.protected_deletion_failed": "La branche \"%s\" est protégé. Elle ne peut pas être supprimée.", - "repo.branch.default_deletion_failed": "La branche \"%s\" est la branche par défaut. Elle ne peut pas être supprimée.", + "repo.branch.default_deletion_failed": "« %s » est la branche par défaut ou la cible de demandes d’ajout. Elle ne peut pas être supprimée.", "repo.branch.default_branch_not_exist": "La branche par défaut « %s » n‘existe pas.", "repo.branch.restore": "Restaurer la branche \"%s\"", "repo.branch.download": "Télécharger la branche \"%s\"", @@ -2663,7 +2679,7 @@ "repo.branch.new_branch_from": "Créer une nouvelle branche à partir de \"%s\"", "repo.branch.renamed": "La branche %s à été renommée en %s.", "repo.branch.rename_default_or_protected_branch_error": "Seuls les administrateurs peuvent renommer les branches par défaut ou protégées.", - "repo.branch.rename_protected_branch_failed": "Cette branche est protégée par des règles de protection basées sur des globs.", + "repo.branch.rename_protected_branch_failed": "Impossible de renommer cette branche en raison des règles de protection de branche.", "repo.branch.commits_divergence_from": "Divergence de révisions : %[1]d en retard et %[2]d en avance sur %[3]s", "repo.branch.commits_no_divergence": "Identique à la branche %[1]s", "repo.tag.create_tag": "Créer l'étiquette %s", @@ -3679,6 +3695,7 @@ "actions.runs.delete.description": "Êtes-vous sûr de vouloir supprimer définitivement cette exécution ? Cette action ne peut pas être annulée.", "actions.runs.not_done": "Cette exécution du flux de travail n’est pas terminée.", "actions.runs.view_workflow_file": "Voir le fichier du flux de travail", + "actions.runs.workflow_graph": "Graphique du flux", "actions.workflow.disable": "Désactiver le flux de travail", "actions.workflow.disable_success": "Le flux de travail « %s » a bien été désactivé.", "actions.workflow.enable": "Activer le flux de travail", diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 14123c5002..ad00325b02 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -148,6 +148,13 @@ "filter.private": "Príobháideach", "no_results_found": "Níor aimsíodh aon torthaí.", "internal_error_skipped": "Tharla earráid inmheánach ach éirithe as: %s", + "characters_spaces": "Spásanna", + "characters_tabs": "Cluaisíní", + "text_indent_style": "Stíl eangaithe", + "text_indent_size": "Méid an línithe", + "text_line_wrap": "Fillte", + "text_line_nowrap": "Gan fillte", + "text_line_wrap_mode": "Mód fillte líne", "search.search": "Cuardaigh…", "search.type_tooltip": "Cineál cuardaigh", "search.fuzzy": "Doiléir", @@ -751,6 +758,7 @@ "settings.add_email": "Cuir Seoladh R-phoist leis", "settings.add_openid": "Cuir OpenID URI", "settings.add_email_confirmation_sent": "Seoladh ríomhphost deimhnithe chuig “%s”. Seiceáil do bhosca isteach laistigh den chéad %s eile chun do sheoladh ríomhphoist a dhearbhú.", + "settings.email_primary_not_found": "Níorbh fhéidir an seoladh ríomhphoist roghnaithe a aimsiú.", "settings.add_email_success": "Cuireadh an seoladh ríomhphoist nua leis.", "settings.email_preference_set_success": "Socraíodh rogha ríomhphoist go rathúil.", "settings.add_openid_success": "Cuireadh an seoladh OpenID nua leis.", @@ -1525,7 +1533,7 @@ "repo.issues.comment_pull_merged_at": "cumasc tiomantas %[1]s le %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "cumasc tiomantas %[1]s le %[2]s %[3]s", "repo.issues.close_comment_issue": "Dún le trácht", - "repo.issues.reopen_issue": "Athoscail", + "repo.issues.reopen_issue": "Athoscail an Cheist", "repo.issues.reopen_comment_issue": "Athoscail le trácht", "repo.issues.create_comment": "Trácht", "repo.issues.comment.blocked_user": "Ní féidir trácht a chruthú nó a chur in eagar toisc go bhfuil an tráchtaire nó úinéir an stórais bac ort.", @@ -1869,6 +1877,7 @@ "repo.pulls.update_not_allowed": "Ní cheadaítear duit brainse a nuashonrú", "repo.pulls.outdated_with_base_branch": "Tá an brainse seo as dáta leis an mbunbhrainse", "repo.pulls.close": "Dún Iarratas Tarraing", + "repo.pulls.reopen": "Athoscail Iarratas Tarraingthe", "repo.pulls.closed_at": "dhún an t-iarratas tarraingthe seo %[2]s", "repo.pulls.reopened_at": "athoscail an t-iarratas tarraingthe seo %[2]s", "repo.pulls.cmd_instruction_hint": "Féach ar threoracha na n-orduithe", @@ -3686,6 +3695,7 @@ "actions.runs.delete.description": "An bhfuil tú cinnte gur mian leat an rith sreabha oibre seo a scriosadh go buan? Ní féidir an gníomh seo a chealú.", "actions.runs.not_done": "Níl an rith sreabha oibre seo críochnaithe.", "actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre", + "actions.runs.workflow_graph": "Graf Sreabhadh Oibre", "actions.workflow.disable": "Díchumasaigh sreabhadh oibre", "actions.workflow.disable_success": "D'éirigh le sreabhadh oibre '%s' a dhíchumasú.", "actions.workflow.enable": "Cumasaigh sreabhadh oibre", diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index d306927001..0578ab540f 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -282,7 +282,7 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) compareInfo, err := git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo, git.RefName(baseCommit), git.RefName(pull.GetGitHeadRefName()), false, false) if err != nil { - if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") { + if gitcmd.IsStdErrorNotValidObjectName(err) || strings.Contains(err.Error(), "unknown revision or path not in the working tree") { ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 @@ -442,7 +442,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_s compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, git.RefName(pull.MergeBase), git.RefName(pull.GetGitHeadRefName()), false, false) if err != nil { - if strings.Contains(err.Error(), "fatal: Not a valid object name") { + if gitcmd.IsStdErrorNotValidObjectName(err) { ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 @@ -584,7 +584,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_s compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, git.RefNameFromBranch(pull.BaseBranch), git.RefName(pull.GetGitHeadRefName()), false, false) if err != nil { - if strings.Contains(err.Error(), "fatal: Not a valid object name") { + if gitcmd.IsStdErrorNotValidObjectName(err) { ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 diff --git a/services/convert/issue.go b/services/convert/issue.go index b396dd0737..fe4870b5db 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -13,6 +13,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -226,7 +227,21 @@ func ToStopWatches(ctx context.Context, doer *user_model.User, sws []*issues_mod // ToTrackedTimeList converts TrackedTimeList to API format func ToTrackedTimeList(ctx context.Context, doer *user_model.User, tl issues_model.TrackedTimeList) api.TrackedTimeList { result := make([]*api.TrackedTime, 0, len(tl)) + permCache := cache.NewEphemeralCache() for _, t := range tl { + // If the issue is not loaded, conservatively skip this entry to avoid bypassing permission checks. + if t.Issue == nil || t.Issue.Repo == nil { + continue + } + perm, err := cache.GetWithEphemeralCache(ctx, permCache, "repo-perm", t.Issue.RepoID, func(ctx context.Context, repoID int64) (access_model.Permission, error) { + return access_model.GetUserRepoPermission(ctx, t.Issue.Repo, doer) + }) + if err != nil { + continue + } + if !perm.CanReadIssuesOrPulls(t.Issue.IsPull) { + continue + } result = append(result, ToTrackedTime(ctx, doer, t)) } return result diff --git a/services/convert/issue_test.go b/services/convert/issue_test.go index a12a69288a..109bf63e7d 100644 --- a/services/convert/issue_test.go +++ b/services/convert/issue_test.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLabel_ToLabel(t *testing.T) { @@ -83,3 +84,43 @@ func TestToStopWatchesRespectsPermissions(t *testing.T) { assert.Len(t, visibleAdmin, 2) assert.ElementsMatch(t, []string{"repo1", "repo3"}, []string{visibleAdmin[0].RepoName, visibleAdmin[1].RepoName}) } + +func TestToTrackedTime(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + ctx := t.Context() + publicIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 1}) + privateIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 3}) + regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + publicTrackedTime := &issues_model.TrackedTime{IssueID: publicIssue.ID, UserID: regularUser.ID, Time: 3600} + privateTrackedTime := &issues_model.TrackedTime{IssueID: privateIssue.ID, UserID: regularUser.ID, Time: 1800} + require.NoError(t, db.Insert(ctx, publicTrackedTime)) + require.NoError(t, db.Insert(ctx, privateTrackedTime)) + + t.Run("NilIssues", func(t *testing.T) { + list := ToTrackedTimeList(ctx, regularUser, issues_model.TrackedTimeList{publicTrackedTime, privateTrackedTime}) + assert.Empty(t, list) + }) + + t.Run("NilRepo", func(t *testing.T) { + badTrackedTime := &issues_model.TrackedTime{Issue: &issues_model.Issue{RepoID: 999999}} + visible := ToTrackedTimeList(ctx, regularUser, issues_model.TrackedTimeList{badTrackedTime}) + assert.Empty(t, visible) + }) + + trackedTimes := issues_model.TrackedTimeList{publicTrackedTime, privateTrackedTime} + require.NoError(t, trackedTimes.LoadAttributes(ctx)) + + t.Run("ToRegularUser", func(t *testing.T) { + list := ToTrackedTimeList(ctx, regularUser, trackedTimes) + require.Len(t, list, 1) + assert.Equal(t, "repo1", list[0].Issue.Repo.Name) + }) + t.Run("ToAdminUser", func(t *testing.T) { + list := ToTrackedTimeList(ctx, adminUser, trackedTimes) + require.Len(t, list, 2) + assert.ElementsMatch(t, []string{"repo1", "repo3"}, []string{list[0].Issue.Repo.Name, list[1].Issue.Repo.Name}) + }) +} diff --git a/services/repository/generate.go b/services/repository/generate.go index bc37bc7bfe..83e9c22e54 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -103,12 +103,12 @@ func generateExpansion(ctx context.Context, src string, templateRepo, generateRe // giteaTemplateFileMatcher holds information about a .gitea/template file type giteaTemplateFileMatcher struct { - LocalFullPath string - globs []glob.Glob + relPath string + globs []glob.Glob } -func newGiteaTemplateFileMatcher(fullPath string, content []byte) *giteaTemplateFileMatcher { - gt := &giteaTemplateFileMatcher{LocalFullPath: fullPath} +func newGiteaTemplateFileMatcher(relPath string, content []byte) *giteaTemplateFileMatcher { + gt := &giteaTemplateFileMatcher{relPath: relPath} gt.globs = make([]glob.Glob, 0) scanner := bufio.NewScanner(bytes.NewReader(content)) for scanner.Scan() { @@ -139,64 +139,44 @@ func (gt *giteaTemplateFileMatcher) Match(s string) bool { return false } -func readLocalTmpRepoFileContent(localPath string, limit int) ([]byte, error) { - ok, err := util.IsRegularFile(localPath) - if err != nil { - return nil, err - } else if !ok { - return nil, fs.ErrNotExist - } - - f, err := os.Open(localPath) - if err != nil { - return nil, err - } - defer f.Close() - - return util.ReadWithLimit(f, limit) -} - func readGiteaTemplateFile(tmpDir string) (*giteaTemplateFileMatcher, error) { - localPath := filepath.Join(tmpDir, ".gitea", "template") - content, err := readLocalTmpRepoFileContent(localPath, 1024*1024) + templateRelPath := filepath.Join(".gitea", "template") + content, err := util.ReadRegularPathFile(tmpDir, templateRelPath, 1024*1024) if err != nil { - return nil, err + return nil, util.Iif(errors.Is(err, util.ErrNotRegularPathFile), os.ErrNotExist, err) } - return newGiteaTemplateFileMatcher(localPath, content), nil + return newGiteaTemplateFileMatcher(templateRelPath, content), nil } func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, templateRepo, generateRepo *repo_model.Repository) error { - tmpFullPath := filepath.Join(tmpDir, tmpDirSubPath) - content, err := readLocalTmpRepoFileContent(tmpFullPath, 1024*1024) + content, err := util.ReadRegularPathFile(tmpDir, tmpDirSubPath, 1024*1024) if err != nil { - return util.Iif(errors.Is(err, fs.ErrNotExist), nil, err) + if errors.Is(err, fs.ErrNotExist) { + return nil + } + return err } - if err := util.Remove(tmpFullPath); err != nil { + if err := os.Remove(util.FilePathJoinAbs(tmpDir, tmpDirSubPath)); err != nil { return err } generatedContent := generateExpansion(ctx, string(content), templateRepo, generateRepo) substSubPath := filePathSanitize(generateExpansion(ctx, tmpDirSubPath, templateRepo, generateRepo)) - newLocalPath := filepath.Join(tmpDir, substSubPath) - regular, err := util.IsRegularFile(newLocalPath) - if canWrite := regular || errors.Is(err, fs.ErrNotExist); !canWrite { - return nil - } - if err := os.MkdirAll(filepath.Dir(newLocalPath), 0o755); err != nil { - return err - } - return os.WriteFile(newLocalPath, []byte(generatedContent), 0o644) + return util.WriteRegularPathFile(tmpDir, substSubPath, []byte(generatedContent), 0o755, 0o644) } -func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, fileMatcher *giteaTemplateFileMatcher) error { - if err := util.Remove(fileMatcher.LocalFullPath); err != nil { - return fmt.Errorf("unable to remove .gitea/template: %w", err) +// processGiteaTemplateFile processes and removes the .gitea/template file, does variable expansion for template files +// and save the processed files to the filesystem. It returns a list of skipped files that are not regular paths. +func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, fileMatcher *giteaTemplateFileMatcher) (skippedFiles []string, _ error) { + // Why not use "os.Root" here: symlink is unsafe even in the same root but "os.Root" can't help, it's more difficult to use "os.Root" to do the WalkDir. + if err := os.Remove(util.FilePathJoinAbs(tmpDir, fileMatcher.relPath)); err != nil { + return nil, fmt.Errorf("unable to remove .gitea/template: %w", err) } if !fileMatcher.HasRules() { - return nil // Avoid walking tree if there are no globs + return skippedFiles, nil // Avoid walking tree if there are no globs } - return filepath.WalkDir(tmpDir, func(fullPath string, d os.DirEntry, walkErr error) error { + err := filepath.WalkDir(tmpDir, func(fullPath string, d os.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } @@ -208,10 +188,22 @@ func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, return err } if fileMatcher.Match(filepath.ToSlash(tmpDirSubPath)) { - return substGiteaTemplateFile(ctx, tmpDir, tmpDirSubPath, templateRepo, generateRepo) + err := substGiteaTemplateFile(ctx, tmpDir, tmpDirSubPath, templateRepo, generateRepo) + if errors.Is(err, util.ErrNotRegularPathFile) { + skippedFiles = append(skippedFiles, tmpDirSubPath) + } else if err != nil { + return err + } } return nil }) // end: WalkDir + if err != nil { + return nil, err + } + if err = util.RemoveAll(util.FilePathJoinAbs(tmpDir, ".git")); err != nil { + return nil, err + } + return skippedFiles, nil } func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error { @@ -236,7 +228,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r // Variable expansion fileMatcher, err := readGiteaTemplateFile(tmpDir) if err == nil { - err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, fileMatcher) + _, err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, fileMatcher) if err != nil { return fmt.Errorf("processGiteaTemplateFile: %w", err) } diff --git a/services/repository/generate_test.go b/services/repository/generate_test.go index 432de4dc59..160dbb9a06 100644 --- a/services/repository/generate_test.go +++ b/services/repository/generate_test.go @@ -74,7 +74,7 @@ func TestFilePathSanitize(t *testing.T) { assert.Equal(t, ".", filePathSanitize("/")) } -func TestProcessGiteaTemplateFile(t *testing.T) { +func TestProcessGiteaTemplateFileGenerate(t *testing.T) { tmpDir := filepath.Join(t.TempDir(), "gitea-template-test") assertFileContent := func(path, expected string) { @@ -97,6 +97,8 @@ func TestProcessGiteaTemplateFile(t *testing.T) { assert.Equal(t, expected, link, "symlink target mismatch for %s", path) } + require.NoError(t, os.MkdirAll(tmpDir+"/.git", 0o755)) + require.NoError(t, os.WriteFile(tmpDir+"/.git/config", []byte("git-config-dummy"), 0o644)) require.NoError(t, os.MkdirAll(tmpDir+"/.gitea", 0o755)) require.NoError(t, os.WriteFile(tmpDir+"/.gitea/template", []byte("*\ninclude/**"), 0o644)) require.NoError(t, os.MkdirAll(tmpDir+"/sub", 0o755)) @@ -127,10 +129,20 @@ func TestProcessGiteaTemplateFile(t *testing.T) { assertFileContent("subst-${TEMPLATE_NAME}-to-link", toLinkContent) assertFileContent("subst-${TEMPLATE_NAME}-from-link", fromLinkContent) } + + // case-5 + { + require.NoError(t, os.MkdirAll(tmpDir+"/real-dir", 0o755)) + require.NoError(t, os.WriteFile(tmpDir+"/real-dir/real-file", []byte("origin content"), 0o644)) + require.NoError(t, os.MkdirAll(tmpDir+"/include/subst-${TEMPLATE_NAME}-link-dir", 0o755)) + require.NoError(t, os.WriteFile(tmpDir+"/include/subst-${TEMPLATE_NAME}-link-dir/real-file", []byte("template content"), 0o644)) + require.NoError(t, os.Symlink(tmpDir+"/real-dir", tmpDir+"/include/subst-TemplateRepoName-link-dir")) + } + { // will succeed require.NoError(t, os.WriteFile(tmpDir+"/subst-${TEMPLATE_NAME}-normal", []byte("dummy subst template name normal"), 0o644)) - // will skil if the path subst result is a link + // will be skipped if the path subst result is a link require.NoError(t, os.WriteFile(tmpDir+"/subst-${TEMPLATE_NAME}-to-link", []byte("dummy subst template name to link"), 0o644)) require.NoError(t, os.Symlink(tmpDir+"/sub/link-target", tmpDir+"/subst-TemplateRepoName-to-link")) // will be skipped since the source is a symlink @@ -143,9 +155,20 @@ func TestProcessGiteaTemplateFile(t *testing.T) { { templateRepo := &repo_model.Repository{Name: "TemplateRepoName"} generatedRepo := &repo_model.Repository{Name: "/../.gIt/name"} + assertFileContent(".git/config", "git-config-dummy") fileMatcher, _ := readGiteaTemplateFile(tmpDir) - err := processGiteaTemplateFile(t.Context(), tmpDir, templateRepo, generatedRepo, fileMatcher) + skippedFiles, err := processGiteaTemplateFile(t.Context(), tmpDir, templateRepo, generatedRepo, fileMatcher) require.NoError(t, err) + assert.Equal(t, []string{ + "include/subst-${TEMPLATE_NAME}-link-dir/real-file", + "include/subst-TemplateRepoName-link-dir", + "link", + "subst-${TEMPLATE_NAME}-from-link", + "subst-${TEMPLATE_NAME}-to-link", + "subst-TemplateRepoName-to-link", + }, skippedFiles) + assertFileContent(".git/config", "") + assertFileContent(".gitea/template", "") assertFileContent("include/foo/bar/test.txt", "include subdir TemplateRepoName") } @@ -182,32 +205,38 @@ func TestProcessGiteaTemplateFile(t *testing.T) { assertSymLink("subst-${TEMPLATE_NAME}-from-link", tmpDir+"/sub/link-target") } + // case-5 { - templateFilePath := tmpDir + "/.gitea/template" - - _ = os.Remove(templateFilePath) - _, err := os.Lstat(templateFilePath) - require.ErrorIs(t, err, fs.ErrNotExist) - _, err = readGiteaTemplateFile(tmpDir) // no template file - require.ErrorIs(t, err, fs.ErrNotExist) - - _ = os.WriteFile(templateFilePath+".target", []byte("test-data-target"), 0o644) - _ = os.Symlink(templateFilePath+".target", templateFilePath) - content, _ := os.ReadFile(templateFilePath) - require.Equal(t, "test-data-target", string(content)) - _, err = readGiteaTemplateFile(tmpDir) // symlinked template file - require.ErrorIs(t, err, fs.ErrNotExist) - - _ = os.Remove(templateFilePath) - _ = os.WriteFile(templateFilePath, []byte("test-data-regular"), 0o644) - content, _ = os.ReadFile(templateFilePath) - require.Equal(t, "test-data-regular", string(content)) - fm, err := readGiteaTemplateFile(tmpDir) // regular template file - require.NoError(t, err) - assert.Len(t, fm.globs, 1) + assertFileContent("real-dir/real-file", "origin content") } } +func TestProcessGiteaTemplateFileRead(t *testing.T) { + tmpDir := t.TempDir() + _ = os.Mkdir(tmpDir+"/.gitea", 0o755) + templateFilePath := tmpDir + "/.gitea/template" + _ = os.Remove(templateFilePath) + _, err := os.Lstat(templateFilePath) + require.ErrorIs(t, err, fs.ErrNotExist) + _, err = readGiteaTemplateFile(tmpDir) // no template file + require.ErrorIs(t, err, fs.ErrNotExist) + + _ = os.WriteFile(templateFilePath+".target", []byte("test-data-target"), 0o644) + _ = os.Symlink(templateFilePath+".target", templateFilePath) + content, _ := os.ReadFile(templateFilePath) + require.Equal(t, "test-data-target", string(content)) + _, err = readGiteaTemplateFile(tmpDir) // symlinked template file + require.ErrorIs(t, err, fs.ErrNotExist) + + _ = os.Remove(templateFilePath) + _ = os.WriteFile(templateFilePath, []byte("test-data-regular"), 0o644) + content, _ = os.ReadFile(templateFilePath) + require.Equal(t, "test-data-regular", string(content)) + fm, err := readGiteaTemplateFile(tmpDir) // regular template file + require.NoError(t, err) + assert.Len(t, fm.globs, 1) +} + func TestTransformers(t *testing.T) { cases := []struct { name string diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index a025f26051..5bc62d451e 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "os" - "strings" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -16,6 +15,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/graceful" @@ -59,7 +59,7 @@ func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath // Look for both files filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath) if err != nil { - if strings.Contains(err.Error(), "Not a valid object name") { + if gitcmd.IsStdErrorNotValidObjectName(err) { return false, gitPath, nil // branch doesn't exist } log.Error("Wiki LsTree failed, err: %v", err)