package furgit import ( "bufio" "bytes" "fmt" "os" "path" "path/filepath" "slices" "strings" ) func (repo *Repository) resolveLooseRef(refname string) (Ref, error) { data, err := os.ReadFile(repo.repoPath(refname)) if err != nil { if os.IsNotExist(err) { return Ref{}, ErrNotFound } return Ref{}, err } line := strings.TrimSpace(string(data)) if strings.HasPrefix(line, "ref: ") { target := strings.TrimSpace(line[5:]) if target == "" { return Ref{Name: refname, Kind: RefKindInvalid}, ErrInvalidRef } return Ref{ Name: refname, Kind: RefKindSymbolic, Ref: target, }, nil } id, err := repo.ParseHash(line) if err != nil { return Ref{Name: refname, Kind: RefKindInvalid}, err } return Ref{ Name: refname, Kind: RefKindDetached, Hash: id, }, nil } func (repo *Repository) resolvePackedRef(refname string) (Ref, error) { // According to git-pack-refs(1), symbolic refs are never // stored in packed-refs, so we only need to look for detached // refs here. path := repo.repoPath("packed-refs") f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { return Ref{}, ErrNotFound } return Ref{}, err } defer func() { _ = f.Close() }() want := []byte(refname) scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Bytes() if len(line) == 0 || line[0] == '#' || line[0] == '^' { continue } sp := bytes.IndexByte(line, ' ') if sp != repo.hashAlgo.size()*2 { continue } name := line[sp+1:] if !bytes.Equal(name, want) { continue } hex := string(line[:sp]) id, err := repo.ParseHash(hex) if err != nil { return Ref{Name: refname, Kind: RefKindInvalid}, err } ref := Ref{ Name: refname, Kind: RefKindDetached, Hash: id, } if scanner.Scan() { next := scanner.Bytes() if len(next) > 0 && next[0] == '^' { peeledHex := strings.TrimPrefix(string(next), "^") peeledHex = strings.TrimSpace(peeledHex) peeledID, err := repo.ParseHash(peeledHex) if err != nil { return Ref{Name: refname, Kind: RefKindInvalid}, err } ref.Peeled = peeledID } } if scanErr := scanner.Err(); scanErr != nil { return Ref{Name: refname, Kind: RefKindInvalid}, scanErr } return ref, nil } if scanErr := scanner.Err(); scanErr != nil { return Ref{Name: refname, Kind: RefKindInvalid}, scanErr } return Ref{}, ErrNotFound } // RefKind represents the kind of HEAD reference. type RefKind int const ( // The HEAD reference is invalid. RefKindInvalid RefKind = iota // The HEAD reference points to a detached commit hash. RefKindDetached // The HEAD reference points to a symbolic ref. RefKindSymbolic ) // Ref represents a reference. type Ref struct { // Name is the fully qualified ref name (e.g., refs/heads/main). // It may be empty for detached hashes that were not looked up // by name (e.g., ResolveRef on a raw hash). Name string // Kind is the kind of the reference. Kind RefKind // When Kind is RefKindSymbolic, Ref is the fully qualified ref name. // Otherwise the value is undefined. Ref string // When Kind is RefKindDetached, Hash is the commit hash. // Otherwise the value is undefined. Hash Hash // When Kind is RefKindDetached, and the ref supposedly points to an // annotated tag, Peeled is the peeled hash, i.e., the hash of the // object that the tag points to. Peeled Hash } type refParseRule struct { fmtStr string prefix string suffix string } func parseRule(rule string) refParseRule { prefix, suffix, _ := strings.Cut(rule, "%s") return refParseRule{ fmtStr: rule, prefix: prefix, suffix: suffix, } } var refRevParseRules = []refParseRule{ parseRule("%s"), parseRule("refs/%s"), parseRule("refs/tags/%s"), parseRule("refs/heads/%s"), parseRule("refs/remotes/%s"), parseRule("refs/remotes/%s/HEAD"), } func (rule refParseRule) match(name string) (string, bool) { if rule.suffix != "" { if !strings.HasSuffix(name, rule.suffix) { return "", false } name = strings.TrimSuffix(name, rule.suffix) } var short string n, err := fmt.Sscanf(name, rule.prefix+"%s", &short) if err != nil || n != 1 { return "", false } if fmt.Sprintf(rule.prefix+"%s", short) != name { return "", false } return short, true } func (rule refParseRule) render(short string) string { return rule.prefix + short + rule.suffix } // Short returns the shortest unambiguous shorthand for the ref name, // following the rev-parse rules used by Git. The provided list of refs // is used to test for ambiguity. // // When strict is true, all other rules must fail to resolve to an // existing ref; otherwise only rules prior to the matched rule must // fail. func (ref *Ref) Short(all []Ref, strict bool) string { if ref == nil { return "" } name := ref.Name if name == "" { return "" } names := make(map[string]struct{}, len(all)) for _, r := range all { if r.Name == "" { continue } names[r.Name] = struct{}{} } for i := len(refRevParseRules) - 1; i > 0; i-- { short, ok := refRevParseRules[i].match(name) if !ok { continue } rulesToFail := i if strict { rulesToFail = len(refRevParseRules) } ambiguous := false for j := 0; j < rulesToFail; j++ { if j == i { continue } full := refRevParseRules[j].render(short) if _, found := names[full]; found { ambiguous = true break } } if !ambiguous { return short } } return name } // ResolveRef reads the given fully qualified ref (such as "HEAD" or "refs/heads/main") // and interprets its contents as either a symbolic ref ("ref: refs/..."), a detached // hash, or invalid. // If path is empty, it defaults to "HEAD". // (While typically only HEAD may be a symbolic reference, others may be as well.) func (repo *Repository) ResolveRef(path string) (Ref, error) { if path == "" { path = "HEAD" } if !strings.HasPrefix(path, "refs/") && !slices.Contains([]string{ "HEAD", "ORIG_HEAD", "FETCH_HEAD", "MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "REBASE_HEAD", "BISECT_HEAD", }, path) { id, err := repo.ParseHash(path) if err == nil { return Ref{ Name: path, Kind: RefKindDetached, Hash: id, }, nil } // For now let's keep this to prevent e.g., random users from // specifying something crazy like objects/... or ./config. // There may be other legal pseudo-refs in the future, // but it's probably the best to stay cautious for now. return Ref{Name: path, Kind: RefKindInvalid}, ErrInvalidRef } loose, err := repo.resolveLooseRef(path) if err == nil { return loose, nil } if err != ErrNotFound { return Ref{Name: path, Kind: RefKindInvalid}, err } packed, err := repo.resolvePackedRef(path) if err == nil { return packed, nil } if err != ErrNotFound { return Ref{Name: path, Kind: RefKindInvalid}, err } return Ref{Name: path, Kind: RefKindInvalid}, ErrNotFound } // ResolveRefFully resolves a ref by recursively following // symbolic references until it reaches a detached ref. // Symbolic cycles are detected and reported. // Annotated tags are not peeled. func (repo *Repository) ResolveRefFully(path string) (Ref, error) { seen := make(map[string]struct{}) return repo.resolveRefFully(path, seen) } func (repo *Repository) resolveRefFully(path string, seen map[string]struct{}) (Ref, error) { if _, found := seen[path]; found { return Ref{}, fmt.Errorf("symbolic ref cycle involving %q", path) } seen[path] = struct{}{} ref, err := repo.ResolveRef(path) if err != nil { return Ref{}, err } switch ref.Kind { case RefKindDetached: return ref, nil case RefKindSymbolic: if ref.Ref == "" { return Ref{}, ErrInvalidRef } return repo.resolveRefFully(ref.Ref, seen) default: return Ref{}, ErrInvalidRef } } // ListRefs lists refs similarly to git-show-ref. // // The pattern must be empty or begin with "refs/". An empty pattern is // treated as "refs/*". // // Loose refs are resolved using filesystem globbing relative to the // repository root, then packed refs are read while skipping any names // that already appeared as loose refs. Packed refs are filtered // similarly. func (repo *Repository) ListRefs(pattern string) ([]Ref, error) { if pattern == "" { pattern = "refs/*" } if !strings.HasPrefix(pattern, "refs/") { return nil, ErrInvalidRef } if filepath.IsAbs(pattern) { return nil, ErrInvalidRef } var out []Ref seen := make(map[string]struct{}) globPattern := filepath.Join(repo.rootPath, filepath.FromSlash(pattern)) matches, err := filepath.Glob(globPattern) if err != nil { return nil, err } for _, match := range matches { info, statErr := os.Stat(match) if statErr != nil { return nil, statErr } if info.IsDir() { continue } rel, relErr := filepath.Rel(repo.rootPath, match) if relErr != nil { return nil, relErr } name := filepath.ToSlash(rel) if !strings.HasPrefix(name, "refs/") { continue } ref, resolveErr := repo.resolveLooseRef(name) if resolveErr != nil { if resolveErr == ErrNotFound || os.IsNotExist(resolveErr) { continue } return nil, resolveErr } seen[name] = struct{}{} out = append(out, ref) } packedPath := repo.repoPath("packed-refs") f, err := os.Open(packedPath) if err != nil { if os.IsNotExist(err) { return out, nil } return nil, err } defer func() { _ = f.Close() }() scanner := bufio.NewScanner(f) lastIdx := -1 for scanner.Scan() { line := scanner.Bytes() if len(line) == 0 || line[0] == '#' { continue } if line[0] == '^' { if lastIdx < 0 { continue } peeledHex := strings.TrimPrefix(string(line), "^") peeledHex = strings.TrimSpace(peeledHex) peeled, parseErr := repo.ParseHash(peeledHex) if parseErr != nil { return nil, parseErr } out[lastIdx].Peeled = peeled continue } sp := bytes.IndexByte(line, ' ') if sp != repo.hashAlgo.size()*2 { lastIdx = -1 continue } name := string(line[sp+1:]) if !strings.HasPrefix(name, "refs/") { lastIdx = -1 continue } if _, ok := seen[name]; ok { lastIdx = -1 continue } match, matchErr := path.Match(pattern, name) if matchErr != nil { return nil, matchErr } if !match { lastIdx = -1 continue } hash, parseErr := repo.ParseHash(string(line[:sp])) if parseErr != nil { return nil, parseErr } out = append(out, Ref{ Name: name, Kind: RefKindDetached, Hash: hash, }) lastIdx = len(out) - 1 } if scanErr := scanner.Err(); scanErr != nil { return nil, scanErr } return out, nil }