diff options
-rw-r--r-- | commands/patch/remove.go | 43 | ||||
-rw-r--r-- | lib/pama/pama_test.go | 166 | ||||
-rw-r--r-- | lib/pama/remove.go | 94 | ||||
-rw-r--r-- | lib/pama/remove_test.go | 85 |
4 files changed, 388 insertions, 0 deletions
diff --git a/commands/patch/remove.go b/commands/patch/remove.go new file mode 100644 index 00000000..6697177b --- /dev/null +++ b/commands/patch/remove.go @@ -0,0 +1,43 @@ +package patch + +import ( + "fmt" + "time" + + "git.sr.ht/~rjarry/aerc/app" + "git.sr.ht/~rjarry/aerc/commands" + "git.sr.ht/~rjarry/aerc/lib/pama" + "git.sr.ht/~rjarry/aerc/log" +) + +type Remove struct { + Tag string `opt:"tag" complete:"CompleteTag"` +} + +func init() { + register(Remove{}) +} + +func (Remove) Aliases() []string { + return []string{"remove"} +} + +func (*Remove) CompleteTag(arg string) []string { + patches, err := pama.New().CurrentPatches() + if err != nil { + log.Errorf("failed to get current patches: %v", err) + return nil + } + return commands.FilterList(patches, arg, nil) +} + +func (r Remove) Execute(args []string) error { + patch := r.Tag + err := pama.New().RemovePatch(patch) + if err != nil { + return err + } + app.PushStatus(fmt.Sprintf("Patch %s has been removed", patch), + 10*time.Second) + return nil +} diff --git a/lib/pama/pama_test.go b/lib/pama/pama_test.go new file mode 100644 index 00000000..7a4d1961 --- /dev/null +++ b/lib/pama/pama_test.go @@ -0,0 +1,166 @@ +package pama_test + +import ( + "errors" + + "git.sr.ht/~rjarry/aerc/lib/pama" + "git.sr.ht/~rjarry/aerc/lib/pama/models" +) + +var errNotFound = errors.New("not found") + +func newCommit(id, subj, tag string) models.Commit { + return models.Commit{ID: id, Subject: subj, Tag: tag} +} + +func newTestManager( + commits []string, + subjects []string, + data map[string]models.Project, + current string, +) (pama.PatchManager, models.RevisionController, models.PersistentStorer) { + rc := mockRevctrl{ + commitIDs: commits, + titles: subjects, + } + store := mockStore{ + data: data, + current: current, + } + return pama.FromFunc( + nil, + func(_ string, _ string) (models.RevisionController, error) { + return &rc, nil + }, + func() models.PersistentStorer { + return &store + }, + ), &rc, &store +} + +type mockRevctrl struct { + commitIDs []string + titles []string +} + +func (c *mockRevctrl) Support() bool { + return true +} + +func (c *mockRevctrl) Clean() bool { + return true +} + +func (c *mockRevctrl) Root() (string, error) { + return "", nil +} + +func (c *mockRevctrl) Head() (string, error) { + return c.commitIDs[len(c.commitIDs)-1], nil +} + +func (c *mockRevctrl) History(commit string) ([]string, error) { + for i, s := range c.commitIDs { + if s == commit { + cp := make([]string, len(c.commitIDs[i+1:])) + copy(cp, c.commitIDs[i+1:]) + return cp, nil + } + } + return nil, errNotFound +} + +func (c *mockRevctrl) Exists(commit string) bool { + for _, s := range c.commitIDs { + if s == commit { + return true + } + } + return false +} + +func (c *mockRevctrl) Subject(commit string) string { + for i, s := range c.commitIDs { + if s == commit { + return c.titles[i] + } + } + return "" +} + +func (c *mockRevctrl) Author(commit string) string { + return "" +} + +func (c *mockRevctrl) Date(commit string) string { + return "" +} + +func (c *mockRevctrl) Remove(commit string) error { + for i, s := range c.commitIDs { + if s == commit { + c.commitIDs = append(c.commitIDs[:i], c.commitIDs[i+1:]...) + c.titles = append(c.titles[:i], c.titles[i+1:]...) + // modify commitIDs to simulate a "real" change in + // commit history that will also change all subsequent + // commitIDs + for j := i; j < len(c.commitIDs); j++ { + c.commitIDs[j] += "_new" + } + return nil + } + } + return errNotFound +} + +func (c *mockRevctrl) ApplyCmd() string { + return "" +} + +type mockStore struct { + data map[string]models.Project + current string +} + +func (s *mockStore) StoreProject(p models.Project, ow bool) error { + _, ok := s.data[p.Name] + if ok && !ow { + return errors.New("alreay there") + } + s.data[p.Name] = p + return nil +} + +func (s *mockStore) DeleteProject(name string) error { + delete(s.data, name) + return nil +} + +func (s *mockStore) CurrentName() (string, error) { + return s.current, nil +} + +func (s *mockStore) SetCurrent(c string) error { + s.current = c + return nil +} + +func (s *mockStore) Current() (models.Project, error) { + return s.data[s.current], nil +} + +func (s *mockStore) Names() ([]string, error) { + var names []string + for name := range s.data { + names = append(names, name) + } + return names, nil +} + +func (s *mockStore) Projects() ([]models.Project, error) { + var ps []models.Project + for _, p := range s.data { + ps = append(ps, p) + } + return ps, nil +} diff --git a/lib/pama/remove.go b/lib/pama/remove.go new file mode 100644 index 00000000..d74874a1 --- /dev/null +++ b/lib/pama/remove.go @@ -0,0 +1,94 @@ +package pama + +import ( + "fmt" + + "git.sr.ht/~rjarry/aerc/lib/pama/models" + "git.sr.ht/~rjarry/aerc/log" +) + +func (m PatchManager) RemovePatch(patch string) error { + p, err := m.CurrentProject() + if err != nil { + return err + } + + if !models.Commits(p.Commits).HasTag(patch) { + return fmt.Errorf("Patch '%s' not found in project '%s'", patch, p.Name) + } + + rc, err := m.rc(p.RevctrlID, p.Root) + if err != nil { + return revErr(err) + } + + if !rc.Clean() { + return fmt.Errorf("Aborting... There are unstaged changes " + + "or a rebase in progress") + } + + toRemove := make([]models.Commit, 0) + for _, c := range p.Commits { + if !rc.Exists(c.ID) { + log.Errorf("failed to find commit. %v", c) + return fmt.Errorf("Cannot remove patch. " + + "Please rebase first with ':patch rebase'") + } + if c.Tag == patch { + toRemove = append(toRemove, c) + } + } + + removed := make(map[string]struct{}) + for i := len(toRemove) - 1; i >= 0; i-- { + commitID := toRemove[i].ID + beforeIDs, err := rc.History(commitID) + if err != nil { + log.Errorf("failed to remove %v (commits before): %v", toRemove[i], err) + continue + } + err = rc.Remove(commitID) + if err != nil { + log.Errorf("failed to remove %v (remove): %v", toRemove[i], err) + continue + } + removed[commitID] = struct{}{} + afterIDs, err := rc.History(p.Base.ID) + if err != nil { + log.Errorf("failed to remove %v (commits after): %v", toRemove[i], err) + continue + } + afterIDs = afterIDs[len(afterIDs)-len(beforeIDs):] + transform := make(map[string]string) + for j := 0; j < len(beforeIDs); j++ { + transform[beforeIDs[j]] = afterIDs[j] + } + for j, c := range p.Commits { + if newId, ok := transform[c.ID]; ok { + msgid := p.Commits[j].MessageId + p.Commits[j] = models.NewCommit( + rc, + newId, + p.Commits[j].Tag, + ) + p.Commits[j].MessageId = msgid + } + } + } + + if len(removed) < len(toRemove) { + return fmt.Errorf("Failed to remove commits. Removed %d of %d.", + len(removed), len(toRemove)) + } + + commits := make([]models.Commit, 0, len(p.Commits)) + for _, c := range p.Commits { + if _, ok := removed[c.ID]; ok { + continue + } + commits = append(commits, c) + } + p.Commits = commits + + return storeErr(m.store().StoreProject(p, true)) +} diff --git a/lib/pama/remove_test.go b/lib/pama/remove_test.go new file mode 100644 index 00000000..c9ce6c65 --- /dev/null +++ b/lib/pama/remove_test.go @@ -0,0 +1,85 @@ +package pama_test + +import ( + "reflect" + "testing" + + "git.sr.ht/~rjarry/aerc/lib/pama" + "git.sr.ht/~rjarry/aerc/lib/pama/models" +) + +func TestPatchmgmt_Remove(t *testing.T) { + setup := func(p models.Project) (pama.PatchManager, models.RevisionController, models.PersistentStorer) { + return newTestManager( + []string{"0", "1", "2", "3", "4", "5"}, + []string{"0", "a", "b", "c", "d", "f"}, + map[string]models.Project{p.Name: p}, p.Name, + ) + } + + tests := []struct { + name string + remove string + commits []models.Commit + want []models.Commit + }{ + { + name: "remove only patch", + remove: "patch1", + commits: []models.Commit{ + newCommit("1", "a", "patch1"), + }, + want: []models.Commit{}, + }, + { + name: "remove second one of two patch", + remove: "patch2", + commits: []models.Commit{ + newCommit("1", "a", "patch1"), + newCommit("2", "b", "patch2"), + }, + want: []models.Commit{ + newCommit("1", "a", "patch1"), + }, + }, + { + name: "remove first one of two patch", + remove: "patch1", + commits: []models.Commit{ + newCommit("1", "a", "patch1"), + newCommit("2", "b", "patch2"), + }, + want: []models.Commit{ + newCommit("2_new", "b", "patch2"), + }, + }, + } + + for _, test := range tests { + p := models.Project{ + Name: "project1", + Commits: test.commits, + Base: newCommit("0", "0", ""), + } + mgr, rc, _ := setup(p) + + err := mgr.RemovePatch(test.remove) + if err != nil { + t.Errorf("test '%s' failed. %v", test.name, err) + } + + q, _ := mgr.CurrentProject() + if !reflect.DeepEqual(q.Commits, test.want) { + t.Errorf("test '%s' failed. Commits don't match: "+ + "got %v, but wanted %v", test.name, q.Commits, + test.want) + } + + if len(test.want) > 0 { + last := test.want[len(test.want)-1] + if !rc.Exists(last.ID) { + t.Errorf("test '%s' failed. Could not find last commits: %v", test.name, last) + } + } + } +} |