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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 17m ago 4b8c9e1d
history blame raw 3.1 KiB
1package client
2
3import (
4 "fmt"
5 "io"
6 "net/url"
7 "os"
8 "os/exec"
9 "strings"
10)
11
12// AdminExitError is a remote admin command that exited non-zero. It carries the
13// code so the CLI can preserve a usage error instead of turning everything into
14// exit 1.
15type AdminExitError struct{ Code int }
16
17func (e *AdminExitError) Error() string {
18 return fmt.Sprintf("remote admin command exited with status %d", e.Code)
19}
20
21// ExitCode lets main preserve the remote command's status without printing a
22// second error over the one the server already wrote.
23func (e *AdminExitError) ExitCode() int { return e.Code }
24
25// AdminTarget turns a Git remote name or an SSH URL into the host and port the
26// admin command should connect to. Unlike a Git remote, an admin URL may name
27// only the instance: there is no repository path when you are creating one.
28func AdminTarget(gitDir, remote string) (host, port string, err error) {
29 if remote == "" {
30 remote = "origin"
31 }
32 target, err := RemoteURL(gitDir, remote)
33 if err != nil {
34 return "", "", err
35 }
36 return ParseSSHServer(target)
37}
38
39// ParseSSHServer is ParseSSHRemote without the repository requirement. It
40// accepts ssh://user@host:port[/path] and git's scp-like user@host:path form.
41func ParseSSHServer(remote string) (host, port string, err error) {
42 if !strings.Contains(remote, "://") {
43 i := strings.Index(remote, ":")
44 if i < 0 {
45 return "", "", fmt.Errorf("%q is not an SSH address", remote)
46 }
47 host = remote[:i]
48 if host == "" {
49 return "", "", fmt.Errorf("%q has no host", remote)
50 }
51 return host, "", nil
52 }
53 u, err := url.Parse(remote)
54 if err != nil {
55 return "", "", fmt.Errorf("%q is not a URL: %w", remote, err)
56 }
57 if u.Scheme != "ssh" {
58 return "", "", fmt.Errorf("%q is not an SSH URL", remote)
59 }
60 if u.Hostname() == "" {
61 return "", "", fmt.Errorf("%q has no host", remote)
62 }
63 user := u.User.Username()
64 if user == "" {
65 user = "git"
66 }
67 return user + "@" + u.Hostname(), u.Port(), nil
68}
69
70// RunAdmin runs one `vole-admin` command over SSH. Output goes straight to the
71// terminal; the server decides whether that output is human-readable text or
72// JSON, based on jsonOut.
73func RunAdmin(host, port string, args []string, jsonOut bool, stdin io.Reader) error {
74 remoteArgs := []string{"vole-admin"}
75 if jsonOut {
76 remoteArgs = append(remoteArgs, "--json")
77 }
78 remoteArgs = append(remoteArgs, args...)
79 quoted := make([]string, len(remoteArgs))
80 for i, arg := range remoteArgs {
81 quoted[i] = quoteSSHArg(arg)
82 }
83 cmd := SSHCommand(host, port, strings.Join(quoted, " "))
84 cmd.Stdin = stdin
85 cmd.Stdout = os.Stdout
86 cmd.Stderr = os.Stderr
87 if err := cmd.Run(); err != nil {
88 if ee, ok := err.(*exec.ExitError); ok {
89 return &AdminExitError{Code: ee.ExitCode()}
90 }
91 return err
92 }
93 return nil
94}
95
96// quoteSSHArg quotes one argument for the server's exec command parser. The
97// server does not use a shell, so this only has to survive splitCommand.
98func quoteSSHArg(s string) string {
99 if s != "" && !strings.ContainsAny(s, " \t\n'\"\\") {
100 return s
101 }
102 var b strings.Builder
103 b.WriteByte('"')
104 for _, r := range s {
105 if r == '\\' || r == '"' {
106 b.WriteByte('\\')
107 }
108 b.WriteRune(r)
109 }
110 b.WriteByte('"')
111 return b.String()
112}
113