vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
refactor
c5902451905acdaa478f8476844ccd4a510d9ca9 · Keenan Nemetz · 16d ago · parent ec2aba4b
client/git.go +151
| @@ -0,0 +1,151 @@ | |||
| 1 | // Package client implements the client side of talking to a Vole instance. | ||
| 2 | // | ||
| 3 | // It owns the parts of the CLI that are not about parsing arguments or printing: | ||
| 4 | // the clone's metadata branch, the SSH verbs that read the roster and ask who a | ||
| 5 | // key belongs to, the object-transfer protocol, and publishing a site snapshot. | ||
| 6 | // Tools that extend Vole can use this package instead of shelling out to the | ||
| 7 | // vole binary. | ||
| 8 | package client | ||
| 9 | |||
| 10 | import ( | ||
| 11 | "fmt" | ||
| 12 | "net/url" | ||
| 13 | "os" | ||
| 14 | "os/exec" | ||
| 15 | "strings" | ||
| 16 | ) | ||
| 17 | |||
| 18 | // CurrentGitDir is the clone the command is working in. | ||
| 19 | func CurrentGitDir() (string, error) { | ||
| 20 | out, err := exec.Command("git", "rev-parse", "--absolute-git-dir").Output() | ||
| 21 | if err != nil { | ||
| 22 | return "", fmt.Errorf("not inside a git repository") | ||
| 23 | } | ||
| 24 | return strings.TrimSpace(string(out)), nil | ||
| 25 | } | ||
| 26 | |||
| 27 | // GitDirOf asks a checkout where its git directory is, so a caller can commit | ||
| 28 | // into it without changing directory. | ||
| 29 | func GitDirOf(dir string) (string, error) { | ||
| 30 | out, err := exec.Command("git", "-C", dir, "rev-parse", "--absolute-git-dir").Output() | ||
| 31 | if err != nil { | ||
| 32 | return "", fmt.Errorf("%s is not a git checkout", dir) | ||
| 33 | } | ||
| 34 | return strings.TrimSpace(string(out)), nil | ||
| 35 | } | ||
| 36 | |||
| 37 | // GitOut runs one git command in dir and returns its stdout. | ||
| 38 | func GitOut(dir string, args ...string) (string, error) { | ||
| 39 | cmd := exec.Command("git", args...) | ||
| 40 | cmd.Dir = dir | ||
| 41 | cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") | ||
| 42 | var out, stderr strings.Builder | ||
| 43 | cmd.Stdout, cmd.Stderr = &out, &stderr | ||
| 44 | if err := cmd.Run(); err != nil { | ||
| 45 | return "", fmt.Errorf("%w: %s", err, strings.TrimSpace(stderr.String())) | ||
| 46 | } | ||
| 47 | return out.String(), nil | ||
| 48 | } | ||
| 49 | |||
| 50 | // IsRemoteURL reports whether a remote argument is a location rather than a name. | ||
| 51 | func IsRemoteURL(s string) bool { | ||
| 52 | return strings.Contains(s, "://") || strings.HasPrefix(s, "git@") | ||
| 53 | } | ||
| 54 | |||
| 55 | // HasRemote reports whether the clone has a remote with this name. | ||
| 56 | func HasRemote(gitDir, name string) bool { | ||
| 57 | cmd := exec.Command("git", "config", "--get", "remote."+name+".url") | ||
| 58 | cmd.Dir = gitDir | ||
| 59 | return cmd.Run() == nil | ||
| 60 | } | ||
| 61 | |||
| 62 | // RemoteURL reads the URL a metadata push would go to. A URL is a remote in its | ||
| 63 | // own right, which is what lets a CI runner or a fresh repository push without | ||
| 64 | // configuring a named remote. | ||
| 65 | func RemoteURL(gitDir, remote string) (string, error) { | ||
| 66 | if IsRemoteURL(remote) { | ||
| 67 | return remote, nil | ||
| 68 | } | ||
| 69 | out, err := GitOut(gitDir, "remote", "get-url", remote) | ||
| 70 | if err != nil { | ||
| 71 | return "", fmt.Errorf("no remote named %q; pass --remote or add one", remote) | ||
| 72 | } | ||
| 73 | u := strings.TrimSpace(out) | ||
| 74 | if u == "" { | ||
| 75 | return "", fmt.Errorf("remote %q has no URL", remote) | ||
| 76 | } | ||
| 77 | return u, nil | ||
| 78 | } | ||
| 79 | |||
| 80 | // LSRemote asks a URL or a remote name where a ref points, or "" when it is gone. | ||
| 81 | // A clone is a cache — after an unpublish the local branch survives — so callers | ||
| 82 | // that need the live state ask the server rather than the checkout. | ||
| 83 | func LSRemote(gitDir, remote, ref string) (string, error) { | ||
| 84 | cmd := exec.Command("git", "ls-remote", "--quiet", remote, ref) | ||
| 85 | cmd.Dir = gitDir | ||
| 86 | var stderr strings.Builder | ||
| 87 | cmd.Stderr = &stderr | ||
| 88 | out, err := cmd.Output() | ||
| 89 | if err != nil { | ||
| 90 | return "", fmt.Errorf("ask %s about %s: %w: %s", remote, ref, err, strings.TrimSpace(stderr.String())) | ||
| 91 | } | ||
| 92 | for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { | ||
| 93 | f := strings.Fields(line) | ||
| 94 | if len(f) == 2 && f[1] == ref { | ||
| 95 | return f[0], nil | ||
| 96 | } | ||
| 97 | } | ||
| 98 | return "", nil | ||
| 99 | } | ||
| 100 | |||
| 101 | // DefaultAuthor prefers an explicit vole.author, then git's user.name, then | ||
| 102 | // $USER. Vole usernames are lowercase ASCII, so the result is normalised to that. | ||
| 103 | func DefaultAuthor(gitDir string) string { | ||
| 104 | for _, key := range []string{"vole.author", "user.name"} { | ||
| 105 | out, err := GitOut(gitDir, "config", "--get", key) | ||
| 106 | if err == nil { | ||
| 107 | if s := SanitizeAuthor(out); s != "" { | ||
| 108 | return s | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | return SanitizeAuthor(os.Getenv("USER")) | ||
| 113 | } | ||
| 114 | |||
| 115 | // SanitizeAuthor turns a display name into the shape of a Vole account name. | ||
| 116 | func SanitizeAuthor(s string) string { | ||
| 117 | s = strings.ToLower(strings.TrimSpace(s)) | ||
| 118 | var b strings.Builder | ||
| 119 | for _, r := range s { | ||
| 120 | switch { | ||
| 121 | case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_': | ||
| 122 | b.WriteRune(r) | ||
| 123 | case r == ' ' || r == '.': | ||
| 124 | b.WriteRune('-') | ||
| 125 | } | ||
| 126 | } | ||
| 127 | return strings.Trim(b.String(), "-") | ||
| 128 | } | ||
| 129 | |||
| 130 | // RepoURL builds the SSH URL for one repository from an instance base URL, so an | ||
| 131 | // upload lands where the job's push would. The base is the instance's address, | ||
| 132 | // which need not name a repository itself. | ||
| 133 | func RepoURL(remote, repo string) (string, error) { | ||
| 134 | u, err := url.Parse(remote) | ||
| 135 | if err != nil { | ||
| 136 | return "", err | ||
| 137 | } | ||
| 138 | if u.Scheme != "ssh" || u.Host == "" { | ||
| 139 | return "", fmt.Errorf("%q is not an ssh URL (expected ssh://user@host:port)", remote) | ||
| 140 | } | ||
| 141 | return fmt.Sprintf("%s://%s/%s.git", u.Scheme, u.Host, repo), nil | ||
| 142 | } | ||
| 143 | |||
| 144 | // RepoNameFromPath is the repository a remote path names: /demo.git -> demo. | ||
| 145 | func RepoNameFromPath(path string) string { | ||
| 146 | name := strings.TrimSuffix(strings.TrimPrefix(path, "/"), ".git") | ||
| 147 | if i := strings.IndexByte(name, '/'); i >= 0 { | ||
| 148 | name = name[i+1:] | ||
| 149 | } | ||
| 150 | return name | ||
| 151 | } | ||