diff options
Diffstat (limited to 'commands/patch/rebase.go')
-rw-r--r-- | commands/patch/rebase.go | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/commands/patch/rebase.go b/commands/patch/rebase.go new file mode 100644 index 00000000..10da2a63 --- /dev/null +++ b/commands/patch/rebase.go @@ -0,0 +1,249 @@ +package patch + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strings" + "time" + + "git.sr.ht/~rjarry/aerc/app" + "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) 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.NewDialog( + ui.NewBox(viewer, fmt.Sprintf("Patch Rebase on %-6.6s", baseID), "", + app.SelectedAccountUiConfig(), + ), + // start pos on screen + func(h int) int { + return h / 8 + }, + // dialog height + func(h int) int { + return h - 2*h/8 + }, + )) + + 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]] + }) +} |