diff options
author | Sami Hiltunen <shiltunen@gitlab.com> | 2023-09-12 15:22:52 +0300 |
---|---|---|
committer | Sami Hiltunen <shiltunen@gitlab.com> | 2023-09-15 16:42:48 +0300 |
commit | 979350b60cc37f5bdde29e33f8dd02a3173335da (patch) | |
tree | 3915f7c080d83ae21660d93baa833c6070d82280 | |
parent | 16cd7e7fa2bb9afdd7061ae6187f739dc2fe120f (diff) |
Return specific errors for packed-refs locks
After a crash, the packed-refs file may have either 'packed-refs.lock'
or 'packed-refs.new' leftover by the crashed a Git process. These files
prevent further operations on the repository prior to cleaning them.
Parse the errors indicating this case from the Updater's stderr and
return a well-known error so the caller can handle the case.
-rw-r--r-- | internal/git/updateref/updateref.go | 15 | ||||
-rw-r--r-- | internal/git/updateref/updateref_test.go | 38 |
2 files changed, 51 insertions, 2 deletions
diff --git a/internal/git/updateref/updateref.go b/internal/git/updateref/updateref.go index 4290846c6..9979e7d83 100644 --- a/internal/git/updateref/updateref.go +++ b/internal/git/updateref/updateref.go @@ -13,8 +13,13 @@ import ( "gitlab.com/gitlab-org/gitaly/v16/internal/structerr" ) -// errClosed is returned when accessing an updater that has already been closed. -var errClosed = errors.New("closed") +var ( + // errClosed is returned when accessing an updater that has already been closed. + errClosed = errors.New("closed") + // ErrPackedRefsLocked indicates an operation failed due to the 'packed-refs' being locked. This is + // the case if either `packed-refs.new` or `packed-refs.lock` exists in the repository. + ErrPackedRefsLocked = errors.New("packed-refs locked") +) // MultipleUpdatesError indicates that a reference cannot have multiple updates // within the same transaction. @@ -475,6 +480,7 @@ func (u *Updater) write(format string, args ...interface{}) error { } var ( + packedRefsLockedRegex = regexp.MustCompile(`(packed-refs\.lock': File exists\.\n)|(packed-refs\.new: File exists\n$)`) refLockedRegex = regexp.MustCompile(`^fatal: (prepare|commit): cannot lock ref '(.+?)': Unable to create '.*': File exists.`) refInvalidFormatRegex = regexp.MustCompile(`^fatal: invalid ref format: (.*)\n$`) referenceAlreadyExistsRegex = regexp.MustCompile(`^fatal: .*: cannot lock ref '(.*)': reference already exists\n$`) @@ -516,6 +522,11 @@ func (u *Updater) parseStderr() error { return AlreadyLockedError{ReferenceName: string(matches[2])} } + matches = packedRefsLockedRegex.FindSubmatch(stderr) + if len(matches) > 1 { + return ErrPackedRefsLocked + } + matches = refInvalidFormatRegex.FindSubmatch(stderr) if len(matches) > 1 { return InvalidReferenceFormatError{ReferenceName: string(matches[1])} diff --git a/internal/git/updateref/updateref_test.go b/internal/git/updateref/updateref_test.go index c661d87fe..7fdd89b87 100644 --- a/internal/git/updateref/updateref_test.go +++ b/internal/git/updateref/updateref_test.go @@ -720,6 +720,44 @@ func TestUpdater_capturesStderr(t *testing.T) { require.Equal(t, expectedErr, updater.Commit()) } +func TestUpdater_packedRefsLocked(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + desc string + runMethod func(u *Updater) error + }{ + {desc: "commit", runMethod: func(u *Updater) error { return u.Commit() }}, + {desc: "prepare", runMethod: func(u *Updater) error { return u.Prepare() }}, + } { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + for _, lockFile := range []string{"packed-refs.lock", "packed-refs.new"} { + lockFile := lockFile + t.Run(lockFile, func(t *testing.T) { + t.Parallel() + + ctx := testhelper.Context(t) + + cfg, _, repoPath, updater := setupUpdater(t, ctx) + defer func() { require.Equal(t, ErrPackedRefsLocked, updater.Close()) }() + + // Write a reference an pack it. + gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("main")) + gittest.Exec(t, cfg, "-C", repoPath, "pack-refs", "--all") + // Write a lock file file so we can assert a lock related error is raised. + require.NoError(t, os.WriteFile(filepath.Join(repoPath, lockFile), nil, fs.ModePerm)) + + require.NoError(t, updater.Start()) + require.NoError(t, updater.Delete("refs/heads/main")) + require.Equal(t, ErrPackedRefsLocked, updater.Commit()) + }) + } + }) + } +} + func BenchmarkUpdater(b *testing.B) { ctx := testhelper.Context(b) |