internal/git/localrepo/refs.go (279 lines of code) (raw):

package localrepo import ( "bufio" "bytes" "context" "errors" "fmt" "io" "strings" "gitlab.com/gitlab-org/gitaly/v16/internal/command" "gitlab.com/gitlab-org/gitaly/v16/internal/git" "gitlab.com/gitlab-org/gitaly/v16/internal/git/gitcmd" "gitlab.com/gitlab-org/gitaly/v16/internal/git/updateref" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/transaction" ) // ErrMismatchingState is similar to updateref.MismatchingStateError. It's declared separately to avoid // an import cycle and to allow us to compare error types in the FetchSourceBranch RPC. var ErrMismatchingState = errors.New("reference does not point to expected object") // HasRevision checks if a revision in the repository exists. This will not // verify whether the target object exists. To do so, you can peel the revision // to a given object type, e.g. by passing `refs/heads/master^{commit}`. func (repo *Repo) HasRevision(ctx context.Context, revision git.Revision) (bool, error) { if _, err := repo.ResolveRevision(ctx, revision); err != nil { if errors.Is(err, git.ErrReferenceNotFound) { return false, nil } return false, err } return true, nil } // ResolveRevision resolves the given revision to its object ID. This will not // verify whether the target object exists. To do so, you can peel the // reference to a given object type, e.g. by passing // `refs/heads/master^{commit}`. Returns an ErrReferenceNotFound error in case // the revision does not exist. func (repo *Repo) ResolveRevision(ctx context.Context, revision git.Revision) (git.ObjectID, error) { if revision.String() == "" { return "", errors.New("repository cannot contain empty reference name") } var stdout bytes.Buffer if err := repo.ExecAndWait(ctx, gitcmd.Command{ Name: "rev-parse", Flags: []gitcmd.Option{gitcmd.Flag{Name: "--verify"}}, Args: []string{revision.String()}, }, gitcmd.WithStderr(io.Discard), gitcmd.WithStdout(&stdout), ); err != nil { if _, ok := command.ExitStatus(err); ok { return "", git.ErrReferenceNotFound } return "", err } objectHash, err := repo.ObjectHash(ctx) if err != nil { return "", fmt.Errorf("detecting object hash: %w", err) } hex := strings.TrimSpace(stdout.String()) oid, err := objectHash.FromHex(hex) if err != nil { return "", fmt.Errorf("unsupported object hash %q: %w", hex, err) } return oid, nil } // GetReference looks up and returns the given reference. Returns a // ReferenceNotFound error if the reference was not found. func (repo *Repo) GetReference(ctx context.Context, reference git.ReferenceName) (git.Reference, error) { refs, err := gitcmd.GetReferences(ctx, repo, gitcmd.GetReferencesConfig{ Patterns: []string{reference.String()}, Limit: 1, }) if err != nil { return git.Reference{}, err } if len(refs) == 0 { return git.Reference{}, git.ErrReferenceNotFound } if refs[0].Name != reference { return git.Reference{}, fmt.Errorf("%w: conflicts with %q", git.ErrReferenceAmbiguous, refs[0].Name) } return refs[0], nil } // HasBranches determines whether there is at least one branch in the // repository. func (repo *Repo) HasBranches(ctx context.Context) (bool, error) { refs, err := gitcmd.GetReferences(ctx, repo, gitcmd.GetReferencesConfig{ Patterns: []string{"refs/heads/"}, Limit: 1, }) return len(refs) > 0, err } // GetReferences returns references matching any of the given patterns. If no patterns are given, // all references are returned. func (repo *Repo) GetReferences(ctx context.Context, patterns ...string) ([]git.Reference, error) { return gitcmd.GetReferences(ctx, repo, gitcmd.GetReferencesConfig{ Patterns: patterns, }) } // GetBranches returns all branches. func (repo *Repo) GetBranches(ctx context.Context) ([]git.Reference, error) { return repo.GetReferences(ctx, "refs/heads/") } // UpdateRef updates reference from oldValue to newValue. If oldValue is a // non-empty string, the update will fail it the reference is not currently at // that revision. If newValue is the ZeroOID, the reference will be deleted. // If oldValue is the ZeroOID, the reference will created. func (repo *Repo) UpdateRef(ctx context.Context, reference git.ReferenceName, newValue, oldValue git.ObjectID) error { updater, err := updateref.New(ctx, repo) if err != nil { return fmt.Errorf("creating updateref: %w", err) } if err := updater.Start(); err != nil { return fmt.Errorf("start: %w", err) } if err := updater.Update(reference, newValue, oldValue); err != nil { return fmt.Errorf("update: %w", err) } if err := updater.Commit(); err != nil { return fmt.Errorf("commit: %w", err) } if err := updater.Close(); err != nil { return fmt.Errorf("close: %w", err) } return nil } // SetDefaultBranch sets the repository's HEAD to point to the given reference. // It will not verify the reference actually exists. func (repo *Repo) SetDefaultBranch(ctx context.Context, txManager transaction.Manager, reference git.ReferenceName) error { version, err := repo.GitVersion(ctx) if err != nil { return fmt.Errorf("detecting Git version: %w", err) } if err := git.ValidateReference(reference.String()); err != nil { return fmt.Errorf("%q is a malformed refname", reference) } return repo.setDefaultBranchWithUpdateRef(ctx, reference, version) } // setDefaultBranchWithUpdateRef uses 'symref-update' command to update HEAD. func (repo *Repo) setDefaultBranchWithUpdateRef( ctx context.Context, reference git.ReferenceName, version git.Version, ) (err error) { updater, err := updateref.New(ctx, repo, updateref.WithNoDeref()) if err != nil { return fmt.Errorf("creating updateref: %w", err) } defer func() { if cErr := updater.Close(); err == nil && cErr != nil { err = fmt.Errorf("close: %w", cErr) } }() if err = updater.Start(); err != nil { return fmt.Errorf("start: %w", err) } if err = updater.UpdateSymbolicReference(version, "HEAD", reference); err != nil { return fmt.Errorf("update: %w", err) } if err := updater.Commit(); err != nil { return fmt.Errorf("commit: %w", err) } return nil } type getRemoteReferenceConfig struct { patterns []string config []gitcmd.ConfigPair sshCommand string } // GetRemoteReferencesOption is an option which can be passed to GetRemoteReferences. type GetRemoteReferencesOption func(*getRemoteReferenceConfig) // WithPatterns sets up a set of patterns which are then used to filter the list of returned // references. func WithPatterns(patterns ...string) GetRemoteReferencesOption { return func(cfg *getRemoteReferenceConfig) { cfg.patterns = patterns } } // WithSSHCommand sets the SSH invocation to use when communicating with the remote. func WithSSHCommand(cmd string) GetRemoteReferencesOption { return func(cfg *getRemoteReferenceConfig) { cfg.sshCommand = cmd } } // WithConfig sets up Git configuration for the git-ls-remote(1) invocation. The config pairs are // set up via `WithConfigEnv()` and are thus not exposed via the command line. func WithConfig(config ...gitcmd.ConfigPair) GetRemoteReferencesOption { return func(cfg *getRemoteReferenceConfig) { cfg.config = config } } // GetRemoteReferences lists references of the remote. Peeled tags are not returned. func (repo *Repo) GetRemoteReferences(ctx context.Context, remote string, opts ...GetRemoteReferencesOption) ([]git.Reference, error) { var cfg getRemoteReferenceConfig for _, opt := range opts { opt(&cfg) } var env []string if cfg.sshCommand != "" { env = append(env, envGitSSHCommand(cfg.sshCommand)) } stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} if err := repo.ExecAndWait(ctx, gitcmd.Command{ Name: "ls-remote", Flags: []gitcmd.Option{ gitcmd.Flag{Name: "--refs"}, gitcmd.Flag{Name: "--symref"}, }, Args: append([]string{remote}, cfg.patterns...), }, gitcmd.WithStdout(stdout), gitcmd.WithStderr(stderr), gitcmd.WithEnv(env...), gitcmd.WithConfigEnv(cfg.config...), ); err != nil { return nil, fmt.Errorf("create git ls-remote: %w, stderr: %q", err, stderr) } var refs []git.Reference scanner := bufio.NewScanner(stdout) for scanner.Scan() { split := strings.SplitN(scanner.Text(), "\t", 2) if len(split) != 2 { return nil, fmt.Errorf("invalid ls-remote output line: %q", scanner.Text()) } // Symbolic references are outputted as: // ref: refs/heads/master refs/heads/symbolic-ref // 0c9cf732b5774fa948348bbd6f273009bd66e04c refs/heads/symbolic-ref if strings.HasPrefix(split[0], "ref: ") { symRef := split[1] if !scanner.Scan() { if err := scanner.Err(); err != nil { return nil, fmt.Errorf("scan dereferenced symbolic ref: %w", err) } return nil, fmt.Errorf("missing dereferenced symbolic ref line for %q", symRef) } split = strings.SplitN(scanner.Text(), "\t", 2) if len(split) != 2 { return nil, fmt.Errorf("invalid dereferenced symbolic ref line: %q", scanner.Text()) } if split[1] != symRef { return nil, fmt.Errorf("expected dereferenced symbolic ref %q but got reference %q", symRef, split[1]) } refs = append(refs, git.NewSymbolicReference(git.ReferenceName(symRef), git.ReferenceName(split[0]))) continue } refs = append(refs, git.NewReference(git.ReferenceName(split[1]), git.ObjectID(split[0]))) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("scan: %w", err) } return refs, nil } // GetDefaultBranch determines the default branch name func (repo *Repo) GetDefaultBranch(ctx context.Context) (git.ReferenceName, error) { headReference, err := repo.HeadReference(ctx) if err != nil { return "", err } // Ideally we would only use HEAD to determine the default branch, but // gitlab-rails depends on the branch being determined like this. for _, headCandidate := range []git.ReferenceName{ headReference, git.DefaultRef, git.LegacyDefaultRef, } { has, err := repo.HasRevision(ctx, headCandidate.Revision()) if err != nil { return "", err } if has { return headCandidate, nil } } // If all else fails, return the first branch name branches, err := gitcmd.GetReferences(ctx, repo, gitcmd.GetReferencesConfig{ Patterns: []string{"refs/heads/"}, Limit: 1, }) if err != nil || len(branches) == 0 { return "", err } return branches[0].Name, nil } // HeadReference returns the current value of HEAD. func (repo *Repo) HeadReference(ctx context.Context) (git.ReferenceName, error) { symref, err := gitcmd.GetSymbolicRef(ctx, repo, "HEAD") if err != nil { return "", err } return git.ReferenceName(symref.Target), nil } // GuessHead tries to guess what branch HEAD would be pointed at. If no // reference is found git.ErrReferenceNotFound is returned. // // This function should be roughly equivalent to the corresponding function in // git: // https://github.com/git/git/blob/2a97289ad8b103625d3a1a12f66c27f50df822ce/remote.c#L2198 func (repo *Repo) GuessHead(ctx context.Context, head git.Reference) (git.ReferenceName, error) { if head.IsSymbolic { return git.ReferenceName(head.Target), nil } // Try current and historic default branches first. Ideally we might look // up the git config `init.defaultBranch` but we do not allow this // configuration to be set by the user. It is always set to // `git.DefaultRef`. for _, name := range []git.ReferenceName{git.DefaultRef, git.LegacyDefaultRef} { ref, err := repo.GetReference(ctx, name) switch { case errors.Is(err, git.ErrReferenceNotFound): continue case err != nil: return "", fmt.Errorf("guess head: default: %w", err) case head.Target == ref.Target: return ref.Name, nil } } refs, err := repo.GetBranches(ctx) if err != nil { return "", fmt.Errorf("guess head: %w", err) } for _, ref := range refs { if ref.Target != head.Target { continue } return ref.Name, nil } return "", fmt.Errorf("guess head: %w", git.ErrReferenceNotFound) }