v2.31.0-dev2 4b72742

pass.go RawHistoryFolder

1package remote
2
3import (
4	"bytes"
5	"fmt"
6	"os"
7	"os/exec"
8	"regexp"
9	"strings"
10	"sync"
11
12	"github.com/bnema/gordon/internal/domain"
13)
14
15var (
16	passAvailableOnce   sync.Once
17	passAvailableResult bool
18	passWarnOnce        sync.Once
19	validRemoteName     = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
20)
21
22func validateRemoteName(name string) error {
23	if name == "" {
24		return domain.ErrEmptyRemoteName
25	}
26	if !validRemoteName.MatchString(name) {
27		return fmt.Errorf("%w (allowed: a-z, A-Z, 0-9, '.', '_', '-')", domain.ErrInvalidRemoteNameChar)
28	}
29	if strings.Contains(name, "..") {
30		return domain.ErrConsecutiveDots
31	}
32	return nil
33}
34
35func passAvailable() bool {
36	passAvailableOnce.Do(func() {
37		path, err := exec.LookPath("pass")
38		if err != nil || path == "" {
39			passAvailableResult = false
40			return
41		}
42
43		cmd := exec.Command("pass", "ls") //nolint:gosec // pass binary name is constant
44		if err := cmd.Run(); err != nil {
45			passAvailableResult = false
46			return
47		}
48
49		passAvailableResult = true
50	})
51
52	return passAvailableResult
53}
54
55func passTokenPath(name string) string {
56	return "gordon/remotes/" + name + "/token"
57}
58
59func passReadToken(name string) (string, error) {
60	if err := validateRemoteName(name); err != nil {
61		return "", err
62	}
63	if !passAvailable() {
64		return "", domain.ErrPassUnavailable
65	}
66
67	cmd := exec.Command("pass", "show", passTokenPath(name)) //nolint:gosec // pass binary name is constant; remote names are validated
68	var stdout bytes.Buffer
69	var stderr bytes.Buffer
70	cmd.Stdout = &stdout
71	cmd.Stderr = &stderr
72	if err := cmd.Run(); err != nil {
73		return "", fmt.Errorf("pass show failed: %w: %s", err, stderr.String())
74	}
75
76	return strings.TrimSpace(stdout.String()), nil
77}
78
79func passWriteToken(name, token string) error {
80	if err := validateRemoteName(name); err != nil {
81		return err
82	}
83	if !passAvailable() {
84		return domain.ErrPassUnavailable
85	}
86
87	cmd := exec.Command("pass", "insert", "--multiline", "--force", passTokenPath(name)) //nolint:gosec // pass binary name is constant; remote names are validated
88	cmd.Stdin = strings.NewReader(token + "\n")
89	var stderr bytes.Buffer
90	cmd.Stderr = &stderr
91	if err := cmd.Run(); err != nil {
92		return fmt.Errorf("pass insert failed: %w: %s", err, stderr.String())
93	}
94
95	return nil
96}
97
98func passDeleteToken(name string) error {
99	if err := validateRemoteName(name); err != nil {
100		return err
101	}
102	if !passAvailable() {
103		return nil // nothing to delete
104	}
105
106	cmd := exec.Command("pass", "rm", "-f", passTokenPath(name)) //nolint:gosec // pass binary name is constant; remote names are validated
107	var stderr bytes.Buffer
108	cmd.Stderr = &stderr
109	if err := cmd.Run(); err != nil {
110		return fmt.Errorf("pass rm failed: %w: %s", err, stderr.String())
111	}
112	return nil
113}
114
115func warnPassUnavailable() {
116	passWarnOnce.Do(func() {
117		fmt.Fprintf(os.Stderr, "Warning: 'pass' not available. Storing token in plaintext config. Consider installing pass (https://www.passwordstore.org/) for secure token storage.\n")
118	})
119}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close