Skip to content

Commit cc1895b

Browse files
authored
Merge pull request #950 from aymanbagabas/validate-ref
git: validate reference names (#929)
2 parents d87110b + de1d5a5 commit cc1895b

File tree

8 files changed

+224
-3
lines changed

8 files changed

+224
-3
lines changed

config/branch.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func (b *Branch) Validate() error {
5454
return errBranchInvalidRebase
5555
}
5656

57-
return nil
57+
return plumbing.NewBranchReferenceName(b.Name).Validate()
5858
}
5959

6060
func (b *Branch) marshal() *format.Subsection {

config/config.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/go-git/go-billy/v5/osfs"
1515
"github.com/go-git/go-git/v5/internal/url"
16+
"github.com/go-git/go-git/v5/plumbing"
1617
format "github.com/go-git/go-git/v5/plumbing/format/config"
1718
)
1819

@@ -614,7 +615,7 @@ func (c *RemoteConfig) Validate() error {
614615
c.Fetch = []RefSpec{RefSpec(fmt.Sprintf(DefaultFetchRefSpec, c.Name))}
615616
}
616617

617-
return nil
618+
return plumbing.NewRemoteHEADReferenceName(c.Name).Validate()
618619
}
619620

620621
func (c *RemoteConfig) unmarshal(s *format.Subsection) error {

plumbing/reference.go

+89
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package plumbing
33
import (
44
"errors"
55
"fmt"
6+
"regexp"
67
"strings"
78
)
89

@@ -29,6 +30,9 @@ var RefRevParseRules = []string{
2930

3031
var (
3132
ErrReferenceNotFound = errors.New("reference not found")
33+
34+
// ErrInvalidReferenceName is returned when a reference name is invalid.
35+
ErrInvalidReferenceName = errors.New("invalid reference name")
3236
)
3337

3438
// ReferenceType reference type's
@@ -124,6 +128,91 @@ func (r ReferenceName) Short() string {
124128
return res
125129
}
126130

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
214+
}
215+
127216
const (
128217
HEAD ReferenceName = "HEAD"
129218
Master ReferenceName = "refs/heads/master"

plumbing/reference_test.go

+59
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,65 @@ func (s *ReferenceSuite) TestIsTag(c *C) {
103103
c.Assert(r.IsTag(), Equals, true)
104104
}
105105

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+
106165
func benchMarkReferenceString(r *Reference, b *testing.B) {
107166
for n := 0; n < b.N; n++ {
108167
_ = r.String()

repository.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ func InitWithOptions(s storage.Storer, worktree billy.Filesystem, options InitOp
9898
options.DefaultBranch = plumbing.Master
9999
}
100100

101+
if err := options.DefaultBranch.Validate(); err != nil {
102+
return nil, err
103+
}
104+
101105
r := newRepository(s, worktree)
102106
_, err := r.Reference(plumbing.HEAD, false)
103107
switch err {
@@ -724,7 +728,10 @@ func (r *Repository) DeleteBranch(name string) error {
724728
// CreateTag creates a tag. If opts is included, the tag is an annotated tag,
725729
// otherwise a lightweight tag is created.
726730
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+
}
728735

729736
_, err := r.Storer.Reference(rname)
730737
switch err {

repository_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ func (s *RepositorySuite) TestInitWithOptions(c *C) {
7575

7676
}
7777

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+
7885
func createCommit(c *C, r *Repository) {
7986
// Create a commit so there is a HEAD to check
8087
wt, err := r.Worktree()
@@ -391,6 +398,22 @@ func (s *RepositorySuite) TestDeleteRemote(c *C) {
391398
c.Assert(alt, IsNil)
392399
}
393400

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)
415+
}
416+
394417
func (s *RepositorySuite) TestCreateBranchAndBranch(c *C) {
395418
r, _ := Init(memory.NewStorage(), nil)
396419
testBranch := &config.Branch{
@@ -2797,6 +2820,20 @@ func (s *RepositorySuite) TestDeleteTagAnnotatedUnpacked(c *C) {
27972820
c.Assert(err, Equals, plumbing.ErrObjectNotFound)
27982821
}
27992822

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+
}
2835+
}
2836+
28002837
func (s *RepositorySuite) TestBranches(c *C) {
28012838
f := fixtures.ByURL("https://github.com/git-fixtures/root-references.git").One()
28022839
sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault())

worktree.go

+4
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ func (w *Worktree) Checkout(opts *CheckoutOptions) error {
189189
return w.Reset(ro)
190190
}
191191
func (w *Worktree) createBranch(opts *CheckoutOptions) error {
192+
if err := opts.Branch.Validate(); err != nil {
193+
return err
194+
}
195+
192196
_, err := w.r.Storer.Reference(opts.Branch)
193197
if err == nil {
194198
return fmt.Errorf("a branch named %q already exists", opts.Branch)

worktree_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,30 @@ func (s *WorktreeSuite) TestCheckoutCreateMissingBranch(c *C) {
785785
c.Assert(err, Equals, ErrCreateRequiresBranch)
786786
}
787787

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+
788812
func (s *WorktreeSuite) TestCheckoutTag(c *C) {
789813
f := fixtures.ByTag("tags").One()
790814
r := s.NewRepositoryWithEmptyWorktree(f)

0 commit comments

Comments
 (0)