Skip to content

Commit e1c7c63

Browse files
authored
Merge pull request #715 from sviscaino/add-keep-worktrees-option
Add --stale-worktree-timeout option
2 parents 4b7b68d + 622ced3 commit e1c7c63

File tree

2 files changed

+327
-42
lines changed

2 files changed

+327
-42
lines changed

cmd/git-sync/main.go

Lines changed: 98 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ package main // import "k8s.io/git-sync/cmd/git-sync"
2121
import (
2222
"context"
2323
"crypto/md5"
24+
"errors"
2425
"fmt"
2526
"io"
27+
"io/fs"
2628
"net"
2729
"net/http"
2830
"net/http/pprof"
@@ -166,6 +168,9 @@ var flAskPassURL = pflag.String("askpass-url",
166168
envString("", "GITSYNC_ASKPASS_URL", "GIT_SYNC_ASKPASS_URL", "GIT_ASKPASS_URL"),
167169
"a URL to query for git credentials (username=<value> and password=<value>)")
168170

171+
var flStaleWorktreeTimeout = pflag.Duration("stale-worktree-timeout", envDuration(0, "GITSYNC_STALE_WORKTREE_TIMEOUT"),
172+
"how long to retain non-current worktrees")
173+
169174
var flGitCmd = pflag.String("git",
170175
envString("git", "GITSYNC_GIT", "GIT_SYNC_GIT"),
171176
"the git command to run (subject to PATH search, mostly for testing)")
@@ -475,19 +480,20 @@ func (abs absPath) Base() string {
475480

476481
// repoSync represents the remote repo and the local sync of it.
477482
type repoSync struct {
478-
cmd string // the git command to run
479-
root absPath // absolute path to the root directory
480-
repo string // remote repo to sync
481-
ref string // the ref to sync
482-
depth int // for shallow sync
483-
submodules submodulesMode // how to handle submodules
484-
gc gcMode // garbage collection
485-
link absPath // absolute path to the symlink to publish
486-
authURL string // a URL to re-fetch credentials, or ""
487-
sparseFile string // path to a sparse-checkout file
488-
syncCount int // how many times have we synced?
489-
log *logging.Logger
490-
run cmd.Runner
483+
cmd string // the git command to run
484+
root absPath // absolute path to the root directory
485+
repo string // remote repo to sync
486+
ref string // the ref to sync
487+
depth int // for shallow sync
488+
submodules submodulesMode // how to handle submodules
489+
gc gcMode // garbage collection
490+
link absPath // absolute path to the symlink to publish
491+
authURL string // a URL to re-fetch credentials, or ""
492+
sparseFile string // path to a sparse-checkout file
493+
syncCount int // how many times have we synced?
494+
log *logging.Logger
495+
run cmd.Runner
496+
staleTimeout time.Duration // time for worktrees to be cleaned up
491497
}
492498

493499
func main() {
@@ -791,18 +797,19 @@ func main() {
791797

792798
// Capture the various git parameters.
793799
git := &repoSync{
794-
cmd: *flGitCmd,
795-
root: absRoot,
796-
repo: *flRepo,
797-
ref: *flRef,
798-
depth: *flDepth,
799-
submodules: submodulesMode(*flSubmodules),
800-
gc: gcMode(*flGitGC),
801-
link: absLink,
802-
authURL: *flAskPassURL,
803-
sparseFile: *flSparseCheckoutFile,
804-
log: log,
805-
run: cmdRunner,
800+
cmd: *flGitCmd,
801+
root: absRoot,
802+
repo: *flRepo,
803+
ref: *flRef,
804+
depth: *flDepth,
805+
submodules: submodulesMode(*flSubmodules),
806+
gc: gcMode(*flGitGC),
807+
link: absLink,
808+
authURL: *flAskPassURL,
809+
sparseFile: *flSparseCheckoutFile,
810+
log: log,
811+
run: cmdRunner,
812+
staleTimeout: *flStaleWorktreeTimeout,
806813
}
807814

808815
// This context is used only for git credentials initialization. There are
@@ -963,6 +970,12 @@ func main() {
963970
start := time.Now()
964971
ctx, cancel := context.WithTimeout(context.Background(), *flSyncTimeout)
965972

973+
if git.staleTimeout > 0 {
974+
if err := git.cleanupStaleWorktrees(); err != nil {
975+
log.Error(err, "failed to clean up stale worktrees")
976+
}
977+
}
978+
966979
if changed, hash, err := git.SyncRepo(ctx, refreshCreds); err != nil {
967980
failCount++
968981
updateSyncMetrics(metricKeyError, start)
@@ -1068,11 +1081,15 @@ func touch(path absPath) error {
10681081
if err := os.MkdirAll(dir, defaultDirMode); err != nil {
10691082
return err
10701083
}
1071-
file, err := os.Create(path.String())
1072-
if err != nil {
1084+
if err := os.Chtimes(path.String(), time.Now(), time.Now()); errors.Is(err, fs.ErrNotExist) {
1085+
file, createErr := os.Create(path.String())
1086+
if createErr != nil {
1087+
return createErr
1088+
}
1089+
return file.Close()
1090+
} else {
10731091
return err
10741092
}
1075-
return file.Close()
10761093
}
10771094

10781095
const redactedString = "REDACTED"
@@ -1272,6 +1289,21 @@ func (git *repoSync) initRepo(ctx context.Context) error {
12721289
return nil
12731290
}
12741291

1292+
func (git *repoSync) cleanupStaleWorktrees() error {
1293+
currentWorktree, err := git.currentWorktree()
1294+
if err != nil {
1295+
return err
1296+
}
1297+
err = removeDirContentsIf(git.worktreeFor("").Path(), git.log, func(fi os.FileInfo) (bool, error) {
1298+
// delete files that are over the stale time out, and make sure to never delete the current worktree
1299+
return fi.Name() != currentWorktree.Hash() && time.Since(fi.ModTime()) > git.staleTimeout, nil
1300+
})
1301+
if err != nil {
1302+
return err
1303+
}
1304+
return nil
1305+
}
1306+
12751307
// sanityCheckRepo tries to make sure that the repo dir is a valid git repository.
12761308
func (git *repoSync) sanityCheckRepo(ctx context.Context) bool {
12771309
git.log.V(3).Info("sanity-checking git repo", "repo", git.root)
@@ -1340,27 +1372,36 @@ func dirIsEmpty(dir absPath) (bool, error) {
13401372
return len(dirents) == 0, nil
13411373
}
13421374

1343-
// removeDirContents iterated the specified dir and removes all contents,
1344-
// except entries which are specifically excepted.
1345-
func removeDirContents(dir absPath, log *logging.Logger, except ...string) error {
1375+
// removeDirContents iterated the specified dir and removes all contents
1376+
func removeDirContents(dir absPath, log *logging.Logger) error {
1377+
return removeDirContentsIf(dir, log, func(fi os.FileInfo) (bool, error) {
1378+
return true, nil
1379+
})
1380+
}
1381+
1382+
func removeDirContentsIf(dir absPath, log *logging.Logger, fn func(fi os.FileInfo) (bool, error)) error {
13461383
dirents, err := os.ReadDir(dir.String())
13471384
if err != nil {
13481385
return err
13491386
}
13501387

1351-
exceptMap := map[string]bool{}
1352-
for _, x := range except {
1353-
exceptMap[x] = true
1354-
}
1355-
13561388
// Save errors until the end.
13571389
var errs multiError
13581390
for _, fi := range dirents {
13591391
name := fi.Name()
1360-
if exceptMap[name] {
1392+
p := filepath.Join(dir.String(), name)
1393+
stat, err := os.Stat(p)
1394+
if err != nil {
1395+
log.Error(err, "failed to stat path, skipping", "path", p)
1396+
continue
1397+
}
1398+
if shouldDelete, err := fn(stat); err != nil {
1399+
log.Error(err, "failed to evaluate shouldDelete function, skipping", "path", p)
1400+
continue
1401+
} else if !shouldDelete {
1402+
log.V(3).Info("skipping path that should not be removed", "path", p)
13611403
continue
13621404
}
1363-
p := filepath.Join(dir.String(), name)
13641405
if log != nil {
13651406
log.V(3).Info("removing path recursively", "path", p, "isDir", fi.IsDir())
13661407
}
@@ -1539,12 +1580,12 @@ func (git *repoSync) configureWorktree(ctx context.Context, worktree worktree) e
15391580

15401581
// cleanup removes old worktrees and runs git's garbage collection. The
15411582
// specified worktree is preserved.
1542-
func (git *repoSync) cleanup(ctx context.Context, worktree worktree) error {
1583+
func (git *repoSync) cleanup(ctx context.Context) error {
15431584
// Save errors until the end.
15441585
var cleanupErrs multiError
15451586

15461587
// Clean up previous worktree(s).
1547-
if err := removeDirContents(git.worktreeFor("").Path(), git.log, worktree.Hash()); err != nil {
1588+
if err := git.cleanupStaleWorktrees(); err != nil {
15481589
cleanupErrs = append(cleanupErrs, err)
15491590
}
15501591

@@ -1639,7 +1680,7 @@ func (git *repoSync) IsKnownHash(ctx context.Context, ref string) (bool, error)
16391680
return strings.HasPrefix(line, ref), nil
16401681
}
16411682

1642-
// worktree represents a git worktree (which may or may not exisat on disk).
1683+
// worktree represents a git worktree (which may or may not exist on disk).
16431684
type worktree absPath
16441685

16451686
// Hash returns the intended commit hash for this worktree.
@@ -1781,6 +1822,14 @@ func (git *repoSync) SyncRepo(ctx context.Context, refreshCreds func(context.Con
17811822
if err != nil {
17821823
return false, "", err
17831824
}
1825+
if currentWorktree != "" {
1826+
// Touch the old worktree -- which will make cleanupStaleWorktrees delete it in the future,
1827+
// if the stale timeout option is enabled
1828+
err = touch(currentWorktree.Path())
1829+
if err != nil {
1830+
git.log.Error(err, "Couldn't change stale worktree mtime", "path", currentWorktree.Path())
1831+
}
1832+
}
17841833
}
17851834

17861835
// Mark ourselves as "ready".
@@ -1793,7 +1842,7 @@ func (git *repoSync) SyncRepo(ctx context.Context, refreshCreds func(context.Con
17931842
// not get caught by the normal cleanup.
17941843
os.RemoveAll(currentWorktree.Path().String())
17951844
}
1796-
if err := git.cleanup(ctx, newWorktree); err != nil {
1845+
if err := git.cleanup(ctx); err != nil {
17971846
git.log.Error(err, "git cleanup failed", "newWorktree", newWorktree)
17981847
}
17991848
}
@@ -2412,6 +2461,13 @@ OPTIONS
24122461
The known_hosts file to use when --ssh-known-hosts is specified.
24132462
If not specified, this defaults to "/etc/git-secret/known_hosts".
24142463
2464+
--stale-worktree-timeout <duration>, $GITSYNC_STALE_WORKTREE_TIMEOUT
2465+
The length of time to retain stale (not the current link target)
2466+
worktrees before being removed. Once this duration has elapsed,
2467+
a stale worktree will be removed during the next sync attempt
2468+
(as determined by --sync-timeout). If not specified, this defaults
2469+
to 0, meaning that stale worktrees will be removed immediately.
2470+
24152471
--submodules <string>, $GITSYNC_SUBMODULES
24162472
The git submodule behavior: one of "recursive", "shallow", or
24172473
"off". If not specified, this defaults to "recursive".

0 commit comments

Comments
 (0)