mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 11:13:33 +01:00
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>
131 lines
2.8 KiB
Go
131 lines
2.8 KiB
Go
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
|
|
}
|