mirror of https://git.sr.ht/~rjarry/aerc
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
251 lines
5.2 KiB
Go
251 lines
5.2 KiB
Go
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/log"
|
|
"git.sr.ht/~rjarry/aerc/lib/pama"
|
|
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
|
)
|
|
|
|
type Rebase struct {
|
|
Commit string `opt:"commit" required:"false"`
|
|
}
|
|
|
|
func init() {
|
|
register(Rebase{})
|
|
}
|
|
|
|
func (Rebase) Description() string {
|
|
return "Rebase the patch data."
|
|
}
|
|
|
|
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.DefaultDialog(
|
|
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]]
|
|
})
|
|
}
|