v0.2.20 6938d13

quote_paste_test.go RawHistoryFolder

1package tui
2
3import (
4	"net/url"
5	"os"
6	"path/filepath"
7	"runtime"
8	"strings"
9	"testing"
10)
11
12// makeFiles creates files (and intermediate dirs) under base for each
13// relative path in rels. Returns base for convenience.
14func makeFiles(t *testing.T, base string, rels ...string) string {
15	t.Helper()
16	for _, rel := range rels {
17		full := filepath.Join(base, rel)
18		if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
19			t.Fatalf("mkdir: %v", err)
20		}
21		if err := os.WriteFile(full, []byte("x"), 0o644); err != nil {
22			t.Fatalf("write: %v", err)
23		}
24	}
25	return base
26}
27
28func TestQuotePastedFilePaths(t *testing.T) {
29	// normalisePathToken's heuristics target Unix-shaped paths
30	// (leading "/", "~", or "file://" with a "/" path component) —
31	// the same shapes drag-and-drop produces on macOS and Linux.
32	// Windows TempDir paths look like C:\Users\... which never
33	// match those prefixes, so the test fixtures wouldn't exercise
34	// any real code path on that platform.
35	if runtime.GOOS == "windows" {
36		t.Skip("path-quoting heuristics are unix-shaped")
37	}
38	dir := t.TempDir()
39	makeFiles(t, dir,
40		"foo bar.png",
41		"file.png",
42		"a.png",
43		"b.png",
44		"x.png",
45		"it's.png",
46		"only.png",
47	)
48
49	// file:// URL form needs URL-encoded spaces.
50	fileURL := "file://" + dir + "/" + url.PathEscape("foo bar.png")
51	// PathEscape doesn't escape "/" but escapes the space — that's
52	// what macOS Finder produces. Build the path-only segment so the
53	// "file://" prefix is added cleanly.
54	fileURLPath := dir + "/" + strings.ReplaceAll("foo bar.png", " ", "%20")
55	fileURLForm := "file://" + fileURLPath
56	_ = fileURL // keep linter happy if unused
57
58	cases := []struct {
59		name string
60		in   string
61		want string
62	}{
63		// macOS Terminal default: backslash-escaped spaces in path.
64		{"backslash-escaped space", dir + `/foo\ bar.png`, `'` + dir + `/foo bar.png'`},
65
66		// Plain absolute path with no special chars.
67		{"plain absolute", dir + `/file.png`, `'` + dir + `/file.png'`},
68
69		// file:// URL form; URL-decoded and scheme stripped.
70		{"file url", fileURLForm, `'` + dir + `/foo bar.png'`},
71
72		// Multi-file drop, each path quoted independently.
73		{"two paths", dir + `/a.png ` + dir + `/b.png`, `'` + dir + `/a.png' '` + dir + `/b.png'`},
74
75		// Already single-quoted: re-normalise to consistent quoting.
76		{"already single quoted", `'` + dir + `/x.png'`, `'` + dir + `/x.png'`},
77
78		// Already double-quoted: re-normalise.
79		{"already double quoted", `"` + dir + `/x.png"`, `'` + dir + `/x.png'`},
80
81		// Embedded apostrophe gets the standard '\'' splice escape.
82		{"embedded apostrophe", dir + `/it's.png`, `'` + dir + `/it'\''s.png'`},
83
84		// Plain prose left alone.
85		{"prose", `hello world`, `hello world`},
86
87		// Multi-line paste left alone (typical code paste).
88		{"multiline", "foo\nbar", "foo\nbar"},
89
90		// Anything containing shell metacharacters is left alone so a
91		// crafted "drop" can't smuggle a command.
92		{"metachar", `/foo;rm -rf /`, `/foo;rm -rf /`},
93
94		// Mixed: valid path token quoted, surrounding words preserved.
95		{"path mixed with prose", dir + `/only.png is good`, `'` + dir + `/only.png' is good`},
96
97		// URL path segment that happens to start with "/" but doesn't
98		// exist on disk: must NOT be quoted or chip-collapsed. This
99		// is the regression that motivated the existence check.
100		{"url path segment", `/de/downloads/dokumentenarchiv`, `/de/downloads/dokumentenarchiv`},
101
102		// Non-existent path that looks like a real one: also untouched.
103		{"non-existent absolute", `/this/does/not/exist.png`, `/this/does/not/exist.png`},
104	}
105	for _, c := range cases {
106		t.Run(c.name, func(t *testing.T) {
107			got := quotePastedFilePaths(c.in)
108			if got != c.want {
109				t.Errorf("input %q\n  want %q\n  got  %q", c.in, c.want, got)
110			}
111		})
112	}
113}
114
115// TestTildePathExists checks that ~ expansion in pathExists works
116// when the home directory contains the candidate. We don't write
117// into the user's real home; instead, override HOME for the test.
118func TestTildePathExists(t *testing.T) {
119	if runtime.GOOS == "windows" {
120		t.Skip("path-quoting heuristics are unix-shaped")
121	}
122	tmp := t.TempDir()
123	makeFiles(t, tmp, "tilde-test.png")
124	t.Setenv("HOME", tmp)
125
126	in := `~/tilde-test.png`
127	want := `'~/tilde-test.png'`
128	got := quotePastedFilePaths(in)
129	if got != want {
130		t.Errorf("input %q\n  want %q\n  got  %q", in, want, got)
131	}
132
133	// A tilde path that doesn't exist must be left alone.
134	in = `~/no-such-file.png`
135	got = quotePastedFilePaths(in)
136	if got != in {
137		t.Errorf("non-existent tilde path mutated: got %q want %q", got, in)
138	}
139}

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 1

Tags 476

↑↓ moveenter openesc close