diff --git a/go.mod b/go.mod index 2432f93..1edd6a9 100644 --- a/go.mod +++ b/go.mod @@ -15,11 +15,11 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.63.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/sys v0.33.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect + github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/sys v0.34.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index efe7b40..679d05a 100644 --- a/go.sum +++ b/go.sum @@ -20,30 +20,30 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs= -github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4= +github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= -github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/integration_test/e2e_race_test.go b/internal/integration_test/e2e_race_test.go index da786f3..ecc5e24 100644 --- a/internal/integration_test/e2e_race_test.go +++ b/internal/integration_test/e2e_race_test.go @@ -57,6 +57,8 @@ func Test_mirror_detect_race_clone(t *testing.T) { t.Log("TEST-2: forward HEAD") fileSHA2 := mustCommit(t, upstream, "file", testName+"-2") + time.Sleep(3 * time.Second) // wait for repo.Mirror to grab lock + t.Run("clone-test", func(t *testing.T) { wg := &sync.WaitGroup{} // all following assertions will always be true diff --git a/internal/integration_test/e2e_test.go b/internal/integration_test/e2e_test.go index d6a365c..8363127 100644 --- a/internal/integration_test/e2e_test.go +++ b/internal/integration_test/e2e_test.go @@ -225,11 +225,19 @@ func Test_mirror_head_and_main(t *testing.T) { if err := repo.RemoveWorktreeLink(link2); err != nil { t.Errorf("unable to remove worktree error: %v", err) } + // run mirror loop to remove links + if err := repo.Mirror(txtCtx); err != nil { + t.Fatalf("unable to mirror error: %v", err) + } assertMissingLink(t, root, link2) if err := repo.RemoveWorktreeLink(link1); err != nil { t.Errorf("unable to remove worktree error: %v", err) } + // run mirror loop to remove links + if err := repo.Mirror(txtCtx); err != nil { + t.Fatalf("unable to mirror error: %v", err) + } assertMissingLink(t, root, link1) } @@ -331,11 +339,19 @@ func Test_mirror_other_branch(t *testing.T) { if err := repo.RemoveWorktreeLink(link2); err != nil { t.Errorf("unable to remove worktree error: %v", err) } + // run mirror loop to remove links + if err := repo.Mirror(txtCtx); err != nil { + t.Fatalf("unable to mirror error: %v", err) + } assertMissingLink(t, root, link2) if err := repo.RemoveWorktreeLink(link1); err != nil { t.Errorf("unable to remove worktree error: %v", err) } + // run mirror loop to remove links + if err := repo.Mirror(txtCtx); err != nil { + t.Fatalf("unable to mirror error: %v", err) + } assertMissingLink(t, root, link1) } @@ -1503,6 +1519,9 @@ func Test_RepoPool_Success(t *testing.T) { if err := rp.RemoveWorktreeLink(remote2, "link2"); err != nil { t.Errorf("unable to remove worktree error: %v", err) } + // wait for the mirror + time.Sleep(2 * time.Second) + assertMissingLink(t, root, "link2") if cloneSHA, err := rp.Clone(txtCtx, remote1, tempClone, testMainBranch, nil, false); err != nil { diff --git a/repository/helper.go b/repository/helper.go index fdd86ed..d83c5cd 100644 --- a/repository/helper.go +++ b/repository/helper.go @@ -73,6 +73,22 @@ func publishSymlink(linkPath string, targetPath string) error { return nil } +// readlinkAbs returns the destination of the named symbolic link. +// If the link destination is relative, it will resolve it to an absolute one. +func readlinkAbs(linkPath string) (string, error) { + dst, err := os.Readlink(linkPath) + if err != nil { + return "", err + } + + if filepath.IsAbs(dst) { + return dst, nil + } else { + // Symlink targets are relative to the directory containing the link. + return filepath.Join(filepath.Dir(linkPath), dst), nil + } +} + // removeDirContents iterated the specified dir and removes all contents func removeDirContents(dir string, log *slog.Logger) error { return removeDirContentsIf(dir, log, func(fi os.FileInfo) (bool, error) { diff --git a/repository/repository.go b/repository/repository.go index e9bb443..193972e 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -24,14 +24,13 @@ const ( defaultDirMode fs.FileMode = os.FileMode(0755) // 'rwxr-xr-x' defaultRefSpec = "+refs/*:refs/*" MinAllowedInterval = time.Second + tracerSuffix = "-link-tracker" ) var ( ErrRepoMirrorFailed = errors.New("repository mirror failed") ErrRepoWTUpdateFailed = errors.New("repository worktree update failed") - staleTimeout time.Duration = 10 * time.Second // time for stale worktrees to be cleaned up - // to parse output of "git ls-remote --symref origin HEAD" // ref: refs/heads/xxxx HEAD remoteDefaultBranchRgx = regexp.MustCompile(`^ref:\s+([^\s]+)\s+HEAD`) @@ -524,23 +523,29 @@ func (r *Repository) Mirror(ctx context.Context) error { // so always ensure worktree even if nothing fetched. // continue on error to make sync process more resilient for _, wl := range r.workTreeLinks { - if err := r.ensureWorktreeLink(ctx, wl); err != nil { - r.log.Error("unable to ensure worktree links", "err", err) + if err := r.ensureWorktree(ctx, wl); err != nil { + r.log.Error("unable to ensure worktree", "link", wl.link, "err", err) + wtError = ErrRepoWTUpdateFailed + } + } + // ensure links after all worktrees are checked out for atomic changes + for _, wl := range r.workTreeLinks { + if err := r.ensureWorktreeLink(wl); err != nil { + r.log.Error("unable to ensure worktree links", "link", wl.link, "err", err) wtError = ErrRepoWTUpdateFailed } } - // clean-up can be skipped - if len(refs) == 0 { + // in case of worktree error skip clean-up as it might remove existing + // linked worktree which might not be desired + if wtError != nil { return wtError } - if err := r.cleanup(ctx); err != nil { - r.log.Error("unable to cleanup repo", "err", err) - } + r.cleanup(ctx) r.log.Debug("mirror cycle complete", "time", time.Since(start), "fetch-time", fetchTime, "updated-refs", len(refs)) - return wtError + return nil } // RemoveWorktreeLink removes workTree link from the mirror repository. @@ -549,28 +554,9 @@ func (r *Repository) RemoveWorktreeLink(link string) error { r.lock.Lock() defer r.lock.Unlock() - wl, ok := r.workTreeLinks[link] - if !ok { - return fmt.Errorf("worktree with given link not found") - } - - defer func() { - // remove worktree link from repo object - delete(r.workTreeLinks, link) - // remove published link - if err := os.Remove(wl.linkAbs); err != nil { - r.log.Error("unable to remove published link", "err", err) - } - }() - - wtPath, err := wl.currentWorktree() - if err != nil { - return err - } - - if err := r.removeWorktree(context.TODO(), wtPath); err != nil { - return err - } + // To ensure atomic changes only remove link from the config, actual link + // and worktree will be removed as part for mirror loop in cleanup process. + delete(r.workTreeLinks, link) return nil } @@ -763,9 +749,8 @@ func (r *Repository) hash(ctx context.Context, ref, path string) (string, error) return r.git(ctx, nil, "", args...) } -// ensureWorktreeLink will create / validate worktrees -// it will remove worktree if tracking ref is removed from the remote -func (r *Repository) ensureWorktreeLink(ctx context.Context, wl *WorkTreeLink) error { +// ensureWorktree will create / validate worktrees +func (r *Repository) ensureWorktree(ctx context.Context, wl *WorkTreeLink) error { // get remote hash from mirrored repo for the worktree link remoteHash, err := r.hash(ctx, wl.ref, "") if err != nil { @@ -778,18 +763,18 @@ func (r *Repository) ensureWorktreeLink(ctx context.Context, wl *WorkTreeLink) e return fmt.Errorf("hash not found for given ref:%s for worktree:%s", wl.ref, wl.link) } - var currentHash, currentPath string + var currentHash string // we do not care if we cant get old worktree path as we can create it - currentPath, err = wl.currentWorktree() + wl.dir, err = wl.currentWorktree() if err != nil { // in case of error we create new worktree wl.log.Error("unable to get current worktree path", "err", err) } - if currentPath != "" { + if wl.dir != "" { // get hash from the worktree folder - currentHash, err = r.workTreeHash(ctx, wl, currentPath) + currentHash, err = r.workTreeHash(ctx, wl, wl.dir) if err != nil { // in case of error we create new worktree wl.log.Error("unable to get current worktree hash", "err", err) @@ -800,7 +785,7 @@ func (r *Repository) ensureWorktreeLink(ctx context.Context, wl *WorkTreeLink) e if r.sanityCheckWorktree(ctx, wl) { return nil } - wl.log.Error("worktree failed checks, re-creating...", "path", currentPath) + wl.log.Error("worktree failed checks, re-creating...", "path", wl.dir) } wl.log.Info("worktree update required", "remoteHash", remoteHash, "currentHash", currentHash) @@ -808,16 +793,41 @@ func (r *Repository) ensureWorktreeLink(ctx context.Context, wl *WorkTreeLink) e if err != nil { return fmt.Errorf("unable to create worktree for '%s' err:%w", wl.link, err) } + // swap dir path on success + wl.dir = newPath + + return nil +} + +// ensureWorktreeLink will create/update worktree links +func (r *Repository) ensureWorktreeLink(wl *WorkTreeLink) error { + if wl.dir == "" { + return fmt.Errorf("worktree's checkout dir path not set") + } - if err = publishSymlink(wl.linkAbs, newPath); err != nil { - return fmt.Errorf("unable to publish symlink err:%w", err) + // read symlink path of the given worktree link + currentPath, err := wl.currentWorktree() + if err != nil { + // in case of error we create new worktree + return fmt.Errorf("unable to get current worktree path err:%w", err) + } + + if currentPath != wl.dir { + // publish worktree to given symlink target + if err = publishSymlink(wl.linkAbs, wl.dir); err != nil { + return fmt.Errorf("unable to publish link err:%w", err) + } + wl.log.Info("publishing worktree link", "link", wl.link, "linkAbs", wl.linkAbs) } - // since we use hash to create worktree path it is possible that we - // may have re-created current worktree - if currentPath != "" && currentPath != newPath { - if err := r.removeWorktree(ctx, currentPath); err != nil { - wl.log.Error("unable to remove old worktree", "err", err) + // create symlink in worktree root to keep track of target symlink + // this will be used by cleanup process to remove target symlink if + // worktree is removed + var tracker = wl.dir + tracerSuffix + trackedDstLink, _ := readlinkAbs(tracker) + if wl.linkAbs != trackedDstLink { + if err = publishSymlink(wl.dir+tracerSuffix, wl.linkAbs); err != nil { + return fmt.Errorf("unable to publish link tracking symlink err:%w", err) } } return nil @@ -881,24 +891,29 @@ func (r *Repository) removeWorktree(ctx context.Context, path string) error { } // cleanup removes old worktrees and runs git's garbage collection. -func (r *Repository) cleanup(ctx context.Context) error { - var cleanupErrs []error +func (r *Repository) cleanup(ctx context.Context) bool { + var success bool + + // Clean up stale worktrees and links. + success = r.removeStaleWorktreeLinks() - // Clean up previous worktree(s). if _, err := r.removeStaleWorktrees(); err != nil { - cleanupErrs = append(cleanupErrs, err) + r.log.Error("cleanup: unable to remove stale worktree", "err", err) + success = false } // Let git know we don't need those old commits any more. // git worktree prune -v if _, err := r.git(ctx, nil, "", "worktree", "prune", "--verbose"); err != nil { - cleanupErrs = append(cleanupErrs, err) + r.log.Error("cleanup: git worktree prune failed", "err", err) + success = false } // Expire old refs. // git reflog expire --expire-unreachable=all --all if _, err := r.git(ctx, nil, "", "reflog", "expire", "--expire-unreachable=all", "--all"); err != nil { - cleanupErrs = append(cleanupErrs, err) + r.log.Error("cleanup: git reflog failed", "err", err) + success = false } // Run GC if needed. @@ -913,14 +928,76 @@ func (r *Repository) cleanup(ctx context.Context) error { args = append(args, "--aggressive") } if _, err := r.git(ctx, nil, "", args...); err != nil { - cleanupErrs = append(cleanupErrs, err) + r.log.Error("cleanup: git gc failed", "err", err) + success = false } } - if len(cleanupErrs) > 0 { - return fmt.Errorf("%s", cleanupErrs) + return success +} + +// removeStaleWorktreeLinks will clear stale links by comparing links in config +// and tracker links on disk. +func (r *Repository) removeStaleWorktreeLinks() bool { + success := true + var configLinks []string + + for _, wl := range r.workTreeLinks { + configLinks = append(configLinks, wl.linkAbs) } - return nil + + // map of abs path of tracker to the abs path of its link + onDiskTrackedLinks := make(map[string]string) + dirents, err := os.ReadDir(r.worktreesRoot()) + if err != nil { + r.log.Error("unable to read link worktree root dir", "err", err) + return false + } + + for _, fi := range dirents { + if fi.IsDir() { + continue + } + + if strings.HasSuffix(fi.Name(), tracerSuffix) { + tracker := filepath.Join(r.worktreesRoot(), fi.Name()) + trackedDstLink, err := readlinkAbs(tracker) + if err != nil { + r.log.Error("unable to read link tracking symlink", "file", fi.Name(), "err", err) + success = false + continue + } + onDiskTrackedLinks[tracker] = trackedDstLink + } + } + + for tracker, trackedDstLink := range onDiskTrackedLinks { + if slices.Contains(configLinks, trackedDstLink) { + continue + } + + // read link of tracked dst file and confirm its a actually pointing + // to the stale worktree + if wtPath, err := readlinkAbs(trackedDstLink); err == nil { + if wtPath == strings.TrimSuffix(tracker, tracerSuffix) { + if err := os.Remove(trackedDstLink); err != nil { + r.log.Error("unable to remove stale published link", "link", trackedDstLink, "err", err) + success = false + continue + } + } + } + + if err := os.Remove(tracker); err != nil { + r.log.Error("unable to remove stale link tracker file", "tracker", tracker, "trackedLink", trackedDstLink, "err", err) + success = false + continue + } + + r.log.Info("stale link removed", "link", trackedDstLink) + } + + return success } func (r *Repository) removeStaleWorktrees() (int, error) { @@ -935,15 +1012,16 @@ func (r *Repository) removeStaleWorktrees() (int, error) { if t != "" { _, wtDir := utils.SplitAbs(t) currentWTDirs = append(currentWTDirs, wtDir) + currentWTDirs = append(currentWTDirs, wtDir+tracerSuffix) } } count := 0 err := removeDirContentsIf(r.worktreesRoot(), r.log, func(fi os.FileInfo) (bool, error) { - // delete files that are over the stale time out, and make sure to never delete the current worktree - if !slices.Contains(currentWTDirs, fi.Name()) && time.Since(fi.ModTime()) > staleTimeout { + // only keep files related to current worktrees + if !slices.Contains(currentWTDirs, fi.Name()) { count++ - r.log.Info("removing stale worktree", "worktree", fi.Name()) + r.log.Info("removing stale file/folder", "worktree", fi.Name()) return true, nil } return false, nil diff --git a/repository/worktree.go b/repository/worktree.go index e269df8..ad0168e 100644 --- a/repository/worktree.go +++ b/repository/worktree.go @@ -14,6 +14,7 @@ import ( type WorkTreeLink struct { link string // link name as its specified in config, might not be unique only use it for logging linkAbs string // the path at which to create a symlink to the worktree dir + dir string // the path of the dir where valid worktree is checked out ref string // the ref of the worktree pathspecs []string // pathspecs of the paths to checkout log *slog.Logger @@ -85,45 +86,40 @@ func (r *Repository) isInsideWorkTree(ctx context.Context, wl *WorkTreeLink, wt // files checked out - git could have died halfway through and the repo will // still pass this check. func (r *Repository) sanityCheckWorktree(ctx context.Context, wl *WorkTreeLink) bool { - wt, err := wl.currentWorktree() - if err != nil { - wl.log.Error("can't get current worktree", "err", err) - return false - } - if wt == "" { + if wl.dir == "" { return false } // If it is empty, we are done. - if empty, err := dirIsEmpty(wt); err != nil { - wl.log.Error("can't list worktree directory", "path", wt, "err", err) + if empty, err := dirIsEmpty(wl.dir); err != nil { + wl.log.Error("can't list worktree directory", "path", wl.dir, "err", err) return false } else if empty { - wl.log.Info("worktree directory is empty", "path", wt) + wl.log.Info("worktree directory is empty", "path", wl.dir) return false } // makes sure path is inside the work tree of the repository - if !r.isInsideWorkTree(ctx, wl, wt) { + if !r.isInsideWorkTree(ctx, wl, wl.dir) { return false } // Check that this is actually the root of the worktree. // git rev-parse --show-toplevel - if root, err := r.git(ctx, nil, wt, "rev-parse", "--show-toplevel"); err != nil { - wl.log.Error("can't get worktree git dir", "path", wt, "err", err) + if root, err := r.git(ctx, nil, wl.dir, "rev-parse", "--show-toplevel"); err != nil { + wl.log.Error("can't get worktree git dir", "path", wl.dir, "err", err) return false } else { - if root != wt { - wl.log.Error("worktree directory is under another worktree", "path", wt, "parent", root) + if root != wl.dir { + wl.log.Error("worktree directory is under another worktree", "path", wl.dir, "parent", root) return false } } // Consistency-check the repo. // git fsck --no-progress --connectivity-only - if _, err := r.git(ctx, nil, wt, "fsck", "--no-progress", "--connectivity-only"); err != nil { - wl.log.Error("repo fsck failed", "path", wt, "err", err) + if _, err := r.git(ctx, nil, wl.dir, "fsck", "--no-progress", "--connectivity-only"); err != nil { + wl.log.Error("repo fsck failed", "path", wl.dir, "err", err) return false }