aboutsummaryrefslogtreecommitdiffstats
path: root/lib/pama
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2023-11-24 16:03:04 +0100
committerRobin Jarry <robin@jarry.cc>2023-12-30 15:42:09 +0100
commit41e066768c18268fe3deecc60b5797e26f44cf4e (patch)
treee3e14165ef3a5308d9205bc8f6d4f1831ec654c8 /lib/pama
parentcf47763e5582563f712b4a40a9b299378aba9003 (diff)
downloadaerc-41e066768c18268fe3deecc60b5797e26f44cf4e.tar.gz
patch/remove: add remove sub-cmd
Implement the :patch remove command. Remove a patch set from the respository and from the internal storage. Note that in git, this will change all commit hashes that appear after the removed one since the commit hash depends on its parents. Adjust the code to handle such cases and add tests for this. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
Diffstat (limited to 'lib/pama')
-rw-r--r--lib/pama/pama_test.go166
-rw-r--r--lib/pama/remove.go94
-rw-r--r--lib/pama/remove_test.go85
3 files changed, 345 insertions, 0 deletions
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)
+ }
+ }
+ }
+}