From 929b8cc620abca70b3444b09be5249f6c6cb7812 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Thu, 26 Mar 2026 09:19:01 +0000 Subject: ref/name: Rename from ref/refname --- ref/name/branch.go | 25 ++ ref/name/component.go | 88 +++++ ref/name/current.go | 5 + ref/name/disposition.go | 20 ++ ref/name/doc.go | 2 + ref/name/errors.go | 14 + ref/name/flags.go | 6 + ref/name/length.go | 11 + ref/name/lock.go | 3 + ref/name/normalize.go | 53 +++ ref/name/options.go | 30 ++ ref/name/pseudo.go | 11 + ref/name/refname_test.go | 622 ++++++++++++++++++++++++++++++++++ ref/name/root.go | 21 ++ ref/name/root_syntax.go | 13 + ref/name/safe.go | 31 ++ ref/name/sanitize.go | 19 ++ ref/name/slashes.go | 26 ++ ref/name/tag.go | 20 ++ ref/name/update.go | 56 +++ ref/name/utils.go | 22 ++ ref/name/validate.go | 65 ++++ ref/name/worktree.go | 75 ++++ ref/refname/branch.go | 25 -- ref/refname/component.go | 88 ----- ref/refname/current.go | 5 - ref/refname/disposition.go | 20 -- ref/refname/doc.go | 2 - ref/refname/errors.go | 14 - ref/refname/flags.go | 6 - ref/refname/length.go | 11 - ref/refname/lock.go | 3 - ref/refname/normalize.go | 53 --- ref/refname/options.go | 30 -- ref/refname/pseudo.go | 11 - ref/refname/refname_test.go | 622 ---------------------------------- ref/refname/root.go | 21 -- ref/refname/root_syntax.go | 13 - ref/refname/safe.go | 31 -- ref/refname/sanitize.go | 19 -- ref/refname/slashes.go | 26 -- ref/refname/tag.go | 20 -- ref/refname/update.go | 56 --- ref/refname/utils.go | 22 -- ref/refname/validate.go | 65 ---- ref/refname/worktree.go | 75 ---- ref/store/files/root_loose_path.go | 2 +- ref/store/files/update_direct_read.go | 2 +- ref/store/files/update_validate.go | 2 +- 49 files changed, 1241 insertions(+), 1241 deletions(-) create mode 100644 ref/name/branch.go create mode 100644 ref/name/component.go create mode 100644 ref/name/current.go create mode 100644 ref/name/disposition.go create mode 100644 ref/name/doc.go create mode 100644 ref/name/errors.go create mode 100644 ref/name/flags.go create mode 100644 ref/name/length.go create mode 100644 ref/name/lock.go create mode 100644 ref/name/normalize.go create mode 100644 ref/name/options.go create mode 100644 ref/name/pseudo.go create mode 100644 ref/name/refname_test.go create mode 100644 ref/name/root.go create mode 100644 ref/name/root_syntax.go create mode 100644 ref/name/safe.go create mode 100644 ref/name/sanitize.go create mode 100644 ref/name/slashes.go create mode 100644 ref/name/tag.go create mode 100644 ref/name/update.go create mode 100644 ref/name/utils.go create mode 100644 ref/name/validate.go create mode 100644 ref/name/worktree.go delete mode 100644 ref/refname/branch.go delete mode 100644 ref/refname/component.go delete mode 100644 ref/refname/current.go delete mode 100644 ref/refname/disposition.go delete mode 100644 ref/refname/doc.go delete mode 100644 ref/refname/errors.go delete mode 100644 ref/refname/flags.go delete mode 100644 ref/refname/length.go delete mode 100644 ref/refname/lock.go delete mode 100644 ref/refname/normalize.go delete mode 100644 ref/refname/options.go delete mode 100644 ref/refname/pseudo.go delete mode 100644 ref/refname/refname_test.go delete mode 100644 ref/refname/root.go delete mode 100644 ref/refname/root_syntax.go delete mode 100644 ref/refname/safe.go delete mode 100644 ref/refname/sanitize.go delete mode 100644 ref/refname/slashes.go delete mode 100644 ref/refname/tag.go delete mode 100644 ref/refname/update.go delete mode 100644 ref/refname/utils.go delete mode 100644 ref/refname/validate.go delete mode 100644 ref/refname/worktree.go diff --git a/ref/name/branch.go b/ref/name/branch.go new file mode 100644 index 00000000..274a95e3 --- /dev/null +++ b/ref/name/branch.go @@ -0,0 +1,25 @@ +package refname + +import "strings" + +// Branch checks one branch shorthand and returns its fully-qualified +// refs/heads/... name. +// +// Unlike Git in-repository branch parsing, this helper does not expand @{-n}. +func Branch(name string) (string, error) { + full := "refs/heads/" + name + if strings.HasPrefix(name, "-") || full == "refs/heads/HEAD" { + return "", &NameError{Name: name, Reason: "invalid branch name"} + } + + err := validate(full, 0) + if err != nil { + return "", err + } + + if strings.HasPrefix(name, "refs/") { + return name, nil + } + + return full, nil +} diff --git a/ref/name/component.go b/ref/name/component.go new file mode 100644 index 00000000..f5adba46 --- /dev/null +++ b/ref/name/component.go @@ -0,0 +1,88 @@ +package refname + +import "strings" + +func checkRefnameComponent(name string, flags *int, sanitized *strings.Builder, fullName string) (int, error) { + var last byte + + componentStart := sanitizedLen(sanitized) + + for i := range len(name) { + ch := name[i] + disp := refnameDisposition(ch) + + if sanitized != nil && disp != 1 { + sanitized.WriteByte(ch) + } + + switch disp { + case 1: + goto out + case 2: + if last == '.' { + if sanitized != nil { + truncateBuilder(sanitized, sanitized.Len()-1) + } else { + return 0, &NameError{Name: fullName, Reason: "name contains '..'"} + } + } + case 3: + if last == '@' { + if sanitized != nil { + overwriteLastByte(sanitized, '-') + } else { + return 0, &NameError{Name: fullName, Reason: "name contains '@{'"} + } + } + case 4: + if sanitized != nil { + overwriteLastByte(sanitized, '-') + } else { + return 0, &NameError{Name: fullName, Reason: "name contains one forbidden character"} + } + case 5: + if *flags&refnameRefspecPattern == 0 { + if sanitized != nil { + overwriteLastByte(sanitized, '-') + } else { + return 0, &NameError{Name: fullName, Reason: "name contains '*'"} + } + } + + *flags &^= refnameRefspecPattern + } + + last = ch + } + +out: + componentLen := strings.IndexByte(name, '/') + + if componentLen < 0 { + componentLen = len(name) + } + + if componentLen == 0 { + return 0, nil + } + + if name[0] == '.' { + if sanitized != nil { + overwriteBuilderAt(sanitized, componentStart, '-') + } else { + return 0, &NameError{Name: fullName, Reason: "component starts with '.'"} + } + } + + if componentLen >= len(lockSuffix) && name[componentLen-len(lockSuffix):componentLen] == lockSuffix { + if sanitized == nil { + return 0, &NameError{Name: fullName, Reason: "component ends with .lock"} + } + + for strings.HasSuffix(sanitized.String(), lockSuffix) { + truncateBuilder(sanitized, sanitized.Len()-len(lockSuffix)) + } + } + + return componentLen, nil +} diff --git a/ref/name/current.go b/ref/name/current.go new file mode 100644 index 00000000..3a5394cc --- /dev/null +++ b/ref/name/current.go @@ -0,0 +1,5 @@ +package refname + +func isCurrentWorktreeRef(name string) bool { + return IsRootSyntax(name) || IsPerWorktree(name) +} diff --git a/ref/name/disposition.go b/ref/name/disposition.go new file mode 100644 index 00000000..5153e633 --- /dev/null +++ b/ref/name/disposition.go @@ -0,0 +1,20 @@ +package refname + +func refnameDisposition(ch byte) byte { + switch { + case ch == '/': + return 1 + case ch == '.': + return 2 + case ch == '{': + return 3 + case ch == '*': + return 5 + case ch < 0x20 || ch == 0x7f: + return 4 + case ch == ':' || ch == '?' || ch == '[' || ch == '\\' || ch == '^' || ch == '~' || ch == ' ' || ch == '\t': + return 4 + default: + return 0 + } +} diff --git a/ref/name/doc.go b/ref/name/doc.go new file mode 100644 index 00000000..363733f3 --- /dev/null +++ b/ref/name/doc.go @@ -0,0 +1,2 @@ +// Package refname provides various routines to check and normalize reference names. +package refname diff --git a/ref/name/errors.go b/ref/name/errors.go new file mode 100644 index 00000000..e39bc73b --- /dev/null +++ b/ref/name/errors.go @@ -0,0 +1,14 @@ +package refname + +import "fmt" + +// NameError reports one invalid reference name. +type NameError struct { + Name string + Reason string +} + +// Error implements error. +func (err *NameError) Error() string { + return fmt.Sprintf("ref: invalid name %q: %s", err.Name, err.Reason) +} diff --git a/ref/name/flags.go b/ref/name/flags.go new file mode 100644 index 00000000..72f0a58f --- /dev/null +++ b/ref/name/flags.go @@ -0,0 +1,6 @@ +package refname + +const ( + refnameAllowOneLevel = 1 << iota + refnameRefspecPattern +) diff --git a/ref/name/length.go b/ref/name/length.go new file mode 100644 index 00000000..94c0322b --- /dev/null +++ b/ref/name/length.go @@ -0,0 +1,11 @@ +package refname + +import "strings" + +func sanitizedLen(builder *strings.Builder) int { + if builder == nil { + return 0 + } + + return builder.Len() +} diff --git a/ref/name/lock.go b/ref/name/lock.go new file mode 100644 index 00000000..33db902f --- /dev/null +++ b/ref/name/lock.go @@ -0,0 +1,3 @@ +package refname + +const lockSuffix = ".lock" diff --git a/ref/name/normalize.go b/ref/name/normalize.go new file mode 100644 index 00000000..9cbe7126 --- /dev/null +++ b/ref/name/normalize.go @@ -0,0 +1,53 @@ +package refname + +import "strings" + +// Normalize collapses slashes according to what Git wants +// then validates the normalized name. +func Normalize(name string, options Options) (string, error) { + normalized := collapseSlashes(name) + + err := validate(normalized, options.flags()) + if err != nil { + return "", err + } + + return normalized, nil +} + +func normalizeRefPath(path string) (string, bool) { + components := make([]string, 0, strings.Count(path, "/")+1) + i := 0 + + for i < len(path) { + for i < len(path) && path[i] == '/' { + i++ + } + + if i == len(path) { + break + } + + j := i + for j < len(path) && path[j] != '/' { + j++ + } + + component := path[i:j] + switch component { + case ".": + case "..": + if len(components) == 0 { + return "", false + } + + components = components[:len(components)-1] + default: + components = append(components, component) + } + + i = j + } + + return strings.Join(components, "/"), true +} diff --git a/ref/name/options.go b/ref/name/options.go new file mode 100644 index 00000000..5ae81541 --- /dev/null +++ b/ref/name/options.go @@ -0,0 +1,30 @@ +package refname + +import "fmt" + +// Options controls Git refname validation. +type Options struct { + // AllowOneLevel permits one-component refnames like HEAD. + AllowOneLevel bool + + // RefspecPattern permits one '*' anywhere in the refname. + RefspecPattern bool +} + +// String returns one stable text form of the options. +func (options Options) String() string { + return fmt.Sprintf("allow_onelevel=%t,refspec_pattern=%t", options.AllowOneLevel, options.RefspecPattern) +} + +func (options Options) flags() int { + var flags int + if options.AllowOneLevel { + flags |= refnameAllowOneLevel + } + + if options.RefspecPattern { + flags |= refnameRefspecPattern + } + + return flags +} diff --git a/ref/name/pseudo.go b/ref/name/pseudo.go new file mode 100644 index 00000000..f0ad1ae8 --- /dev/null +++ b/ref/name/pseudo.go @@ -0,0 +1,11 @@ +package refname + +// IsPseudo reports whether name is one Git pseudo-ref. +func IsPseudo(name string) bool { + switch name { + case "FETCH_HEAD", "MERGE_HEAD": + return true + default: + return false + } +} diff --git a/ref/name/refname_test.go b/ref/name/refname_test.go new file mode 100644 index 00000000..a37314f4 --- /dev/null +++ b/ref/name/refname_test.go @@ -0,0 +1,622 @@ +package refname_test + +import ( + "context" + "os/exec" + "strings" + "testing" + + "codeberg.org/lindenii/furgit/ref/name" +) + +func TestValidateNameAgainstGit(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + opts refname.Options + } + + tests := []testCase{ + {name: ""}, + {name: "/"}, + {name: "/", opts: refname.Options{AllowOneLevel: true}}, + {name: "foo/bar/baz"}, + {name: "refs/heads/main"}, + {name: "refs/tags/v1.0.0"}, + {name: "refs///heads/foo"}, + {name: "heads/foo/"}, + {name: "/heads/foo"}, + {name: "///heads/foo"}, + {name: "./foo"}, + {name: "./foo/bar"}, + {name: "foo/./bar"}, + {name: "foo/bar/."}, + {name: ".refs/foo"}, + {name: "refs/heads/foo."}, + {name: "HEAD"}, + {name: "HEAD", opts: refname.Options{AllowOneLevel: true}}, + {name: "refs/heads/.main"}, + {name: "heads/foo..bar"}, + {name: "refs/heads/main.lock"}, + {name: "heads///foo.lock"}, + {name: "refs/heads/foo..bar"}, + {name: "refs/heads/foo bar"}, + {name: "refs/heads/foo@{bar"}, + {name: "heads/foo?bar"}, + {name: "foo./bar"}, + {name: "foo.lock/bar"}, + {name: "foo.lock///bar"}, + {name: "heads/foo@bar"}, + {name: "heads/foo\\bar"}, + {name: "heads/foo\tbar"}, + {name: "heads/foo\x7fbar"}, + {name: "heads/fu\xC3\x9F"}, + {name: "heads/*foo/bar", opts: refname.Options{RefspecPattern: true}}, + {name: "heads/foo*/bar", opts: refname.Options{RefspecPattern: true}}, + {name: "heads/f*o/bar", opts: refname.Options{RefspecPattern: true}}, + {name: "heads/f*o*/bar", opts: refname.Options{RefspecPattern: true}}, + {name: "heads/foo*/bar*", opts: refname.Options{RefspecPattern: true}}, + {name: "refs/heads/foo/bar."}, + {name: "refs//heads///main"}, + {name: "foo"}, + {name: "foo", opts: refname.Options{AllowOneLevel: true}}, + {name: "foo", opts: refname.Options{RefspecPattern: true}}, + {name: "foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + {name: "foo/bar"}, + {name: "foo/bar", opts: refname.Options{AllowOneLevel: true}}, + {name: "foo/bar", opts: refname.Options{RefspecPattern: true}}, + {name: "foo/bar", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + {name: "refs/heads/*"}, + {name: "refs/heads/*", opts: refname.Options{RefspecPattern: true}}, + {name: "refs/heads/feature*branch", opts: refname.Options{RefspecPattern: true}}, + {name: "refs/heads/foo*bar*baz", opts: refname.Options{RefspecPattern: true}}, + {name: "foo/*"}, + {name: "foo/*", opts: refname.Options{RefspecPattern: true}}, + {name: "foo/*", opts: refname.Options{AllowOneLevel: true}}, + {name: "foo/*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + {name: "*/foo"}, + {name: "*/foo", opts: refname.Options{RefspecPattern: true}}, + {name: "*/foo", opts: refname.Options{AllowOneLevel: true}}, + {name: "*/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + {name: "foo/*/bar"}, + {name: "foo/*/bar", opts: refname.Options{RefspecPattern: true}}, + {name: "foo/*/bar", opts: refname.Options{AllowOneLevel: true}}, + {name: "foo/*/bar", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + {name: "*"}, + {name: "*", opts: refname.Options{AllowOneLevel: true}}, + {name: "*", opts: refname.Options{RefspecPattern: true}}, + {name: "*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + {name: "foo/*/*", opts: refname.Options{RefspecPattern: true}}, + {name: "foo/*/*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + {name: "*/foo/*", opts: refname.Options{RefspecPattern: true}}, + {name: "*/foo/*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + {name: "*/*/foo", opts: refname.Options{RefspecPattern: true}}, + {name: "*/*/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + {name: "/foo"}, + {name: "/foo", opts: refname.Options{AllowOneLevel: true}}, + {name: "/foo", opts: refname.Options{RefspecPattern: true}}, + {name: "/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + {name: "@"}, + } + + for _, tt := range tests { + t.Run(tt.name+"_"+tt.opts.String(), func(t *testing.T) { + t.Parallel() + + err := refname.Validate(tt.name, tt.opts) + gitErr := gitCheckRefFormat(t, tt.name, tt.opts) + + if (err == nil) != (gitErr == nil) { + t.Fatalf("ValidateName(%q, %+v) err=%v, git err=%v", tt.name, tt.opts, err, gitErr) + } + }) + } +} + +func TestNormalizeNameAgainstGit(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + opts refname.Options + } + + tests := []testCase{ + {name: "/"}, + {name: "/", opts: refname.Options{AllowOneLevel: true}}, + {name: "///refs///heads//main"}, + {name: "refs////tags///v1"}, + {name: "refs///heads///"}, + {name: "HEAD", opts: refname.Options{AllowOneLevel: true}}, + {name: "refs/heads/*", opts: refname.Options{RefspecPattern: true}}, + {name: "refs///heads/foo"}, + {name: "/heads/foo", opts: refname.Options{AllowOneLevel: true}}, + {name: "///heads/foo"}, + {name: "heads/foo/../bar"}, + {name: "heads/./foo"}, + {name: "heads\\foo"}, + {name: "heads/foo.lock"}, + {name: "heads///foo.lock"}, + {name: "foo.lock/bar"}, + {name: "foo.lock///bar"}, + {name: "foo"}, + {name: "/foo", opts: refname.Options{AllowOneLevel: true}}, + {name: "/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, + } + + for _, tt := range tests { + t.Run(tt.name+"_"+tt.opts.String(), func(t *testing.T) { + t.Parallel() + + got, err := refname.Normalize(tt.name, tt.opts) + want, gitErr := gitNormalizeRefFormat(t, tt.name, tt.opts) + + if (err == nil) != (gitErr == nil) { + t.Fatalf("NormalizeName(%q, %+v) err=%v, git err=%v", tt.name, tt.opts, err, gitErr) + } + + if err == nil && got != want { + t.Fatalf("NormalizeName(%q, %+v) = %q, want %q", tt.name, tt.opts, got, want) + } + }) + } +} + +func TestBranchNameAgainstGit(t *testing.T) { + t.Parallel() + + tests := []string{ + "main", + "feature/topic", + "-main", + "HEAD", + "@{-1}", + "feature.lock", + "topic@{1}", + "refs/heads/main", + "refs/heads/HEAD", + "refs/tags/x", + } + + for _, name := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := refname.Branch(name) + want, gitErr := gitCheckBranchName(t, name) + + if (err == nil) != (gitErr == nil) { + t.Fatalf("BranchName(%q) err=%v, git err=%v", name, err, gitErr) + } + + if err == nil && got != want { + t.Fatalf("BranchName(%q) = %q, want %q", name, got, want) + } + }) + } +} + +func TestTagName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + }{ + {name: "v1.0.0"}, + {name: "main/topic"}, + {name: "-bad"}, + {name: "HEAD"}, + {name: "feature.lock"}, + {name: "refs/tags/v1.0.0"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := refname.Tag(tt.name) + want, gitErr := gitCheckTagName(t, tt.name) + + if (err == nil) != (gitErr == nil) { + t.Fatalf("TagName(%q) err=%v, git err=%v", tt.name, err, gitErr) + } + + if err == nil && got != want { + t.Fatalf("TagName(%q) = %q, want %q", tt.name, got, want) + } + }) + } +} + +func TestIsSafeName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want bool + }{ + {name: "", want: false}, + {name: "HEAD", want: true}, + {name: "MERGE_HEAD", want: true}, + {name: "Head", want: false}, + {name: "refs/heads/main", want: true}, + {name: "refs/", want: false}, + {name: "refs//heads/main", want: false}, + {name: "refs/heads/main/", want: false}, + {name: "refs/foo/../bar", want: false}, + {name: "refs/foo/../../bar", want: false}, + {name: "refs/heads/main.lock", want: true}, + } + + for _, tt := range tests { + if got := refname.IsSafe(tt.name); got != tt.want { + t.Fatalf("IsSafeName(%q) = %v, want %v", tt.name, got, tt.want) + } + } +} + +func TestIsPerWorktree(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want bool + }{ + {name: "refs/worktree/foo", want: true}, + {name: "refs/bisect/foo", want: true}, + {name: "refs/rewritten/foo", want: true}, + {name: "refs/heads/foo", want: false}, + {name: "worktrees/wt1/HEAD", want: false}, + } + + for _, tt := range tests { + if got := refname.IsPerWorktree(tt.name); got != tt.want { + t.Fatalf("IsPerWorktree(%q) = %v, want %v", tt.name, got, tt.want) + } + } +} + +func TestIsPseudo(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want bool + }{ + {name: "FETCH_HEAD", want: true}, + {name: "MERGE_HEAD", want: true}, + {name: "HEAD", want: false}, + {name: "AUTO_MERGE", want: false}, + } + + for _, tt := range tests { + if got := refname.IsPseudo(tt.name); got != tt.want { + t.Fatalf("IsPseudo(%q) = %v, want %v", tt.name, got, tt.want) + } + } +} + +func TestIsRootSyntax(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want bool + }{ + {name: "", want: true}, + {name: "HEAD", want: true}, + {name: "AUTO_MERGE", want: true}, + {name: "BISECT-EXPECTED_REV", want: true}, + {name: "refs/heads/main", want: false}, + {name: "Head", want: false}, + {name: "HEAD1", want: false}, + } + + for _, tt := range tests { + if got := refname.IsRootSyntax(tt.name); got != tt.want { + t.Fatalf("IsRootSyntax(%q) = %v, want %v", tt.name, got, tt.want) + } + } +} + +func TestIsRoot(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want bool + }{ + {name: "HEAD", want: true}, + {name: "ORIG_HEAD", want: true}, + {name: "BOGUS_HEAD", want: true}, + {name: "CHERRY_PICK_HEAD", want: true}, + {name: "REVERT_HEAD", want: true}, + {name: "AUTO_MERGE", want: true}, + {name: "BISECT_EXPECTED_REV", want: true}, + {name: "NOTES_MERGE_PARTIAL", want: true}, + {name: "NOTES_MERGE_REF", want: true}, + {name: "MERGE_AUTOSTASH", want: true}, + {name: "FETCH_HEAD", want: false}, + {name: "MERGE_HEAD", want: false}, + {name: "Head", want: false}, + {name: "refs/heads/main", want: false}, + } + + for _, tt := range tests { + if got := refname.IsRoot(tt.name); got != tt.want { + t.Fatalf("IsRoot(%q) = %v, want %v", tt.name, got, tt.want) + } + } +} + +func TestParseWorktree(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want refname.ParsedWorktreeRef + }{ + { + name: "refs/heads/main", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeShared, + BareRefName: "refs/heads/main", + }, + }, + { + name: "HEAD", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeCurrent, + BareRefName: "HEAD", + }, + }, + { + name: "refs/worktree/foo", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeCurrent, + BareRefName: "refs/worktree/foo", + }, + }, + { + name: "main-worktree/HEAD", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeMain, + BareRefName: "HEAD", + }, + }, + { + name: "main-worktree/FOO", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeMain, + BareRefName: "FOO", + }, + }, + { + name: "main-worktree/refs/worktree/foo", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeMain, + BareRefName: "refs/worktree/foo", + }, + }, + { + name: "main-worktree/", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeMain, + BareRefName: "", + }, + }, + { + name: "main-worktree/refs/heads/main", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeShared, + BareRefName: "main-worktree/refs/heads/main", + }, + }, + { + name: "worktrees/wt1/HEAD", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeOther, + WorktreeName: "wt1", + BareRefName: "HEAD", + }, + }, + { + name: "worktrees/wt1/BAR", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeOther, + WorktreeName: "wt1", + BareRefName: "BAR", + }, + }, + { + name: "worktrees/wt1/refs/bisect/foo", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeOther, + WorktreeName: "wt1", + BareRefName: "refs/bisect/foo", + }, + }, + { + name: "worktrees/wt1/refs/heads/main", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeShared, + BareRefName: "worktrees/wt1/refs/heads/main", + }, + }, + { + name: "worktrees/wt1", + want: refname.ParsedWorktreeRef{ + Type: refname.WorktreeOther, + WorktreeName: "wt1", + BareRefName: "", + }, + }, + } + + for _, tt := range tests { + if got := refname.ParseWorktree(tt.name); got != tt.want { + t.Fatalf("ParseWorktree(%q) = %#v, want %#v", tt.name, got, tt.want) + } + } +} + +func TestValidateUpdateName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + hasNewValue bool + wantErr bool + }{ + {name: "refs/heads/main", hasNewValue: true, wantErr: false}, + {name: "HEAD", hasNewValue: true, wantErr: false}, + {name: "PSEUDOREF", hasNewValue: true, wantErr: false}, + {name: "FETCH_HEAD", hasNewValue: true, wantErr: true}, + {name: "MERGE_HEAD", hasNewValue: true, wantErr: true}, + {name: "refs/heads/.bad", hasNewValue: true, wantErr: true}, + {name: "foo/bar", hasNewValue: true, wantErr: false}, + {name: "foo/bar", hasNewValue: false, wantErr: true}, + {name: "PSEUDOREF", hasNewValue: false, wantErr: false}, + {name: "HEAD", hasNewValue: false, wantErr: false}, + } + + for _, tt := range tests { + err := refname.ValidateUpdateName(tt.name, tt.hasNewValue) + if (err != nil) != tt.wantErr { + t.Fatalf("ValidateUpdateName(%q, %v) err=%v, wantErr=%v", tt.name, tt.hasNewValue, err, tt.wantErr) + } + } +} + +func TestValidateSymbolicTarget(t *testing.T) { + t.Parallel() + + tests := []struct { + ref string + target string + wantErr bool + }{ + {ref: "HEAD", target: "refs/heads/main", wantErr: false}, + {ref: "HEAD", target: "foo", wantErr: true}, + {ref: "HEAD", target: "ORIG_HEAD", wantErr: true}, + {ref: "refs/heads/top", target: "ORIG_HEAD", wantErr: false}, + {ref: "refs/heads/top", target: "refs/heads/main", wantErr: false}, + {ref: "refs/heads/top", target: "worktrees/wt1/HEAD", wantErr: false}, + {ref: "refs/heads/top", target: "foo", wantErr: true}, + {ref: "refs/heads/top", target: "foo..bar", wantErr: true}, + {ref: "main-worktree/HEAD", target: "refs/heads/main", wantErr: false}, + {ref: "main-worktree/HEAD", target: "refs/tags/v1", wantErr: true}, + } + + for _, tt := range tests { + err := refname.ValidateSymbolicTarget(tt.ref, tt.target) + if (err != nil) != tt.wantErr { + t.Fatalf("ValidateSymbolicTarget(%q, %q) err=%v, wantErr=%v", tt.ref, tt.target, err, tt.wantErr) + } + } +} + +func TestSanitizeComponent(t *testing.T) { + t.Parallel() + + tests := []struct { + component string + want string + }{ + {component: ".", want: "-"}, + {component: "..", want: "-"}, + {component: "foo..bar", want: "foo.bar"}, + {component: "foo.lock", want: "foo"}, + {component: "foo.lock.lock", want: "foo"}, + {component: "foo bar", want: "foo-bar"}, + {component: "@", want: "-/@"}, + {component: "a@{b", want: "a@-b"}, + {component: "a*b", want: "a-b"}, + } + + for _, tt := range tests { + if got := refname.SanitizeComponent(tt.component); got != tt.want { + t.Fatalf("SanitizeComponent(%q) = %q, want %q", tt.component, got, tt.want) + } + } +} + +func gitCheckRefFormat(tb testing.TB, name string, opts refname.Options) error { + tb.Helper() + + args := []string{"check-ref-format"} + if opts.AllowOneLevel { + args = append(args, "--allow-onelevel") + } + + if opts.RefspecPattern { + args = append(args, "--refspec-pattern") + } + + args = append(args, name) + + return exec.CommandContext(context.Background(), "git", args...).Run() +} + +func gitNormalizeRefFormat(tb testing.TB, name string, opts refname.Options) (string, error) { + tb.Helper() + + args := []string{"check-ref-format", "--normalize"} + if opts.AllowOneLevel { + args = append(args, "--allow-onelevel") + } + + if opts.RefspecPattern { + args = append(args, "--refspec-pattern") + } + + args = append(args, name) + + out, err := exec.CommandContext(context.Background(), "git", args...).Output() + if err != nil { + return "", err + } + + return strings.TrimSuffix(string(out), "\n"), nil +} + +func gitCheckBranchName(tb testing.TB, name string) (string, error) { + tb.Helper() + + cmd := exec.CommandContext(context.Background(), "git", "check-ref-format", "--branch", name) + cmd.Dir = tb.TempDir() + + out, err := cmd.Output() + if err != nil { + return "", err + } + + branchName := strings.TrimSuffix(string(out), "\n") + if strings.HasPrefix(branchName, "refs/") { + return branchName, nil + } + + return "refs/heads/" + branchName, nil +} + +func gitCheckTagName(tb testing.TB, name string) (string, error) { + tb.Helper() + + if strings.HasPrefix(name, "-") || name == "HEAD" { + return "", exec.ErrNotFound + } + + //nolint:gosec + err := exec.CommandContext( + context.Background(), + "git", + "check-ref-format", + "refs/tags/"+name, + ).Run() + if err != nil { + return "", err + } + + return "refs/tags/" + name, nil +} diff --git a/ref/name/root.go b/ref/name/root.go new file mode 100644 index 00000000..43361846 --- /dev/null +++ b/ref/name/root.go @@ -0,0 +1,21 @@ +package refname + +import "strings" + +// IsRoot reports whether name is one root ref according to Git. +func IsRoot(name string) bool { + if !IsRootSyntax(name) || IsPseudo(name) { + return false + } + + if strings.HasSuffix(name, "_HEAD") { + return true + } + + switch name { + case "HEAD", "AUTO_MERGE", "BISECT_EXPECTED_REV", "NOTES_MERGE_PARTIAL", "NOTES_MERGE_REF", "MERGE_AUTOSTASH": + return true + default: + return false + } +} diff --git a/ref/name/root_syntax.go b/ref/name/root_syntax.go new file mode 100644 index 00000000..97a15cb9 --- /dev/null +++ b/ref/name/root_syntax.go @@ -0,0 +1,13 @@ +package refname + +// IsRootSyntax reports whether name matches Git's all-caps root-ref syntax. +func IsRootSyntax(name string) bool { + for i := range len(name) { + ch := name[i] + if (ch < 'A' || ch > 'Z') && ch != '-' && ch != '_' { + return false + } + } + + return true +} diff --git a/ref/name/safe.go b/ref/name/safe.go new file mode 100644 index 00000000..b36d3b2f --- /dev/null +++ b/ref/name/safe.go @@ -0,0 +1,31 @@ +package refname + +import "strings" + +// IsSafe reports whether name is one safe refname for direct filesystem +// operations; see refname_is_safe. +func IsSafe(name string) bool { + rest, ok := strings.CutPrefix(name, "refs/") + if ok { + if rest == "" || rest[0] == '/' || rest[len(rest)-1] == '/' { + return false + } + + normalized, normOK := normalizeRefPath(rest) + + return normOK && normalized == rest + } + + if name == "" { + return false + } + + for i := range len(name) { + ch := name[i] + if (ch < 'A' || ch > 'Z') && ch != '_' { + return false + } + } + + return true +} diff --git a/ref/name/sanitize.go b/ref/name/sanitize.go new file mode 100644 index 00000000..f543de7c --- /dev/null +++ b/ref/name/sanitize.go @@ -0,0 +1,19 @@ +package refname + +import ( + "fmt" + "strings" +) + +// SanitizeComponent mutates component until it satisfies +// sanitize_refname_component. +func SanitizeComponent(component string) string { + var builder strings.Builder + + err := checkOrSanitizeRefname(component, refnameAllowOneLevel, &builder) + if err != nil { + panic(fmt.Sprintf("ref: sanitize component %q: %v", component, err)) + } + + return builder.String() +} diff --git a/ref/name/slashes.go b/ref/name/slashes.go new file mode 100644 index 00000000..44d3e4ea --- /dev/null +++ b/ref/name/slashes.go @@ -0,0 +1,26 @@ +package refname + +import "strings" + +func collapseSlashes(name string) string { + if name == "" { + return "" + } + + var builder strings.Builder + builder.Grow(len(name)) + + prev := byte('/') + + for i := range len(name) { + ch := name[i] + if prev == '/' && ch == '/' { + continue + } + + builder.WriteByte(ch) + prev = ch + } + + return builder.String() +} diff --git a/ref/name/tag.go b/ref/name/tag.go new file mode 100644 index 00000000..226c0fdd --- /dev/null +++ b/ref/name/tag.go @@ -0,0 +1,20 @@ +package refname + +import "strings" + +// Tag checks one tag shorthand and returns its fully-qualified +// refs/tags/... name. +func Tag(name string) (string, error) { + if strings.HasPrefix(name, "-") || name == "HEAD" { + return "", &NameError{Name: name, Reason: "invalid tag name"} + } + + full := "refs/tags/" + name + + err := validate(full, 0) + if err != nil { + return "", err + } + + return full, nil +} diff --git a/ref/name/update.go b/ref/name/update.go new file mode 100644 index 00000000..92830f1a --- /dev/null +++ b/ref/name/update.go @@ -0,0 +1,56 @@ +package refname + +import "strings" + +// ValidateUpdateName checks whether name is valid for one direct ref update. +// +// See transaction_refname_valid(); +// updates with a new OID use check_refname_format(..., ALLOW_ONELEVEL), +// while delete/verify style operations use refname_is_safe(). +func ValidateUpdateName(name string, hasNewValue bool) error { + if IsPseudo(name) { + return &NameError{Name: name, Reason: "pseudoref updates are not allowed"} + } + + if hasNewValue { + return Validate(name, Options{AllowOneLevel: true}) + } + + if !IsSafe(name) { + return &NameError{Name: name, Reason: "unsafe refname for update"} + } + + return nil +} + +// ValidateSymbolicTarget checks whether target is valid for one symref target. +// +// See refs_fsck_symref(); +// root refs are allowed directly, HEAD must point to refs/heads/..., +// and non-root targets must be valid full refnames rooted at refs/ or +// worktrees/. +func ValidateSymbolicTarget(refname string, target string) error { + parsed := ParseWorktree(refname) + if parsed.BareRefName == "HEAD" && !strings.HasPrefix(target, "refs/heads/") { + return &NameError{Name: target, Reason: refname + " must point to refs/heads/..."} + } + + if IsRoot(target) { + return nil + } + + err := Validate(target, Options{}) + if err != nil { + return err + } + + if strings.HasPrefix(target, "refs/") { + return nil + } + + if strings.HasPrefix(target, "worktrees/") { + return nil + } + + return &NameError{Name: target, Reason: "symref target is not a ref"} +} diff --git a/ref/name/utils.go b/ref/name/utils.go new file mode 100644 index 00000000..58944748 --- /dev/null +++ b/ref/name/utils.go @@ -0,0 +1,22 @@ +package refname + +import ( + "strings" +) + +func overwriteLastByte(builder *strings.Builder, ch byte) { + overwriteBuilderAt(builder, builder.Len()-1, ch) +} + +func overwriteBuilderAt(builder *strings.Builder, index int, ch byte) { + value := builder.String() + truncateBuilder(builder, index) + builder.WriteByte(ch) + builder.WriteString(value[index+1:]) +} + +func truncateBuilder(builder *strings.Builder, n int) { + value := builder.String() + builder.Reset() + builder.WriteString(value[:n]) +} diff --git a/ref/name/validate.go b/ref/name/validate.go new file mode 100644 index 00000000..1b8ad396 --- /dev/null +++ b/ref/name/validate.go @@ -0,0 +1,65 @@ +package refname + +import "strings" + +// Validate checks whether name is one valid Git refname. +func Validate(name string, options Options) error { + return validate(name, options.flags()) +} + +func validate(name string, flags int) error { + return checkOrSanitizeRefname(name, flags, nil) +} + +func checkOrSanitizeRefname(name string, flags int, sanitized *strings.Builder) error { + componentCount := 0 + remaining := name + + if name == "@" { + if sanitized == nil { + return &NameError{Name: name, Reason: "single @ is not allowed"} + } + + sanitized.WriteByte('-') + } + + for { + if sanitized != nil && sanitized.Len() > 0 { + sanitized.WriteByte('/') + } + + componentLen, err := checkRefnameComponent(remaining, &flags, sanitized, name) + switch { + case sanitized != nil && componentLen == 0: + case componentLen <= 0: + if err != nil { + return err + } + + return &NameError{Name: name, Reason: "component has zero length"} + case err != nil: + return err + } + + componentCount++ + + if componentLen == len(remaining) { + break + } + + remaining = remaining[componentLen+1:] + } + + componentLen := len(remaining) + if componentLen > 0 && remaining[componentLen-1] == '.' { + if sanitized == nil { + return &NameError{Name: name, Reason: "name ends with '.'"} + } + } + + if flags&refnameAllowOneLevel == 0 && componentCount < 2 { + return &NameError{Name: name, Reason: "one-level refname is not allowed"} + } + + return nil +} diff --git a/ref/name/worktree.go b/ref/name/worktree.go new file mode 100644 index 00000000..48ca215d --- /dev/null +++ b/ref/name/worktree.go @@ -0,0 +1,75 @@ +package refname + +import "strings" + +// WorktreeType classifies one worktree-qualified refname prefix. +type WorktreeType uint8 + +const ( + // WorktreeShared is one ordinary shared refname. + WorktreeShared WorktreeType = iota + + // WorktreeCurrent is one current-worktree-only refname like HEAD or refs/worktree/... + WorktreeCurrent + + // WorktreeMain is one main-worktree-qualified refname like main-worktree/HEAD. + WorktreeMain + + // WorktreeOther is one other-worktree-qualified refname like worktrees/wt1/HEAD. + WorktreeOther +) + +// IsPerWorktree reports whether name is one per-worktree ref namespace. +func IsPerWorktree(name string) bool { + return strings.HasPrefix(name, "refs/worktree/") || + strings.HasPrefix(name, "refs/bisect/") || + strings.HasPrefix(name, "refs/rewritten/") +} + +// ParsedWorktreeRef is the result of parsing one worktree-qualified refname. +type ParsedWorktreeRef struct { + Type WorktreeType + WorktreeName string + BareRefName string +} + +// ParseWorktree parses Git's worktree ref prefixes. +func ParseWorktree(name string) ParsedWorktreeRef { + if bare, ok := strings.CutPrefix(name, "worktrees/"); ok { + worktreeName, rest, found := strings.Cut(bare, "/") + if !found { + return ParsedWorktreeRef{ + Type: WorktreeOther, + WorktreeName: worktreeName, + BareRefName: "", + } + } + + if isCurrentWorktreeRef(rest) { + return ParsedWorktreeRef{ + Type: WorktreeOther, + WorktreeName: worktreeName, + BareRefName: rest, + } + } + } + + if bare, ok := strings.CutPrefix(name, "main-worktree/"); ok && isCurrentWorktreeRef(bare) { + return ParsedWorktreeRef{ + Type: WorktreeMain, + BareRefName: bare, + } + } + + if isCurrentWorktreeRef(name) { + return ParsedWorktreeRef{ + Type: WorktreeCurrent, + BareRefName: name, + } + } + + return ParsedWorktreeRef{ + Type: WorktreeShared, + BareRefName: name, + } +} diff --git a/ref/refname/branch.go b/ref/refname/branch.go deleted file mode 100644 index 274a95e3..00000000 --- a/ref/refname/branch.go +++ /dev/null @@ -1,25 +0,0 @@ -package refname - -import "strings" - -// Branch checks one branch shorthand and returns its fully-qualified -// refs/heads/... name. -// -// Unlike Git in-repository branch parsing, this helper does not expand @{-n}. -func Branch(name string) (string, error) { - full := "refs/heads/" + name - if strings.HasPrefix(name, "-") || full == "refs/heads/HEAD" { - return "", &NameError{Name: name, Reason: "invalid branch name"} - } - - err := validate(full, 0) - if err != nil { - return "", err - } - - if strings.HasPrefix(name, "refs/") { - return name, nil - } - - return full, nil -} diff --git a/ref/refname/component.go b/ref/refname/component.go deleted file mode 100644 index f5adba46..00000000 --- a/ref/refname/component.go +++ /dev/null @@ -1,88 +0,0 @@ -package refname - -import "strings" - -func checkRefnameComponent(name string, flags *int, sanitized *strings.Builder, fullName string) (int, error) { - var last byte - - componentStart := sanitizedLen(sanitized) - - for i := range len(name) { - ch := name[i] - disp := refnameDisposition(ch) - - if sanitized != nil && disp != 1 { - sanitized.WriteByte(ch) - } - - switch disp { - case 1: - goto out - case 2: - if last == '.' { - if sanitized != nil { - truncateBuilder(sanitized, sanitized.Len()-1) - } else { - return 0, &NameError{Name: fullName, Reason: "name contains '..'"} - } - } - case 3: - if last == '@' { - if sanitized != nil { - overwriteLastByte(sanitized, '-') - } else { - return 0, &NameError{Name: fullName, Reason: "name contains '@{'"} - } - } - case 4: - if sanitized != nil { - overwriteLastByte(sanitized, '-') - } else { - return 0, &NameError{Name: fullName, Reason: "name contains one forbidden character"} - } - case 5: - if *flags&refnameRefspecPattern == 0 { - if sanitized != nil { - overwriteLastByte(sanitized, '-') - } else { - return 0, &NameError{Name: fullName, Reason: "name contains '*'"} - } - } - - *flags &^= refnameRefspecPattern - } - - last = ch - } - -out: - componentLen := strings.IndexByte(name, '/') - - if componentLen < 0 { - componentLen = len(name) - } - - if componentLen == 0 { - return 0, nil - } - - if name[0] == '.' { - if sanitized != nil { - overwriteBuilderAt(sanitized, componentStart, '-') - } else { - return 0, &NameError{Name: fullName, Reason: "component starts with '.'"} - } - } - - if componentLen >= len(lockSuffix) && name[componentLen-len(lockSuffix):componentLen] == lockSuffix { - if sanitized == nil { - return 0, &NameError{Name: fullName, Reason: "component ends with .lock"} - } - - for strings.HasSuffix(sanitized.String(), lockSuffix) { - truncateBuilder(sanitized, sanitized.Len()-len(lockSuffix)) - } - } - - return componentLen, nil -} diff --git a/ref/refname/current.go b/ref/refname/current.go deleted file mode 100644 index 3a5394cc..00000000 --- a/ref/refname/current.go +++ /dev/null @@ -1,5 +0,0 @@ -package refname - -func isCurrentWorktreeRef(name string) bool { - return IsRootSyntax(name) || IsPerWorktree(name) -} diff --git a/ref/refname/disposition.go b/ref/refname/disposition.go deleted file mode 100644 index 5153e633..00000000 --- a/ref/refname/disposition.go +++ /dev/null @@ -1,20 +0,0 @@ -package refname - -func refnameDisposition(ch byte) byte { - switch { - case ch == '/': - return 1 - case ch == '.': - return 2 - case ch == '{': - return 3 - case ch == '*': - return 5 - case ch < 0x20 || ch == 0x7f: - return 4 - case ch == ':' || ch == '?' || ch == '[' || ch == '\\' || ch == '^' || ch == '~' || ch == ' ' || ch == '\t': - return 4 - default: - return 0 - } -} diff --git a/ref/refname/doc.go b/ref/refname/doc.go deleted file mode 100644 index 363733f3..00000000 --- a/ref/refname/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package refname provides various routines to check and normalize reference names. -package refname diff --git a/ref/refname/errors.go b/ref/refname/errors.go deleted file mode 100644 index e39bc73b..00000000 --- a/ref/refname/errors.go +++ /dev/null @@ -1,14 +0,0 @@ -package refname - -import "fmt" - -// NameError reports one invalid reference name. -type NameError struct { - Name string - Reason string -} - -// Error implements error. -func (err *NameError) Error() string { - return fmt.Sprintf("ref: invalid name %q: %s", err.Name, err.Reason) -} diff --git a/ref/refname/flags.go b/ref/refname/flags.go deleted file mode 100644 index 72f0a58f..00000000 --- a/ref/refname/flags.go +++ /dev/null @@ -1,6 +0,0 @@ -package refname - -const ( - refnameAllowOneLevel = 1 << iota - refnameRefspecPattern -) diff --git a/ref/refname/length.go b/ref/refname/length.go deleted file mode 100644 index 94c0322b..00000000 --- a/ref/refname/length.go +++ /dev/null @@ -1,11 +0,0 @@ -package refname - -import "strings" - -func sanitizedLen(builder *strings.Builder) int { - if builder == nil { - return 0 - } - - return builder.Len() -} diff --git a/ref/refname/lock.go b/ref/refname/lock.go deleted file mode 100644 index 33db902f..00000000 --- a/ref/refname/lock.go +++ /dev/null @@ -1,3 +0,0 @@ -package refname - -const lockSuffix = ".lock" diff --git a/ref/refname/normalize.go b/ref/refname/normalize.go deleted file mode 100644 index 9cbe7126..00000000 --- a/ref/refname/normalize.go +++ /dev/null @@ -1,53 +0,0 @@ -package refname - -import "strings" - -// Normalize collapses slashes according to what Git wants -// then validates the normalized name. -func Normalize(name string, options Options) (string, error) { - normalized := collapseSlashes(name) - - err := validate(normalized, options.flags()) - if err != nil { - return "", err - } - - return normalized, nil -} - -func normalizeRefPath(path string) (string, bool) { - components := make([]string, 0, strings.Count(path, "/")+1) - i := 0 - - for i < len(path) { - for i < len(path) && path[i] == '/' { - i++ - } - - if i == len(path) { - break - } - - j := i - for j < len(path) && path[j] != '/' { - j++ - } - - component := path[i:j] - switch component { - case ".": - case "..": - if len(components) == 0 { - return "", false - } - - components = components[:len(components)-1] - default: - components = append(components, component) - } - - i = j - } - - return strings.Join(components, "/"), true -} diff --git a/ref/refname/options.go b/ref/refname/options.go deleted file mode 100644 index 5ae81541..00000000 --- a/ref/refname/options.go +++ /dev/null @@ -1,30 +0,0 @@ -package refname - -import "fmt" - -// Options controls Git refname validation. -type Options struct { - // AllowOneLevel permits one-component refnames like HEAD. - AllowOneLevel bool - - // RefspecPattern permits one '*' anywhere in the refname. - RefspecPattern bool -} - -// String returns one stable text form of the options. -func (options Options) String() string { - return fmt.Sprintf("allow_onelevel=%t,refspec_pattern=%t", options.AllowOneLevel, options.RefspecPattern) -} - -func (options Options) flags() int { - var flags int - if options.AllowOneLevel { - flags |= refnameAllowOneLevel - } - - if options.RefspecPattern { - flags |= refnameRefspecPattern - } - - return flags -} diff --git a/ref/refname/pseudo.go b/ref/refname/pseudo.go deleted file mode 100644 index f0ad1ae8..00000000 --- a/ref/refname/pseudo.go +++ /dev/null @@ -1,11 +0,0 @@ -package refname - -// IsPseudo reports whether name is one Git pseudo-ref. -func IsPseudo(name string) bool { - switch name { - case "FETCH_HEAD", "MERGE_HEAD": - return true - default: - return false - } -} diff --git a/ref/refname/refname_test.go b/ref/refname/refname_test.go deleted file mode 100644 index 5b26196f..00000000 --- a/ref/refname/refname_test.go +++ /dev/null @@ -1,622 +0,0 @@ -package refname_test - -import ( - "context" - "os/exec" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/ref/refname" -) - -func TestValidateNameAgainstGit(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - opts refname.Options - } - - tests := []testCase{ - {name: ""}, - {name: "/"}, - {name: "/", opts: refname.Options{AllowOneLevel: true}}, - {name: "foo/bar/baz"}, - {name: "refs/heads/main"}, - {name: "refs/tags/v1.0.0"}, - {name: "refs///heads/foo"}, - {name: "heads/foo/"}, - {name: "/heads/foo"}, - {name: "///heads/foo"}, - {name: "./foo"}, - {name: "./foo/bar"}, - {name: "foo/./bar"}, - {name: "foo/bar/."}, - {name: ".refs/foo"}, - {name: "refs/heads/foo."}, - {name: "HEAD"}, - {name: "HEAD", opts: refname.Options{AllowOneLevel: true}}, - {name: "refs/heads/.main"}, - {name: "heads/foo..bar"}, - {name: "refs/heads/main.lock"}, - {name: "heads///foo.lock"}, - {name: "refs/heads/foo..bar"}, - {name: "refs/heads/foo bar"}, - {name: "refs/heads/foo@{bar"}, - {name: "heads/foo?bar"}, - {name: "foo./bar"}, - {name: "foo.lock/bar"}, - {name: "foo.lock///bar"}, - {name: "heads/foo@bar"}, - {name: "heads/foo\\bar"}, - {name: "heads/foo\tbar"}, - {name: "heads/foo\x7fbar"}, - {name: "heads/fu\xC3\x9F"}, - {name: "heads/*foo/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "heads/foo*/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "heads/f*o/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "heads/f*o*/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "heads/foo*/bar*", opts: refname.Options{RefspecPattern: true}}, - {name: "refs/heads/foo/bar."}, - {name: "refs//heads///main"}, - {name: "foo"}, - {name: "foo", opts: refname.Options{AllowOneLevel: true}}, - {name: "foo", opts: refname.Options{RefspecPattern: true}}, - {name: "foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "foo/bar"}, - {name: "foo/bar", opts: refname.Options{AllowOneLevel: true}}, - {name: "foo/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "foo/bar", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "refs/heads/*"}, - {name: "refs/heads/*", opts: refname.Options{RefspecPattern: true}}, - {name: "refs/heads/feature*branch", opts: refname.Options{RefspecPattern: true}}, - {name: "refs/heads/foo*bar*baz", opts: refname.Options{RefspecPattern: true}}, - {name: "foo/*"}, - {name: "foo/*", opts: refname.Options{RefspecPattern: true}}, - {name: "foo/*", opts: refname.Options{AllowOneLevel: true}}, - {name: "foo/*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "*/foo"}, - {name: "*/foo", opts: refname.Options{RefspecPattern: true}}, - {name: "*/foo", opts: refname.Options{AllowOneLevel: true}}, - {name: "*/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "foo/*/bar"}, - {name: "foo/*/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "foo/*/bar", opts: refname.Options{AllowOneLevel: true}}, - {name: "foo/*/bar", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "*"}, - {name: "*", opts: refname.Options{AllowOneLevel: true}}, - {name: "*", opts: refname.Options{RefspecPattern: true}}, - {name: "*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "foo/*/*", opts: refname.Options{RefspecPattern: true}}, - {name: "foo/*/*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "*/foo/*", opts: refname.Options{RefspecPattern: true}}, - {name: "*/foo/*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "*/*/foo", opts: refname.Options{RefspecPattern: true}}, - {name: "*/*/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "/foo"}, - {name: "/foo", opts: refname.Options{AllowOneLevel: true}}, - {name: "/foo", opts: refname.Options{RefspecPattern: true}}, - {name: "/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "@"}, - } - - for _, tt := range tests { - t.Run(tt.name+"_"+tt.opts.String(), func(t *testing.T) { - t.Parallel() - - err := refname.Validate(tt.name, tt.opts) - gitErr := gitCheckRefFormat(t, tt.name, tt.opts) - - if (err == nil) != (gitErr == nil) { - t.Fatalf("ValidateName(%q, %+v) err=%v, git err=%v", tt.name, tt.opts, err, gitErr) - } - }) - } -} - -func TestNormalizeNameAgainstGit(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - opts refname.Options - } - - tests := []testCase{ - {name: "/"}, - {name: "/", opts: refname.Options{AllowOneLevel: true}}, - {name: "///refs///heads//main"}, - {name: "refs////tags///v1"}, - {name: "refs///heads///"}, - {name: "HEAD", opts: refname.Options{AllowOneLevel: true}}, - {name: "refs/heads/*", opts: refname.Options{RefspecPattern: true}}, - {name: "refs///heads/foo"}, - {name: "/heads/foo", opts: refname.Options{AllowOneLevel: true}}, - {name: "///heads/foo"}, - {name: "heads/foo/../bar"}, - {name: "heads/./foo"}, - {name: "heads\\foo"}, - {name: "heads/foo.lock"}, - {name: "heads///foo.lock"}, - {name: "foo.lock/bar"}, - {name: "foo.lock///bar"}, - {name: "foo"}, - {name: "/foo", opts: refname.Options{AllowOneLevel: true}}, - {name: "/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - } - - for _, tt := range tests { - t.Run(tt.name+"_"+tt.opts.String(), func(t *testing.T) { - t.Parallel() - - got, err := refname.Normalize(tt.name, tt.opts) - want, gitErr := gitNormalizeRefFormat(t, tt.name, tt.opts) - - if (err == nil) != (gitErr == nil) { - t.Fatalf("NormalizeName(%q, %+v) err=%v, git err=%v", tt.name, tt.opts, err, gitErr) - } - - if err == nil && got != want { - t.Fatalf("NormalizeName(%q, %+v) = %q, want %q", tt.name, tt.opts, got, want) - } - }) - } -} - -func TestBranchNameAgainstGit(t *testing.T) { - t.Parallel() - - tests := []string{ - "main", - "feature/topic", - "-main", - "HEAD", - "@{-1}", - "feature.lock", - "topic@{1}", - "refs/heads/main", - "refs/heads/HEAD", - "refs/tags/x", - } - - for _, name := range tests { - t.Run(name, func(t *testing.T) { - t.Parallel() - - got, err := refname.Branch(name) - want, gitErr := gitCheckBranchName(t, name) - - if (err == nil) != (gitErr == nil) { - t.Fatalf("BranchName(%q) err=%v, git err=%v", name, err, gitErr) - } - - if err == nil && got != want { - t.Fatalf("BranchName(%q) = %q, want %q", name, got, want) - } - }) - } -} - -func TestTagName(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - }{ - {name: "v1.0.0"}, - {name: "main/topic"}, - {name: "-bad"}, - {name: "HEAD"}, - {name: "feature.lock"}, - {name: "refs/tags/v1.0.0"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got, err := refname.Tag(tt.name) - want, gitErr := gitCheckTagName(t, tt.name) - - if (err == nil) != (gitErr == nil) { - t.Fatalf("TagName(%q) err=%v, git err=%v", tt.name, err, gitErr) - } - - if err == nil && got != want { - t.Fatalf("TagName(%q) = %q, want %q", tt.name, got, want) - } - }) - } -} - -func TestIsSafeName(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want bool - }{ - {name: "", want: false}, - {name: "HEAD", want: true}, - {name: "MERGE_HEAD", want: true}, - {name: "Head", want: false}, - {name: "refs/heads/main", want: true}, - {name: "refs/", want: false}, - {name: "refs//heads/main", want: false}, - {name: "refs/heads/main/", want: false}, - {name: "refs/foo/../bar", want: false}, - {name: "refs/foo/../../bar", want: false}, - {name: "refs/heads/main.lock", want: true}, - } - - for _, tt := range tests { - if got := refname.IsSafe(tt.name); got != tt.want { - t.Fatalf("IsSafeName(%q) = %v, want %v", tt.name, got, tt.want) - } - } -} - -func TestIsPerWorktree(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want bool - }{ - {name: "refs/worktree/foo", want: true}, - {name: "refs/bisect/foo", want: true}, - {name: "refs/rewritten/foo", want: true}, - {name: "refs/heads/foo", want: false}, - {name: "worktrees/wt1/HEAD", want: false}, - } - - for _, tt := range tests { - if got := refname.IsPerWorktree(tt.name); got != tt.want { - t.Fatalf("IsPerWorktree(%q) = %v, want %v", tt.name, got, tt.want) - } - } -} - -func TestIsPseudo(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want bool - }{ - {name: "FETCH_HEAD", want: true}, - {name: "MERGE_HEAD", want: true}, - {name: "HEAD", want: false}, - {name: "AUTO_MERGE", want: false}, - } - - for _, tt := range tests { - if got := refname.IsPseudo(tt.name); got != tt.want { - t.Fatalf("IsPseudo(%q) = %v, want %v", tt.name, got, tt.want) - } - } -} - -func TestIsRootSyntax(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want bool - }{ - {name: "", want: true}, - {name: "HEAD", want: true}, - {name: "AUTO_MERGE", want: true}, - {name: "BISECT-EXPECTED_REV", want: true}, - {name: "refs/heads/main", want: false}, - {name: "Head", want: false}, - {name: "HEAD1", want: false}, - } - - for _, tt := range tests { - if got := refname.IsRootSyntax(tt.name); got != tt.want { - t.Fatalf("IsRootSyntax(%q) = %v, want %v", tt.name, got, tt.want) - } - } -} - -func TestIsRoot(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want bool - }{ - {name: "HEAD", want: true}, - {name: "ORIG_HEAD", want: true}, - {name: "BOGUS_HEAD", want: true}, - {name: "CHERRY_PICK_HEAD", want: true}, - {name: "REVERT_HEAD", want: true}, - {name: "AUTO_MERGE", want: true}, - {name: "BISECT_EXPECTED_REV", want: true}, - {name: "NOTES_MERGE_PARTIAL", want: true}, - {name: "NOTES_MERGE_REF", want: true}, - {name: "MERGE_AUTOSTASH", want: true}, - {name: "FETCH_HEAD", want: false}, - {name: "MERGE_HEAD", want: false}, - {name: "Head", want: false}, - {name: "refs/heads/main", want: false}, - } - - for _, tt := range tests { - if got := refname.IsRoot(tt.name); got != tt.want { - t.Fatalf("IsRoot(%q) = %v, want %v", tt.name, got, tt.want) - } - } -} - -func TestParseWorktree(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want refname.ParsedWorktreeRef - }{ - { - name: "refs/heads/main", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeShared, - BareRefName: "refs/heads/main", - }, - }, - { - name: "HEAD", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeCurrent, - BareRefName: "HEAD", - }, - }, - { - name: "refs/worktree/foo", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeCurrent, - BareRefName: "refs/worktree/foo", - }, - }, - { - name: "main-worktree/HEAD", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeMain, - BareRefName: "HEAD", - }, - }, - { - name: "main-worktree/FOO", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeMain, - BareRefName: "FOO", - }, - }, - { - name: "main-worktree/refs/worktree/foo", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeMain, - BareRefName: "refs/worktree/foo", - }, - }, - { - name: "main-worktree/", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeMain, - BareRefName: "", - }, - }, - { - name: "main-worktree/refs/heads/main", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeShared, - BareRefName: "main-worktree/refs/heads/main", - }, - }, - { - name: "worktrees/wt1/HEAD", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeOther, - WorktreeName: "wt1", - BareRefName: "HEAD", - }, - }, - { - name: "worktrees/wt1/BAR", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeOther, - WorktreeName: "wt1", - BareRefName: "BAR", - }, - }, - { - name: "worktrees/wt1/refs/bisect/foo", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeOther, - WorktreeName: "wt1", - BareRefName: "refs/bisect/foo", - }, - }, - { - name: "worktrees/wt1/refs/heads/main", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeShared, - BareRefName: "worktrees/wt1/refs/heads/main", - }, - }, - { - name: "worktrees/wt1", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeOther, - WorktreeName: "wt1", - BareRefName: "", - }, - }, - } - - for _, tt := range tests { - if got := refname.ParseWorktree(tt.name); got != tt.want { - t.Fatalf("ParseWorktree(%q) = %#v, want %#v", tt.name, got, tt.want) - } - } -} - -func TestValidateUpdateName(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - hasNewValue bool - wantErr bool - }{ - {name: "refs/heads/main", hasNewValue: true, wantErr: false}, - {name: "HEAD", hasNewValue: true, wantErr: false}, - {name: "PSEUDOREF", hasNewValue: true, wantErr: false}, - {name: "FETCH_HEAD", hasNewValue: true, wantErr: true}, - {name: "MERGE_HEAD", hasNewValue: true, wantErr: true}, - {name: "refs/heads/.bad", hasNewValue: true, wantErr: true}, - {name: "foo/bar", hasNewValue: true, wantErr: false}, - {name: "foo/bar", hasNewValue: false, wantErr: true}, - {name: "PSEUDOREF", hasNewValue: false, wantErr: false}, - {name: "HEAD", hasNewValue: false, wantErr: false}, - } - - for _, tt := range tests { - err := refname.ValidateUpdateName(tt.name, tt.hasNewValue) - if (err != nil) != tt.wantErr { - t.Fatalf("ValidateUpdateName(%q, %v) err=%v, wantErr=%v", tt.name, tt.hasNewValue, err, tt.wantErr) - } - } -} - -func TestValidateSymbolicTarget(t *testing.T) { - t.Parallel() - - tests := []struct { - ref string - target string - wantErr bool - }{ - {ref: "HEAD", target: "refs/heads/main", wantErr: false}, - {ref: "HEAD", target: "foo", wantErr: true}, - {ref: "HEAD", target: "ORIG_HEAD", wantErr: true}, - {ref: "refs/heads/top", target: "ORIG_HEAD", wantErr: false}, - {ref: "refs/heads/top", target: "refs/heads/main", wantErr: false}, - {ref: "refs/heads/top", target: "worktrees/wt1/HEAD", wantErr: false}, - {ref: "refs/heads/top", target: "foo", wantErr: true}, - {ref: "refs/heads/top", target: "foo..bar", wantErr: true}, - {ref: "main-worktree/HEAD", target: "refs/heads/main", wantErr: false}, - {ref: "main-worktree/HEAD", target: "refs/tags/v1", wantErr: true}, - } - - for _, tt := range tests { - err := refname.ValidateSymbolicTarget(tt.ref, tt.target) - if (err != nil) != tt.wantErr { - t.Fatalf("ValidateSymbolicTarget(%q, %q) err=%v, wantErr=%v", tt.ref, tt.target, err, tt.wantErr) - } - } -} - -func TestSanitizeComponent(t *testing.T) { - t.Parallel() - - tests := []struct { - component string - want string - }{ - {component: ".", want: "-"}, - {component: "..", want: "-"}, - {component: "foo..bar", want: "foo.bar"}, - {component: "foo.lock", want: "foo"}, - {component: "foo.lock.lock", want: "foo"}, - {component: "foo bar", want: "foo-bar"}, - {component: "@", want: "-/@"}, - {component: "a@{b", want: "a@-b"}, - {component: "a*b", want: "a-b"}, - } - - for _, tt := range tests { - if got := refname.SanitizeComponent(tt.component); got != tt.want { - t.Fatalf("SanitizeComponent(%q) = %q, want %q", tt.component, got, tt.want) - } - } -} - -func gitCheckRefFormat(tb testing.TB, name string, opts refname.Options) error { - tb.Helper() - - args := []string{"check-ref-format"} - if opts.AllowOneLevel { - args = append(args, "--allow-onelevel") - } - - if opts.RefspecPattern { - args = append(args, "--refspec-pattern") - } - - args = append(args, name) - - return exec.CommandContext(context.Background(), "git", args...).Run() -} - -func gitNormalizeRefFormat(tb testing.TB, name string, opts refname.Options) (string, error) { - tb.Helper() - - args := []string{"check-ref-format", "--normalize"} - if opts.AllowOneLevel { - args = append(args, "--allow-onelevel") - } - - if opts.RefspecPattern { - args = append(args, "--refspec-pattern") - } - - args = append(args, name) - - out, err := exec.CommandContext(context.Background(), "git", args...).Output() - if err != nil { - return "", err - } - - return strings.TrimSuffix(string(out), "\n"), nil -} - -func gitCheckBranchName(tb testing.TB, name string) (string, error) { - tb.Helper() - - cmd := exec.CommandContext(context.Background(), "git", "check-ref-format", "--branch", name) - cmd.Dir = tb.TempDir() - - out, err := cmd.Output() - if err != nil { - return "", err - } - - branchName := strings.TrimSuffix(string(out), "\n") - if strings.HasPrefix(branchName, "refs/") { - return branchName, nil - } - - return "refs/heads/" + branchName, nil -} - -func gitCheckTagName(tb testing.TB, name string) (string, error) { - tb.Helper() - - if strings.HasPrefix(name, "-") || name == "HEAD" { - return "", exec.ErrNotFound - } - - //nolint:gosec - err := exec.CommandContext( - context.Background(), - "git", - "check-ref-format", - "refs/tags/"+name, - ).Run() - if err != nil { - return "", err - } - - return "refs/tags/" + name, nil -} diff --git a/ref/refname/root.go b/ref/refname/root.go deleted file mode 100644 index 43361846..00000000 --- a/ref/refname/root.go +++ /dev/null @@ -1,21 +0,0 @@ -package refname - -import "strings" - -// IsRoot reports whether name is one root ref according to Git. -func IsRoot(name string) bool { - if !IsRootSyntax(name) || IsPseudo(name) { - return false - } - - if strings.HasSuffix(name, "_HEAD") { - return true - } - - switch name { - case "HEAD", "AUTO_MERGE", "BISECT_EXPECTED_REV", "NOTES_MERGE_PARTIAL", "NOTES_MERGE_REF", "MERGE_AUTOSTASH": - return true - default: - return false - } -} diff --git a/ref/refname/root_syntax.go b/ref/refname/root_syntax.go deleted file mode 100644 index 97a15cb9..00000000 --- a/ref/refname/root_syntax.go +++ /dev/null @@ -1,13 +0,0 @@ -package refname - -// IsRootSyntax reports whether name matches Git's all-caps root-ref syntax. -func IsRootSyntax(name string) bool { - for i := range len(name) { - ch := name[i] - if (ch < 'A' || ch > 'Z') && ch != '-' && ch != '_' { - return false - } - } - - return true -} diff --git a/ref/refname/safe.go b/ref/refname/safe.go deleted file mode 100644 index b36d3b2f..00000000 --- a/ref/refname/safe.go +++ /dev/null @@ -1,31 +0,0 @@ -package refname - -import "strings" - -// IsSafe reports whether name is one safe refname for direct filesystem -// operations; see refname_is_safe. -func IsSafe(name string) bool { - rest, ok := strings.CutPrefix(name, "refs/") - if ok { - if rest == "" || rest[0] == '/' || rest[len(rest)-1] == '/' { - return false - } - - normalized, normOK := normalizeRefPath(rest) - - return normOK && normalized == rest - } - - if name == "" { - return false - } - - for i := range len(name) { - ch := name[i] - if (ch < 'A' || ch > 'Z') && ch != '_' { - return false - } - } - - return true -} diff --git a/ref/refname/sanitize.go b/ref/refname/sanitize.go deleted file mode 100644 index f543de7c..00000000 --- a/ref/refname/sanitize.go +++ /dev/null @@ -1,19 +0,0 @@ -package refname - -import ( - "fmt" - "strings" -) - -// SanitizeComponent mutates component until it satisfies -// sanitize_refname_component. -func SanitizeComponent(component string) string { - var builder strings.Builder - - err := checkOrSanitizeRefname(component, refnameAllowOneLevel, &builder) - if err != nil { - panic(fmt.Sprintf("ref: sanitize component %q: %v", component, err)) - } - - return builder.String() -} diff --git a/ref/refname/slashes.go b/ref/refname/slashes.go deleted file mode 100644 index 44d3e4ea..00000000 --- a/ref/refname/slashes.go +++ /dev/null @@ -1,26 +0,0 @@ -package refname - -import "strings" - -func collapseSlashes(name string) string { - if name == "" { - return "" - } - - var builder strings.Builder - builder.Grow(len(name)) - - prev := byte('/') - - for i := range len(name) { - ch := name[i] - if prev == '/' && ch == '/' { - continue - } - - builder.WriteByte(ch) - prev = ch - } - - return builder.String() -} diff --git a/ref/refname/tag.go b/ref/refname/tag.go deleted file mode 100644 index 226c0fdd..00000000 --- a/ref/refname/tag.go +++ /dev/null @@ -1,20 +0,0 @@ -package refname - -import "strings" - -// Tag checks one tag shorthand and returns its fully-qualified -// refs/tags/... name. -func Tag(name string) (string, error) { - if strings.HasPrefix(name, "-") || name == "HEAD" { - return "", &NameError{Name: name, Reason: "invalid tag name"} - } - - full := "refs/tags/" + name - - err := validate(full, 0) - if err != nil { - return "", err - } - - return full, nil -} diff --git a/ref/refname/update.go b/ref/refname/update.go deleted file mode 100644 index 92830f1a..00000000 --- a/ref/refname/update.go +++ /dev/null @@ -1,56 +0,0 @@ -package refname - -import "strings" - -// ValidateUpdateName checks whether name is valid for one direct ref update. -// -// See transaction_refname_valid(); -// updates with a new OID use check_refname_format(..., ALLOW_ONELEVEL), -// while delete/verify style operations use refname_is_safe(). -func ValidateUpdateName(name string, hasNewValue bool) error { - if IsPseudo(name) { - return &NameError{Name: name, Reason: "pseudoref updates are not allowed"} - } - - if hasNewValue { - return Validate(name, Options{AllowOneLevel: true}) - } - - if !IsSafe(name) { - return &NameError{Name: name, Reason: "unsafe refname for update"} - } - - return nil -} - -// ValidateSymbolicTarget checks whether target is valid for one symref target. -// -// See refs_fsck_symref(); -// root refs are allowed directly, HEAD must point to refs/heads/..., -// and non-root targets must be valid full refnames rooted at refs/ or -// worktrees/. -func ValidateSymbolicTarget(refname string, target string) error { - parsed := ParseWorktree(refname) - if parsed.BareRefName == "HEAD" && !strings.HasPrefix(target, "refs/heads/") { - return &NameError{Name: target, Reason: refname + " must point to refs/heads/..."} - } - - if IsRoot(target) { - return nil - } - - err := Validate(target, Options{}) - if err != nil { - return err - } - - if strings.HasPrefix(target, "refs/") { - return nil - } - - if strings.HasPrefix(target, "worktrees/") { - return nil - } - - return &NameError{Name: target, Reason: "symref target is not a ref"} -} diff --git a/ref/refname/utils.go b/ref/refname/utils.go deleted file mode 100644 index 58944748..00000000 --- a/ref/refname/utils.go +++ /dev/null @@ -1,22 +0,0 @@ -package refname - -import ( - "strings" -) - -func overwriteLastByte(builder *strings.Builder, ch byte) { - overwriteBuilderAt(builder, builder.Len()-1, ch) -} - -func overwriteBuilderAt(builder *strings.Builder, index int, ch byte) { - value := builder.String() - truncateBuilder(builder, index) - builder.WriteByte(ch) - builder.WriteString(value[index+1:]) -} - -func truncateBuilder(builder *strings.Builder, n int) { - value := builder.String() - builder.Reset() - builder.WriteString(value[:n]) -} diff --git a/ref/refname/validate.go b/ref/refname/validate.go deleted file mode 100644 index 1b8ad396..00000000 --- a/ref/refname/validate.go +++ /dev/null @@ -1,65 +0,0 @@ -package refname - -import "strings" - -// Validate checks whether name is one valid Git refname. -func Validate(name string, options Options) error { - return validate(name, options.flags()) -} - -func validate(name string, flags int) error { - return checkOrSanitizeRefname(name, flags, nil) -} - -func checkOrSanitizeRefname(name string, flags int, sanitized *strings.Builder) error { - componentCount := 0 - remaining := name - - if name == "@" { - if sanitized == nil { - return &NameError{Name: name, Reason: "single @ is not allowed"} - } - - sanitized.WriteByte('-') - } - - for { - if sanitized != nil && sanitized.Len() > 0 { - sanitized.WriteByte('/') - } - - componentLen, err := checkRefnameComponent(remaining, &flags, sanitized, name) - switch { - case sanitized != nil && componentLen == 0: - case componentLen <= 0: - if err != nil { - return err - } - - return &NameError{Name: name, Reason: "component has zero length"} - case err != nil: - return err - } - - componentCount++ - - if componentLen == len(remaining) { - break - } - - remaining = remaining[componentLen+1:] - } - - componentLen := len(remaining) - if componentLen > 0 && remaining[componentLen-1] == '.' { - if sanitized == nil { - return &NameError{Name: name, Reason: "name ends with '.'"} - } - } - - if flags&refnameAllowOneLevel == 0 && componentCount < 2 { - return &NameError{Name: name, Reason: "one-level refname is not allowed"} - } - - return nil -} diff --git a/ref/refname/worktree.go b/ref/refname/worktree.go deleted file mode 100644 index 48ca215d..00000000 --- a/ref/refname/worktree.go +++ /dev/null @@ -1,75 +0,0 @@ -package refname - -import "strings" - -// WorktreeType classifies one worktree-qualified refname prefix. -type WorktreeType uint8 - -const ( - // WorktreeShared is one ordinary shared refname. - WorktreeShared WorktreeType = iota - - // WorktreeCurrent is one current-worktree-only refname like HEAD or refs/worktree/... - WorktreeCurrent - - // WorktreeMain is one main-worktree-qualified refname like main-worktree/HEAD. - WorktreeMain - - // WorktreeOther is one other-worktree-qualified refname like worktrees/wt1/HEAD. - WorktreeOther -) - -// IsPerWorktree reports whether name is one per-worktree ref namespace. -func IsPerWorktree(name string) bool { - return strings.HasPrefix(name, "refs/worktree/") || - strings.HasPrefix(name, "refs/bisect/") || - strings.HasPrefix(name, "refs/rewritten/") -} - -// ParsedWorktreeRef is the result of parsing one worktree-qualified refname. -type ParsedWorktreeRef struct { - Type WorktreeType - WorktreeName string - BareRefName string -} - -// ParseWorktree parses Git's worktree ref prefixes. -func ParseWorktree(name string) ParsedWorktreeRef { - if bare, ok := strings.CutPrefix(name, "worktrees/"); ok { - worktreeName, rest, found := strings.Cut(bare, "/") - if !found { - return ParsedWorktreeRef{ - Type: WorktreeOther, - WorktreeName: worktreeName, - BareRefName: "", - } - } - - if isCurrentWorktreeRef(rest) { - return ParsedWorktreeRef{ - Type: WorktreeOther, - WorktreeName: worktreeName, - BareRefName: rest, - } - } - } - - if bare, ok := strings.CutPrefix(name, "main-worktree/"); ok && isCurrentWorktreeRef(bare) { - return ParsedWorktreeRef{ - Type: WorktreeMain, - BareRefName: bare, - } - } - - if isCurrentWorktreeRef(name) { - return ParsedWorktreeRef{ - Type: WorktreeCurrent, - BareRefName: name, - } - } - - return ParsedWorktreeRef{ - Type: WorktreeShared, - BareRefName: name, - } -} diff --git a/ref/store/files/root_loose_path.go b/ref/store/files/root_loose_path.go index a78d9bf3..7764073b 100644 --- a/ref/store/files/root_loose_path.go +++ b/ref/store/files/root_loose_path.go @@ -3,7 +3,7 @@ package files import ( "path" - "codeberg.org/lindenii/furgit/ref/refname" + "codeberg.org/lindenii/furgit/ref/name" ) func (store *Store) loosePath(name string) refPath { diff --git a/ref/store/files/update_direct_read.go b/ref/store/files/update_direct_read.go index e3557965..50e15026 100644 --- a/ref/store/files/update_direct_read.go +++ b/ref/store/files/update_direct_read.go @@ -5,7 +5,7 @@ import ( "fmt" "codeberg.org/lindenii/furgit/ref" - "codeberg.org/lindenii/furgit/ref/refname" + "codeberg.org/lindenii/furgit/ref/name" refstore "codeberg.org/lindenii/furgit/ref/store" ) diff --git a/ref/store/files/update_validate.go b/ref/store/files/update_validate.go index 4e5c2be6..cfbaca1d 100644 --- a/ref/store/files/update_validate.go +++ b/ref/store/files/update_validate.go @@ -5,7 +5,7 @@ import ( "strings" objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/ref/refname" + "codeberg.org/lindenii/furgit/ref/name" refstore "codeberg.org/lindenii/furgit/ref/store" ) -- cgit v1.3.1-10-gc9f91