package patch import ( "bufio" "bytes" "fmt" "io" "os" "os/exec" "sort" "strings" "time" "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands" "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib/pama" "git.sr.ht/~rjarry/aerc/lib/pama/models" "git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/log" ) type Rebase struct { Commit string `opt:"commit" required:"false"` } func init() { register(Rebase{}) } func (Rebase) Context() commands.CommandContext { return commands.GLOBAL } func (Rebase) Aliases() []string { return []string{"rebase"} } func (r Rebase) Execute(args []string) error { m := pama.New() current, err := m.CurrentProject() if err != nil { return err } baseID := r.Commit if baseID == "" { baseID = current.Base.ID } commits, err := m.RebaseCommits(current, baseID) if err != nil { return err } if len(commits) == 0 { err := m.SaveRebased(current, baseID, nil) if err != nil { return fmt.Errorf("No commits to rebase, but saving of new reference failed: %w", err) } app.PushStatus("No commits to rebase.", 10*time.Second) return nil } rebase := newRebase(commits) f, err := os.CreateTemp("", "aerc-patch-rebase-*") if err != nil { return err } name := f.Name() _, err = io.Copy(f, rebase.content()) if err != nil { return err } f.Close() createWidget := func() (ui.DrawableInteractive, error) { editorCmd, err := app.CmdFallbackSearch(config.EditorCmds(), true) if err != nil { return nil, err } editor := exec.Command("/bin/sh", "-c", editorCmd+" "+name) term, err := app.NewTerminal(editor) if err != nil { return nil, err } term.OnClose = func(_ error) { app.CloseDialog() defer os.Remove(name) defer term.Focus(false) f, err := os.Open(name) if err != nil { app.PushError(fmt.Sprintf("failed to open file: %v", err)) return } defer f.Close() if editor.ProcessState.ExitCode() > 0 { app.PushError("Quitting rebase without saving.") return } err = m.SaveRebased(current, baseID, rebase.parse(f)) if err != nil { app.PushError(fmt.Sprintf("Failed to save rebased commits: %v", err)) return } app.PushStatus("Successfully rebased.", 10*time.Second) } term.Show(true) term.Focus(true) return term, nil } viewer, err := createWidget() if err != nil { return err } app.AddDialog(app.LargeDialog( ui.NewBox(viewer, fmt.Sprintf("Patch Rebase on %-6.6s", baseID), "", app.SelectedAccountUiConfig(), ), )) return nil } type rebase struct { commits []models.Commit table map[string]models.Commit order []string } func newRebase(commits []models.Commit) *rebase { return &rebase{ commits: commits, table: make(map[string]models.Commit), } } const ( header string = "" footer string = ` # Rebase aerc's patch data. This will not affect the underlying repository in # any way. # # Change the name in the first column to assign a new tag to a commit. To group # multiple commits, use the same tag name. # # An 'untracked' tag indicates that aerc lost track of that commit, either due # to a commit-hash change or because that commit was applied outside of aerc. # # Do not change anything else besides the tag names (first column). # # Do not reorder the lines. The ordering should remain as in the repository. # # If you remove a line or keep an 'untracked' tag, those commits will be removed # from aerc's patch tracking. # ` ) func (r *rebase) content() io.Reader { var buf bytes.Buffer buf.WriteString(header) for _, c := range r.commits { tag := c.Tag if tag == "" { tag = models.Untracked } shortHash := fmt.Sprintf("%6.6s", c.ID) buf.WriteString( fmt.Sprintf("%-12s %6.6s %s\n", tag, shortHash, c.Info())) r.table[shortHash] = c r.order = append(r.order, shortHash) } buf.WriteString(footer) return &buf } func (r *rebase) parse(reader io.Reader) []models.Commit { var commits []models.Commit var hashes []string scanner := bufio.NewScanner(reader) duplicated := make(map[string]struct{}) for scanner.Scan() { s := scanner.Text() i := strings.Index(s, "#") if i >= 0 { s = s[:i] } if strings.TrimSpace(s) == "" { continue } fds := strings.Fields(s) if len(fds) < 2 { continue } tag, shortHash := fds[0], fds[1] if tag == models.Untracked { continue } _, dedup := duplicated[shortHash] if dedup { log.Warnf("rebase: skipping duplicated hash: %s", shortHash) continue } hashes = append(hashes, shortHash) c, ok := r.table[shortHash] if !ok { log.Errorf("Looks like the commit hashes were changed "+ "during the rebase. Dropping: %v", shortHash) continue } log.Tracef("save commit %s with tag %s", shortHash, tag) c.Tag = tag commits = append(commits, c) duplicated[shortHash] = struct{}{} } reorder(commits, hashes, r.order) return commits } func reorder(toSort []models.Commit, now []string, by []string) { byMap := make(map[string]int) for i, s := range by { byMap[s] = i } complete := true for _, s := range now { _, ok := byMap[s] complete = complete && ok } if !complete { return } sort.SliceStable(toSort, func(i, j int) bool { return byMap[now[i]] < byMap[now[j]] }) }