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.
aerc/commands/patch/apply.go

269 lines
5.1 KiB
Go

package patch
import (
"fmt"
"sort"
"strings"
"unicode"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/commands/msg"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/pama"
"git.sr.ht/~rjarry/aerc/lib/pama/models"
)
type Apply struct {
Cmd string `opt:"-c" desc:"Apply patches with provided command."`
Worktree string `opt:"-w" desc:"Create linked worktree on this <commit-ish>."`
Tag string `opt:"tag" required:"true" complete:"CompleteTag" desc:"Identify patches with tag."`
}
func init() {
register(Apply{})
}
func (Apply) Description() string {
return "Apply the selected message(s) to the current project."
}
func (Apply) Context() commands.CommandContext {
return commands.MESSAGE_LIST | commands.MESSAGE_VIEWER
}
func (Apply) Aliases() []string {
return []string{"apply"}
}
func (*Apply) CompleteTag(arg string) []string {
patches, err := pama.New().CurrentPatches()
if err != nil {
log.Errorf("failed to current patches for completion: %v", err)
patches = nil
}
acct := app.SelectedAccount()
if acct == nil {
return nil
}
uids, err := acct.MarkedMessages()
if err != nil {
return nil
}
if len(uids) == 0 {
msg, err := acct.SelectedMessage()
if err == nil {
uids = append(uids, msg.Uid)
}
}
store := acct.Store()
if store == nil {
return nil
}
var subjects []string
for _, uid := range uids {
if msg, ok := store.Messages[uid]; !ok || msg == nil || msg.Envelope == nil {
continue
} else {
subjects = append(subjects, msg.Envelope.Subject)
}
}
return proposePatchName(patches, subjects)
}
func (a Apply) Execute(args []string) error {
patch := a.Tag
worktree := a.Worktree
applyCmd := a.Cmd
m := pama.New()
p, err := m.CurrentProject()
if err != nil {
return err
}
log.Tracef("Current project: %v", p)
if worktree != "" {
p, err = m.CreateWorktree(p, worktree, patch)
if err != nil {
return err
}
err = m.SwitchProject(p.Name)
if err != nil {
log.Warnf("could not switch to worktree project: %v", err)
}
}
if models.Commits(p.Commits).HasTag(patch) {
return fmt.Errorf("Patch name '%s' already exists.", patch)
}
if !m.Clean(p) {
return fmt.Errorf("Aborting... There are unstaged changes in " +
"your repository.")
}
commit, err := m.Head(p)
if err != nil {
return err
}
log.Tracef("HEAD commit before: %s", commit)
if applyCmd != "" {
rootFmt := "%r"
if strings.Contains(applyCmd, rootFmt) {
applyCmd = strings.ReplaceAll(applyCmd, rootFmt, p.Root)
}
log.Infof("use custom apply command: %s", applyCmd)
} else {
applyCmd, err = m.ApplyCmd(p)
if err != nil {
return err
}
}
msgData := collectMessageData()
// apply patches with the pipe cmd
pipe := msg.Pipe{
Background: false,
Full: true,
Part: false,
Command: applyCmd,
}
return pipe.Run(func() {
p, err = m.ApplyUpdate(p, patch, commit, msgData)
if err != nil {
log.Errorf("Failed to save patch data: %v", err)
}
})
}
// collectMessageData returns a map where the key is the message id and the
// value the subject of the marked messages
func collectMessageData() map[string]string {
acct := app.SelectedAccount()
if acct == nil {
return nil
}
uids, err := commands.MarkedOrSelected(acct)
if err != nil {
log.Errorf("error occurred: %v", err)
return nil
}
store := acct.Store()
if store == nil {
return nil
}
kv := make(map[string]string)
for _, uid := range uids {
msginfo, ok := store.Messages[uid]
if !ok || msginfo == nil {
continue
}
id, err := msginfo.MsgId()
if err != nil {
continue
}
if msginfo.Envelope == nil {
continue
}
kv[id] = msginfo.Envelope.Subject
}
return kv
}
func proposePatchName(patches, subjects []string) []string {
parse := func(s string) (string, string, bool) {
var tag strings.Builder
var version string
var i, j int
i = strings.Index(s, "[")
if i < 0 {
goto noPatch
}
s = s[i+1:]
j = strings.Index(s, "]")
if j < 0 {
goto noPatch
}
for _, elem := range strings.Fields(s[:j]) {
vers := strings.ToLower(elem)
if !strings.HasPrefix(vers, "v") {
continue
}
isVersion := true
for _, r := range vers[1:] {
if !unicode.IsDigit(r) {
isVersion = false
break
}
}
if isVersion {
version = vers
break
}
}
s = strings.TrimSpace(s[j+1:])
for _, r := range s {
if unicode.IsSpace(r) || r == ':' {
break
}
_, err := tag.WriteRune(r)
if err != nil {
continue
}
}
return tag.String(), version, true
noPatch:
return "", "", false
}
summary := make(map[string]struct{})
var results []string
for _, s := range subjects {
tag, version, isPatch := parse(s)
if tag == "" || !isPatch {
continue
}
if version == "" {
version = "v1"
}
result := fmt.Sprintf("%s_%s", tag, version)
result = strings.ReplaceAll(result, " ", "")
collision := false
for _, name := range patches {
if name == result {
collision = true
}
}
if collision {
continue
}
_, ok := summary[result]
if ok {
continue
}
results = append(results, result)
summary[result] = struct{}{}
}
sort.Strings(results)
return results
}