fork of go-git with some jj specific features

git: validate reference names

Check reference names format before creating branches/tags/remotes.

This should probably be in a lower level somewhere in `plumbing`.
Validating the names under `plumbing.NewReference*` is not possible
since these functions don't return errors.

Fixes: https://github.com/go-git/go-git/issues/929

+1 -1
config/branch.go
··· 54 54 return errBranchInvalidRebase 55 55 } 56 56 57 - return nil 57 + return plumbing.NewBranchReferenceName(b.Name).Validate() 58 58 } 59 59 60 60 func (b *Branch) marshal() *format.Subsection {
+2 -1
config/config.go
··· 13 13 14 14 "github.com/go-git/go-billy/v5/osfs" 15 15 "github.com/go-git/go-git/v5/internal/url" 16 + "github.com/go-git/go-git/v5/plumbing" 16 17 format "github.com/go-git/go-git/v5/plumbing/format/config" 17 18 ) 18 19 ··· 614 615 c.Fetch = []RefSpec{RefSpec(fmt.Sprintf(DefaultFetchRefSpec, c.Name))} 615 616 } 616 617 617 - return nil 618 + return plumbing.NewRemoteHEADReferenceName(c.Name).Validate() 618 619 } 619 620 620 621 func (c *RemoteConfig) unmarshal(s *format.Subsection) error {
+89
plumbing/reference.go
··· 3 3 import ( 4 4 "errors" 5 5 "fmt" 6 + "regexp" 6 7 "strings" 7 8 ) 8 9 ··· 29 30 30 31 var ( 31 32 ErrReferenceNotFound = errors.New("reference not found") 33 + 34 + // ErrInvalidReferenceName is returned when a reference name is invalid. 35 + ErrInvalidReferenceName = errors.New("invalid reference name") 32 36 ) 33 37 34 38 // ReferenceType reference type's ··· 122 126 } 123 127 124 128 return res 129 + } 130 + 131 + var ( 132 + ctrlSeqs = regexp.MustCompile(`[\000-\037\177]`) 133 + ) 134 + 135 + // Validate validates a reference name. 136 + // This follows the git-check-ref-format rules. 137 + // See https://git-scm.com/docs/git-check-ref-format 138 + // 139 + // It is important to note that this function does not check if the reference 140 + // exists in the repository. 141 + // It only checks if the reference name is valid. 142 + // This functions does not support the --refspec-pattern, --normalize, and 143 + // --allow-onelevel options. 144 + // 145 + // Git imposes the following rules on how references are named: 146 + // 147 + // 1. They can include slash / for hierarchical (directory) grouping, but no 148 + // slash-separated component can begin with a dot . or end with the 149 + // sequence .lock. 150 + // 2. They must contain at least one /. This enforces the presence of a 151 + // category like heads/, tags/ etc. but the actual names are not 152 + // restricted. If the --allow-onelevel option is used, this rule is 153 + // waived. 154 + // 3. They cannot have two consecutive dots .. anywhere. 155 + // 4. They cannot have ASCII control characters (i.e. bytes whose values are 156 + // lower than \040, or \177 DEL), space, tilde ~, caret ^, or colon : 157 + // anywhere. 158 + // 5. They cannot have question-mark ?, asterisk *, or open bracket [ 159 + // anywhere. See the --refspec-pattern option below for an exception to this 160 + // rule. 161 + // 6. They cannot begin or end with a slash / or contain multiple consecutive 162 + // slashes (see the --normalize option below for an exception to this rule). 163 + // 7. They cannot end with a dot .. 164 + // 8. They cannot contain a sequence @{. 165 + // 9. They cannot be the single character @. 166 + // 10. They cannot contain a \. 167 + func (r ReferenceName) Validate() error { 168 + s := string(r) 169 + if len(s) == 0 { 170 + return ErrInvalidReferenceName 171 + } 172 + 173 + // HEAD is a special case 174 + if r == HEAD { 175 + return nil 176 + } 177 + 178 + // rule 7 179 + if strings.HasSuffix(s, ".") { 180 + return ErrInvalidReferenceName 181 + } 182 + 183 + // rule 2 184 + parts := strings.Split(s, "/") 185 + if len(parts) < 2 { 186 + return ErrInvalidReferenceName 187 + } 188 + 189 + isBranch := r.IsBranch() 190 + isTag := r.IsTag() 191 + for _, part := range parts { 192 + // rule 6 193 + if len(part) == 0 { 194 + return ErrInvalidReferenceName 195 + } 196 + 197 + if strings.HasPrefix(part, ".") || // rule 1 198 + strings.Contains(part, "..") || // rule 3 199 + ctrlSeqs.MatchString(part) || // rule 4 200 + strings.ContainsAny(part, "~^:?*[ \t\n") || // rule 4 & 5 201 + strings.Contains(part, "@{") || // rule 8 202 + part == "@" || // rule 9 203 + strings.Contains(part, "\\") || // rule 10 204 + strings.HasSuffix(part, ".lock") { // rule 1 205 + return ErrInvalidReferenceName 206 + } 207 + 208 + if (isBranch || isTag) && strings.HasPrefix(part, "-") { // branches & tags can't start with - 209 + return ErrInvalidReferenceName 210 + } 211 + } 212 + 213 + return nil 125 214 } 126 215 127 216 const (
+59
plumbing/reference_test.go
··· 103 103 c.Assert(r.IsTag(), Equals, true) 104 104 } 105 105 106 + func (s *ReferenceSuite) TestValidReferenceNames(c *C) { 107 + valid := []ReferenceName{ 108 + "refs/heads/master", 109 + "refs/notes/commits", 110 + "refs/remotes/origin/master", 111 + "HEAD", 112 + "refs/tags/v3.1.1", 113 + "refs/pulls/1/head", 114 + "refs/pulls/1/merge", 115 + "refs/pulls/1/abc.123", 116 + "refs/pulls", 117 + "refs/-", // should this be allowed? 118 + } 119 + for _, v := range valid { 120 + c.Assert(v.Validate(), IsNil) 121 + } 122 + 123 + invalid := []ReferenceName{ 124 + "refs", 125 + "refs/", 126 + "refs//", 127 + "refs/heads/\\", 128 + "refs/heads/\\foo", 129 + "refs/heads/\\foo/bar", 130 + "abc", 131 + "", 132 + "refs/heads/ ", 133 + "refs/heads/ /", 134 + "refs/heads/ /foo", 135 + "refs/heads/.", 136 + "refs/heads/..", 137 + "refs/heads/foo..", 138 + "refs/heads/foo.lock", 139 + "refs/heads/foo@{bar}", 140 + "refs/heads/foo[", 141 + "refs/heads/foo~", 142 + "refs/heads/foo^", 143 + "refs/heads/foo:", 144 + "refs/heads/foo?", 145 + "refs/heads/foo*", 146 + "refs/heads/foo[bar", 147 + "refs/heads/foo\t", 148 + "refs/heads/@", 149 + "refs/heads/@{bar}", 150 + "refs/heads/\n", 151 + "refs/heads/-foo", 152 + "refs/heads/foo..bar", 153 + "refs/heads/-", 154 + "refs/tags/-", 155 + "refs/tags/-foo", 156 + } 157 + 158 + for i, v := range invalid { 159 + comment := Commentf("invalid reference name case %d: %s", i, v) 160 + c.Assert(v.Validate(), NotNil, comment) 161 + c.Assert(v.Validate(), ErrorMatches, "invalid reference name", comment) 162 + } 163 + } 164 + 106 165 func benchMarkReferenceString(r *Reference, b *testing.B) { 107 166 for n := 0; n < b.N; n++ { 108 167 _ = r.String()
+8 -1
repository.go
··· 98 98 options.DefaultBranch = plumbing.Master 99 99 } 100 100 101 + if err := options.DefaultBranch.Validate(); err != nil { 102 + return nil, err 103 + } 104 + 101 105 r := newRepository(s, worktree) 102 106 _, err := r.Reference(plumbing.HEAD, false) 103 107 switch err { ··· 724 728 // CreateTag creates a tag. If opts is included, the tag is an annotated tag, 725 729 // otherwise a lightweight tag is created. 726 730 func (r *Repository) CreateTag(name string, hash plumbing.Hash, opts *CreateTagOptions) (*plumbing.Reference, error) { 727 - rname := plumbing.ReferenceName(path.Join("refs", "tags", name)) 731 + rname := plumbing.NewTagReferenceName(name) 732 + if err := rname.Validate(); err != nil { 733 + return nil, err 734 + } 728 735 729 736 _, err := r.Storer.Reference(rname) 730 737 switch err {
+37
repository_test.go
··· 75 75 76 76 } 77 77 78 + func (s *RepositorySuite) TestInitWithInvalidDefaultBranch(c *C) { 79 + _, err := InitWithOptions(memory.NewStorage(), memfs.New(), InitOptions{ 80 + DefaultBranch: "foo", 81 + }) 82 + c.Assert(err, NotNil) 83 + } 84 + 78 85 func createCommit(c *C, r *Repository) { 79 86 // Create a commit so there is a HEAD to check 80 87 wt, err := r.Worktree() ··· 389 396 alt, err := r.Remote("foo") 390 397 c.Assert(err, Equals, ErrRemoteNotFound) 391 398 c.Assert(alt, IsNil) 399 + } 400 + 401 + func (s *RepositorySuite) TestEmptyCreateBranch(c *C) { 402 + r, _ := Init(memory.NewStorage(), nil) 403 + err := r.CreateBranch(&config.Branch{}) 404 + 405 + c.Assert(err, NotNil) 406 + } 407 + 408 + func (s *RepositorySuite) TestInvalidCreateBranch(c *C) { 409 + r, _ := Init(memory.NewStorage(), nil) 410 + err := r.CreateBranch(&config.Branch{ 411 + Name: "-foo", 412 + }) 413 + 414 + c.Assert(err, NotNil) 392 415 } 393 416 394 417 func (s *RepositorySuite) TestCreateBranchAndBranch(c *C) { ··· 2795 2818 obj, err = r.TagObject(ref.Hash()) 2796 2819 c.Assert(obj, IsNil) 2797 2820 c.Assert(err, Equals, plumbing.ErrObjectNotFound) 2821 + } 2822 + 2823 + func (s *RepositorySuite) TestInvalidTagName(c *C) { 2824 + r, err := Init(memory.NewStorage(), nil) 2825 + c.Assert(err, IsNil) 2826 + for i, name := range []string{ 2827 + "", 2828 + "foo bar", 2829 + "foo\tbar", 2830 + "foo\nbar", 2831 + } { 2832 + _, err = r.CreateTag(name, plumbing.ZeroHash, nil) 2833 + c.Assert(err, NotNil, Commentf("case %d %q", i, name)) 2834 + } 2798 2835 } 2799 2836 2800 2837 func (s *RepositorySuite) TestBranches(c *C) {
+4
worktree.go
··· 189 189 return w.Reset(ro) 190 190 } 191 191 func (w *Worktree) createBranch(opts *CheckoutOptions) error { 192 + if err := opts.Branch.Validate(); err != nil { 193 + return err 194 + } 195 + 192 196 _, err := w.r.Storer.Reference(opts.Branch) 193 197 if err == nil { 194 198 return fmt.Errorf("a branch named %q already exists", opts.Branch)
+24
worktree_test.go
··· 785 785 c.Assert(err, Equals, ErrCreateRequiresBranch) 786 786 } 787 787 788 + func (s *WorktreeSuite) TestCheckoutCreateInvalidBranch(c *C) { 789 + w := &Worktree{ 790 + r: s.Repository, 791 + Filesystem: memfs.New(), 792 + } 793 + 794 + for _, name := range []plumbing.ReferenceName{ 795 + "foo", 796 + "-", 797 + "-foo", 798 + "refs/heads//", 799 + "refs/heads/..", 800 + "refs/heads/a..b", 801 + "refs/heads/.", 802 + } { 803 + err := w.Checkout(&CheckoutOptions{ 804 + Create: true, 805 + Branch: name, 806 + }) 807 + 808 + c.Assert(err, Equals, plumbing.ErrInvalidReferenceName) 809 + } 810 + } 811 + 788 812 func (s *WorktreeSuite) TestCheckoutTag(c *C) { 789 813 f := fixtures.ByTag("tags").One() 790 814 r := s.NewRepositoryWithEmptyWorktree(f)