feat: add --update/-u flag to pull git-backed cheatpaths (#552)

Iterates over configured cheatpaths and runs git pull on each one that
is a git repository with a clean worktree. Supports SSH remotes via key
file discovery and SSH agent fallback. Works with --path filtering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christopher Allen Lane
2026-02-15 19:40:07 -05:00
parent 417b5b4e42
commit 80e0e0d3ae
11 changed files with 535 additions and 16 deletions

130
internal/repo/pull.go Normal file
View File

@@ -0,0 +1,130 @@
package repo
import (
"errors"
"os"
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport"
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
"github.com/mitchellh/go-homedir"
)
// ErrDirtyWorktree indicates that the worktree has uncommitted changes.
var ErrDirtyWorktree = errors.New("dirty worktree")
// Pull performs a git pull on the repository at path. It returns
// ErrDirtyWorktree if the worktree has uncommitted changes, and
// git.ErrRepositoryNotExists if path is not a git repository.
func Pull(path string) error {
// open the repository
r, err := git.PlainOpen(path)
if err != nil {
return err
}
// get the worktree
wt, err := r.Worktree()
if err != nil {
return err
}
// check if the worktree is clean
status, err := wt.Status()
if err != nil {
return err
}
if !status.IsClean() {
return ErrDirtyWorktree
}
// build pull options, using SSH auth when the remote is SSH
opts := &git.PullOptions{}
if auth, err := sshAuth(r); err == nil && auth != nil {
opts.Auth = auth
}
// pull
err = wt.Pull(opts)
if errors.Is(err, git.NoErrAlreadyUpToDate) {
return nil
}
return err
}
// defaultKeyFiles are the SSH key filenames tried in order, matching the
// default behavior of OpenSSH.
var defaultKeyFiles = []string{
"id_rsa",
"id_ecdsa",
"id_ecdsa_sk",
"id_ed25519",
"id_ed25519_sk",
"id_dsa",
}
// sshAuth returns an appropriate SSH auth method if the origin remote uses
// the SSH protocol, or nil if it does not. It tries the SSH agent first, then
// falls back to default key files in ~/.ssh/.
func sshAuth(r *git.Repository) (transport.AuthMethod, error) {
remote, err := r.Remote("origin")
if err != nil {
return nil, err
}
urls := remote.Config().URLs
if len(urls) == 0 {
return nil, nil
}
ep, err := transport.NewEndpoint(urls[0])
if err != nil {
return nil, err
}
if ep.Protocol != "ssh" {
return nil, nil
}
user := ep.User
if user == "" {
user = "git"
}
// try default key files first — this is more reliable than the SSH
// agent, which may report success even when no keys are loaded
home, err := homedir.Dir()
if err == nil {
if auth := findKeyFile(filepath.Join(home, ".ssh"), user); auth != nil {
return auth, nil
}
}
// fall back to SSH agent
if auth, err := gitssh.NewSSHAgentAuth(user); err == nil {
return auth, nil
}
return nil, nil
}
// findKeyFile looks for a usable SSH private key in sshDir, trying the
// standard OpenSSH default filenames in order. Returns nil if no usable key
// is found.
func findKeyFile(sshDir, user string) transport.AuthMethod {
for _, name := range defaultKeyFiles {
keyPath := filepath.Join(sshDir, name)
if _, err := os.Stat(keyPath); err != nil {
continue
}
auth, err := gitssh.NewPublicKeysFromFile(user, keyPath, "")
if err != nil {
continue
}
return auth
}
return nil
}