diff options
| author | 2026-03-07 18:09:20 +0800 | |
|---|---|---|
| committer | 2026-03-07 18:17:54 +0800 | |
| commit | e667c3c52a535ee67fe895bb0240fbad6e920087 (patch) | |
| tree | 0815f7cc9b2c4a06d00722bce4c3ac57c515288b /refstore/files | |
| parent | receivepack: Connect protocol with service (diff) | |
| signature | No signature | |
refstore/files: Accept timeout instead of reading from config
And split things up again.
Diffstat (limited to 'refstore/files')
45 files changed, 1281 insertions, 1179 deletions
diff --git a/refstore/files/close.go b/refstore/files/close.go new file mode 100644 index 00000000..37dde9b9 --- /dev/null +++ b/refstore/files/close.go @@ -0,0 +1,13 @@ +package files + +// Close releases resources associated with the store. +func (store *Store) Close() error { + err := store.gitRoot.Close() + commonErr := store.commonRoot.Close() + + if err != nil { + return err + } + + return commonErr +} diff --git a/refstore/files/errors.go b/refstore/files/errors.go new file mode 100644 index 00000000..daa40849 --- /dev/null +++ b/refstore/files/errors.go @@ -0,0 +1,16 @@ +package files + +import "fmt" + +type brokenRefError struct { + name string + err error +} + +func (err brokenRefError) Error() string { + return fmt.Sprintf("refstore/files: broken reference %q: %v", err.name, err.err) +} + +func (err brokenRefError) Unwrap() error { + return err.err +} diff --git a/refstore/files/helpers_test.go b/refstore/files/helpers_test.go index 2a0eca74..bdef6bb5 100644 --- a/refstore/files/helpers_test.go +++ b/refstore/files/helpers_test.go @@ -5,18 +5,21 @@ import ( "slices" "strings" "testing" + "time" "codeberg.org/lindenii/furgit/internal/testgit" "codeberg.org/lindenii/furgit/objectid" "codeberg.org/lindenii/furgit/refstore/files" ) +const testPackedRefsTimeout = time.Second + func openFilesStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *files.Store { t.Helper() root := testRepo.OpenGitRoot(t) - store, err := files.New(root, algo) + store, err := files.New(root, algo, testPackedRefsTimeout) if err != nil { t.Fatalf("files.New: %v", err) } @@ -27,7 +30,7 @@ func openFilesStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algo func openFilesStoreAt(t *testing.T, root *os.Root, algo objectid.Algorithm) *files.Store { t.Helper() - store, err := files.New(root, algo) + store, err := files.New(root, algo, testPackedRefsTimeout) if err != nil { t.Fatalf("files.New: %v", err) } diff --git a/refstore/files/new.go b/refstore/files/new.go new file mode 100644 index 00000000..6e113e64 --- /dev/null +++ b/refstore/files/new.go @@ -0,0 +1,29 @@ +package files + +import ( + "math/rand" + "os" + "time" + + "codeberg.org/lindenii/furgit/objectid" +) + +// New creates one files ref store rooted at one repository gitdir. +func New(root *os.Root, algo objectid.Algorithm, packedRefsTimeout time.Duration) (*Store, error) { + if algo.Size() == 0 { + return nil, objectid.ErrInvalidAlgorithm + } + + commonRoot, err := openCommonRoot(root) + if err != nil { + return nil, err + } + + return &Store{ + gitRoot: root, + commonRoot: commonRoot, + algo: algo, + lockRand: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint:gosec + packedRefsTimeout: packedRefsTimeout, + }, nil +} diff --git a/refstore/files/packed_delete_test.go b/refstore/files/packed_delete_test.go index 49b86ebe..23df14e8 100644 --- a/refstore/files/packed_delete_test.go +++ b/refstore/files/packed_delete_test.go @@ -232,7 +232,6 @@ func TestFilesDeleteWaitsForPackedRefsLockWithoutIntermediateState(t *testing.T) testRepo.UpdateRef(t, prefix+"/foo", packedID) testRepo.PackRefs(t, "--all", "--prune") testRepo.UpdateRef(t, prefix+"/foo", looseID) - testRepo.Run(t, "config", "core.packedrefstimeout", "3000") testRepo.WriteFile(t, "packed-refs.lock", []byte{}, 0o644) store := openFilesStore(t, testRepo, algo) diff --git a/refstore/files/packed_parse.go b/refstore/files/packed_parse.go new file mode 100644 index 00000000..5582ee37 --- /dev/null +++ b/refstore/files/packed_parse.go @@ -0,0 +1,113 @@ +package files + +import ( + "bufio" + "fmt" + "io" + "strings" + + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/ref" +) + +func parsePackedRefs(r io.Reader, algo objectid.Algorithm) (map[string]ref.Detached, []ref.Detached, error) { + byName := make(map[string]ref.Detached) + ordered := make([]ref.Detached, 0, 32) + + br := bufio.NewReader(r) + prev := -1 + lineNum := 0 + hexsz := algo.Size() * 2 + + for { + line, err := br.ReadString('\n') + if err != nil && err != io.EOF { + return nil, nil, err + } + + if line == "" && err == io.EOF { + break + } + + lineNum++ + hadNewline := strings.HasSuffix(line, "\n") + line = strings.TrimSuffix(line, "\n") + + if err == io.EOF && !hadNewline { + return nil, nil, fmt.Errorf("refstore/files: line %d: unterminated line", lineNum) + } + + if line == "" || strings.HasPrefix(line, "#") { + if err == io.EOF { + break + } + + continue + } + + if strings.HasPrefix(line, "^") { + if prev < 0 { + return nil, nil, fmt.Errorf("refstore/files: line %d: peeled line without preceding ref", lineNum) + } + + if len(line) != hexsz+1 { + return nil, nil, fmt.Errorf("refstore/files: line %d: malformed peeled line", lineNum) + } + + peeled, parseErr := objectid.ParseHex(algo, line[1:]) + if parseErr != nil { + return nil, nil, fmt.Errorf("refstore/files: line %d: invalid peeled oid: %w", lineNum, parseErr) + } + + peeledCopy := peeled + cur := ordered[prev] + cur.Peeled = &peeledCopy + ordered[prev] = cur + byName[cur.Name()] = cur + + if err == io.EOF { + break + } + + continue + } + + if len(line) < hexsz+2 { + return nil, nil, fmt.Errorf("refstore/files: line %d: malformed entry", lineNum) + } + + if line[hexsz] != ' ' { + return nil, nil, fmt.Errorf("refstore/files: line %d: malformed entry", lineNum) + } + + idText := line[:hexsz] + + name := line[hexsz+1:] + if name == "" { + return nil, nil, fmt.Errorf("refstore/files: line %d: empty ref name", lineNum) + } + + id, parseErr := objectid.ParseHex(algo, idText) + if parseErr != nil { + return nil, nil, fmt.Errorf("refstore/files: line %d: invalid oid: %w", lineNum, parseErr) + } + + if _, exists := byName[name]; exists { + return nil, nil, fmt.Errorf("refstore/files: line %d: duplicate ref %q", lineNum, name) + } + + detached := ref.Detached{ + RefName: name, + ID: id, + } + ordered = append(ordered, detached) + prev = len(ordered) - 1 + byName[name] = detached + + if err == io.EOF { + break + } + } + + return byName, ordered, nil +} diff --git a/refstore/files/packed_read.go b/refstore/files/packed_read.go new file mode 100644 index 00000000..20800709 --- /dev/null +++ b/refstore/files/packed_read.go @@ -0,0 +1,35 @@ +package files + +import ( + "errors" + "fmt" + "os" + + "codeberg.org/lindenii/furgit/ref" +) + +func (store *Store) readPackedRefs() (*packedRefs, error) { + file, err := store.commonRoot.Open("packed-refs") + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return &packedRefs{ + byName: make(map[string]ref.Detached), + ordered: nil, + }, nil + } + + return nil, fmt.Errorf("refstore/files: open packed-refs: %w", err) + } + + defer func() { _ = file.Close() }() + + byName, ordered, err := parsePackedRefs(file, store.algo) + if err != nil { + return nil, err + } + + return &packedRefs{ + byName: byName, + ordered: ordered, + }, nil +} diff --git a/refstore/files/packed_refs.go b/refstore/files/packed_refs.go index 0c534f59..f3e91d83 100644 --- a/refstore/files/packed_refs.go +++ b/refstore/files/packed_refs.go @@ -1,14 +1,6 @@ package files import ( - "bufio" - "errors" - "fmt" - "io" - "os" - "strings" - - "codeberg.org/lindenii/furgit/objectid" "codeberg.org/lindenii/furgit/ref" ) @@ -16,135 +8,3 @@ type packedRefs struct { byName map[string]ref.Detached ordered []ref.Detached } - -func (store *Store) readPackedRefs() (*packedRefs, error) { - file, err := store.commonRoot.Open("packed-refs") - if err != nil { - if errorsIsNotExist(err) { - return &packedRefs{ - byName: make(map[string]ref.Detached), - ordered: nil, - }, nil - } - - return nil, fmt.Errorf("refstore/files: open packed-refs: %w", err) - } - - defer func() { _ = file.Close() }() - - byName, ordered, err := parsePackedRefs(file, store.algo) - if err != nil { - return nil, err - } - - return &packedRefs{ - byName: byName, - ordered: ordered, - }, nil -} - -func parsePackedRefs(r io.Reader, algo objectid.Algorithm) (map[string]ref.Detached, []ref.Detached, error) { - byName := make(map[string]ref.Detached) - ordered := make([]ref.Detached, 0, 32) - - br := bufio.NewReader(r) - prev := -1 - lineNum := 0 - hexsz := algo.Size() * 2 - - for { - line, err := br.ReadString('\n') - if err != nil && err != io.EOF { - return nil, nil, err - } - - if line == "" && err == io.EOF { - break - } - - lineNum++ - hadNewline := strings.HasSuffix(line, "\n") - line = strings.TrimSuffix(line, "\n") - - if err == io.EOF && !hadNewline { - return nil, nil, fmt.Errorf("refstore/files: line %d: unterminated line", lineNum) - } - - if line == "" || strings.HasPrefix(line, "#") { - if err == io.EOF { - break - } - - continue - } - - if strings.HasPrefix(line, "^") { - if prev < 0 { - return nil, nil, fmt.Errorf("refstore/files: line %d: peeled line without preceding ref", lineNum) - } - - if len(line) != hexsz+1 { - return nil, nil, fmt.Errorf("refstore/files: line %d: malformed peeled line", lineNum) - } - - peeled, parseErr := objectid.ParseHex(algo, line[1:]) - if parseErr != nil { - return nil, nil, fmt.Errorf("refstore/files: line %d: invalid peeled oid: %w", lineNum, parseErr) - } - - peeledCopy := peeled - cur := ordered[prev] - cur.Peeled = &peeledCopy - ordered[prev] = cur - byName[cur.Name()] = cur - - if err == io.EOF { - break - } - - continue - } - - if len(line) < hexsz+2 { - return nil, nil, fmt.Errorf("refstore/files: line %d: malformed entry", lineNum) - } - - if line[hexsz] != ' ' { - return nil, nil, fmt.Errorf("refstore/files: line %d: malformed entry", lineNum) - } - - idText := line[:hexsz] - - name := line[hexsz+1:] - if name == "" { - return nil, nil, fmt.Errorf("refstore/files: line %d: empty ref name", lineNum) - } - - id, parseErr := objectid.ParseHex(algo, idText) - if parseErr != nil { - return nil, nil, fmt.Errorf("refstore/files: line %d: invalid oid: %w", lineNum, parseErr) - } - - if _, exists := byName[name]; exists { - return nil, nil, fmt.Errorf("refstore/files: line %d: duplicate ref %q", lineNum, name) - } - - detached := ref.Detached{ - RefName: name, - ID: id, - } - ordered = append(ordered, detached) - prev = len(ordered) - 1 - byName[name] = detached - - if err == io.EOF { - break - } - } - - return byName, ordered, nil -} - -func errorsIsNotExist(err error) bool { - return errors.Is(err, os.ErrNotExist) -} diff --git a/refstore/files/read_list.go b/refstore/files/read_list.go index 95d37ccd..358ec007 100644 --- a/refstore/files/read_list.go +++ b/refstore/files/read_list.go @@ -2,10 +2,8 @@ package files import ( "errors" - "os" "path" "slices" - "strings" "codeberg.org/lindenii/furgit/ref" "codeberg.org/lindenii/furgit/refstore" @@ -76,73 +74,3 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) { return refs, nil } - -func (store *Store) collectLooseRefNames() ([]string, error) { - names := make([]string, 0, 16) - seen := make(map[string]struct{}, 16) - - _, err := store.gitRoot.Stat("HEAD") - if err == nil { - names = append(names, "HEAD") - seen["HEAD"] = struct{}{} - } else if !errors.Is(err, os.ErrNotExist) { - return nil, err - } - - var walk func(*os.Root, string) error - - walk = func(root *os.Root, dir string) error { - file, openErr := root.Open(dir) - if openErr != nil { - if errors.Is(openErr, os.ErrNotExist) { - return nil - } - - return openErr - } - - defer func() { _ = file.Close() }() - - entries, readErr := file.ReadDir(-1) - if readErr != nil { - return readErr - } - - for _, entry := range entries { - name := path.Join(dir, entry.Name()) - if entry.IsDir() { - err := walk(root, name) - if err != nil { - return err - } - - continue - } - - if strings.HasSuffix(name, ".lock") { - continue - } - - if _, ok := seen[name]; ok { - continue - } - - seen[name] = struct{}{} - names = append(names, name) - } - - return nil - } - - err = walk(store.commonRoot, "refs") - if err != nil { - return nil, err - } - - err = walk(store.gitRoot, "refs") - if err != nil { - return nil, err - } - - return names, nil -} diff --git a/refstore/files/read_list_collect.go b/refstore/files/read_list_collect.go new file mode 100644 index 00000000..f4e2cb69 --- /dev/null +++ b/refstore/files/read_list_collect.go @@ -0,0 +1,78 @@ +package files + +import ( + "errors" + "os" + "path" + "strings" +) + +func (store *Store) collectLooseRefNames() ([]string, error) { + names := make([]string, 0, 16) + seen := make(map[string]struct{}, 16) + + _, err := store.gitRoot.Stat("HEAD") + if err == nil { + names = append(names, "HEAD") + seen["HEAD"] = struct{}{} + } else if !errors.Is(err, os.ErrNotExist) { + return nil, err + } + + var walk func(*os.Root, string) error + + walk = func(root *os.Root, dir string) error { + file, openErr := root.Open(dir) + if openErr != nil { + if errors.Is(openErr, os.ErrNotExist) { + return nil + } + + return openErr + } + + defer func() { _ = file.Close() }() + + entries, readErr := file.ReadDir(-1) + if readErr != nil { + return readErr + } + + for _, entry := range entries { + name := path.Join(dir, entry.Name()) + if entry.IsDir() { + err := walk(root, name) + if err != nil { + return err + } + + continue + } + + if strings.HasSuffix(name, ".lock") { + continue + } + + if _, ok := seen[name]; ok { + continue + } + + seen[name] = struct{}{} + names = append(names, name) + } + + return nil + } + + err = walk(store.commonRoot, "refs") + if err != nil { + return nil, err + } + + err = walk(store.gitRoot, "refs") + if err != nil { + return nil, err + } + + return names, nil +} diff --git a/refstore/files/read.go b/refstore/files/read_loose.go index 7b97467d..cc2bed49 100644 --- a/refstore/files/read.go +++ b/refstore/files/read_loose.go @@ -11,19 +11,6 @@ import ( "codeberg.org/lindenii/furgit/refstore" ) -type brokenRefError struct { - name string - err error -} - -func (err brokenRefError) Error() string { - return fmt.Sprintf("refstore/files: broken reference %q: %v", err.name, err.err) -} - -func (err brokenRefError) Unwrap() error { - return err.err -} - func (store *Store) readLooseRef(name string) (ref.Ref, error) { //nolint:ireturn refPath := store.loosePath(name) @@ -36,7 +23,7 @@ func (store *Store) readLooseRef(name string) (ref.Ref, error) { //nolint:iretur return nil, err } - line := trimTrailingRefWhitespace(string(data)) + line := strings.TrimRightFunc(string(data), isRefWhitespace) if strings.HasPrefix(line, "ref:") { target := strings.TrimLeftFunc(line[len("ref:"):], isRefWhitespace) if target == "" { @@ -59,16 +46,3 @@ func (store *Store) readLooseRef(name string) (ref.Ref, error) { //nolint:iretur ID: id, }, nil } - -func trimTrailingRefWhitespace(text string) string { - return strings.TrimRightFunc(text, isRefWhitespace) -} - -func isRefWhitespace(r rune) bool { - switch r { - case ' ', '\t', '\n', '\r', '\v', '\f': - return true - default: - return false - } -} diff --git a/refstore/files/read_resolve.go b/refstore/files/read_resolve.go index 770d6034..33d5b3e8 100644 --- a/refstore/files/read_resolve.go +++ b/refstore/files/read_resolve.go @@ -2,8 +2,6 @@ package files import ( "errors" - "fmt" - "strings" "codeberg.org/lindenii/furgit/ref" "codeberg.org/lindenii/furgit/refstore" @@ -41,37 +39,3 @@ func (store *Store) Resolve(name string) (ref.Ref, error) { //nolint:ireturn return detached, nil } - -// ResolveFully resolves symbolic references through the visible files store -// namespace until one detached reference is reached. -func (store *Store) ResolveFully(name string) (ref.Detached, error) { - cur := name - seen := make(map[string]struct{}) - - for { - if _, ok := seen[cur]; ok { - return ref.Detached{}, fmt.Errorf("refstore/files: symbolic reference cycle at %q", cur) - } - - seen[cur] = struct{}{} - - resolved, err := store.Resolve(cur) - if err != nil { - return ref.Detached{}, err - } - - switch resolved := resolved.(type) { - case ref.Detached: - return resolved, nil - case ref.Symbolic: - target := strings.TrimSpace(resolved.Target) - if target == "" { - return ref.Detached{}, fmt.Errorf("refstore/files: symbolic reference %q has empty target", resolved.Name()) - } - - cur = target - default: - return ref.Detached{}, fmt.Errorf("refstore/files: unsupported reference type %T", resolved) - } - } -} diff --git a/refstore/files/read_resolve_fully.go b/refstore/files/read_resolve_fully.go new file mode 100644 index 00000000..7bc4cfea --- /dev/null +++ b/refstore/files/read_resolve_fully.go @@ -0,0 +1,42 @@ +package files + +import ( + "fmt" + "strings" + + "codeberg.org/lindenii/furgit/ref" +) + +// ResolveFully resolves symbolic references through the visible files store +// namespace until one detached reference is reached. +func (store *Store) ResolveFully(name string) (ref.Detached, error) { + cur := name + seen := make(map[string]struct{}) + + for { + if _, ok := seen[cur]; ok { + return ref.Detached{}, fmt.Errorf("refstore/files: symbolic reference cycle at %q", cur) + } + + seen[cur] = struct{}{} + + resolved, err := store.Resolve(cur) + if err != nil { + return ref.Detached{}, err + } + + switch resolved := resolved.(type) { + case ref.Detached: + return resolved, nil + case ref.Symbolic: + target := strings.TrimSpace(resolved.Target) + if target == "" { + return ref.Detached{}, fmt.Errorf("refstore/files: symbolic reference %q has empty target", resolved.Name()) + } + + cur = target + default: + return ref.Detached{}, fmt.Errorf("refstore/files: unsupported reference type %T", resolved) + } + } +} diff --git a/refstore/files/root_for.go b/refstore/files/root_for.go new file mode 100644 index 00000000..cb968ad9 --- /dev/null +++ b/refstore/files/root_for.go @@ -0,0 +1,13 @@ +package files + +import ( + "os" +) + +func (store *Store) rootFor(kind rootKind) *os.Root { + if kind == rootCommon { + return store.commonRoot + } + + return store.gitRoot +} diff --git a/refstore/files/root_kind.go b/refstore/files/root_kind.go new file mode 100644 index 00000000..d0ae8cf1 --- /dev/null +++ b/refstore/files/root_kind.go @@ -0,0 +1,8 @@ +package files + +type rootKind uint8 + +const ( + rootGit rootKind = iota + rootCommon +) diff --git a/refstore/files/root_loose_path.go b/refstore/files/root_loose_path.go new file mode 100644 index 00000000..a78d9bf3 --- /dev/null +++ b/refstore/files/root_loose_path.go @@ -0,0 +1,24 @@ +package files + +import ( + "path" + + "codeberg.org/lindenii/furgit/ref/refname" +) + +func (store *Store) loosePath(name string) refPath { + parsed := refname.ParseWorktree(name) + switch parsed.Type { + case refname.WorktreeCurrent: + return refPath{root: rootGit, path: parsed.BareRefName} + case refname.WorktreeMain, refname.WorktreeShared: + return refPath{root: rootCommon, path: parsed.BareRefName} + case refname.WorktreeOther: + return refPath{ + root: rootCommon, + path: path.Join("worktrees", parsed.WorktreeName, parsed.BareRefName), + } + default: + return refPath{root: rootCommon, path: name} + } +} diff --git a/refstore/files/root_open_common.go b/refstore/files/root_open_common.go new file mode 100644 index 00000000..cac98cbc --- /dev/null +++ b/refstore/files/root_open_common.go @@ -0,0 +1,31 @@ +package files + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +func openCommonRoot(gitRoot *os.Root) (*os.Root, error) { + content, err := gitRoot.ReadFile("commondir") + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return gitRoot.OpenRoot(".") + } + + return nil, err + } + + commonDir := strings.TrimSpace(string(content)) + if commonDir == "" { + return nil, os.ErrNotExist + } + + if filepath.IsAbs(commonDir) { + return os.OpenRoot(commonDir) + } + + // This is okay because that's how Git defines it anyway. + return os.OpenRoot(filepath.Join(gitRoot.Name(), commonDir)) +} diff --git a/refstore/files/root_ref_path.go b/refstore/files/root_ref_path.go new file mode 100644 index 00000000..ed79ca3b --- /dev/null +++ b/refstore/files/root_ref_path.go @@ -0,0 +1,28 @@ +package files + +import ( + "fmt" + "strings" +) + +type refPath struct { + root rootKind + path string +} + +func (tx *Transaction) targetKey(name refPath) string { + return fmt.Sprintf("%d:%s", name.root, name.path) +} + +func refPathFromKey(key string) refPath { + rootValue, pathValue, ok := strings.Cut(key, ":") + if !ok || rootValue == "" { + return refPath{root: rootCommon, path: key} + } + + if rootValue == "0" { + return refPath{root: rootGit, path: pathValue} + } + + return refPath{root: rootCommon, path: pathValue} +} diff --git a/refstore/files/store.go b/refstore/files/store.go index 1eea0fa9..0328bc65 100644 --- a/refstore/files/store.go +++ b/refstore/files/store.go @@ -3,18 +3,11 @@ package files import ( - "errors" - "io" "math/rand" "os" - "path" - "path/filepath" - "strings" "time" - "codeberg.org/lindenii/furgit/config" "codeberg.org/lindenii/furgit/objectid" - "codeberg.org/lindenii/furgit/ref/refname" "codeberg.org/lindenii/furgit/refstore" ) @@ -35,118 +28,3 @@ var ( _ refstore.ReadingStore = (*Store)(nil) _ refstore.TransactionalStore = (*Store)(nil) ) - -type rootKind uint8 - -const ( - rootGit rootKind = iota - rootCommon -) - -type refPath struct { - root rootKind - path string -} - -// New creates one files ref store rooted at one repository gitdir. -func New(root *os.Root, algo objectid.Algorithm) (*Store, error) { - if algo.Size() == 0 { - return nil, objectid.ErrInvalidAlgorithm - } - - commonRoot, err := openCommonRoot(root) - if err != nil { - return nil, err - } - - return &Store{ - gitRoot: root, - commonRoot: commonRoot, - algo: algo, - lockRand: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint:gosec - packedRefsTimeout: detectPackedRefsTimeout(commonRoot), - }, nil -} - -// Close releases resources associated with the store. -func (store *Store) Close() error { - err := store.gitRoot.Close() - commonErr := store.commonRoot.Close() - - if err != nil { - return err - } - - return commonErr -} - -func openCommonRoot(gitRoot *os.Root) (*os.Root, error) { - content, err := gitRoot.ReadFile("commondir") - if err != nil { - if errorsIsNotExist(err) { - return gitRoot.OpenRoot(".") - } - - return nil, err - } - - commonDir := strings.TrimSpace(string(content)) - if commonDir == "" { - return nil, os.ErrNotExist - } - - if filepath.IsAbs(commonDir) { - return os.OpenRoot(commonDir) - } - - // This is okay because that's how Git defines it anyway. - return os.OpenRoot(filepath.Join(gitRoot.Name(), commonDir)) -} - -func (store *Store) rootFor(kind rootKind) *os.Root { - if kind == rootCommon { - return store.commonRoot - } - - return store.gitRoot -} - -func (store *Store) loosePath(name string) refPath { - parsed := refname.ParseWorktree(name) - switch parsed.Type { - case refname.WorktreeCurrent: - return refPath{root: rootGit, path: parsed.BareRefName} - case refname.WorktreeMain, refname.WorktreeShared: - return refPath{root: rootCommon, path: parsed.BareRefName} - case refname.WorktreeOther: - return refPath{ - root: rootCommon, - path: path.Join("worktrees", parsed.WorktreeName, parsed.BareRefName), - } - default: - return refPath{root: rootCommon, path: name} - } -} - -func detectPackedRefsTimeout(commonRoot *os.Root) time.Duration { - const defaultTimeout = time.Second - - file, err := commonRoot.Open("config") - if err != nil { - return defaultTimeout - } - - defer func() { _ = file.Close() }() - - cfg, err := config.ParseConfig(file) - if err != nil && !errors.Is(err, io.EOF) { - return defaultTimeout - } - - timeoutValue, err := cfg.Lookup("core", "", "packedrefstimeout").Int() - if err != nil { - return defaultTimeout - } - - return time.Duration(timeoutValue) * time.Millisecond -} diff --git a/refstore/files/transaction.go b/refstore/files/transaction.go index 27ce5e66..09b92caf 100644 --- a/refstore/files/transaction.go +++ b/refstore/files/transaction.go @@ -1,66 +1,9 @@ package files import ( - "errors" - "fmt" - "os" - "strings" - - "codeberg.org/lindenii/furgit/objectid" - "codeberg.org/lindenii/furgit/ref/refname" "codeberg.org/lindenii/furgit/refstore" ) -type txKind uint8 - -const ( - txCreate txKind = iota - txUpdate - txDelete - txVerify - txCreateSymbolic - txUpdateSymbolic - txDeleteSymbolic - txVerifySymbolic -) - -type txOp struct { - name string - kind txKind - newID objectid.ObjectID - oldID objectid.ObjectID - newTarget string - oldTarget string -} - -type directKind uint8 - -const ( - directMissing directKind = iota - directDetached - directSymbolic -) - -type directRef struct { - kind directKind - name string - id objectid.ObjectID - target string - isLoose bool - isPacked bool -} - -type resolvedWriteTarget struct { - name string - loc refPath - ref directRef -} - -type preparedTxOp struct { - op txOp - target resolvedWriteTarget -} - type Transaction struct { store *Store ops []txOp @@ -68,195 +11,3 @@ type Transaction struct { } var _ refstore.Transaction = (*Transaction)(nil) - -// BeginTransaction creates one new files transaction. -// -//nolint:ireturn -func (store *Store) BeginTransaction() (refstore.Transaction, error) { - return &Transaction{ - store: store, - ops: make([]txOp, 0, 8), - }, nil -} - -func (tx *Transaction) Create(name string, newID objectid.ObjectID) error { - return tx.queue(txOp{name: name, kind: txCreate, newID: newID}) -} - -func (tx *Transaction) Update(name string, newID, oldID objectid.ObjectID) error { - return tx.queue(txOp{name: name, kind: txUpdate, newID: newID, oldID: oldID}) -} - -func (tx *Transaction) Delete(name string, oldID objectid.ObjectID) error { - return tx.queue(txOp{name: name, kind: txDelete, oldID: oldID}) -} - -func (tx *Transaction) Verify(name string, oldID objectid.ObjectID) error { - return tx.queue(txOp{name: name, kind: txVerify, oldID: oldID}) -} - -func (tx *Transaction) CreateSymbolic(name, newTarget string) error { - return tx.queue(txOp{name: name, kind: txCreateSymbolic, newTarget: newTarget}) -} - -func (tx *Transaction) UpdateSymbolic(name, newTarget, oldTarget string) error { - return tx.queue(txOp{name: name, kind: txUpdateSymbolic, newTarget: newTarget, oldTarget: oldTarget}) -} - -func (tx *Transaction) DeleteSymbolic(name, oldTarget string) error { - return tx.queue(txOp{name: name, kind: txDeleteSymbolic, oldTarget: oldTarget}) -} - -func (tx *Transaction) VerifySymbolic(name, oldTarget string) error { - return tx.queue(txOp{name: name, kind: txVerifySymbolic, oldTarget: oldTarget}) -} - -func (tx *Transaction) Commit() error { - err := tx.ensureOpen() - if err != nil { - return err - } - - prepared, err := tx.prepare() - if err != nil { - tx.closed = true - - return err - } - - defer func() { - _ = tx.cleanup(prepared) - }() - - for _, item := range prepared { - if item.op.kind == txDelete || item.op.kind == txDeleteSymbolic || item.op.kind == txVerify || item.op.kind == txVerifySymbolic { - continue - } - - err = tx.writeLoose(item) - if err != nil { - tx.closed = true - - return err - } - } - - err = tx.applyPackedDeletes(prepared) - if err != nil { - tx.closed = true - - return err - } - - for _, item := range prepared { - switch item.op.kind { - case txDelete, txDeleteSymbolic: - if item.target.ref.isLoose { - err = tx.store.rootFor(item.target.loc.root).Remove(item.target.loc.path) - if err != nil && !errors.Is(err, os.ErrNotExist) { - tx.closed = true - - return err - } - - tx.tryRemoveEmptyParents(item.target.name) - } - case txCreate, txUpdate, txVerify, txCreateSymbolic, txUpdateSymbolic, txVerifySymbolic: - } - } - - tx.closed = true - - return nil -} - -func (tx *Transaction) Abort() error { - err := tx.ensureOpen() - if err != nil { - return err - } - - tx.closed = true - - return nil -} - -func (tx *Transaction) ensureOpen() error { - if tx.closed { - return fmt.Errorf("refstore/files: transaction already closed") - } - - return nil -} - -func (tx *Transaction) queue(op txOp) error { - err := tx.ensureOpen() - if err != nil { - return err - } - - err = tx.validateOp(op) - if err != nil { - return err - } - - tx.ops = append(tx.ops, op) - - return nil -} - -func (tx *Transaction) validateOp(op txOp) error { - if op.name == "" { - return fmt.Errorf("refstore/files: empty reference name") - } - - switch op.kind { - case txCreate, txUpdate: - err := refname.ValidateUpdateName(op.name, true) - if err != nil { - return err - } - - if op.newID.Size() == 0 { - return objectid.ErrInvalidAlgorithm - } - case txDelete, txVerify: - err := refname.ValidateUpdateName(op.name, false) - if err != nil { - return err - } - - if op.oldID.Size() == 0 { - return objectid.ErrInvalidAlgorithm - } - case txCreateSymbolic, txUpdateSymbolic: - err := refname.ValidateUpdateName(op.name, true) - if err != nil { - return err - } - - if strings.TrimSpace(op.newTarget) == "" { - return fmt.Errorf("refstore/files: empty symbolic target") - } - - err = refname.ValidateSymbolicTarget(op.name, strings.TrimSpace(op.newTarget)) - if err != nil { - return err - } - case txDeleteSymbolic, txVerifySymbolic: - err := refname.ValidateUpdateName(op.name, false) - if err != nil { - return err - } - default: - return fmt.Errorf("refstore/files: unsupported transaction operation %d", op.kind) - } - - if op.kind == txUpdateSymbolic || op.kind == txDeleteSymbolic || op.kind == txVerifySymbolic { - if strings.TrimSpace(op.oldTarget) == "" { - return fmt.Errorf("refstore/files: empty symbolic old target") - } - } - - return nil -} diff --git a/refstore/files/transaction_abort.go b/refstore/files/transaction_abort.go new file mode 100644 index 00000000..7b92472c --- /dev/null +++ b/refstore/files/transaction_abort.go @@ -0,0 +1,22 @@ +package files + +import "fmt" + +func (tx *Transaction) Abort() error { + err := tx.ensureOpen() + if err != nil { + return err + } + + tx.closed = true + + return nil +} + +func (tx *Transaction) ensureOpen() error { + if tx.closed { + return fmt.Errorf("refstore/files: transaction already closed") + } + + return nil +} diff --git a/refstore/files/transaction_begin.go b/refstore/files/transaction_begin.go new file mode 100644 index 00000000..73bc9767 --- /dev/null +++ b/refstore/files/transaction_begin.go @@ -0,0 +1,13 @@ +package files + +import "codeberg.org/lindenii/furgit/refstore" + +// BeginTransaction creates one new files transaction. +// +//nolint:ireturn +func (store *Store) BeginTransaction() (refstore.Transaction, error) { + return &Transaction{ + store: store, + ops: make([]txOp, 0, 8), + }, nil +} diff --git a/refstore/files/transaction_cleanup.go b/refstore/files/transaction_cleanup.go index 798688ea..8de1d19f 100644 --- a/refstore/files/transaction_cleanup.go +++ b/refstore/files/transaction_cleanup.go @@ -2,9 +2,7 @@ package files import ( "errors" - "fmt" "os" - "path" "slices" ) @@ -39,82 +37,3 @@ func (tx *Transaction) cleanup(prepared []preparedTxOp) error { return firstErr } - -func (tx *Transaction) tryRemoveEmptyParents(name string) { - loc := tx.store.loosePath(name) - tx.tryRemoveEmptyParentPaths(loc.root, loc.path) -} - -func (tx *Transaction) tryRemoveEmptyParentPaths(kind rootKind, name string) { - root := tx.store.rootFor(kind) - dir := path.Dir(name) - - for dir != "." && dir != "/" { - err := root.Remove(dir) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return - } - - var pathErr *os.PathError - if errors.As(err, &pathErr) { - return - } - - return - } - - dir = path.Dir(dir) - } -} - -func (tx *Transaction) removeEmptyDirTree(name refPath) error { - root := tx.store.rootFor(name.root) - - info, err := root.Stat(name.path) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - - return err - } - - if !info.IsDir() { - return nil - } - - return tx.removeEmptyDirTreeRecursive(name) -} - -func (tx *Transaction) removeEmptyDirTreeRecursive(name refPath) error { - root := tx.store.rootFor(name.root) - - dir, err := root.Open(name.path) - if err != nil { - return err - } - - entries, err := dir.ReadDir(-1) - _ = dir.Close() - - if err != nil { - return err - } - - for _, entry := range entries { - if !entry.IsDir() { - return fmt.Errorf("refstore/files: non-empty directory blocks reference %q", name.path) - } - - err = tx.removeEmptyDirTreeRecursive(refPath{ - root: name.root, - path: path.Join(name.path, entry.Name()), - }) - if err != nil { - return err - } - } - - return root.Remove(name.path) -} diff --git a/refstore/files/transaction_cleanup_parents.go b/refstore/files/transaction_cleanup_parents.go new file mode 100644 index 00000000..1e62e637 --- /dev/null +++ b/refstore/files/transaction_cleanup_parents.go @@ -0,0 +1,35 @@ +package files + +import ( + "errors" + "os" + "path" +) + +func (tx *Transaction) tryRemoveEmptyParents(name string) { + loc := tx.store.loosePath(name) + tx.tryRemoveEmptyParentPaths(loc.root, loc.path) +} + +func (tx *Transaction) tryRemoveEmptyParentPaths(kind rootKind, name string) { + root := tx.store.rootFor(kind) + dir := path.Dir(name) + + for dir != "." && dir != "/" { + err := root.Remove(dir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return + } + + var pathErr *os.PathError + if errors.As(err, &pathErr) { + return + } + + return + } + + dir = path.Dir(dir) + } +} diff --git a/refstore/files/transaction_commit.go b/refstore/files/transaction_commit.go new file mode 100644 index 00000000..eba959cb --- /dev/null +++ b/refstore/files/transaction_commit.go @@ -0,0 +1,65 @@ +package files + +import ( + "errors" + "os" +) + +func (tx *Transaction) Commit() error { + err := tx.ensureOpen() + if err != nil { + return err + } + + prepared, err := tx.prepare() + if err != nil { + tx.closed = true + + return err + } + + defer func() { + _ = tx.cleanup(prepared) + }() + + for _, item := range prepared { + if item.op.kind == txDelete || item.op.kind == txDeleteSymbolic || item.op.kind == txVerify || item.op.kind == txVerifySymbolic { + continue + } + + err = tx.writeLoose(item) + if err != nil { + tx.closed = true + + return err + } + } + + err = tx.applyPackedDeletes(prepared) + if err != nil { + tx.closed = true + + return err + } + + for _, item := range prepared { + switch item.op.kind { + case txDelete, txDeleteSymbolic: + if item.target.ref.isLoose { + err = tx.store.rootFor(item.target.loc.root).Remove(item.target.loc.path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + tx.closed = true + + return err + } + + tx.tryRemoveEmptyParents(item.target.name) + } + case txCreate, txUpdate, txVerify, txCreateSymbolic, txUpdateSymbolic, txVerifySymbolic: + } + } + + tx.closed = true + + return nil +} diff --git a/refstore/files/transaction_dir_tree.go b/refstore/files/transaction_dir_tree.go new file mode 100644 index 00000000..e317f604 --- /dev/null +++ b/refstore/files/transaction_dir_tree.go @@ -0,0 +1,59 @@ +package files + +import ( + "errors" + "fmt" + "os" + "path" +) + +func (tx *Transaction) removeEmptyDirTree(name refPath) error { + root := tx.store.rootFor(name.root) + + info, err := root.Stat(name.path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + + return err + } + + if !info.IsDir() { + return nil + } + + return tx.removeEmptyDirTreeRecursive(name) +} + +func (tx *Transaction) removeEmptyDirTreeRecursive(name refPath) error { + root := tx.store.rootFor(name.root) + + dir, err := root.Open(name.path) + if err != nil { + return err + } + + entries, err := dir.ReadDir(-1) + _ = dir.Close() + + if err != nil { + return err + } + + for _, entry := range entries { + if !entry.IsDir() { + return fmt.Errorf("refstore/files: non-empty directory blocks reference %q", name.path) + } + + err = tx.removeEmptyDirTreeRecursive(refPath{ + root: name.root, + path: path.Join(name.path, entry.Name()), + }) + if err != nil { + return err + } + } + + return root.Remove(name.path) +} diff --git a/refstore/files/transaction_direct_read.go b/refstore/files/transaction_direct_read.go new file mode 100644 index 00000000..4da6a499 --- /dev/null +++ b/refstore/files/transaction_direct_read.go @@ -0,0 +1,76 @@ +package files + +import ( + "errors" + "fmt" + + "codeberg.org/lindenii/furgit/ref" + "codeberg.org/lindenii/furgit/ref/refname" + "codeberg.org/lindenii/furgit/refstore" +) + +func (tx *Transaction) directRead(name string) (directRef, error) { + loc := tx.store.loosePath(name) + hasPacked := false + + if loc.root == rootCommon && refname.ParseWorktree(name).Type == refname.WorktreeShared { + packed, packedErr := tx.store.readPackedRefs() + if packedErr != nil { + return directRef{}, packedErr + } + + _, hasPacked = packed.byName[name] + } + + loose, err := tx.store.readLooseRef(name) + if err == nil { + switch loose := loose.(type) { + case ref.Detached: + return directRef{ + kind: directDetached, + name: name, + id: loose.ID, + isLoose: true, + isPacked: hasPacked, + }, nil + case ref.Symbolic: + return directRef{ + kind: directSymbolic, + name: name, + target: loose.Target, + isLoose: true, + isPacked: hasPacked, + }, nil + default: + return directRef{}, fmt.Errorf("refstore/files: unsupported reference type %T", loose) + } + } + + if !errors.Is(err, refstore.ErrReferenceNotFound) { + info, statErr := tx.store.rootFor(loc.root).Stat(loc.path) + if statErr != nil || !info.IsDir() { + return directRef{}, err + } + } + + if hasPacked { + packed, packedErr := tx.store.readPackedRefs() + if packedErr != nil { + return directRef{}, packedErr + } + + detached := packed.byName[name] + + return directRef{ + kind: directDetached, + name: name, + id: detached.ID, + isPacked: true, + }, nil + } + + return directRef{ + kind: directMissing, + name: name, + }, nil +} diff --git a/refstore/files/transaction_direct_ref.go b/refstore/files/transaction_direct_ref.go new file mode 100644 index 00000000..970e7b6a --- /dev/null +++ b/refstore/files/transaction_direct_ref.go @@ -0,0 +1,20 @@ +package files + +import "codeberg.org/lindenii/furgit/objectid" + +type directKind uint8 + +const ( + directMissing directKind = iota + directDetached + directSymbolic +) + +type directRef struct { + kind directKind + name string + id objectid.ObjectID + target string + isLoose bool + isPacked bool +} diff --git a/refstore/files/transaction_kind.go b/refstore/files/transaction_kind.go new file mode 100644 index 00000000..d4f84031 --- /dev/null +++ b/refstore/files/transaction_kind.go @@ -0,0 +1,14 @@ +package files + +type txKind uint8 + +const ( + txCreate txKind = iota + txUpdate + txDelete + txVerify + txCreateSymbolic + txUpdateSymbolic + txDeleteSymbolic + txVerifySymbolic +) diff --git a/refstore/files/transaction_lock.go b/refstore/files/transaction_lock.go index e10c1a68..20a89c78 100644 --- a/refstore/files/transaction_lock.go +++ b/refstore/files/transaction_lock.go @@ -1,12 +1,8 @@ package files import ( - "errors" - "fmt" "os" "path" - "strings" - "time" ) func (tx *Transaction) createLock(name refPath) error { @@ -27,58 +23,3 @@ func (tx *Transaction) createLock(name refPath) error { return file.Close() } - -func (tx *Transaction) createPackedLock() error { - const ( - initialBackoffMs = 1 - backoffMaxMultiplier = 1000 - ) - - timeout := tx.store.packedRefsTimeout - deadline := time.Now().Add(timeout) - multiplier := 1 - n := 1 - - for { - file, err := tx.store.commonRoot.OpenFile("packed-refs.lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) - if err == nil { - return file.Close() - } - - if !errors.Is(err, os.ErrExist) { - return err - } - - if timeout == 0 || (timeout > 0 && time.Now().After(deadline)) { - return err - } - - backoffMs := multiplier * initialBackoffMs - waitMs := (750 + tx.store.lockRand.Intn(500)) * backoffMs / 1000 - time.Sleep(time.Duration(waitMs) * time.Millisecond) - - multiplier += 2*n + 1 - if multiplier > backoffMaxMultiplier { - multiplier = backoffMaxMultiplier - } else { - n++ - } - } -} - -func (tx *Transaction) targetKey(name refPath) string { - return fmt.Sprintf("%d:%s", name.root, name.path) -} - -func refPathFromKey(key string) refPath { - rootValue, pathValue, ok := strings.Cut(key, ":") - if !ok || rootValue == "" { - return refPath{root: rootCommon, path: key} - } - - if rootValue == "0" { - return refPath{root: rootGit, path: pathValue} - } - - return refPath{root: rootCommon, path: pathValue} -} diff --git a/refstore/files/transaction_lock_packed.go b/refstore/files/transaction_lock_packed.go new file mode 100644 index 00000000..4538e5e5 --- /dev/null +++ b/refstore/files/transaction_lock_packed.go @@ -0,0 +1,44 @@ +package files + +import ( + "errors" + "os" + "time" +) + +func (tx *Transaction) createPackedLock(timeout time.Duration) error { + const ( + initialBackoffMs = 1 + backoffMaxMultiplier = 1000 + ) + + deadline := time.Now().Add(timeout) + multiplier := 1 + n := 1 + + for { + file, err := tx.store.commonRoot.OpenFile("packed-refs.lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + if err == nil { + return file.Close() + } + + if !errors.Is(err, os.ErrExist) { + return err + } + + if timeout == 0 || (timeout > 0 && time.Now().After(deadline)) { + return err + } + + backoffMs := multiplier * initialBackoffMs + waitMs := (750 + tx.store.lockRand.Intn(500)) * backoffMs / 1000 + time.Sleep(time.Duration(waitMs) * time.Millisecond) + + multiplier += 2*n + 1 + if multiplier > backoffMaxMultiplier { + multiplier = backoffMaxMultiplier + } else { + n++ + } + } +} diff --git a/refstore/files/transaction_operation.go b/refstore/files/transaction_operation.go new file mode 100644 index 00000000..bb24c5a2 --- /dev/null +++ b/refstore/files/transaction_operation.go @@ -0,0 +1,23 @@ +package files + +import "codeberg.org/lindenii/furgit/objectid" + +type txOp struct { + name string + kind txKind + newID objectid.ObjectID + oldID objectid.ObjectID + newTarget string + oldTarget string +} + +type preparedTxOp struct { + op txOp + target resolvedWriteTarget +} + +type resolvedWriteTarget struct { + name string + loc refPath + ref directRef +} diff --git a/refstore/files/transaction_prepare.go b/refstore/files/transaction_prepare.go index 68dc15dc..38eea9d8 100644 --- a/refstore/files/transaction_prepare.go +++ b/refstore/files/transaction_prepare.go @@ -1,14 +1,8 @@ package files import ( - "errors" "fmt" "slices" - "strings" - - "codeberg.org/lindenii/furgit/ref" - "codeberg.org/lindenii/furgit/ref/refname" - "codeberg.org/lindenii/furgit/refstore" ) func (tx *Transaction) prepare() (prepared []preparedTxOp, err error) { @@ -82,7 +76,7 @@ func (tx *Transaction) prepare() (prepared []preparedTxOp, err error) { hasDeletes := len(deleted) > 0 if hasDeletes { - err = tx.createPackedLock() + err = tx.createPackedLock(tx.store.packedRefsTimeout) if err != nil { return prepared, err } @@ -106,187 +100,3 @@ func (tx *Transaction) prepare() (prepared []preparedTxOp, err error) { return prepared, nil } - -func (tx *Transaction) resolveTarget(op txOp) (resolvedWriteTarget, error) { - switch op.kind { - case txCreate: - return tx.resolveOrdinaryTarget(op.name, true) - case txUpdate, txDelete, txVerify: - return tx.resolveOrdinaryTarget(op.name, false) - case txCreateSymbolic, txUpdateSymbolic, txDeleteSymbolic, txVerifySymbolic: - refState, err := tx.directRead(op.name) - if err != nil { - return resolvedWriteTarget{}, err - } - - return resolvedWriteTarget{name: op.name, loc: tx.store.loosePath(op.name), ref: refState}, nil - default: - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: unsupported transaction operation %d", op.kind) - } -} - -func (tx *Transaction) resolveOrdinaryTarget(name string, allowMissing bool) (resolvedWriteTarget, error) { - cur := name - seen := make(map[string]struct{}) - - for { - if _, ok := seen[cur]; ok { - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: symbolic reference cycle at %q", cur) - } - - seen[cur] = struct{}{} - - refState, err := tx.directRead(cur) - if err != nil { - return resolvedWriteTarget{}, err - } - - switch refState.kind { - case directMissing: - if !allowMissing { - return resolvedWriteTarget{}, refstore.ErrReferenceNotFound - } - - return resolvedWriteTarget{name: cur, loc: tx.store.loosePath(cur), ref: refState}, nil - case directDetached: - return resolvedWriteTarget{name: cur, loc: tx.store.loosePath(cur), ref: refState}, nil - case directSymbolic: - target := strings.TrimSpace(refState.target) - if target == "" { - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: symbolic reference %q has empty target", cur) - } - - cur = target - default: - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: unsupported direct reference state %d", refState.kind) - } - } -} - -func (tx *Transaction) directRead(name string) (directRef, error) { - loc := tx.store.loosePath(name) - hasPacked := false - - if loc.root == rootCommon && refname.ParseWorktree(name).Type == refname.WorktreeShared { - packed, packedErr := tx.store.readPackedRefs() - if packedErr != nil { - return directRef{}, packedErr - } - - _, hasPacked = packed.byName[name] - } - - loose, err := tx.store.readLooseRef(name) - if err == nil { - switch loose := loose.(type) { - case ref.Detached: - return directRef{ - kind: directDetached, - name: name, - id: loose.ID, - isLoose: true, - isPacked: hasPacked, - }, nil - case ref.Symbolic: - return directRef{ - kind: directSymbolic, - name: name, - target: loose.Target, - isLoose: true, - isPacked: hasPacked, - }, nil - default: - return directRef{}, fmt.Errorf("refstore/files: unsupported reference type %T", loose) - } - } - - if !errors.Is(err, refstore.ErrReferenceNotFound) { - info, statErr := tx.store.rootFor(loc.root).Stat(loc.path) - if statErr != nil || !info.IsDir() { - return directRef{}, err - } - } - - if hasPacked { - packed, packedErr := tx.store.readPackedRefs() - if packedErr != nil { - return directRef{}, packedErr - } - - detached := packed.byName[name] - - return directRef{ - kind: directDetached, - name: name, - id: detached.ID, - isPacked: true, - }, nil - } - - return directRef{ - kind: directMissing, - name: name, - }, nil -} - -func (tx *Transaction) visibleNames() (map[string]struct{}, error) { - names := make(map[string]struct{}) - - looseNames, err := tx.store.collectLooseRefNames() - if err != nil { - return nil, err - } - - for _, name := range looseNames { - names[name] = struct{}{} - } - - packed, err := tx.store.readPackedRefs() - if err != nil { - return nil, err - } - - for name := range packed.byName { - if _, exists := names[name]; exists { - continue - } - - names[name] = struct{}{} - } - - return names, nil -} - -func verifyRefnameAvailable(name string, existing map[string]struct{}, writes []string, deleted map[string]struct{}) error { - for existingName := range existing { - if existingName == name { - continue - } - - if _, skip := deleted[existingName]; skip { - continue - } - - if refnamesConflict(name, existingName) { - return fmt.Errorf("refstore/files: reference name conflict between %q and %q", name, existingName) - } - } - - for _, other := range writes { - if other == name { - continue - } - - if refnamesConflict(name, other) { - return fmt.Errorf("refstore/files: reference name conflict between %q and %q", name, other) - } - } - - return nil -} - -func refnamesConflict(left, right string) bool { - return left == right || - strings.HasPrefix(left, right+"/") || - strings.HasPrefix(right, left+"/") -} diff --git a/refstore/files/transaction_queue.go b/refstore/files/transaction_queue.go new file mode 100644 index 00000000..59b49419 --- /dev/null +++ b/refstore/files/transaction_queue.go @@ -0,0 +1,17 @@ +package files + +func (tx *Transaction) queue(op txOp) error { + err := tx.ensureOpen() + if err != nil { + return err + } + + err = tx.validateOp(op) + if err != nil { + return err + } + + tx.ops = append(tx.ops, op) + + return nil +} diff --git a/refstore/files/transaction_queue_ops.go b/refstore/files/transaction_queue_ops.go new file mode 100644 index 00000000..ff966559 --- /dev/null +++ b/refstore/files/transaction_queue_ops.go @@ -0,0 +1,35 @@ +package files + +import "codeberg.org/lindenii/furgit/objectid" + +func (tx *Transaction) Create(name string, newID objectid.ObjectID) error { + return tx.queue(txOp{name: name, kind: txCreate, newID: newID}) +} + +func (tx *Transaction) Update(name string, newID, oldID objectid.ObjectID) error { + return tx.queue(txOp{name: name, kind: txUpdate, newID: newID, oldID: oldID}) +} + +func (tx *Transaction) Delete(name string, oldID objectid.ObjectID) error { + return tx.queue(txOp{name: name, kind: txDelete, oldID: oldID}) +} + +func (tx *Transaction) Verify(name string, oldID objectid.ObjectID) error { + return tx.queue(txOp{name: name, kind: txVerify, oldID: oldID}) +} + +func (tx *Transaction) CreateSymbolic(name, newTarget string) error { + return tx.queue(txOp{name: name, kind: txCreateSymbolic, newTarget: newTarget}) +} + +func (tx *Transaction) UpdateSymbolic(name, newTarget, oldTarget string) error { + return tx.queue(txOp{name: name, kind: txUpdateSymbolic, newTarget: newTarget, oldTarget: oldTarget}) +} + +func (tx *Transaction) DeleteSymbolic(name, oldTarget string) error { + return tx.queue(txOp{name: name, kind: txDeleteSymbolic, oldTarget: oldTarget}) +} + +func (tx *Transaction) VerifySymbolic(name, oldTarget string) error { + return tx.queue(txOp{name: name, kind: txVerifySymbolic, oldTarget: oldTarget}) +} diff --git a/refstore/files/transaction_resolve_target.go b/refstore/files/transaction_resolve_target.go new file mode 100644 index 00000000..08f24b1c --- /dev/null +++ b/refstore/files/transaction_resolve_target.go @@ -0,0 +1,21 @@ +package files + +import "fmt" + +func (tx *Transaction) resolveTarget(op txOp) (resolvedWriteTarget, error) { + switch op.kind { + case txCreate: + return tx.resolveOrdinaryTarget(op.name, true) + case txUpdate, txDelete, txVerify: + return tx.resolveOrdinaryTarget(op.name, false) + case txCreateSymbolic, txUpdateSymbolic, txDeleteSymbolic, txVerifySymbolic: + refState, err := tx.directRead(op.name) + if err != nil { + return resolvedWriteTarget{}, err + } + + return resolvedWriteTarget{name: op.name, loc: tx.store.loosePath(op.name), ref: refState}, nil + default: + return resolvedWriteTarget{}, fmt.Errorf("refstore/files: unsupported transaction operation %d", op.kind) + } +} diff --git a/refstore/files/transaction_resolve_target_ordinary.go b/refstore/files/transaction_resolve_target_ordinary.go new file mode 100644 index 00000000..a495b2af --- /dev/null +++ b/refstore/files/transaction_resolve_target_ordinary.go @@ -0,0 +1,46 @@ +package files + +import ( + "fmt" + "strings" + + "codeberg.org/lindenii/furgit/refstore" +) + +func (tx *Transaction) resolveOrdinaryTarget(name string, allowMissing bool) (resolvedWriteTarget, error) { + cur := name + seen := make(map[string]struct{}) + + for { + if _, ok := seen[cur]; ok { + return resolvedWriteTarget{}, fmt.Errorf("refstore/files: symbolic reference cycle at %q", cur) + } + + seen[cur] = struct{}{} + + refState, err := tx.directRead(cur) + if err != nil { + return resolvedWriteTarget{}, err + } + + switch refState.kind { + case directMissing: + if !allowMissing { + return resolvedWriteTarget{}, refstore.ErrReferenceNotFound + } + + return resolvedWriteTarget{name: cur, loc: tx.store.loosePath(cur), ref: refState}, nil + case directDetached: + return resolvedWriteTarget{name: cur, loc: tx.store.loosePath(cur), ref: refState}, nil + case directSymbolic: + target := strings.TrimSpace(refState.target) + if target == "" { + return resolvedWriteTarget{}, fmt.Errorf("refstore/files: symbolic reference %q has empty target", cur) + } + + cur = target + default: + return resolvedWriteTarget{}, fmt.Errorf("refstore/files: unsupported direct reference state %d", refState.kind) + } + } +} diff --git a/refstore/files/transaction_validate.go b/refstore/files/transaction_validate.go new file mode 100644 index 00000000..784db2a4 --- /dev/null +++ b/refstore/files/transaction_validate.go @@ -0,0 +1,65 @@ +package files + +import ( + "fmt" + "strings" + + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/ref/refname" +) + +func (tx *Transaction) validateOp(op txOp) error { + if op.name == "" { + return fmt.Errorf("refstore/files: empty reference name") + } + + switch op.kind { + case txCreate, txUpdate: + err := refname.ValidateUpdateName(op.name, true) + if err != nil { + return err + } + + if op.newID.Size() == 0 { + return objectid.ErrInvalidAlgorithm + } + case txDelete, txVerify: + err := refname.ValidateUpdateName(op.name, false) + if err != nil { + return err + } + + if op.oldID.Size() == 0 { + return objectid.ErrInvalidAlgorithm + } + case txCreateSymbolic, txUpdateSymbolic: + err := refname.ValidateUpdateName(op.name, true) + if err != nil { + return err + } + + if strings.TrimSpace(op.newTarget) == "" { + return fmt.Errorf("refstore/files: empty symbolic target") + } + + err = refname.ValidateSymbolicTarget(op.name, strings.TrimSpace(op.newTarget)) + if err != nil { + return err + } + case txDeleteSymbolic, txVerifySymbolic: + err := refname.ValidateUpdateName(op.name, false) + if err != nil { + return err + } + default: + return fmt.Errorf("refstore/files: unsupported transaction operation %d", op.kind) + } + + if op.kind == txUpdateSymbolic || op.kind == txDeleteSymbolic || op.kind == txVerifySymbolic { + if strings.TrimSpace(op.oldTarget) == "" { + return fmt.Errorf("refstore/files: empty symbolic old target") + } + } + + return nil +} diff --git a/refstore/files/transaction_verify_current.go b/refstore/files/transaction_verify_current.go new file mode 100644 index 00000000..03ee3e9c --- /dev/null +++ b/refstore/files/transaction_verify_current.go @@ -0,0 +1,53 @@ +package files + +import ( + "fmt" + "strings" +) + +func (tx *Transaction) verifyCurrent(item preparedTxOp) error { + switch item.op.kind { + case txCreate: + if item.target.ref.kind != directMissing { + return fmt.Errorf("refstore/files: reference %q already exists", item.target.name) + } + + return nil + case txUpdate, txDelete, txVerify: + if item.target.ref.kind == directMissing { + return fmt.Errorf("refstore/files: reference %q is missing", item.target.name) + } + + if item.target.ref.kind != directDetached { + return fmt.Errorf("refstore/files: reference %q is not detached", item.target.name) + } + + if item.target.ref.id != item.op.oldID { + return fmt.Errorf("refstore/files: reference %q is at %s but expected %s", item.target.name, item.target.ref.id, item.op.oldID) + } + + return nil + case txCreateSymbolic: + if item.target.ref.kind != directMissing { + return fmt.Errorf("refstore/files: reference %q already exists", item.target.name) + } + + return nil + case txUpdateSymbolic, txDeleteSymbolic, txVerifySymbolic: + if item.target.ref.kind == directMissing { + return fmt.Errorf("refstore/files: symbolic reference %q is missing", item.target.name) + } + + if item.target.ref.kind != directSymbolic { + return fmt.Errorf("refstore/files: reference %q is not symbolic", item.target.name) + } + + if strings.TrimSpace(item.target.ref.target) != strings.TrimSpace(item.op.oldTarget) { + return fmt.Errorf("refstore/files: reference %q points at %q, expected %q", item.target.name, item.target.ref.target, item.op.oldTarget) + } + + return nil + default: + return fmt.Errorf("refstore/files: unsupported transaction operation %d", item.op.kind) + } +} diff --git a/refstore/files/transaction_verify_refnames.go b/refstore/files/transaction_verify_refnames.go new file mode 100644 index 00000000..2efc872a --- /dev/null +++ b/refstore/files/transaction_verify_refnames.go @@ -0,0 +1,40 @@ +package files + +import ( + "fmt" + "strings" +) + +func verifyRefnameAvailable(name string, existing map[string]struct{}, writes []string, deleted map[string]struct{}) error { + for existingName := range existing { + if existingName == name { + continue + } + + if _, skip := deleted[existingName]; skip { + continue + } + + if refnamesConflict(name, existingName) { + return fmt.Errorf("refstore/files: reference name conflict between %q and %q", name, existingName) + } + } + + for _, other := range writes { + if other == name { + continue + } + + if refnamesConflict(name, other) { + return fmt.Errorf("refstore/files: reference name conflict between %q and %q", name, other) + } + } + + return nil +} + +func refnamesConflict(left, right string) bool { + return left == right || + strings.HasPrefix(left, right+"/") || + strings.HasPrefix(right, left+"/") +} diff --git a/refstore/files/transaction_visible_names.go b/refstore/files/transaction_visible_names.go new file mode 100644 index 00000000..ef5941c2 --- /dev/null +++ b/refstore/files/transaction_visible_names.go @@ -0,0 +1,29 @@ +package files + +func (tx *Transaction) visibleNames() (map[string]struct{}, error) { + names := make(map[string]struct{}) + + looseNames, err := tx.store.collectLooseRefNames() + if err != nil { + return nil, err + } + + for _, name := range looseNames { + names[name] = struct{}{} + } + + packed, err := tx.store.readPackedRefs() + if err != nil { + return nil, err + } + + for name := range packed.byName { + if _, exists := names[name]; exists { + continue + } + + names[name] = struct{}{} + } + + return names, nil +} diff --git a/refstore/files/transaction_write.go b/refstore/files/transaction_write.go deleted file mode 100644 index 678994f0..00000000 --- a/refstore/files/transaction_write.go +++ /dev/null @@ -1,199 +0,0 @@ -package files - -import ( - "errors" - "fmt" - "os" - "path" - "strings" -) - -func (tx *Transaction) verifyCurrent(item preparedTxOp) error { - switch item.op.kind { - case txCreate: - if item.target.ref.kind != directMissing { - return fmt.Errorf("refstore/files: reference %q already exists", item.target.name) - } - - return nil - case txUpdate, txDelete, txVerify: - if item.target.ref.kind == directMissing { - return fmt.Errorf("refstore/files: reference %q is missing", item.target.name) - } - - if item.target.ref.kind != directDetached { - return fmt.Errorf("refstore/files: reference %q is not detached", item.target.name) - } - - if item.target.ref.id != item.op.oldID { - return fmt.Errorf("refstore/files: reference %q is at %s but expected %s", item.target.name, item.target.ref.id, item.op.oldID) - } - - return nil - case txCreateSymbolic: - if item.target.ref.kind != directMissing { - return fmt.Errorf("refstore/files: reference %q already exists", item.target.name) - } - - return nil - case txUpdateSymbolic, txDeleteSymbolic, txVerifySymbolic: - if item.target.ref.kind == directMissing { - return fmt.Errorf("refstore/files: symbolic reference %q is missing", item.target.name) - } - - if item.target.ref.kind != directSymbolic { - return fmt.Errorf("refstore/files: reference %q is not symbolic", item.target.name) - } - - if strings.TrimSpace(item.target.ref.target) != strings.TrimSpace(item.op.oldTarget) { - return fmt.Errorf("refstore/files: reference %q points at %q, expected %q", item.target.name, item.target.ref.target, item.op.oldTarget) - } - - return nil - default: - return fmt.Errorf("refstore/files: unsupported transaction operation %d", item.op.kind) - } -} - -func (tx *Transaction) writeLoose(item preparedTxOp) error { - root := tx.store.rootFor(item.target.loc.root) - lockName := item.target.loc.path + ".lock" - - lock, err := root.OpenFile(lockName, os.O_WRONLY|os.O_TRUNC, 0o644) - if err != nil { - return err - } - - var content string - - switch item.op.kind { - case txCreate, txUpdate: - content = item.op.newID.String() + "\n" - case txCreateSymbolic, txUpdateSymbolic: - content = "ref: " + strings.TrimSpace(item.op.newTarget) + "\n" - case txDelete, txVerify, txDeleteSymbolic, txVerifySymbolic: - default: - _ = lock.Close() - - return fmt.Errorf("refstore/files: unsupported write operation %d", item.op.kind) - } - - _, err = lock.WriteString(content) - if err != nil { - _ = lock.Close() - - return err - } - - err = lock.Close() - if err != nil { - return err - } - - dir := path.Dir(item.target.loc.path) - if dir != "." { - err = root.MkdirAll(dir, 0o755) - if err != nil { - return err - } - } - - err = tx.removeEmptyDirTree(item.target.loc) - if err != nil { - return err - } - - return root.Rename(lockName, item.target.loc.path) -} - -func (tx *Transaction) applyPackedDeletes(prepared []preparedTxOp) error { - _, err := tx.store.commonRoot.Stat("packed-refs.lock") - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - - return err - } - - packed, err := tx.store.readPackedRefs() - if err != nil { - return err - } - - deleted := make(map[string]struct{}) - needed := false - - for _, item := range prepared { - if item.op.kind != txDelete && item.op.kind != txDeleteSymbolic { - continue - } - - deleted[item.target.name] = struct{}{} - if item.target.ref.isPacked { - needed = true - } - } - - if !needed { - return nil - } - - lock, err := tx.store.commonRoot.OpenFile("packed-refs.new", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) - if err != nil { - return err - } - - createdTemp := true - - defer func() { - if !createdTemp { - return - } - - _ = tx.store.commonRoot.Remove("packed-refs.new") - }() - - _, err = lock.WriteString("# pack-refs with: peeled fully-peeled sorted\n") - if err != nil { - _ = lock.Close() - - return err - } - - for _, entry := range packed.ordered { - if _, skip := deleted[entry.Name()]; skip { - continue - } - - _, err = lock.WriteString(entry.ID.String() + " " + entry.Name() + "\n") - if err != nil { - _ = lock.Close() - - return err - } - - if entry.Peeled != nil { - _, err = lock.WriteString("^" + entry.Peeled.String() + "\n") - if err != nil { - _ = lock.Close() - - return err - } - } - } - - err = lock.Close() - if err != nil { - return err - } - - err = tx.store.commonRoot.Rename("packed-refs.new", "packed-refs") - if err != nil { - return err - } - - createdTemp = false - - return nil -} diff --git a/refstore/files/transaction_write_loose.go b/refstore/files/transaction_write_loose.go new file mode 100644 index 00000000..b2a0e5d6 --- /dev/null +++ b/refstore/files/transaction_write_loose.go @@ -0,0 +1,59 @@ +package files + +import ( + "fmt" + "os" + "path" + "strings" +) + +func (tx *Transaction) writeLoose(item preparedTxOp) error { + root := tx.store.rootFor(item.target.loc.root) + lockName := item.target.loc.path + ".lock" + + lock, err := root.OpenFile(lockName, os.O_WRONLY|os.O_TRUNC, 0o644) + if err != nil { + return err + } + + var content string + + switch item.op.kind { + case txCreate, txUpdate: + content = item.op.newID.String() + "\n" + case txCreateSymbolic, txUpdateSymbolic: + content = "ref: " + strings.TrimSpace(item.op.newTarget) + "\n" + case txDelete, txVerify, txDeleteSymbolic, txVerifySymbolic: + default: + _ = lock.Close() + + return fmt.Errorf("refstore/files: unsupported write operation %d", item.op.kind) + } + + _, err = lock.WriteString(content) + if err != nil { + _ = lock.Close() + + return err + } + + err = lock.Close() + if err != nil { + return err + } + + dir := path.Dir(item.target.loc.path) + if dir != "." { + err = root.MkdirAll(dir, 0o755) + if err != nil { + return err + } + } + + err = tx.removeEmptyDirTree(item.target.loc) + if err != nil { + return err + } + + return root.Rename(lockName, item.target.loc.path) +} diff --git a/refstore/files/transaction_write_packed_deltas.go b/refstore/files/transaction_write_packed_deltas.go new file mode 100644 index 00000000..5fe07a7a --- /dev/null +++ b/refstore/files/transaction_write_packed_deltas.go @@ -0,0 +1,98 @@ +package files + +import ( + "errors" + "os" +) + +func (tx *Transaction) applyPackedDeletes(prepared []preparedTxOp) error { + _, err := tx.store.commonRoot.Stat("packed-refs.lock") + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + + return err + } + + packed, err := tx.store.readPackedRefs() + if err != nil { + return err + } + + deleted := make(map[string]struct{}) + needed := false + + for _, item := range prepared { + if item.op.kind != txDelete && item.op.kind != txDeleteSymbolic { + continue + } + + deleted[item.target.name] = struct{}{} + if item.target.ref.isPacked { + needed = true + } + } + + if !needed { + return nil + } + + lock, err := tx.store.commonRoot.OpenFile("packed-refs.new", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + if err != nil { + return err + } + + createdTemp := true + + defer func() { + if !createdTemp { + return + } + + _ = tx.store.commonRoot.Remove("packed-refs.new") + }() + + _, err = lock.WriteString("# pack-refs with: peeled fully-peeled sorted\n") + if err != nil { + _ = lock.Close() + + return err + } + + for _, entry := range packed.ordered { + if _, skip := deleted[entry.Name()]; skip { + continue + } + + _, err = lock.WriteString(entry.ID.String() + " " + entry.Name() + "\n") + if err != nil { + _ = lock.Close() + + return err + } + + if entry.Peeled != nil { + _, err = lock.WriteString("^" + entry.Peeled.String() + "\n") + if err != nil { + _ = lock.Close() + + return err + } + } + } + + err = lock.Close() + if err != nil { + return err + } + + err = tx.store.commonRoot.Rename("packed-refs.new", "packed-refs") + if err != nil { + return err + } + + createdTemp = false + + return nil +} diff --git a/refstore/files/trim.go b/refstore/files/trim.go new file mode 100644 index 00000000..69a851dc --- /dev/null +++ b/refstore/files/trim.go @@ -0,0 +1,10 @@ +package files + +func isRefWhitespace(r rune) bool { + switch r { + case ' ', '\t', '\n', '\r', '\v', '\f': + return true + default: + return false + } +} |
