aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--commands/patch/apply.go34
-rw-r--r--doc/aerc-patch.7.scd32
-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
10 files changed, 234 insertions, 31 deletions
diff --git a/commands/patch/apply.go b/commands/patch/apply.go
index f9c74ec3..2806b7d1 100644
--- a/commands/patch/apply.go
+++ b/commands/patch/apply.go
@@ -15,8 +15,9 @@ import (
)
type Apply struct {
- Cmd string `opt:"-c"`
- Tag string `opt:"tag" required:"true" complete:"CompleteTag"`
+ Cmd string `opt:"-c"`
+ Worktree string `opt:"-w"`
+ Tag string `opt:"tag" required:"true" complete:"CompleteTag"`
}
func init() {
@@ -68,6 +69,8 @@ func (*Apply) CompleteTag(arg string) []string {
func (a Apply) Execute(args []string) error {
patch := a.Tag
+ worktree := a.Worktree
+ applyCmd := a.Cmd
m := pama.New()
p, err := m.CurrentProject()
@@ -76,6 +79,17 @@ func (a Apply) Execute(args []string) error {
}
log.Tracef("Current project: %v", p)
+ if worktree != "" {
+ p, err = m.CreateWorktree(p, worktree, patch)
+ if err != nil {
+ return err
+ }
+ err = m.SwitchProject(p.Name)
+ if err != nil {
+ log.Warnf("could not switch to worktree project: %v", err)
+ }
+ }
+
if models.Commits(p.Commits).HasTag(patch) {
return fmt.Errorf("Patch name '%s' already exists.", patch)
}
@@ -91,19 +105,17 @@ func (a Apply) Execute(args []string) error {
}
log.Tracef("HEAD commit before: %s", commit)
- applyCmd, err := m.ApplyCmd(p)
- if err != nil {
- return err
- }
-
- if a.Cmd != "" {
- applyCmd = a.Cmd
+ if applyCmd != "" {
rootFmt := "%r"
if strings.Contains(applyCmd, rootFmt) {
applyCmd = strings.ReplaceAll(applyCmd, rootFmt, p.Root)
}
- log.Infof("overwrite apply cmd by '%s'",
- applyCmd)
+ log.Infof("use custom apply command: %s", applyCmd)
+ } else {
+ applyCmd, err = m.ApplyCmd(p)
+ if err != nil {
+ return err
+ }
}
msgData := collectMessageData()
diff --git a/doc/aerc-patch.7.scd b/doc/aerc-patch.7.scd
index 98c271bb..932bf8b1 100644
--- a/doc/aerc-patch.7.scd
+++ b/doc/aerc-patch.7.scd
@@ -35,21 +35,39 @@ The following *:patch* sub-commands are supported:
*-a*: Lists all projects.
-*:patch apply* [*-c* _<cmd>_] _<tag>_
+*:patch apply* [*-c* _<cmd>_] [*-w* _<commit-ish>_] _<tag>_
Applies the selected message(s) to the repository of the current
project. It uses the *:pipe* command for this and keeps track of the
applied patch.
- A user-defined command for applying patches can be used with the *-c*
- option. Any occurence of '%r' in the command string will be replaced
- with the root directory of the current project. However, this approach
- is not recommended in general and should only be used for very specific
- purposes, i.e. when a maintainer is applying a patch set via a separate
- script to deal with git trailers.
+ Completions for the _<tag>_ are available based on the subject lines of
+ the selected or marked messages.
+
+ *-c* _<cmd>_: Apply patches with the provided _<cmd>_. Any occurence of
+ '%r' in the command string will be replaced with the root directory of
+ the current project. Note that this approach is not recommended in
+ general and should only be used for very specific purposes, i.e. when
+ a maintainer is applying a patch set via a separate script to deal with
+ git trailers.
*aerc* will propose completions for the _<tag>_ based on the subject
lines of the selected or marked messages.
+ Example:
+ ```
+ :patch apply -c "git -C %r am -3" fix_v2
+ ```
+
+ *-w* _<commit-ish>_: Create a linked worktree for the current project at
+ _<commit-ish>_ and apply the patches to the linked worktree. A new
+ project is created to store the worktree information. When this project
+ is deleted, the worktree will be deleted as well.
+
+ Example:
+ ```
+ :patch apply -w origin/master fix_v2
+ ```
+
*:patch remove* _<tag>_
Removes the patch _<tag>_ from the repository.
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
+}