aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2023-11-24 16:03:10 +0100
committerRobin Jarry <robin@jarry.cc>2023-12-30 15:42:09 +0100
commitf8c9e7fff564667700c3dbc239d55db8fcd032a6 (patch)
tree2fbe2e87ed8e713e119aaadf0f2e317fb01d90fc /lib
parentcfcab6c5c883a08afa3a56c11e4f48d9725be2a0 (diff)
downloadaerc-f8c9e7fff564667700c3dbc239d55db8fcd032a6.tar.gz
patch: implement worktree support
Implement worktree support for the patch management. Use ":patch apply -w <commit-ish> <tag>" to create a new worktree and apply the selected messages to it. The worktree is linked to repo in the current project. Internally, the worktree is stored as a new project. When this project is deleted with ":patch delete", the underlying linked worktree is removed as well. ":patch list" shows when a project is a worktree and to what project it is linked to. Worktrees enable the users to create a new copy of the repo at a given commit and apply patches without interrupting the current work in the base repo. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
Diffstat (limited to 'lib')
-rw-r--r--lib/pama/apply.go4
-rw-r--r--lib/pama/delete.go22
-rw-r--r--lib/pama/models/models.go20
-rw-r--r--lib/pama/models/view.go20
-rw-r--r--lib/pama/pama_test.go12
-rw-r--r--lib/pama/revctrl/git.go16
-rw-r--r--lib/pama/store/store.go17
-rw-r--r--lib/pama/worktree.go88
8 files changed, 186 insertions, 13 deletions
diff --git a/lib/pama/apply.go b/lib/pama/apply.go
index 15875818..3d701347 100644
--- a/lib/pama/apply.go
+++ b/lib/pama/apply.go
@@ -15,7 +15,7 @@ func (m PatchManager) CurrentProject() (p models.Project, err error) {
name, err := store.CurrentName()
if name == "" || err != nil {
log.Errorf("failed to get current name: %v", storeErr(err))
- err = fmt.Errorf("No current project set. " +
+ err = fmt.Errorf("no current project set. " +
"Run :patch init first")
return
}
@@ -32,7 +32,7 @@ func (m PatchManager) CurrentProject() (p models.Project, err error) {
}
}
if notFound {
- err = fmt.Errorf("Project '%s' does not exist anymore. "+
+ err = fmt.Errorf("project '%s' does not exist anymore. "+
"Run :patch init or :patch switch", name)
return
}
diff --git a/lib/pama/delete.go b/lib/pama/delete.go
index 18a93a27..53dbeeb1 100644
--- a/lib/pama/delete.go
+++ b/lib/pama/delete.go
@@ -1,6 +1,10 @@
package pama
-import "fmt"
+import (
+ "fmt"
+
+ "git.sr.ht/~rjarry/aerc/log"
+)
// Delete removes provided project
func (m PatchManager) Delete(name string) error {
@@ -21,8 +25,8 @@ func (m PatchManager) Delete(name string) error {
return fmt.Errorf("Project '%s' not found", name)
}
- cur, err := m.CurrentProject()
- if err == nil && cur.Name == name {
+ cur, err := store.CurrentName()
+ if err == nil && cur == name {
var next string
for _, s := range names {
if name != s {
@@ -36,6 +40,18 @@ func (m PatchManager) Delete(name string) error {
}
}
+ p, err := store.Project(name)
+ if err == nil && isWorktree(p) {
+ err = m.deleteWorktree(p)
+ if err != nil {
+ log.Errorf("failed to delete worktree: %v", err)
+ }
+ err = store.SetCurrent(p.Worktree.Name)
+ if err != nil {
+ log.Errorf("failed to set current project: %v", err)
+ }
+ }
+
return storeErr(m.store().DeleteProject(name))
}
diff --git a/lib/pama/models/models.go b/lib/pama/models/models.go
index 1c098885..9b21f0f5 100644
--- a/lib/pama/models/models.go
+++ b/lib/pama/models/models.go
@@ -20,6 +20,15 @@ type Commit struct {
Tag string
}
+// WorktreeParent stores the name and repo location for the base project in the
+// linked worktree project.
+type WorktreeParent struct {
+ // Name is the project name from the base repo.
+ Name string
+ // Root is the root directory of the base repo.
+ Root string
+}
+
// Project contains the data to access a revision control system and to store
// the internal patch tracking data.
type Project struct {
@@ -30,6 +39,9 @@ type Project struct {
Root string
// RevctrlID stores the ID for the revision control system.
RevctrlID string
+ // Worktree keeps the base repo information. If Worktree.Name and
+ // Worktree.Root are not zero, this project contains a linked worktree.
+ Worktree WorktreeParent
// Base represents the reference (base) commit.
Base Commit
// Commits contains the commits that are being tracked. The slice can
@@ -64,6 +76,12 @@ type RevisionController interface {
// ApplyCmd returns a string with an executable command that is used to
// apply patches with the :pipe command.
ApplyCmd() string
+ // CreateWorktree creats a worktree in path at commit.
+ CreateWorktree(path string, commit string) error
+ // DeleteWorktree removes the linked worktree stored in the path
+ // location. Note that this function should be called from the base
+ // repo.
+ DeleteWorktree(path string) error
}
// PersistentStorer is an interface to a persistent storage for Project structs.
@@ -81,6 +99,8 @@ type PersistentStorer interface {
Current() (Project, error)
// Names returns a slice of Project.Name for all stored projects.
Names() ([]string, error)
+ // Project returns the stored project for the provided name.
+ Project(string) (Project, error)
// Projects returns all stored projects.
Projects() ([]Project, error)
}
diff --git a/lib/pama/models/view.go b/lib/pama/models/view.go
index 093ca6f0..6a5b6bd4 100644
--- a/lib/pama/models/view.go
+++ b/lib/pama/models/view.go
@@ -10,7 +10,7 @@ import (
)
var templateText = `
-Project {{.Name}} {{if .IsActive}}[active]{{end}}
+Project {{.Name}} {{if .IsActive}}[active]{{end}} {{if .IsWorktree}}[Linked worktree to {{.WorktreeParent}}]{{end}}
Directory {{.Root}}
Base {{with .Base.ID}}{{if ge (len .) 40}}{{printf "%-6.6s" .}}{{else}}{{.}}{{end}}{{end}}
{{$notes := .Notes}}{{$commits := .Commits}}
@@ -37,17 +37,21 @@ type view struct {
// the key and the annotation is the value.
Notes map[string]string
// IsActive is true if the current project is selected.
- IsActive bool
+ IsActive bool
+ IsWorktree bool
+ WorktreeParent string
}
func newView(p Project, active bool, notes map[string]string) view {
v := view{
- Name: p.Name,
- Root: p.Root,
- Base: p.Base,
- Commits: make(map[string][]Commit),
- Notes: notes,
- IsActive: active,
+ Name: p.Name,
+ Root: p.Root,
+ Base: p.Base,
+ Commits: make(map[string][]Commit),
+ Notes: notes,
+ IsActive: active,
+ IsWorktree: p.Worktree.Root != "" && p.Worktree.Name != "",
+ WorktreeParent: p.Worktree.Name,
}
for _, commit := range p.Commits {
diff --git a/lib/pama/pama_test.go b/lib/pama/pama_test.go
index 7a4d1961..98258249 100644
--- a/lib/pama/pama_test.go
+++ b/lib/pama/pama_test.go
@@ -113,6 +113,14 @@ func (c *mockRevctrl) Remove(commit string) error {
return errNotFound
}
+func (c *mockRevctrl) CreateWorktree(_, _ string) error {
+ return nil
+}
+
+func (c *mockRevctrl) DeleteWorktree(_ string) error {
+ return nil
+}
+
func (c *mockRevctrl) ApplyCmd() string {
return ""
}
@@ -157,6 +165,10 @@ func (s *mockStore) Names() ([]string, error) {
return names, nil
}
+func (s *mockStore) Project(_ string) (models.Project, error) {
+ return models.Project{}, nil
+}
+
func (s *mockStore) Projects() ([]models.Project, error) {
var ps []models.Project
for _, p := range s.data {
diff --git a/lib/pama/revctrl/git.go b/lib/pama/revctrl/git.go
index f4b78dd5..7be9de58 100644
--- a/lib/pama/revctrl/git.go
+++ b/lib/pama/revctrl/git.go
@@ -98,6 +98,22 @@ func (g git) Clean() bool {
return len(s) == 0 && exitcode == 0 && err == nil
}
+func (g git) CreateWorktree(target, commit string) error {
+ _, exitcode, err := g.do("worktree", "add", target, commit)
+ if exitcode > 0 {
+ return fmt.Errorf("failed to create worktree in %s: %w", target, err)
+ }
+ return err
+}
+
+func (g git) DeleteWorktree(target string) error {
+ _, exitcode, err := g.do("worktree", "remove", target)
+ if exitcode > 0 {
+ return fmt.Errorf("failed to delete worktree in %s: %w", target, err)
+ }
+ return err
+}
+
func (g git) ApplyCmd() string {
// TODO: should we return a *exec.Cmd instead of a string?
return fmt.Sprintf("git -C %s am -3 --empty drop", g.path)
diff --git a/lib/pama/store/store.go b/lib/pama/store/store.go
index 8f93873d..aaee5412 100644
--- a/lib/pama/store/store.go
+++ b/lib/pama/store/store.go
@@ -221,6 +221,23 @@ func (instance) Names() ([]string, error) {
return names, nil
}
+func (instance) Project(name string) (models.Project, error) {
+ db, err := openStorage()
+ if err != nil {
+ return models.Project{}, err
+ }
+ defer db.Close()
+ raw, err := db.Get(createKey(name), nil)
+ if err != nil {
+ return models.Project{}, err
+ }
+ p, err := decode(raw)
+ if err != nil {
+ return models.Project{}, err
+ }
+ return p, nil
+}
+
func (instance) Projects() ([]models.Project, error) {
var projects []models.Project
db, err := openStorage()
diff --git a/lib/pama/worktree.go b/lib/pama/worktree.go
new file mode 100644
index 00000000..9ecfadf6
--- /dev/null
+++ b/lib/pama/worktree.go
@@ -0,0 +1,88 @@
+package pama
+
+import (
+ "fmt"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "git.sr.ht/~rjarry/aerc/lib/pama/models"
+ "git.sr.ht/~rjarry/aerc/lib/xdg"
+ "git.sr.ht/~rjarry/aerc/log"
+)
+
+func cacheDir() (string, error) {
+ dir, err := os.UserCacheDir()
+ if err != nil {
+ dir = xdg.ExpandHome("~/.cache")
+ }
+ return path.Join(dir, "aerc"), nil
+}
+
+func makeWorktreeName(baseProject, tag string) string {
+ unique, err := generateTag(4)
+ if err != nil {
+ log.Infof("could not generate unique id: %v", err)
+ }
+ return strings.Join([]string{baseProject, "worktree", tag, unique}, "_")
+}
+
+func isWorktree(p models.Project) bool {
+ return p.Worktree.Name != "" && p.Worktree.Root != ""
+}
+
+func (m PatchManager) CreateWorktree(p models.Project, commitID, tag string,
+) (models.Project, error) {
+ var w models.Project
+
+ if isWorktree(p) {
+ return w, fmt.Errorf("This is already a worktree.")
+ }
+
+ w.RevctrlID = p.RevctrlID
+ w.Base = models.Commit{ID: commitID}
+ w.Name = makeWorktreeName(p.Name, tag)
+ w.Worktree = models.WorktreeParent{Name: p.Name, Root: p.Root}
+
+ dir, err := cacheDir()
+ if err != nil {
+ return p, err
+ }
+ w.Root = filepath.Join(dir, "worktrees", w.Name)
+
+ rc, err := m.rc(p.RevctrlID, p.Root)
+ if err != nil {
+ return p, revErr(err)
+ }
+
+ err = rc.CreateWorktree(w.Root, w.Base.ID)
+ if err != nil {
+ return p, revErr(err)
+ }
+
+ err = m.store().StoreProject(w, true)
+ if err != nil {
+ return p, storeErr(err)
+ }
+
+ return w, nil
+}
+
+func (m PatchManager) deleteWorktree(p models.Project) error {
+ if !isWorktree(p) {
+ return nil
+ }
+
+ rc, err := m.rc(p.RevctrlID, p.Worktree.Root)
+ if err != nil {
+ return revErr(err)
+ }
+
+ err = rc.DeleteWorktree(p.Root)
+ if err != nil {
+ return revErr(err)
+ }
+
+ return nil
+}