A
.envrc
+1,
-0
1@@ -0,0 +1 @@
2+use flake
+4,
-0
1@@ -6,6 +6,10 @@ public/
2 testdata.site/
3 testdata.repo/
4 docs/plans/
5+.direnv/
6+update-vendor-hash
7+update-flake-lock
8+result
9 # Only ignore 'bug' at root level, not in cmd/bug/
10 /bug
11 bug-test
M
Makefile
+46,
-12
1@@ -1,21 +1,23 @@
2 REV=$(shell git rev-parse --short HEAD)
3 PROJECT="git-pgit-$(REV)"
4
5-smol:
6- curl https://pico.sh/smol.css -o ./static/smol.css
7-.PHONY: smol
8
9-clean:
10- rm -rf ./public
11-.PHONY: clean
12-
13-build:
14- go build -o pgit ./cmd/pgit
15+build: clean
16+ @if command -v nix >/dev/null 2>&1; then \
17+ echo "Building with nix..."; \
18+ nix build; \
19+ else \
20+ echo "Building with go..."; \
21+ mkdir -p result/bin; \
22+ go build -o result/bin/pgit ./cmd/pgit; \
23+ fi
24 .PHONY: build
25
26-img:
27- docker build -t neurosnap/pgit:latest .
28-.PHONY: img
29+clean:
30+ @# Remove result symlink (not the target in /nix/store)
31+ @rm -f result
32+ @rm -rf bin/
33+.PHONY: clean
34
35 fmt:
36 go fmt ./...
37@@ -29,6 +31,38 @@ test:
38 go test ./...
39 .PHONY: test
40
41+# Update flake.nix vendorHash when go.mod/go.sum changes
42+# This target depends on go.mod and go.sum, so it will only run when they change
43+update-vendor-hash: flake.nix go.mod go.sum
44+ @echo "Checking if vendorHash needs update..."
45+ @nix build 2>&1 | tee /tmp/nix-build.log; \
46+ if grep -q "hash mismatch" /tmp/nix-build.log; then \
47+ NEW_HASH=$$(grep "got:" /tmp/nix-build.log | sed 's/.*got: *//' | tail -1); \
48+ echo "Updating vendorHash to $$NEW_HASH"; \
49+ sed -i "s|vendorHash = \"sha256-[^\"]*\";|vendorHash = \"$$NEW_HASH\";|" flake.nix; \
50+ sed -i "s|vendorHash = pkgs.lib.fakeHash;|vendorHash = \"$$NEW_HASH\";|" flake.nix; \
51+ elif grep -q "error:.*fakeHash" /tmp/nix-build.log; then \
52+ NEW_HASH=$$(grep "got:" /tmp/nix-build.log | sed 's/.*got: *//' | tail -1); \
53+ echo "Updating vendorHash to $$NEW_HASH"; \
54+ sed -i "s|vendorHash = pkgs.lib.fakeHash;|vendorHash = \"$$NEW_HASH\";|" flake.nix; \
55+ else \
56+ echo "vendorHash is up to date"; \
57+ fi
58+ @rm -f /tmp/nix-build.log
59+ @touch update-vendor-hash
60+.PHONY: update-vendor-hash
61+
62+# Update flake.lock when flake.nix inputs change
63+update-flake-lock: flake.nix
64+ nix flake lock
65+ touch update-flake-lock
66+.PHONY: update-flake-lock
67+
68+# Update both vendor hash and flake lock
69+update-flake: update-vendor-hash update-flake-lock
70+ @echo "Flake updated successfully"
71+.PHONY: update-flake
72+
73 test-site:
74 mkdir -p testdata.site
75 rm -rf testdata.site/*
+37,
-54
1@@ -9,6 +9,8 @@ It will only generate a commit log and files for the provided revisions.
2
3 ## usage
4
5+### building with make
6+
7 ```bash
8 make build
9 ```
10@@ -17,6 +19,36 @@ make build
11 ./pgit --revs main --label pico --out ./public
12 ```
13
14+### building with nix flakes
15+
16+Build the project:
17+
18+```bash
19+nix build
20+```
21+
22+This creates a `result` symlink containing the `pgit` binary at `result/bin/pgit`.
23+
24+Run the binary:
25+
26+```bash
27+./result/bin/pgit --revs main --label pico --out ./public
28+```
29+
30+### development shell
31+
32+Enter a development environment with all dependencies:
33+
34+```bash
35+nix develop
36+```
37+
38+Or run a single command:
39+
40+```bash
41+nix develop --command make build
42+```
43+
44 To learn more about the options run:
45
46 ```bash
47@@ -121,61 +153,12 @@ Remove the hook:
48
49 This will restore any previous post-commit hook if one existed.
50
51-## editing issues and comments
52-
53-The `bug edit` command allows you to modify existing issues and comments.
54
55-### Edit an issue
56+## Special Thanks
57
58-Update the title, description, or both:
59-
60-```bash
61-# Open editor with current content
62-bug edit abc1234
63-
64-# Update only the title
65-bug edit abc1234 --title "New Title"
66-bug edit abc1234 -t "New Title"
67-
68-# Update only the description
69-bug edit abc1234 --message "New description"
70-bug edit abc1234 -m "New description"
71-
72-# Update both title and description
73-bug edit abc1234 -t "New Title" -m "New description"
74-```
75-
76-### Edit a comment
77-
78-```bash
79-# Open editor with current comment text
80-bug edit def5678
81-
82-# Update comment directly
83-bug edit def5678 -m "Updated comment text"
84-```
85-
86-When using the editor:
87-- For issues: edit the title on the first line, leave a blank line, then edit the description
88-- For comments: edit the comment text directly
89-- Lines starting with `;;` are ignored (instructions)
90-
91-### Agent edit command
92-
93-For automated/agent use, the `bug agent edit` command is non-interactive and
94-requires at least one of `--title` or `--message`:
95-
96-```bash
97-# Edit an issue as agent
98-bug agent edit abc1234 --title "New Title"
99-bug agent edit abc1234 --message "New description"
100-bug agent edit abc1234 --title "Title" --message "Description"
101-
102-# Edit a comment as agent (requires --message)
103-bug agent edit def5678 --message "Updated comment"
104-```
105+This project was heavily inspired by
106+[stagit](https://codemadness.org/stagit.html) and is a fork of [pgit](https://pgit.pico.sh).
107
108-## inspiration
109+## License
110
111-This project was heavily inspired by
112-[stagit](https://codemadness.org/stagit.html)
113+The pgit project was originally licensed under MIT.
+0,
-493
1@@ -1,493 +0,0 @@
2-//go:build integration
3-
4-package main
5-
6-import (
7- "os/exec"
8- "strings"
9- "testing"
10-
11- "github.com/git-bug/git-bug/entities/bug"
12- "github.com/git-bug/git-bug/entity"
13- "github.com/git-bug/git-bug/repository"
14-)
15-
16-// TestAgentCommentCommand_WithFlag adds a comment as agent using the message flag
17-func TestAgentCommentCommand_WithFlag(t *testing.T) {
18- tmpDir := t.TempDir()
19-
20- // Initialize a git repo
21- initCmd := exec.Command("git", "init")
22- initCmd.Dir = tmpDir
23- if err := initCmd.Run(); err != nil {
24- t.Fatalf("failed to init git repo: %v", err)
25- }
26-
27- // Initialize git-bug with both user and agent identities
28- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
29- t.Fatalf("failed to create user identity: %v", err)
30- }
31- if err := createIdentity(tmpDir, "agent", "", false); err != nil {
32- t.Fatalf("failed to create agent identity: %v", err)
33- }
34-
35- // Create a bug first as user
36- title := "Test Bug for Agent Comment"
37- message := "Initial bug description"
38- if err := runNew(tmpDir, title, message); err != nil {
39- t.Fatalf("runNew failed: %v", err)
40- }
41-
42- // Get the bug ID
43- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
44- if err != nil {
45- t.Fatalf("failed to open repo: %v", err)
46- }
47-
48- var bugID string
49- for streamedBug := range bug.ReadAll(repo) {
50- if streamedBug.Err != nil {
51- t.Fatalf("failed to read bug: %v", streamedBug.Err)
52- }
53- b := streamedBug.Entity
54- snap := b.Compile()
55- if snap.Title == title {
56- bugID = b.Id().String()
57- break
58- }
59- }
60- repo.Close()
61-
62- if bugID == "" {
63- t.Fatal("could not find created bug")
64- }
65-
66- // Add a comment as agent using the flag
67- commentText := "This is an automated agent comment"
68- if err := runAgentComment(tmpDir, bugID, commentText); err != nil {
69- t.Fatalf("runAgentComment failed: %v", err)
70- }
71-
72- // Verify comment was added with agent as author
73- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
74- if err != nil {
75- t.Fatalf("failed to open repo: %v", err)
76- }
77- defer repo.Close()
78-
79- b, err := bug.Read(repo, entity.Id(bugID))
80- if err != nil {
81- t.Fatalf("failed to read bug: %v", err)
82- }
83-
84- snap := b.Compile()
85- if len(snap.Comments) != 2 {
86- t.Errorf("expected 2 comments (original + new), got %d", len(snap.Comments))
87- }
88-
89- // Check the second comment (index 1) is our agent comment
90- if snap.Comments[1].Message != commentText {
91- t.Errorf("comment message = %q, want %q", snap.Comments[1].Message, commentText)
92- }
93-
94- if snap.Comments[1].Author.Name() != "agent" {
95- t.Errorf("comment author = %q, want %q", snap.Comments[1].Author.Name(), "agent")
96- }
97-
98- if snap.Comments[1].Author.Email() != "" {
99- t.Errorf("comment author email = %q, want empty", snap.Comments[1].Author.Email())
100- }
101-}
102-
103-// TestAgentCommentCommand_WithShortID adds a comment as agent using short bug ID
104-func TestAgentCommentCommand_WithShortID(t *testing.T) {
105- tmpDir := t.TempDir()
106-
107- // Initialize a git repo
108- initCmd := exec.Command("git", "init")
109- initCmd.Dir = tmpDir
110- if err := initCmd.Run(); err != nil {
111- t.Fatalf("failed to init git repo: %v", err)
112- }
113-
114- // Initialize identities
115- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
116- t.Fatalf("failed to create user identity: %v", err)
117- }
118- if err := createIdentity(tmpDir, "agent", "", false); err != nil {
119- t.Fatalf("failed to create agent identity: %v", err)
120- }
121-
122- // Create a bug
123- title := "Test Bug for Agent Short ID"
124- if err := runNew(tmpDir, title, ""); err != nil {
125- t.Fatalf("runNew failed: %v", err)
126- }
127-
128- // Get the bug ID and short ID
129- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
130- if err != nil {
131- t.Fatalf("failed to open repo: %v", err)
132- }
133-
134- var bugID, shortID string
135- for streamedBug := range bug.ReadAll(repo) {
136- if streamedBug.Err != nil {
137- t.Fatalf("failed to read bug: %v", streamedBug.Err)
138- }
139- b := streamedBug.Entity
140- snap := b.Compile()
141- if snap.Title == title {
142- bugID = b.Id().String()
143- if len(bugID) > 7 {
144- shortID = bugID[:7]
145- } else {
146- shortID = bugID
147- }
148- break
149- }
150- }
151- repo.Close()
152-
153- if shortID == "" {
154- t.Fatal("could not find created bug")
155- }
156-
157- // Add a comment as agent using short ID
158- commentText := "Agent comment via short ID"
159- if err := runAgentComment(tmpDir, shortID, commentText); err != nil {
160- t.Fatalf("runAgentComment with short ID failed: %v", err)
161- }
162-
163- // Verify comment was added
164- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
165- if err != nil {
166- t.Fatalf("failed to open repo: %v", err)
167- }
168- defer repo.Close()
169-
170- b, err := bug.Read(repo, entity.Id(bugID))
171- if err != nil {
172- t.Fatalf("failed to read bug: %v", err)
173- }
174-
175- snap := b.Compile()
176- if len(snap.Comments) != 2 {
177- t.Errorf("expected 2 comments, got %d", len(snap.Comments))
178- }
179-
180- if snap.Comments[1].Message != commentText {
181- t.Errorf("comment message = %q, want %q", snap.Comments[1].Message, commentText)
182- }
183-
184- if snap.Comments[1].Author.Name() != "agent" {
185- t.Errorf("comment author = %q, want %q", snap.Comments[1].Author.Name(), "agent")
186- }
187-}
188-
189-// TestAgentCommentCommand_MissingAgentIdentity tests error when agent not initialized
190-func TestAgentCommentCommand_MissingAgentIdentity(t *testing.T) {
191- tmpDir := t.TempDir()
192-
193- // Initialize a git repo
194- initCmd := exec.Command("git", "init")
195- initCmd.Dir = tmpDir
196- if err := initCmd.Run(); err != nil {
197- t.Fatalf("failed to init git repo: %v", err)
198- }
199-
200- // Only create user identity, NOT agent
201- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
202- t.Fatalf("failed to create user identity: %v", err)
203- }
204-
205- // Create a bug as user
206- title := "Test Bug"
207- if err := runNew(tmpDir, title, ""); err != nil {
208- t.Fatalf("runNew failed: %v", err)
209- }
210-
211- // Get the bug ID
212- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
213- if err != nil {
214- t.Fatalf("failed to open repo: %v", err)
215- }
216-
217- var bugID string
218- for streamedBug := range bug.ReadAll(repo) {
219- if streamedBug.Err != nil {
220- t.Fatalf("failed to read bug: %v", streamedBug.Err)
221- }
222- b := streamedBug.Entity
223- snap := b.Compile()
224- if snap.Title == title {
225- bugID = b.Id().String()
226- break
227- }
228- }
229- repo.Close()
230-
231- // Try to add a comment as agent (should fail)
232- err = runAgentComment(tmpDir, bugID, "Test comment")
233- if err == nil {
234- t.Error("expected error when agent identity not found, got nil")
235- }
236-
237- if !strings.Contains(err.Error(), "agent identity not found") {
238- t.Errorf("expected 'agent identity not found' error, got: %v", err)
239- }
240-}
241-
242-// TestAgentCommentCommand_EmptyMessageError tests error for empty message
243-func TestAgentCommentCommand_EmptyMessageError(t *testing.T) {
244- tmpDir := t.TempDir()
245-
246- // Initialize a git repo
247- initCmd := exec.Command("git", "init")
248- initCmd.Dir = tmpDir
249- if err := initCmd.Run(); err != nil {
250- t.Fatalf("failed to init git repo: %v", err)
251- }
252-
253- // Initialize identities
254- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
255- t.Fatalf("failed to create user identity: %v", err)
256- }
257- if err := createIdentity(tmpDir, "agent", "", false); err != nil {
258- t.Fatalf("failed to create agent identity: %v", err)
259- }
260-
261- // Create a bug
262- title := "Test Bug"
263- if err := runNew(tmpDir, title, ""); err != nil {
264- t.Fatalf("runNew failed: %v", err)
265- }
266-
267- // Get the bug ID
268- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
269- if err != nil {
270- t.Fatalf("failed to open repo: %v", err)
271- }
272-
273- var bugID string
274- for streamedBug := range bug.ReadAll(repo) {
275- if streamedBug.Err != nil {
276- t.Fatalf("failed to read bug: %v", streamedBug.Err)
277- }
278- b := streamedBug.Entity
279- snap := b.Compile()
280- if snap.Title == title {
281- bugID = b.Id().String()
282- break
283- }
284- }
285- repo.Close()
286-
287- // Try with empty message
288- err = runAgentComment(tmpDir, bugID, " ")
289- if err == nil {
290- t.Error("expected error for empty message, got nil")
291- }
292-
293- if !strings.Contains(err.Error(), "message cannot be empty") {
294- t.Errorf("expected 'message cannot be empty' error, got: %v", err)
295- }
296-}
297-
298-// TestAgentCommentCommand_InvalidBugID tests error for invalid bug ID
299-func TestAgentCommentCommand_InvalidBugID(t *testing.T) {
300- tmpDir := t.TempDir()
301-
302- // Initialize a git repo
303- initCmd := exec.Command("git", "init")
304- initCmd.Dir = tmpDir
305- if err := initCmd.Run(); err != nil {
306- t.Fatalf("failed to init git repo: %v", err)
307- }
308-
309- // Initialize identities
310- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
311- t.Fatalf("failed to create user identity: %v", err)
312- }
313- if err := createIdentity(tmpDir, "agent", "", false); err != nil {
314- t.Fatalf("failed to create agent identity: %v", err)
315- }
316-
317- // Try to add a comment to non-existent bug
318- err := runAgentComment(tmpDir, "nonexistent", "Test comment")
319- if err == nil {
320- t.Error("expected error for invalid bug ID, got nil")
321- }
322-
323- if !strings.Contains(err.Error(), "failed to resolve bug ID") {
324- t.Errorf("expected 'failed to resolve bug ID' error, got: %v", err)
325- }
326-}
327-
328-// TestAgentCommentCommand_MultipleComments tests adding multiple agent comments
329-func TestAgentCommentCommand_MultipleComments(t *testing.T) {
330- tmpDir := t.TempDir()
331-
332- // Initialize a git repo
333- initCmd := exec.Command("git", "init")
334- initCmd.Dir = tmpDir
335- if err := initCmd.Run(); err != nil {
336- t.Fatalf("failed to init git repo: %v", err)
337- }
338-
339- // Initialize identities
340- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
341- t.Fatalf("failed to create user identity: %v", err)
342- }
343- if err := createIdentity(tmpDir, "agent", "", false); err != nil {
344- t.Fatalf("failed to create agent identity: %v", err)
345- }
346-
347- // Create a bug as user
348- title := "Test Bug for Multiple Agent Comments"
349- if err := runNew(tmpDir, title, ""); err != nil {
350- t.Fatalf("runNew failed: %v", err)
351- }
352-
353- // Get the bug ID
354- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
355- if err != nil {
356- t.Fatalf("failed to open repo: %v", err)
357- }
358-
359- var bugID string
360- for streamedBug := range bug.ReadAll(repo) {
361- if streamedBug.Err != nil {
362- t.Fatalf("failed to read bug: %v", streamedBug.Err)
363- }
364- b := streamedBug.Entity
365- snap := b.Compile()
366- if snap.Title == title {
367- bugID = b.Id().String()
368- break
369- }
370- }
371- repo.Close()
372-
373- // Add multiple comments as agent
374- comments := []string{
375- "First agent comment",
376- "Second agent comment",
377- "Third agent comment with **markdown**",
378- }
379-
380- for _, comment := range comments {
381- if err := runAgentComment(tmpDir, bugID, comment); err != nil {
382- t.Fatalf("runAgentComment failed: %v", err)
383- }
384- }
385-
386- // Verify all comments were added
387- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
388- if err != nil {
389- t.Fatalf("failed to open repo: %v", err)
390- }
391- defer repo.Close()
392-
393- b, err := bug.Read(repo, entity.Id(bugID))
394- if err != nil {
395- t.Fatalf("failed to read bug: %v", err)
396- }
397-
398- snap := b.Compile()
399- // Original + 3 comments = 4 total
400- if len(snap.Comments) != 4 {
401- t.Errorf("expected 4 comments, got %d", len(snap.Comments))
402- }
403-
404- // Verify each comment (index 1-3 are our added comments)
405- for i, expected := range comments {
406- if snap.Comments[i+1].Message != expected {
407- t.Errorf("comment %d message = %q, want %q", i+1, snap.Comments[i+1].Message, expected)
408- }
409- if snap.Comments[i+1].Author.Name() != "agent" {
410- t.Errorf("comment %d author = %q, want %q", i+1, snap.Comments[i+1].Author.Name(), "agent")
411- }
412- }
413-}
414-
415-// TestAgentCommentCommand_WithMarkdown tests that markdown is preserved
416-func TestAgentCommentCommand_WithMarkdown(t *testing.T) {
417- tmpDir := t.TempDir()
418-
419- // Initialize a git repo
420- initCmd := exec.Command("git", "init")
421- initCmd.Dir = tmpDir
422- if err := initCmd.Run(); err != nil {
423- t.Fatalf("failed to init git repo: %v", err)
424- }
425-
426- // Initialize identities
427- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
428- t.Fatalf("failed to create user identity: %v", err)
429- }
430- if err := createIdentity(tmpDir, "agent", "", false); err != nil {
431- t.Fatalf("failed to create agent identity: %v", err)
432- }
433-
434- // Create a bug
435- title := "Test Bug for Markdown"
436- if err := runNew(tmpDir, title, ""); err != nil {
437- t.Fatalf("runNew failed: %v", err)
438- }
439-
440- // Get the bug ID
441- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
442- if err != nil {
443- t.Fatalf("failed to open repo: %v", err)
444- }
445-
446- var bugID string
447- for streamedBug := range bug.ReadAll(repo) {
448- if streamedBug.Err != nil {
449- t.Fatalf("failed to read bug: %v", streamedBug.Err)
450- }
451- b := streamedBug.Entity
452- snap := b.Compile()
453- if snap.Title == title {
454- bugID = b.Id().String()
455- break
456- }
457- }
458- repo.Close()
459-
460- // Add a comment with markdown
461- markdownComment := `# Analysis Report
462-
463-## Summary
464-- **Status**: PASSED
465-- **Duration**: 5m 32s
466-
467-## Details
468-All tests passed successfully.`
469-
470- if err := runAgentComment(tmpDir, bugID, markdownComment); err != nil {
471- t.Fatalf("runAgentComment failed: %v", err)
472- }
473-
474- // Verify comment was added with markdown preserved
475- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
476- if err != nil {
477- t.Fatalf("failed to open repo: %v", err)
478- }
479- defer repo.Close()
480-
481- b, err := bug.Read(repo, entity.Id(bugID))
482- if err != nil {
483- t.Fatalf("failed to read bug: %v", err)
484- }
485-
486- snap := b.Compile()
487- if len(snap.Comments) != 2 {
488- t.Errorf("expected 2 comments, got %d", len(snap.Comments))
489- }
490-
491- if snap.Comments[1].Message != markdownComment {
492- t.Errorf("comment message does not match expected markdown:\ngot:\n%s\nwant:\n%s", snap.Comments[1].Message, markdownComment)
493- }
494-}
+0,
-400
1@@ -1,400 +0,0 @@
2-//go:build integration
3-
4-package main
5-
6-import (
7- "os/exec"
8- "strings"
9- "testing"
10-
11- "github.com/git-bug/git-bug/entities/bug"
12- "github.com/git-bug/git-bug/entity"
13- "github.com/git-bug/git-bug/repository"
14-)
15-
16-// TestAgentEditCommand_EditIssue edits an issue as the agent
17-func TestAgentEditCommand_EditIssue(t *testing.T) {
18- tmpDir := t.TempDir()
19-
20- // Initialize a git repo
21- initCmd := exec.Command("git", "init")
22- initCmd.Dir = tmpDir
23- if err := initCmd.Run(); err != nil {
24- t.Fatalf("failed to init git repo: %v", err)
25- }
26- // Add origin remote for bug init
27- remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
28- remoteCmd.Dir = tmpDir
29- if err := remoteCmd.Run(); err != nil {
30- t.Fatalf("failed to add origin remote: %v", err)
31- }
32-
33- // Initialize git-bug identities (creates both user and agent)
34- if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
35- t.Fatalf("runInit failed: %v", err)
36- }
37-
38- // Create a bug as agent
39- if err := runAgentNew(tmpDir, "Original Title", "Original description"); err != nil {
40- t.Fatalf("runAgentNew failed: %v", err)
41- }
42-
43- // Get the bug ID
44- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
45- if err != nil {
46- t.Fatalf("failed to open repo: %v", err)
47- }
48-
49- var bugID string
50- for streamedBug := range bug.ReadAll(repo) {
51- if streamedBug.Err != nil {
52- t.Fatalf("failed to read bug: %v", streamedBug.Err)
53- }
54- b := streamedBug.Entity
55- snap := b.Compile()
56- if snap.Title == "Original Title" {
57- bugID = b.Id().String()
58- break
59- }
60- }
61- repo.Close()
62-
63- if bugID == "" {
64- t.Fatal("could not find created bug")
65- }
66-
67- // Edit as agent
68- newTitle := "Agent Updated Title"
69- newMessage := "Agent updated description"
70- if err := runAgentEdit(tmpDir, bugID, newTitle, newMessage); err != nil {
71- t.Fatalf("runAgentEdit failed: %v", err)
72- }
73-
74- // Verify both were updated
75- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
76- if err != nil {
77- t.Fatalf("failed to open repo: %v", err)
78- }
79- defer repo.Close()
80-
81- b, err := bug.Read(repo, entity.Id(bugID))
82- if err != nil {
83- t.Fatalf("failed to read bug: %v", err)
84- }
85-
86- snap := b.Compile()
87- if snap.Title != newTitle {
88- t.Errorf("title = %q, want %q", snap.Title, newTitle)
89- }
90- if len(snap.Comments) == 0 || snap.Comments[0].Message != newMessage {
91- t.Errorf("description = %q, want %q", snap.Comments[0].Message, newMessage)
92- }
93-}
94-
95-// TestAgentEditCommand_EditComment edits a comment as the agent
96-func TestAgentEditCommand_EditComment(t *testing.T) {
97- tmpDir := t.TempDir()
98-
99- // Initialize a git repo
100- initCmd := exec.Command("git", "init")
101- initCmd.Dir = tmpDir
102- if err := initCmd.Run(); err != nil {
103- t.Fatalf("failed to init git repo: %v", err)
104- }
105- // Add origin remote for bug init
106- remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
107- remoteCmd.Dir = tmpDir
108- if err := remoteCmd.Run(); err != nil {
109- t.Fatalf("failed to add origin remote: %v", err)
110- }
111-
112- // Initialize git-bug identities
113- if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
114- t.Fatalf("runInit failed: %v", err)
115- }
116-
117- // Create a bug as agent
118- if err := runAgentNew(tmpDir, "Test Bug", "Description"); err != nil {
119- t.Fatalf("runAgentNew failed: %v", err)
120- }
121-
122- // Get the bug ID
123- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
124- if err != nil {
125- t.Fatalf("failed to open repo: %v", err)
126- }
127-
128- var bugID string
129- for streamedBug := range bug.ReadAll(repo) {
130- if streamedBug.Err != nil {
131- t.Fatalf("failed to read bug: %v", streamedBug.Err)
132- }
133- b := streamedBug.Entity
134- snap := b.Compile()
135- if snap.Title == "Test Bug" {
136- bugID = b.Id().String()
137- break
138- }
139- }
140- repo.Close()
141-
142- // Add a comment as agent
143- originalComment := "Original agent comment"
144- if err := runAgentComment(tmpDir, bugID, originalComment); err != nil {
145- t.Fatalf("runAgentComment failed: %v", err)
146- }
147-
148- // Get the comment ID
149- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
150- if err != nil {
151- t.Fatalf("failed to open repo: %v", err)
152- }
153-
154- b, err := bug.Read(repo, entity.Id(bugID))
155- if err != nil {
156- t.Fatalf("failed to read bug: %v", err)
157- }
158- snap := b.Compile()
159- if len(snap.Comments) < 2 {
160- t.Fatal("expected at least 2 comments")
161- }
162- commentID := snap.Comments[1].CombinedId().String()
163- repo.Close()
164-
165- // Edit the comment as agent
166- newComment := "Updated agent comment"
167- if err := runAgentEdit(tmpDir, commentID, "", newComment); err != nil {
168- t.Fatalf("runAgentEdit failed: %v", err)
169- }
170-
171- // Verify comment was updated
172- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
173- if err != nil {
174- t.Fatalf("failed to open repo: %v", err)
175- }
176- defer repo.Close()
177-
178- b, err = bug.Read(repo, entity.Id(bugID))
179- if err != nil {
180- t.Fatalf("failed to read bug: %v", err)
181- }
182-
183- snap = b.Compile()
184- if len(snap.Comments) < 2 {
185- t.Fatal("expected at least 2 comments")
186- }
187- if snap.Comments[1].Message != newComment {
188- t.Errorf("comment = %q, want %q", snap.Comments[1].Message, newComment)
189- }
190-}
191-
192-// TestAgentEditCommand_RequiresMessage tests that agent edit requires at least one field
193-func TestAgentEditCommand_RequiresMessage(t *testing.T) {
194- tmpDir := t.TempDir()
195-
196- // Initialize a git repo
197- initCmd := exec.Command("git", "init")
198- initCmd.Dir = tmpDir
199- if err := initCmd.Run(); err != nil {
200- t.Fatalf("failed to init git repo: %v", err)
201- }
202- // Add origin remote for bug init
203- remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
204- remoteCmd.Dir = tmpDir
205- if err := remoteCmd.Run(); err != nil {
206- t.Fatalf("failed to add origin remote: %v", err)
207- }
208-
209- // Initialize git-bug identities
210- if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
211- t.Fatalf("runInit failed: %v", err)
212- }
213-
214- // Create a bug as agent
215- if err := runAgentNew(tmpDir, "Test Bug", "Description"); err != nil {
216- t.Fatalf("runAgentNew failed: %v", err)
217- }
218-
219- // Get the bug ID
220- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
221- if err != nil {
222- t.Fatalf("failed to open repo: %v", err)
223- }
224-
225- var bugID string
226- for streamedBug := range bug.ReadAll(repo) {
227- if streamedBug.Err != nil {
228- t.Fatalf("failed to read bug: %v", streamedBug.Err)
229- }
230- b := streamedBug.Entity
231- snap := b.Compile()
232- if snap.Title == "Test Bug" {
233- bugID = b.Id().String()
234- break
235- }
236- }
237- repo.Close()
238-
239- // Add a comment as agent
240- if err := runAgentComment(tmpDir, bugID, "Original comment"); err != nil {
241- t.Fatalf("runAgentComment failed: %v", err)
242- }
243-
244- // Get the comment ID
245- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
246- if err != nil {
247- t.Fatalf("failed to open repo: %v", err)
248- }
249-
250- b, err := bug.Read(repo, entity.Id(bugID))
251- if err != nil {
252- t.Fatalf("failed to read bug: %v", err)
253- }
254- snap := b.Compile()
255- commentID := snap.Comments[1].CombinedId().String()
256- repo.Close()
257-
258- // Try to edit comment without message
259- err = runAgentEdit(tmpDir, commentID, "", "")
260- if err == nil {
261- t.Error("expected error when editing comment without message, got nil")
262- }
263-
264- if !strings.Contains(err.Error(), "either --title or --message") {
265- t.Errorf("expected 'either --title or --message' error, got: %v", err)
266- }
267-}
268-
269-// TestAgentEditCommand_RequiresAtLeastOneField tests that agent edit requires at least one field
270-func TestAgentEditCommand_RequiresAtLeastOneField(t *testing.T) {
271- tmpDir := t.TempDir()
272-
273- // Initialize a git repo
274- initCmd := exec.Command("git", "init")
275- initCmd.Dir = tmpDir
276- if err := initCmd.Run(); err != nil {
277- t.Fatalf("failed to init git repo: %v", err)
278- }
279- // Add origin remote for bug init
280- remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
281- remoteCmd.Dir = tmpDir
282- if err := remoteCmd.Run(); err != nil {
283- t.Fatalf("failed to add origin remote: %v", err)
284- }
285-
286- // Initialize git-bug identities
287- if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
288- t.Fatalf("runInit failed: %v", err)
289- }
290-
291- // Create a bug as agent
292- if err := runAgentNew(tmpDir, "Test Bug", "Description"); err != nil {
293- t.Fatalf("runAgentNew failed: %v", err)
294- }
295-
296- // Get the bug ID
297- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
298- if err != nil {
299- t.Fatalf("failed to open repo: %v", err)
300- }
301-
302- var bugID string
303- for streamedBug := range bug.ReadAll(repo) {
304- if streamedBug.Err != nil {
305- t.Fatalf("failed to read bug: %v", streamedBug.Err)
306- }
307- b := streamedBug.Entity
308- snap := b.Compile()
309- if snap.Title == "Test Bug" {
310- bugID = b.Id().String()
311- break
312- }
313- }
314- repo.Close()
315-
316- // Try to edit without any flags
317- err = runAgentEdit(tmpDir, bugID, "", "")
318- if err == nil {
319- t.Error("expected error when editing without any fields, got nil")
320- }
321-
322- if !strings.Contains(err.Error(), "either --title or --message") {
323- t.Errorf("expected 'either --title or --message' error, got: %v", err)
324- }
325-}
326-
327-// TestAgentEditCommand_OnlyTitle edits only the title as agent
328-func TestAgentEditCommand_OnlyTitle(t *testing.T) {
329- tmpDir := t.TempDir()
330-
331- // Initialize a git repo
332- initCmd := exec.Command("git", "init")
333- initCmd.Dir = tmpDir
334- if err := initCmd.Run(); err != nil {
335- t.Fatalf("failed to init git repo: %v", err)
336- }
337- // Add origin remote for bug init
338- remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
339- remoteCmd.Dir = tmpDir
340- if err := remoteCmd.Run(); err != nil {
341- t.Fatalf("failed to add origin remote: %v", err)
342- }
343-
344- // Initialize git-bug identities
345- if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
346- t.Fatalf("runInit failed: %v", err)
347- }
348-
349- // Create a bug as agent
350- originalTitle := "Original Title"
351- originalDesc := "Original description"
352- if err := runAgentNew(tmpDir, originalTitle, originalDesc); err != nil {
353- t.Fatalf("runAgentNew failed: %v", err)
354- }
355-
356- // Get the bug ID
357- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
358- if err != nil {
359- t.Fatalf("failed to open repo: %v", err)
360- }
361-
362- var bugID string
363- for streamedBug := range bug.ReadAll(repo) {
364- if streamedBug.Err != nil {
365- t.Fatalf("failed to read bug: %v", streamedBug.Err)
366- }
367- b := streamedBug.Entity
368- snap := b.Compile()
369- if snap.Title == originalTitle {
370- bugID = b.Id().String()
371- break
372- }
373- }
374- repo.Close()
375-
376- // Edit only title as agent
377- newTitle := "New Title"
378- if err := runAgentEdit(tmpDir, bugID, newTitle, ""); err != nil {
379- t.Fatalf("runAgentEdit failed: %v", err)
380- }
381-
382- // Verify only title changed
383- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
384- if err != nil {
385- t.Fatalf("failed to open repo: %v", err)
386- }
387- defer repo.Close()
388-
389- b, err := bug.Read(repo, entity.Id(bugID))
390- if err != nil {
391- t.Fatalf("failed to read bug: %v", err)
392- }
393-
394- snap := b.Compile()
395- if snap.Title != newTitle {
396- t.Errorf("title = %q, want %q", snap.Title, newTitle)
397- }
398- if len(snap.Comments) == 0 || snap.Comments[0].Message != originalDesc {
399- t.Errorf("description was changed unexpectedly")
400- }
401-}
+0,
-233
1@@ -1,233 +0,0 @@
2-//go:build integration
3-
4-package main
5-
6-import (
7- "os/exec"
8- "strings"
9- "testing"
10-
11- "github.com/git-bug/git-bug/entities/bug"
12- "github.com/git-bug/git-bug/repository"
13-)
14-
15-// TestAgentNewCommand_WithFlags creates a bug as agent using flags
16-func TestAgentNewCommand_WithFlags(t *testing.T) {
17- tmpDir := t.TempDir()
18-
19- // Initialize a git repo
20- initCmd := exec.Command("git", "init")
21- initCmd.Dir = tmpDir
22- if err := initCmd.Run(); err != nil {
23- t.Fatalf("failed to init git repo: %v", err)
24- }
25-
26- // Initialize git-bug with both user and agent identities
27- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
28- t.Fatalf("failed to create user identity: %v", err)
29- }
30- if err := createIdentity(tmpDir, "agent", "", false); err != nil {
31- t.Fatalf("failed to create agent identity: %v", err)
32- }
33-
34- // Create a bug as agent using flags
35- title := "Agent Created Bug"
36- message := "This bug was created by an automated agent"
37- if err := runAgentNew(tmpDir, title, message); err != nil {
38- t.Fatalf("runAgentNew failed: %v", err)
39- }
40-
41- // Verify bug was created with agent as author
42- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
43- if err != nil {
44- t.Fatalf("failed to open repo: %v", err)
45- }
46- defer repo.Close()
47-
48- // Count bugs
49- bugCount := 0
50- for streamedBug := range bug.ReadAll(repo) {
51- if streamedBug.Err != nil {
52- t.Fatalf("failed to read bug: %v", streamedBug.Err)
53- }
54- b := streamedBug.Entity
55- snap := b.Compile()
56-
57- if snap.Title == title {
58- bugCount++
59- if snap.Comments[0].Message != message {
60- t.Errorf("message = %q, want %q", snap.Comments[0].Message, message)
61- }
62- if snap.Author.Name() != "agent" {
63- t.Errorf("author = %q, want %q", snap.Author.Name(), "agent")
64- }
65- if snap.Author.Email() != "" {
66- t.Errorf("author email = %q, want empty", snap.Author.Email())
67- }
68- }
69- }
70-
71- if bugCount != 1 {
72- t.Errorf("expected 1 bug with title %q, found %d", title, bugCount)
73- }
74-}
75-
76-// TestAgentNewCommand_MissingAgentIdentity tests error when agent not initialized
77-func TestAgentNewCommand_MissingAgentIdentity(t *testing.T) {
78- tmpDir := t.TempDir()
79-
80- // Initialize a git repo
81- initCmd := exec.Command("git", "init")
82- initCmd.Dir = tmpDir
83- if err := initCmd.Run(); err != nil {
84- t.Fatalf("failed to init git repo: %v", err)
85- }
86-
87- // Only create user identity, NOT agent
88- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
89- t.Fatalf("failed to create user identity: %v", err)
90- }
91-
92- // Try to create a bug as agent (should fail)
93- err := runAgentNew(tmpDir, "Title", "Message")
94- if err == nil {
95- t.Error("expected error when agent identity not found, got nil")
96- }
97-
98- if !strings.Contains(err.Error(), "agent identity not found") {
99- t.Errorf("expected 'agent identity not found' error, got: %v", err)
100- }
101-}
102-
103-// TestAgentNewCommand_EmptyTitleError tests error for empty title
104-func TestAgentNewCommand_EmptyTitleError(t *testing.T) {
105- tmpDir := t.TempDir()
106-
107- // Initialize a git repo
108- initCmd := exec.Command("git", "init")
109- initCmd.Dir = tmpDir
110- if err := initCmd.Run(); err != nil {
111- t.Fatalf("failed to init git repo: %v", err)
112- }
113-
114- // Initialize identities
115- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
116- t.Fatalf("failed to create user identity: %v", err)
117- }
118- if err := createIdentity(tmpDir, "agent", "", false); err != nil {
119- t.Fatalf("failed to create agent identity: %v", err)
120- }
121-
122- // Try with empty title
123- err := runAgentNew(tmpDir, " ", "Message")
124- if err == nil {
125- t.Error("expected error for empty title, got nil")
126- }
127-
128- if !strings.Contains(err.Error(), "title cannot be empty") {
129- t.Errorf("expected 'title cannot be empty' error, got: %v", err)
130- }
131-}
132-
133-// TestAgentNewCommand_EmptyMessageError tests error for empty message
134-func TestAgentNewCommand_EmptyMessageError(t *testing.T) {
135- tmpDir := t.TempDir()
136-
137- // Initialize a git repo
138- initCmd := exec.Command("git", "init")
139- initCmd.Dir = tmpDir
140- if err := initCmd.Run(); err != nil {
141- t.Fatalf("failed to init git repo: %v", err)
142- }
143-
144- // Initialize identities
145- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
146- t.Fatalf("failed to create user identity: %v", err)
147- }
148- if err := createIdentity(tmpDir, "agent", "", false); err != nil {
149- t.Fatalf("failed to create agent identity: %v", err)
150- }
151-
152- // Try with empty message
153- err := runAgentNew(tmpDir, "Title", " ")
154- if err == nil {
155- t.Error("expected error for empty message, got nil")
156- }
157-
158- if !strings.Contains(err.Error(), "message cannot be empty") {
159- t.Errorf("expected 'message cannot be empty' error, got: %v", err)
160- }
161-}
162-
163-// TestGetAgentIdentity_Success tests retrieving agent identity
164-func TestGetAgentIdentity_Success(t *testing.T) {
165- tmpDir := t.TempDir()
166-
167- // Initialize a git repo
168- initCmd := exec.Command("git", "init")
169- initCmd.Dir = tmpDir
170- if err := initCmd.Run(); err != nil {
171- t.Fatalf("failed to init git repo: %v", err)
172- }
173-
174- // Create user and agent identities
175- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
176- t.Fatalf("failed to create user identity: %v", err)
177- }
178- if err := createIdentity(tmpDir, "agent", "", false); err != nil {
179- t.Fatalf("failed to create agent identity: %v", err)
180- }
181-
182- // Open repo and get agent identity
183- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
184- if err != nil {
185- t.Fatalf("failed to open repo: %v", err)
186- }
187- defer repo.Close()
188-
189- agent, err := getAgentIdentity(repo)
190- if err != nil {
191- t.Fatalf("getAgentIdentity failed: %v", err)
192- }
193-
194- if agent.Name() != "agent" {
195- t.Errorf("agent name = %q, want %q", agent.Name(), "agent")
196- }
197-
198- if agent.Email() != "" {
199- t.Errorf("agent email = %q, want empty", agent.Email())
200- }
201-}
202-
203-// TestGetAgentIdentity_NotFound tests error when agent doesn't exist
204-func TestGetAgentIdentity_NotFound(t *testing.T) {
205- tmpDir := t.TempDir()
206-
207- // Initialize a git repo
208- initCmd := exec.Command("git", "init")
209- initCmd.Dir = tmpDir
210- if err := initCmd.Run(); err != nil {
211- t.Fatalf("failed to init git repo: %v", err)
212- }
213-
214- // Only create user identity
215- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
216- t.Fatalf("failed to create user identity: %v", err)
217- }
218-
219- // Open repo and try to get agent identity
220- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
221- if err != nil {
222- t.Fatalf("failed to open repo: %v", err)
223- }
224- defer repo.Close()
225-
226- _, err = getAgentIdentity(repo)
227- if err == nil {
228- t.Error("expected error when agent identity not found, got nil")
229- }
230-
231- if !strings.Contains(err.Error(), "agent identity not found") {
232- t.Errorf("expected 'agent identity not found' error, got: %v", err)
233- }
234-}
+0,
-360
1@@ -1,360 +0,0 @@
2-//go:build integration
3-
4-package main
5-
6-import (
7- "os/exec"
8- "strings"
9- "testing"
10-
11- "github.com/git-bug/git-bug/entities/bug"
12- "github.com/git-bug/git-bug/entity"
13- "github.com/git-bug/git-bug/repository"
14-)
15-
16-// TestCommentCommand_WithFlag adds a comment using the message flag
17-func TestCommentCommand_WithFlag(t *testing.T) {
18- tmpDir := t.TempDir()
19-
20- // Initialize a git repo
21- initCmd := exec.Command("git", "init")
22- initCmd.Dir = tmpDir
23- if err := initCmd.Run(); err != nil {
24- t.Fatalf("failed to init git repo: %v", err)
25- }
26-
27- // Initialize git-bug identities
28- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
29- t.Fatalf("failed to create identity: %v", err)
30- }
31-
32- // Create a bug first
33- title := "Test Bug for Comment"
34- message := "Initial bug description"
35- if err := runNew(tmpDir, title, message); err != nil {
36- t.Fatalf("runNew failed: %v", err)
37- }
38-
39- // Get the bug ID
40- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
41- if err != nil {
42- t.Fatalf("failed to open repo: %v", err)
43- }
44-
45- var bugID string
46- for streamedBug := range bug.ReadAll(repo) {
47- if streamedBug.Err != nil {
48- t.Fatalf("failed to read bug: %v", streamedBug.Err)
49- }
50- b := streamedBug.Entity
51- snap := b.Compile()
52- if snap.Title == title {
53- bugID = b.Id().String()
54- break
55- }
56- }
57- repo.Close()
58-
59- if bugID == "" {
60- t.Fatal("could not find created bug")
61- }
62-
63- // Add a comment using the flag
64- commentText := "This is a test comment"
65- if err := runComment(tmpDir, bugID, commentText); err != nil {
66- t.Fatalf("runComment failed: %v", err)
67- }
68-
69- // Verify comment was added
70- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
71- if err != nil {
72- t.Fatalf("failed to open repo: %v", err)
73- }
74- defer repo.Close()
75-
76- b, err := bug.Read(repo, entity.Id(bugID))
77- if err != nil {
78- t.Fatalf("failed to read bug: %v", err)
79- }
80-
81- snap := b.Compile()
82- if len(snap.Comments) != 2 {
83- t.Errorf("expected 2 comments (original + new), got %d", len(snap.Comments))
84- }
85-
86- // Check the second comment (index 1) is our new comment
87- if snap.Comments[1].Message != commentText {
88- t.Errorf("comment message = %q, want %q", snap.Comments[1].Message, commentText)
89- }
90-
91- if snap.Comments[1].Author.Name() != "Test User" {
92- t.Errorf("comment author = %q, want %q", snap.Comments[1].Author.Name(), "Test User")
93- }
94-}
95-
96-// TestCommentCommand_WithShortID adds a comment using a short bug ID
97-func TestCommentCommand_WithShortID(t *testing.T) {
98- tmpDir := t.TempDir()
99-
100- // Initialize a git repo
101- initCmd := exec.Command("git", "init")
102- initCmd.Dir = tmpDir
103- if err := initCmd.Run(); err != nil {
104- t.Fatalf("failed to init git repo: %v", err)
105- }
106-
107- // Initialize git-bug identities
108- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
109- t.Fatalf("failed to create identity: %v", err)
110- }
111-
112- // Create a bug
113- title := "Test Bug for Short ID"
114- if err := runNew(tmpDir, title, ""); err != nil {
115- t.Fatalf("runNew failed: %v", err)
116- }
117-
118- // Get the bug ID and short ID
119- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
120- if err != nil {
121- t.Fatalf("failed to open repo: %v", err)
122- }
123-
124- var bugID, shortID string
125- for streamedBug := range bug.ReadAll(repo) {
126- if streamedBug.Err != nil {
127- t.Fatalf("failed to read bug: %v", streamedBug.Err)
128- }
129- b := streamedBug.Entity
130- snap := b.Compile()
131- if snap.Title == title {
132- bugID = b.Id().String()
133- // Use first 7 chars as short ID
134- if len(bugID) > 7 {
135- shortID = bugID[:7]
136- } else {
137- shortID = bugID
138- }
139- break
140- }
141- }
142- repo.Close()
143-
144- if shortID == "" {
145- t.Fatal("could not find created bug")
146- }
147-
148- // Add a comment using short ID
149- commentText := "Comment added via short ID"
150- if err := runComment(tmpDir, shortID, commentText); err != nil {
151- t.Fatalf("runComment with short ID failed: %v", err)
152- }
153-
154- // Verify comment was added
155- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
156- if err != nil {
157- t.Fatalf("failed to open repo: %v", err)
158- }
159- defer repo.Close()
160-
161- b, err := bug.Read(repo, entity.Id(bugID))
162- if err != nil {
163- t.Fatalf("failed to read bug: %v", err)
164- }
165-
166- snap := b.Compile()
167- if len(snap.Comments) != 2 {
168- t.Errorf("expected 2 comments, got %d", len(snap.Comments))
169- }
170-
171- if snap.Comments[1].Message != commentText {
172- t.Errorf("comment message = %q, want %q", snap.Comments[1].Message, commentText)
173- }
174-}
175-
176-// TestCommentCommand_NoIdentityError tests error when identity not set
177-func TestCommentCommand_NoIdentityError(t *testing.T) {
178- tmpDir := t.TempDir()
179-
180- // Initialize a git repo (but no git-bug identities)
181- initCmd := exec.Command("git", "init")
182- initCmd.Dir = tmpDir
183- if err := initCmd.Run(); err != nil {
184- t.Fatalf("failed to init git repo: %v", err)
185- }
186-
187- // Don't create any identities - try to add a comment to non-existent bug
188- // Bug ID resolution happens before identity check, so we should get ID error first
189- err := runComment(tmpDir, "nonexistent123", "Test comment")
190- if err == nil {
191- t.Error("expected error, got nil")
192- }
193-
194- // Since no bugs exist at all, we should get an ID resolution error
195- if !strings.Contains(err.Error(), "failed to resolve bug ID") {
196- t.Errorf("expected 'failed to resolve bug ID' error, got: %v", err)
197- }
198-}
199-
200-// TestCommentCommand_EmptyMessageError tests error for empty message
201-func TestCommentCommand_EmptyMessageError(t *testing.T) {
202- tmpDir := t.TempDir()
203-
204- // Initialize a git repo
205- initCmd := exec.Command("git", "init")
206- initCmd.Dir = tmpDir
207- if err := initCmd.Run(); err != nil {
208- t.Fatalf("failed to init git repo: %v", err)
209- }
210-
211- // Initialize git-bug identities
212- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
213- t.Fatalf("failed to create identity: %v", err)
214- }
215-
216- // Create a bug first
217- title := "Test Bug"
218- if err := runNew(tmpDir, title, ""); err != nil {
219- t.Fatalf("runNew failed: %v", err)
220- }
221-
222- // Get the bug ID
223- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
224- if err != nil {
225- t.Fatalf("failed to open repo: %v", err)
226- }
227-
228- var bugID string
229- for streamedBug := range bug.ReadAll(repo) {
230- if streamedBug.Err != nil {
231- t.Fatalf("failed to read bug: %v", streamedBug.Err)
232- }
233- b := streamedBug.Entity
234- snap := b.Compile()
235- if snap.Title == title {
236- bugID = b.Id().String()
237- break
238- }
239- }
240- repo.Close()
241-
242- // Try to add a comment with empty message (whitespace only)
243- // This triggers the editor path, which returns "no comment provided"
244- err = runComment(tmpDir, bugID, " ")
245- if err == nil {
246- t.Error("expected error for empty message, got nil")
247- }
248-
249- // When empty message is provided, editor opens and returns "no comment provided"
250- if !strings.Contains(err.Error(), "no comment provided") {
251- t.Errorf("expected 'no comment provided' error, got: %v", err)
252- }
253-}
254-
255-// TestCommentCommand_InvalidBugID tests error for invalid bug ID
256-func TestCommentCommand_InvalidBugID(t *testing.T) {
257- tmpDir := t.TempDir()
258-
259- // Initialize a git repo
260- initCmd := exec.Command("git", "init")
261- initCmd.Dir = tmpDir
262- if err := initCmd.Run(); err != nil {
263- t.Fatalf("failed to init git repo: %v", err)
264- }
265-
266- // Initialize git-bug identities
267- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
268- t.Fatalf("failed to create identity: %v", err)
269- }
270-
271- // Try to add a comment to non-existent bug
272- err := runComment(tmpDir, "nonexistent", "Test comment")
273- if err == nil {
274- t.Error("expected error for invalid bug ID, got nil")
275- }
276-
277- if !strings.Contains(err.Error(), "failed to resolve bug ID") {
278- t.Errorf("expected 'failed to resolve bug ID' error, got: %v", err)
279- }
280-}
281-
282-// TestCommentCommand_MultipleComments tests adding multiple comments
283-func TestCommentCommand_MultipleComments(t *testing.T) {
284- tmpDir := t.TempDir()
285-
286- // Initialize a git repo
287- initCmd := exec.Command("git", "init")
288- initCmd.Dir = tmpDir
289- if err := initCmd.Run(); err != nil {
290- t.Fatalf("failed to init git repo: %v", err)
291- }
292-
293- // Initialize git-bug identities
294- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
295- t.Fatalf("failed to create identity: %v", err)
296- }
297-
298- // Create a bug
299- title := "Test Bug for Multiple Comments"
300- if err := runNew(tmpDir, title, ""); err != nil {
301- t.Fatalf("runNew failed: %v", err)
302- }
303-
304- // Get the bug ID
305- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
306- if err != nil {
307- t.Fatalf("failed to open repo: %v", err)
308- }
309-
310- var bugID string
311- for streamedBug := range bug.ReadAll(repo) {
312- if streamedBug.Err != nil {
313- t.Fatalf("failed to read bug: %v", streamedBug.Err)
314- }
315- b := streamedBug.Entity
316- snap := b.Compile()
317- if snap.Title == title {
318- bugID = b.Id().String()
319- break
320- }
321- }
322- repo.Close()
323-
324- // Add multiple comments
325- comments := []string{
326- "First comment",
327- "Second comment",
328- "Third comment with **markdown**",
329- }
330-
331- for _, comment := range comments {
332- if err := runComment(tmpDir, bugID, comment); err != nil {
333- t.Fatalf("runComment failed: %v", err)
334- }
335- }
336-
337- // Verify all comments were added
338- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
339- if err != nil {
340- t.Fatalf("failed to open repo: %v", err)
341- }
342- defer repo.Close()
343-
344- b, err := bug.Read(repo, entity.Id(bugID))
345- if err != nil {
346- t.Fatalf("failed to read bug: %v", err)
347- }
348-
349- snap := b.Compile()
350- // Original + 3 comments = 4 total
351- if len(snap.Comments) != 4 {
352- t.Errorf("expected 4 comments, got %d", len(snap.Comments))
353- }
354-
355- // Verify each comment (index 1-3 are our added comments)
356- for i, expected := range comments {
357- if snap.Comments[i+1].Message != expected {
358- t.Errorf("comment %d message = %q, want %q", i+1, snap.Comments[i+1].Message, expected)
359- }
360- }
361-}
+0,
-424
1@@ -1,424 +0,0 @@
2-//go:build integration
3-
4-package main
5-
6-import (
7- "os/exec"
8- "strings"
9- "testing"
10-
11- "github.com/git-bug/git-bug/entities/bug"
12- "github.com/git-bug/git-bug/entity"
13- "github.com/git-bug/git-bug/repository"
14-)
15-
16-// TestEditCommand_EditIssueTitle edits only the issue title
17-func TestEditCommand_EditIssueTitle(t *testing.T) {
18- tmpDir := t.TempDir()
19-
20- // Initialize a git repo
21- initCmd := exec.Command("git", "init")
22- initCmd.Dir = tmpDir
23- if err := initCmd.Run(); err != nil {
24- t.Fatalf("failed to init git repo: %v", err)
25- }
26-
27- // Initialize git-bug identities
28- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
29- t.Fatalf("failed to create identity: %v", err)
30- }
31-
32- // Create a bug
33- title := "Original Title"
34- message := "Original description"
35- if err := runNew(tmpDir, title, message); err != nil {
36- t.Fatalf("runNew failed: %v", err)
37- }
38-
39- // Get the bug ID
40- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
41- if err != nil {
42- t.Fatalf("failed to open repo: %v", err)
43- }
44-
45- var bugID string
46- for streamedBug := range bug.ReadAll(repo) {
47- if streamedBug.Err != nil {
48- t.Fatalf("failed to read bug: %v", streamedBug.Err)
49- }
50- b := streamedBug.Entity
51- snap := b.Compile()
52- if snap.Title == title {
53- bugID = b.Id().String()
54- break
55- }
56- }
57- repo.Close()
58-
59- if bugID == "" {
60- t.Fatal("could not find created bug")
61- }
62-
63- // Edit only the title
64- newTitle := "Updated Title"
65- if err := runEdit(tmpDir, bugID, newTitle, ""); err != nil {
66- t.Fatalf("runEdit failed: %v", err)
67- }
68-
69- // Verify title was updated
70- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
71- if err != nil {
72- t.Fatalf("failed to open repo: %v", err)
73- }
74- defer repo.Close()
75-
76- b, err := bug.Read(repo, entity.Id(bugID))
77- if err != nil {
78- t.Fatalf("failed to read bug: %v", err)
79- }
80-
81- snap := b.Compile()
82- if snap.Title != newTitle {
83- t.Errorf("title = %q, want %q", snap.Title, newTitle)
84- }
85- // Description should remain unchanged
86- if len(snap.Comments) == 0 || snap.Comments[0].Message != message {
87- t.Errorf("description was changed unexpectedly")
88- }
89-}
90-
91-// TestEditCommand_EditIssueDescription edits only the issue description
92-func TestEditCommand_EditIssueDescription(t *testing.T) {
93- tmpDir := t.TempDir()
94-
95- // Initialize a git repo
96- initCmd := exec.Command("git", "init")
97- initCmd.Dir = tmpDir
98- if err := initCmd.Run(); err != nil {
99- t.Fatalf("failed to init git repo: %v", err)
100- }
101-
102- // Initialize git-bug identities
103- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
104- t.Fatalf("failed to create identity: %v", err)
105- }
106-
107- // Create a bug
108- title := "Original Title"
109- message := "Original description"
110- if err := runNew(tmpDir, title, message); err != nil {
111- t.Fatalf("runNew failed: %v", err)
112- }
113-
114- // Get the bug ID
115- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
116- if err != nil {
117- t.Fatalf("failed to open repo: %v", err)
118- }
119-
120- var bugID string
121- for streamedBug := range bug.ReadAll(repo) {
122- if streamedBug.Err != nil {
123- t.Fatalf("failed to read bug: %v", streamedBug.Err)
124- }
125- b := streamedBug.Entity
126- snap := b.Compile()
127- if snap.Title == title {
128- bugID = b.Id().String()
129- break
130- }
131- }
132- repo.Close()
133-
134- if bugID == "" {
135- t.Fatal("could not find created bug")
136- }
137-
138- // Edit only the description
139- newMessage := "Updated description"
140- if err := runEdit(tmpDir, bugID, "", newMessage); err != nil {
141- t.Fatalf("runEdit failed: %v", err)
142- }
143-
144- // Verify description was updated
145- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
146- if err != nil {
147- t.Fatalf("failed to open repo: %v", err)
148- }
149- defer repo.Close()
150-
151- b, err := bug.Read(repo, entity.Id(bugID))
152- if err != nil {
153- t.Fatalf("failed to read bug: %v", err)
154- }
155-
156- snap := b.Compile()
157- // Title should remain unchanged
158- if snap.Title != title {
159- t.Errorf("title was changed unexpectedly")
160- }
161- if len(snap.Comments) == 0 || snap.Comments[0].Message != newMessage {
162- t.Errorf("description = %q, want %q", snap.Comments[0].Message, newMessage)
163- }
164-}
165-
166-// TestEditCommand_EditIssueBoth edits both title and description
167-func TestEditCommand_EditIssueBoth(t *testing.T) {
168- tmpDir := t.TempDir()
169-
170- // Initialize a git repo
171- initCmd := exec.Command("git", "init")
172- initCmd.Dir = tmpDir
173- if err := initCmd.Run(); err != nil {
174- t.Fatalf("failed to init git repo: %v", err)
175- }
176-
177- // Initialize git-bug identities
178- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
179- t.Fatalf("failed to create identity: %v", err)
180- }
181-
182- // Create a bug
183- if err := runNew(tmpDir, "Original Title", "Original description"); err != nil {
184- t.Fatalf("runNew failed: %v", err)
185- }
186-
187- // Get the bug ID
188- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
189- if err != nil {
190- t.Fatalf("failed to open repo: %v", err)
191- }
192-
193- var bugID string
194- for streamedBug := range bug.ReadAll(repo) {
195- if streamedBug.Err != nil {
196- t.Fatalf("failed to read bug: %v", streamedBug.Err)
197- }
198- b := streamedBug.Entity
199- snap := b.Compile()
200- if snap.Title == "Original Title" {
201- bugID = b.Id().String()
202- break
203- }
204- }
205- repo.Close()
206-
207- // Edit both title and description
208- newTitle := "Updated Title"
209- newMessage := "Updated description"
210- if err := runEdit(tmpDir, bugID, newTitle, newMessage); err != nil {
211- t.Fatalf("runEdit failed: %v", err)
212- }
213-
214- // Verify both were updated
215- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
216- if err != nil {
217- t.Fatalf("failed to open repo: %v", err)
218- }
219- defer repo.Close()
220-
221- b, err := bug.Read(repo, entity.Id(bugID))
222- if err != nil {
223- t.Fatalf("failed to read bug: %v", err)
224- }
225-
226- snap := b.Compile()
227- if snap.Title != newTitle {
228- t.Errorf("title = %q, want %q", snap.Title, newTitle)
229- }
230- if len(snap.Comments) == 0 || snap.Comments[0].Message != newMessage {
231- t.Errorf("description = %q, want %q", snap.Comments[0].Message, newMessage)
232- }
233-}
234-
235-// TestEditCommand_EditComment edits a comment
236-func TestEditCommand_EditComment(t *testing.T) {
237- tmpDir := t.TempDir()
238-
239- // Initialize a git repo
240- initCmd := exec.Command("git", "init")
241- initCmd.Dir = tmpDir
242- if err := initCmd.Run(); err != nil {
243- t.Fatalf("failed to init git repo: %v", err)
244- }
245-
246- // Initialize git-bug identities
247- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
248- t.Fatalf("failed to create identity: %v", err)
249- }
250-
251- // Create a bug
252- if err := runNew(tmpDir, "Test Bug", "Description"); err != nil {
253- t.Fatalf("runNew failed: %v", err)
254- }
255-
256- // Get the bug ID
257- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
258- if err != nil {
259- t.Fatalf("failed to open repo: %v", err)
260- }
261-
262- var bugID string
263- for streamedBug := range bug.ReadAll(repo) {
264- if streamedBug.Err != nil {
265- t.Fatalf("failed to read bug: %v", streamedBug.Err)
266- }
267- b := streamedBug.Entity
268- snap := b.Compile()
269- if snap.Title == "Test Bug" {
270- bugID = b.Id().String()
271- break
272- }
273- }
274- repo.Close()
275-
276- // Add a comment
277- originalComment := "Original comment text"
278- if err := runComment(tmpDir, bugID, originalComment); err != nil {
279- t.Fatalf("runComment failed: %v", err)
280- }
281-
282- // Get the comment ID
283- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
284- if err != nil {
285- t.Fatalf("failed to open repo: %v", err)
286- }
287-
288- b, err := bug.Read(repo, entity.Id(bugID))
289- if err != nil {
290- t.Fatalf("failed to read bug: %v", err)
291- }
292- snap := b.Compile()
293- if len(snap.Comments) < 2 {
294- t.Fatal("expected at least 2 comments")
295- }
296- commentID := snap.Comments[1].CombinedId().String()
297- repo.Close()
298-
299- // Edit the comment
300- newComment := "Updated comment text"
301- if err := runEdit(tmpDir, commentID, "", newComment); err != nil {
302- t.Fatalf("runEdit failed: %v", err)
303- }
304-
305- // Verify comment was updated
306- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
307- if err != nil {
308- t.Fatalf("failed to open repo: %v", err)
309- }
310- defer repo.Close()
311-
312- b, err = bug.Read(repo, entity.Id(bugID))
313- if err != nil {
314- t.Fatalf("failed to read bug: %v", err)
315- }
316-
317- snap = b.Compile()
318- if len(snap.Comments) < 2 {
319- t.Fatal("expected at least 2 comments")
320- }
321- if snap.Comments[1].Message != newComment {
322- t.Errorf("comment = %q, want %q", snap.Comments[1].Message, newComment)
323- }
324-}
325-
326-// TestEditCommand_InvalidID tests error for invalid ID
327-func TestEditCommand_InvalidID(t *testing.T) {
328- tmpDir := t.TempDir()
329-
330- // Initialize a git repo
331- initCmd := exec.Command("git", "init")
332- initCmd.Dir = tmpDir
333- if err := initCmd.Run(); err != nil {
334- t.Fatalf("failed to init git repo: %v", err)
335- }
336-
337- // Initialize git-bug identities
338- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
339- t.Fatalf("failed to create identity: %v", err)
340- }
341-
342- // Try to edit non-existent ID
343- err := runEdit(tmpDir, "nonexistent123", "Title", "Message")
344- if err == nil {
345- t.Error("expected error for invalid ID, got nil")
346- }
347-
348- if !strings.Contains(err.Error(), "failed to resolve ID") {
349- t.Errorf("expected 'failed to resolve ID' error, got: %v", err)
350- }
351-}
352-
353-// TestEditCommand_WithShortID tests editing using a short ID
354-func TestEditCommand_WithShortID(t *testing.T) {
355- tmpDir := t.TempDir()
356-
357- // Initialize a git repo
358- initCmd := exec.Command("git", "init")
359- initCmd.Dir = tmpDir
360- if err := initCmd.Run(); err != nil {
361- t.Fatalf("failed to init git repo: %v", err)
362- }
363-
364- // Initialize git-bug identities
365- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
366- t.Fatalf("failed to create identity: %v", err)
367- }
368-
369- // Create a bug
370- if err := runNew(tmpDir, "Original Title", "Original description"); err != nil {
371- t.Fatalf("runNew failed: %v", err)
372- }
373-
374- // Get the bug ID and short ID
375- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
376- if err != nil {
377- t.Fatalf("failed to open repo: %v", err)
378- }
379-
380- var bugID, shortID string
381- for streamedBug := range bug.ReadAll(repo) {
382- if streamedBug.Err != nil {
383- t.Fatalf("failed to read bug: %v", streamedBug.Err)
384- }
385- b := streamedBug.Entity
386- snap := b.Compile()
387- if snap.Title == "Original Title" {
388- bugID = b.Id().String()
389- if len(bugID) > 7 {
390- shortID = bugID[:7]
391- } else {
392- shortID = bugID
393- }
394- break
395- }
396- }
397- repo.Close()
398-
399- if shortID == "" {
400- t.Fatal("could not find created bug")
401- }
402-
403- // Edit using short ID
404- newTitle := "Updated Title"
405- if err := runEdit(tmpDir, shortID, newTitle, ""); err != nil {
406- t.Fatalf("runEdit with short ID failed: %v", err)
407- }
408-
409- // Verify title was updated
410- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
411- if err != nil {
412- t.Fatalf("failed to open repo: %v", err)
413- }
414- defer repo.Close()
415-
416- b, err := bug.Read(repo, entity.Id(bugID))
417- if err != nil {
418- t.Fatalf("failed to read bug: %v", err)
419- }
420-
421- snap := b.Compile()
422- if snap.Title != newTitle {
423- t.Errorf("title = %q, want %q", snap.Title, newTitle)
424- }
425-}
+0,
-150
1@@ -1,150 +0,0 @@
2-package main
3-
4-import (
5- "testing"
6-)
7-
8-func TestParseEditContent_IssueFormat(t *testing.T) {
9- tests := []struct {
10- name string
11- content string
12- wantTitle string
13- wantMessage string
14- wantErr bool
15- }{
16- {
17- name: "simple title and message",
18- content: "Title\n\nMessage body",
19- wantTitle: "Title",
20- wantMessage: "\nMessage body",
21- wantErr: false,
22- },
23- {
24- name: "title with comment lines",
25- content: ";; comment\nTitle\n\nMessage",
26- wantTitle: "Title",
27- wantMessage: "Message",
28- wantErr: false,
29- },
30- {
31- name: "multiline message",
32- content: "Title\n\nLine 1\nLine 2\nLine 3",
33- wantTitle: "Title",
34- wantMessage: "\nLine 1\nLine 2\nLine 3",
35- wantErr: false,
36- },
37- {
38- name: "title only no message",
39- content: "Title",
40- wantTitle: "Title",
41- wantMessage: "",
42- wantErr: false,
43- },
44- {
45- name: "empty content",
46- content: "",
47- wantTitle: "",
48- wantMessage: "",
49- wantErr: true,
50- },
51- {
52- name: "only comment lines",
53- content: ";; comment 1\n;; comment 2",
54- wantTitle: "",
55- wantMessage: "",
56- wantErr: true,
57- },
58- {
59- name: "title with leading/trailing whitespace in lines",
60- content: " Title \n\n Line 1 \n Line 2 ",
61- wantTitle: "Title",
62- wantMessage: "\n Line 1 \n Line 2 ",
63- wantErr: false,
64- },
65- }
66-
67- for _, tt := range tests {
68- t.Run(tt.name, func(t *testing.T) {
69- gotTitle, gotMessage, err := parseEditContent(tt.content, false)
70- if (err != nil) != tt.wantErr {
71- t.Errorf("parseEditContent() error = %v, wantErr %v", err, tt.wantErr)
72- return
73- }
74- if gotTitle != tt.wantTitle {
75- t.Errorf("parseEditContent() title = %q, want %q", gotTitle, tt.wantTitle)
76- }
77- if gotMessage != tt.wantMessage {
78- t.Errorf("parseEditContent() message = %q, want %q", gotMessage, tt.wantMessage)
79- }
80- })
81- }
82-}
83-
84-func TestParseEditContent_CommentFormat(t *testing.T) {
85- tests := []struct {
86- name string
87- content string
88- wantTitle string
89- wantMessage string
90- wantErr bool
91- }{
92- {
93- name: "simple comment",
94- content: "Comment text",
95- wantTitle: "",
96- wantMessage: "Comment text",
97- wantErr: false,
98- },
99- {
100- name: "multiline comment",
101- content: "Line 1\nLine 2\nLine 3",
102- wantTitle: "",
103- wantMessage: "Line 1\nLine 2\nLine 3",
104- wantErr: false,
105- },
106- {
107- name: "comment with instruction lines",
108- content: ";; instruction\nComment text",
109- wantTitle: "",
110- wantMessage: "Comment text",
111- wantErr: false,
112- },
113- {
114- name: "empty content",
115- content: "",
116- wantTitle: "",
117- wantMessage: "",
118- wantErr: true,
119- },
120- {
121- name: "only whitespace",
122- content: " \n ",
123- wantTitle: "",
124- wantMessage: "",
125- wantErr: true,
126- },
127- {
128- name: "only comment lines",
129- content: ";; comment 1\n;; comment 2",
130- wantTitle: "",
131- wantMessage: "",
132- wantErr: true,
133- },
134- }
135-
136- for _, tt := range tests {
137- t.Run(tt.name, func(t *testing.T) {
138- gotTitle, gotMessage, err := parseEditContent(tt.content, true)
139- if (err != nil) != tt.wantErr {
140- t.Errorf("parseEditContent() error = %v, wantErr %v", err, tt.wantErr)
141- return
142- }
143- if gotTitle != tt.wantTitle {
144- t.Errorf("parseEditContent() title = %q, want %q", gotTitle, tt.wantTitle)
145- }
146- if gotMessage != tt.wantMessage {
147- t.Errorf("parseEditContent() message = %q, want %q", gotMessage, tt.wantMessage)
148- }
149- })
150- }
151-}
+0,
-390
1@@ -1,390 +0,0 @@
2-//go:build integration
3-
4-package main
5-
6-import (
7- "os"
8- "os/exec"
9- "path/filepath"
10- "strings"
11- "testing"
12-
13- "github.com/git-bug/git-bug/entities/identity"
14- "github.com/git-bug/git-bug/repository"
15-)
16-
17-// setupGitRepo creates a git repo with an origin remote for testing
18-func setupGitRepo(t *testing.T) string {
19- tmpDir := t.TempDir()
20-
21- // Initialize a git repo
22- initCmd := exec.Command("git", "init")
23- initCmd.Dir = tmpDir
24- if err := initCmd.Run(); err != nil {
25- t.Fatalf("failed to init git repo: %v", err)
26- }
27- // Add origin remote for bug init
28- remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
29- remoteCmd.Dir = tmpDir
30- if err := remoteCmd.Run(); err != nil {
31- t.Fatalf("failed to add origin remote: %v", err)
32- }
33-
34- // Configure git user for the repo (needed for git operations)
35- exec.Command("git", "-C", tmpDir, "config", "user.email", "[email protected]").Run()
36- exec.Command("git", "-C", tmpDir, "config", "user.name", "Test User").Run()
37-
38- return tmpDir
39-}
40-
41-// setupGitRepoWithInit creates a git repo with origin remote and runs bug init
42-func setupGitRepoWithInit(t *testing.T) string {
43- tmpDir := setupGitRepo(t)
44-
45- // Initialize git-bug
46- if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
47- t.Fatalf("runInit failed: %v", err)
48- }
49-
50- return tmpDir
51-}
52-
53-// TestInitCommand_WithJJ tests init when jj config is available
54-func TestInitCommand_WithJJ(t *testing.T) {
55- tmpDir := setupGitRepo(t)
56-
57- // Create .jj directory and config file directly
58- jjDir := filepath.Join(tmpDir, ".jj")
59- if err := os.MkdirAll(jjDir, 0755); err != nil {
60- t.Fatalf("failed to create .jj directory: %v", err)
61- }
62-
63- // Write jj config file directly
64- configContent := `[user]
65-name = "JJ Test User"
66-email = "[email protected]"
67-`
68- configPath := filepath.Join(jjDir, "config.toml")
69- if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
70- t.Fatalf("failed to write jj config: %v", err)
71- }
72-
73- // Run init
74- err := runInit(tmpDir)
75- if err != nil {
76- t.Fatalf("runInit failed: %v", err)
77- }
78-
79- // Verify identities were created
80- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
81- if err != nil {
82- t.Fatalf("failed to open repo: %v", err)
83- }
84- defer repo.Close()
85-
86- ids, err := identity.ListLocalIds(repo)
87- if err != nil {
88- t.Fatalf("ListLocalIds failed: %v", err)
89- }
90-
91- // Should have at least 2 identities (user + agent)
92- if len(ids) < 2 {
93- t.Errorf("expected at least 2 identities, got %d", len(ids))
94- }
95-
96- // Check if user identity is set
97- isSet, err := identity.IsUserIdentitySet(repo)
98- if err != nil {
99- t.Fatalf("IsUserIdentitySet failed: %v", err)
100- }
101- if !isSet {
102- t.Error("expected user identity to be set")
103- }
104-
105- // Verify refspecs were configured
106- config, err := parseGitConfig(tmpDir)
107- if err != nil {
108- t.Fatalf("failed to parse git config: %v", err)
109- }
110-
111- originSection := `remote "origin"`
112- if _, exists := config[originSection]; !exists {
113- t.Fatal("origin remote not found in config")
114- }
115-
116- // Check fetch refspecs
117- hasBugsFetch := false
118- hasIdentitiesFetch := false
119- for _, refspec := range config[originSection]["fetch"] {
120- if refspec == "+refs/bugs/*:refs/remotes/origin/bugs/*" {
121- hasBugsFetch = true
122- }
123- if refspec == "+refs/identities/*:refs/remotes/origin/identities/*" {
124- hasIdentitiesFetch = true
125- }
126- }
127- if !hasBugsFetch {
128- t.Error("bugs fetch refspec not configured")
129- }
130- if !hasIdentitiesFetch {
131- t.Error("identities fetch refspec not configured")
132- }
133-
134- // Check push refspecs
135- hasBugsPush := false
136- hasIdentitiesPush := false
137- for _, refspec := range config[originSection]["push"] {
138- if refspec == "+refs/bugs/*:refs/bugs/*" {
139- hasBugsPush = true
140- }
141- if refspec == "+refs/identities/*:refs/identities/*" {
142- hasIdentitiesPush = true
143- }
144- }
145- if !hasBugsPush {
146- t.Error("bugs push refspec not configured")
147- }
148- if !hasIdentitiesPush {
149- t.Error("identities push refspec not configured")
150- }
151-}
152-
153-// TestInitCommand_WithoutJJ tests init without jj config (interactive mode)
154-func TestInitCommand_WithoutJJ(t *testing.T) {
155- tmpDir := setupGitRepo(t)
156-
157- // Simulate user input for interactive mode
158- input := "Interactive User\[email protected]\n"
159-
160- // Run init with simulated input
161- err := runInitWithReader(tmpDir, strings.NewReader(input))
162- if err != nil {
163- t.Fatalf("runInitWithReader failed: %v", err)
164- }
165-
166- // Verify identities were created
167- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
168- if err != nil {
169- t.Fatalf("failed to open repo: %v", err)
170- }
171- defer repo.Close()
172-
173- ids, err := identity.ListLocalIds(repo)
174- if err != nil {
175- t.Fatalf("ListLocalIds failed: %v", err)
176- }
177-
178- // Should have at least 2 identities (user + agent)
179- if len(ids) < 2 {
180- t.Errorf("expected at least 2 identities, got %d", len(ids))
181- }
182-
183- // Verify the user identity has the correct info
184- isSet, err := identity.IsUserIdentitySet(repo)
185- if err != nil {
186- t.Fatalf("IsUserIdentitySet failed: %v", err)
187- }
188- if !isSet {
189- t.Error("expected user identity to be set")
190- }
191-
192- userIdentity, err := identity.GetUserIdentity(repo)
193- if err != nil {
194- t.Fatalf("GetUserIdentity failed: %v", err)
195- }
196-
197- if userIdentity.Name() != "Interactive User" {
198- t.Errorf("expected name 'Interactive User', got %q", userIdentity.Name())
199- }
200-
201- if userIdentity.Email() != "[email protected]" {
202- t.Errorf("expected email '[email protected]', got %q", userIdentity.Email())
203- }
204-}
205-
206-// TestInitCommand_EmptyNameError tests that empty name is rejected
207-func TestInitCommand_EmptyNameError(t *testing.T) {
208- tmpDir := setupGitRepo(t)
209-
210- // Simulate empty name input
211- input := "\n"
212-
213- err := runInitWithReader(tmpDir, strings.NewReader(input))
214- if err == nil {
215- t.Error("expected error for empty name, got nil")
216- }
217-
218- if !strings.Contains(err.Error(), "name cannot be empty") {
219- t.Errorf("expected 'name cannot be empty' error, got: %v", err)
220- }
221-}
222-
223-// TestInitCommand_NoOriginRemote tests that init fails without origin remote
224-func TestInitCommand_NoOriginRemote(t *testing.T) {
225- tmpDir := t.TempDir()
226-
227- // Initialize a git repo WITHOUT origin remote
228- initCmd := exec.Command("git", "init")
229- initCmd.Dir = tmpDir
230- if err := initCmd.Run(); err != nil {
231- t.Fatalf("failed to init git repo: %v", err)
232- }
233-
234- // Configure git user
235- exec.Command("git", "-C", tmpDir, "config", "user.email", "[email protected]").Run()
236- exec.Command("git", "-C", tmpDir, "config", "user.name", "Test User").Run()
237-
238- // Create jj config to avoid interactive mode
239- jjDir := filepath.Join(tmpDir, ".jj")
240- if err := os.MkdirAll(jjDir, 0755); err != nil {
241- t.Fatalf("failed to create .jj directory: %v", err)
242- }
243- configContent := `[user]
244-name = "JJ Test User"
245-email = "[email protected]"
246-`
247- configPath := filepath.Join(jjDir, "config.toml")
248- if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
249- t.Fatalf("failed to write jj config: %v", err)
250- }
251-
252- // Run init - should fail with ErrNoOriginRemote
253- err := runInit(tmpDir)
254- if err == nil {
255- t.Fatal("expected error when no origin remote, got nil")
256- }
257-
258- // Check that it's the specific ErrNoOriginRemote type
259- if _, ok := err.(ErrNoOriginRemote); !ok {
260- t.Errorf("expected ErrNoOriginRemote, got: %T - %v", err, err)
261- }
262-}
263-
264-// TestInitCommand_IdempotentIdentities tests that running init twice doesn't duplicate identities
265-func TestInitCommand_IdempotentIdentities(t *testing.T) {
266- tmpDir := setupGitRepo(t)
267-
268- // Create jj config
269- jjDir := filepath.Join(tmpDir, ".jj")
270- if err := os.MkdirAll(jjDir, 0755); err != nil {
271- t.Fatalf("failed to create .jj directory: %v", err)
272- }
273- configContent := `[user]
274-name = "JJ Test User"
275-email = "[email protected]"
276-`
277- configPath := filepath.Join(jjDir, "config.toml")
278- if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
279- t.Fatalf("failed to write jj config: %v", err)
280- }
281-
282- // Run init first time
283- err := runInit(tmpDir)
284- if err != nil {
285- t.Fatalf("first runInit failed: %v", err)
286- }
287-
288- // Count identities after first run
289- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
290- if err != nil {
291- t.Fatalf("failed to open repo: %v", err)
292- }
293-
294- ids1, err := identity.ListLocalIds(repo)
295- if err != nil {
296- t.Fatalf("ListLocalIds failed: %v", err)
297- }
298- repo.Close()
299-
300- // Run init second time
301- err = runInit(tmpDir)
302- if err != nil {
303- t.Fatalf("second runInit failed: %v", err)
304- }
305-
306- // Count identities after second run
307- repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
308- if err != nil {
309- t.Fatalf("failed to open repo: %v", err)
310- }
311- defer repo.Close()
312-
313- ids2, err := identity.ListLocalIds(repo)
314- if err != nil {
315- t.Fatalf("ListLocalIds failed: %v", err)
316- }
317-
318- // Should have same number of identities
319- if len(ids1) != len(ids2) {
320- t.Errorf("identities count changed after second init: first=%d, second=%d", len(ids1), len(ids2))
321- }
322-
323- // Verify identities are the same
324- idMap := make(map[string]bool)
325- for _, id := range ids1 {
326- idMap[id.String()] = true
327- }
328- for _, id := range ids2 {
329- if !idMap[id.String()] {
330- t.Errorf("new identity found after second init: %s", id.String())
331- }
332- }
333-}
334-
335-// TestInitCommand_IdempotentRefspecs tests that running init twice doesn't duplicate refspecs
336-func TestInitCommand_IdempotentRefspecs(t *testing.T) {
337- tmpDir := setupGitRepo(t)
338-
339- // Create jj config
340- jjDir := filepath.Join(tmpDir, ".jj")
341- if err := os.MkdirAll(jjDir, 0755); err != nil {
342- t.Fatalf("failed to create .jj directory: %v", err)
343- }
344- configContent := `[user]
345-name = "JJ Test User"
346-email = "[email protected]"
347-`
348- configPath := filepath.Join(jjDir, "config.toml")
349- if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
350- t.Fatalf("failed to write jj config: %v", err)
351- }
352-
353- // Run init first time
354- err := runInit(tmpDir)
355- if err != nil {
356- t.Fatalf("first runInit failed: %v", err)
357- }
358-
359- // Get refspec count after first run
360- config1, err := parseGitConfig(tmpDir)
361- if err != nil {
362- t.Fatalf("failed to parse git config: %v", err)
363- }
364-
365- originSection := `remote "origin"`
366- fetchCount1 := len(config1[originSection]["fetch"])
367- pushCount1 := len(config1[originSection]["push"])
368-
369- // Run init second time
370- err = runInit(tmpDir)
371- if err != nil {
372- t.Fatalf("second runInit failed: %v", err)
373- }
374-
375- // Get refspec count after second run
376- config2, err := parseGitConfig(tmpDir)
377- if err != nil {
378- t.Fatalf("failed to parse git config: %v", err)
379- }
380-
381- fetchCount2 := len(config2[originSection]["fetch"])
382- pushCount2 := len(config2[originSection]["push"])
383-
384- // Should have same number of refspecs
385- if fetchCount1 != fetchCount2 {
386- t.Errorf("fetch refspecs count changed: first=%d, second=%d", fetchCount1, fetchCount2)
387- }
388- if pushCount1 != pushCount2 {
389- t.Errorf("push refspecs count changed: first=%d, second=%d", pushCount1, pushCount2)
390- }
391-}
+0,
-2544
1@@ -1,2544 +0,0 @@
2-package main
3-
4-import (
5- "bufio"
6- "fmt"
7- "io"
8- "os"
9- "os/exec"
10- "path/filepath"
11- "sort"
12- "strings"
13- "time"
14-
15- "github.com/charmbracelet/lipgloss"
16- "github.com/dustin/go-humanize"
17- "github.com/git-bug/git-bug/entities/bug"
18- "github.com/git-bug/git-bug/entities/identity"
19- "github.com/git-bug/git-bug/entity"
20- "github.com/git-bug/git-bug/repository"
21- "github.com/spf13/cobra"
22-)
23-
24-// BugIssue represents a git-bug issue for display
25-type BugIssue struct {
26- FullID string
27- ShortID string
28- Title string
29- Labels []string
30- CreatedAt time.Time
31- Status string
32-}
33-
34-// LoadBugs loads all issues from the git-bug repository
35-func LoadBugs(repoPath string) ([]BugIssue, error) {
36- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
37- if err != nil {
38- return nil, fmt.Errorf("failed to open repository: %w", err)
39- }
40-
41- // Generate short IDs for all issues
42- gen := NewShortIDGenerator(repoPath)
43- shortIDMap, err := gen.Generate()
44- if err != nil {
45- return nil, fmt.Errorf("failed to generate short IDs: %w", err)
46- }
47-
48- var issues []BugIssue
49-
50- for streamedBug := range bug.ReadAll(repo) {
51- if streamedBug.Err != nil {
52- continue // Skip errors, process what we can
53- }
54-
55- b := streamedBug.Entity
56- snap := b.Compile()
57-
58- fullID := b.Id().String()
59- shortID, _ := shortIDMap.GetShortID(fullID)
60-
61- labels := make([]string, len(snap.Labels))
62- for i, label := range snap.Labels {
63- labels[i] = string(label)
64- }
65-
66- issues = append(issues, BugIssue{
67- FullID: fullID,
68- ShortID: shortID,
69- Title: snap.Title,
70- Labels: labels,
71- CreatedAt: snap.CreateTime,
72- Status: snap.Status.String(),
73- })
74- }
75-
76- return issues, nil
77-}
78-
79-// FilterSpec represents a filter criteria
80-type FilterSpec struct {
81- Labels []string // List of labels that must all be present
82- AgeOp string // "<" or ">"
83- AgeValue time.Duration // The duration value for age comparison
84-}
85-
86-// ParseFilter parses a filter string like "label:bug" or "age:<10d"
87-func ParseFilter(filterStr string) (*FilterSpec, error) {
88- if filterStr == "" {
89- return nil, nil
90- }
91-
92- spec := &FilterSpec{}
93-
94- // Handle label filter: label:value or label:value1,value2,...
95- if strings.HasPrefix(filterStr, "label:") {
96- labelStr := strings.TrimPrefix(filterStr, "label:")
97- // Split by comma to support multiple labels
98- labels := strings.Split(labelStr, ",")
99- // Trim whitespace from each label
100- for i, label := range labels {
101- labels[i] = strings.TrimSpace(label)
102- }
103- spec.Labels = labels
104- return spec, nil
105- }
106-
107- // Handle age filter: age:<duration>, age:>duration, or age:duration
108- if strings.HasPrefix(filterStr, "age:") {
109- ageStr := strings.TrimPrefix(filterStr, "age:")
110-
111- // Check for operator
112- spec.AgeOp = "<" // Default operator
113- if strings.HasPrefix(ageStr, "<") {
114- ageStr = strings.TrimPrefix(ageStr, "<")
115- } else if strings.HasPrefix(ageStr, ">") {
116- spec.AgeOp = ">"
117- ageStr = strings.TrimPrefix(ageStr, ">")
118- }
119-
120- // Parse duration like "10d", "1h", etc.
121- duration, err := time.ParseDuration(ageStr)
122- if err != nil {
123- // Try to parse days (e.g., "10d" -> "240h")
124- if strings.HasSuffix(ageStr, "d") {
125- daysStr := strings.TrimSuffix(ageStr, "d")
126- var days int
127- if _, err := fmt.Sscanf(daysStr, "%d", &days); err == nil {
128- duration = time.Duration(days) * 24 * time.Hour
129- } else {
130- return nil, fmt.Errorf("invalid age filter: %s", filterStr)
131- }
132- } else {
133- return nil, fmt.Errorf("invalid age filter: %s", filterStr)
134- }
135- }
136- spec.AgeValue = duration
137- return spec, nil
138- }
139-
140- return nil, fmt.Errorf("invalid filter format: %s", filterStr)
141-}
142-
143-// ApplyFilter filters issues based on the filter spec
144-func ApplyFilter(issues []BugIssue, spec *FilterSpec) []BugIssue {
145- if spec == nil {
146- return issues
147- }
148-
149- var filtered []BugIssue
150- now := time.Now()
151-
152- for _, issue := range issues {
153- // Check label filter - must have ALL specified labels
154- if len(spec.Labels) > 0 {
155- hasAllLabels := true
156- for _, requiredLabel := range spec.Labels {
157- found := false
158- for _, label := range issue.Labels {
159- if label == requiredLabel {
160- found = true
161- break
162- }
163- }
164- if !found {
165- hasAllLabels = false
166- break
167- }
168- }
169- if !hasAllLabels {
170- continue
171- }
172- }
173-
174- // Check age filter
175- if spec.AgeValue > 0 {
176- age := now.Sub(issue.CreatedAt)
177- if spec.AgeOp == "<" {
178- // Bugs newer than AgeValue
179- if age > spec.AgeValue {
180- continue
181- }
182- } else if spec.AgeOp == ">" {
183- // Bugs older than AgeValue
184- if age < spec.AgeValue {
185- continue
186- }
187- }
188- }
189-
190- filtered = append(filtered, issue)
191- }
192-
193- return filtered
194-}
195-
196-// SortSpec represents sort criteria
197-type SortSpec struct {
198- Field string // "id" or "age"
199- Direction string // "asc" or "desc"
200-}
201-
202-// ParseSort parses a sort string like "id:asc" or "age:desc"
203-func ParseSort(sortStr string) (*SortSpec, error) {
204- if sortStr == "" {
205- // Default: age:desc (newest first)
206- return &SortSpec{Field: "age", Direction: "desc"}, nil
207- }
208-
209- parts := strings.Split(sortStr, ":")
210- if len(parts) != 2 {
211- return nil, fmt.Errorf("invalid sort format: %s (expected field:direction)", sortStr)
212- }
213-
214- field := strings.ToLower(parts[0])
215- direction := strings.ToLower(parts[1])
216-
217- if field != "id" && field != "age" {
218- return nil, fmt.Errorf("invalid sort field: %s (expected 'id' or 'age')", field)
219- }
220-
221- if direction != "asc" && direction != "desc" {
222- return nil, fmt.Errorf("invalid sort direction: %s (expected 'asc' or 'desc')", direction)
223- }
224-
225- return &SortSpec{Field: field, Direction: direction}, nil
226-}
227-
228-// ApplySort sorts issues based on the sort spec
229-func ApplySort(issues []BugIssue, spec *SortSpec) {
230- if spec == nil {
231- return
232- }
233-
234- sort.Slice(issues, func(i, j int) bool {
235- var less bool
236-
237- switch spec.Field {
238- case "id":
239- less = issues[i].FullID < issues[j].FullID
240- case "age":
241- less = issues[i].CreatedAt.After(issues[j].CreatedAt) // Newer first
242- }
243-
244- if spec.Direction == "desc" {
245- return !less
246- }
247- return less
248- })
249-}
250-
251-// formatID renders the ID with the short portion in purple
252-// Shows at least 7 characters, with the short ID portion colored
253-func formatID(fullID, shortID string) string {
254- purpleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#9932CC")) // Purple
255- normalStyle := lipgloss.NewStyle()
256-
257- // Always show at least 7 characters
258- displayLen := 7
259- if len(shortID) > displayLen {
260- displayLen = len(shortID)
261- }
262- if displayLen > len(fullID) {
263- displayLen = len(fullID)
264- }
265-
266- shortLen := len(shortID)
267- if shortLen > displayLen {
268- shortLen = displayLen
269- }
270-
271- return purpleStyle.Render(fullID[:shortLen]) + normalStyle.Render(fullID[shortLen:displayLen])
272-}
273-
274-// stripANSI removes ANSI escape codes from a string
275-func stripANSI(s string) string {
276- result := make([]rune, 0, len(s))
277- inEscape := false
278- for _, r := range s {
279- if inEscape {
280- if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
281- inEscape = false
282- }
283- continue
284- }
285- if r == '\x1b' {
286- inEscape = true
287- continue
288- }
289- result = append(result, r)
290- }
291- return string(result)
292-}
293-
294-// printTable prints a simple text table with styled output
295-// Columns are dynamically sized with at least 2 spaces between them
296-func printTable(issues []BugIssue) error {
297- // First pass: calculate column widths based on content
298- const minColSpacing = 2
299- const maxSummaryLen = 70
300-
301- idWidth := len("ID")
302- summaryWidth := len("Summary")
303- labelsWidth := len("Labels")
304- ageWidth := len("Age")
305-
306- // Pre-calculate all formatted values and widths
307- type rowData struct {
308- id string
309- summary string
310- labels string
311- age string
312- }
313-
314- rows := make([]rowData, len(issues))
315-
316- for i, issue := range issues {
317- // Format ID (with ANSI codes)
318- id := formatID(issue.FullID, issue.ShortID)
319- idVisualLen := len(stripANSI(id))
320- if idVisualLen > idWidth {
321- idWidth = idVisualLen
322- }
323-
324- // Format summary (truncate at 70 chars)
325- summary := issue.Title
326- if len(summary) > maxSummaryLen {
327- summary = summary[:maxSummaryLen-3] + "..."
328- }
329- if len(summary) > summaryWidth {
330- summaryWidth = len(summary)
331- }
332-
333- // Format labels
334- labels := strings.Join(issue.Labels, ", ")
335- if labels == "" {
336- labels = "-"
337- }
338- if len(labels) > labelsWidth {
339- labelsWidth = len(labels)
340- }
341-
342- // Format age
343- age := humanize.Time(issue.CreatedAt)
344- if len(age) > ageWidth {
345- ageWidth = len(age)
346- }
347-
348- rows[i] = rowData{id: id, summary: summary, labels: labels, age: age}
349- }
350-
351- // Print header
352- fmt.Printf("%s%s%s%s%s%s%s\n",
353- padRight("ID", idWidth),
354- strings.Repeat(" ", minColSpacing),
355- padRight("Summary", summaryWidth),
356- strings.Repeat(" ", minColSpacing),
357- padRight("Labels", labelsWidth),
358- strings.Repeat(" ", minColSpacing),
359- padRight("Age", ageWidth),
360- )
361-
362- // Print separator
363- totalWidth := idWidth + minColSpacing + summaryWidth + minColSpacing + labelsWidth + minColSpacing + ageWidth
364- fmt.Println(strings.Repeat("-", totalWidth))
365-
366- // Print rows
367- for _, row := range rows {
368- fmt.Printf("%s%s%s%s%s%s%s\n",
369- padRight(row.id, idWidth),
370- strings.Repeat(" ", minColSpacing),
371- padRight(row.summary, summaryWidth),
372- strings.Repeat(" ", minColSpacing),
373- padRight(row.labels, labelsWidth),
374- strings.Repeat(" ", minColSpacing),
375- padRight(row.age, ageWidth),
376- )
377- }
378-
379- return nil
380-}
381-
382-// padRight pads a string to the specified width, accounting for ANSI codes
383-func padRight(s string, width int) string {
384- visualLen := len(stripANSI(s))
385- padding := width - visualLen
386- if padding < 0 {
387- padding = 0
388- }
389- return s + strings.Repeat(" ", padding)
390-}
391-
392-// promptUser prompts the user for input with the given message
393-// It reads from stdin and returns the trimmed input
394-func promptUser(prompt string) (string, error) {
395- fmt.Print(prompt)
396- scanner := bufio.NewScanner(os.Stdin)
397- if scanner.Scan() {
398- return strings.TrimSpace(scanner.Text()), nil
399- }
400- if err := scanner.Err(); err != nil {
401- return "", fmt.Errorf("failed to read input: %w", err)
402- }
403- return "", nil
404-}
405-
406-// invalidateGitBugCache deletes the git-bug cache directories to force a rebuild.
407-// This ensures the `bug` command and `git-bug` binary stay in sync when either
408-// is used to modify issues or identities. Failures are silent (best effort).
409-func invalidateGitBugCache(repoPath string) {
410- gitBugPath := filepath.Join(repoPath, ".git", "git-bug")
411-
412- // Remove cache directory (serialized excerpts)
413- cachePath := filepath.Join(gitBugPath, "cache")
414- _ = os.RemoveAll(cachePath)
415-
416- // Remove indexes directory (bleve search indexes)
417- indexesPath := filepath.Join(gitBugPath, "indexes")
418- _ = os.RemoveAll(indexesPath)
419-}
420-
421-// promptUserWithScanner prompts the user using the provided scanner
422-// It prints the prompt and returns the trimmed input
423-func promptUserWithScanner(prompt string, scanner *bufio.Scanner) (string, error) {
424- fmt.Print(prompt)
425- if scanner.Scan() {
426- return strings.TrimSpace(scanner.Text()), nil
427- }
428- if err := scanner.Err(); err != nil {
429- return "", fmt.Errorf("failed to read input: %w", err)
430- }
431- return "", nil
432-}
433-
434-// getEditor returns the editor command to use for interactive editing
435-// Checks $EDITOR environment variable, falls back to 'vi' or 'notepad'
436-func getEditor() string {
437- if editor := os.Getenv("EDITOR"); editor != "" {
438- return editor
439- }
440- // Default editors based on OS
441- if os.PathSeparator == '\\' {
442- return "notepad"
443- }
444- return "vi"
445-}
446-
447-// launchEditorWithTemplate opens the user's preferred editor with a custom template
448-// Returns the content written by the user
449-func launchEditorWithTemplate(template string) (string, error) {
450- // Create temporary file
451- tmpFile, err := os.CreateTemp("", "bug-edit-*.txt")
452- if err != nil {
453- return "", fmt.Errorf("failed to create temp file: %w", err)
454- }
455- tmpPath := tmpFile.Name()
456- defer os.Remove(tmpPath)
457-
458- if _, err := tmpFile.WriteString(template); err != nil {
459- tmpFile.Close()
460- return "", fmt.Errorf("failed to write template: %w", err)
461- }
462- tmpFile.Close()
463-
464- // Launch editor
465- editor := getEditor()
466- cmd := exec.Command(editor, tmpPath)
467- cmd.Stdin = os.Stdin
468- cmd.Stdout = os.Stdout
469- cmd.Stderr = os.Stderr
470-
471- if err := cmd.Run(); err != nil {
472- return "", fmt.Errorf("editor failed: %w", err)
473- }
474-
475- // Read the result
476- content, err := os.ReadFile(tmpPath)
477- if err != nil {
478- return "", fmt.Errorf("failed to read edited file: %w", err)
479- }
480-
481- return string(content), nil
482-}
483-
484-// launchEditor opens the user's preferred editor with a temporary file for new issues
485-// Returns the content written by the user
486-func launchEditor(initialContent string) (string, error) {
487- // Write initial content with instructions
488- template := `;; Enter your issue title on the first line (required)
489-;; Enter the description below the title
490-;; Lines starting with ;; will be ignored
491-;; Save and close the editor when done
492-;;
493-` + initialContent
494-
495- return launchEditorWithTemplate(template)
496-}
497-
498-// parseEditorContent parses editor output into title and message
499-// First non-empty line is title, rest is message
500-// Lines starting with ;; are treated as comments and ignored
501-func parseEditorContent(content string) (title, message string, err error) {
502- lines := strings.Split(content, "\n")
503- var nonCommentLines []string
504- commentsFiltered := false
505-
506- // Filter out comment lines and collect non-empty lines
507- for _, line := range lines {
508- trimmed := strings.TrimSpace(line)
509- if strings.HasPrefix(trimmed, ";;") {
510- commentsFiltered = true
511- continue
512- }
513- nonCommentLines = append(nonCommentLines, line)
514- }
515-
516- // Find first non-empty line for title
517- titleIdx := -1
518- for i, line := range nonCommentLines {
519- if strings.TrimSpace(line) != "" {
520- title = strings.TrimSpace(line)
521- titleIdx = i
522- break
523- }
524- }
525-
526- if titleIdx == -1 {
527- return "", "", fmt.Errorf("no title provided")
528- }
529-
530- // Rest is the message (preserve original formatting)
531- if titleIdx+1 < len(nonCommentLines) {
532- messageLines := nonCommentLines[titleIdx+1:]
533- startIdx := 0
534- // Trim leading empty lines only if comments were filtered
535- if commentsFiltered {
536- for startIdx < len(messageLines) && strings.TrimSpace(messageLines[startIdx]) == "" {
537- startIdx++
538- }
539- }
540- // Trim trailing empty lines from message
541- endIdx := len(messageLines)
542- for endIdx > startIdx && strings.TrimSpace(messageLines[endIdx-1]) == "" {
543- endIdx--
544- }
545- message = strings.Join(messageLines[startIdx:endIdx], "\n")
546- }
547-
548- return title, message, nil
549-}
550-
551-// parseEditContent parses editor output for edit operations
552-// It handles two formats:
553-// 1. Issue format: title on first line, blank line, then description
554-// 2. Comment format: single message (when parsing comment content)
555-// For issue format, returns title and message
556-// For comment format, set isComment=true to skip title parsing
557-func parseEditContent(content string, isComment bool) (title, message string, err error) {
558- lines := strings.Split(content, "\n")
559- var nonCommentLines []string
560- commentsFiltered := false
561-
562- // Filter out comment lines and collect non-empty lines
563- for _, line := range lines {
564- trimmed := strings.TrimSpace(line)
565- if strings.HasPrefix(trimmed, ";;") {
566- commentsFiltered = true
567- continue
568- }
569- nonCommentLines = append(nonCommentLines, line)
570- }
571-
572- if isComment {
573- // Comment format: just extract the message
574- startIdx := 0
575- for startIdx < len(nonCommentLines) && strings.TrimSpace(nonCommentLines[startIdx]) == "" {
576- startIdx++
577- }
578- if startIdx >= len(nonCommentLines) {
579- return "", "", fmt.Errorf("no content provided")
580- }
581- endIdx := len(nonCommentLines)
582- for endIdx > startIdx && strings.TrimSpace(nonCommentLines[endIdx-1]) == "" {
583- endIdx--
584- }
585- message = strings.Join(nonCommentLines[startIdx:endIdx], "\n")
586- return "", message, nil
587- }
588-
589- // Issue format: title on first line, description follows
590- // Find first non-empty line for title
591- titleIdx := -1
592- for i, line := range nonCommentLines {
593- if strings.TrimSpace(line) != "" {
594- title = strings.TrimSpace(line)
595- titleIdx = i
596- break
597- }
598- }
599-
600- if titleIdx == -1 {
601- return "", "", fmt.Errorf("no title provided")
602- }
603-
604- // Rest is the message (preserve original formatting)
605- if titleIdx+1 < len(nonCommentLines) {
606- messageLines := nonCommentLines[titleIdx+1:]
607- startIdx := 0
608- // Trim leading empty lines only if comments were filtered
609- if commentsFiltered {
610- for startIdx < len(messageLines) && strings.TrimSpace(messageLines[startIdx]) == "" {
611- startIdx++
612- }
613- }
614- // Trim trailing empty lines from message
615- endIdx := len(messageLines)
616- for endIdx > startIdx && strings.TrimSpace(messageLines[endIdx-1]) == "" {
617- endIdx--
618- }
619- message = strings.Join(messageLines[startIdx:endIdx], "\n")
620- }
621-
622- return title, message, nil
623-}
624-
625-// detectJJConfig attempts to get user.name and user.email from jj config
626-// Returns empty strings if .jj directory doesn't exist or config is not found
627-// Note: We read the config file directly because 'jj config get' walks up the
628-// directory tree and may find parent repo configs, which is not what we want.
629-func detectJJConfig(repoPath string) (name, email string, err error) {
630- configPath := filepath.Join(repoPath, ".jj", "config.toml")
631-
632- // Check if the config file exists
633- if _, err := os.Stat(configPath); os.IsNotExist(err) {
634- return "", "", nil
635- }
636-
637- // Read the config file
638- content, err := os.ReadFile(configPath)
639- if err != nil {
640- return "", "", nil
641- }
642-
643- // Parse the TOML content line by line to extract user.name and user.email
644- // We do simple parsing to avoid adding a TOML dependency
645- lines := strings.Split(string(content), "\n")
646- inUserSection := false
647-
648- for _, line := range lines {
649- trimmed := strings.TrimSpace(line)
650-
651- // Check if we're entering the [user] section
652- if trimmed == "[user]" {
653- inUserSection = true
654- continue
655- }
656-
657- // Check if we're leaving the [user] section (new section starts)
658- if strings.HasPrefix(trimmed, "[") && trimmed != "[user]" {
659- inUserSection = false
660- continue
661- }
662-
663- // Extract name and email from the user section
664- if inUserSection {
665- if strings.HasPrefix(trimmed, "name") {
666- parts := strings.SplitN(trimmed, "=", 2)
667- if len(parts) == 2 {
668- name = strings.Trim(strings.TrimSpace(parts[1]), `"`)
669- }
670- }
671- if strings.HasPrefix(trimmed, "email") {
672- parts := strings.SplitN(trimmed, "=", 2)
673- if len(parts) == 2 {
674- email = strings.Trim(strings.TrimSpace(parts[1]), `"`)
675- }
676- }
677- }
678- }
679-
680- return name, email, nil
681-}
682-
683-// createIdentity creates a new git-bug identity with the given name and email
684-// If setAsUser is true, the identity will be set as the current user identity
685-func createIdentity(repoPath, name, email string, setAsUser bool) error {
686- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
687- if err != nil {
688- return fmt.Errorf("failed to open repository: %w", err)
689- }
690- defer repo.Close()
691-
692- newIdentity, err := identity.NewIdentity(repo, name, email)
693- if err != nil {
694- return fmt.Errorf("failed to create identity: %w", err)
695- }
696-
697- if err := newIdentity.Commit(repo); err != nil {
698- return fmt.Errorf("failed to commit identity: %w", err)
699- }
700-
701- if setAsUser {
702- if err := identity.SetUserIdentity(repo, newIdentity); err != nil {
703- return fmt.Errorf("failed to set user identity: %w", err)
704- }
705- }
706-
707- return nil
708-}
709-
710-// getAgentIdentity retrieves the agent identity from the repository
711-// The agent identity is created during 'bug init' with name "agent" and empty email
712-func getAgentIdentity(repo repository.Repo) (*identity.Identity, error) {
713- // List all local identities
714- ids, err := identity.ListLocalIds(repo)
715- if err != nil {
716- return nil, fmt.Errorf("failed to list identities: %w", err)
717- }
718-
719- // Find the identity with name "agent"
720- for _, id := range ids {
721- i, err := identity.ReadLocal(repo, id)
722- if err != nil {
723- continue // Skip identities we can't read
724- }
725- if i.Name() == "agent" {
726- return i, nil
727- }
728- }
729-
730- return nil, fmt.Errorf("agent identity not found. Run 'bug init' first")
731-}
732-
733-// checkIdentitiesExist checks if user and agent identities already exist
734-// Returns (userExists, agentExists, error)
735-func checkIdentitiesExist(repoPath string) (bool, bool, error) {
736- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
737- if err != nil {
738- return false, false, fmt.Errorf("failed to open repository: %w", err)
739- }
740- defer repo.Close()
741-
742- // Check if user identity is set
743- userExists, err := identity.IsUserIdentitySet(repo)
744- if err != nil {
745- return false, false, fmt.Errorf("failed to check user identity: %w", err)
746- }
747-
748- // Check if agent identity exists
749- agentExists := false
750- if userExists {
751- _, err := getAgentIdentity(repo)
752- if err == nil {
753- agentExists = true
754- }
755- }
756-
757- return userExists, agentExists, nil
758-}
759-
760-// parseGitConfig reads and parses the git config file
761-// Returns a map of section names to their key-value pairs
762-// For sections with multiple values (like refspec), values are stored as a slice
763-func parseGitConfig(repoPath string) (map[string]map[string][]string, error) {
764- configPath := filepath.Join(repoPath, ".git", "config")
765-
766- data, err := os.ReadFile(configPath)
767- if err != nil {
768- if os.IsNotExist(err) {
769- return make(map[string]map[string][]string), nil
770- }
771- return nil, fmt.Errorf("failed to read git config: %w", err)
772- }
773-
774- config := make(map[string]map[string][]string)
775- var currentSection string
776-
777- lines := strings.Split(string(data), "\n")
778- for _, line := range lines {
779- trimmed := strings.TrimSpace(line)
780-
781- // Skip empty lines and comments
782- if trimmed == "" || strings.HasPrefix(trimmed, "#") {
783- continue
784- }
785-
786- // Check for section header like [remote "origin"]
787- if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
788- sectionContent := trimmed[1 : len(trimmed)-1]
789- currentSection = sectionContent
790- if _, exists := config[currentSection]; !exists {
791- config[currentSection] = make(map[string][]string)
792- }
793- continue
794- }
795-
796- // Parse key-value pairs within a section
797- if currentSection != "" && strings.Contains(trimmed, "=") {
798- parts := strings.SplitN(trimmed, "=", 2)
799- if len(parts) == 2 {
800- key := strings.TrimSpace(parts[0])
801- value := strings.TrimSpace(parts[1])
802- config[currentSection][key] = append(config[currentSection][key], value)
803- }
804- }
805- }
806-
807- return config, nil
808-}
809-
810-// writeGitConfig writes the git config back to file
811-func writeGitConfig(repoPath string, config map[string]map[string][]string) error {
812- configPath := filepath.Join(repoPath, ".git", "config")
813-
814- var buf strings.Builder
815-
816- for section, values := range config {
817- buf.WriteString("[" + section + "]\n")
818- for key, vals := range values {
819- for _, val := range vals {
820- buf.WriteString("\t" + key + " = " + val + "\n")
821- }
822- }
823- buf.WriteString("\n")
824- }
825-
826- if err := os.WriteFile(configPath, []byte(buf.String()), 0644); err != nil {
827- return fmt.Errorf("failed to write git config: %w", err)
828- }
829-
830- return nil
831-}
832-
833-// hasRefspec checks if a refspec is already configured
834-func hasRefspec(config map[string][]string, refspec string) bool {
835- for _, existing := range config["fetch"] {
836- if existing == refspec {
837- return true
838- }
839- }
840- for _, existing := range config["push"] {
841- if existing == refspec {
842- return true
843- }
844- }
845- return false
846-}
847-
848-// configureOriginRefspecs adds bug and identity refspecs to origin remote if not present
849-// Returns true if any changes were made, false if all refspecs already exist
850-func configureOriginRefspecs(repoPath string) (bool, error) {
851- config, err := parseGitConfig(repoPath)
852- if err != nil {
853- return false, err
854- }
855-
856- // Check if origin remote exists
857- originSection := `remote "origin"`
858- if _, exists := config[originSection]; !exists {
859- return false, fmt.Errorf("no 'origin' remote configured")
860- }
861-
862- // Define the refspecs we want to add
863- fetchRefspecs := []string{
864- "+refs/bugs/*:refs/remotes/origin/bugs/*",
865- "+refs/identities/*:refs/remotes/origin/identities/*",
866- }
867- pushRefspecs := []string{
868- "+refs/bugs/*:refs/bugs/*",
869- "+refs/identities/*:refs/identities/*",
870- }
871-
872- changed := false
873-
874- // Add fetch refspecs if not present
875- for _, refspec := range fetchRefspecs {
876- if !hasRefspec(config[originSection], refspec) {
877- config[originSection]["fetch"] = append(config[originSection]["fetch"], refspec)
878- changed = true
879- }
880- }
881-
882- // Add push refspecs if not present
883- for _, refspec := range pushRefspecs {
884- if !hasRefspec(config[originSection], refspec) {
885- config[originSection]["push"] = append(config[originSection]["push"], refspec)
886- changed = true
887- }
888- }
889-
890- if !changed {
891- return false, nil
892- }
893-
894- if err := writeGitConfig(repoPath, config); err != nil {
895- return false, err
896- }
897-
898- return true, nil
899-}
900-
901-// resolveBugID resolves a bug ID (short or full) to a bug entity
902-// It uses ShortIDMap to handle short ID resolution
903-func resolveBugID(repoPath, idStr string) (*bug.Bug, error) {
904- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
905- if err != nil {
906- return nil, fmt.Errorf("failed to open repository: %w", err)
907- }
908- defer repo.Close()
909-
910- // Generate short ID map for resolution
911- gen := NewShortIDGenerator(repoPath)
912- shortIDMap, err := gen.Generate()
913- if err != nil {
914- return nil, fmt.Errorf("failed to generate short IDs: %w", err)
915- }
916-
917- // Try to resolve the ID (could be short or full)
918- fullID, err := shortIDMap.GetFullID(idStr)
919- if err != nil {
920- return nil, fmt.Errorf("failed to resolve bug ID %q: %w", idStr, err)
921- }
922-
923- // Read the bug using the full ID
924- b, err := bug.Read(repo, entity.Id(fullID))
925- if err != nil {
926- return nil, fmt.Errorf("failed to read bug %q: %w", idStr, err)
927- }
928-
929- return b, nil
930-}
931-
932-// resolveCommentID resolves a comment ID (short or full) and returns the bug and comment
933-// It uses ShortIDMap to handle short ID resolution
934-func resolveCommentID(repoPath, idStr string) (*bug.Bug, *bug.Comment, error) {
935- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
936- if err != nil {
937- return nil, nil, fmt.Errorf("failed to open repository: %w", err)
938- }
939- defer repo.Close()
940-
941- // Generate short ID map for resolution
942- gen := NewShortIDGenerator(repoPath)
943- shortIDMap, err := gen.Generate()
944- if err != nil {
945- return nil, nil, fmt.Errorf("failed to generate short IDs: %w", err)
946- }
947-
948- // Try to resolve the ID (could be short or full)
949- fullID, err := shortIDMap.GetFullID(idStr)
950- if err != nil {
951- return nil, nil, fmt.Errorf("failed to resolve comment ID %q: %w", idStr, err)
952- }
953-
954- // Parse the combined ID to get bug ID prefix and comment ID prefix
955- combinedID := entity.CombinedId(fullID)
956- bugIDPrefix, _ := entity.SeparateIds(string(combinedID))
957-
958- // SeparateIds returns prefixes, not full IDs. We need to look up the full bug ID.
959- // The bug ID prefix is 50 characters, but we need the full 64-character ID.
960- fullBugID, err := shortIDMap.GetFullID(bugIDPrefix)
961- if err != nil {
962- return nil, nil, fmt.Errorf("failed to resolve bug ID from prefix %q: %w", bugIDPrefix, err)
963- }
964-
965- // Read the bug using the full bug ID
966- b, err := bug.Read(repo, entity.Id(fullBugID))
967- if err != nil {
968- return nil, nil, fmt.Errorf("failed to read bug for comment %q: %w", idStr, err)
969- }
970-
971- // Search for the comment in the bug
972- snap := b.Compile()
973- comment, err := snap.SearchComment(combinedID)
974- if err != nil {
975- return nil, nil, fmt.Errorf("comment %q not found in bug: %w", idStr, err)
976- }
977-
978- return b, comment, nil
979-}
980-
981-// IDType represents the type of ID resolved
982-type IDType int
983-
984-const (
985- IDTypeBug IDType = iota
986- IDTypeComment
987-)
988-
989-// ResolvedID holds the result of ID resolution
990-type ResolvedID struct {
991- Type IDType
992- Bug *bug.Bug
993- Comment *bug.Comment
994- ID string // Full ID
995-}
996-
997-// resolveID attempts to resolve an ID as a bug ID first, then as a comment ID
998-// Returns ResolvedID with the type and appropriate data
999-func resolveID(repoPath, idStr string) (*ResolvedID, error) {
1000- // Try to resolve as bug ID first
1001- b, err := resolveBugID(repoPath, idStr)
1002- if err == nil {
1003- return &ResolvedID{
1004- Type: IDTypeBug,
1005- Bug: b,
1006- ID: b.Id().String(),
1007- }, nil
1008- }
1009-
1010- // Try to resolve as comment ID
1011- b, comment, err := resolveCommentID(repoPath, idStr)
1012- if err == nil {
1013- return &ResolvedID{
1014- Type: IDTypeComment,
1015- Bug: b,
1016- Comment: comment,
1017- ID: comment.CombinedId().String(),
1018- }, nil
1019- }
1020-
1021- // Neither resolved - return combined error
1022- return nil, fmt.Errorf("failed to resolve ID %q as bug or comment: %w", idStr, err)
1023-}
1024-
1025-// launchCommentEditor opens the user's preferred editor for comment input
1026-// Returns the content written by the user with comment lines filtered
1027-func launchCommentEditor(initialContent string) (string, error) {
1028- // Write initial content with instructions
1029- template := `;; Enter your comment below
1030-;; Lines starting with ;; will be ignored
1031-;; You can use markdown formatting
1032-;; Save and close the editor when done
1033-;;
1034-` + initialContent
1035-
1036- return launchEditorWithTemplate(template)
1037-}
1038-
1039-// runRead reads and displays a bug/issue with all its comments
1040-// Outputs structured metadata followed by description and all comments
1041-func runRead(repoPath, bugIDStr string) error {
1042- // Resolve bug ID
1043- b, err := resolveBugID(repoPath, bugIDStr)
1044- if err != nil {
1045- return err
1046- }
1047-
1048- // Compile bug snapshot
1049- snap := b.Compile()
1050-
1051- // Get the first comment (description) for author info
1052- var authorName, authorEmail string
1053- var createdAt time.Time
1054- if len(snap.Comments) > 0 {
1055- firstComment := snap.Comments[0]
1056- authorName = firstComment.Author.Name()
1057- authorEmail = firstComment.Author.Email()
1058- // Parse the formatted time string
1059- createdAt, _ = time.Parse("Mon Jan 2 15:04:05 2006 -0700", firstComment.FormatTime())
1060- }
1061- if createdAt.IsZero() {
1062- createdAt = snap.CreateTime
1063- }
1064-
1065- // Format author string (name only if no email)
1066- authorStr := authorName
1067- if authorEmail != "" {
1068- authorStr = fmt.Sprintf("%s <%s>", authorName, authorEmail)
1069- }
1070-
1071- // Format labels
1072- labelsStr := "-"
1073- if len(snap.Labels) > 0 {
1074- labels := make([]string, len(snap.Labels))
1075- for i, label := range snap.Labels {
1076- labels[i] = string(label)
1077- }
1078- labelsStr = strings.Join(labels, ", ")
1079- }
1080-
1081- // Print metadata header
1082- fmt.Printf("Title: %s\n", snap.Title)
1083- fmt.Printf("Author: %s\n", authorStr)
1084- fmt.Printf("Created: %s\n", createdAt.Format(time.RFC3339))
1085- fmt.Printf("Status: %s\n", snap.Status.String())
1086- fmt.Printf("Labels: %s\n", labelsStr)
1087- fmt.Println()
1088-
1089- // Print description (first comment)
1090- if len(snap.Comments) > 0 {
1091- description := snap.Comments[0].Message
1092- if description != "" {
1093- fmt.Println(description)
1094- }
1095-
1096- // Print additional comments
1097- for i := 1; i < len(snap.Comments); i++ {
1098- comment := snap.Comments[i]
1099- commentID := comment.CombinedId().String()
1100- if len(commentID) > 7 {
1101- commentID = commentID[:7]
1102- }
1103-
1104- commentAuthor := comment.Author.Name()
1105- commentDate := comment.FormatTime()
1106-
1107- fmt.Println()
1108- fmt.Printf("Comment %s made by %s on %s:\n\n", commentID, commentAuthor, commentDate)
1109- fmt.Println(comment.Message)
1110- }
1111- }
1112-
1113- return nil
1114-}
1115-
1116-// parseCommentContent parses editor output for comments
1117-// Lines starting with ;; are treated as comments and ignored
1118-func parseCommentContent(content string) (string, error) {
1119- lines := strings.Split(content, "\n")
1120- var nonCommentLines []string
1121-
1122- // Filter out comment lines
1123- for _, line := range lines {
1124- trimmed := strings.TrimSpace(line)
1125- if strings.HasPrefix(trimmed, ";;") {
1126- continue
1127- }
1128- nonCommentLines = append(nonCommentLines, line)
1129- }
1130-
1131- // Find first non-empty line
1132- startIdx := 0
1133- for startIdx < len(nonCommentLines) && strings.TrimSpace(nonCommentLines[startIdx]) == "" {
1134- startIdx++
1135- }
1136-
1137- // If no content left, return error
1138- if startIdx >= len(nonCommentLines) {
1139- return "", fmt.Errorf("no comment provided")
1140- }
1141-
1142- // Trim trailing empty lines
1143- endIdx := len(nonCommentLines)
1144- for endIdx > startIdx && strings.TrimSpace(nonCommentLines[endIdx-1]) == "" {
1145- endIdx--
1146- }
1147-
1148- // Join remaining lines
1149- message := strings.Join(nonCommentLines[startIdx:endIdx], "\n")
1150-
1151- return message, nil
1152-}
1153-
1154-// runNew creates a new bug/issue in the repository
1155-// If title is empty, opens editor for interactive input
1156-func runNew(repoPath, title, message string) error {
1157- // If no title provided via flags, open editor
1158- if title == "" && message == "" {
1159- content, err := launchEditor("")
1160- if err != nil {
1161- return err
1162- }
1163- title, message, err = parseEditorContent(content)
1164- if err != nil {
1165- return err
1166- }
1167- } else if title == "" {
1168- // Message provided but no title - still need editor
1169- content, err := launchEditor(message)
1170- if err != nil {
1171- return err
1172- }
1173- title, message, err = parseEditorContent(content)
1174- if err != nil {
1175- return err
1176- }
1177- }
1178- // If only title provided, message can be empty (that's OK)
1179-
1180- // Validate title
1181- if strings.TrimSpace(title) == "" {
1182- return fmt.Errorf("title cannot be empty")
1183- }
1184-
1185- // Open repository
1186- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
1187- if err != nil {
1188- return fmt.Errorf("failed to open repository: %w", err)
1189- }
1190- defer repo.Close()
1191-
1192- // Get user identity
1193- author, err := identity.GetUserIdentity(repo)
1194- if err != nil {
1195- return fmt.Errorf("failed to get user identity: %w\n\nRun 'bug init' first to set up identities", err)
1196- }
1197-
1198- // Create the bug
1199- unixTime := time.Now().Unix()
1200- newBug, _, err := bug.Create(author, unixTime, title, message, nil, nil)
1201- if err != nil {
1202- return fmt.Errorf("failed to create bug: %w", err)
1203- }
1204-
1205- // Commit the bug to the repository
1206- if err := newBug.Commit(repo); err != nil {
1207- return fmt.Errorf("failed to commit bug: %w", err)
1208- }
1209-
1210- // Get the bug ID and display first 7 characters (like bug ls table)
1211- bugID := newBug.Id().String()
1212- if len(bugID) > 7 {
1213- bugID = bugID[:7]
1214- }
1215-
1216- fmt.Printf("Created bug %s: %s\n", bugID, title)
1217-
1218- // Invalidate cache so git-bug sees the new bug
1219- invalidateGitBugCache(repoPath)
1220-
1221- return nil
1222-}
1223-
1224-// runInit creates initial identities in the repository
1225-// It attempts to get user info from jj config if .jj exists, otherwise prompts the user
1226-// It also creates an agent identity
1227-func runInit(repoPath string) error {
1228- return runInitWithReader(repoPath, os.Stdin)
1229-}
1230-
1231-// ErrNoOriginRemote is returned when no origin remote is configured
1232-type ErrNoOriginRemote struct{}
1233-
1234-func (e ErrNoOriginRemote) Error() string {
1235- return "no 'origin' remote configured"
1236-}
1237-
1238-// runInitWithReader is the testable version that accepts an io.Reader for stdin
1239-func runInitWithReader(repoPath string, reader io.Reader) error {
1240- // Check if identities already exist (idempotent behavior)
1241- userExists, agentExists, err := checkIdentitiesExist(repoPath)
1242- if err != nil {
1243- return err
1244- }
1245-
1246- // Track if we need to prompt for user info
1247- var name, email string
1248-
1249- if userExists {
1250- fmt.Println("User identity already exists, skipping creation")
1251- } else {
1252- // Try to get user info from jj config
1253- name, email, err = detectJJConfig(repoPath)
1254- if err != nil {
1255- return fmt.Errorf("failed to detect jj config: %w", err)
1256- }
1257-
1258- // If jj config was found, use it
1259- if name != "" && email != "" {
1260- fmt.Printf("Creating user identity from jj config: %s <%s>\n", name, email)
1261- } else {
1262- // Prompt user for name and email
1263- fmt.Println("No jj config found. Please provide your information:")
1264-
1265- // Create a single scanner to use for all reads
1266- scanner := bufio.NewScanner(reader)
1267-
1268- name, err = promptUserWithScanner("Name: ", scanner)
1269- if err != nil {
1270- return fmt.Errorf("failed to read name: %w", err)
1271- }
1272- if name == "" {
1273- return fmt.Errorf("name cannot be empty")
1274- }
1275-
1276- email, err = promptUserWithScanner("Email: ", scanner)
1277- if err != nil {
1278- return fmt.Errorf("failed to read email: %w", err)
1279- }
1280- }
1281-
1282- // Create user identity
1283- fmt.Printf("Creating user identity: %s <%s>\n", name, email)
1284- if err := createIdentity(repoPath, name, email, true); err != nil {
1285- return fmt.Errorf("failed to create user identity: %w", err)
1286- }
1287- fmt.Println("User identity created successfully")
1288- }
1289-
1290- if agentExists {
1291- fmt.Println("Agent identity already exists, skipping creation")
1292- } else {
1293- // Create agent identity
1294- // Empty email is intentional per spec: equivalent to `git-bug user new --name 'agent' --email ''`
1295- fmt.Println("Creating agent identity...")
1296- if err := createIdentity(repoPath, "agent", "", false); err != nil {
1297- return fmt.Errorf("failed to create agent identity: %w", err)
1298- }
1299- fmt.Println("Agent identity created successfully")
1300- }
1301-
1302- // Configure git remote refspecs for origin
1303- configured, err := configureOriginRefspecs(repoPath)
1304- if err != nil {
1305- if err.Error() == "no 'origin' remote configured" {
1306- return ErrNoOriginRemote{}
1307- }
1308- return fmt.Errorf("failed to configure git refspecs: %w", err)
1309- }
1310-
1311- if configured {
1312- fmt.Println("Configured git to automatically sync bugs and identities with origin")
1313- } else if userExists && agentExists {
1314- // All identities exist and refspecs are configured - nothing to do
1315- fmt.Println("Nothing to do")
1316- }
1317-
1318- // Invalidate cache so git-bug sees the new identities
1319- invalidateGitBugCache(repoPath)
1320-
1321- return nil
1322-}
1323-
1324-var (
1325- repoPath string
1326- filterFlag string
1327- sortFlag string
1328- newTitle string
1329- newMessage string
1330- agentTitle string
1331- agentMessage string
1332- commentMessage string
1333- commentBugID string
1334- agentCommentMsg string
1335- editTitle string
1336- editMessage string
1337- agentEditTitle string
1338- agentEditMsg string
1339- statusFlag string
1340-)
1341-
1342-var rootCmd = &cobra.Command{
1343- Use: "bug",
1344- Short: "A CLI for git-bug issue tracking",
1345- Long: `bug is a simplified CLI interface to git-bug issues and comments.
1346-It provides an easy way to list and manage issues in your git repository.`,
1347-}
1348-
1349-var listCmd = &cobra.Command{
1350- Use: "list",
1351- Aliases: []string{"ls"},
1352- Short: "List all issues in the repository",
1353- Long: `Display a table of all git-bug issues with their ID, summary, labels, and age.
1354-
1355-Filtering:
1356- --filter label:bug Show only issues with label "bug"
1357- --filter age:<10d Show only issues newer than 10 days
1358-
1359-Sorting:
1360- --sort id:asc Sort by ID (ascending)
1361- --sort id:desc Sort by ID (descending)
1362- --sort age:asc Sort by age, oldest first
1363- --sort age:desc Sort by age, newest first (default)
1364-
1365-Status Filtering:
1366- -S, --status open Show only open issues (default)
1367- -S, --status closed Show only closed issues
1368- -S, --status all Show all issues`,
1369- RunE: func(cmd *cobra.Command, args []string) error {
1370- // Load issues
1371- issues, err := LoadBugs(repoPath)
1372- if err != nil {
1373- return err
1374- }
1375-
1376- if len(issues) == 0 {
1377- fmt.Println("No Issues")
1378- return nil
1379- }
1380-
1381- // Parse and apply filter
1382- filterSpec, err := ParseFilter(filterFlag)
1383- if err != nil {
1384- return err
1385- }
1386- issues = ApplyFilter(issues, filterSpec)
1387-
1388- // Apply status filter
1389- if statusFlag != "" && strings.ToLower(statusFlag) != "all" {
1390- statusLower := strings.ToLower(statusFlag)
1391- if statusLower != "open" && statusLower != "closed" {
1392- return fmt.Errorf("invalid status: %s (expected 'open', 'closed', or 'all')", statusFlag)
1393- }
1394- var filtered []BugIssue
1395- for _, issue := range issues {
1396- if strings.ToLower(issue.Status) == statusLower {
1397- filtered = append(filtered, issue)
1398- }
1399- }
1400- issues = filtered
1401- }
1402-
1403- // Parse and apply sort
1404- sortSpec, err := ParseSort(sortFlag)
1405- if err != nil {
1406- return err
1407- }
1408- ApplySort(issues, sortSpec)
1409-
1410- if len(issues) == 0 {
1411- fmt.Println("No Issues")
1412- return nil
1413- }
1414-
1415- return printTable(issues)
1416- },
1417-}
1418-
1419-var initCmd = &cobra.Command{
1420- Use: "init",
1421- Short: "Initialize git-bug identities in the repository",
1422- Long: `Initialize git-bug by creating initial identities in the repository.
1423-
1424-If a .jj directory exists, the command will attempt to read user.name and user.email
1425-from jj config and create a user identity. Otherwise, it will prompt you interactively
1426-for your name and email. It also creates an agent identity.
1427-
1428-This command also configures git push/fetch refspecs for the 'origin' remote
1429-to automatically sync bugs and identities.
1430-
1431-Examples:
1432- bug init # Initialize in current directory
1433- bug init --repo /path/to/repo # Initialize in specific repository`,
1434- RunE: func(cmd *cobra.Command, args []string) error {
1435- err := runInit(repoPath)
1436- if err != nil {
1437- // Check for special error types
1438- if _, ok := err.(ErrNoOriginRemote); ok {
1439- fmt.Fprintln(os.Stderr, "Error: No 'origin' remote configured.")
1440- fmt.Fprintln(os.Stderr, "")
1441- fmt.Fprintln(os.Stderr, "Please add a remote with: git remote add origin <url>")
1442- fmt.Fprintln(os.Stderr, "Then run 'bug init' again to configure bug push/fetch refspecs.")
1443- os.Exit(1)
1444- }
1445- return err
1446- }
1447- return nil
1448- },
1449-}
1450-
1451-var newCmd = &cobra.Command{
1452- Use: "new",
1453- Short: "Create a new issue/bug",
1454- Long: `Create a new issue in the git-bug repository.
1455-
1456-You can provide the title and message via flags:
1457- bug new --title "Bug title" --message "Detailed description"
1458- bug new -t "Bug title" -m "Detailed description"
1459-
1460-Or launch an editor to write them interactively:
1461- bug new
1462-
1463-The editor will open with a template. The first non-empty line becomes the title,
1464-and the rest becomes the description.`,
1465- RunE: func(cmd *cobra.Command, args []string) error {
1466- return runNew(repoPath, newTitle, newMessage)
1467- },
1468-}
1469-
1470-var agentCmd = &cobra.Command{
1471- Use: "agent",
1472- Short: "Agent commands for automated bug creation",
1473- Long: `Commands for creating bugs as the agent identity.
1474-
1475-These commands are designed for automated/agent use and are non-interactive.
1476-All agent commands use the agent identity created during 'bug init'.
1477-
1478-Example:
1479- bug agent new --title "Automated bug" --message "Found by CI"`,
1480-}
1481-
1482-var agentNewCmd = &cobra.Command{
1483- Use: "new",
1484- Short: "Create a new issue as the agent",
1485- Long: `Create a new issue in the git-bug repository using the agent identity.
1486-
1487-This command is non-interactive and requires both --title and --message flags.
1488-It uses the agent identity created during 'bug init'.
1489-
1490-Example:
1491- bug agent new --title "CI Failure" --message "Build failed on commit abc123"
1492- bug agent new -t "Bug found" -m "Automated scan detected issue"`,
1493- RunE: func(cmd *cobra.Command, args []string) error {
1494- return runAgentNew(repoPath, agentTitle, agentMessage)
1495- },
1496-}
1497-
1498-var commentCmd = &cobra.Command{
1499- Use: "comment [bugid]",
1500- Short: "Add a comment to an existing bug",
1501- Long: `Add a comment to an existing bug in the git-bug repository.
1502-
1503-You can provide the comment via the message flag:
1504- bug comment abc1234 --message "This is my comment"
1505- bug comment abc1234 -m "This is my comment"
1506-
1507-Or launch an editor to write it interactively:
1508- bug comment abc1234
1509-
1510-The editor will open with a template. Lines starting with ;; are ignored.
1511-You can use markdown formatting in your comment.`,
1512- Args: cobra.ExactArgs(1),
1513- RunE: func(cmd *cobra.Command, args []string) error {
1514- commentBugID = args[0]
1515- return runComment(repoPath, commentBugID, commentMessage)
1516- },
1517-}
1518-
1519-var editCmd = &cobra.Command{
1520- Use: "edit [bugid|commentid]",
1521- Short: "Edit an issue or comment",
1522- Long: `Edit an existing issue or comment in the git-bug repository.
1523-
1524-The ID can be a bug ID (full or short) or a comment ID (full or short).
1525-The command first tries to resolve the ID as a bug, then as a comment.
1526-
1527-For issues:
1528- bug edit abc1234 # Open editor with current title and description
1529- bug edit abc1234 -t "New Title" # Update only the title
1530- bug edit abc1234 -m "New desc" # Update only the description
1531- bug edit abc1234 -t "Title" -m "Desc" # Update both
1532-
1533-For comments:
1534- bug edit def5678 # Open editor with current comment text
1535- bug edit def5678 -m "New text" # Update the comment
1536-
1537-When using the editor:
1538- - For issues: title on first line, blank line, then description
1539- - For comments: edit the comment text directly
1540- - Lines starting with ;; are ignored (instructions)`,
1541- Args: cobra.ExactArgs(1),
1542- RunE: func(cmd *cobra.Command, args []string) error {
1543- return runEdit(repoPath, args[0], editTitle, editMessage)
1544- },
1545-}
1546-
1547-var readCmd = &cobra.Command{
1548- Use: "read [bugid]",
1549- Aliases: []string{"show"},
1550- Short: "Display a bug/issue with all comments",
1551- Long: `Display a bug/issue and all its comments in a structured format.
1552-
1553-The output includes metadata (title, author, creation date, status, labels)
1554-followed by the description and all comments.
1555-
1556-Examples:
1557- bug read abc1234 # Display bug with ID prefix abc1234
1558- bug show abc1234 # Same as above (alias)
1559- bug read --repo /path abc # Display bug from specific repository`,
1560- Args: cobra.ExactArgs(1),
1561- RunE: func(cmd *cobra.Command, args []string) error {
1562- return runRead(repoPath, args[0])
1563- },
1564-}
1565-
1566-var rmCmd = &cobra.Command{
1567- Use: "rm [bugid|commentid]",
1568- Aliases: []string{"remove"},
1569- Short: "Remove a bug/issue by its ID",
1570- Long: `Remove a bug/issue from the git-bug repository.
1571-
1572-The ID can be a bug ID (full or short). Comments cannot be removed individually;
1573-use 'bug rm <bug-id>' to remove the entire issue including all its comments.
1574-
1575-WARNING: This action is permanent and cannot be undone.
1576-
1577-Examples:
1578- bug rm abc1234 # Remove bug with short ID abc1234
1579- bug rm abc1234567890abcdef # Remove bug with full ID
1580- bug remove abc1234 # Same as above (alias)
1581-
1582-Note: If a comment ID is provided, the command will return an error explaining
1583-that individual comments cannot be removed.`,
1584- Args: cobra.ExactArgs(1),
1585- RunE: func(cmd *cobra.Command, args []string) error {
1586- return runRemove(repoPath, args[0])
1587- },
1588-}
1589-
1590-var agentRmCmd = &cobra.Command{
1591- Use: "rm [bugid]",
1592- Aliases: []string{"remove"},
1593- Short: "Remove a bug/issue as the agent",
1594- Long: `Remove a bug/issue from the git-bug repository using the agent identity.
1595-
1596-This command is non-interactive and uses the agent identity created during 'bug init'.
1597-Comments cannot be removed individually; use 'bug agent rm <bug-id>' to remove
1598- the entire issue including all its comments.
1599-
1600-WARNING: This action is permanent and cannot be undone.
1601-
1602-Examples:
1603- bug agent rm abc1234 # Remove bug with short ID abc1234
1604- bug agent remove abc1234 # Same as above (alias)`,
1605- Args: cobra.ExactArgs(1),
1606- RunE: func(cmd *cobra.Command, args []string) error {
1607- return runRemove(repoPath, args[0])
1608- },
1609-}
1610-
1611-var openCmd = &cobra.Command{
1612- Use: "open [bugid]",
1613- Short: "Open a closed bug/issue",
1614- Long: `Open a bug/issue by changing its status from closed to open.
1615-
1616-The ID can be a bug ID (full or short). This command changes the bug's
1617- status to "open".
1618-
1619-Examples:
1620- bug open abc1234 # Open bug with short ID abc1234
1621- bug open abc1234567890abcdef # Open bug with full ID`,
1622- Args: cobra.ExactArgs(1),
1623- RunE: func(cmd *cobra.Command, args []string) error {
1624- return runOpen(repoPath, args[0])
1625- },
1626-}
1627-
1628-var closeCmd = &cobra.Command{
1629- Use: "close [bugid]",
1630- Short: "Close an open bug/issue",
1631- Long: `Close a bug/issue by changing its status from open to closed.
1632-
1633-The ID can be a bug ID (full or short). This command changes the bug's
1634- status to "closed".
1635-
1636-Examples:
1637- bug close abc1234 # Close bug with short ID abc1234
1638- bug close abc1234567890abcdef # Close bug with full ID`,
1639- Args: cobra.ExactArgs(1),
1640- RunE: func(cmd *cobra.Command, args []string) error {
1641- return runClose(repoPath, args[0])
1642- },
1643-}
1644-
1645-var agentCommentCmd = &cobra.Command{
1646- Use: "comment [bugid]",
1647- Short: "Add a comment to a bug as the agent",
1648- Long: `Add a comment to an existing bug using the agent identity.
1649-
1650-This command is non-interactive and requires the --message flag.
1651-It uses the agent identity created during 'bug init'.
1652-
1653-Example:
1654- bug agent comment abc1234 --message "Automated analysis complete"
1655- bug agent comment abc1234 -m "Build passed all tests"`,
1656- Args: cobra.ExactArgs(1),
1657- RunE: func(cmd *cobra.Command, args []string) error {
1658- commentBugID = args[0]
1659- return runAgentComment(repoPath, commentBugID, agentCommentMsg)
1660- },
1661-}
1662-
1663-var agentEditCmd = &cobra.Command{
1664- Use: "edit [bugid|commentid]",
1665- Short: "Edit an issue or comment as the agent",
1666- Long: `Edit an existing issue or comment using the agent identity.
1667-
1668-This command is non-interactive and requires at least one of --title or --message.
1669-It uses the agent identity created during 'bug init'.
1670-
1671-For issues:
1672- bug agent edit abc1234 --title "New Title"
1673- bug agent edit abc1234 --message "New description"
1674- bug agent edit abc1234 --title "Title" --message "Description"
1675-
1676-For comments:
1677- bug agent edit def5678 --message "Updated comment text"`,
1678- Args: cobra.ExactArgs(1),
1679- RunE: func(cmd *cobra.Command, args []string) error {
1680- return runAgentEdit(repoPath, args[0], agentEditTitle, agentEditMsg)
1681- },
1682-}
1683-
1684-var agentOpenCmd = &cobra.Command{
1685- Use: "open [bugid]",
1686- Short: "Open a closed bug/issue as the agent",
1687- Long: `Open a bug/issue by changing its status from closed to open.
1688-
1689-This command uses the agent identity created during 'bug init'.
1690-It is non-interactive and designed for automated/agent use.
1691-
1692-Examples:
1693- bug agent open abc1234 # Open bug with short ID abc1234`,
1694- Args: cobra.ExactArgs(1),
1695- RunE: func(cmd *cobra.Command, args []string) error {
1696- return runAgentOpen(repoPath, args[0])
1697- },
1698-}
1699-
1700-var agentCloseCmd = &cobra.Command{
1701- Use: "close [bugid]",
1702- Short: "Close an open bug/issue as the agent",
1703- Long: `Close a bug/issue by changing its status from open to closed.
1704-
1705-This command uses the agent identity created during 'bug init'.
1706-It is non-interactive and designed for automated/agent use.
1707-
1708-Examples:
1709- bug agent close abc1234 # Close bug with short ID abc1234`,
1710- Args: cobra.ExactArgs(1),
1711- RunE: func(cmd *cobra.Command, args []string) error {
1712- return runAgentClose(repoPath, args[0])
1713- },
1714-}
1715-
1716-var agentReadCmd = &cobra.Command{
1717- Use: "read [bugid]",
1718- Short: "Display a bug/issue with all comments",
1719- Long: `Display a bug/issue and all its comments in a structured format.
1720-
1721-This is the same as 'bug read' but accessible under the agent subcommand.
1722-The output includes metadata (title, author, creation date, status, labels)
1723-followed by the description and all comments.
1724-
1725-Examples:
1726- bug agent read abc1234 # Display bug with ID prefix abc1234`,
1727- Args: cobra.ExactArgs(1),
1728- RunE: func(cmd *cobra.Command, args []string) error {
1729- return runRead(repoPath, args[0])
1730- },
1731-}
1732-
1733-func init() {
1734- rootCmd.PersistentFlags().StringVar(&repoPath, "repo", ".", "path to git repository")
1735- listCmd.Flags().StringVarP(&filterFlag, "filter", "f", "", "filter issues (label:value or age:<duration>)")
1736- listCmd.Flags().StringVarP(&sortFlag, "sort", "s", "", "sort issues (field:direction, default: age:desc)")
1737- listCmd.Flags().StringVarP(&statusFlag, "status", "S", "open", "filter by status (open|closed|all)")
1738-
1739- newCmd.Flags().StringVarP(&newTitle, "title", "t", "", "issue title")
1740- newCmd.Flags().StringVarP(&newMessage, "message", "m", "", "issue description/message")
1741-
1742- // Agent new command flags - both required, no shorthand to force explicit usage
1743- agentNewCmd.Flags().StringVar(&agentTitle, "title", "", "issue title (required)")
1744- agentNewCmd.Flags().StringVar(&agentMessage, "message", "", "issue description (required)")
1745- agentNewCmd.MarkFlagRequired("title")
1746- agentNewCmd.MarkFlagRequired("message")
1747-
1748- // Comment command flags - message is optional (opens editor if not provided)
1749- commentCmd.Flags().StringVarP(&commentMessage, "message", "m", "", "comment message (opens editor if not provided)")
1750-
1751- // Agent comment command flags - message is required (non-interactive)
1752- agentCommentCmd.Flags().StringVarP(&agentCommentMsg, "message", "m", "", "comment message (required)")
1753- agentCommentCmd.MarkFlagRequired("message")
1754-
1755- // Edit command flags - both optional (opens editor if neither provided)
1756- editCmd.Flags().StringVarP(&editTitle, "title", "t", "", "new issue title")
1757- editCmd.Flags().StringVarP(&editMessage, "message", "m", "", "new issue description or comment text")
1758-
1759- // Agent edit command flags - at least one required
1760- agentEditCmd.Flags().StringVar(&agentEditTitle, "title", "", "new issue title")
1761- agentEditCmd.Flags().StringVarP(&agentEditMsg, "message", "m", "", "new issue description or comment text")
1762-
1763- rootCmd.AddCommand(listCmd)
1764- rootCmd.AddCommand(initCmd)
1765- rootCmd.AddCommand(newCmd)
1766- rootCmd.AddCommand(commentCmd)
1767- rootCmd.AddCommand(editCmd)
1768- rootCmd.AddCommand(readCmd)
1769- rootCmd.AddCommand(rmCmd) // NEW: Add rm command
1770- rootCmd.AddCommand(termCmd) // NEW: Add term command
1771- rootCmd.AddCommand(openCmd) // NEW: Add open command
1772- rootCmd.AddCommand(closeCmd) // NEW: Add close command
1773-
1774- // Add agent command with its subcommands
1775- agentCmd.AddCommand(agentNewCmd)
1776- agentCmd.AddCommand(agentCommentCmd)
1777- agentCmd.AddCommand(agentEditCmd)
1778- agentCmd.AddCommand(agentReadCmd)
1779- agentCmd.AddCommand(agentRmCmd) // NEW: Add agent rm command
1780- agentCmd.AddCommand(agentOpenCmd) // NEW: Add agent open command
1781- agentCmd.AddCommand(agentCloseCmd) // NEW: Add agent close command
1782- rootCmd.AddCommand(agentCmd)
1783-}
1784-
1785-// runAgentNew creates a new bug/issue using the agent identity
1786-// Both title and message are required - this is non-interactive
1787-func runAgentNew(repoPath, title, message string) error {
1788- // Validate inputs (both required for agent commands)
1789- if strings.TrimSpace(title) == "" {
1790- return fmt.Errorf("title cannot be empty")
1791- }
1792- if strings.TrimSpace(message) == "" {
1793- return fmt.Errorf("message cannot be empty")
1794- }
1795-
1796- // Open repository
1797- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
1798- if err != nil {
1799- return fmt.Errorf("failed to open repository: %w", err)
1800- }
1801- defer repo.Close()
1802-
1803- // Get agent identity (not user identity)
1804- author, err := getAgentIdentity(repo)
1805- if err != nil {
1806- return err
1807- }
1808-
1809- // Create the bug
1810- unixTime := time.Now().Unix()
1811- newBug, _, err := bug.Create(author, unixTime, title, message, nil, nil)
1812- if err != nil {
1813- return fmt.Errorf("failed to create bug: %w", err)
1814- }
1815-
1816- // Commit the bug to persist it
1817- if err := newBug.Commit(repo); err != nil {
1818- return fmt.Errorf("failed to commit bug: %w", err)
1819- }
1820-
1821- // Get the bug ID and display first 7 characters (like bug ls table)
1822- bugID := newBug.Id().String()
1823- if len(bugID) > 7 {
1824- bugID = bugID[:7]
1825- }
1826-
1827- fmt.Printf("Created bug %s: %s\n", bugID, title)
1828-
1829- // Invalidate cache so git-bug sees the new bug
1830- invalidateGitBugCache(repoPath)
1831-
1832- return nil
1833-}
1834-
1835-// runComment adds a comment to an existing bug
1836-// If message is empty, opens editor for interactive input
1837-func runComment(repoPath, bugIDStr, message string) error {
1838- // If no message provided, open editor
1839- if strings.TrimSpace(message) == "" {
1840- content, err := launchCommentEditor("")
1841- if err != nil {
1842- return err
1843- }
1844- message, err = parseCommentContent(content)
1845- if err != nil {
1846- return err
1847- }
1848- }
1849-
1850- // Validate message
1851- if strings.TrimSpace(message) == "" {
1852- return fmt.Errorf("comment cannot be empty")
1853- }
1854-
1855- // Resolve bug ID
1856- b, err := resolveBugID(repoPath, bugIDStr)
1857- if err != nil {
1858- return err
1859- }
1860-
1861- // Open repository for identity lookup
1862- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
1863- if err != nil {
1864- return fmt.Errorf("failed to open repository: %w", err)
1865- }
1866- defer repo.Close()
1867-
1868- // Get user identity
1869- author, err := identity.GetUserIdentity(repo)
1870- if err != nil {
1871- return fmt.Errorf("failed to get user identity: %w\n\nRun 'bug init' first to set up identities", err)
1872- }
1873-
1874- // Add the comment
1875- unixTime := time.Now().Unix()
1876- commentID, _, err := bug.AddComment(b, author, unixTime, message, nil, nil)
1877- if err != nil {
1878- return fmt.Errorf("failed to add comment: %w", err)
1879- }
1880-
1881- // Commit the bug to persist the comment
1882- if err = b.Commit(repo); err != nil {
1883- return fmt.Errorf("failed to commit comment: %w", err)
1884- }
1885-
1886- // Get IDs for display (first 7 characters)
1887- displayCommentID := commentID.String()
1888- displayBugID := b.Id().String()
1889- if len(displayCommentID) > 7 {
1890- displayCommentID = displayCommentID[:7]
1891- }
1892- if len(displayBugID) > 7 {
1893- displayBugID = displayBugID[:7]
1894- }
1895-
1896- // Display success message
1897- fmt.Printf("Created comment %s for bug %s\n", displayCommentID, displayBugID)
1898-
1899- // Invalidate cache so git-bug sees the new comment
1900- invalidateGitBugCache(repoPath)
1901-
1902- return nil
1903-}
1904-
1905-// runAgentComment adds a comment to an existing bug as the agent
1906-// Message is required - this is non-interactive
1907-func runAgentComment(repoPath, bugIDStr, message string) error {
1908- // Validate message (required for agent commands)
1909- if strings.TrimSpace(message) == "" {
1910- return fmt.Errorf("message cannot be empty")
1911- }
1912-
1913- // Resolve bug ID
1914- b, err := resolveBugID(repoPath, bugIDStr)
1915- if err != nil {
1916- return err
1917- }
1918-
1919- // Open repository for identity lookup
1920- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
1921- if err != nil {
1922- return fmt.Errorf("failed to open repository: %w", err)
1923- }
1924- defer repo.Close()
1925-
1926- // Get agent identity
1927- author, err := getAgentIdentity(repo)
1928- if err != nil {
1929- return err
1930- }
1931-
1932- // Add the comment
1933- unixTime := time.Now().Unix()
1934- commentID, _, err := bug.AddComment(b, author, unixTime, message, nil, nil)
1935- if err != nil {
1936- return fmt.Errorf("failed to add comment: %w", err)
1937- }
1938-
1939- // Commit the bug to persist the comment
1940- if err = b.Commit(repo); err != nil {
1941- return fmt.Errorf("failed to commit comment: %w", err)
1942- }
1943-
1944- // Get IDs for display (first 7 characters)
1945- displayCommentID := commentID.String()
1946- displayBugID := b.Id().String()
1947- if len(displayCommentID) > 7 {
1948- displayCommentID = displayCommentID[:7]
1949- }
1950- if len(displayBugID) > 7 {
1951- displayBugID = displayBugID[:7]
1952- }
1953-
1954- // Display success message
1955- fmt.Printf("Created comment %s for bug %s\n", displayCommentID, displayBugID)
1956-
1957- // Invalidate cache so git-bug sees the new comment
1958- invalidateGitBugCache(repoPath)
1959-
1960- return nil
1961-}
1962-
1963-// runEdit edits an issue or comment
1964-// If title and message flags are provided, uses them directly
1965-// Otherwise, opens an editor with current content pre-populated
1966-func runEdit(repoPath, idStr, newTitle, newMessage string) error {
1967- // Resolve the ID (bug or comment)
1968- resolved, err := resolveID(repoPath, idStr)
1969- if err != nil {
1970- return err
1971- }
1972-
1973- // Open repository for identity lookup
1974- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
1975- if err != nil {
1976- return fmt.Errorf("failed to open repository: %w", err)
1977- }
1978- defer repo.Close()
1979-
1980- // Get user identity
1981- author, err := identity.GetUserIdentity(repo)
1982- if err != nil {
1983- return fmt.Errorf("failed to get user identity: %w\n\nRun 'bug init' first to set up identities", err)
1984- }
1985-
1986- unixTime := time.Now().Unix()
1987-
1988- var editErr error
1989- switch resolved.Type {
1990- case IDTypeBug:
1991- editErr = editBug(repo, resolved.Bug, author, unixTime, newTitle, newMessage)
1992- case IDTypeComment:
1993- editErr = editComment(repo, resolved.Bug, resolved.Comment, author, unixTime, newMessage)
1994- default:
1995- return fmt.Errorf("unknown ID type")
1996- }
1997-
1998- if editErr == nil {
1999- // Invalidate cache so git-bug sees the changes
2000- invalidateGitBugCache(repoPath)
2001- }
2002-
2003- return editErr
2004-}
2005-
2006-// editBug edits an issue's title and/or description
2007-func editBug(repo repository.ClockedRepo, b *bug.Bug, author identity.Interface, unixTime int64, newTitle, newMessage string) error {
2008- snap := b.Compile()
2009- currentTitle := snap.Title
2010- currentMessage := ""
2011- if len(snap.Comments) > 0 {
2012- currentMessage = snap.Comments[0].Message
2013- }
2014-
2015- // If no flags provided, open editor with current content
2016- if strings.TrimSpace(newTitle) == "" && strings.TrimSpace(newMessage) == "" {
2017- // Prepare template: title on first line, blank line, description
2018- template := fmt.Sprintf("%s\n\n%s", currentTitle, currentMessage)
2019-
2020- // Add edit instructions
2021- template = `;; Edit the title on the first line (required)
2022-;; Edit the description below the blank line
2023-;; Lines starting with ;; will be ignored
2024-;; Save and close the editor when done
2025-;;
2026-` + template
2027-
2028- content, err := launchEditorWithTemplate(template)
2029- if err != nil {
2030- return err
2031- }
2032- newTitle, newMessage, err = parseEditContent(content, false)
2033- if err != nil {
2034- return err
2035- }
2036- } else {
2037- // Flags provided - use current values for fields not specified
2038- if strings.TrimSpace(newTitle) == "" {
2039- newTitle = currentTitle
2040- }
2041- if strings.TrimSpace(newMessage) == "" {
2042- newMessage = currentMessage
2043- }
2044- }
2045-
2046- // Validate title
2047- if strings.TrimSpace(newTitle) == "" {
2048- return fmt.Errorf("title cannot be empty")
2049- }
2050-
2051- // Apply changes
2052- changes := false
2053-
2054- // Update title if changed
2055- if newTitle != currentTitle {
2056- _, err := bug.SetTitle(b, author, unixTime, newTitle, nil)
2057- if err != nil {
2058- return fmt.Errorf("failed to update title: %w", err)
2059- }
2060- changes = true
2061- }
2062-
2063- // Update description (first comment) if changed
2064- if newMessage != currentMessage {
2065- _, _, err := bug.EditCreateComment(b, author, unixTime, newMessage, nil, nil)
2066- if err != nil {
2067- return fmt.Errorf("failed to update description: %w", err)
2068- }
2069- changes = true
2070- }
2071-
2072- // Commit changes if any
2073- if changes {
2074- if err := b.Commit(repo); err != nil {
2075- return fmt.Errorf("failed to commit changes: %w", err)
2076- }
2077- }
2078-
2079- // Get short ID for display
2080- bugID := b.Id().String()
2081- if len(bugID) > 7 {
2082- bugID = bugID[:7]
2083- }
2084-
2085- fmt.Printf("Updated bug %s: %s\n", bugID, newTitle)
2086- return nil
2087-}
2088-
2089-// editComment edits an existing comment
2090-func editComment(repo repository.ClockedRepo, b *bug.Bug, comment *bug.Comment, author identity.Interface, unixTime int64, newMessage string) error {
2091- currentMessage := comment.Message
2092-
2093- // If no message provided, open editor with current content
2094- if strings.TrimSpace(newMessage) == "" {
2095- // Prepare template: current comment content
2096- template := currentMessage
2097-
2098- // Add edit instructions
2099- template = `;; Edit your comment below
2100-;; Lines starting with ;; will be ignored
2101-;; You can use markdown formatting
2102-;; Save and close the editor when done
2103-;;
2104-` + template
2105-
2106- content, err := launchEditorWithTemplate(template)
2107- if err != nil {
2108- return err
2109- }
2110- _, newMessage, err = parseEditContent(content, true)
2111- if err != nil {
2112- return err
2113- }
2114- }
2115-
2116- // Validate message
2117- if strings.TrimSpace(newMessage) == "" {
2118- return fmt.Errorf("comment cannot be empty")
2119- }
2120-
2121- // Only update if changed
2122- if newMessage != currentMessage {
2123- // Get the target comment ID (the operation ID for this comment)
2124- targetID := comment.TargetId()
2125-
2126- _, _, err := bug.EditComment(b, author, unixTime, targetID, newMessage, nil, nil)
2127- if err != nil {
2128- return fmt.Errorf("failed to update comment: %w", err)
2129- }
2130-
2131- // Commit the changes
2132- if err := b.Commit(repo); err != nil {
2133- return fmt.Errorf("failed to commit changes: %w", err)
2134- }
2135- }
2136-
2137- // Get IDs for display
2138- bugID := b.Id().String()
2139- commentID := comment.CombinedId().String()
2140- if len(bugID) > 7 {
2141- bugID = bugID[:7]
2142- }
2143- if len(commentID) > 7 {
2144- commentID = commentID[:7]
2145- }
2146-
2147- fmt.Printf("Updated comment %s in bug %s\n", commentID, bugID)
2148- return nil
2149-}
2150-
2151-// runAgentEdit edits an issue or comment as the agent identity
2152-// This is non-interactive and requires at least one of --title or --message
2153-func runAgentEdit(repoPath, idStr, newTitle, newMessage string) error {
2154- // Validate that at least one field is being updated
2155- if strings.TrimSpace(newTitle) == "" && strings.TrimSpace(newMessage) == "" {
2156- return fmt.Errorf("either --title or --message must be provided")
2157- }
2158-
2159- // Resolve the ID (bug or comment)
2160- resolved, err := resolveID(repoPath, idStr)
2161- if err != nil {
2162- return err
2163- }
2164-
2165- // Open repository for identity lookup
2166- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2167- if err != nil {
2168- return fmt.Errorf("failed to open repository: %w", err)
2169- }
2170- defer repo.Close()
2171-
2172- // Get agent identity
2173- author, err := getAgentIdentity(repo)
2174- if err != nil {
2175- return err
2176- }
2177-
2178- unixTime := time.Now().Unix()
2179-
2180- var editErr error
2181- switch resolved.Type {
2182- case IDTypeBug:
2183- editErr = editBugAgent(repo, resolved.Bug, author, unixTime, newTitle, newMessage)
2184- case IDTypeComment:
2185- // For comments, only message is applicable
2186- if strings.TrimSpace(newMessage) == "" {
2187- return fmt.Errorf("--message is required when editing a comment")
2188- }
2189- editErr = editCommentAgent(repo, resolved.Bug, resolved.Comment, author, unixTime, newMessage)
2190- default:
2191- return fmt.Errorf("unknown ID type")
2192- }
2193-
2194- if editErr == nil {
2195- // Invalidate cache so git-bug sees the changes
2196- invalidateGitBugCache(repoPath)
2197- }
2198-
2199- return editErr
2200-}
2201-
2202-// editBugAgent edits an issue as the agent (non-interactive)
2203-func editBugAgent(repo repository.ClockedRepo, b *bug.Bug, author identity.Interface, unixTime int64, newTitle, newMessage string) error {
2204- snap := b.Compile()
2205- currentTitle := snap.Title
2206- currentMessage := ""
2207- if len(snap.Comments) > 0 {
2208- currentMessage = snap.Comments[0].Message
2209- }
2210-
2211- // For bugs, require at least one field to be set
2212- if strings.TrimSpace(newTitle) == "" && strings.TrimSpace(newMessage) == "" {
2213- return fmt.Errorf("either --title or --message must be provided")
2214- }
2215-
2216- // Use current values for fields not provided
2217- if strings.TrimSpace(newTitle) == "" {
2218- newTitle = currentTitle
2219- }
2220- if strings.TrimSpace(newMessage) == "" {
2221- newMessage = currentMessage
2222- }
2223-
2224- changes := false
2225-
2226- // Update title if changed
2227- if newTitle != currentTitle {
2228- _, err := bug.SetTitle(b, author, unixTime, newTitle, nil)
2229- if err != nil {
2230- return fmt.Errorf("failed to update title: %w", err)
2231- }
2232- changes = true
2233- }
2234-
2235- // Update description (first comment) if changed
2236- if newMessage != currentMessage {
2237- _, _, err := bug.EditCreateComment(b, author, unixTime, newMessage, nil, nil)
2238- if err != nil {
2239- return fmt.Errorf("failed to update description: %w", err)
2240- }
2241- changes = true
2242- }
2243-
2244- // Commit changes if any
2245- if changes {
2246- if err := b.Commit(repo); err != nil {
2247- return fmt.Errorf("failed to commit changes: %w", err)
2248- }
2249- }
2250-
2251- // Get short ID for display
2252- bugID := b.Id().String()
2253- if len(bugID) > 7 {
2254- bugID = bugID[:7]
2255- }
2256-
2257- fmt.Printf("Updated bug %s: %s\n", bugID, newTitle)
2258- return nil
2259-}
2260-
2261-// editCommentAgent edits a comment as the agent (non-interactive)
2262-func editCommentAgent(repo repository.ClockedRepo, b *bug.Bug, comment *bug.Comment, author identity.Interface, unixTime int64, newMessage string) error {
2263- currentMessage := comment.Message
2264-
2265- // Validate message
2266- if strings.TrimSpace(newMessage) == "" {
2267- return fmt.Errorf("message cannot be empty")
2268- }
2269-
2270- // Only update if changed
2271- if newMessage != currentMessage {
2272- // Get the target comment ID (the operation ID for this comment)
2273- targetID := comment.TargetId()
2274-
2275- _, _, err := bug.EditComment(b, author, unixTime, targetID, newMessage, nil, nil)
2276- if err != nil {
2277- return fmt.Errorf("failed to update comment: %w", err)
2278- }
2279-
2280- // Commit the changes
2281- if err := b.Commit(repo); err != nil {
2282- return fmt.Errorf("failed to commit changes: %w", err)
2283- }
2284- }
2285-
2286- // Get IDs for display
2287- bugID := b.Id().String()
2288- commentID := comment.CombinedId().String()
2289- if len(bugID) > 7 {
2290- bugID = bugID[:7]
2291- }
2292- if len(commentID) > 7 {
2293- commentID = commentID[:7]
2294- }
2295-
2296- fmt.Printf("Updated comment %s in bug %s\n", commentID, bugID)
2297- return nil
2298-}
2299-
2300-// runOpen opens a bug by changing its status to Open
2301-func runOpen(repoPath, bugIDStr string) error {
2302- // Resolve bug ID
2303- b, err := resolveBugID(repoPath, bugIDStr)
2304- if err != nil {
2305- return err
2306- }
2307-
2308- // Open repository for identity lookup
2309- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2310- if err != nil {
2311- return fmt.Errorf("failed to open repository: %w", err)
2312- }
2313- defer repo.Close()
2314-
2315- // Get user identity
2316- author, err := identity.GetUserIdentity(repo)
2317- if err != nil {
2318- return fmt.Errorf("failed to get user identity: %w\n\nRun 'bug init' first to set up identities", err)
2319- }
2320-
2321- // Open the bug
2322- unixTime := time.Now().Unix()
2323- _, err = bug.Open(b, author, unixTime, nil)
2324- if err != nil {
2325- return fmt.Errorf("failed to open bug: %w", err)
2326- }
2327-
2328- // Commit the changes
2329- if err := b.Commit(repo); err != nil {
2330- return fmt.Errorf("failed to commit changes: %w", err)
2331- }
2332-
2333- // Get short ID for display
2334- bugID := b.Id().String()
2335- if len(bugID) > 7 {
2336- bugID = bugID[:7]
2337- }
2338-
2339- fmt.Printf("Opened bug %s: %s\n", bugID, b.Compile().Title)
2340-
2341- // Invalidate cache so git-bug sees the changes
2342- invalidateGitBugCache(repoPath)
2343-
2344- return nil
2345-}
2346-
2347-// runClose closes a bug by changing its status to Closed
2348-func runClose(repoPath, bugIDStr string) error {
2349- // Resolve bug ID
2350- b, err := resolveBugID(repoPath, bugIDStr)
2351- if err != nil {
2352- return err
2353- }
2354-
2355- // Open repository for identity lookup
2356- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2357- if err != nil {
2358- return fmt.Errorf("failed to open repository: %w", err)
2359- }
2360- defer repo.Close()
2361-
2362- // Get user identity
2363- author, err := identity.GetUserIdentity(repo)
2364- if err != nil {
2365- return fmt.Errorf("failed to get user identity: %w\n\nRun 'bug init' first to set up identities", err)
2366- }
2367-
2368- // Close the bug
2369- unixTime := time.Now().Unix()
2370- _, err = bug.Close(b, author, unixTime, nil)
2371- if err != nil {
2372- return fmt.Errorf("failed to close bug: %w", err)
2373- }
2374-
2375- // Commit the changes
2376- if err := b.Commit(repo); err != nil {
2377- return fmt.Errorf("failed to commit changes: %w", err)
2378- }
2379-
2380- // Get short ID for display
2381- bugID := b.Id().String()
2382- if len(bugID) > 7 {
2383- bugID = bugID[:7]
2384- }
2385-
2386- fmt.Printf("Closed bug %s: %s\n", bugID, b.Compile().Title)
2387-
2388- // Invalidate cache so git-bug sees the changes
2389- invalidateGitBugCache(repoPath)
2390-
2391- return nil
2392-}
2393-
2394-// runAgentOpen opens a bug using the agent identity
2395-func runAgentOpen(repoPath, bugIDStr string) error {
2396- // Resolve bug ID
2397- b, err := resolveBugID(repoPath, bugIDStr)
2398- if err != nil {
2399- return err
2400- }
2401-
2402- // Open repository for identity lookup
2403- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2404- if err != nil {
2405- return fmt.Errorf("failed to open repository: %w", err)
2406- }
2407- defer repo.Close()
2408-
2409- // Get agent identity
2410- author, err := getAgentIdentity(repo)
2411- if err != nil {
2412- return err
2413- }
2414-
2415- // Open the bug
2416- unixTime := time.Now().Unix()
2417- _, err = bug.Open(b, author, unixTime, nil)
2418- if err != nil {
2419- return fmt.Errorf("failed to open bug: %w", err)
2420- }
2421-
2422- // Commit the changes
2423- if err := b.Commit(repo); err != nil {
2424- return fmt.Errorf("failed to commit changes: %w", err)
2425- }
2426-
2427- // Get short ID for display
2428- bugID := b.Id().String()
2429- if len(bugID) > 7 {
2430- bugID = bugID[:7]
2431- }
2432-
2433- fmt.Printf("Opened bug %s: %s\n", bugID, b.Compile().Title)
2434-
2435- // Invalidate cache so git-bug sees the changes
2436- invalidateGitBugCache(repoPath)
2437-
2438- return nil
2439-}
2440-
2441-// runAgentClose closes a bug using the agent identity
2442-func runAgentClose(repoPath, bugIDStr string) error {
2443- // Resolve bug ID
2444- b, err := resolveBugID(repoPath, bugIDStr)
2445- if err != nil {
2446- return err
2447- }
2448-
2449- // Open repository for identity lookup
2450- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2451- if err != nil {
2452- return fmt.Errorf("failed to open repository: %w", err)
2453- }
2454- defer repo.Close()
2455-
2456- // Get agent identity
2457- author, err := getAgentIdentity(repo)
2458- if err != nil {
2459- return err
2460- }
2461-
2462- // Close the bug
2463- unixTime := time.Now().Unix()
2464- _, err = bug.Close(b, author, unixTime, nil)
2465- if err != nil {
2466- return fmt.Errorf("failed to close bug: %w", err)
2467- }
2468-
2469- // Commit the changes
2470- if err := b.Commit(repo); err != nil {
2471- return fmt.Errorf("failed to commit changes: %w", err)
2472- }
2473-
2474- // Get short ID for display
2475- bugID := b.Id().String()
2476- if len(bugID) > 7 {
2477- bugID = bugID[:7]
2478- }
2479-
2480- fmt.Printf("Closed bug %s: %s\n", bugID, b.Compile().Title)
2481-
2482- // Invalidate cache so git-bug sees the changes
2483- invalidateGitBugCache(repoPath)
2484-
2485- return nil
2486-}
2487-
2488-// runRemove removes a bug by its ID
2489-// Resolves ID as bug first, then as comment (though comments cannot be removed individually)
2490-func runRemove(repoPath, idStr string) error {
2491- // Resolve the ID (bug or comment)
2492- resolved, err := resolveID(repoPath, idStr)
2493- if err != nil {
2494- return err
2495- }
2496-
2497- // Open repository
2498- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2499- if err != nil {
2500- return fmt.Errorf("failed to open repository: %w", err)
2501- }
2502- defer repo.Close()
2503-
2504- switch resolved.Type {
2505- case IDTypeBug:
2506- // Remove the bug
2507- if err := bug.Remove(repo, resolved.Bug.Id()); err != nil {
2508- return fmt.Errorf("failed to remove bug: %w", err)
2509- }
2510-
2511- // Get short ID for display
2512- bugID := resolved.Bug.Id().String()
2513- if len(bugID) > 7 {
2514- bugID = bugID[:7]
2515- }
2516-
2517- fmt.Printf("Removed bug %s: %s\n", bugID, resolved.Bug.Compile().Title)
2518-
2519- case IDTypeComment:
2520- // Comments cannot be removed individually in git-bug
2521- return fmt.Errorf("removing individual comments is not supported; use 'bug rm <bug-id>' to remove the entire issue")
2522- default:
2523- return fmt.Errorf("unknown ID type")
2524- }
2525-
2526- // Invalidate cache so git-bug sees the changes
2527- invalidateGitBugCache(repoPath)
2528-
2529- return nil
2530-}
2531-
2532-func main() {
2533- // Get absolute path for repo
2534- if repoPath == "." {
2535- if cwd, err := os.Getwd(); err == nil {
2536- repoPath = cwd
2537- }
2538- }
2539- repoPath, _ = filepath.Abs(repoPath)
2540-
2541- if err := rootCmd.Execute(); err != nil {
2542- fmt.Fprintln(os.Stderr, err)
2543- os.Exit(1)
2544- }
2545-}
+0,
-453
1@@ -1,453 +0,0 @@
2-package main
3-
4-import (
5- "bufio"
6- "os"
7- "os/exec"
8- "path/filepath"
9- "runtime"
10- "strings"
11- "testing"
12-
13- "github.com/git-bug/git-bug/entities/identity"
14- "github.com/git-bug/git-bug/repository"
15-)
16-
17-func TestPromptUser(t *testing.T) {
18- tests := []struct {
19- name string
20- input string
21- prompt string
22- expected string
23- expectError bool
24- }{
25- {
26- name: "simple input",
27- input: "Test User\n",
28- prompt: "Enter name: ",
29- expected: "Test User",
30- },
31- {
32- name: "input with spaces",
33- input: "John Doe\n",
34- prompt: "Enter name: ",
35- expected: "John Doe",
36- },
37- {
38- name: "empty input",
39- input: "\n",
40- prompt: "Enter name: ",
41- expected: "",
42- },
43- {
44- name: "input with trailing newline",
45- input: "[email protected]\n",
46- prompt: "Enter email: ",
47- expected: "[email protected]",
48- },
49- }
50-
51- for _, tt := range tests {
52- t.Run(tt.name, func(t *testing.T) {
53- // Create a scanner from the test input string
54- scanner := bufio.NewScanner(strings.NewReader(tt.input))
55-
56- // Call promptUserWithScanner with our test scanner
57- result, err := promptUserWithScanner(tt.prompt, scanner)
58-
59- if tt.expectError {
60- if err == nil {
61- t.Errorf("expected error, got nil")
62- }
63- return
64- }
65-
66- if err != nil {
67- t.Errorf("unexpected error: %v", err)
68- return
69- }
70-
71- if result != tt.expected {
72- t.Errorf("promptUser() = %q, want %q", result, tt.expected)
73- }
74- })
75- }
76-}
77-
78-func TestDetectJJConfig(t *testing.T) {
79- // Test case 1: No .jj directory
80- t.Run("no_jj_directory", func(t *testing.T) {
81- tmpDir := t.TempDir()
82- name, email, err := detectJJConfig(tmpDir)
83- if err != nil {
84- t.Fatalf("unexpected error: %v", err)
85- }
86- if name != "" || email != "" {
87- t.Errorf("expected empty name and email, got name=%q, email=%q", name, email)
88- }
89- })
90-
91- // Test case 2: .jj directory exists (would need actual jj config in practice)
92- t.Run("jj_directory_exists", func(t *testing.T) {
93- // This test documents the expected behavior when .jj exists
94- // In a real scenario, it would try to run jj commands
95- tmpDir := t.TempDir()
96- jjDir := filepath.Join(tmpDir, ".jj")
97- if err := os.MkdirAll(jjDir, 0755); err != nil {
98- t.Fatalf("failed to create .jj directory: %v", err)
99- }
100- // We expect the function to attempt jj config commands
101- // Since jj is not available in test env, we just verify the directory detection
102- _, _, err := detectJJConfig(tmpDir)
103- // We expect an error since jj binary won't be available
104- if err == nil {
105- t.Skip("jj not available in test environment, skipping")
106- }
107- })
108-}
109-
110-func TestCreateIdentity(t *testing.T) {
111- // Create a temporary directory for a test git repo
112- tmpDir := t.TempDir()
113-
114- // Initialize a git repo
115- initCmd := exec.Command("git", "init")
116- initCmd.Dir = tmpDir
117- if err := initCmd.Run(); err != nil {
118- t.Fatalf("failed to init git repo: %v", err)
119- }
120-
121- // Test creating an identity
122- err := createIdentity(tmpDir, "Test User", "[email protected]", true)
123- if err != nil {
124- t.Fatalf("createIdentity failed: %v", err)
125- }
126-
127- // Verify identity was created by checking if we can get it
128- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
129- if err != nil {
130- t.Fatalf("failed to open repo: %v", err)
131- }
132- defer repo.Close()
133-
134- isSet, err := identity.IsUserIdentitySet(repo)
135- if err != nil {
136- t.Fatalf("IsUserIdentitySet failed: %v", err)
137- }
138- if !isSet {
139- t.Error("expected user identity to be set")
140- }
141-
142- userIdentity, err := identity.GetUserIdentity(repo)
143- if err != nil {
144- t.Fatalf("GetUserIdentity failed: %v", err)
145- }
146-
147- if userIdentity.Name() != "Test User" {
148- t.Errorf("expected name 'Test User', got %q", userIdentity.Name())
149- }
150-
151- if userIdentity.Email() != "[email protected]" {
152- t.Errorf("expected email '[email protected]', got %q", userIdentity.Email())
153- }
154-}
155-
156-func TestInitCommand_InteractiveMode(t *testing.T) {
157- // Create a temporary directory for a test git repo
158- tmpDir := t.TempDir()
159-
160- // Initialize a git repo
161- initCmd := exec.Command("git", "init")
162- initCmd.Dir = tmpDir
163- if err := initCmd.Run(); err != nil {
164- t.Fatalf("failed to init git repo: %v", err)
165- }
166-
167- // Add origin remote (required for bug init)
168- remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
169- remoteCmd.Dir = tmpDir
170- if err := remoteCmd.Run(); err != nil {
171- t.Fatalf("failed to add origin remote: %v", err)
172- }
173-
174- // Test the init function with a mock input (simulating interactive mode)
175- // Since we don't have jj, we'll test the interactive path by mocking stdin
176- input := "Interactive User\[email protected]\n"
177- r, w, _ := os.Pipe()
178- go func() {
179- w.WriteString(input)
180- w.Close()
181- }()
182-
183- // We need to test runInitWithReader to inject our stdin
184- err := runInitWithReader(tmpDir, r)
185- if err != nil {
186- t.Fatalf("runInitWithReader failed: %v", err)
187- }
188-
189- // Verify repo was opened
190- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
191- if err != nil {
192- t.Fatalf("failed to open repo: %v", err)
193- }
194- defer repo.Close()
195-
196- // Check if identities were created
197- ids, err := identity.ListLocalIds(repo)
198- if err != nil {
199- t.Fatalf("ListLocalIds failed: %v", err)
200- }
201-
202- // Should have at least 2 identities (user + agent)
203- if len(ids) < 2 {
204- t.Errorf("expected at least 2 identities (user + agent), got %d", len(ids))
205- }
206-
207- // Verify the user identity has the expected name and email
208- userIdentity, err := identity.GetUserIdentity(repo)
209- if err != nil {
210- t.Fatalf("GetUserIdentity failed: %v", err)
211- }
212-
213- if userIdentity.Name() != "Interactive User" {
214- t.Errorf("expected user name 'Interactive User', got %q", userIdentity.Name())
215- }
216-
217- if userIdentity.Email() != "[email protected]" {
218- t.Errorf("expected user email '[email protected]', got %q", userIdentity.Email())
219- }
220-
221- // Verify the agent identity exists with empty email
222- // The agent identity should be one of the identities with name "agent" and empty email
223- foundAgent := false
224- for _, id := range ids {
225- i, err := identity.ReadLocal(repo, id)
226- if err != nil {
227- t.Fatalf("failed to read identity %s: %v", id, err)
228- }
229- if i.Name() == "agent" && i.Email() == "" {
230- foundAgent = true
231- break
232- }
233- }
234- if !foundAgent {
235- t.Error("expected to find an agent identity with empty email")
236- }
237-}
238-
239-func TestParseEditorContent(t *testing.T) {
240- tests := []struct {
241- name string
242- content string
243- expectedTitle string
244- expectedMessage string
245- expectError bool
246- }{
247- {
248- name: "simple title only",
249- content: "Bug title\n",
250- expectedTitle: "Bug title",
251- expectedMessage: "",
252- },
253- {
254- name: "title with description",
255- content: "Bug title\n\nThis is a description\nwith multiple lines\n",
256- expectedTitle: "Bug title",
257- expectedMessage: "\nThis is a description\nwith multiple lines",
258- },
259- {
260- name: "with comment lines",
261- content: ";; This is a comment\nBug title\n\nDescription here\n;; Another comment",
262- expectedTitle: "Bug title",
263- expectedMessage: "Description here",
264- },
265- {
266- name: "empty content",
267- content: "",
268- expectError: true,
269- },
270- {
271- name: "only comments",
272- content: ";; Comment 1\n;; Comment 2\n",
273- expectError: true,
274- },
275- {
276- name: "leading empty lines",
277- content: "\n\n \nActual title\nDescription",
278- expectedTitle: "Actual title",
279- expectedMessage: "Description",
280- },
281- {
282- name: "markdown headers preserved",
283- content: "Bug title\n\n# Header 1\n## Header 2\nContent here",
284- expectedTitle: "Bug title",
285- expectedMessage: "\n# Header 1\n## Header 2\nContent here",
286- },
287- }
288-
289- for _, tt := range tests {
290- t.Run(tt.name, func(t *testing.T) {
291- title, message, err := parseEditorContent(tt.content)
292-
293- if tt.expectError {
294- if err == nil {
295- t.Errorf("expected error, got nil")
296- }
297- return
298- }
299-
300- if err != nil {
301- t.Errorf("unexpected error: %v", err)
302- return
303- }
304-
305- if title != tt.expectedTitle {
306- t.Errorf("title = %q, want %q", title, tt.expectedTitle)
307- }
308-
309- if message != tt.expectedMessage {
310- t.Errorf("message = %q, want %q", message, tt.expectedMessage)
311- }
312- })
313- }
314-}
315-
316-func TestParseCommentContent(t *testing.T) {
317- tests := []struct {
318- name string
319- content string
320- expectedMessage string
321- expectError bool
322- }{
323- {
324- name: "simple comment",
325- content: "This is a comment\n",
326- expectedMessage: "This is a comment",
327- },
328- {
329- name: "multiline comment",
330- content: "First line\nSecond line\nThird line\n",
331- expectedMessage: "First line\nSecond line\nThird line",
332- },
333- {
334- name: "with comment lines",
335- content: ";; This is a comment\nActual content\n;; Another comment",
336- expectedMessage: "Actual content",
337- },
338- {
339- name: "only comment lines",
340- content: ";; Comment 1\n;; Comment 2\n",
341- expectError: true,
342- },
343- {
344- name: "empty content",
345- content: "",
346- expectError: true,
347- },
348- {
349- name: "only whitespace",
350- content: " \n\n \n",
351- expectError: true,
352- },
353- {
354- name: "leading empty lines",
355- content: "\n\n \nActual content\nMore content",
356- expectedMessage: "Actual content\nMore content",
357- },
358- {
359- name: "trailing empty lines",
360- content: "Actual content\nMore content\n\n \n",
361- expectedMessage: "Actual content\nMore content",
362- },
363- {
364- name: "markdown content",
365- content: "# Header\n\n- List item 1\n- List item 2\n\n**Bold text**",
366- expectedMessage: "# Header\n\n- List item 1\n- List item 2\n\n**Bold text**",
367- },
368- {
369- name: "mixed comments and content",
370- content: ";; Instructions here\n;; More instructions\n\nActual comment\n;; Hidden comment\nMore content\n;; End comment",
371- expectedMessage: "Actual comment\nMore content",
372- },
373- }
374-
375- for _, tt := range tests {
376- t.Run(tt.name, func(t *testing.T) {
377- message, err := parseCommentContent(tt.content)
378-
379- if tt.expectError {
380- if err == nil {
381- t.Errorf("expected error, got nil")
382- }
383- return
384- }
385-
386- if err != nil {
387- t.Errorf("unexpected error: %v", err)
388- return
389- }
390-
391- if message != tt.expectedMessage {
392- t.Errorf("message = %q, want %q", message, tt.expectedMessage)
393- }
394- })
395- }
396-}
397-
398-func TestGetEditor(t *testing.T) {
399- // Save original EDITOR value
400- originalEditor := os.Getenv("EDITOR")
401- defer os.Setenv("EDITOR", originalEditor)
402-
403- tests := []struct {
404- name string
405- editorEnv string
406- check func(t *testing.T, got string)
407- }{
408- {
409- name: "with EDITOR set",
410- editorEnv: "vim",
411- check: func(t *testing.T, got string) {
412- if !strings.Contains(got, "vim") {
413- t.Errorf("getEditor() = %q, want to contain 'vim'", got)
414- }
415- },
416- },
417- {
418- name: "with EDITOR set to path",
419- editorEnv: "/usr/bin/nano",
420- check: func(t *testing.T, got string) {
421- if !strings.Contains(got, "nano") {
422- t.Errorf("getEditor() = %q, want to contain 'nano'", got)
423- }
424- },
425- },
426- {
427- name: "with EDITOR empty uses default",
428- editorEnv: "",
429- check: func(t *testing.T, got string) {
430- if got == "" {
431- t.Error("expected default editor, got empty string")
432- }
433- // Check it returns expected default based on platform
434- if runtime.GOOS == "windows" {
435- if got != "notepad" {
436- t.Errorf("expected 'notepad' on Windows, got %q", got)
437- }
438- } else {
439- if got != "vi" {
440- t.Errorf("expected 'vi' on Unix, got %q", got)
441- }
442- }
443- },
444- },
445- }
446-
447- for _, tt := range tests {
448- t.Run(tt.name, func(t *testing.T) {
449- os.Setenv("EDITOR", tt.editorEnv)
450- got := getEditor()
451- tt.check(t, got)
452- })
453- }
454-}
+0,
-169
1@@ -1,169 +0,0 @@
2-//go:build integration
3-
4-package main
5-
6-import (
7- "os/exec"
8- "strings"
9- "testing"
10-
11- "github.com/git-bug/git-bug/entities/bug"
12- "github.com/git-bug/git-bug/repository"
13-)
14-
15-// TestNewCommand_WithFlags creates a bug using command-line flags
16-func TestNewCommand_WithFlags(t *testing.T) {
17- tmpDir := t.TempDir()
18-
19- // Initialize a git repo
20- initCmd := exec.Command("git", "init")
21- initCmd.Dir = tmpDir
22- if err := initCmd.Run(); err != nil {
23- t.Fatalf("failed to init git repo: %v", err)
24- }
25-
26- // Initialize git-bug identities
27- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
28- t.Fatalf("failed to create identity: %v", err)
29- }
30-
31- // Create a bug using flags
32- title := "Test Bug Title"
33- message := "Test bug description"
34- if err := runNew(tmpDir, title, message); err != nil {
35- t.Fatalf("runNew failed: %v", err)
36- }
37-
38- // Verify bug was created
39- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
40- if err != nil {
41- t.Fatalf("failed to open repo: %v", err)
42- }
43- defer repo.Close()
44-
45- // Count bugs
46- bugCount := 0
47- for streamedBug := range bug.ReadAll(repo) {
48- if streamedBug.Err != nil {
49- t.Fatalf("failed to read bug: %v", streamedBug.Err)
50- }
51- b := streamedBug.Entity
52- snap := b.Compile()
53-
54- if snap.Title == title {
55- bugCount++
56- if snap.Comments[0].Message != message {
57- t.Errorf("message = %q, want %q", snap.Comments[0].Message, message)
58- }
59- if snap.Author.Name() != "Test User" {
60- t.Errorf("author = %q, want %q", snap.Author.Name(), "Test User")
61- }
62- }
63- }
64-
65- if bugCount != 1 {
66- t.Errorf("expected 1 bug with title %q, found %d", title, bugCount)
67- }
68-}
69-
70-// TestNewCommand_TitleOnly creates a bug with only title
71-func TestNewCommand_TitleOnly(t *testing.T) {
72- tmpDir := t.TempDir()
73-
74- // Initialize a git repo
75- initCmd := exec.Command("git", "init")
76- initCmd.Dir = tmpDir
77- if err := initCmd.Run(); err != nil {
78- t.Fatalf("failed to init git repo: %v", err)
79- }
80-
81- // Initialize git-bug identities
82- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
83- t.Fatalf("failed to create identity: %v", err)
84- }
85-
86- // Create a bug with title only
87- title := "Title Only Bug"
88- if err := runNew(tmpDir, title, ""); err != nil {
89- t.Fatalf("runNew failed: %v", err)
90- }
91-
92- // Verify bug was created
93- repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
94- if err != nil {
95- t.Fatalf("failed to open repo: %v", err)
96- }
97- defer repo.Close()
98-
99- // Find the bug
100- found := false
101- for streamedBug := range bug.ReadAll(repo) {
102- if streamedBug.Err != nil {
103- continue
104- }
105- b := streamedBug.Entity
106- snap := b.Compile()
107-
108- if snap.Title == title {
109- found = true
110- if snap.Comments[0].Message != "" {
111- t.Errorf("expected empty message, got %q", snap.Comments[0].Message)
112- }
113- break
114- }
115- }
116-
117- if !found {
118- t.Errorf("bug with title %q not found", title)
119- }
120-}
121-
122-// TestNewCommand_NoIdentityError tests error when identity not set
123-func TestNewCommand_NoIdentityError(t *testing.T) {
124- tmpDir := t.TempDir()
125-
126- // Initialize a git repo (but no git-bug identities)
127- initCmd := exec.Command("git", "init")
128- initCmd.Dir = tmpDir
129- if err := initCmd.Run(); err != nil {
130- t.Fatalf("failed to init git repo: %v", err)
131- }
132-
133- // Try to create a bug without identity
134- err := runNew(tmpDir, "Test Title", "Test message")
135- if err == nil {
136- t.Error("expected error when no identity is set, got nil")
137- }
138-
139- if !strings.Contains(err.Error(), "failed to get user identity") {
140- t.Errorf("expected 'failed to get user identity' error, got: %v", err)
141- }
142-}
143-
144-// TestNewCommand_EmptyTitleError tests error for empty title
145-func TestNewCommand_EmptyTitleError(t *testing.T) {
146- tmpDir := t.TempDir()
147-
148- // Initialize a git repo
149- initCmd := exec.Command("git", "init")
150- initCmd.Dir = tmpDir
151- if err := initCmd.Run(); err != nil {
152- t.Fatalf("failed to init git repo: %v", err)
153- }
154-
155- // Initialize git-bug identities
156- if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
157- t.Fatalf("failed to create identity: %v", err)
158- }
159-
160- // Try to create a bug with empty title (this would normally trigger editor)
161- // For direct testing, we test the validation after editor parsing
162- err := runNew(tmpDir, " ", "") // Whitespace-only title
163- if err == nil {
164- t.Error("expected error for empty title, got nil")
165- }
166-
167- if !strings.Contains(err.Error(), "title cannot be empty") {
168- t.Errorf("expected 'title cannot be empty' error, got: %v", err)
169- }
170-}
+0,
-66
1@@ -1,66 +0,0 @@
2-//go:build integration
3-
4-package main
5-
6-import (
7- "os/exec"
8- "strings"
9- "testing"
10-)
11-
12-// TestReadCommand_Integration tests the read command functionality
13-func TestReadCommand_Integration(t *testing.T) {
14- // Create a temporary directory for our test repo
15- tmpDir := t.TempDir()
16-
17- // Initialize a git repo
18- initCmd := exec.Command("git", "init")
19- initCmd.Dir = tmpDir
20- if err := initCmd.Run(); err != nil {
21- t.Fatalf("failed to init git repo: %v", err)
22- }
23- // Add origin remote for bug init
24- remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
25- remoteCmd.Dir = tmpDir
26- if err := remoteCmd.Run(); err != nil {
27- t.Fatalf("failed to add origin remote: %v", err)
28- }
29-
30- // Initialize bug identities
31- if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
32- t.Fatalf("Failed to init bug: %v", err)
33- }
34-
35- // Create a bug with a title and description
36- title := "Test Issue for Read Command"
37- message := "This is a test description for the read command."
38- if err := runNew(tmpDir, title, message); err != nil {
39- t.Fatalf("Failed to create bug: %v", err)
40- }
41-
42- // Load bugs to get the ID
43- issues, err := LoadBugs(tmpDir)
44- if err != nil {
45- t.Fatalf("Failed to load bugs: %v", err)
46- }
47-
48- if len(issues) == 0 {
49- t.Fatal("No issues found after creating bug")
50- }
51-
52- bugID := issues[0].ShortID
53-
54- // Add a comment to the bug
55- commentMsg := "This is a test comment."
56- if err := runComment(tmpDir, bugID, commentMsg); err != nil {
57- t.Fatalf("Failed to add comment: %v", err)
58- }
59-
60- // Test read command - this should not error
61- // We can't easily capture stdout, but we can verify it doesn't panic or error
62- // For a more complete test, we could refactor runRead to return a string
63- err = runRead(tmpDir, bugID)
64- if err != nil {
65- t.Fatalf("runRead failed: %v", err)
66- }
67-}
+0,
-209
1@@ -1,209 +0,0 @@
2-package main
3-
4-import (
5- "errors"
6- "fmt"
7- "sort"
8- "strings"
9-
10- "github.com/git-bug/git-bug/entities/bug"
11- "github.com/git-bug/git-bug/repository"
12-)
13-
14-// Errors returned by ShortIDMap operations
15-var (
16- ErrIDNotFound = errors.New("full ID not found")
17- ErrAmbiguousPrefix = errors.New("short ID prefix is ambiguous")
18- ErrPrefixNotFound = errors.New("short ID prefix not found")
19-)
20-
21-// ShortIDMap holds the mapping between full IDs and their shortest unique prefixes
22-type ShortIDMap struct {
23- // fullToShort maps full ID โ shortest unique prefix
24- fullToShort map[string]string
25-
26- // shortToFull maps shortest unique prefix โ full ID
27- shortToFull map[string]string
28-
29- // allFullIDs holds all full IDs for prefix matching
30- allFullIDs []string
31-}
32-
33-// ShortIDGenerator creates ShortIDMap from git-bug artifacts (issues and comments)
34-type ShortIDGenerator struct {
35- repoPath string
36-}
37-
38-// NewShortIDGenerator creates a generator for the given repo path
39-func NewShortIDGenerator(repoPath string) *ShortIDGenerator {
40- return &ShortIDGenerator{
41- repoPath: repoPath,
42- }
43-}
44-
45-// diffPosition returns the first position where two strings differ
46-// Returns the length of the shorter string if one is a prefix of the other
47-func diffPosition(a, b string) int {
48- minLen := len(a)
49- if len(b) < minLen {
50- minLen = len(b)
51- }
52-
53- for i := 0; i < minLen; i++ {
54- if a[i] != b[i] {
55- return i
56- }
57- }
58-
59- return minLen
60-}
61-
62-// findMinPrefix calculates the minimum unique prefix length for an ID at the given index
63-// in a sorted slice of IDs. It compares with both previous and next neighbors.
64-func findMinPrefix(sortedIDs []string, index int) int {
65- if len(sortedIDs) == 0 {
66- return 0
67- }
68-
69- if len(sortedIDs) == 1 {
70- return 1
71- }
72-
73- id := sortedIDs[index]
74- maxDiff := 0
75-
76- // Compare with previous neighbor
77- if index > 0 {
78- prevDiff := diffPosition(id, sortedIDs[index-1])
79- if prevDiff > maxDiff {
80- maxDiff = prevDiff
81- }
82- }
83-
84- // Compare with next neighbor
85- if index < len(sortedIDs)-1 {
86- nextDiff := diffPosition(id, sortedIDs[index+1])
87- if nextDiff > maxDiff {
88- maxDiff = nextDiff
89- }
90- }
91-
92- // Minimum unique prefix length is maxDiff + 1
93- // But never exceed the full ID length
94- minLen := maxDiff + 1
95- if minLen > len(id) {
96- minLen = len(id)
97- }
98-
99- return minLen
100-}
101-
102-// Generate builds a ShortIDMap from all artifacts in the repo.
103-// This queries git-bug for all issues and their comments.
104-// Issue IDs are 64-char SHA256 hashes, comment IDs are 64-char CombinedIds
105-// (interleaved BugID + OperationID). Both are unique across the repo.
106-func (g *ShortIDGenerator) Generate() (*ShortIDMap, error) {
107- repo, err := repository.OpenGoGitRepo(g.repoPath, "", nil)
108- if err != nil {
109- return nil, fmt.Errorf("failed to open repository: %w", err)
110- }
111-
112- var allIDs []string
113-
114- // Collect all issue IDs and comment IDs
115- for streamedBug := range bug.ReadAll(repo) {
116- if streamedBug.Err != nil {
117- continue // Skip errors, process what we can
118- }
119-
120- b := streamedBug.Entity
121-
122- // Add issue ID
123- issueID := b.Id().String()
124- allIDs = append(allIDs, issueID)
125-
126- // Add comment IDs
127- // Comment CombinedId is an interleaved ID (BugID + OperationID)
128- // It's unique across the repo because Bug IDs are unique
129- snap := b.Compile()
130- for _, comment := range snap.Comments {
131- commentID := comment.CombinedId().String()
132- allIDs = append(allIDs, commentID)
133- }
134- }
135-
136- if len(allIDs) == 0 {
137- return &ShortIDMap{
138- fullToShort: make(map[string]string),
139- shortToFull: make(map[string]string),
140- allFullIDs: []string{},
141- }, nil
142- }
143-
144- // Sort IDs to enable neighbor comparison
145- sort.Strings(allIDs)
146-
147- // Build mappings
148- fullToShort := make(map[string]string)
149- shortToFull := make(map[string]string)
150-
151- for i, fullID := range allIDs {
152- prefixLen := findMinPrefix(allIDs, i)
153- shortID := fullID[:prefixLen]
154-
155- fullToShort[fullID] = shortID
156- shortToFull[shortID] = fullID
157- }
158-
159- return &ShortIDMap{
160- fullToShort: fullToShort,
161- shortToFull: shortToFull,
162- allFullIDs: allIDs,
163- }, nil
164-}
165-
166-// GetShortID returns the shortest unique prefix for a full ID.
167-// Returns ErrIDNotFound if the full ID is not in the map.
168-func (m *ShortIDMap) GetShortID(fullID string) (string, error) {
169- if m == nil {
170- return "", ErrIDNotFound
171- }
172-
173- shortID, ok := m.fullToShort[fullID]
174- if !ok {
175- return "", fmt.Errorf("%w: %s", ErrIDNotFound, fullID)
176- }
177-
178- return shortID, nil
179-}
180-
181-// GetFullID returns the full ID for a short ID prefix.
182-// The prefix can be the exact short ID or a longer prefix of the full ID.
183-// Returns ErrPrefixNotFound if no ID matches, ErrAmbiguousPrefix if multiple match.
184-func (m *ShortIDMap) GetFullID(shortID string) (string, error) {
185- if m == nil {
186- return "", ErrPrefixNotFound
187- }
188-
189- // First, check if it's an exact match for a short ID
190- if fullID, ok := m.shortToFull[shortID]; ok {
191- return fullID, nil
192- }
193-
194- // Otherwise, search for all IDs that start with this prefix
195- var matches []string
196- for _, fullID := range m.allFullIDs {
197- if strings.HasPrefix(fullID, shortID) {
198- matches = append(matches, fullID)
199- }
200- }
201-
202- switch len(matches) {
203- case 0:
204- return "", fmt.Errorf("%w: %s", ErrPrefixNotFound, shortID)
205- case 1:
206- return matches[0], nil
207- default:
208- return "", fmt.Errorf("%w: prefix %q matches %v", ErrAmbiguousPrefix, shortID, matches)
209- }
210-}
+0,
-46
1@@ -1,46 +0,0 @@
2-//go:build integration
3-// +build integration
4-
5-package main
6-
7-import (
8- "testing"
9-)
10-
11-// TestGenerate_Integration tests the Generate method with a real git-bug repo.
12-// This test requires a git repository with git-bug data at the specified path.
13-// Run with: go test -tags=integration -v ./...
14-func TestGenerate_Integration(t *testing.T) {
15- // This is a placeholder - in practice, you'd need to set up a test repo
16- // or use an environment variable to specify the repo path
17- repoPath := "."
18-
19- gen := NewShortIDGenerator(repoPath)
20- m, err := gen.Generate()
21- if err != nil {
22- // It's okay if there's no git-bug data, just skip
23- t.Skipf("Skipping integration test - no git-bug data: %v", err)
24- }
25-
26- // Verify we can get short IDs for all full IDs
27- for fullID, shortID := range m.fullToShort {
28- gotShortID, err := m.GetShortID(fullID)
29- if err != nil {
30- t.Errorf("GetShortID(%q) failed: %v", fullID, err)
31- continue
32- }
33- if gotShortID != shortID {
34- t.Errorf("GetShortID(%q) = %q, want %q", fullID, gotShortID, shortID)
35- }
36-
37- // Verify we can reverse it
38- gotFullID, err := m.GetFullID(shortID)
39- if err != nil {
40- t.Errorf("GetFullID(%q) failed: %v", shortID, err)
41- continue
42- }
43- if gotFullID != fullID {
44- t.Errorf("GetFullID(%q) = %q, want %q", shortID, gotFullID, fullID)
45- }
46- }
47-}
+0,
-254
1@@ -1,254 +0,0 @@
2-package main
3-
4-import (
5- "errors"
6- "sort"
7- "testing"
8-)
9-
10-func TestDiffPosition(t *testing.T) {
11- tests := []struct {
12- name string
13- a string
14- b string
15- expected int
16- }{
17- {"identical strings", "abc", "abc", 3},
18- {"differs at start", "abc", "xyz", 0},
19- {"differs at position 1", "abc", "axc", 1},
20- {"differs at position 2", "abc", "abx", 2},
21- {"prefix of other", "abc", "abcd", 3},
22- {"empty strings", "", "", 0},
23- {"one empty", "abc", "", 0},
24- {"hex IDs", "87cghty", "87dfty4", 2},
25- }
26-
27- for _, tt := range tests {
28- t.Run(tt.name, func(t *testing.T) {
29- result := diffPosition(tt.a, tt.b)
30- if result != tt.expected {
31- t.Errorf("diffPosition(%q, %q) = %d, want %d", tt.a, tt.b, result, tt.expected)
32- }
33- })
34- }
35-}
36-
37-func TestFindMinPrefix(t *testing.T) {
38- tests := []struct {
39- name string
40- ids []string
41- index int
42- expected int
43- }{
44- {"single ID", []string{"abc"}, 0, 1},
45- {"two IDs - first", []string{"87c", "87d"}, 0, 3},
46- {"two IDs - second", []string{"87c", "87d"}, 1, 3},
47- {"three IDs - first", []string{"87cghty", "87dfty4", "abcdef"}, 0, 3},
48- {"three IDs - middle", []string{"87cghty", "87dfty4", "abcdef"}, 1, 3},
49- {"three IDs - last", []string{"87cghty", "87dfty4", "abcdef"}, 2, 1},
50- {"all unique at start", []string{"a", "b", "c"}, 0, 1},
51- {"shared long prefix", []string{"abcd123", "abcd456", "abcd789"}, 1, 5},
52- {"empty slice", []string{}, 0, 0},
53- }
54-
55- for _, tt := range tests {
56- t.Run(tt.name, func(t *testing.T) {
57- result := findMinPrefix(tt.ids, tt.index)
58- if result != tt.expected {
59- t.Errorf("findMinPrefix(%v, %d) = %d, want %d", tt.ids, tt.index, result, tt.expected)
60- }
61- })
62- }
63-}
64-
65-func TestShortIDMap_GetShortID(t *testing.T) {
66- // Create a test map
67- m := &ShortIDMap{
68- fullToShort: map[string]string{
69- "87cghty": "87c",
70- "87dfty4": "87d",
71- "abcdef": "a",
72- },
73- shortToFull: map[string]string{
74- "87c": "87cghty",
75- "87d": "87dfty4",
76- "a": "abcdef",
77- },
78- allFullIDs: []string{"87cghty", "87dfty4", "abcdef"},
79- }
80-
81- tests := []struct {
82- name string
83- fullID string
84- want string
85- wantErr bool
86- errIs error
87- }{
88- {"existing ID 1", "87cghty", "87c", false, nil},
89- {"existing ID 2", "87dfty4", "87d", false, nil},
90- {"existing ID 3", "abcdef", "a", false, nil},
91- {"non-existent ID", "nonexistent", "", true, ErrIDNotFound},
92- }
93-
94- for _, tt := range tests {
95- t.Run(tt.name, func(t *testing.T) {
96- got, err := m.GetShortID(tt.fullID)
97- if (err != nil) != tt.wantErr {
98- t.Errorf("GetShortID() error = %v, wantErr %v", err, tt.wantErr)
99- return
100- }
101- if tt.wantErr && tt.errIs != nil && !errors.Is(err, tt.errIs) {
102- t.Errorf("GetShortID() error = %v, want error Is %v", err, tt.errIs)
103- return
104- }
105- if got != tt.want {
106- t.Errorf("GetShortID() = %v, want %v", got, tt.want)
107- }
108- })
109- }
110-}
111-
112-func TestShortIDMap_GetShortID_NilMap(t *testing.T) {
113- var m *ShortIDMap
114- _, err := m.GetShortID("test")
115- if !errors.Is(err, ErrIDNotFound) {
116- t.Errorf("expected ErrIDNotFound for nil map, got %v", err)
117- }
118-}
119-
120-func TestShortIDMap_GetFullID(t *testing.T) {
121- // Create a test map
122- m := &ShortIDMap{
123- fullToShort: map[string]string{
124- "87cghty": "87c",
125- "87dfty4": "87d",
126- "abcdef": "a",
127- },
128- shortToFull: map[string]string{
129- "87c": "87cghty",
130- "87d": "87dfty4",
131- "a": "abcdef",
132- },
133- allFullIDs: []string{"87cghty", "87dfty4", "abcdef"},
134- }
135-
136- tests := []struct {
137- name string
138- shortID string
139- want string
140- wantErr bool
141- errIs error
142- }{
143- {"exact short ID 1", "87c", "87cghty", false, nil},
144- {"exact short ID 2", "87d", "87dfty4", false, nil},
145- {"exact short ID 3", "a", "abcdef", false, nil},
146- {"longer prefix", "87cg", "87cghty", false, nil},
147- {"non-existent prefix", "xyz", "", true, ErrPrefixNotFound},
148- {"ambiguous prefix", "87", "", true, ErrAmbiguousPrefix},
149- {"empty prefix", "", "", true, ErrAmbiguousPrefix},
150- }
151-
152- for _, tt := range tests {
153- t.Run(tt.name, func(t *testing.T) {
154- got, err := m.GetFullID(tt.shortID)
155- if (err != nil) != tt.wantErr {
156- t.Errorf("GetFullID() error = %v, wantErr %v", err, tt.wantErr)
157- return
158- }
159- if tt.wantErr && tt.errIs != nil && !errors.Is(err, tt.errIs) {
160- t.Errorf("GetFullID() error = %v, want error Is %v", err, tt.errIs)
161- return
162- }
163- if got != tt.want {
164- t.Errorf("GetFullID() = %v, want %v", got, tt.want)
165- }
166- })
167- }
168-}
169-
170-func TestShortIDMap_GetFullID_NilMap(t *testing.T) {
171- var m *ShortIDMap
172- _, err := m.GetFullID("test")
173- if !errors.Is(err, ErrPrefixNotFound) {
174- t.Errorf("expected ErrPrefixNotFound for nil map, got %v", err)
175- }
176-}
177-
178-func TestShortIDMap_EdgeCases(t *testing.T) {
179- tests := []struct {
180- name string
181- ids []string
182- expectedMap map[string]string // fullID -> expected shortID
183- }{
184- {
185- name: "empty repo",
186- ids: []string{},
187- expectedMap: map[string]string{},
188- },
189- {
190- name: "single ID",
191- ids: []string{"abc123"},
192- expectedMap: map[string]string{
193- "abc123": "a",
194- },
195- },
196- {
197- name: "two IDs differ at start",
198- ids: []string{"abc", "xyz"},
199- expectedMap: map[string]string{
200- "abc": "a",
201- "xyz": "x",
202- },
203- },
204- {
205- name: "shared long prefix",
206- ids: []string{"abcd123", "abcd456", "abcd789"},
207- expectedMap: map[string]string{
208- "abcd123": "abcd1",
209- "abcd456": "abcd4",
210- "abcd789": "abcd7",
211- },
212- },
213- {
214- name: "sequential hex IDs",
215- ids: []string{"87cghty", "87dfty4", "abcdef"},
216- expectedMap: map[string]string{
217- "87cghty": "87c",
218- "87dfty4": "87d",
219- "abcdef": "a",
220- },
221- },
222- }
223-
224- for _, tt := range tests {
225- t.Run(tt.name, func(t *testing.T) {
226- // Sort the IDs like Generate does
227- sort.Strings(tt.ids)
228-
229- // Build the map
230- fullToShort := make(map[string]string)
231- for i, fullID := range tt.ids {
232- prefixLen := findMinPrefix(tt.ids, i)
233- shortID := fullID[:prefixLen]
234- fullToShort[fullID] = shortID
235- }
236-
237- // Verify
238- for fullID, expectedShort := range tt.expectedMap {
239- if got := fullToShort[fullID]; got != expectedShort {
240- t.Errorf("fullID %q: got shortID %q, want %q", fullID, got, expectedShort)
241- }
242- }
243- })
244- }
245-}
246-
247-func TestNewShortIDGenerator(t *testing.T) {
248- gen := NewShortIDGenerator("/path/to/repo")
249- if gen == nil {
250- t.Fatal("NewShortIDGenerator returned nil")
251- }
252- if gen.repoPath != "/path/to/repo" {
253- t.Errorf("repoPath = %q, want %q", gen.repoPath, "/path/to/repo")
254- }
255-}
+0,
-64
1@@ -1,64 +0,0 @@
2-package main
3-
4-import (
5- "fmt"
6-
7- "github.com/git-bug/git-bug/cache"
8- "github.com/git-bug/git-bug/repository"
9- "github.com/git-bug/git-bug/termui"
10- "github.com/spf13/cobra"
11-)
12-
13-// runTerm launches the git-bug terminal UI
14-// It first invalidates the cache to ensure git-bug rebuilds it,
15-// then opens the repository, creates a cache, and runs the termui.
16-func runTerm(repoPath string) error {
17- // Invalidate cache so git-bug is forced to rebuild it
18- invalidateGitBugCache(repoPath)
19-
20- // Open the repository
21- repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
22- if err != nil {
23- return fmt.Errorf("failed to open repository: %w", err)
24- }
25- defer repo.Close()
26-
27- // Create the cache (will rebuild since we invalidated)
28- // Using NewRepoCacheNoEvents to simplify - it waits for all build events
29- cacheInstance, err := cache.NewRepoCacheNoEvents(repo)
30- if err != nil {
31- return fmt.Errorf("failed to create cache: %w", err)
32- }
33- defer cacheInstance.Close()
34-
35- // Run the terminal UI
36- if err := termui.Run(cacheInstance); err != nil {
37- return fmt.Errorf("termui error: %w", err)
38- }
39-
40- return nil
41-}
42-
43-var termCmd = &cobra.Command{
44- Use: "term",
45- Aliases: []string{"termui", "ui"},
46- Short: "Launch the terminal UI to browse and edit bugs",
47- Long: `Launch the git-bug terminal UI to browse and edit bugs interactively.
48-
49-This command starts the full-featured terminal interface for managing git-bug
50-issues. It provides a two-pane view with a bug list and detailed bug view,
51-allowing you to browse, create, edit, and comment on bugs.
52-
53-Before starting the UI, the cache is invalidated to ensure git-bug rebuilds
54-it with the latest data. This keeps the 'bug' command and 'git-bug' binary
55-in sync.
56-
57-Examples:
58- bug term # Launch terminal UI in current directory
59- bug termui # Same as above (alias)
60- bug ui # Same as above (alias)
61- bug term --repo /path/to/repo # Launch UI for specific repository`,
62- RunE: func(cmd *cobra.Command, args []string) error {
63- return runTerm(repoPath)
64- },
65-}
+1,
-1
1@@ -218,7 +218,7 @@ func (s *SummaryPageData) Active() string { return "summary" }
2 func (s *TreePageData) Active() string { return "code" }
3 func (s *FilePageData) Active() string { return "code" }
4 func (s *LogPageData) Active() string { return "commits" }
5-func (s *CommitPageData) Active() string { return "commits" }
6+func (s *CommitPageData) Active() string { return "commit" }
7 func (s *RefPageData) Active() string { return "refs" }
8
9 func Bail(err error) {
+61,
-0
1@@ -0,0 +1,61 @@
2+{
3+ "nodes": {
4+ "flake-utils": {
5+ "inputs": {
6+ "systems": "systems"
7+ },
8+ "locked": {
9+ "lastModified": 1731533236,
10+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
11+ "owner": "numtide",
12+ "repo": "flake-utils",
13+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
14+ "type": "github"
15+ },
16+ "original": {
17+ "owner": "numtide",
18+ "repo": "flake-utils",
19+ "type": "github"
20+ }
21+ },
22+ "nixpkgs": {
23+ "locked": {
24+ "lastModified": 1775710090,
25+ "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
26+ "owner": "NixOS",
27+ "repo": "nixpkgs",
28+ "rev": "4c1018dae018162ec878d42fec712642d214fdfa",
29+ "type": "github"
30+ },
31+ "original": {
32+ "owner": "NixOS",
33+ "ref": "nixos-unstable",
34+ "repo": "nixpkgs",
35+ "type": "github"
36+ }
37+ },
38+ "root": {
39+ "inputs": {
40+ "flake-utils": "flake-utils",
41+ "nixpkgs": "nixpkgs"
42+ }
43+ },
44+ "systems": {
45+ "locked": {
46+ "lastModified": 1681028828,
47+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
48+ "owner": "nix-systems",
49+ "repo": "default",
50+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
51+ "type": "github"
52+ },
53+ "original": {
54+ "owner": "nix-systems",
55+ "repo": "default",
56+ "type": "github"
57+ }
58+ }
59+ },
60+ "root": "root",
61+ "version": 7
62+}
+46,
-0
1@@ -0,0 +1,46 @@
2+{
3+ description = "pgit - static site generator for git repositories";
4+
5+ inputs = {
6+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
7+ flake-utils.url = "github:numtide/flake-utils";
8+ };
9+
10+ outputs = { self, nixpkgs, flake-utils }:
11+ flake-utils.lib.eachDefaultSystem (system:
12+ let
13+ pkgs = nixpkgs.legacyPackages.${system};
14+
15+ # Read the version from go.mod or use a default
16+ version = "0.1.0";
17+ in
18+ {
19+ packages.default = pkgs.buildGoModule {
20+ pname = "pgit";
21+ inherit version;
22+ src = ./.;
23+
24+ # This hash will need to be updated based on go.sum
25+ # Run 'nix build' and it will fail with the expected hash
26+ vendorHash = "sha256-95H+k3LHaB6WjnLpwjviCopwfO9MKbyiVKB5HWyNZgE=";
27+
28+ subPackages = [ "cmd/pgit" ];
29+
30+ meta = with pkgs.lib; {
31+ description = "Static site generator for git repositories";
32+ homepage = "https://code.kilimanjaro.io/pgit";
33+ license = licenses.gpl3Only;
34+ mainProgram = "pgit";
35+ };
36+ };
37+
38+ devShells.default = pkgs.mkShell {
39+ buildInputs = with pkgs; [
40+ go
41+ git
42+ gopls
43+ golangci-lint
44+ ];
45+ };
46+ });
47+}
M
go.mod
+0,
-19
1@@ -4,7 +4,6 @@ go 1.25.0
2
3 require (
4 github.com/alecthomas/chroma/v2 v2.13.0
5- github.com/charmbracelet/lipgloss v1.1.0
6 github.com/dustin/go-humanize v1.0.1
7 github.com/git-bug/git-bug v0.10.1
8 github.com/gogs/git-module v1.6.0
9@@ -17,12 +16,9 @@ require (
10 dario.cat/mergo v1.0.2 // indirect
11 github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
12 github.com/99designs/keyring v1.2.2 // indirect
13- github.com/MichaelMure/go-term-text v0.3.1 // indirect
14 github.com/Microsoft/go-winio v0.6.2 // indirect
15 github.com/ProtonMail/go-crypto v1.4.1 // indirect
16 github.com/RoaringBitmap/roaring v1.9.4 // indirect
17- github.com/awesome-gocui/gocui v1.1.0 // indirect
18- github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
19 github.com/bits-and-blooms/bitset v1.24.4 // indirect
20 github.com/blevesearch/bleve v1.0.14 // indirect
21 github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
22@@ -34,13 +30,6 @@ require (
23 github.com/blevesearch/zap/v13 v13.0.6 // indirect
24 github.com/blevesearch/zap/v14 v14.0.5 // indirect
25 github.com/blevesearch/zap/v15 v15.0.3 // indirect
26- github.com/charmbracelet/colorprofile v0.4.1 // indirect
27- github.com/charmbracelet/x/ansi v0.11.6 // indirect
28- github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
29- github.com/charmbracelet/x/term v0.2.2 // indirect
30- github.com/clipperhouse/displaywidth v0.9.0 // indirect
31- github.com/clipperhouse/stringish v0.1.1 // indirect
32- github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
33 github.com/cloudflare/circl v1.6.3 // indirect
34 github.com/couchbase/vellum v1.0.2 // indirect
35 github.com/cyphar/filepath-securejoin v0.6.1 // indirect
36@@ -50,8 +39,6 @@ require (
37 github.com/dvsekhvalnov/jose2go v1.8.0 // indirect
38 github.com/emirpasic/gods v1.18.1 // indirect
39 github.com/fatih/color v1.19.0 // indirect
40- github.com/gdamore/encoding v1.0.1 // indirect
41- github.com/gdamore/tcell/v2 v2.7.4 // indirect
42 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
43 github.com/go-git/go-billy/v5 v5.8.0 // indirect
44 github.com/go-git/go-git/v5 v5.17.2 // indirect
45@@ -60,23 +47,18 @@ require (
46 github.com/golang/protobuf v1.5.4 // indirect
47 github.com/golang/snappy v1.0.0 // indirect
48 github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
49- github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
50 github.com/inconshreveable/mousetrap v1.1.0 // indirect
51 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
52 github.com/kevinburke/ssh_config v1.6.0 // indirect
53 github.com/klauspost/cpuid/v2 v2.3.0 // indirect
54- github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
55 github.com/mattn/go-colorable v0.1.14 // indirect
56 github.com/mattn/go-isatty v0.0.20 // indirect
57- github.com/mattn/go-runewidth v0.0.19 // indirect
58 github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 // indirect
59 github.com/mschoch/smat v0.2.0 // indirect
60 github.com/mtibben/percent v0.2.1 // indirect
61- github.com/muesli/termenv v0.16.0 // indirect
62 github.com/pjbgf/sha1cd v0.5.0 // indirect
63 github.com/pkg/errors v0.9.1 // indirect
64 github.com/pmezard/go-difflib v1.0.0 // indirect
65- github.com/rivo/uniseg v0.4.7 // indirect
66 github.com/sergi/go-diff v1.4.0 // indirect
67 github.com/skeema/knownhosts v1.3.2 // indirect
68 github.com/spf13/pflag v1.0.9 // indirect
69@@ -85,7 +67,6 @@ require (
70 github.com/tdewolff/parse/v2 v2.8.11 // indirect
71 github.com/willf/bitset v1.1.11 // indirect
72 github.com/xanzy/ssh-agent v0.3.3 // indirect
73- github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
74 go.etcd.io/bbolt v1.4.3 // indirect
75 golang.org/x/crypto v0.49.0 // indirect
76 golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
M
go.sum
+0,
-77
1@@ -5,8 +5,6 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN
2 github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
3 github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
4 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
5-github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0=
6-github.com/MichaelMure/go-term-text v0.3.1/go.mod h1:QgVjAEDUnRMlzpS6ky5CGblux7ebeiLnuy9dAaFZu8o=
7 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
8 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
9 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
10@@ -26,10 +24,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
11 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
12 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
13 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
14-github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII=
15-github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg=
16-github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
17-github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
18 github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
19 github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
20 github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
21@@ -58,22 +52,6 @@ github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67n
22 github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY=
23 github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY=
24 github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU=
25-github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
26-github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
27-github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
28-github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
29-github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
30-github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
31-github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
32-github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
33-github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
34-github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
35-github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
36-github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
37-github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
38-github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
39-github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
40-github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
41 github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
42 github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
43 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
44@@ -112,12 +90,6 @@ github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+ne
45 github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
46 github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
47 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
48-github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
49-github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
50-github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
51-github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
52-github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
53-github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
54 github.com/git-bug/git-bug v0.10.1 h1:mjJK/wrfKWUze5EkVpJj+3N3LzSfeujqqH1qq8x4aco=
55 github.com/git-bug/git-bug v0.10.1/go.mod h1:43BRtb/Nr6QEJlNLkLY/64vyopxA0y5nvjSNsktnan8=
56 github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
57@@ -153,8 +125,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
58 github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
59 github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
60 github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
61-github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
62-github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
63 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
64 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
65 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
66@@ -180,20 +150,11 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
67 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
68 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
69 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
70-github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
71-github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
72-github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
73-github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
74 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
75 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
76 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
77 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
78 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
79-github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
80-github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
81-github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
82-github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
83-github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
84 github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk=
85 github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
86 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
87@@ -203,8 +164,6 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
88 github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
89 github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
90 github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
91-github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
92-github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
93 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
94 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
95 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
96@@ -221,11 +180,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
97 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
98 github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
99 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
100-github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
101-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
102-github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
103-github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
104-github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
105 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
106 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
107 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
108@@ -273,43 +227,28 @@ github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
109 github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
110 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
111 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
112-github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
113-github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
114 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
115-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
116 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
117 go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
118 go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
119 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
120 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
121-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
122-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
123 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
124 golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
125 golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
126 golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
127 golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
128-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
129-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
130 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
131-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
132-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
133 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
134-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
135-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
136 golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
137 golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
138 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
139-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
140 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
141-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
142-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
143 golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
144 golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
145 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
146 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
147 golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
148-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
149 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
150 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
151 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
152@@ -317,34 +256,18 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
153 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
154 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
155 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
156-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
157 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
158-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
159-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
160 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
161-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
162 golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
163 golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
164 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
165-golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
166-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
167-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
168-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
169 golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
170 golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
171 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
172-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
173 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
174-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
175-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
176-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
177 golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
178 golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
179 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
180-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
181-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
182-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
183-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
184 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
185 google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
186 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+5,
-2
1@@ -36,8 +36,11 @@
2 <div class="issue-comment">
3 <div class="issue-comment__header">
4 <span class="issue-comment__author">{{.Author}}</span>
5- <span class="issue-comment__date">
6- <span class="human-time" data-time="{{.CreatedAtISO}}">{{.CreatedAtDisp}}</span>
7+ <span class="issue-comment__meta">
8+ <span class="issue-comment__id">{{.ID}}</span>
9+ <span class="issue-comment__date">
10+ <span class="human-time" data-time="{{.CreatedAtISO}}">{{.CreatedAtDisp}}</span>
11+ </span>
12 </span>
13 </div>
14 <div class="issue-comment__body markdown">{{.Body}}</div>
+3,
-1
1@@ -50,6 +50,7 @@ type IssueData struct {
2 }
3
4 type CommentData struct {
5+ ID string
6 Author string
7 CreatedAt string
8 CreatedAtISO string
9@@ -75,7 +76,7 @@ type IssueDetailPageData struct {
10 }
11
12 func (i *IssuesListPageData) Active() string { return "issues" }
13-func (i *IssueDetailPageData) Active() string { return "issues" }
14+func (i *IssueDetailPageData) Active() string { return "issue" }
15
16 func (c *Config) loadIssues() ([]*IssueData, error) {
17 c.Logger.Info("loading issues from git-bug", "repoPath", c.RepoPath)
18@@ -113,6 +114,7 @@ func (c *Config) loadIssues() ([]*IssueData, error) {
19 }
20 createdAtTime, _ := time.Parse("Mon Jan 2 15:04:05 2006 -0700", comment.FormatTime())
21 comments = append(comments, CommentData{
22+ ID: GetShortID(comment.CombinedId().String()),
23 Author: comment.Author.Name(),
24 CreatedAt: comment.FormatTime(),
25 CreatedAtISO: createdAtTime.UTC().Format(time.RFC3339),
+17,
-0
1@@ -1410,6 +1410,23 @@ sup {
2 font-size: 0.8rem;
3 }
4
5+.issue-comment__meta {
6+ display: flex;
7+ align-items: center;
8+ gap: 0.5rem;
9+}
10+
11+.issue-comment__id {
12+ color: var(--grey-light);
13+ font-size: 0.8rem;
14+ font-family: monospace;
15+}
16+
17+.issue-comment__id::after {
18+ content: " ยท ";
19+ margin-left: 0.25rem;
20+}
21+
22 .issue-comment__body {
23 line-height: var(--line-height);
24 }