From 2ef0735353ca8bb3d309c8529d2010eb25724842 Mon Sep 17 00:00:00 2001 From: Timofey Kirillov Date: Tue, 15 Feb 2022 20:50:03 +0300 Subject: [PATCH] fix: "unable to switch worktree" in gitlab Automatically invalidate inconsistent service git worktree which werf creates in the ~/.werf/local_cache/git_worktrees. Signed-off-by: Timofey Kirillov --- pkg/true_git/work_tree.go | 141 ++++++++++++++++++++++++++------------ pkg/util/file.go | 26 +++++++ pkg/util/lines.go | 16 +++++ 3 files changed, 140 insertions(+), 43 deletions(-) create mode 100644 pkg/util/lines.go diff --git a/pkg/true_git/work_tree.go b/pkg/true_git/work_tree.go index 78992b4268..ab4c902b5f 100644 --- a/pkg/true_git/work_tree.go +++ b/pkg/true_git/work_tree.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/werf/werf/pkg/util" + "github.com/werf/lockgate" "github.com/werf/werf/pkg/werf" @@ -72,59 +74,70 @@ func prepareWorkTree(ctx context.Context, repoDir, workTreeCacheDir string, comm } workTreeDir := filepath.Join(workTreeCacheDir, "worktree") - - isWorkTreeDirExist := false - if _, err := os.Stat(workTreeDir); err == nil { - isWorkTreeDirExist = true - } else if !os.IsNotExist(err) { - return "", fmt.Errorf("error accessing %s: %s", workTreeDir, err) - } - - isWorkTreeRegistered := false - if workTreeList, err := GetWorkTreeList(repoDir); err != nil { - return "", fmt.Errorf("unable to get worktree list for repo %s: %s", repoDir, err) - } else { - for _, workTreeDesc := range workTreeList { - if workTreeDesc.Path == workTreeDir { - isWorkTreeRegistered = true - } - } - } - currentCommit := "" currentCommitPath := filepath.Join(workTreeCacheDir, "current_commit") - currentCommitPathExists := true - if _, err := os.Stat(currentCommitPath); os.IsNotExist(err) { - currentCommitPathExists = false - } else if err != nil { - return "", fmt.Errorf("unable to access %s: %s", currentCommitPath, err) - } - - if isWorkTreeDirExist && !isWorkTreeRegistered { - logboek.Context(ctx).Info().LogFDetails("Removing unregistered work tree dir %s of repo %s\n", workTreeDir, repoDir) - if err := os.RemoveAll(currentCommitPath); err != nil { - return "", fmt.Errorf("unable to remove %s: %s", currentCommitPath, err) + _, err := os.Stat(workTreeDir) + switch { + case os.IsNotExist(err): + case err != nil: + return "", fmt.Errorf("unable to access %q: %s", workTreeDir, err) + default: + isWorkTreeRegistered := false + if workTreeList, err := GetWorkTreeList(repoDir); err != nil { + return "", fmt.Errorf("unable to get worktree list for repo %s: %s", repoDir, err) + } else { + for _, workTreeDesc := range workTreeList { + if filepath.ToSlash(workTreeDesc.Path) == filepath.ToSlash(workTreeDir) { + isWorkTreeRegistered = true + } + } + } + if !isWorkTreeRegistered { + logboek.Context(ctx).Default().LogFDetails("Detected unregistered work tree dir %q of repo %s\n", workTreeDir, repoDir) } - currentCommitPathExists = false - if err := os.RemoveAll(workTreeDir); err != nil { - return "", fmt.Errorf("unable to remove invalidated work tree dir %s: %s", workTreeDir, err) + isWorkTreeConsistent, err := verifyWorkTreeConsistency(ctx, repoDir, workTreeDir) + if err != nil { + return "", fmt.Errorf("unable to verify work tree %q consistency: %s", workTreeDir, err) + } + if !isWorkTreeConsistent { + logboek.Context(ctx).Default().LogFDetails("Detected inconsistent work tree dir %q of repo %s\n", workTreeDir, repoDir) } - isWorkTreeDirExist = false - } else if isWorkTreeDirExist && currentCommitPathExists { - if data, err := ioutil.ReadFile(currentCommitPath); err == nil { - currentCommit = strings.TrimSpace(string(data)) - if currentCommit == commit { - return workTreeDir, nil + if !isWorkTreeRegistered || !isWorkTreeConsistent { + logboek.Context(ctx).Default().LogF("Removing invalidated work tree dir %q of repo %s\n", workTreeDir, repoDir) + + if err := os.RemoveAll(currentCommitPath); err != nil { + return "", fmt.Errorf("unable to remove %s: %s", currentCommitPath, err) + } + + if err := os.RemoveAll(workTreeDir); err != nil { + return "", fmt.Errorf("unable to remove invalidated work tree dir %s: %s", workTreeDir, err) } } else { - return "", fmt.Errorf("error reading %s: %s", currentCommitPath, err) - } + currentCommitPathExists := true + if _, err := os.Stat(currentCommitPath); os.IsNotExist(err) { + currentCommitPathExists = false + } else if err != nil { + return "", fmt.Errorf("unable to access %s: %s", currentCommitPath, err) + } + + if currentCommitPathExists { + if data, err := ioutil.ReadFile(currentCommitPath); err == nil { + currentCommit = strings.TrimSpace(string(data)) - if err := os.RemoveAll(currentCommitPath); err != nil { - return "", fmt.Errorf("unable to remove %s: %s", currentCommitPath, err) + if currentCommit == commit { + return workTreeDir, nil + } + } else { + return "", fmt.Errorf("error reading %s: %s", currentCommitPath, err) + } + + if err := os.RemoveAll(currentCommitPath); err != nil { + return "", fmt.Errorf("unable to remove %s: %s", currentCommitPath, err) + } + } } } @@ -149,6 +162,48 @@ func prepareWorkTree(ctx context.Context, repoDir, workTreeCacheDir string, comm return workTreeDir, nil } +func verifyWorkTreeConsistency(ctx context.Context, repoDir, workTreeDir string) (bool, error) { + resolvedGitDir, err := resolveDotGitFile(ctx, filepath.Join(workTreeDir, ".git")) + if err != nil { + return false, fmt.Errorf("unable to resolve dot-git file %q: %s", filepath.Join(workTreeDir, ".git"), err) + } + + if !util.IsSubpathOfBasePath(repoDir, resolvedGitDir) { + return false, nil + } + + _, err = os.Stat(resolvedGitDir) + switch { + case os.IsNotExist(err): + return false, nil + case err != nil: + return false, fmt.Errorf("error accessing resolved dot git dir %q: %s", resolvedGitDir, err) + } + + return true, nil +} + +func resolveDotGitFile(ctx context.Context, dotGitPath string) (string, error) { + data, err := os.ReadFile(dotGitPath) + if err != nil { + return "", fmt.Errorf("error reading %q: %s", dotGitPath, err) + } + + lines := util.SplitLines(string(data)) + if len(lines) == 0 { + goto InvalidDotGit + } + + if !strings.HasPrefix(lines[0], "gitdir: ") { + goto InvalidDotGit + } + + return strings.TrimSpace(strings.TrimPrefix(lines[0], "gitdir: ")), nil + +InvalidDotGit: + return "", fmt.Errorf("invalid file format: expected gitdir record") +} + func debugWorktreeSwitch() bool { return os.Getenv("WERF_TRUE_GIT_DEBUG_WORKTREE_SWITCH") == "1" } diff --git a/pkg/util/file.go b/pkg/util/file.go index fcdcdf7bc1..ce079028a4 100644 --- a/pkg/util/file.go +++ b/pkg/util/file.go @@ -3,6 +3,7 @@ package util import ( "os" "strings" + "reflect" ) // FileExists returns true if path exists @@ -39,3 +40,28 @@ func isNotExistError(err error) bool { func IsNotADirectoryError(err error) bool { return strings.HasSuffix(err.Error(), "not a directory") } + +func IsSubpathOfBasePath(basePath, path string) bool { + basePathParts := SplitFilepath(basePath) + pathParts := SplitFilepath(path) + + if len(basePathParts) > len(pathParts) { + return false + } + + if reflect.DeepEqual(basePathParts, pathParts) { + return false + } + + for ind := range basePathParts { + if basePathParts[ind] == "" { + continue + } + + if basePathParts[ind] != pathParts[ind] { + return false + } + } + + return true +} diff --git a/pkg/util/lines.go b/pkg/util/lines.go new file mode 100644 index 0000000000..7acf5107be --- /dev/null +++ b/pkg/util/lines.go @@ -0,0 +1,16 @@ +package util + +import ( + "bufio" + "strings" +) + +func SplitLines(s string) []string { + var lines []string + sc := bufio.NewScanner(strings.NewReader(s)) + sc.Split(bufio.ScanLines) + for sc.Scan() { + lines = append(lines, sc.Text()) + } + return lines +}