diff options
-rw-r--r-- | commands/patch/apply.go | 34 | ||||
-rw-r--r-- | doc/aerc-patch.7.scd | 32 | ||||
-rw-r--r-- | lib/pama/apply.go | 4 | ||||
-rw-r--r-- | lib/pama/delete.go | 22 | ||||
-rw-r--r-- | lib/pama/models/models.go | 20 | ||||
-rw-r--r-- | lib/pama/models/view.go | 20 | ||||
-rw-r--r-- | lib/pama/pama_test.go | 12 | ||||
-rw-r--r-- | lib/pama/revctrl/git.go | 16 | ||||
-rw-r--r-- | lib/pama/store/store.go | 17 | ||||
-rw-r--r-- | lib/pama/worktree.go | 88 |
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 +} |