diff options
author | Koni Marti <koni.marti@gmail.com> | 2023-11-24 16:03:02 +0100 |
---|---|---|
committer | Robin Jarry <robin@jarry.cc> | 2023-12-30 15:42:09 +0100 |
commit | fdd9f7991aa50bd99d21c178a2816fc075eead6b (patch) | |
tree | 737207d7dcb5b626f57d59e7b0b247d73a352f28 /commands/patch/apply.go | |
parent | f98382d1dfc8970d3006fcc2175dd514bf7e07d0 (diff) | |
download | aerc-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.go | 248 |
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 +} |