aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-03-07 18:09:20 +0800
committerGravatar Runxi Yu2026-03-07 18:17:54 +0800
commite667c3c52a535ee67fe895bb0240fbad6e920087 (patch)
tree0815f7cc9b2c4a06d00722bce4c3ac57c515288b
parentreceivepack: Connect protocol with service (diff)
signatureNo signature
refstore/files: Accept timeout instead of reading from config
And split things up again.
-rw-r--r--refstore/files/close.go13
-rw-r--r--refstore/files/errors.go16
-rw-r--r--refstore/files/helpers_test.go7
-rw-r--r--refstore/files/new.go29
-rw-r--r--refstore/files/packed_delete_test.go1
-rw-r--r--refstore/files/packed_parse.go113
-rw-r--r--refstore/files/packed_read.go35
-rw-r--r--refstore/files/packed_refs.go140
-rw-r--r--refstore/files/read_list.go72
-rw-r--r--refstore/files/read_list_collect.go78
-rw-r--r--refstore/files/read_loose.go (renamed from refstore/files/read.go)28
-rw-r--r--refstore/files/read_resolve.go36
-rw-r--r--refstore/files/read_resolve_fully.go42
-rw-r--r--refstore/files/root_for.go13
-rw-r--r--refstore/files/root_kind.go8
-rw-r--r--refstore/files/root_loose_path.go24
-rw-r--r--refstore/files/root_open_common.go31
-rw-r--r--refstore/files/root_ref_path.go28
-rw-r--r--refstore/files/store.go122
-rw-r--r--refstore/files/transaction.go249
-rw-r--r--refstore/files/transaction_abort.go22
-rw-r--r--refstore/files/transaction_begin.go13
-rw-r--r--refstore/files/transaction_cleanup.go81
-rw-r--r--refstore/files/transaction_cleanup_parents.go35
-rw-r--r--refstore/files/transaction_commit.go65
-rw-r--r--refstore/files/transaction_dir_tree.go59
-rw-r--r--refstore/files/transaction_direct_read.go76
-rw-r--r--refstore/files/transaction_direct_ref.go20
-rw-r--r--refstore/files/transaction_kind.go14
-rw-r--r--refstore/files/transaction_lock.go59
-rw-r--r--refstore/files/transaction_lock_packed.go44
-rw-r--r--refstore/files/transaction_operation.go23
-rw-r--r--refstore/files/transaction_prepare.go192
-rw-r--r--refstore/files/transaction_queue.go17
-rw-r--r--refstore/files/transaction_queue_ops.go35
-rw-r--r--refstore/files/transaction_resolve_target.go21
-rw-r--r--refstore/files/transaction_resolve_target_ordinary.go46
-rw-r--r--refstore/files/transaction_validate.go65
-rw-r--r--refstore/files/transaction_verify_current.go53
-rw-r--r--refstore/files/transaction_verify_refnames.go40
-rw-r--r--refstore/files/transaction_visible_names.go29
-rw-r--r--refstore/files/transaction_write.go199
-rw-r--r--refstore/files/transaction_write_loose.go59
-rw-r--r--refstore/files/transaction_write_packed_deltas.go98
-rw-r--r--refstore/files/trim.go10
-rw-r--r--repository/open.go2
-rw-r--r--repository/refs.go5
-rw-r--r--repository/refs_timeout.go16
48 files changed, 1301 insertions, 1182 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
+ }
+}
diff --git a/repository/open.go b/repository/open.go
index 3789cc43..f1bff5da 100644
--- a/repository/open.go
+++ b/repository/open.go
@@ -36,7 +36,7 @@ func Open(root *os.Root) (repo *Repository, err error) {
repo.objects = objects
repo.objectsLooseForWritingOnly = objectsLooseForWritingOnly
- refs, err := openRefStore(root, algo)
+ refs, err := openRefStore(root, algo, detectPackedRefsTimeout(cfg))
if err != nil {
return nil, err
}
diff --git a/repository/refs.go b/repository/refs.go
index 7c978ed3..a695252d 100644
--- a/repository/refs.go
+++ b/repository/refs.go
@@ -3,6 +3,7 @@ package repository
import (
"fmt"
"os"
+ "time"
"codeberg.org/lindenii/furgit/objectid"
"codeberg.org/lindenii/furgit/refstore"
@@ -10,13 +11,13 @@ import (
)
//nolint:ireturn
-func openRefStore(root *os.Root, algo objectid.Algorithm) (out refstore.ReadingStore, err error) {
+func openRefStore(root *os.Root, algo objectid.Algorithm, packedRefsTimeout time.Duration) (out refstore.ReadingStore, err error) {
refRoot, err := root.OpenRoot(".")
if err != nil {
return nil, fmt.Errorf("repository: open root for refs: %w", err)
}
- store, err := reffiles.New(refRoot, algo)
+ store, err := reffiles.New(refRoot, algo, packedRefsTimeout)
if err != nil {
_ = refRoot.Close()
diff --git a/repository/refs_timeout.go b/repository/refs_timeout.go
new file mode 100644
index 00000000..275613ba
--- /dev/null
+++ b/repository/refs_timeout.go
@@ -0,0 +1,16 @@
+package repository
+
+import (
+ "time"
+
+ "codeberg.org/lindenii/furgit/config"
+)
+
+func detectPackedRefsTimeout(cfg *config.Config) time.Duration {
+ timeoutValue, err := cfg.Lookup("core", "", "packedrefstimeout").Int()
+ if err != nil {
+ return time.Second
+ }
+
+ return time.Duration(timeoutValue) * time.Millisecond
+}