init: pristine aerc 0.20.0 source

This commit is contained in:
Mortdecai
2026-04-07 19:54:54 -04:00
commit 083402a548
502 changed files with 68722 additions and 0 deletions
+268
View File
@@ -0,0 +1,268 @@
package patch
import (
"fmt"
"sort"
"strings"
"unicode"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/commands/msg"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/pama"
"git.sr.ht/~rjarry/aerc/lib/pama/models"
)
type Apply struct {
Cmd string `opt:"-c" desc:"Apply patches with provided command."`
Worktree string `opt:"-w" desc:"Create linked worktree on this <commit-ish>."`
Tag string `opt:"tag" required:"true" complete:"CompleteTag" desc:"Identify patches with tag."`
}
func init() {
register(Apply{})
}
func (Apply) Description() string {
return "Apply the selected message(s) to the current project."
}
func (Apply) Context() commands.CommandContext {
return commands.MESSAGE_LIST | commands.MESSAGE_VIEWER
}
func (Apply) Aliases() []string {
return []string{"apply"}
}
func (*Apply) CompleteTag(arg string) []string {
patches, err := pama.New().CurrentPatches()
if err != nil {
log.Errorf("failed to current patches for completion: %v", err)
patches = nil
}
acct := app.SelectedAccount()
if acct == nil {
return nil
}
uids, err := acct.MarkedMessages()
if err != nil {
return nil
}
if len(uids) == 0 {
msg, err := acct.SelectedMessage()
if err == nil {
uids = append(uids, msg.Uid)
}
}
store := acct.Store()
if store == nil {
return nil
}
var subjects []string
for _, uid := range uids {
if msg, ok := store.Messages[uid]; !ok || msg == nil || msg.Envelope == nil {
continue
} else {
subjects = append(subjects, msg.Envelope.Subject)
}
}
return proposePatchName(patches, subjects)
}
func (a Apply) Execute(args []string) error {
patch := a.Tag
worktree := a.Worktree
applyCmd := a.Cmd
m := pama.New()
p, err := m.CurrentProject()
if err != nil {
return err
}
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)
}
if !m.Clean(p) {
return fmt.Errorf("Aborting... There are unstaged changes in " +
"your repository.")
}
commit, err := m.Head(p)
if err != nil {
return err
}
log.Tracef("HEAD commit before: %s", commit)
if applyCmd != "" {
rootFmt := "%r"
if strings.Contains(applyCmd, rootFmt) {
applyCmd = strings.ReplaceAll(applyCmd, rootFmt, p.Root)
}
log.Infof("use custom apply command: %s", applyCmd)
} else {
applyCmd, err = m.ApplyCmd(p)
if err != nil {
return err
}
}
msgData := collectMessageData()
// apply patches with the pipe cmd
pipe := msg.Pipe{
Background: false,
Full: true,
Part: false,
Command: applyCmd,
}
return pipe.Run(func() {
p, err = m.ApplyUpdate(p, patch, commit, msgData)
if err != nil {
log.Errorf("Failed to save patch data: %v", err)
}
})
}
// collectMessageData returns a map where the key is the message id and the
// value the subject of the marked messages
func collectMessageData() map[string]string {
acct := app.SelectedAccount()
if acct == nil {
return nil
}
uids, err := commands.MarkedOrSelected(acct)
if err != nil {
log.Errorf("error occurred: %v", err)
return nil
}
store := acct.Store()
if store == nil {
return nil
}
kv := make(map[string]string)
for _, uid := range uids {
msginfo, ok := store.Messages[uid]
if !ok || msginfo == nil {
continue
}
id, err := msginfo.MsgId()
if err != nil {
continue
}
if msginfo.Envelope == nil {
continue
}
kv[id] = msginfo.Envelope.Subject
}
return kv
}
func proposePatchName(patches, subjects []string) []string {
parse := func(s string) (string, string, bool) {
var tag strings.Builder
var version string
var i, j int
i = strings.Index(s, "[")
if i < 0 {
goto noPatch
}
s = s[i+1:]
j = strings.Index(s, "]")
if j < 0 {
goto noPatch
}
for _, elem := range strings.Fields(s[:j]) {
vers := strings.ToLower(elem)
if !strings.HasPrefix(vers, "v") {
continue
}
isVersion := true
for _, r := range vers[1:] {
if !unicode.IsDigit(r) {
isVersion = false
break
}
}
if isVersion {
version = vers
break
}
}
s = strings.TrimSpace(s[j+1:])
for _, r := range s {
if unicode.IsSpace(r) || r == ':' {
break
}
_, err := tag.WriteRune(r)
if err != nil {
continue
}
}
return tag.String(), version, true
noPatch:
return "", "", false
}
summary := make(map[string]struct{})
var results []string
for _, s := range subjects {
tag, version, isPatch := parse(s)
if tag == "" || !isPatch {
continue
}
if version == "" {
version = "v1"
}
result := fmt.Sprintf("%s_%s", tag, version)
result = strings.ReplaceAll(result, " ", "")
collision := false
for _, name := range patches {
if name == result {
collision = true
}
}
if collision {
continue
}
_, ok := summary[result]
if ok {
continue
}
results = append(results, result)
summary[result] = struct{}{}
}
sort.Strings(results)
return results
}
+53
View File
@@ -0,0 +1,53 @@
package patch
import (
"reflect"
"testing"
)
func TestPatchApply_ProposeName(t *testing.T) {
tests := []struct {
name string
exist []string
subjects []string
want []string
}{
{
name: "base case",
exist: nil,
subjects: []string{
"[PATCH aerc v3 3/3] notmuch: remove unused code",
"[PATCH aerc v3 2/3] notmuch: replace notmuch library with internal bindings",
"[PATCH aerc v3 1/3] notmuch: add notmuch bindings",
},
want: []string{"notmuch_v3"},
},
{
name: "distorted case",
exist: nil,
subjects: []string{
"[PATCH vaerc v3 3/3] notmuch: remove unused code",
"[PATCH aerc 3v 2/3] notmuch: replace notmuch library with internal bindings",
},
want: []string{"notmuch_v1", "notmuch_v3"},
},
{
name: "invalid patches",
exist: nil,
subjects: []string{
"notmuch: remove unused code",
": replace notmuch library with internal bindings",
},
want: nil,
},
}
for _, test := range tests {
got := proposePatchName(test.exist, test.subjects)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("test '%s' failed to propose the correct "+
"name: got '%v', but want '%v'", test.name,
got, test.want)
}
}
}
+51
View File
@@ -0,0 +1,51 @@
package patch
import (
"fmt"
"os"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/pama"
)
type Cd struct{}
func init() {
register(Cd{})
}
func (Cd) Description() string {
return "Change aerc's working directory to the current project."
}
func (Cd) Context() commands.CommandContext {
return commands.GLOBAL
}
func (Cd) Aliases() []string {
return []string{"cd"}
}
func (Cd) Execute(args []string) error {
p, err := pama.New().CurrentProject()
if err != nil {
return err
}
cwd, err := os.Getwd()
if err != nil {
return err
}
if cwd == p.Root {
app.PushStatus("Already here.", 10*time.Second)
return nil
}
err = os.Chdir(p.Root)
if err != nil {
return err
}
app.PushStatus(fmt.Sprintf("Changed to %s.", p.Root),
10*time.Second)
return nil
}
+51
View File
@@ -0,0 +1,51 @@
package patch
import (
"fmt"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/pama"
)
type Drop struct {
Tag string `opt:"tag" complete:"CompleteTag" desc:"Repository patch tag."`
}
func init() {
register(Drop{})
}
func (Drop) Description() string {
return "Drop a patch from the repository."
}
func (Drop) Context() commands.CommandContext {
return commands.GLOBAL
}
func (Drop) Aliases() []string {
return []string{"drop"}
}
func (*Drop) CompleteTag(arg string) []string {
patches, err := pama.New().CurrentPatches()
if err != nil {
log.Errorf("failed to get current patches: %v", err)
return nil
}
return commands.FilterList(patches, arg, nil)
}
func (r Drop) Execute(args []string) error {
patch := r.Tag
err := pama.New().DropPatch(patch)
if err != nil {
return err
}
app.PushStatus(fmt.Sprintf("Patch %s has been dropped", patch),
10*time.Second)
return nil
}
+140
View File
@@ -0,0 +1,140 @@
package patch
import (
"errors"
"fmt"
"net/textproto"
"strings"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/commands/account"
"git.sr.ht/~rjarry/aerc/lib/pama"
"git.sr.ht/~rjarry/aerc/lib/pama/models"
"git.sr.ht/~rjarry/go-opt/v2"
)
type Find struct {
Filter bool `opt:"-f" desc:"Filter message list instead of search."`
Commit []string `opt:"..." required:"true" complete:"Complete" desc:"Search for <commit-ish>."`
}
func init() {
register(Find{})
}
func (Find) Description() string {
return "Search for applied patches."
}
func (Find) Context() commands.CommandContext {
return commands.GLOBAL
}
func (Find) Aliases() []string {
return []string{"find"}
}
func (*Find) Complete(arg string) []string {
m := pama.New()
p, err := m.CurrentProject()
if err != nil {
return nil
}
options := make([]string, len(p.Commits))
for i, c := range p.Commits {
options[i] = fmt.Sprintf("%-6.6s %s", c.ID, c.Subject)
}
return commands.FilterList(options, arg, nil)
}
func (s Find) Execute(_ []string) error {
m := pama.New()
p, err := m.CurrentProject()
if err != nil {
return err
}
if len(s.Commit) == 0 {
return errors.New("missing commit hash")
}
lexed := opt.LexArgs(strings.TrimSpace(s.Commit[0]))
hash, err := lexed.ArgSafe(0)
if err != nil {
return err
}
if len(hash) < 4 {
return errors.New("Commit hash is too short.")
}
var c models.Commit
for _, commit := range p.Commits {
if strings.Contains(commit.ID, hash) {
c = commit
break
}
}
if c.ID == "" {
var err error
c, err = m.Find(hash, p)
if err != nil {
return err
}
}
// If Message-Id is provided, find it in store
if c.MessageId != "" {
if selectMessageId(c.MessageId) {
return nil
}
}
// Fallback to a search based on the subject line
args := []string{"search"}
if s.Filter {
args[0] = "filter"
}
headers := make(textproto.MIMEHeader)
args = append(args, fmt.Sprintf("-H Subject:%s", c.Subject))
headers.Add("Subject", c.Subject)
cmd := account.SearchFilter{
Headers: headers,
}
return cmd.Execute(args)
}
func selectMessageId(msgid string) bool {
acct := app.SelectedAccount()
if acct == nil {
return false
}
store := acct.Store()
if store == nil {
return false
}
for uid, msg := range store.Messages {
if msg == nil {
continue
}
if msg.RFC822Headers == nil {
continue
}
id, err := msg.RFC822Headers.MessageID()
if err != nil {
continue
}
if id == msgid {
store.Select(uid)
return true
}
}
return false
}
+45
View File
@@ -0,0 +1,45 @@
package patch
import (
"fmt"
"os"
"path/filepath"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/pama"
)
type Init struct {
Force bool `opt:"-f" desc:"Overwrite any existing project."`
Name string `opt:"name" required:"false"`
}
func init() {
register(Init{})
}
func (Init) Description() string {
return "Create a new project."
}
func (Init) Context() commands.CommandContext {
return commands.GLOBAL
}
func (Init) Aliases() []string {
return []string{"init"}
}
func (i Init) Execute(args []string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("Could not get current directory: %w", err)
}
name := i.Name
if name == "" {
name = filepath.Base(cwd)
}
return pama.New().Init(name, cwd, i.Force)
}
+114
View File
@@ -0,0 +1,114 @@
package patch
import (
"bufio"
"fmt"
"io"
"os/exec"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib/pama"
"git.sr.ht/~rjarry/aerc/lib/pama/models"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/go-opt/v2"
"git.sr.ht/~rockorager/vaxis"
)
type List struct {
All bool `opt:"-a" desc:"List all projects."`
}
func init() {
register(List{})
}
func (List) Description() string {
return "List the current project with the tracked patch sets."
}
func (List) Context() commands.CommandContext {
return commands.GLOBAL
}
func (List) Aliases() []string {
return []string{"list", "ls"}
}
func (l List) Execute(args []string) error {
m := pama.New()
current, err := m.CurrentProject()
if err != nil {
return err
}
projects := []models.Project{current}
if l.All {
projects, err = m.Projects("")
if err != nil {
return err
}
}
app.PushStatus(fmt.Sprintf("Current project: %s", current.Name), 30*time.Second)
createWidget := func(r io.Reader) (ui.DrawableInteractive, error) {
pagerCmd, err := app.CmdFallbackSearch(config.PagerCmds(), true)
if err != nil {
return nil, err
}
cmd := opt.SplitArgs(pagerCmd)
pager := exec.Command(cmd[0], cmd[1:]...)
pager.Stdin = r
term, err := app.NewTerminal(pager)
if err != nil {
return nil, err
}
start := time.Now()
term.OnClose = func(err error) {
if time.Since(start) > 250*time.Millisecond {
app.CloseDialog()
return
}
term.OnEvent = func(_ vaxis.Event) bool {
app.CloseDialog()
return true
}
}
return term, nil
}
viewer, err := createWidget(m.NewReader(projects))
if err != nil {
viewer = app.NewListBox(
"Press <Esc> or <Enter> to close. "+
"Start typing to filter.",
numerify(m.NewReader(projects)), app.SelectedAccountUiConfig(),
func(_ string) { app.CloseDialog() },
)
}
app.AddDialog(app.DefaultDialog(
ui.NewBox(viewer, "Patch Management", "",
app.SelectedAccountUiConfig(),
),
))
return nil
}
func numerify(r io.Reader) []string {
var lines []string
nr := 1
scanner := bufio.NewScanner(r)
for scanner.Scan() {
s := scanner.Text()
lines = append(lines, fmt.Sprintf("%3d %s", nr, s))
nr++
}
return lines
}
+88
View File
@@ -0,0 +1,88 @@
package patch
import (
"errors"
"fmt"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/go-opt/v2"
)
var subCommands map[string]commands.Command
func register(cmd commands.Command) {
if subCommands == nil {
subCommands = make(map[string]commands.Command)
}
for _, alias := range cmd.Aliases() {
if subCommands[alias] != nil {
panic("duplicate sub command alias: " + alias)
}
subCommands[alias] = cmd
}
}
type Patch struct {
SubCmd commands.Command `opt:"command" action:"ParseSub" complete:"CompleteSubNames" desc:"Sub command."`
Args string `opt:"..." required:"false" complete:"CompleteSubArgs"`
}
func init() {
commands.Register(Patch{})
}
func (Patch) Description() string {
return "Local patch management commands."
}
func (Patch) Context() commands.CommandContext {
return commands.GLOBAL
}
func (Patch) Aliases() []string {
return []string{"patch"}
}
func (p *Patch) ParseSub(arg string) error {
cmd, ok := subCommands[arg]
if ok {
context := commands.CurrentContext()
if cmd.Context()&context != 0 {
p.SubCmd = cmd
return nil
}
}
return fmt.Errorf("%s unknown sub-command", arg)
}
func (*Patch) CompleteSubNames(arg string) []string {
context := commands.CurrentContext()
options := make([]string, 0, len(subCommands))
for alias, cmd := range subCommands {
if cmd.Context()&context != 0 {
options = append(options, alias)
}
}
return commands.FilterList(options, arg, commands.QuoteSpace)
}
func (p *Patch) CompleteSubArgs(arg string) []string {
if p.SubCmd == nil {
return nil
}
// prepend arbitrary string to arg to work with sub-commands
options, _ := commands.GetCompletions(p.SubCmd, opt.LexArgs("a "+arg))
completions := make([]string, 0, len(options))
for _, o := range options {
completions = append(completions, o.Value)
}
return completions
}
func (p Patch) Execute(args []string) error {
if p.SubCmd == nil {
return errors.New("no subcommand found")
}
a := opt.QuoteArgs(args[1:]...)
return commands.ExecuteCommand(p.SubCmd, a.String())
}
+250
View File
@@ -0,0 +1,250 @@
package patch
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"os/exec"
"sort"
"strings"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/pama"
"git.sr.ht/~rjarry/aerc/lib/pama/models"
"git.sr.ht/~rjarry/aerc/lib/ui"
)
type Rebase struct {
Commit string `opt:"commit" required:"false"`
}
func init() {
register(Rebase{})
}
func (Rebase) Description() string {
return "Rebase the patch data."
}
func (Rebase) Context() commands.CommandContext {
return commands.GLOBAL
}
func (Rebase) Aliases() []string {
return []string{"rebase"}
}
func (r Rebase) Execute(args []string) error {
m := pama.New()
current, err := m.CurrentProject()
if err != nil {
return err
}
baseID := r.Commit
if baseID == "" {
baseID = current.Base.ID
}
commits, err := m.RebaseCommits(current, baseID)
if err != nil {
return err
}
if len(commits) == 0 {
err := m.SaveRebased(current, baseID, nil)
if err != nil {
return fmt.Errorf("No commits to rebase, but saving of new reference failed: %w", err)
}
app.PushStatus("No commits to rebase.", 10*time.Second)
return nil
}
rebase := newRebase(commits)
f, err := os.CreateTemp("", "aerc-patch-rebase-*")
if err != nil {
return err
}
name := f.Name()
_, err = io.Copy(f, rebase.content())
if err != nil {
return err
}
f.Close()
createWidget := func() (ui.DrawableInteractive, error) {
editorCmd, err := app.CmdFallbackSearch(config.EditorCmds(), true)
if err != nil {
return nil, err
}
editor := exec.Command("/bin/sh", "-c", editorCmd+" "+name)
term, err := app.NewTerminal(editor)
if err != nil {
return nil, err
}
term.OnClose = func(_ error) {
app.CloseDialog()
defer os.Remove(name)
defer term.Focus(false)
f, err := os.Open(name)
if err != nil {
app.PushError(fmt.Sprintf("failed to open file: %v", err))
return
}
defer f.Close()
if editor.ProcessState.ExitCode() > 0 {
app.PushError("Quitting rebase without saving.")
return
}
err = m.SaveRebased(current, baseID, rebase.parse(f))
if err != nil {
app.PushError(fmt.Sprintf("Failed to save rebased commits: %v", err))
return
}
app.PushStatus("Successfully rebased.", 10*time.Second)
}
term.Show(true)
term.Focus(true)
return term, nil
}
viewer, err := createWidget()
if err != nil {
return err
}
app.AddDialog(app.DefaultDialog(
ui.NewBox(viewer, fmt.Sprintf("Patch Rebase on %-6.6s", baseID), "",
app.SelectedAccountUiConfig(),
),
))
return nil
}
type rebase struct {
commits []models.Commit
table map[string]models.Commit
order []string
}
func newRebase(commits []models.Commit) *rebase {
return &rebase{
commits: commits,
table: make(map[string]models.Commit),
}
}
const (
header string = ""
footer string = `
# Rebase aerc's patch data. This will not affect the underlying repository in
# any way.
#
# Change the name in the first column to assign a new tag to a commit. To group
# multiple commits, use the same tag name.
#
# An 'untracked' tag indicates that aerc lost track of that commit, either due
# to a commit-hash change or because that commit was applied outside of aerc.
#
# Do not change anything else besides the tag names (first column).
#
# Do not reorder the lines. The ordering should remain as in the repository.
#
# If you remove a line or keep an 'untracked' tag, those commits will be removed
# from aerc's patch tracking.
#
`
)
func (r *rebase) content() io.Reader {
var buf bytes.Buffer
buf.WriteString(header)
for _, c := range r.commits {
tag := c.Tag
if tag == "" {
tag = models.Untracked
}
shortHash := fmt.Sprintf("%6.6s", c.ID)
buf.WriteString(
fmt.Sprintf("%-12s %6.6s %s\n", tag, shortHash, c.Info()))
r.table[shortHash] = c
r.order = append(r.order, shortHash)
}
buf.WriteString(footer)
return &buf
}
func (r *rebase) parse(reader io.Reader) []models.Commit {
var commits []models.Commit
var hashes []string
scanner := bufio.NewScanner(reader)
duplicated := make(map[string]struct{})
for scanner.Scan() {
s := scanner.Text()
i := strings.Index(s, "#")
if i >= 0 {
s = s[:i]
}
if strings.TrimSpace(s) == "" {
continue
}
fds := strings.Fields(s)
if len(fds) < 2 {
continue
}
tag, shortHash := fds[0], fds[1]
if tag == models.Untracked {
continue
}
_, dedup := duplicated[shortHash]
if dedup {
log.Warnf("rebase: skipping duplicated hash: %s", shortHash)
continue
}
hashes = append(hashes, shortHash)
c, ok := r.table[shortHash]
if !ok {
log.Errorf("Looks like the commit hashes were changed "+
"during the rebase. Dropping: %v", shortHash)
continue
}
log.Tracef("save commit %s with tag %s", shortHash, tag)
c.Tag = tag
commits = append(commits, c)
duplicated[shortHash] = struct{}{}
}
reorder(commits, hashes, r.order)
return commits
}
func reorder(toSort []models.Commit, now []string, by []string) {
byMap := make(map[string]int)
for i, s := range by {
byMap[s] = i
}
complete := true
for _, s := range now {
_, ok := byMap[s]
complete = complete && ok
}
if !complete {
return
}
sort.SliceStable(toSort, func(i, j int) bool {
return byMap[now[i]] < byMap[now[j]]
})
}
+114
View File
@@ -0,0 +1,114 @@
package patch
import (
"fmt"
"reflect"
"strings"
"testing"
"git.sr.ht/~rjarry/aerc/lib/pama/models"
)
func TestRebase_reorder(t *testing.T) {
newCommits := func(order []string) []models.Commit {
var commits []models.Commit
for _, s := range order {
commits = append(commits, models.Commit{ID: s})
}
return commits
}
tests := []struct {
name string
commits []models.Commit
now []string
by []string
want []models.Commit
}{
{
name: "nothing to reorder",
commits: newCommits([]string{"1", "2", "3"}),
now: []string{"1", "2", "3"},
by: []string{"1", "2", "3"},
want: newCommits([]string{"1", "2", "3"}),
},
{
name: "reorder",
commits: newCommits([]string{"1", "3", "2"}),
now: []string{"1", "3", "2"},
by: []string{"1", "2", "3"},
want: newCommits([]string{"1", "2", "3"}),
},
{
name: "reorder inverted",
commits: newCommits([]string{"3", "2", "1"}),
now: []string{"3", "2", "1"},
by: []string{"1", "2", "3"},
want: newCommits([]string{"1", "2", "3"}),
},
{
name: "changed hash: do not sort",
commits: newCommits([]string{"1", "6", "3"}),
now: []string{"1", "6", "3"},
by: []string{"1", "2", "3"},
want: newCommits([]string{"1", "6", "3"}),
},
}
for _, test := range tests {
reorder(test.commits, test.now, test.by)
if !reflect.DeepEqual(test.commits, test.want) {
t.Errorf("test '%s' failed to reorder: got %v but "+
"want %v", test.name, test.commits, test.want)
}
}
}
func newCommit(id, subj, tag string) models.Commit {
return models.Commit{
ID: id,
Subject: subj,
Tag: tag,
}
}
func TestRebase_parse(t *testing.T) {
input := `
# some header info
hello_v1 123 same info
hello_v1 456 same info
untracked 789 same info
hello_v2 012 diff info
untracked 345 diff info # not very useful comment
# some footer info
`
commits := []models.Commit{
newCommit("123123", "same info", "hello_v1"),
newCommit("456456", "same info", "hello_v1"),
newCommit("789789", "same info", models.Untracked),
newCommit("012012", "diff info", "hello_v2"),
newCommit("345345", "diff info", models.Untracked),
}
var order []string
for _, c := range commits {
order = append(order, fmt.Sprintf("%3.3s", c.ID))
}
table := make(map[string]models.Commit)
for i, shortId := range order {
table[shortId] = commits[i]
}
rebase := &rebase{
commits: commits,
table: table,
order: order,
}
results := rebase.parse(strings.NewReader(input))
if len(results) != 3 {
t.Errorf("failed to return correct number of commits: "+
"got %d but wanted 3", len(results))
}
}
+62
View File
@@ -0,0 +1,62 @@
package patch
import (
"fmt"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/pama"
)
type Switch struct {
Project string `opt:"project" complete:"Complete" desc:"Project name."`
}
func init() {
register(Switch{})
}
func (Switch) Description() string {
return "Switch context to the specified project."
}
func (Switch) Context() commands.CommandContext {
return commands.GLOBAL
}
func (Switch) Aliases() []string {
return []string{"switch"}
}
func (s Switch) Complete(arg string) []string {
m := pama.New()
names, err := m.Names()
if err != nil {
log.Errorf("failed to get completion: %v", err)
return nil
}
cur, err := m.CurrentProject()
if err == nil {
i := 0
for ; i < len(names); i++ {
if cur.Name == names[i] {
names = append(names[:i], names[i+1:]...)
break
}
}
}
return commands.FilterList(names, arg, nil)
}
func (s Switch) Execute(_ []string) error {
name := s.Project
err := pama.New().SwitchProject(name)
if err != nil {
return err
}
app.PushStatus(fmt.Sprintf("Project switched to '%s'", name),
10*time.Second)
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package patch
import (
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/pama"
)
type Term struct {
Cmd []string `opt:"..." required:"false"`
}
func init() {
register(Term{})
}
func (Term) Description() string {
return "Open a shell or run a command in the current project's directory."
}
func (Term) Context() commands.CommandContext {
return commands.GLOBAL
}
func (Term) Aliases() []string {
return []string{"term"}
}
func (t Term) Execute(_ []string) error {
p, err := pama.New().CurrentProject()
if err != nil {
return err
}
return commands.TermCoreDirectory(t.Cmd, p.Root)
}
+62
View File
@@ -0,0 +1,62 @@
package patch
import (
"fmt"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/pama"
)
type Unlink struct {
Tag string `opt:"tag" required:"false" complete:"Complete" desc:"Project tag name."`
}
func init() {
register(Unlink{})
}
func (Unlink) Description() string {
return "Delete all patch tracking data for the specified project."
}
func (Unlink) Context() commands.CommandContext {
return commands.GLOBAL
}
func (Unlink) Aliases() []string {
return []string{"unlink"}
}
func (*Unlink) Complete(arg string) []string {
names, err := pama.New().Names()
if err != nil {
log.Errorf("failed to get completion: %v", err)
return nil
}
return commands.FilterList(names, arg, nil)
}
func (d Unlink) Execute(args []string) error {
m := pama.New()
name := d.Tag
if name == "" {
p, err := m.CurrentProject()
if err != nil {
return err
}
name = p.Name
}
err := m.Unlink(name)
if err != nil {
return err
}
app.PushStatus(fmt.Sprintf("Project '%s' unlinked.", name),
10*time.Second)
return nil
}