1package cli
2
3import (
4 "bufio"
5 "fmt"
6 "os"
7 "strings"
8
9 "arche/internal/gitcompat"
10 "arche/internal/tui"
11 "arche/internal/wc"
12
13 "github.com/spf13/cobra"
14)
15
16var (
17 snapInteractive bool
18 snapSign bool
19 snapSignKey string
20 snapAmend bool
21 snapNoAdvance bool
22)
23
24var snapCmd = &cobra.Command{
25 Use: "snap [message]",
26 Aliases: []string{"commit"},
27 Short: "Finalise the working copy draft into a named commit",
28 Long: `Snapshot the current working directory into the draft commit (optionally
29setting a message), finalise it, and create a new empty draft as the next
30HEAD. The snapped commit keeps its change ID; only the content hash changes
31if files were modified since the last snap.
32
33With --interactive (-i) you are shown each hunk and asked whether to include
34it in this snap. Unselected hunks remain as working-copy changes in the new
35draft.`,
36 RunE: func(cmd *cobra.Command, args []string) error {
37 r := openRepo()
38 defer r.Close()
39
40 msg := strings.Join(args, " ")
41 if msg == "" {
42 msg = promptMessage()
43 }
44 if msg == "" {
45 return fmt.Errorf("aborting snap: empty commit message")
46 }
47
48 w := wc.New(r)
49
50 if snapNoAdvance {
51 w.NoAutoAdvance = true
52 }
53
54 if snapSign || r.Cfg.Sign.Auto {
55 w.SignKey = r.Cfg.Sign.KeyFile
56 if snapSignKey != "" {
57 w.SignKey = snapSignKey
58 }
59 }
60
61 if snapAmend {
62 amended, amendedID, err := w.Amend(msg)
63 if err != nil {
64 return err
65 }
66 signedLabel := ""
67 if amended.CommitSig != nil {
68 signedLabel = " [signed]"
69 }
70 fmt.Printf("Amended %s - %s%s\n", "ch:"+amended.ChangeID, amended.Message, signedLabel)
71 fmt.Printf(" %x\n", amendedID[:8])
72 return nil
73 }
74
75 if snapInteractive {
76 diffs, err := w.ComputeWorkingDiffs()
77 if err != nil {
78 return err
79 }
80 if len(diffs) == 0 {
81 return fmt.Errorf("nothing to snap: working copy is clean")
82 }
83
84 var items []tui.HunkItem
85 for _, fhd := range diffs {
86 for hi, h := range fhd.Hunks {
87 items = append(items, tui.HunkItem{
88 FilePath: fhd.Path,
89 HunkIdx: hi,
90 TotalHunksInFile: len(fhd.Hunks),
91 Hunk: h,
92 })
93 }
94 }
95
96 sel, err := tui.RunHunkSelector(items, "include in snap")
97 if err != nil {
98 return err
99 }
100 if sel.Cancelled {
101 fmt.Fprintln(os.Stderr, "Interactive snap cancelled.")
102 return nil
103 }
104
105 perFile := make(map[string][]bool)
106 idx := 0
107 for _, fhd := range diffs {
108 n := len(fhd.Hunks)
109 perFile[fhd.Path] = sel.Selected[idx : idx+n]
110 idx += n
111 }
112
113 snapped, snappedID, err := w.SnapSelectedHunks(msg, diffs, perFile)
114 if err != nil {
115 return err
116 }
117
118 signedLabelI := ""
119 if snapped.CommitSig != nil {
120 signedLabelI = " [signed]"
121 }
122 fmt.Printf("Snapped %s - %s%s\n", "ch:"+snapped.ChangeID, snapped.Message, signedLabelI)
123 fmt.Printf(" %x\n", snappedID[:8])
124
125 if r.Cfg.Git.Enabled {
126 gitHash, err := gitcompat.MirrorCommit(r.Root, r, snappedID)
127 if err != nil {
128 fmt.Fprintf(os.Stderr, "arche: git mirror failed: %v\n", err)
129 } else if gitHash != "" {
130 fmt.Printf(" git: %s\n", gitHash[:8])
131 }
132 }
133
134 head, _ := r.Head()
135 fmt.Printf("Working copy now at %s (draft)\n", head)
136 return nil
137 }
138
139 snapped, snappedID, err := w.Snap(msg)
140 if err != nil {
141 return err
142 }
143
144 signedLabel := ""
145 if snapped.CommitSig != nil {
146 signedLabel = " [signed]"
147 }
148
149 fmt.Printf("Snapped %s - %s%s\n", "ch:"+snapped.ChangeID, snapped.Message, signedLabel)
150 fmt.Printf(" %x\n", snappedID[:8])
151
152 if r.Cfg.Git.Enabled {
153 gitHash, err := gitcompat.MirrorCommit(r.Root, r, snappedID)
154 if err != nil {
155 fmt.Fprintf(os.Stderr, "arche: git mirror failed: %v\n", err)
156 } else if gitHash != "" {
157 fmt.Printf(" git: %s\n", gitHash[:8])
158 }
159 }
160
161 head, _ := r.Head()
162 fmt.Printf("Working copy now at %s (draft, empty)\n", head)
163 return nil
164 },
165}
166
167func promptMessage() string {
168 fmt.Print("Commit message: ")
169 sc := bufio.NewScanner(os.Stdin)
170 sc.Scan()
171 return strings.TrimSpace(sc.Text())
172}
173
174func init() {
175 snapCmd.Flags().BoolVarP(&snapInteractive, "interactive", "i", false, "interactively select hunks to include in this snap")
176 snapCmd.Flags().BoolVar(&snapSign, "sign", false, "sign the commit with your SSH key")
177 snapCmd.Flags().StringVar(&snapSignKey, "key", "", "path to SSH private key to use for signing (default: auto-detect)")
178 snapCmd.Flags().BoolVar(&snapAmend, "amend", false, "amend the current commit in-place and auto-rebase downstream draft dependents")
179 snapCmd.Flags().BoolVar(&snapNoAdvance, "no-advance", false, "disable bookmark auto-advance for this snap (overrides config)")
180}