From 69e73fd7560ec7846451ad6fd859f9286812c64f Mon Sep 17 00:00:00 2001 From: Koni Marti Date: Fri, 24 Nov 2023 16:02:58 +0100 Subject: 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 Acked-by: Robin Jarry --- lib/pama/revctrl/git.go | 112 ++++++++++++++++++++++++++++++++++++++++++++ lib/pama/revctrl/revctrl.go | 48 +++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 lib/pama/revctrl/git.go create mode 100644 lib/pama/revctrl/revctrl.go (limited to 'lib/pama') 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) +} -- cgit