Function bodies 172 total
git.GitCli.executeMutatingGitCommandWithOutput method · go · L77-L87 (11 LOC)internal/git/git_cli.go
func (g *GitCli) executeMutatingGitCommandWithOutput(errContext string, args ...string) (string, error) {
if g.dryRun {
g.log.Info("Would execute git command", "cmd", "git", "args", args)
return fmt.Sprintf("Would execute: git %s", strings.Join(args, " ")), nil
}
output, err := g.executeGitCommand(args...)
if err != nil {
return output, fmt.Errorf("%s: %w", errContext, err)
}
return output, nil
}git.GitCli.GetMainWorktreePath method · go · L89-L110 (22 LOC)internal/git/git_cli.go
func (g *GitCli) GetMainWorktreePath() (string, error) {
commonDir, err := g.executeGitCommand("rev-parse", "--git-common-dir")
if err != nil {
return "", fmt.Errorf("failed to get git common dir: %w", err)
}
absCommonDir := commonDir
if !filepath.IsAbs(commonDir) {
absCommonDir = filepath.Join(g.workingDir, commonDir)
}
absCommonDir, err = filepath.Abs(absCommonDir)
if err != nil {
return "", fmt.Errorf("failed to get absolute path: %w", err)
}
absCommonDir = filepath.Clean(absCommonDir)
mainWorktree := filepath.Dir(absCommonDir)
g.log.Debug("Resolved main worktree path", "commonDir", commonDir, "mainWorktree", mainWorktree)
return mainWorktree, nil
}git.GitCli.GetWorkspacePath method · go · L112-L118 (7 LOC)internal/git/git_cli.go
func (g *GitCli) GetWorkspacePath() (string, error) {
mainWorktreePath, err := g.GetMainWorktreePath()
if err != nil {
return "", fmt.Errorf("failed to get main worktree path: %w", err)
}
return filepath.Dir(mainWorktreePath), nil
}git.GitCli.GetWorktreeRoot method · go · L120-L130 (11 LOC)internal/git/git_cli.go
func (g *GitCli) GetWorktreeRoot() (string, error) {
output, err := g.executeGitCommand("rev-parse", "--show-toplevel")
if err != nil {
if strings.Contains(err.Error(), "not a git repo") {
// Not in a git repo - this is a valid state, not an error
return "", nil
}
return "", fmt.Errorf("git command failed: %w", err)
}
return output, nil
}git.GitCli.GetCurrentBranch method · go · L132-L138 (7 LOC)internal/git/git_cli.go
func (g *GitCli) GetCurrentBranch() (string, error) {
output, err := g.executeGitCommand("rev-parse", "--abbrev-ref", "HEAD")
if err != nil {
return "", fmt.Errorf("failed to get current branch: %w", err)
}
return output, nil
}git.GitCli.GetCommitSubject method · go · L140-L146 (7 LOC)internal/git/git_cli.go
func (g *GitCli) GetCommitSubject() (string, error) {
output, err := g.executeGitCommand("log", "-1", "--format=%s")
if err != nil {
return "", fmt.Errorf("failed to get commit subject: %w", err)
}
return output, nil
}git.GitCli.GetDefaultRemote method · go · L148-L157 (10 LOC)internal/git/git_cli.go
func (g *GitCli) GetDefaultRemote(fallback string) (string, error) {
output, err := g.executeGitCommand("config", "--get", "remote.pushDefault")
if err == nil && output != "" {
g.log.Debug("Found remote.pushDefault", "remote", output)
return output, nil
}
g.log.Debug("No remote.pushDefault configured, using fallback", "fallback", fallback)
return fallback, nil
}Repobility · MCP-ready · https://repobility.com
git.GitCli.remoteExists method · go · L160-L171 (12 LOC)internal/git/git_cli.go
func (g *GitCli) remoteExists(remoteName string) (bool, error) {
_, err := g.executeGitCommand("remote", "get-url", remoteName)
if err == nil {
return true, nil
}
if strings.Contains(err.Error(), "No such remote") {
return false, nil
}
return false, err
}git.GitCli.GetRepoDefaultBranch method · go · L173-L191 (19 LOC)internal/git/git_cli.go
func (g *GitCli) GetRepoDefaultBranch(remoteName string) (string, error) {
if exists, err := g.remoteExists(remoteName); err != nil {
return "", fmt.Errorf("failed to check remote existence: %w", err)
} else if !exists {
return "", fmt.Errorf("remote '%s' does not exist", remoteName)
}
output, err := g.executeGitCommand("rev-parse", "--abbrev-ref", remoteName+"/HEAD")
if err != nil {
if strings.Contains(err.Error(), "unknown revision") {
g.log.Debug("Remote HEAD not configured", "remoteName", remoteName)
return "", nil
}
return "", fmt.Errorf("failed to get remote HEAD: %w", err)
}
branchName := strings.TrimPrefix(output, remoteName+"/")
return branchName, nil
}git.GitCli.ListLocalBranches method · go · L193-L214 (22 LOC)internal/git/git_cli.go
func (g *GitCli) ListLocalBranches() ([]LocalBranch, error) {
format := `branch %(refname:short)
checkedOut %(if)%(HEAD)%(then)true%(else)false%(end)
commit %(objectname:short)
%(if)%(upstream:short)%(then)upstream %(upstream:short)
%(end)%(if)%(upstream:track)%(then)track %(upstream:track)
%(end)committedOn %(committerdate:iso-strict)
committedBy %(committername)
subject %(contents:subject)
worktreepath %(worktreepath)
`
output, err := g.executeGitCommand("for-each-ref", "--format="+format, "refs/heads/")
if err != nil {
return nil, fmt.Errorf("failed to list branches: %w", err)
}
if output == "" {
return []LocalBranch{}, nil
}
return parseBranchesFromFormat(output), nil
}git.parseBranchesFromFormat function · go · L218-L230 (13 LOC)internal/git/git_cli.go
func parseBranchesFromFormat(output string) []LocalBranch {
blocks := splitIntoBlocks(output)
branches := make([]LocalBranch, 0, len(blocks))
for _, block := range blocks {
branch := parseBranchBlock(block)
if branch.Name != "" {
branches = append(branches, branch)
}
}
return branches
}git.splitIntoBlocks function · go · L234-L257 (24 LOC)internal/git/git_cli.go
func splitIntoBlocks(output string) [][]string {
var blocks [][]string
var currentBlock []string
for _, line := range strings.Split(output, "\n") {
if line != "" {
currentBlock = append(currentBlock, line)
continue
}
// found blank line, new block
if len(currentBlock) > 0 {
blocks = append(blocks, currentBlock)
currentBlock = nil
}
}
// handle last block if output doesn't end with blank line
if len(currentBlock) > 0 {
blocks = append(blocks, currentBlock)
}
return blocks
}git.parseBranchBlock function · go · L259-L274 (16 LOC)internal/git/git_cli.go
func parseBranchBlock(lines []string) LocalBranch {
fields := parseLineFields(lines)
name := fields["branch"]
isCheckedOut := fields["checkedOut"] == "true"
sha := fields["commit"]
upstreamName := fields["upstream"]
ahead, behind := parseTrackInfo(fields["track"])
committedOn := parseISO8601Date(fields["committedOn"])
committedBy := fields["committedBy"]
subject := fields["subject"]
worktreeAbsolutePath := fields["worktreepath"]
commit := NewCommit(sha, subject, committedOn, committedBy)
return NewLocalBranch(name, upstreamName, worktreeAbsolutePath, isCheckedOut, ahead, behind, commit)
}git.parseTrackInfo function · go · L277-L291 (15 LOC)internal/git/git_cli.go
func parseTrackInfo(track string) (ahead, behind int) {
if track == "" || track == "[gone]" {
return 0, 0
}
if n, _ := fmt.Sscanf(track, "[ahead %d, behind %d]", &ahead, &behind); n == 2 {
return ahead, behind
}
if n, _ := fmt.Sscanf(track, "[ahead %d]", &ahead); n == 1 {
return ahead, 0
}
if n, _ := fmt.Sscanf(track, "[behind %d]", &behind); n == 1 {
return 0, behind
}
return 0, 0
}git.GitCli.getCommitBySHA method · go · L295-L319 (25 LOC)internal/git/git_cli.go
func (g *GitCli) getCommitBySHA(sha string) (Commit, error) {
// Format: subject<NUL>committer date ISO<NUL>committer name
// NUL separators handle multi-line subjects more robustly than line-based parsing
format := "%s%x00%cI%x00%cn"
output, err := g.executeGitCommand("log", "-1", "--format="+format, sha)
if err != nil {
return Commit{}, fmt.Errorf("failed to get commit info for %s: %w", sha, err)
}
parts := strings.Split(output, "\x00")
if len(parts) != 3 {
return Commit{}, fmt.Errorf("unexpected commit format for %s: got %d parts", sha, len(parts))
}
subject := parts[0]
committedOn := time.Time{}
if parsed, err := time.Parse(time.RFC3339, parts[1]); err == nil {
committedOn = parsed
} else {
g.log.Debug("failed to parse commit date", "sha", sha, "date", parts[1], "error", err)
}
committedBy := parts[2]
return NewCommit(sha, subject, committedOn, committedBy), nil
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
git.GitCli.ListRemoteBranches method · go · L321-L338 (18 LOC)internal/git/git_cli.go
func (g *GitCli) ListRemoteBranches(remoteName string) ([]RemoteBranch, error) {
format := `ref %(refname:short)
commit %(objectname:short)
committedOn %(committerdate:iso-strict)
committedBy %(committername)
subject %(contents:subject)
`
output, err := g.executeGitCommand("for-each-ref", "--format="+format, "refs/remotes/"+remoteName+"/")
if err != nil {
return nil, fmt.Errorf("failed to list remote branches: %w", err)
}
if output == "" {
return []RemoteBranch{}, nil
}
return parseRemoteBranchesFromFormat(output), nil
}git.parseRemoteBranchesFromFormat function · go · L340-L353 (14 LOC)internal/git/git_cli.go
func parseRemoteBranchesFromFormat(output string) []RemoteBranch {
blocks := splitIntoBlocks(output)
branches := make([]RemoteBranch, 0, len(blocks))
for _, block := range blocks {
branch := parseRemoteBranchBlock(block)
// Skip symbolic HEAD ref (e.g., origin/HEAD -> origin/main)
if branch.Name != "" && branch.Name != "HEAD" {
branches = append(branches, branch)
}
}
return branches
}git.parseRemoteBranchBlock function · go · L355-L374 (20 LOC)internal/git/git_cli.go
func parseRemoteBranchBlock(lines []string) RemoteBranch {
fields := parseLineFields(lines)
// ref is "origin/main", split on first "/" to get remote and branch name
var name, remoteName string
if ref := fields["ref"]; ref != "" {
if idx := strings.Index(ref, "/"); idx != -1 {
remoteName = ref[:idx]
name = ref[idx+1:]
}
}
sha := fields["commit"]
committedOn := parseISO8601Date(fields["committedOn"])
committedBy := fields["committedBy"]
subject := fields["subject"]
commit := NewCommit(sha, subject, committedOn, committedBy)
return NewRemoteBranch(name, remoteName, commit)
}git.GitCli.ListRemotes method · go · L376-L394 (19 LOC)internal/git/git_cli.go
func (g *GitCli) ListRemotes() ([]string, error) {
output, err := g.executeGitCommand("remote")
if err != nil {
return nil, fmt.Errorf("failed to list remotes: %w", err)
}
if output == "" {
return []string{}, nil
}
lines := strings.Split(output, "\n")
remotes := make([]string, 0, len(lines))
for _, line := range lines {
if trimmed := strings.TrimSpace(line); trimmed != "" {
remotes = append(remotes, trimmed)
}
}
return remotes, nil
}git.GitCli.SyncTags method · go · L396-L408 (13 LOC)internal/git/git_cli.go
func (g *GitCli) SyncTags(remoteName string) error {
if remoteName == "" {
var err error
remoteName, err = g.GetDefaultRemote("origin")
if err != nil {
return fmt.Errorf("failed to get default remote: %w", err)
}
}
g.log.Info("Syncing tags from remote", "remote", remoteName)
args := []string{"fetch", remoteName, "--prune", "--prune-tags", "--tags"}
return g.executeMutatingGitCommand("failed to sync tags from remote", args...)
}git.GitCli.ListTags method · go · L410-L434 (25 LOC)internal/git/git_cli.go
func (g *GitCli) ListTags() ([]Tag, error) {
format := `name %(refname:short)
objecttype %(objecttype)
objectsha %(objectname)
derefsha %(*objectname)
taggername %(taggername)
taggeremail %(taggeremail)
taggedon %(taggerdate:iso-strict)
message %(contents:subject)
committedby %(*committername)
committedon %(*committerdate:iso-strict)
committerdate %(committerdate:iso-strict)
commitsubject %(*subject)
`
output, err := g.executeGitCommand("for-each-ref", "--format="+format, "refs/tags/")
if err != nil {
return nil, fmt.Errorf("failed to list tags: %w", err)
}
if output == "" {
return []Tag{}, nil
}
return parseTagsFromFormat(output), nil
}git.parseISO8601Date function · go · L437-L442 (6 LOC)internal/git/git_cli.go
func parseISO8601Date(dateStr string) time.Time {
if parsed, err := time.Parse(time.RFC3339, dateStr); err == nil {
return parsed
}
return time.Time{}
}git.parseTagsFromFormat function · go · L444-L456 (13 LOC)internal/git/git_cli.go
func parseTagsFromFormat(output string) []Tag {
blocks := splitIntoBlocks(output)
tags := make([]Tag, 0, len(blocks))
for _, block := range blocks {
tag := parseTagBlock(block)
if tag.Name != "" {
tags = append(tags, tag)
}
}
return tags
}Repobility — the code-quality scanner for AI-generated software · https://repobility.com
git.parseLineFields function · go · L460-L470 (11 LOC)internal/git/git_cli.go
func parseLineFields(lines []string) map[string]string {
fields := make(map[string]string, len(lines))
for _, line := range lines {
if idx := strings.Index(line, " "); idx != -1 {
fields[line[:idx]] = line[idx+1:]
} else if line != "" {
fields[line] = ""
}
}
return fields
}git.parseTagBlock function · go · L472-L516 (45 LOC)internal/git/git_cli.go
func parseTagBlock(lines []string) Tag {
fields := parseLineFields(lines)
name := fields["name"]
objectType := fields["objecttype"]
objectSHA := fields["objectsha"]
derefSHA := fields["derefsha"]
taggerName := fields["taggername"]
taggerEmail := fields["taggeremail"]
taggedOn := parseISO8601Date(fields["taggedon"])
message := fields["message"]
committedBy := fields["committedby"]
committedOn := parseISO8601Date(fields["committedon"])
committerDate := parseISO8601Date(fields["committerdate"])
commitSubject := fields["commitsubject"]
// Fallback: use committerDate if committedOn is zero (lightweight tags)
if committedOn.IsZero() && !committerDate.IsZero() {
committedOn = committerDate
}
// Determine the actual commit SHA and metadata
// For lightweight tags (objecttype=commit): objectSHA IS the commit
// For annotated tags (objecttype=tag): derefSHA is the dereferenced commit SHA
var actualCommitSHA string
var actualMessage string
if objectType == "commit" {
git.GitCli.BranchExists method · go · L518-L537 (20 LOC)internal/git/git_cli.go
func (g *GitCli) BranchExists(branchName string, caseInsensitive bool) (bool, error) {
branches, err := g.ListLocalBranches()
if err != nil {
return false, err
}
for _, branch := range branches {
if caseInsensitive {
if strings.EqualFold(branch.Name, branchName) {
return true, nil
}
} else {
if branch.Name == branchName {
return true, nil
}
}
}
return false, nil
}git.GitCli.ListWorktrees method · go · L539-L566 (28 LOC)internal/git/git_cli.go
func (g *GitCli) ListWorktrees() ([]Worktree, error) {
branches, err := g.ListLocalBranches()
if err != nil {
return nil, fmt.Errorf("failed to list branches for worktree lookup: %w", err)
}
tags, err := g.ListTags()
if err != nil {
return nil, fmt.Errorf("failed to list tags for worktree lookup: %w", err)
}
branchMap := make(map[string]LocalBranch, len(branches))
for _, b := range branches {
branchMap[b.Name] = b
}
tagMap := make(map[string]Tag, len(tags))
for _, t := range tags {
tagMap[t.Commit().SHA] = t
}
output, err := g.executeGitCommand("worktree", "list", "--porcelain")
if err != nil {
return nil, fmt.Errorf("failed to list worktrees: %w", err)
}
return g.parseWorktreesFromPorcelain(output, branchMap, tagMap)
}git.GitCli.parseWorktreesFromPorcelain method · go · L569-L584 (16 LOC)internal/git/git_cli.go
func (g *GitCli) parseWorktreesFromPorcelain(output string, branchMap map[string]LocalBranch, tagMap map[string]Tag) ([]Worktree, error) {
blocks := splitIntoBlocks(output)
worktrees := make([]Worktree, 0, len(blocks))
for _, block := range blocks {
worktree, err := g.parseWorktreeBlock(block, branchMap, tagMap)
if err != nil {
return nil, err
}
if worktree.AbsolutePath != "" {
worktrees = append(worktrees, worktree)
}
}
return worktrees, nil
}git.GitCli.parseWorktreeBlock method · go · L586-L622 (37 LOC)internal/git/git_cli.go
func (g *GitCli) parseWorktreeBlock(lines []string, branchMap map[string]LocalBranch, tagMap map[string]Tag) (Worktree, error) {
fields := parseLineFields(lines)
absolutePath := fields["worktree"]
sha := fields["HEAD"]
// Bare worktrees don't have a ref, skip
if _, isBare := fields["bare"]; isBare {
return Worktree{AbsolutePath: absolutePath}, nil
}
branchName := strings.TrimPrefix(fields["branch"], "refs/heads/")
_, detached := fields["detached"]
worktree := Worktree{AbsolutePath: absolutePath}
if branchName != "" {
branch, ok := branchMap[branchName]
if !ok {
return Worktree{}, fmt.Errorf("worktree branch '%s' at sha '%s' not found in local map", branchName, sha)
}
worktree.Ref = &branch
} else if detached {
if tag, ok := tagMap[sha]; ok {
worktree.Ref = &tag
} else {
// TODO: figure out a way to get the commit information without needing a reference to g GitCli
commit, err := g.getCommitBySHA(sha)
if err != nil {
return Worktree{}, fmgit.GitCli.CreateWorktreeForNewBranchFromRef method · go · L630-L637 (8 LOC)internal/git/git_cli.go
func (g *GitCli) CreateWorktreeForNewBranchFromRef(newBranchName, worktreeAbsPath, baseRef string) error {
g.log.Info("Creating worktree for new branch from ref", "branch", newBranchName, "path", worktreeAbsPath, "baseRef", baseRef)
args := []string{"worktree", "add", "-b", newBranchName, worktreeAbsPath}
if baseRef != "" {
args = append(args, baseRef)
}
return g.executeMutatingGitCommand("failed to create worktree for new branch from ref", args...)
}git.GitCli.FetchRemoteBranch method · go · L645-L650 (6 LOC)internal/git/git_cli.go
func (g *GitCli) FetchRemoteBranch(remote, remoteRef, localRef string) error {
g.log.Info("Fetching remote branch", "remote", remote, "remoteRef", remoteRef, "localRef", localRef)
refSpec := remoteRef + ":" + localRef
args := []string{"fetch", remote, refSpec}
return g.executeMutatingGitCommand("failed to fetch remote branch", args...)
}Repobility · code-quality intelligence · https://repobility.com
git.GitCli.DeleteBranch method · go · L658-L665 (8 LOC)internal/git/git_cli.go
func (g *GitCli) DeleteBranch(name string, force bool) error {
g.log.Info("Deleting branch", "branch", name, "force", force)
flag := "-d"
if force {
flag = "-D"
}
return g.executeMutatingGitCommand("failed to delete branch", "branch", flag, name)
}git.GitCli.IsWorktreeDirty method · go · L667-L674 (8 LOC)internal/git/git_cli.go
func (g *GitCli) IsWorktreeDirty(absPath string) (bool, error) {
g.log.Debug("Checking worktree dirty state", "path", absPath)
output, err := g.executeGitCommand("-C", absPath, "status", "--porcelain")
if err != nil {
return false, fmt.Errorf("failed to check worktree dirty state: %w", err)
}
return output != "", nil
}git.GitCli.RemoveWorktree method · go · L681-L689 (9 LOC)internal/git/git_cli.go
func (g *GitCli) RemoveWorktree(absPath string, force bool) error {
g.log.Info("Removing worktree", "path", absPath, "force", force)
args := []string{"worktree", "remove"}
if force {
args = append(args, "--force")
}
args = append(args, absPath)
return g.executeMutatingGitCommand("failed to remove worktree", args...)
}git.NewCommit function · go · L34-L41 (8 LOC)internal/git/git.go
func NewCommit(sha, subject string, committedOn time.Time, committedBy string) Commit {
return Commit{
CommittedBy: committedBy,
CommittedOn: committedOn,
SHA: sha,
Subject: subject,
}
}git.NewTag function · go · L58-L67 (10 LOC)internal/git/git.go
func NewTag(name string, commit Commit, message, taggerName, taggerEmail string, taggedOn time.Time) Tag {
return Tag{
commit: commit,
Message: message,
Name: name,
TaggedOn: taggedOn,
TaggerEmail: taggerEmail,
TaggerName: taggerName,
}
}git.Tag.Date method · go · L75-L80 (6 LOC)internal/git/git.go
func (t Tag) Date() time.Time {
if !t.TaggedOn.IsZero() {
return t.TaggedOn
}
return t.commit.CommittedOn
}git.NewLocalBranch function · go · L99-L109 (11 LOC)internal/git/git.go
func NewLocalBranch(name, upstreamName, worktreeAbsolutePath string, isCheckedOut bool, ahead, behind int, commit Commit) LocalBranch {
return LocalBranch{
Ahead: ahead,
Behind: behind,
IsCheckedOut: isCheckedOut,
Name: name,
UpstreamName: upstreamName,
WorktreeAbsolutePath: worktreeAbsolutePath,
commit: commit,
}
}git.NewRemoteBranch function · go · L122-L128 (7 LOC)internal/git/git.go
func NewRemoteBranch(name, remoteName string, commit Commit) RemoteBranch {
return RemoteBranch{
commit: commit,
Name: name,
RemoteName: remoteName,
}
}Repobility · MCP-ready · https://repobility.com
github.New function · go · L31-L38 (8 LOC)internal/github/github_cli.go
func New(workingDir string, timeout time.Duration, c *cache.Cache) GitHub {
return &GitHubCli{
cache: c,
log: clog.Default().WithPrefix("github"),
timeout: timeout,
workingDir: workingDir,
}
}github.GitHubCli.executeGhCommand method · go · L41-L59 (19 LOC)internal/github/github_cli.go
func (g *GitHubCli) executeGhCommand(args ...string) (string, error) {
if g.cache == nil {
return g.runGhProcess(args...)
}
cacheKey := cache.BuildKey(g.workingDir, args)
if payload, ok := g.cache.Get(cacheKey); ok {
g.log.Debug("gh command cache hit", "args", args)
return payload, nil
}
output, err := g.runGhProcess(args...)
if err != nil {
return "", err
}
g.cache.Set(cacheKey, output)
return output, nil
}github.GitHubCli.runGhProcess method · go · L61-L87 (27 LOC)internal/github/github_cli.go
func (g *GitHubCli) runGhProcess(args ...string) (string, error) {
g.log.Debug("Executing gh command", "cmd", "gh", "args", args, "workingDir", g.workingDir)
ctx, cancel := context.WithTimeout(context.Background(), g.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "gh", args...)
cmd.Dir = g.workingDir
cmd.Env = append(os.Environ(), "GH_PROMPT_DISABLED=1")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
g.log.Warn("gh command timed out", "args", args, "timeout", g.timeout, "error", err)
return "", fmt.Errorf("gh %s timed out after %s", strings.Join(args, " "), g.timeout)
}
g.log.Warn("gh command failed", "args", args, "stderr", stderr.String(), "error", err)
return "", fmt.Errorf("gh %s failed: %w: %s", strings.Join(args, " "), err, stderr.String())
}
output := strings.TrimSpace(stdout.String())
g.log.Debug("gh command succeeded", "args", args, "github.GitHubCli.GetPullRequest method · go · L93-L110 (18 LOC)internal/github/github_cli.go
func (g *GitHubCli) GetPullRequest(prNum int) (PullRequest, error) {
args := []string{
"pr", "view", fmt.Sprintf("%d", prNum),
"--json", prJsonFields + ",files",
}
output, err := g.executeGhCommand(args...)
if err != nil {
return PullRequest{}, fmt.Errorf("failed to get pull request #%d: %w", prNum, err)
}
var pr PullRequest
if err := json.Unmarshal([]byte(output), &pr); err != nil {
return PullRequest{}, fmt.Errorf("failed to parse pull request #%d: %w", prNum, err)
}
return pr, nil
}github.GitHubCli.GetPullRequestActivity method · go · L112-L204 (93 LOC)internal/github/github_cli.go
func (g *GitHubCli) GetPullRequestActivity(owner, repo string, prNum int) ([]ReviewThread, []TimelineEvent, error) {
query := fmt.Sprintf(`{
repository(owner: %q, name: %q) {
pullRequest(number: %d) {
reviewThreads(first: 100) {
nodes {
isResolved
path
comments {
totalCount
}
}
}
timelineItems(first: 100, itemTypes: [
PULL_REQUEST_REVIEW,
ISSUE_COMMENT,
HEAD_REF_FORCE_PUSHED_EVENT,
PULL_REQUEST_COMMIT,
LABELED_EVENT,
MERGED_EVENT,
CLOSED_EVENT,
REOPENED_EVENT,
READY_FOR_REVIEW_EVENT,
CONVERT_TO_DRAFT_EVENT,
REVIEW_REQUESTED_EVENT
]) {
nodes {
__typename
... on PullRequestReview {
author { login }
createdAt
state
}
... on IssueComment {
author { login }
createdAt
}
... on HeadRgithub.GitHubCli.GetPullRequestByBranch method · go · L206-L230 (25 LOC)internal/github/github_cli.go
func (g *GitHubCli) GetPullRequestByBranch(branchName string) (*PullRequest, error) {
args := []string{
"pr", "list",
"--head", branchName,
"--state", "all",
"--json", prJsonFields,
"--limit", "1",
}
output, err := g.executeGhCommand(args...)
if err != nil {
return nil, fmt.Errorf("failed to get pull request for branch %s: %w", branchName, err)
}
var prs []PullRequest
if err := json.Unmarshal([]byte(output), &prs); err != nil {
return nil, fmt.Errorf("failed to parse pull requests for branch %s: %w", branchName, err)
}
if len(prs) == 0 {
return nil, nil
}
return &prs[0], nil
}github.GitHubCli.ListPullRequests method · go · L232-L253 (22 LOC)internal/github/github_cli.go
func (g *GitHubCli) ListPullRequests(query PRQuery, limit int) ([]PullRequest, error) {
searchQuery := query.ToSearchQuery()
args := []string{
"pr", "list",
"--search", searchQuery,
"--json", prJsonFields,
"--limit", fmt.Sprintf("%d", limit),
}
output, err := g.executeGhCommand(args...)
if err != nil {
return nil, fmt.Errorf("failed to list pull requests: %w", err)
}
var prs []PullRequest
if err := json.Unmarshal([]byte(output), &prs); err != nil {
return nil, fmt.Errorf("failed to parse pull requests: %w", err)
}
return prs, nil
}github.parseActivityResponse function · go · L263-L298 (36 LOC)internal/github/github_cli.go
func parseActivityResponse(data string) ([]ReviewThread, []TimelineEvent, error) {
var result struct {
Data struct {
Repository struct {
PullRequest struct {
ReviewThreads struct {
Nodes []graphQLThreadNode `json:"nodes"`
} `json:"reviewThreads"`
TimelineItems struct {
Nodes []json.RawMessage `json:"nodes"`
} `json:"timelineItems"`
} `json:"pullRequest"`
} `json:"repository"`
} `json:"data"`
}
if err := json.Unmarshal([]byte(data), &result); err != nil {
return nil, nil, fmt.Errorf("failed to parse activity response: %w", err)
}
threads := aggregateThreadsByPath(result.Data.Repository.PullRequest.ReviewThreads.Nodes)
var events []TimelineEvent
for _, raw := range result.Data.Repository.PullRequest.TimelineItems.Nodes {
event, err := parseTimelineNode(raw)
if err != nil {
fmt.Fprintf(os.Stderr, "warning: skipping timeline event: %v\n", err)
continue
}
if event != nil {
events = append(events, *event)
}
}
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
github.aggregateThreadsByPath function · go · L300-L322 (23 LOC)internal/github/github_cli.go
func aggregateThreadsByPath(nodes []graphQLThreadNode) []ReviewThread {
byPath := make(map[string]*ReviewThread, len(nodes))
for _, node := range nodes {
if rt, ok := byPath[node.Path]; ok {
rt.CommentCount += node.Comments.TotalCount
rt.IsResolved = rt.IsResolved && node.IsResolved
} else {
byPath[node.Path] = &ReviewThread{
CommentCount: node.Comments.TotalCount,
IsResolved: node.IsResolved,
Path: node.Path,
}
}
}
threads := make([]ReviewThread, 0, len(byPath))
for _, rt := range byPath {
threads = append(threads, *rt)
}
slices.SortFunc(threads, func(a, b ReviewThread) int {
return strings.Compare(a.Path, b.Path)
})
return threads
}github.parseTimelineNode function · go · L324-L358 (35 LOC)internal/github/github_cli.go
func parseTimelineNode(raw json.RawMessage) (*TimelineEvent, error) {
var base struct {
TypeName string `json:"__typename"`
}
if err := json.Unmarshal(raw, &base); err != nil {
return nil, err
}
switch base.TypeName {
case "PullRequestReview":
return parseReviewEvent(raw)
case "IssueComment":
return parseCommentEvent(raw)
case "HeadRefForcePushedEvent":
return parseActorEvent(raw, TimelineEventForcePushed)
case "PullRequestCommit":
return parseCommitEvent(raw)
case "LabeledEvent":
return parseLabeledEvent(raw)
case "MergedEvent":
return parseActorEvent(raw, TimelineEventMerged)
case "ClosedEvent":
return parseActorEvent(raw, TimelineEventClosed)
case "ReopenedEvent":
return parseActorEvent(raw, TimelineEventReopened)
case "ReadyForReviewEvent":
return parseActorEvent(raw, TimelineEventReadyForReview)
case "ConvertToDraftEvent":
return parseActorEvent(raw, TimelineEventConvertToDraft)
case "ReviewRequestedEvent":
return parseReviewRequestedEvengithub.parseReviewEvent function · go · L360-L375 (16 LOC)internal/github/github_cli.go
func parseReviewEvent(raw json.RawMessage) (*TimelineEvent, error) {
var v struct {
Author struct{ Login string } `json:"author"`
CreatedAt time.Time `json:"createdAt"`
State string `json:"state"`
}
if err := json.Unmarshal(raw, &v); err != nil {
return nil, err
}
return &TimelineEvent{
Actor: v.Author.Login,
CreatedAt: v.CreatedAt,
Details: strings.ToLower(strings.ReplaceAll(v.State, "_", " ")),
Type: TimelineEventReviewed,
}, nil
}