diff options
Diffstat (limited to 'commands/patch')
-rw-r--r-- | commands/patch/apply.go | 248 | ||||
-rw-r--r-- | commands/patch/apply_test.go | 53 |
2 files changed, 301 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 +} diff --git a/commands/patch/apply_test.go b/commands/patch/apply_test.go new file mode 100644 index 00000000..12a87c76 --- /dev/null +++ b/commands/patch/apply_test.go @@ -0,0 +1,53 @@ +package patch + +import ( + "reflect" + "testing" +) + +func TestPatchApply_ProposeName(t *testing.T) { + tests := []struct { + name string + exist []string + subjects []string + want []string + }{ + { + name: "base case", + exist: nil, + subjects: []string{ + "[PATCH aerc v3 3/3] notmuch: remove unused code", + "[PATCH aerc v3 2/3] notmuch: replace notmuch library with internal bindings", + "[PATCH aerc v3 1/3] notmuch: add notmuch bindings", + }, + want: []string{"notmuch_v3"}, + }, + { + name: "distorted case", + exist: nil, + subjects: []string{ + "[PATCH vaerc v3 3/3] notmuch: remove unused code", + "[PATCH aerc 3v 2/3] notmuch: replace notmuch library with internal bindings", + }, + want: []string{"notmuch_v1", "notmuch_v3"}, + }, + { + name: "invalid patches", + exist: nil, + subjects: []string{ + "notmuch: remove unused code", + ": replace notmuch library with internal bindings", + }, + want: nil, + }, + } + + for _, test := range tests { + got := proposePatchName(test.exist, test.subjects) + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test '%s' failed to propose the correct "+ + "name: got '%v', but want '%v'", test.name, + got, test.want) + } + } +} |