init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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]]
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user