Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 24m ago 32290114
history blame raw 4.9 KiB
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.
8package client
9
10import (
11 "fmt"
12 "net/url"
13 "os"
14 "os/exec"
15 "strings"
16)
17
18// CurrentGitDir is the clone the command is working in.
19func 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.
29func 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.
38func 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.
51func 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.
56func 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.
65func 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.
83func 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.
103func 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 person's name from git config into the shape of a Vole
116// account name.
117func SanitizeAuthor(s string) string {
118 s = strings.ToLower(strings.TrimSpace(s))
119 var b strings.Builder
120 for _, r := range s {
121 switch {
122 case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_':
123 b.WriteRune(r)
124 case r == ' ' || r == '.':
125 b.WriteRune('-')
126 }
127 }
128 return strings.Trim(b.String(), "-")
129}
130
131// RepoURL builds the SSH URL for one repository from an instance base URL, so an
132// upload lands where the job's push would. The base is the instance's address,
133// which need not name a repository itself.
134func RepoURL(remote, repo string) (string, error) {
135 u, err := url.Parse(remote)
136 if err != nil {
137 return "", err
138 }
139 if u.Scheme != "ssh" || u.Host == "" {
140 return "", fmt.Errorf("%q is not an ssh URL (expected ssh://user@host:port)", remote)
141 }
142 return fmt.Sprintf("%s://%s/%s.git", u.Scheme, u.Host, repo), nil
143}
144
145// RepoNameFromPath is the repository a remote path names: /demo.git -> demo.
146func RepoNameFromPath(path string) string {
147 name := strings.TrimSuffix(strings.TrimPrefix(path, "/"), ".git")
148 if i := strings.IndexByte(name, '/'); i >= 0 {
149 name = name[i+1:]
150 }
151 return name
152}
153