← back to jmcampanini__grove-cli

Function bodies 172 total

All specs Real LLM only Function bodies
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{}, fm
git.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 HeadR
github.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 parseReviewRequestedEven
github.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
}
‹ prevpage 3 / 4next ›