turbo-editors/turbo-corepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

git.go · 139 lines · 4.4 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Talking to git: which remote answers, what it calls its branches, and a
// shallow clone of one of them.

package configrepo

import (
	"context"
	"errors"
	"fmt"
	"os/exec"
	"strings"
)

// git runs the git that was found on PATH.
type git struct {
	path string
}

// fetch finds the repository among the remotes, settles which ref the URL
// meant, and clones that ref into dir. It returns the remote that answered,
// the ref, and the path inside the repository once the ref has taken its
// share of it.
func (g git) fetch(ctx context.Context, remotes []string, src Source, dir string) (remote, ref, path string, err error) {
	remote, refs, err := g.firstAnswering(ctx, remotes)
	if err != nil {
		return "", "", "", err
	}
	ref, path = resolveRef(refs, src.Ref, src.Path)
	if err := g.clone(ctx, remote, ref, dir); err != nil {
		return "", "", "", err
	}
	return remote, ref, path, nil
}

// firstAnswering asks each remote for its refs and returns the first that
// answers, with what it said.
//
// The listing does two jobs: it tells the candidates apart — a forge whose
// pages and repositories live on different hosts fails fast here, not after
// a clone — and it is what a ref with a slash in it is resolved against.
func (g git) firstAnswering(ctx context.Context, remotes []string) (string, []string, error) {
	var failures []error
	for _, remote := range remotes {
		refs, err := g.lsRemote(ctx, remote)
		if err == nil {
			return remote, refs, nil
		}
		failures = append(failures, err)
	}
	return "", nil, fmt.Errorf("configrepo: no repository answered: %w", errors.Join(failures...))
}

// lsRemote returns the names of a remote's branches and tags.
func (g git) lsRemote(ctx context.Context, remote string) ([]string, error) {
	out, err := g.run(ctx, "ls-remote", "--heads", "--tags", "--", remote)
	if err != nil {
		return nil, err
	}
	return parseRefs(out), nil
}

// parseRefs reads ls-remote's output — a hash, a tab, a ref — into names.
// Peeled tags ("v1^{}") name the same tag twice and are dropped.
func parseRefs(output string) []string {
	var names []string
	for _, line := range strings.Split(output, "\n") {
		_, ref, ok := strings.Cut(line, "\t")
		if !ok || strings.HasSuffix(ref, "^{}") {
			continue
		}
		for _, prefix := range []string{"refs/heads/", "refs/tags/"} {
			if name, found := strings.CutPrefix(ref, prefix); found {
				names = append(names, name)
			}
		}
	}
	return names
}

// resolveRef settles where the branch ends and the path begins.
//
// The URL gave a first segment and a rest; a branch called "feature/x" makes
// the first segment "feature", which names nothing. So the two are joined
// back together and the **longest** branch or tag that begins them wins,
// with the remainder as the path. Nothing matching leaves both as they were,
// and the clone then says, in git's own words, that there is no such branch.
func resolveRef(refs []string, ref, path string) (string, string) {
	if ref == "" {
		return ref, path
	}
	full := ref
	if path != "" {
		full += "/" + path
	}

	best := ""
	for _, name := range refs {
		if name != full && !strings.HasPrefix(full, name+"/") {
			continue
		}
		if len(name) > len(best) {
			best = name
		}
	}
	if best == "" {
		return ref, path
	}
	return best, strings.TrimPrefix(strings.TrimPrefix(full, best), "/")
}

// clone makes a shallow checkout of one ref — or of the default branch when
// ref is empty — into dir, which must exist and be empty.
func (g git) clone(ctx context.Context, remote, ref, dir string) error {
	args := []string{"clone", "--quiet", "--depth", "1"}
	if ref != "" {
		args = append(args, "--branch", ref)
	}
	args = append(args, "--", remote, dir)
	_, err := g.run(ctx, args...)
	return err
}

// run executes git and returns what it printed, or what it complained about.
func (g git) run(ctx context.Context, args ...string) (string, error) {
	cmd := exec.CommandContext(ctx, g.path, args...)
	// Never a password prompt: this runs from a command line that asked for a
	// URL, and a repository that needs one is reported, not waited on.
	cmd.Env = append(cmd.Environ(), "GIT_TERMINAL_PROMPT=0")

	out, err := cmd.Output()
	if err != nil {
		var exit *exec.ExitError
		if errors.As(err, &exit) && len(exit.Stderr) > 0 {
			return "", fmt.Errorf("configrepo: git %s: %s", args[0], strings.TrimSpace(string(exit.Stderr)))
		}
		return "", fmt.Errorf("configrepo: git %s: %w", args[0], err)
	}
	return string(out), nil
}