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)