Something went wrong. Try again.
An alternative CLI for git-bug that simplifies working with agents on issues
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169package main
import ( "fmt" "os/exec" "strings"
"github.com/git-bug/git-bug/cache" "github.com/git-bug/git-bug/entities/identity" "github.com/git-bug/git-bug/entity" "github.com/git-bug/git-bug/repository" "github.com/spf13/cobra")
const ( bugNamespace = "bugs" identityNamespace = "identities")
// runGit runs a git command in repoPath, returning its combined output on error.func runGit(repoPath string, args ...string) error { cmd := exec.Command("git", args...) cmd.Dir = repoPath out, err := cmd.CombinedOutput() if err != nil { msg := strings.TrimSpace(string(out)) if msg != "" { return fmt.Errorf("git %s failed: %w\n%s", args[0], err, msg) } return fmt.Errorf("git %s failed: %w", args[0], err) } return nil}
// gitFetch fetches bugs and identities from remote into remote-tracking refs.//// The git binary is used rather than go-git so the user's SSH configuration,// keys and agent are honored exactly as by a normal `git fetch`. go-git's SSH// transport only supports the ssh-agent (SSH_AUTH_SOCK), which breaks setups// that rely on ssh config / key files.func gitFetch(repoPath, remote string) error { args := []string{ "fetch", remote, fmt.Sprintf("+refs/%s/*:refs/remotes/%s/%s/*", bugNamespace, remote, bugNamespace), fmt.Sprintf("+refs/%s/*:refs/remotes/%s/%s/*", identityNamespace, remote, identityNamespace), } return runGit(repoPath, args...)}
// gitPush publishes local bugs and identities to remote using the git binary.func gitPush(repoPath, remote string) error { args := []string{ "push", remote, fmt.Sprintf("+refs/%s/*:refs/%s/*", bugNamespace, bugNamespace), fmt.Sprintf("+refs/%s/*:refs/%s/*", identityNamespace, identityNamespace), } return runGit(repoPath, args...)}
// mergeRemoteRefs merges already-fetched remote bugs and identities into local// refs, returning how many entities were created and updated. A user identity// must be set first: MergeAll uses it as the merge author.func mergeRemoteRefs(repoPath, remote string) (created, updated int, err error) { repo, err := repository.OpenGoGitRepo(repoPath, "", nil) if err != nil { return 0, 0, fmt.Errorf("failed to open repository: %w", err) } defer repo.Close()
cacheInstance, err := cache.NewRepoCacheNoEvents(repo) if err != nil { return 0, 0, fmt.Errorf("failed to open bug cache: %w", err) } defer cacheInstance.Close()
for result := range cacheInstance.MergeAll(remote) { if result.Err != nil { return 0, 0, result.Err } if result.Status == entity.MergeStatusInvalid { return 0, 0, fmt.Errorf("invalid remote data: %s", result.Reason) } switch result.Status { case entity.MergeStatusNew: created++ case entity.MergeStatusUpdated: updated++ } }
return created, updated, nil}
// runSync syncs bugs and identities with a remote. If neither pull nor push is// requested, both directions are performed. Pull runs first so concurrent remote// edits are merged (not clobbered) before the local state is published.func runSync(repoPath, remote string, pull, push bool) error { if !pull && !push { pull, push = true, true }
if pull { repo, err := repository.OpenGoGitRepo(repoPath, "", nil) if err != nil { return fmt.Errorf("failed to open repository: %w", err) } _, idErr := identity.GetUserIdentity(repo) repo.Close() if idErr != nil { return fmt.Errorf("cannot pull bugs without an identity: %w\n\nRun 'bug init' first to set up identities", idErr) }
if err := gitFetch(repoPath, remote); err != nil { return err }
created, updated, err := mergeRemoteRefs(repoPath, remote) if err != nil { return fmt.Errorf("failed to merge from %s: %w", remote, err) } fmt.Printf("Pulled from %s: %d new, %d updated\n", remote, created, updated) }
if push { if err := gitPush(repoPath, remote); err != nil { return err } fmt.Printf("Pushed bugs and identities to %s\n", remote) }
invalidateGitBugCache(repoPath) return nil}
var ( syncRemoteFlag string syncPullFlag bool syncPushFlag bool)
var syncCmd = &cobra.Command{ Use: "sync", Short: "Sync bugs and identities with a remote", Long: `Sync bugs and identities with the 'origin' remote (or --remote).
By default this performs a full bidirectional sync: it first pulls (fetchingbugs and identities and merging them into the local repository), then pushesthe resulting state back to the remote. Because the pull happens first,concurrent edits are merged rather than overwritten.
Transfers use the git binary, so your normal SSH configuration and keys arehonored, and you do not need a separate 'git push' or 'jj git push':
bug sync # pull then push bug sync --pull # only fetch and merge remote changes bug sync --push # only publish local changes bug sync --remote upstream`, RunE: func(cmd *cobra.Command, args []string) error { return runSync(repoPath, syncRemoteFlag, syncPullFlag, syncPushFlag) },}
func init() { syncCmd.Flags().StringVar(&syncRemoteFlag, "remote", "origin", "remote to sync with") syncCmd.Flags().BoolVar(&syncPullFlag, "pull", false, "only pull (fetch and merge) remote changes") syncCmd.Flags().BoolVar(&syncPushFlag, "push", false, "only push local changes") rootCmd.AddCommand(syncCmd)}