Skip to content

Commit 8e58871

Browse files
committed
Adjustments to remove dangling repository locks
Signed-off-by: Bruno Sofiato <[email protected]>
1 parent f35e2b0 commit 8e58871

File tree

3 files changed

+172
-1
lines changed

3 files changed

+172
-1
lines changed

modules/git/command.go

+23-1
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,30 @@ func CommonCmdServEnvs() []string {
269269

270270
var ErrBrokenCommand = errors.New("git command is broken")
271271

272-
// Run runs the command with the RunOpts
273272
func (c *Command) Run(opts *RunOpts) error {
273+
// Check if there is some dangling locks older than the given threshold
274+
if err := ForciblyUnlockRepositoryIfNeeded(c.parentContext, opts.Dir); err != nil {
275+
log.Error("Error while trying to unlock repository: %v", err)
276+
return err
277+
}
278+
// Execute the git command
279+
if err := c.run(opts); err != nil {
280+
if exitError, ok := err.(*exec.ExitError); ok {
281+
if exitError.ExitCode() > 128 {
282+
// Errors greater than 128 means that the process was killed by an OS signal (see https://unix.stackexchange.com/questions/99112/default-exit-code-when-process-is-terminated)
283+
log.Warn("It appears that the git process %s has crashed. Attempting to forcbily unlock it [repo: %s]", exitError.Pid(), opts.Dir)
284+
ForciblyUnlockRepository(c.parentContext, opts.Dir)
285+
return err
286+
}
287+
return err
288+
}
289+
return err
290+
}
291+
return nil
292+
}
293+
294+
// Run runs the command with the RunOpts
295+
func (c *Command) run(opts *RunOpts) error {
274296
if len(c.brokenArgs) != 0 {
275297
log.Error("git command is broken: %s, broken args: %s", c.String(), strings.Join(c.brokenArgs, " "))
276298
return ErrBrokenCommand

modules/git/repo_cleanup.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package git
5+
6+
import (
7+
"context"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
"time"
12+
13+
"code.gitea.io/gitea/modules/log"
14+
)
15+
16+
const threshold = -1 * time.Hour
17+
18+
func ForciblyUnlockRepository(ctx context.Context, repoPath string) error {
19+
return cleanLocksIfNeeded(repoPath, time.Now())
20+
}
21+
22+
func ForciblyUnlockRepositoryIfNeeded(ctx context.Context, repoPath string) error {
23+
lockThreshold := time.Now().Add(threshold)
24+
return cleanLocksIfNeeded(repoPath, lockThreshold)
25+
}
26+
27+
func cleanLocksIfNeeded(repoPath string, threshold time.Time) error {
28+
if repoPath == "" {
29+
return nil
30+
} else {
31+
log.Trace("Checking if repository %s is locked [lock threshold is %s]", repoPath, threshold)
32+
return filepath.Walk(repoPath, func(filePath string, fileInfo os.FileInfo, err error) error {
33+
if err != nil {
34+
return err
35+
}
36+
if err := cleanLockIfNeeded(filePath, fileInfo, threshold); err != nil {
37+
log.Error("Failed to remove lock file %s: %v", filePath, err)
38+
return err
39+
}
40+
return nil
41+
})
42+
}
43+
}
44+
45+
func cleanLockIfNeeded(filePath string, fileInfo os.FileInfo, threshold time.Time) error {
46+
if isLock(fileInfo) && fileInfo.ModTime().Before(threshold) {
47+
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
48+
return err
49+
}
50+
log.Info("Lock file %s has been removed since its older than %s [timestamp: %s]", filePath, threshold, fileInfo.ModTime())
51+
return nil
52+
}
53+
log.Warn("Cannot exclude lock file %s because it is younger than the thredhold %s [timestamp: %s]", filePath, threshold, fileInfo.ModTime())
54+
return nil
55+
}
56+
57+
func isLock(lockFile os.FileInfo) bool {
58+
return !lockFile.IsDir() && strings.HasSuffix(lockFile.Name(), ".lock")
59+
}

modules/git/repo_cleanup_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package git
5+
6+
import (
7+
"context"
8+
"os"
9+
"os/exec"
10+
"runtime"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
// This test mimics a repository having dangling locks. If the locks are older than the threshold, they should be
17+
// removed. Otherwise, they'll remain and the command will fail.
18+
19+
func TestMaintainExistentLock(t *testing.T) {
20+
if runtime.GOOS != "linux" {
21+
// Need to use touch to change the last access time of the lock files
22+
t.Skip("Skipping test on non-linux OS")
23+
}
24+
25+
shouldRemainLocked := func(lockFiles []string, err error) {
26+
assert.Error(t, err)
27+
for _, lockFile := range lockFiles {
28+
assert.FileExists(t, lockFile)
29+
}
30+
}
31+
32+
shouldBeUnlocked := func(lockFiles []string, err error) {
33+
assert.NoError(t, err)
34+
for _, lockFile := range lockFiles {
35+
assert.NoFileExists(t, lockFile)
36+
}
37+
}
38+
39+
t.Run("2 days lock file", func(t *testing.T) {
40+
doTestLockCleanup(t, "2 days", shouldBeUnlocked)
41+
})
42+
43+
t.Run("1 hour lock file", func(t *testing.T) {
44+
doTestLockCleanup(t, "1 hour", shouldBeUnlocked)
45+
})
46+
47+
t.Run("1 minutes lock file", func(t *testing.T) {
48+
doTestLockCleanup(t, "1 minutes", shouldRemainLocked)
49+
})
50+
}
51+
52+
func doTestLockCleanup(t *testing.T, lockAge string, expectedResult func(lockFiles []string, err error)) {
53+
if tmpDir, err := os.MkdirTemp("", "cleanup-after-crash"); err != nil {
54+
t.Fatal(err)
55+
} else {
56+
defer os.RemoveAll(tmpDir)
57+
58+
if err := os.CopyFS(tmpDir, os.DirFS("../../tests/gitea-repositories-meta/org3/repo3.git")); err != nil {
59+
t.Fatal(err)
60+
}
61+
62+
lockFiles := lockFilesFor(tmpDir)
63+
64+
os.MkdirAll(tmpDir+"/objects/info/commit-graphs", os.ModeSticky|os.ModePerm)
65+
66+
for _, lockFile := range lockFiles {
67+
createLockFiles(t, lockFile, lockAge)
68+
}
69+
70+
cmd := NewCommand(context.Background(), "fetch")
71+
_, _, cmdErr := cmd.RunStdString(&RunOpts{Dir: tmpDir})
72+
73+
expectedResult(lockFiles, cmdErr)
74+
}
75+
}
76+
77+
func lockFilesFor(path string) []string {
78+
return []string{
79+
path + "/config.lock",
80+
path + "/HEAD.lock",
81+
path + "/objects/info/commit-graphs/commit-graph-chain.lock",
82+
}
83+
}
84+
85+
func createLockFiles(t *testing.T, file, lockAge string) {
86+
cmd := exec.Command("touch", "-m", "-a", "-d", "-"+lockAge, file)
87+
if err := cmd.Run(); err != nil {
88+
t.Error(err)
89+
}
90+
}

0 commit comments

Comments
 (0)