@@ -21,8 +21,10 @@ package main // import "k8s.io/git-sync/cmd/git-sync"
21
21
import (
22
22
"context"
23
23
"crypto/md5"
24
+ "errors"
24
25
"fmt"
25
26
"io"
27
+ "io/fs"
26
28
"net"
27
29
"net/http"
28
30
"net/http/pprof"
@@ -166,6 +168,9 @@ var flAskPassURL = pflag.String("askpass-url",
166
168
envString ("" , "GITSYNC_ASKPASS_URL" , "GIT_SYNC_ASKPASS_URL" , "GIT_ASKPASS_URL" ),
167
169
"a URL to query for git credentials (username=<value> and password=<value>)" )
168
170
171
+ var flStaleWorktreeTimeout = pflag .Duration ("stale-worktree-timeout" , envDuration (0 , "GITSYNC_STALE_WORKTREE_TIMEOUT" ),
172
+ "how long to retain non-current worktrees" )
173
+
169
174
var flGitCmd = pflag .String ("git" ,
170
175
envString ("git" , "GITSYNC_GIT" , "GIT_SYNC_GIT" ),
171
176
"the git command to run (subject to PATH search, mostly for testing)" )
@@ -475,19 +480,20 @@ func (abs absPath) Base() string {
475
480
476
481
// repoSync represents the remote repo and the local sync of it.
477
482
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
491
497
}
492
498
493
499
func main () {
@@ -791,18 +797,19 @@ func main() {
791
797
792
798
// Capture the various git parameters.
793
799
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 ,
806
813
}
807
814
808
815
// This context is used only for git credentials initialization. There are
@@ -963,6 +970,12 @@ func main() {
963
970
start := time .Now ()
964
971
ctx , cancel := context .WithTimeout (context .Background (), * flSyncTimeout )
965
972
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
+
966
979
if changed , hash , err := git .SyncRepo (ctx , refreshCreds ); err != nil {
967
980
failCount ++
968
981
updateSyncMetrics (metricKeyError , start )
@@ -1068,11 +1081,15 @@ func touch(path absPath) error {
1068
1081
if err := os .MkdirAll (dir , defaultDirMode ); err != nil {
1069
1082
return err
1070
1083
}
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 {
1073
1091
return err
1074
1092
}
1075
- return file .Close ()
1076
1093
}
1077
1094
1078
1095
const redactedString = "REDACTED"
@@ -1272,6 +1289,21 @@ func (git *repoSync) initRepo(ctx context.Context) error {
1272
1289
return nil
1273
1290
}
1274
1291
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
+
1275
1307
// sanityCheckRepo tries to make sure that the repo dir is a valid git repository.
1276
1308
func (git * repoSync ) sanityCheckRepo (ctx context.Context ) bool {
1277
1309
git .log .V (3 ).Info ("sanity-checking git repo" , "repo" , git .root )
@@ -1340,27 +1372,36 @@ func dirIsEmpty(dir absPath) (bool, error) {
1340
1372
return len (dirents ) == 0 , nil
1341
1373
}
1342
1374
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 {
1346
1383
dirents , err := os .ReadDir (dir .String ())
1347
1384
if err != nil {
1348
1385
return err
1349
1386
}
1350
1387
1351
- exceptMap := map [string ]bool {}
1352
- for _ , x := range except {
1353
- exceptMap [x ] = true
1354
- }
1355
-
1356
1388
// Save errors until the end.
1357
1389
var errs multiError
1358
1390
for _ , fi := range dirents {
1359
1391
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 )
1361
1403
continue
1362
1404
}
1363
- p := filepath .Join (dir .String (), name )
1364
1405
if log != nil {
1365
1406
log .V (3 ).Info ("removing path recursively" , "path" , p , "isDir" , fi .IsDir ())
1366
1407
}
@@ -1539,12 +1580,12 @@ func (git *repoSync) configureWorktree(ctx context.Context, worktree worktree) e
1539
1580
1540
1581
// cleanup removes old worktrees and runs git's garbage collection. The
1541
1582
// specified worktree is preserved.
1542
- func (git * repoSync ) cleanup (ctx context.Context , worktree worktree ) error {
1583
+ func (git * repoSync ) cleanup (ctx context.Context ) error {
1543
1584
// Save errors until the end.
1544
1585
var cleanupErrs multiError
1545
1586
1546
1587
// 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 {
1548
1589
cleanupErrs = append (cleanupErrs , err )
1549
1590
}
1550
1591
@@ -1639,7 +1680,7 @@ func (git *repoSync) IsKnownHash(ctx context.Context, ref string) (bool, error)
1639
1680
return strings .HasPrefix (line , ref ), nil
1640
1681
}
1641
1682
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).
1643
1684
type worktree absPath
1644
1685
1645
1686
// Hash returns the intended commit hash for this worktree.
@@ -1781,6 +1822,14 @@ func (git *repoSync) SyncRepo(ctx context.Context, refreshCreds func(context.Con
1781
1822
if err != nil {
1782
1823
return false , "" , err
1783
1824
}
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
+ }
1784
1833
}
1785
1834
1786
1835
// Mark ourselves as "ready".
@@ -1793,7 +1842,7 @@ func (git *repoSync) SyncRepo(ctx context.Context, refreshCreds func(context.Con
1793
1842
// not get caught by the normal cleanup.
1794
1843
os .RemoveAll (currentWorktree .Path ().String ())
1795
1844
}
1796
- if err := git .cleanup (ctx , newWorktree ); err != nil {
1845
+ if err := git .cleanup (ctx ); err != nil {
1797
1846
git .log .Error (err , "git cleanup failed" , "newWorktree" , newWorktree )
1798
1847
}
1799
1848
}
@@ -2412,6 +2461,13 @@ OPTIONS
2412
2461
The known_hosts file to use when --ssh-known-hosts is specified.
2413
2462
If not specified, this defaults to "/etc/git-secret/known_hosts".
2414
2463
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
+
2415
2471
--submodules <string>, $GITSYNC_SUBMODULES
2416
2472
The git submodule behavior: one of "recursive", "shallow", or
2417
2473
"off". If not specified, this defaults to "recursive".
0 commit comments