aboutsummaryrefslogtreecommitdiffstats
path: root/lib/pama
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2023-11-24 16:02:58 +0100
committerRobin Jarry <robin@jarry.cc>2023-12-30 15:42:09 +0100
commit69e73fd7560ec7846451ad6fd859f9286812c64f (patch)
tree1ffbda4855c1a0afbf5f831fc6e3f2b187baee89 /lib/pama
parent724d3a22cdaae3299df23195788ccfad7e332db8 (diff)
downloadaerc-69e73fd7560ec7846451ad6fd859f9286812c64f.tar.gz
pama: implement the revision control logic
Implement the RevisionController interface to interact with a respository control system. Add the implementation for git. Other revision systems such as mercurial, pijul or fossil can be extended. 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/revctrl/git.go112
-rw-r--r--lib/pama/revctrl/revctrl.go48
2 files changed, 160 insertions, 0 deletions
diff --git a/lib/pama/revctrl/git.go b/lib/pama/revctrl/git.go
new file mode 100644
index 00000000..f4b78dd5
--- /dev/null
+++ b/lib/pama/revctrl/git.go
@@ -0,0 +1,112 @@
+package revctrl
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "git.sr.ht/~rjarry/aerc/lib/pama/models"
+ "git.sr.ht/~rjarry/aerc/log"
+)
+
+func init() {
+ register("git", newGit)
+}
+
+func newGit(s string) models.RevisionController {
+ return &git{path: strings.TrimSpace(s)}
+}
+
+type git struct {
+ path string
+}
+
+func (g git) Support() bool {
+ _, exitcode, err := g.do("rev-parse")
+ return exitcode == 0 && err == nil
+}
+
+func (g git) Root() (string, error) {
+ s, _, err := g.do("rev-parse", "--show-toplevel")
+ return s, err
+}
+
+func (g git) Head() (string, error) {
+ s, _, err := g.do("rev-list", "-n 1", "HEAD")
+ return s, err
+}
+
+func (g git) History(commit string) ([]string, error) {
+ s, _, err := g.do("rev-list", "--reverse", fmt.Sprintf("%s..HEAD", commit))
+ return strings.Fields(s), err
+}
+
+func (g git) Subject(commit string) string {
+ s, exitcode, err := g.do("log", "-1", "--pretty=%s", commit)
+ if exitcode > 0 || err != nil {
+ return ""
+ }
+ return s
+}
+
+func (g git) Author(commit string) string {
+ s, exitcode, err := g.do("log", "-1", "--pretty=%an", commit)
+ if exitcode > 0 || err != nil {
+ return ""
+ }
+ return s
+}
+
+func (g git) Date(commit string) string {
+ s, exitcode, err := g.do("log", "-1", "--pretty=%as", commit)
+ if exitcode > 0 || err != nil {
+ return ""
+ }
+ return s
+}
+
+func (g git) Remove(commit string) error {
+ _, exitcode, err := g.do("rebase", "--onto", commit+"^", commit)
+ if exitcode > 0 {
+ return fmt.Errorf("failed to remove commit %s", commit)
+ }
+ return err
+}
+
+func (g git) Exists(commit string) bool {
+ _, exitcode, err := g.do("merge-base", "--is-ancestor", commit, "HEAD")
+ return exitcode == 0 && err == nil
+}
+
+func (g git) Clean() bool {
+ // is a rebase in progress?
+ dirs := []string{"rebase-merge", "rebase-apply"}
+ for _, dir := range dirs {
+ relPath, _, err := g.do("rev-parse", "--git-path", dir)
+ if err == nil {
+ if _, err := os.Stat(filepath.Join(g.path, relPath)); !os.IsNotExist(err) {
+ log.Errorf("%s exists: another rebase in progress..", dir)
+ return false
+ }
+ }
+ }
+ // are there unstaged changes?
+ s, exitcode, err := g.do("diff-index", "HEAD")
+ return len(s) == 0 && exitcode == 0 && err == nil
+}
+
+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)
+}
+
+func (g git) do(args ...string) (string, int, error) {
+ proc := exec.Command("git", "-C", g.path)
+ proc.Args = append(proc.Args, args...)
+ proc.Env = os.Environ()
+ result, err := proc.Output()
+ return string(bytes.TrimSpace(result)), proc.ProcessState.ExitCode(), err
+}
diff --git a/lib/pama/revctrl/revctrl.go b/lib/pama/revctrl/revctrl.go
new file mode 100644
index 00000000..42532216
--- /dev/null
+++ b/lib/pama/revctrl/revctrl.go
@@ -0,0 +1,48 @@
+package revctrl
+
+import (
+ "errors"
+ "fmt"
+
+ "git.sr.ht/~rjarry/aerc/lib/pama/models"
+ "git.sr.ht/~rjarry/aerc/log"
+)
+
+var ErrUnsupported = errors.New("unsupported")
+
+type factoryFunc func(string) models.RevisionController
+
+var controllers = map[string]factoryFunc{}
+
+func register(controllerID string, fn factoryFunc) {
+ controllers[controllerID] = fn
+}
+
+func New(controllerID string, path string) (models.RevisionController, error) {
+ factoryFunc, ok := controllers[controllerID]
+ if !ok {
+ return nil, errors.New("cannot create revision control instance")
+ }
+ return factoryFunc(path), nil
+}
+
+type detector interface {
+ Support() bool
+ Root() (string, error)
+}
+
+func Detect(path string) (string, string, error) {
+ for controllerID, factoryFunc := range controllers {
+ rc, ok := factoryFunc(path).(detector)
+ if ok && rc.Support() {
+ log.Tracef("support found for %v", controllerID)
+ root, err := rc.Root()
+ if err != nil {
+ continue
+ }
+ log.Tracef("root found in %s", root)
+ return controllerID, root, nil
+ }
+ }
+ return "", "", fmt.Errorf("no supported repository found in %s", path)
+}