init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package pama
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
mathrand "math/rand"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
)
|
||||
|
||||
func (m PatchManager) CurrentProject() (p models.Project, err error) {
|
||||
store := m.store()
|
||||
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. " +
|
||||
"Run :patch init first")
|
||||
return
|
||||
}
|
||||
names, err := store.Names()
|
||||
if err != nil {
|
||||
err = storeErr(err)
|
||||
return
|
||||
}
|
||||
notFound := true
|
||||
for _, s := range names {
|
||||
if s == name {
|
||||
notFound = !notFound
|
||||
break
|
||||
}
|
||||
}
|
||||
if notFound {
|
||||
err = fmt.Errorf("project '%s' does not exist anymore. "+
|
||||
"Run :patch init or :patch switch", name)
|
||||
return
|
||||
}
|
||||
p, err = store.Current()
|
||||
if err != nil {
|
||||
err = storeErr(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m PatchManager) CurrentPatches() ([]string, error) {
|
||||
c, err := m.CurrentProject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return models.Commits(c.Commits).Tags(), nil
|
||||
}
|
||||
|
||||
func (m PatchManager) Head(p models.Project) (string, error) {
|
||||
rc, err := m.rc(p.RevctrlID, p.Root)
|
||||
if err != nil {
|
||||
return "", revErr(err)
|
||||
}
|
||||
return rc.Head()
|
||||
}
|
||||
|
||||
func (m PatchManager) Clean(p models.Project) bool {
|
||||
rc, err := m.rc(p.RevctrlID, p.Root)
|
||||
if err != nil {
|
||||
log.Errorf("could not get revctl: %v", revErr(err))
|
||||
return false
|
||||
}
|
||||
return rc.Clean()
|
||||
}
|
||||
|
||||
func (m PatchManager) ApplyCmd(p models.Project) (string, error) {
|
||||
rc, err := m.rc(p.RevctrlID, p.Root)
|
||||
if err != nil {
|
||||
return "", revErr(err)
|
||||
}
|
||||
return rc.ApplyCmd(), nil
|
||||
}
|
||||
|
||||
func generateTag(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func makeUnique(s string) string {
|
||||
tag, err := generateTag(4)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%s_%d", s, mathrand.Uint32())
|
||||
}
|
||||
return fmt.Sprintf("%s_%s", s, tag)
|
||||
}
|
||||
|
||||
// ApplyUpdate is called after the commits have been applied with the
|
||||
// ApplyCmd(). It will determine the additional commits from the commitID (last
|
||||
// HEAD position), assign the patch tag to those commits and store them in
|
||||
// project p.
|
||||
func (m PatchManager) ApplyUpdate(p models.Project, patch, commitID string,
|
||||
kv map[string]string,
|
||||
) (models.Project, error) {
|
||||
rc, err := m.rc(p.RevctrlID, p.Root)
|
||||
if err != nil {
|
||||
return p, revErr(err)
|
||||
}
|
||||
|
||||
commitIDs, err := rc.History(commitID)
|
||||
if err != nil {
|
||||
return p, revErr(err)
|
||||
}
|
||||
if len(commitIDs) == 0 {
|
||||
return p, fmt.Errorf("no commits found for patch %s", patch)
|
||||
}
|
||||
|
||||
if models.Commits(p.Commits).HasTag(patch) {
|
||||
log.Warnf("Patch name '%s' already exists", patch)
|
||||
patch = makeUnique(patch)
|
||||
log.Warnf("Creating new name: '%s'", patch)
|
||||
}
|
||||
|
||||
for _, c := range commitIDs {
|
||||
nc := models.NewCommit(rc, c, patch)
|
||||
for msgid, subj := range kv {
|
||||
if nc.Subject == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(subj, nc.Subject) {
|
||||
nc.MessageId = msgid
|
||||
}
|
||||
}
|
||||
p.Commits = append(p.Commits, nc)
|
||||
}
|
||||
|
||||
err = m.store().StoreProject(p, true)
|
||||
return p, storeErr(err)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package pama
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
)
|
||||
|
||||
func (m PatchManager) DropPatch(patch string) error {
|
||||
p, err := m.CurrentProject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !models.Commits(p.Commits).HasTag(patch) {
|
||||
return fmt.Errorf("Patch '%s' not found in project '%s'", patch, p.Name)
|
||||
}
|
||||
|
||||
rc, err := m.rc(p.RevctrlID, p.Root)
|
||||
if err != nil {
|
||||
return revErr(err)
|
||||
}
|
||||
|
||||
if !rc.Clean() {
|
||||
return fmt.Errorf("Aborting... There are unstaged changes " +
|
||||
"or a rebase in progress")
|
||||
}
|
||||
|
||||
toRemove := make([]models.Commit, 0)
|
||||
for _, c := range p.Commits {
|
||||
if !rc.Exists(c.ID) {
|
||||
log.Errorf("failed to find commit. %v", c)
|
||||
return fmt.Errorf("Cannot drop patch. " +
|
||||
"Please rebase first with ':patch rebase'")
|
||||
}
|
||||
if c.Tag == patch {
|
||||
toRemove = append(toRemove, c)
|
||||
}
|
||||
}
|
||||
|
||||
removed := make(map[string]struct{})
|
||||
for i := len(toRemove) - 1; i >= 0; i-- {
|
||||
commitID := toRemove[i].ID
|
||||
beforeIDs, err := rc.History(commitID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to drop %v (commits before): %v", toRemove[i], err)
|
||||
continue
|
||||
}
|
||||
err = rc.Drop(commitID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to drop %v: %v", toRemove[i], err)
|
||||
continue
|
||||
}
|
||||
removed[commitID] = struct{}{}
|
||||
afterIDs, err := rc.History(p.Base.ID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to drop %v (commits after): %v", toRemove[i], err)
|
||||
continue
|
||||
}
|
||||
afterIDs = afterIDs[len(afterIDs)-len(beforeIDs):]
|
||||
transform := make(map[string]string)
|
||||
for j := 0; j < len(beforeIDs); j++ {
|
||||
transform[beforeIDs[j]] = afterIDs[j]
|
||||
}
|
||||
for j, c := range p.Commits {
|
||||
if newId, ok := transform[c.ID]; ok {
|
||||
msgid := p.Commits[j].MessageId
|
||||
p.Commits[j] = models.NewCommit(
|
||||
rc,
|
||||
newId,
|
||||
p.Commits[j].Tag,
|
||||
)
|
||||
p.Commits[j].MessageId = msgid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(removed) < len(toRemove) {
|
||||
return fmt.Errorf("Failed to drop commits. Dropped %d of %d.",
|
||||
len(removed), len(toRemove))
|
||||
}
|
||||
|
||||
commits := make([]models.Commit, 0, len(p.Commits))
|
||||
for _, c := range p.Commits {
|
||||
if _, ok := removed[c.ID]; ok {
|
||||
continue
|
||||
}
|
||||
commits = append(commits, c)
|
||||
}
|
||||
p.Commits = commits
|
||||
|
||||
return storeErr(m.store().StoreProject(p, true))
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package pama_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
)
|
||||
|
||||
func TestPatchmgmt_Drop(t *testing.T) {
|
||||
setup := func(p models.Project) (pama.PatchManager, models.RevisionController, models.PersistentStorer) {
|
||||
return newTestManager(
|
||||
[]string{"0", "1", "2", "3", "4", "5"},
|
||||
[]string{"0", "a", "b", "c", "d", "f"},
|
||||
map[string]models.Project{p.Name: p}, p.Name,
|
||||
)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
drop string
|
||||
commits []models.Commit
|
||||
want []models.Commit
|
||||
}{
|
||||
{
|
||||
name: "drop only patch",
|
||||
drop: "patch1",
|
||||
commits: []models.Commit{
|
||||
newCommit("1", "a", "patch1"),
|
||||
},
|
||||
want: []models.Commit{},
|
||||
},
|
||||
{
|
||||
name: "drop second one of two patch",
|
||||
drop: "patch2",
|
||||
commits: []models.Commit{
|
||||
newCommit("1", "a", "patch1"),
|
||||
newCommit("2", "b", "patch2"),
|
||||
},
|
||||
want: []models.Commit{
|
||||
newCommit("1", "a", "patch1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "drop first one of two patch",
|
||||
drop: "patch1",
|
||||
commits: []models.Commit{
|
||||
newCommit("1", "a", "patch1"),
|
||||
newCommit("2", "b", "patch2"),
|
||||
},
|
||||
want: []models.Commit{
|
||||
newCommit("2_new", "b", "patch2"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
p := models.Project{
|
||||
Name: "project1",
|
||||
Commits: test.commits,
|
||||
Base: newCommit("0", "0", ""),
|
||||
}
|
||||
mgr, rc, _ := setup(p)
|
||||
|
||||
err := mgr.DropPatch(test.drop)
|
||||
if err != nil {
|
||||
t.Errorf("test '%s' failed. %v", test.name, err)
|
||||
}
|
||||
|
||||
q, _ := mgr.CurrentProject()
|
||||
if !reflect.DeepEqual(q.Commits, test.want) {
|
||||
t.Errorf("test '%s' failed. Commits don't match: "+
|
||||
"got %v, but wanted %v", test.name, q.Commits,
|
||||
test.want)
|
||||
}
|
||||
|
||||
if len(test.want) > 0 {
|
||||
last := test.want[len(test.want)-1]
|
||||
if !rc.Exists(last.ID) {
|
||||
t.Errorf("test '%s' failed. Could not find last commits: %v", test.name, last)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package pama
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
)
|
||||
|
||||
func (m PatchManager) Find(hash string, p models.Project) (models.Commit, error) {
|
||||
var c models.Commit
|
||||
rc, err := m.rc(p.RevctrlID, p.Root)
|
||||
if err != nil {
|
||||
return c, revErr(err)
|
||||
}
|
||||
if !rc.Exists(hash) {
|
||||
return c, fmt.Errorf("no commit found for hash %s", hash)
|
||||
}
|
||||
return models.NewCommit(rc, hash, ""), nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package pama
|
||||
|
||||
import (
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
)
|
||||
|
||||
// Init creates a new revision control project
|
||||
func (m PatchManager) Init(name, path string, overwrite bool) error {
|
||||
id, root, err := m.detect(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc, err := m.rc(id, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headID, err := rc.Head()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := models.Project{
|
||||
Name: name,
|
||||
Root: root,
|
||||
RevctrlID: id,
|
||||
Base: models.NewCommit(rc, headID, ""),
|
||||
Commits: make([]models.Commit, 0),
|
||||
}
|
||||
store := m.store()
|
||||
err = store.StoreProject(p, overwrite)
|
||||
if err != nil {
|
||||
return storeErr(err)
|
||||
}
|
||||
return storeErr(store.SetCurrent(name))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package pama
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
)
|
||||
|
||||
func (m PatchManager) Projects(name string) ([]models.Project, error) {
|
||||
all, err := m.store().Projects()
|
||||
if err != nil {
|
||||
return nil, storeErr(err)
|
||||
}
|
||||
if len(name) == 0 {
|
||||
return all, nil
|
||||
}
|
||||
var projects []models.Project
|
||||
for _, p := range all {
|
||||
if strings.Contains(p.Name, name) {
|
||||
projects = append(projects, p)
|
||||
}
|
||||
}
|
||||
if len(projects) == 0 {
|
||||
return nil, errors.New("No projects found.")
|
||||
}
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func (m PatchManager) NewReader(projects []models.Project) io.Reader {
|
||||
cur, err := m.CurrentProject()
|
||||
currentName := cur.Name
|
||||
if err != nil {
|
||||
log.Warnf("could not get current project: %v", err)
|
||||
currentName = ""
|
||||
}
|
||||
|
||||
readers := make([]io.Reader, 0, len(projects))
|
||||
for _, p := range projects {
|
||||
rc, err := m.rc(p.RevctrlID, p.Root)
|
||||
if err != nil {
|
||||
log.Errorf("project '%s' failed with: %v", p.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
notes := make(map[string]string)
|
||||
for _, c := range p.Commits {
|
||||
if !rc.Exists(c.ID) {
|
||||
notes[c.ID] = "Rebase needed"
|
||||
}
|
||||
}
|
||||
|
||||
active := p.Name == currentName && len(projects) > 1
|
||||
readers = append(readers, p.NewReader(active, notes))
|
||||
}
|
||||
return io.MultiReader(readers...)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Untracked = "untracked"
|
||||
)
|
||||
|
||||
func NewCommit(r RevisionController, id, tag string) Commit {
|
||||
return Commit{
|
||||
ID: id,
|
||||
Subject: r.Subject(id),
|
||||
Author: r.Author(id),
|
||||
Date: r.Date(id),
|
||||
MessageId: "",
|
||||
Tag: tag,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Commit) Untracked() bool {
|
||||
return c.Tag == Untracked
|
||||
}
|
||||
|
||||
func (c Commit) Info() string {
|
||||
s := []string{}
|
||||
if c.Subject == "" {
|
||||
s = append(s, "(no subject)")
|
||||
} else {
|
||||
s = append(s, c.Subject)
|
||||
}
|
||||
if c.Author != "" {
|
||||
s = append(s, c.Author)
|
||||
}
|
||||
if c.Date != "" {
|
||||
s = append(s, c.Date)
|
||||
}
|
||||
if c.MessageId != "" {
|
||||
s = append(s, "<"+c.MessageId+">")
|
||||
}
|
||||
return strings.Join(s, ", ")
|
||||
}
|
||||
|
||||
func (c Commit) String() string {
|
||||
return fmt.Sprintf("%-6.6s %s", c.ID, c.Info())
|
||||
}
|
||||
|
||||
type Commits []Commit
|
||||
|
||||
func (h Commits) Tags() []string {
|
||||
var tags []string
|
||||
dedup := make(map[string]struct{})
|
||||
for _, c := range h {
|
||||
_, ok := dedup[c.Tag]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
tags = append(tags, c.Tag)
|
||||
dedup[c.Tag] = struct{}{}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func (h Commits) HasTag(t string) bool {
|
||||
for _, c := range h {
|
||||
if c.Tag == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h Commits) Lookup(id string) (Commit, bool) {
|
||||
for _, c := range h {
|
||||
if c.ID == id {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return Commit{}, false
|
||||
}
|
||||
|
||||
type CommitIDs []string
|
||||
|
||||
func (c CommitIDs) Has(id string) bool {
|
||||
for _, cid := range c {
|
||||
if cid == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package models
|
||||
|
||||
// Commit represents a commit object in a revision control system.
|
||||
type Commit struct {
|
||||
// ID is the commit hash.
|
||||
ID string
|
||||
// Subject is the subject line of the commit.
|
||||
Subject string
|
||||
// Author is the author's name.
|
||||
Author string
|
||||
// Date associated with the given commit.
|
||||
Date string
|
||||
// MessageId is the message id for the message that contains the commit
|
||||
// diff. This field is only set when commits were applied via patch
|
||||
// apply system.
|
||||
MessageId string
|
||||
// Tag is a user label that is assigned to one or multiple commits. It
|
||||
// creates a logical connection between a group of commits to represent
|
||||
// a patch set.
|
||||
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 {
|
||||
// Name is the project name and works as the project ID. Do not change
|
||||
// it.
|
||||
Name string
|
||||
// Root represents the root directory of the revision control system.
|
||||
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
|
||||
// contain any commit between the Base commit and HEAD. These commits
|
||||
// will be updated by an applying, removing or rebase operation.
|
||||
Commits []Commit
|
||||
}
|
||||
|
||||
// RevisionController is an interface to a revision control system.
|
||||
type RevisionController interface {
|
||||
// Returns the commit hash of the HEAD commit.
|
||||
Head() (string, error)
|
||||
// History accepts a commit hash and returns a list of commit hashes
|
||||
// between the provided hash and HEAD. The order of the returned slice
|
||||
// is important. The commit hashes should be ordered from "earlier" to
|
||||
// "later" where the last element must be HEAD.
|
||||
History(string) ([]string, error)
|
||||
// Clean returns true if there are no unstaged changes. If there are
|
||||
// unstaged changes, applying and removing patches will not work.
|
||||
Clean() bool
|
||||
// Exists returns true if the commit hash exists in the commit history.
|
||||
Exists(string) bool
|
||||
// Subject returns the subject line for the provided commit hash.
|
||||
Subject(string) string
|
||||
// Author returns the author for the provided commit hash.
|
||||
Author(string) string
|
||||
// Date returns the date for the provided commit hash.
|
||||
Date(string) string
|
||||
// Drop removes the commit with the provided commit hash from the
|
||||
// repository.
|
||||
Drop(string) error
|
||||
// ApplyCmd returns a string with an executable command that is used to
|
||||
// apply patches with the :pipe command.
|
||||
ApplyCmd() string
|
||||
// CreateWorktree creates 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.
|
||||
type PersistentStorer interface {
|
||||
// StoreProject saves the project data persistently. If overwrite is
|
||||
// true, it will write over existing data.
|
||||
StoreProject(Project, bool) error
|
||||
// DeleteProject removes the project data from the store.
|
||||
DeleteProject(string) error
|
||||
// CurrentName returns the Project.Name for the active project.
|
||||
CurrentName() (string, error)
|
||||
// SetCurrent stores a Project.Name and make that project active.
|
||||
SetCurrent(string) error
|
||||
// Current returns the project data for the active project.
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
)
|
||||
|
||||
var templateText = `
|
||||
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}}
|
||||
{{- range $index, $patch := .Patches}}
|
||||
{{$patch}}:
|
||||
{{- range (index $commits $patch)}}
|
||||
{{with (index $notes .ID)}}[{{.}}] {{end}}{{. -}}
|
||||
{{end}}
|
||||
{{end -}}
|
||||
`
|
||||
|
||||
var viewRenderer = template.Must(template.New("ProjectToText").Parse(templateText))
|
||||
|
||||
type view struct {
|
||||
Name string
|
||||
Root string
|
||||
Base Commit
|
||||
// Patches are the unique tag names.
|
||||
Patches []string
|
||||
// Commits is a map where the tag names are keys and the associated
|
||||
// commits the values.
|
||||
Commits map[string][]Commit
|
||||
// Notes contain annotations of the commits where the commit hash is
|
||||
// the key and the annotation is the value.
|
||||
Notes map[string]string
|
||||
// IsActive is true if the current project is selected.
|
||||
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,
|
||||
IsWorktree: p.Worktree.Root != "" && p.Worktree.Name != "",
|
||||
WorktreeParent: p.Worktree.Name,
|
||||
}
|
||||
|
||||
for _, commit := range p.Commits {
|
||||
patch := commit.Tag
|
||||
commits, ok := v.Commits[patch]
|
||||
if !ok {
|
||||
v.Patches = append(v.Patches, patch)
|
||||
}
|
||||
commits = append(commits, commit)
|
||||
v.Commits[patch] = commits
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (v view) String() string {
|
||||
var buf bytes.Buffer
|
||||
err := viewRenderer.Execute(&buf, v)
|
||||
if err != nil {
|
||||
log.Errorf("failed to run template: %v", err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (p Project) String() string {
|
||||
return newView(p, false, nil).String()
|
||||
}
|
||||
|
||||
func (p Project) NewReader(isActive bool, notes map[string]string) io.Reader {
|
||||
return strings.NewReader(newView(p, isActive, notes).String())
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package pama
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/revctrl"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/store"
|
||||
)
|
||||
|
||||
type (
|
||||
detectFn func(string) (string, string, error)
|
||||
rcFn func(string, string) (models.RevisionController, error)
|
||||
storeFn func() models.PersistentStorer
|
||||
)
|
||||
|
||||
type PatchManager struct {
|
||||
detect detectFn
|
||||
rc rcFn
|
||||
store storeFn
|
||||
}
|
||||
|
||||
func New() PatchManager {
|
||||
return PatchManager{
|
||||
detect: revctrl.Detect,
|
||||
rc: revctrl.New,
|
||||
store: store.Store,
|
||||
}
|
||||
}
|
||||
|
||||
func FromFunc(d detectFn, r rcFn, s storeFn) PatchManager {
|
||||
return PatchManager{
|
||||
detect: d,
|
||||
rc: r,
|
||||
store: s,
|
||||
}
|
||||
}
|
||||
|
||||
func storeErr(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("store error: %w", err)
|
||||
}
|
||||
|
||||
func revErr(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("revision control error: %w", err)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package pama_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
)
|
||||
|
||||
var errNotFound = errors.New("not found")
|
||||
|
||||
func newCommit(id, subj, tag string) models.Commit {
|
||||
return models.Commit{ID: id, Subject: subj, Tag: tag}
|
||||
}
|
||||
|
||||
func newTestManager(
|
||||
commits []string,
|
||||
subjects []string,
|
||||
data map[string]models.Project,
|
||||
current string,
|
||||
) (pama.PatchManager, models.RevisionController, models.PersistentStorer) {
|
||||
rc := mockRevctrl{
|
||||
commitIDs: commits,
|
||||
titles: subjects,
|
||||
}
|
||||
store := mockStore{
|
||||
data: data,
|
||||
current: current,
|
||||
}
|
||||
return pama.FromFunc(
|
||||
nil,
|
||||
func(_ string, _ string) (models.RevisionController, error) {
|
||||
return &rc, nil
|
||||
},
|
||||
func() models.PersistentStorer {
|
||||
return &store
|
||||
},
|
||||
), &rc, &store
|
||||
}
|
||||
|
||||
type mockRevctrl struct {
|
||||
commitIDs []string
|
||||
titles []string
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) Support() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) Clean() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) Root() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) Head() (string, error) {
|
||||
return c.commitIDs[len(c.commitIDs)-1], nil
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) History(commit string) ([]string, error) {
|
||||
for i, s := range c.commitIDs {
|
||||
if s == commit {
|
||||
cp := make([]string, len(c.commitIDs[i+1:]))
|
||||
copy(cp, c.commitIDs[i+1:])
|
||||
return cp, nil
|
||||
}
|
||||
}
|
||||
return nil, errNotFound
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) Exists(commit string) bool {
|
||||
for _, s := range c.commitIDs {
|
||||
if s == commit {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) Subject(commit string) string {
|
||||
for i, s := range c.commitIDs {
|
||||
if s == commit {
|
||||
return c.titles[i]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) Author(commit string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) Date(commit string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) Drop(commit string) error {
|
||||
for i, s := range c.commitIDs {
|
||||
if s == commit {
|
||||
c.commitIDs = append(c.commitIDs[:i], c.commitIDs[i+1:]...)
|
||||
c.titles = append(c.titles[:i], c.titles[i+1:]...)
|
||||
// modify commitIDs to simulate a "real" change in
|
||||
// commit history that will also change all subsequent
|
||||
// commitIDs
|
||||
for j := i; j < len(c.commitIDs); j++ {
|
||||
c.commitIDs[j] += "_new"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errNotFound
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) CreateWorktree(_, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) DeleteWorktree(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockRevctrl) ApplyCmd() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type mockStore struct {
|
||||
data map[string]models.Project
|
||||
current string
|
||||
}
|
||||
|
||||
func (s *mockStore) StoreProject(p models.Project, ow bool) error {
|
||||
_, ok := s.data[p.Name]
|
||||
if ok && !ow {
|
||||
return errors.New("already there")
|
||||
}
|
||||
s.data[p.Name] = p
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockStore) DeleteProject(name string) error {
|
||||
delete(s.data, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockStore) CurrentName() (string, error) {
|
||||
return s.current, nil
|
||||
}
|
||||
|
||||
func (s *mockStore) SetCurrent(c string) error {
|
||||
s.current = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockStore) Current() (models.Project, error) {
|
||||
return s.data[s.current], nil
|
||||
}
|
||||
|
||||
func (s *mockStore) Names() ([]string, error) {
|
||||
var names []string
|
||||
for name := range s.data {
|
||||
names = append(names, name)
|
||||
}
|
||||
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 {
|
||||
ps = append(ps, p)
|
||||
}
|
||||
return ps, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package pama
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
)
|
||||
|
||||
// RebaseCommits fetches the commits between baseID and HEAD. The tags from the
|
||||
// current project will be mapped onto the fetched commits based on either the
|
||||
// commit hash or the commit subject.
|
||||
func (m PatchManager) RebaseCommits(p models.Project, baseID string) ([]models.Commit, error) {
|
||||
rc, err := m.rc(p.RevctrlID, p.Root)
|
||||
if err != nil {
|
||||
return nil, revErr(err)
|
||||
}
|
||||
|
||||
if !rc.Exists(baseID) {
|
||||
return nil, fmt.Errorf("cannot rebase on %s. "+
|
||||
"commit does not exist", baseID)
|
||||
}
|
||||
|
||||
commitIDs, err := rc.History(baseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commits := make([]models.Commit, len(commitIDs))
|
||||
for i := 0; i < len(commitIDs); i++ {
|
||||
commits[i] = models.NewCommit(
|
||||
rc,
|
||||
commitIDs[i],
|
||||
models.Untracked,
|
||||
)
|
||||
}
|
||||
|
||||
// map tags from the commits from the project p
|
||||
for i, r := range commits {
|
||||
for _, c := range p.Commits {
|
||||
if c.ID == r.ID || c.Subject == r.Subject {
|
||||
commits[i].MessageId = c.MessageId
|
||||
commits[i].Tag = c.Tag
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
// SaveRebased checks if the commits actually exist in the repo, repopulate the
|
||||
// info fields and saves the baseID for project p.
|
||||
func (m PatchManager) SaveRebased(p models.Project, baseID string, commits []models.Commit) error {
|
||||
rc, err := m.rc(p.RevctrlID, p.Root)
|
||||
if err != nil {
|
||||
return revErr(err)
|
||||
}
|
||||
|
||||
exist := make([]models.Commit, 0, len(commits))
|
||||
for _, c := range commits {
|
||||
if !rc.Exists(c.ID) {
|
||||
continue
|
||||
}
|
||||
exist = append(exist, c)
|
||||
}
|
||||
|
||||
for i, c := range exist {
|
||||
exist[i].Subject = rc.Subject(c.ID)
|
||||
exist[i].Author = rc.Author(c.ID)
|
||||
exist[i].Date = rc.Date(c.ID)
|
||||
}
|
||||
|
||||
p.Commits = exist
|
||||
|
||||
if rc.Exists(baseID) {
|
||||
p.Base = models.NewCommit(rc, baseID, "")
|
||||
}
|
||||
|
||||
err = m.store().StoreProject(p, true)
|
||||
return storeErr(err)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package revctrl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
)
|
||||
|
||||
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) Drop(commit string) error {
|
||||
_, exitcode, err := g.do("rebase", "--onto", commit+"^", commit)
|
||||
if exitcode > 0 {
|
||||
return fmt.Errorf("failed to drop 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) 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package revctrl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
"git.sr.ht/~rjarry/aerc/lib/xdg"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
const (
|
||||
keyPrefix = "project."
|
||||
)
|
||||
|
||||
var (
|
||||
// versTag should be incremented when the underlying data structure
|
||||
// changes.
|
||||
versTag = []byte("0001")
|
||||
versTagKey = []byte("version.tag")
|
||||
currentKey = []byte("current.project")
|
||||
)
|
||||
|
||||
func createKey(name string) []byte {
|
||||
return []byte(keyPrefix + name)
|
||||
}
|
||||
|
||||
func parseKey(key []byte) string {
|
||||
return strings.TrimPrefix(string(key), keyPrefix)
|
||||
}
|
||||
|
||||
func isProjectKey(key []byte) bool {
|
||||
return bytes.HasPrefix(key, []byte(keyPrefix))
|
||||
}
|
||||
|
||||
func cacheDir() (string, error) {
|
||||
dir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
dir = xdg.ExpandHome("~/.cache")
|
||||
}
|
||||
return path.Join(dir, "aerc"), nil
|
||||
}
|
||||
|
||||
func openStorage() (*leveldb.DB, error) {
|
||||
cd, err := cacheDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to find project store directory: %w", err)
|
||||
}
|
||||
p := path.Join(cd, "projects")
|
||||
|
||||
db, err := leveldb.OpenFile(p, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to open project store: %w", err)
|
||||
}
|
||||
|
||||
has, err := db.Has(versTagKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setTag := !has
|
||||
if has {
|
||||
vers, err := db.Get(versTagKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !bytes.Equal(vers, versTag) {
|
||||
log.Warnf("patch store: version mismatch: wipe data")
|
||||
iter := db.NewIterator(nil, nil)
|
||||
for iter.Next() {
|
||||
err := db.Delete(iter.Key(), nil)
|
||||
if err != nil {
|
||||
log.Errorf("delete: %v")
|
||||
}
|
||||
}
|
||||
iter.Release()
|
||||
setTag = true
|
||||
}
|
||||
}
|
||||
|
||||
if setTag {
|
||||
err := db.Put(versTagKey, versTag, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("patch store: set version: %s", string(versTag))
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func encode(p models.Project) ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
func decode(data []byte) (p models.Project, err error) {
|
||||
err = json.Unmarshal(data, &p)
|
||||
return
|
||||
}
|
||||
|
||||
func Store() models.PersistentStorer {
|
||||
return &instance{}
|
||||
}
|
||||
|
||||
type instance struct{}
|
||||
|
||||
func (instance) StoreProject(p models.Project, overwrite bool) error {
|
||||
db, err := openStorage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
key := createKey(p.Name)
|
||||
has, err := db.Has(key, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if has && !overwrite {
|
||||
return fmt.Errorf("Project '%s' already exists.", p.Name)
|
||||
}
|
||||
|
||||
log.Debugf("project data: %v", p)
|
||||
|
||||
encoded, err := encode(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Put(key, encoded, nil)
|
||||
}
|
||||
|
||||
func (instance) DeleteProject(name string) error {
|
||||
db, err := openStorage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
key := createKey(name)
|
||||
has, err := db.Has(key, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return fmt.Errorf("Project does not exist.")
|
||||
}
|
||||
return db.Delete(key, nil)
|
||||
}
|
||||
|
||||
func (instance) CurrentName() (string, error) {
|
||||
db, err := openStorage()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer db.Close()
|
||||
cur, err := db.Get(currentKey, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return parseKey(cur), nil
|
||||
}
|
||||
|
||||
func (instance) SetCurrent(name string) error {
|
||||
db, err := openStorage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
key := createKey(name)
|
||||
return db.Put(currentKey, key, nil)
|
||||
}
|
||||
|
||||
func (instance) Current() (models.Project, error) {
|
||||
db, err := openStorage()
|
||||
if err != nil {
|
||||
return models.Project{}, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
has, err := db.Has(currentKey, nil)
|
||||
if err != nil {
|
||||
return models.Project{}, err
|
||||
}
|
||||
if !has {
|
||||
return models.Project{}, fmt.Errorf("No (current) project found; run 'project init' first.")
|
||||
}
|
||||
curProjectKey, err := db.Get(currentKey, nil)
|
||||
if err != nil {
|
||||
return models.Project{}, err
|
||||
}
|
||||
raw, err := db.Get(curProjectKey, nil)
|
||||
if err != nil {
|
||||
return models.Project{}, err
|
||||
}
|
||||
p, err := decode(raw)
|
||||
if err != nil {
|
||||
return models.Project{}, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (instance) Names() ([]string, error) {
|
||||
db, err := openStorage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
var names []string
|
||||
iter := db.NewIterator(nil, nil)
|
||||
for iter.Next() {
|
||||
if !isProjectKey(iter.Key()) {
|
||||
continue
|
||||
}
|
||||
names = append(names, parseKey(iter.Key()))
|
||||
}
|
||||
iter.Release()
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
iter := db.NewIterator(nil, nil)
|
||||
for iter.Next() {
|
||||
if !isProjectKey(iter.Key()) {
|
||||
continue
|
||||
}
|
||||
p, err := decode(iter.Value())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projects = append(projects, p)
|
||||
}
|
||||
iter.Release()
|
||||
return projects, nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package pama
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
)
|
||||
|
||||
func (m PatchManager) SwitchProject(name string) error {
|
||||
c, err := m.CurrentProject()
|
||||
if err == nil {
|
||||
if c.Name == name {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
names, err := m.store().Names()
|
||||
if err != nil {
|
||||
return storeErr(err)
|
||||
}
|
||||
found := false
|
||||
for _, n := range names {
|
||||
if n == name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("Project '%s' not found", name)
|
||||
}
|
||||
return storeErr(m.store().SetCurrent(name))
|
||||
}
|
||||
|
||||
var switchDebouncer *time.Timer
|
||||
|
||||
func DebouncedSwitchProject(name string) {
|
||||
if switchDebouncer != nil {
|
||||
if switchDebouncer.Stop() {
|
||||
log.Debugf("pama: switch debounced")
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
switchDebouncer = time.AfterFunc(500*time.Millisecond, func() {
|
||||
if err := New().SwitchProject(name); err != nil {
|
||||
log.Debugf("could not switch to project %s: %v",
|
||||
name, err)
|
||||
} else {
|
||||
log.Debugf("project switch to project %s", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var fromSubject = regexp.MustCompile(
|
||||
`\[\s*(RFC|DRAFT|[Dd]raft)*\s*(PATCH|[Pp]atch)\s+([^\s\]]+)\s*[vV]*[0-9/]*\s*\] `)
|
||||
|
||||
func FromSubject(s string) string {
|
||||
matches := fromSubject.FindStringSubmatch(s)
|
||||
if len(matches) >= 3 {
|
||||
return matches[3]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package pama_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama"
|
||||
)
|
||||
|
||||
func TestFromSubject(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
s: "[PATCH aerc] pama: new patch",
|
||||
want: "aerc",
|
||||
},
|
||||
{
|
||||
s: "[PATCH aerc v2] pama: new patch",
|
||||
want: "aerc",
|
||||
},
|
||||
{
|
||||
s: "[PATCH aerc 1/2] pama: new patch",
|
||||
want: "aerc",
|
||||
},
|
||||
{
|
||||
s: "[Patch aerc] pama: new patch",
|
||||
want: "aerc",
|
||||
},
|
||||
{
|
||||
s: "[patch aerc] pama: new patch",
|
||||
want: "aerc",
|
||||
},
|
||||
{
|
||||
s: "[RFC PATCH aerc] pama: new patch",
|
||||
want: "aerc",
|
||||
},
|
||||
{
|
||||
s: "[DRAFT PATCH aerc] pama: new patch",
|
||||
want: "aerc",
|
||||
},
|
||||
{
|
||||
s: "RE: [PATCH aerc v1] pama: new patch",
|
||||
want: "aerc",
|
||||
},
|
||||
{
|
||||
s: "[PATCH] pama: new patch",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
s: "just a subject line",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
s: "just a subject line with unrelated [asdf aerc v1]",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := pama.FromSubject(test.s)
|
||||
if got != test.want {
|
||||
t.Errorf("failed to get name from '%s': "+
|
||||
"got '%s', want '%s'", test.s, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package pama
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
)
|
||||
|
||||
// Unlink removes provided project
|
||||
func (m PatchManager) Unlink(name string) error {
|
||||
store := m.store()
|
||||
names, err := m.Names()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
index := -1
|
||||
for i, s := range names {
|
||||
if s == name {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index < 0 {
|
||||
return fmt.Errorf("Project '%s' not found", name)
|
||||
}
|
||||
|
||||
cur, err := store.CurrentName()
|
||||
if err == nil && cur == name {
|
||||
var next string
|
||||
for _, s := range names {
|
||||
if name != s {
|
||||
next = s
|
||||
break
|
||||
}
|
||||
}
|
||||
err = store.SetCurrent(next)
|
||||
if err != nil {
|
||||
return storeErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func (m PatchManager) Names() ([]string, error) {
|
||||
names, err := m.store().Names()
|
||||
return names, storeErr(err)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package pama
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pama/models"
|
||||
"git.sr.ht/~rjarry/aerc/lib/xdg"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user