github.com/go-git/go-git/v6@v6.0.0-20251216093047-22c365fcee9c/plumbing/reference.go (about)

     1  package plumbing
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  )
     9  
    10  const (
    11  	refPrefix       = "refs/"
    12  	refHeadPrefix   = refPrefix + "heads/"
    13  	refTagPrefix    = refPrefix + "tags/"
    14  	refRemotePrefix = refPrefix + "remotes/"
    15  	refNotePrefix   = refPrefix + "notes/"
    16  	symrefPrefix    = "ref: "
    17  )
    18  
    19  // RefRevParseRules are a set of rules to parse references into short names, or expand into a full reference.
    20  // These are the same rules as used by git in shorten_unambiguous_ref and expand_ref.
    21  // See: https://github.com/git/git/blob/e0aaa1b6532cfce93d87af9bc813fb2e7a7ce9d7/refs.c#L417
    22  var RefRevParseRules = []string{
    23  	"%s",
    24  	"refs/%s",
    25  	"refs/tags/%s",
    26  	"refs/heads/%s",
    27  	"refs/remotes/%s",
    28  	"refs/remotes/%s/HEAD",
    29  }
    30  
    31  var (
    32  	// ErrReferenceNotFound is returned when a reference is not found.
    33  	ErrReferenceNotFound = errors.New("reference not found")
    34  
    35  	// ErrInvalidReferenceName is returned when a reference name is invalid.
    36  	ErrInvalidReferenceName = errors.New("invalid reference name")
    37  )
    38  
    39  // ReferenceType reference type's
    40  type ReferenceType int8
    41  
    42  const (
    43  	// InvalidReference represents an invalid reference type.
    44  	InvalidReference ReferenceType = 0
    45  	// HashReference represents a hash reference.
    46  	HashReference ReferenceType = 1
    47  	// SymbolicReference represents a symbolic reference.
    48  	SymbolicReference ReferenceType = 2
    49  )
    50  
    51  func (r ReferenceType) String() string {
    52  	switch r {
    53  	case InvalidReference:
    54  		return "invalid-reference"
    55  	case HashReference:
    56  		return "hash-reference"
    57  	case SymbolicReference:
    58  		return "symbolic-reference"
    59  	}
    60  
    61  	return ""
    62  }
    63  
    64  // ReferenceName reference name's
    65  type ReferenceName string
    66  
    67  // NewBranchReferenceName returns a reference name describing a branch based on
    68  // his short name.
    69  func NewBranchReferenceName(name string) ReferenceName {
    70  	return ReferenceName(refHeadPrefix + name)
    71  }
    72  
    73  // NewNoteReferenceName returns a reference name describing a note based on his
    74  // short name.
    75  func NewNoteReferenceName(name string) ReferenceName {
    76  	return ReferenceName(refNotePrefix + name)
    77  }
    78  
    79  // NewRemoteReferenceName returns a reference name describing a remote branch
    80  // based on his short name and the remote name.
    81  func NewRemoteReferenceName(remote, name string) ReferenceName {
    82  	return ReferenceName(refRemotePrefix + fmt.Sprintf("%s/%s", remote, name))
    83  }
    84  
    85  // NewRemoteHEADReferenceName returns a reference name describing a the HEAD
    86  // branch of a remote.
    87  func NewRemoteHEADReferenceName(remote string) ReferenceName {
    88  	return ReferenceName(refRemotePrefix + fmt.Sprintf("%s/%s", remote, HEAD))
    89  }
    90  
    91  // NewTagReferenceName returns a reference name describing a tag based on short
    92  // his name.
    93  func NewTagReferenceName(name string) ReferenceName {
    94  	return ReferenceName(refTagPrefix + name)
    95  }
    96  
    97  // IsBranch check if a reference is a branch
    98  func (r ReferenceName) IsBranch() bool {
    99  	return strings.HasPrefix(string(r), refHeadPrefix)
   100  }
   101  
   102  // IsNote check if a reference is a note
   103  func (r ReferenceName) IsNote() bool {
   104  	return strings.HasPrefix(string(r), refNotePrefix)
   105  }
   106  
   107  // IsRemote check if a reference is a remote
   108  func (r ReferenceName) IsRemote() bool {
   109  	return strings.HasPrefix(string(r), refRemotePrefix)
   110  }
   111  
   112  // IsTag check if a reference is a tag
   113  func (r ReferenceName) IsTag() bool {
   114  	return strings.HasPrefix(string(r), refTagPrefix)
   115  }
   116  
   117  func (r ReferenceName) String() string {
   118  	return string(r)
   119  }
   120  
   121  // Short returns the short name of a ReferenceName
   122  func (r ReferenceName) Short() string {
   123  	s := string(r)
   124  	res := s
   125  	for _, format := range RefRevParseRules[1:] {
   126  		_, err := fmt.Sscanf(s, format, &res)
   127  		if err == nil {
   128  			continue
   129  		}
   130  	}
   131  
   132  	return res
   133  }
   134  
   135  var ctrlSeqs = regexp.MustCompile(`[\000-\037\177]`)
   136  
   137  // Validate validates a reference name.
   138  // This follows the git-check-ref-format rules.
   139  // See https://git-scm.com/docs/git-check-ref-format
   140  //
   141  // It is important to note that this function does not check if the reference
   142  // exists in the repository.
   143  // It only checks if the reference name is valid.
   144  // This functions does not support the --refspec-pattern, --normalize, and
   145  // --allow-onelevel options.
   146  //
   147  // Git imposes the following rules on how references are named:
   148  //
   149  //  1. They can include slash / for hierarchical (directory) grouping, but no
   150  //     slash-separated component can begin with a dot . or end with the
   151  //     sequence .lock.
   152  //  2. They must contain at least one /. This enforces the presence of a
   153  //     category like heads/, tags/ etc. but the actual names are not
   154  //     restricted. If the --allow-onelevel option is used, this rule is
   155  //     waived.
   156  //  3. They cannot have two consecutive dots .. anywhere.
   157  //  4. They cannot have ASCII control characters (i.e. bytes whose values are
   158  //     lower than \040, or \177 DEL), space, tilde ~, caret ^, or colon :
   159  //     anywhere.
   160  //  5. They cannot have question-mark ?, asterisk *, or open bracket [
   161  //     anywhere. See the --refspec-pattern option below for an exception to this
   162  //     rule.
   163  //  6. They cannot begin or end with a slash / or contain multiple consecutive
   164  //     slashes (see the --normalize option below for an exception to this rule).
   165  //  7. They cannot end with a dot ..
   166  //  8. They cannot contain a sequence @{.
   167  //  9. They cannot be the single character @.
   168  //  10. They cannot contain a \.
   169  func (r ReferenceName) Validate() error {
   170  	s := string(r)
   171  	if len(s) == 0 {
   172  		return ErrInvalidReferenceName
   173  	}
   174  
   175  	// HEAD is a special case
   176  	if r == HEAD {
   177  		return nil
   178  	}
   179  
   180  	// rule 7
   181  	if strings.HasSuffix(s, ".") {
   182  		return ErrInvalidReferenceName
   183  	}
   184  
   185  	// rule 2
   186  	parts := strings.Split(s, "/")
   187  	if len(parts) < 2 {
   188  		return ErrInvalidReferenceName
   189  	}
   190  
   191  	isBranch := r.IsBranch()
   192  	isTag := r.IsTag()
   193  	for i, part := range parts {
   194  		// rule 6
   195  		if len(part) == 0 {
   196  			return ErrInvalidReferenceName
   197  		}
   198  
   199  		if strings.HasPrefix(part, ".") || // rule 1
   200  			strings.Contains(part, "..") || // rule 3
   201  			ctrlSeqs.MatchString(part) || // rule 4
   202  			strings.ContainsAny(part, "~^:?*[ \t\n") || // rule 4 & 5
   203  			strings.Contains(part, "@{") || // rule 8
   204  			part == "@" || // rule 9
   205  			strings.Contains(part, "\\") || // rule 10
   206  			strings.HasSuffix(part, ".lock") { // rule 1
   207  			return ErrInvalidReferenceName
   208  		}
   209  
   210  		if (isBranch || isTag) && strings.HasPrefix(part, "-") && (i == 2) { // branches & tags can't start with -
   211  			return ErrInvalidReferenceName
   212  		}
   213  	}
   214  
   215  	return nil
   216  }
   217  
   218  const (
   219  	// HEAD is the special reference pointing to the current branch.
   220  	HEAD ReferenceName = "HEAD"
   221  	// Master is the master branch reference.
   222  	Master ReferenceName = "refs/heads/master"
   223  	// Main is the main branch reference.
   224  	Main ReferenceName = "refs/heads/main"
   225  )
   226  
   227  // Reference is a representation of git reference
   228  type Reference struct {
   229  	t      ReferenceType
   230  	n      ReferenceName
   231  	h      Hash
   232  	target ReferenceName
   233  }
   234  
   235  // NewReferenceFromStrings creates a reference from name and target as string,
   236  // the resulting reference can be a SymbolicReference or a HashReference base
   237  // on the target provided
   238  func NewReferenceFromStrings(name, target string) *Reference {
   239  	n := ReferenceName(name)
   240  
   241  	if strings.HasPrefix(target, symrefPrefix) {
   242  		target := ReferenceName(target[len(symrefPrefix):])
   243  		return NewSymbolicReference(n, target)
   244  	}
   245  
   246  	return NewHashReference(n, NewHash(target))
   247  }
   248  
   249  // NewSymbolicReference creates a new SymbolicReference reference
   250  func NewSymbolicReference(n, target ReferenceName) *Reference {
   251  	return &Reference{
   252  		t:      SymbolicReference,
   253  		n:      n,
   254  		target: target,
   255  	}
   256  }
   257  
   258  // NewHashReference creates a new HashReference reference
   259  func NewHashReference(n ReferenceName, h Hash) *Reference {
   260  	return &Reference{
   261  		t: HashReference,
   262  		n: n,
   263  		h: h,
   264  	}
   265  }
   266  
   267  // Type returns the type of a reference
   268  func (r *Reference) Type() ReferenceType {
   269  	return r.t
   270  }
   271  
   272  // Name returns the name of a reference
   273  func (r *Reference) Name() ReferenceName {
   274  	return r.n
   275  }
   276  
   277  // Hash returns the hash of a hash reference
   278  func (r *Reference) Hash() Hash {
   279  	return r.h
   280  }
   281  
   282  // Target returns the target of a symbolic reference
   283  func (r *Reference) Target() ReferenceName {
   284  	return r.target
   285  }
   286  
   287  // Strings dump a reference as a [2]string
   288  func (r *Reference) Strings() [2]string {
   289  	var o [2]string
   290  	o[0] = r.Name().String()
   291  
   292  	switch r.Type() {
   293  	case HashReference:
   294  		o[1] = r.Hash().String()
   295  	case SymbolicReference:
   296  		o[1] = symrefPrefix + r.Target().String()
   297  	}
   298  
   299  	return o
   300  }
   301  
   302  func (r *Reference) String() string {
   303  	ref := ""
   304  	switch r.Type() {
   305  	case HashReference:
   306  		ref = r.Hash().String()
   307  	case SymbolicReference:
   308  		ref = symrefPrefix + r.Target().String()
   309  	default:
   310  		return ""
   311  	}
   312  
   313  	name := r.Name().String()
   314  	var v strings.Builder
   315  	v.Grow(len(ref) + len(name) + 1)
   316  	v.WriteString(ref)
   317  	v.WriteString(" ")
   318  	v.WriteString(name)
   319  	return v.String()
   320  }