aboutsummaryrefslogtreecommitdiffstats
path: root/commands/patch/apply.go
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2023-11-24 16:03:02 +0100
committerRobin Jarry <robin@jarry.cc>2023-12-30 15:42:09 +0100
commitfdd9f7991aa50bd99d21c178a2816fc075eead6b (patch)
tree737207d7dcb5b626f57d59e7b0b247d73a352f28 /commands/patch/apply.go
parentf98382d1dfc8970d3006fcc2175dd514bf7e07d0 (diff)
downloadaerc-fdd9f7991aa50bd99d21c178a2816fc075eead6b.tar.gz
patch/apply: add apply sub-cmd
Add the :patch apply command to apply a patch set and create a corresponding tag. The tag command argument can be completed based on the subject lines of the selected messages. Add a test for the completion proposal. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
Diffstat (limited to 'commands/patch/apply.go')
-rw-r--r--commands/patch/apply.go248
1 files changed, 248 insertions, 0 deletions
diff --git a/commands/patch/apply.go b/commands/patch/apply.go
new file mode 100644
index 00000000..f9c74ec3
--- /dev/null
+++ b/commands/patch/apply.go
@@ -0,0 +1,248 @@
+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/pama"
+ "git.sr.ht/~rjarry/aerc/lib/pama/models"
+ "git.sr.ht/~rjarry/aerc/log"
+)
+
+type Apply struct {
+ Cmd string `opt:"-c"`
+ Tag string `opt:"tag" required:"true" complete:"CompleteTag"`
+}
+
+func init() {
+ register(Apply{})
+}
+
+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
+
+ m := pama.New()
+ p, err := m.CurrentProject()
+ if err != nil {
+ return err
+ }
+ log.Tracef("Current project: %v", p)
+
+ 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)
+
+ applyCmd, err := m.ApplyCmd(p)
+ if err != nil {
+ return err
+ }
+
+ if a.Cmd != "" {
+ applyCmd = a.Cmd
+ rootFmt := "%r"
+ if strings.Contains(applyCmd, rootFmt) {
+ applyCmd = strings.ReplaceAll(applyCmd, rootFmt, p.Root)
+ }
+ log.Infof("overwrite apply cmd by '%s'",
+ applyCmd)
+ }
+
+ 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
+}