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/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 }