arche / internal/cli/cmd_snap.go

commit 154431fd
  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}