← back to jmcampanini__grove-cli

Function bodies 172 total

All specs Real LLM only Function bodies
cmd.renderFileList function · go · L540-L569 (30 LOC)
cmd/pr_preview_render.go
func renderFileList(files []github.PullRequestFile, fileComments map[string]int, totalChanged int, haPaths map[string]bool, prURL string, contentWidth int) string {
	var displayFiles []github.PullRequestFile
	for _, f := range files {
		if haPaths[f.Path] {
			continue
		}
		if len(displayFiles) >= maxPreviewFiles {
			break
		}
		displayFiles = append(displayFiles, f)
	}
	cw := computeFileColumnWidths(displayFiles, fileComments)

	header := sectionHeader(fmt.Sprintf("Files (%d)", totalChanged))
	var sb strings.Builder
	sb.WriteString(header)
	sb.WriteString("\n")

	for _, f := range displayFiles {
		sb.WriteString(formatFileEntry(f, fileComments[f.Path], prURL, contentWidth, cw))
		sb.WriteString("\n")
	}

	remaining := totalChanged - len(displayFiles) - len(haPaths)
	if remaining > 0 {
		fmt.Fprintf(&sb, "  … and %d more\n", remaining)
	}

	return strings.TrimRight(sb.String(), "\n")
}
cmd.renderBody function · go · L571-L597 (27 LOC)
cmd/pr_preview_render.go
func renderBody(body string, width int, colorMode string) string {
	if body == "" {
		return ""
	}
	opts := []glamour.TermRendererOption{
		glamour.WithAutoStyle(),
		glamour.WithWordWrap(max(width-4, 20)),
	}
	switch colorMode {
	case "always":
		opts = append(opts, glamour.WithColorProfile(termenv.ANSI256))
	case "never":
		opts = append(opts, glamour.WithColorProfile(termenv.Ascii))
	}
	// TODO: replace with structured debug logging when a logging framework is added
	renderer, err := glamour.NewTermRenderer(opts...)
	if err != nil {
		fmt.Fprintf(os.Stderr, "warning: markdown renderer init failed, using plain text: %v\n", err)
		return wrapBody(body, width)
	}
	rendered, err := renderer.Render(body)
	if err != nil {
		fmt.Fprintf(os.Stderr, "warning: markdown rendering failed, using plain text: %v\n", err)
		return wrapBody(body, width)
	}
	return strings.TrimSpace(rendered)
}
cmd.collapseTimeline function · go · L610-L623 (14 LOC)
cmd/pr_preview_render.go
func collapseTimeline(events []github.TimelineEvent) []collapsedEvent {
	var collapsed []collapsedEvent
	for _, e := range events {
		n := len(collapsed)
		if n > 0 && isConsecutiveCommit(collapsed[n-1], e) {
			collapsed[n-1].count++
			collapsed[n-1].event.CreatedAt = e.CreatedAt
			collapsed[n-1].event.Details = e.Details
		} else {
			collapsed = append(collapsed, collapsedEvent{count: 1, event: e})
		}
	}
	return collapsed
}
cmd.renderTimeline function · go · L625-L673 (49 LOC)
cmd/pr_preview_render.go
func renderTimeline(pr github.PullRequest, timeline []github.TimelineEvent) string {
	var events []github.TimelineEvent

	if !pr.CreatedAt.IsZero() {
		events = append(events, github.TimelineEvent{
			Actor:     pr.AuthorLogin,
			CreatedAt: pr.CreatedAt,
			Type:      github.TimelineEventOpened,
		})
	}
	events = append(events, timeline...)

	slices.SortFunc(events, func(a, b github.TimelineEvent) int {
		return a.CreatedAt.Compare(b.CreatedAt)
	})

	collapsed := collapseTimeline(events)
	if len(collapsed) == 0 {
		return ""
	}

	header := sectionHeader("Activity")
	timeStyle := lipgloss.NewStyle().Foreground(colorGray).Width(16).Align(lipgloss.Right)
	lineStyle := lipgloss.NewStyle().Foreground(colorPurple)

	var sb strings.Builder
	sb.WriteString(header)
	sb.WriteString("\n")
	for i, ce := range collapsed {
		e := ce.event
		var icon, msg string
		switch {
		case e.Type == github.TimelineEventOpened:
			icon = colorIcon(iconOpened, colorGreen)
			msg = fmt.Sprintf("@%s opened this P
cmd.renderPreview function · go · L675-L716 (42 LOC)
cmd/pr_preview_render.go
func renderPreview(w io.Writer, pr github.PullRequest, fileComments map[string]int, timeline []github.TimelineEvent, width int, colorMode string) error {
	border := lipgloss.RoundedBorder()
	boxStyle := lipgloss.NewStyle().
		Border(border).
		BorderForeground(colorPurple).
		Width(width-2).
		Padding(0, 1)

	var sections []string

	sections = append(sections, boxStyle.Render(renderHeader(pr)))

	if checksContent := renderChecks(pr); checksContent != "" {
		sections = append(sections, boxStyle.Render(checksContent))
	}

	scored := scoreFiles(pr.Files, fileComments)
	contentWidth := width - 4

	highActivity, shownFiles := renderHighActivity(scored, pr.URL, contentWidth)
	if highActivity != "" {
		sections = append(sections, boxStyle.Render(highActivity))
	}

	haPaths := make(map[string]bool, len(shownFiles))
	for _, sf := range shownFiles {
		haPaths[sf.file.Path] = true
	}

	sections = append(sections, boxStyle.Render(renderFileList(pr.Files, fileComments, pr.FilesChanged, haPaths, pr.
cmd.runPrune function · go · L49-L67 (19 LOC)
cmd/prune.go
func runPrune(cmd *cobra.Command, _ []string) error {
	rt, err := loadCommandRuntime()
	if err != nil {
		return err
	}

	ghClient, err := rt.newCachedGitHubClient()
	if err != nil {
		return fmt.Errorf("failed to create GitHub client: %w", err)
	}

	ctx := &statusContext{
		ghClient:         ghClient,
		gitClient:        git.New(false, rt.mainWorktreePath, rt.cfg.Git.Timeout),
		mainWorktreePath: rt.mainWorktreePath,
	}

	return executePrune(cmd.OutOrStdout(), ctx)
}
cmd.executePrune function · go · L69-L111 (43 LOC)
cmd/prune.go
func executePrune(w io.Writer, ctx *statusContext) error {
	statuses, err := gatherStatuses(ctx)
	if err != nil {
		return err
	}

	remoteBranches, err := buildRemoteBranchSet(ctx.gitClient)
	if err != nil {
		return err
	}

	prunables := findPrunable(statuses, remoteBranches)
	if len(prunables) == 0 {
		_, err := fmt.Fprintln(w, "Nothing to prune.")
		return err
	}

	selected, err := promptPruneSelection(prunables)
	if err != nil {
		if errors.Is(err, huh.ErrUserAborted) {
			return nil
		}
		return err
	}
	if len(selected) == 0 {
		_, err := fmt.Fprintln(w, "No worktrees selected.")
		return err
	}

	confirmed, err := promptPruneConfirm(len(selected))
	if err != nil {
		if errors.Is(err, huh.ErrUserAborted) {
			return nil
		}
		return err
	}
	if !confirmed {
		_, err := fmt.Fprintln(w, "Aborted.")
		return err
	}

	return executeRemovals(w, ctx.gitClient, selected)
}
Open data scored by Repobility · https://repobility.com
cmd.buildRemoteBranchSet function · go · L113-L130 (18 LOC)
cmd/prune.go
func buildRemoteBranchSet(gitClient git.Git) (map[string]bool, error) {
	remotes, err := gitClient.ListRemotes()
	if err != nil {
		return nil, fmt.Errorf("failed to list remotes: %w", err)
	}

	remoteBranches := make(map[string]bool)
	for _, remote := range remotes {
		branches, err := gitClient.ListRemoteBranches(remote)
		if err != nil {
			return nil, fmt.Errorf("failed to list branches for remote %q: %w", remote, err)
		}
		for _, b := range branches {
			remoteBranches[remote+"/"+b.Name] = true
		}
	}
	return remoteBranches, nil
}
cmd.findPrunable function · go · L132-L149 (18 LOC)
cmd/prune.go
func findPrunable(statuses []worktreeStatus, remoteBranches map[string]bool) []prunable {
	var result []prunable
	for _, ws := range statuses {
		if ws.isMain {
			continue
		}

		if reason := pruneReason(ws, remoteBranches); reason != "" {
			result = append(result, prunable{
				branchName: ws.branchName,
				name:       filepath.Base(ws.absPath),
				path:       ws.absPath,
				reason:     reason,
			})
		}
	}
	return result
}
cmd.pruneReason function · go · L151-L170 (20 LOC)
cmd/prune.go
func pruneReason(ws worktreeStatus, remoteBranches map[string]bool) string {
	if _, err := os.Stat(ws.absPath); os.IsNotExist(err) {
		return "orphaned"
	}

	if ws.pr != nil {
		switch ws.pr.State {
		case github.PRStateMerged:
			return fmt.Sprintf("PR #%d merged", ws.pr.Number)
		case github.PRStateClosed:
			return fmt.Sprintf("PR #%d closed", ws.pr.Number)
		}
	}

	if ws.tracking.upstream != "" && !remoteBranches[ws.tracking.upstream] {
		return "upstream gone"
	}

	return ""
}
cmd.promptPruneSelection function · go · L178-L206 (29 LOC)
cmd/prune.go
func promptPruneSelection(prunables []prunable) ([]prunable, error) {
	options := make([]huh.Option[int], len(prunables))
	for i, p := range prunables {
		label := fmt.Sprintf("%s  %s",
			p.name,
			lipgloss.NewStyle().Foreground(colorGray).Render("("+p.reason+")"),
		)
		options[i] = huh.NewOption(label, i).Selected(true)
	}

	var selected []int
	err := huh.NewForm(
		huh.NewGroup(
			huh.NewMultiSelect[int]().
				Title("Select worktrees to remove").
				Options(options...).
				Value(&selected),
		),
	).WithKeyMap(pruneKeyMap()).Run()
	if err != nil {
		return nil, err
	}

	var result []prunable
	for _, idx := range selected {
		result = append(result, prunables[idx])
	}
	return result, nil
}
cmd.promptPruneConfirm function · go · L208-L228 (21 LOC)
cmd/prune.go
func promptPruneConfirm(count int) (bool, error) {
	noun := "worktree"
	if count > 1 {
		noun = "worktrees"
	}

	var confirmed bool
	err := huh.NewForm(
		huh.NewGroup(
			huh.NewConfirm().
				Title(fmt.Sprintf("Remove %d %s?", count, noun)).
				Affirmative("Yes").
				Negative("No").
				Value(&confirmed),
		),
	).WithKeyMap(pruneKeyMap()).Run()
	if err != nil {
		return false, err
	}
	return confirmed, nil
}
cmd.executeRemovals function · go · L235-L248 (14 LOC)
cmd/prune.go
func executeRemovals(w io.Writer, gitClient git.Git, selected []prunable) error {
	results := make([]pruneResult, len(selected))
	for i, p := range selected {
		var err error
		if p.reason == "orphaned" {
			err = removeOrphanedWorktree(gitClient, p.branchName)
		} else {
			err = removeWorktreeAndBranch(gitClient, p.path, p.branchName)
		}
		results[i] = pruneResult{prunable: p, err: err}
	}

	return renderPruneResults(w, results)
}
cmd.renderPruneResults function · go · L250-L293 (44 LOC)
cmd/prune.go
func renderPruneResults(w io.Writer, results []pruneResult) error {
	successStyle := lipgloss.NewStyle().Foreground(colorGreen)
	failStyle := lipgloss.NewStyle().Foreground(colorRed)
	reasonStyle := lipgloss.NewStyle().Foreground(colorGray)

	var rows [][]string
	var removed, failed int
	for _, r := range results {
		icon := successStyle.Render(iconCheck)
		status := ""
		if r.err != nil {
			icon = failStyle.Render(iconCross)
			status = failStyle.Render(r.err.Error())
			failed++
		} else {
			removed++
		}
		rows = append(rows, []string{icon, r.prunable.name, r.prunable.branchName, reasonStyle.Render(r.prunable.reason), status})
	}

	t := table.New().
		Border(lipgloss.HiddenBorder()).
		StyleFunc(func(_, _ int) lipgloss.Style {
			return lipgloss.NewStyle().Padding(0, 1)
		}).
		Rows(rows...)

	if _, err := fmt.Fprintln(w, t); err != nil {
		return err
	}

	summary := successStyle.Render(fmt.Sprintf("%d removed", removed))
	if failed > 0 {
		summary += ", " + failStyle.Render(fmt.S
cmd.removeOrphanedWorktree function · go · L295-L307 (13 LOC)
cmd/prune.go
func removeOrphanedWorktree(gitClient git.Git, branchName string) error {
	if err := gitClient.PruneWorktrees(); err != nil {
		return fmt.Errorf("failed to prune worktrees: %w", err)
	}
	if branchName != "" {
		if err := gitClient.DeleteBranch(branchName, true); err != nil {
			if !strings.Contains(err.Error(), "not found") {
				return fmt.Errorf("failed to delete branch %q: %w", branchName, err)
			}
		}
	}
	return nil
}
Repobility analyzer · published findings · https://repobility.com
cmd.init function · go · L37-L42 (6 LOC)
cmd/remove.go
func init() {
	removeCmd.GroupID = "worktree"
	removeCmd.Flags().BoolVar(&removeForceFlag, "force", false, "Force removal even with uncommitted changes")
	removeCmd.Flags().BoolVar(&removeKeepBranchFlag, "keep-branch", false, "Keep the local branch after removing the worktree")
	rootCmd.AddCommand(removeCmd)
}
cmd.runRemove function · go · L49-L63 (15 LOC)
cmd/remove.go
func runRemove(cmd *cobra.Command, args []string) error {
	rt, err := loadCommandRuntime()
	if err != nil {
		return err
	}

	// Root the git client at mainWorktreePath so that post-removal commands
	// (DeleteBranch, PruneWorktrees) still work even when cwd is the removed worktree.
	ctx := &removeContext{
		gitClient:        git.New(false, rt.mainWorktreePath, rt.cfg.Git.Timeout),
		mainWorktreePath: rt.mainWorktreePath,
	}

	return executeRemove(cmd.OutOrStdout(), ctx, args[0], removeForceFlag, removeKeepBranchFlag)
}
cmd.executeRemove function · go · L65-L117 (53 LOC)
cmd/remove.go
func executeRemove(w io.Writer, ctx *removeContext, target string, force, keepBranch bool) error {
	worktrees, err := ctx.gitClient.ListWorktrees()
	if err != nil {
		return fmt.Errorf("failed to list worktrees: %w", err)
	}

	workspacePath, err := ctx.gitClient.GetWorkspacePath()
	if err != nil {
		return fmt.Errorf("failed to get workspace path: %w", err)
	}

	wt, err := resolveTarget(target, worktrees, workspacePath)
	if err != nil {
		return err
	}

	if wt.AbsolutePath == ctx.mainWorktreePath {
		return errors.New("cannot remove the main worktree")
	}

	if !force {
		dirty, err := ctx.gitClient.IsWorktreeDirty(wt.AbsolutePath)
		if err != nil {
			return fmt.Errorf("failed to check worktree state: %w", err)
		}
		if dirty {
			return fmt.Errorf("worktree %q has uncommitted changes; use --force to remove anyway", filepath.Base(wt.AbsolutePath))
		}
	}

	branchName := extractBranchName(wt)

	if err := ctx.gitClient.RemoveWorktree(wt.AbsolutePath, force); err != nil {
		return fmt.Err
cmd.resolveTarget function · go · L119-L140 (22 LOC)
cmd/remove.go
func resolveTarget(target string, worktrees []git.Worktree, workspacePath string) (*git.Worktree, error) {
	for i := range worktrees {
		if worktrees[i].AbsolutePath == target {
			return &worktrees[i], nil
		}
	}

	absTarget := filepath.Join(workspacePath, target)
	for i := range worktrees {
		if worktrees[i].AbsolutePath == absTarget {
			return &worktrees[i], nil
		}
	}

	for i := range worktrees {
		if name := extractBranchName(&worktrees[i]); name == target {
			return &worktrees[i], nil
		}
	}

	return nil, fmt.Errorf("no worktree found matching %q", target)
}
cmd.extractBranchName function · go · L142-L150 (9 LOC)
cmd/remove.go
func extractBranchName(wt *git.Worktree) string {
	if wt.Ref == nil {
		return ""
	}
	if branch, ok := wt.Ref.FullBranch(); ok {
		return branch.Name
	}
	return ""
}
cmd.removeWorktreeAndBranch function · go · L152-L167 (16 LOC)
cmd/remove.go
func removeWorktreeAndBranch(gitClient git.Git, absPath, branchName string) error {
	if err := gitClient.RemoveWorktree(absPath, true); err != nil {
		return fmt.Errorf("failed to remove worktree %q: %w", filepath.Base(absPath), err)
	}
	if branchName != "" {
		if err := gitClient.DeleteBranch(branchName, true); err != nil {
			if !strings.Contains(err.Error(), "not found") {
				return fmt.Errorf("failed to delete branch %q: %w", branchName, err)
			}
		}
	}
	if err := gitClient.PruneWorktrees(); err != nil {
		return fmt.Errorf("failed to prune worktrees: %w", err)
	}
	return nil
}
cmd.init function · go · L23-L41 (19 LOC)
cmd/root.go
func init() {
	rootCmd.Version = Version
	rootCmd.PersistentFlags().Bool("debug", false, "Enable debug logging")

	rootCmd.AddGroup(
		&cobra.Group{ID: "worktree", Title: "Worktree Commands:"},
		&cobra.Group{ID: "pr", Title: "Pull Request Commands:"},
		&cobra.Group{ID: "config", Title: "Configuration Commands:"},
	)
	rootCmd.SetHelpCommandGroupID("config")
	rootCmd.SetCompletionCommandGroupID("config")
	configureLogStyles()

	rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
		if debug, _ := cmd.Flags().GetBool("debug"); debug {
			log.SetLevel(log.DebugLevel)
		}
	}
}
cmd.configureLogStyles function · go · L43-L65 (23 LOC)
cmd/root.go
func configureLogStyles() {
	styles := log.DefaultStyles()

	// Catppuccin Latte (light) / Mocha (dark) palette.
	muted := lipgloss.AdaptiveColor{Light: "#6c6f85", Dark: "#7f849c"}
	styles.Caller = lipgloss.NewStyle().Foreground(muted)
	styles.Key = lipgloss.NewStyle().Foreground(muted)
	styles.Prefix = lipgloss.NewStyle().Foreground(muted).Bold(true)
	styles.Separator = lipgloss.NewStyle().Foreground(muted)

	styles.Levels[log.DebugLevel] = styles.Levels[log.DebugLevel].
		Foreground(lipgloss.AdaptiveColor{Light: "#8c8fa1", Dark: "#7f849c"})
	styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].
		Foreground(lipgloss.AdaptiveColor{Light: "#179299", Dark: "#94e2d5"})
	styles.Levels[log.WarnLevel] = styles.Levels[log.WarnLevel].
		Foreground(lipgloss.AdaptiveColor{Light: "#df8e1d", Dark: "#f9e2af"})
	styles.Levels[log.ErrorLevel] = styles.Levels[log.ErrorLevel].
		Foreground(lipgloss.AdaptiveColor{Light: "#d20f39", Dark: "#f38ba8"})
	styles.Levels[log.FatalLevel] = styles.Levels[
Same scanner, your repo: https://repobility.com — Repobility
cmd.commandRuntime.newCachedGitHubClient method · go · L31-L38 (8 LOC)
cmd/runtime_context.go
func (rt *commandRuntime) newCachedGitHubClient() (github.GitHub, error) {
	dir, err := cache.DefaultDir()
	if err != nil {
		return nil, err
	}
	c := cache.New(dir, rt.cfg.GitHub.PreviewCacheTTL)
	return github.New(rt.cwd, rt.cfg.Git.Timeout, c), nil
}
cmd.loadCommandRuntime function · go · L40-L98 (59 LOC)
cmd/runtime_context.go
func loadCommandRuntime() (*commandRuntime, error) {
	originalCwd, err := os.Getwd()
	if err != nil {
		return nil, fmt.Errorf("failed to get current directory: %w", err)
	}

	homeDir, err := os.UserHomeDir()
	if err != nil {
		return nil, fmt.Errorf("failed to get user home directory: %w", err)
	}

	defaultTimeout := config.DefaultConfig().Git.Timeout
	gitDir := originalCwd
	initGit := git.New(false, gitDir, defaultTimeout)

	worktreeRoot, err := initGit.GetWorktreeRoot()
	if err != nil {
		return nil, fmt.Errorf("git error: %w", err)
	}

	if worktreeRoot == "" {
		log.Debug("not in a git repository, attempting workspace root detection", "cwd", originalCwd)

		bootstrapPaths := config.BootstrapConfigPaths(originalCwd, homeDir)
		bootstrapResult, err := config.NewDefaultLoader().Load(bootstrapPaths)
		if err != nil {
			return nil, fmt.Errorf("failed to load bootstrap config: %w", err)
		}
		log.Debug("bootstrap config loaded", "paths", bootstrapPaths, "sources", bootstrapResult.Source
cmd.resolveWorkspaceRoot function · go · L102-L131 (30 LOC)
cmd/runtime_context.go
func resolveWorkspaceRoot(cwd string, primaryBranches []string, timeout time.Duration) (string, error) {
	if len(primaryBranches) == 0 {
		return "", errors.New("no primary branches configured")
	}

	log.Debug("probing for primary worktree", "candidates", primaryBranches)

	for _, name := range primaryBranches {
		candidate := filepath.Join(cwd, name)
		if !hasGitMarker(candidate) {
			continue
		}

		testGit := git.New(false, candidate, timeout)
		testRoot, err := testGit.GetWorktreeRoot()
		if err != nil {
			log.Debug("candidate git error", "dir", candidate, "err", err)
			continue
		}
		if testRoot == "" {
			log.Debug("candidate has .git marker but is not a valid repo", "dir", candidate)
			continue
		}

		log.Debug("found valid worktree", "dir", candidate)
		return testRoot, nil
	}

	return "", errors.New("no valid worktree found in workspace root")
}
cmd.runStatus function · go · L52-L75 (24 LOC)
cmd/status.go
func runStatus(cmd *cobra.Command, _ []string) error {
	rt, err := loadCommandRuntime()
	if err != nil {
		return err
	}

	ghClient, err := rt.newCachedGitHubClient()
	if err != nil {
		return fmt.Errorf("failed to create GitHub client: %w", err)
	}

	ctx := &statusContext{
		ghClient:         ghClient,
		gitClient:        rt.gitClient,
		mainWorktreePath: rt.mainWorktreePath,
	}

	statuses, err := gatherStatuses(ctx)
	if err != nil {
		return err
	}

	return renderStatusTable(cmd.OutOrStdout(), statuses)
}
cmd.gatherStatuses function · go · L77-L100 (24 LOC)
cmd/status.go
func gatherStatuses(ctx *statusContext) ([]worktreeStatus, error) {
	worktrees, err := ctx.gitClient.ListWorktrees()
	if err != nil {
		return nil, fmt.Errorf("failed to list worktrees: %w", err)
	}

	branches, err := ctx.gitClient.ListLocalBranches()
	if err != nil {
		return nil, fmt.Errorf("failed to list branches: %w", err)
	}

	branchMap := make(map[string]git.LocalBranch, len(branches))
	for _, b := range branches {
		branchMap[b.Name] = b
	}

	var statuses []worktreeStatus
	for _, wt := range worktrees {
		ws := buildWorktreeStatus(ctx, wt, branchMap)
		statuses = append(statuses, ws)
	}

	return statuses, nil
}
cmd.buildWorktreeStatus function · go · L102-L136 (35 LOC)
cmd/status.go
func buildWorktreeStatus(ctx *statusContext, wt git.Worktree, branchMap map[string]git.LocalBranch) worktreeStatus {
	ws := worktreeStatus{
		absPath: wt.AbsolutePath,
		isMain:  wt.AbsolutePath == ctx.mainWorktreePath,
	}

	ws.branchName = extractBranchName(&wt)

	if ws.branchName == "" {
		return ws
	}

	if b, ok := branchMap[ws.branchName]; ok {
		ws.tracking = trackingInfo{
			ahead:    b.Ahead,
			behind:   b.Behind,
			upstream: b.UpstreamName,
		}
	}

	dirty, err := ctx.gitClient.IsWorktreeDirty(wt.AbsolutePath)
	if err == nil {
		ws.dirty = dirty
	}

	pr, err := ctx.ghClient.GetPullRequestByBranch(ws.branchName)
	if err == nil && pr != nil {
		ws.kind = "PR"
		ws.pr = pr
	} else if !ws.isMain {
		ws.kind = "local"
	}

	return ws
}
cmd.renderStatusTable function · go · L138-L183 (46 LOC)
cmd/status.go
func renderStatusTable(w io.Writer, statuses []worktreeStatus) error {
	if len(statuses) == 0 {
		_, err := fmt.Fprintln(w, "No worktrees found.")
		return err
	}

	purple := lipgloss.Color("99")
	gray := lipgloss.Color("245")
	lightGray := lipgloss.Color("241")

	headerStyle := lipgloss.NewStyle().Foreground(purple).Bold(true).Align(lipgloss.Center)
	cellStyle := lipgloss.NewStyle().Padding(0, 1)
	oddRowStyle := cellStyle.Foreground(gray)
	evenRowStyle := cellStyle.Foreground(lightGray)

	rows := make([][]string, len(statuses))
	for i, ws := range statuses {
		rows[i] = []string{
			filepath.Base(ws.absPath),
			ws.branchName,
			formatDirtyStatus(ws),
			formatTracking(ws.tracking),
			formatKind(ws),
			formatPRInfo(ws),
		}
	}

	t := table.New().
		Border(lipgloss.NormalBorder()).
		BorderStyle(lipgloss.NewStyle().Foreground(purple)).
		StyleFunc(func(row, col int) lipgloss.Style {
			switch {
			case row == table.HeaderRow:
				return headerStyle
			case row%2 == 0:
				return eve
cmd.formatDirtyStatus function · go · L185-L193 (9 LOC)
cmd/status.go
func formatDirtyStatus(ws worktreeStatus) string {
	if ws.branchName == "" {
		return ""
	}
	if ws.dirty {
		return colorIcon(iconCross, colorRed) + " dirty"
	}
	return colorIcon(iconCheck, colorGreen) + " clean"
}
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
cmd.formatTracking function · go · L195-L210 (16 LOC)
cmd/status.go
func formatTracking(ti trackingInfo) string {
	if ti.upstream == "" {
		return ""
	}
	if ti.ahead == 0 && ti.behind == 0 {
		return "\u2261" // ≡
	}
	var parts []string
	if ti.ahead > 0 {
		parts = append(parts, fmt.Sprintf("\u2191%d", ti.ahead))
	}
	if ti.behind > 0 {
		parts = append(parts, fmt.Sprintf("\u2193%d", ti.behind))
	}
	return strings.Join(parts, " ")
}
cmd.formatKind function · go · L212-L221 (10 LOC)
cmd/status.go
func formatKind(ws worktreeStatus) string {
	switch ws.kind {
	case "PR":
		return lipgloss.NewStyle().Foreground(colorPurple).Render("PR")
	case "local":
		return lipgloss.NewStyle().Foreground(colorGray).Render("local")
	default:
		return ""
	}
}
cmd.formatPRInfo function · go · L223-L246 (24 LOC)
cmd/status.go
func formatPRInfo(ws worktreeStatus) string {
	if ws.pr == nil {
		return ""
	}
	pr := ws.pr

	stateStr := lipgloss.NewStyle().Foreground(stateColor(pr.State)).Render(strings.ToLower(string(pr.State)))
	info := fmt.Sprintf("#%d %s", pr.Number, stateStr)

	if checksStr := formatChecksSummary(pr.StatusChecks); checksStr != "" {
		info += "  " + checksStr
	}

	if reviewStr := formatReviewSummary(pr.Reviews); reviewStr != "" {
		info += "  " + reviewStr
	}

	info += fmt.Sprintf("  %s %s",
		lipgloss.NewStyle().Foreground(colorGreen).Render(fmt.Sprintf("+%d", pr.LinesAdded)),
		lipgloss.NewStyle().Foreground(colorRed).Render(fmt.Sprintf("-%d", pr.LinesDeleted)),
	)

	return info
}
cmd.formatChecksSummary function · go · L248-L264 (17 LOC)
cmd/status.go
func formatChecksSummary(checks []github.StatusCheck) string {
	if len(checks) == 0 {
		return ""
	}
	passed := 0
	for _, c := range checks {
		if c.Conclusion == github.CheckConclusionSuccess {
			passed++
		}
	}
	total := len(checks)
	icon := colorIcon(iconCross, colorRed)
	if passed == total {
		icon = colorIcon(iconCheck, colorGreen)
	}
	return icon + fmt.Sprintf("%d/%d checks", passed, total)
}
cmd.formatReviewSummary function · go · L266-L292 (27 LOC)
cmd/status.go
func formatReviewSummary(reviews []github.Review) string {
	if len(reviews) == 0 {
		return ""
	}
	deduped := deduplicateReviews(reviews)
	hasApproval := false
	hasChangesRequested := false
	for _, r := range deduped {
		switch r.State {
		case github.ReviewStateApproved:
			hasApproval = true
		case github.ReviewStateChangesRequested:
			hasChangesRequested = true
		}
	}
	if hasChangesRequested {
		return lipgloss.NewStyle().Foreground(colorRed).Render("changes requested")
	}
	if hasApproval {
		return lipgloss.NewStyle().Foreground(colorGreen).Render("approved")
	}
	noun := "reviews"
	if len(deduped) == 1 {
		noun = "review"
	}
	return lipgloss.NewStyle().Foreground(colorGray).Render(fmt.Sprintf("%d %s", len(deduped), noun))
}
cache.New function · go · L32-L38 (7 LOC)
internal/cache/cache.go
func New(dir string, ttl time.Duration) *Cache {
	return &Cache{
		dir: dir,
		log: clog.Default().WithPrefix("cache"),
		ttl: ttl,
	}
}
cache.DefaultDir function · go · L41-L47 (7 LOC)
internal/cache/cache.go
func DefaultDir() (string, error) {
	base, err := os.UserCacheDir()
	if err != nil {
		return "", fmt.Errorf("failed to get user cache dir: %w", err)
	}
	return filepath.Join(base, "grove", "v1"), nil
}
cache.Cache.Get method · go · L57-L86 (30 LOC)
internal/cache/cache.go
func (c *Cache) Get(key string) (string, bool) {
	if c == nil || c.ttl == 0 {
		return "", false
	}

	path := c.path(key)
	data, err := os.ReadFile(path)
	if err != nil {
		return "", false
	}

	var env envelope
	if err := json.Unmarshal(data, &env); err != nil {
		c.log.Debug("cache: corrupt entry", "key", key, "error", err)
		return "", false
	}

	if env.Key != key {
		c.log.Debug("cache: key mismatch", "want", key, "got", env.Key)
		return "", false
	}

	if time.Since(env.CreatedAt) > c.ttl {
		c.log.Debug("cache: expired", "key", key, "age", time.Since(env.CreatedAt))
		return "", false
	}

	c.log.Debug("cache: hit", "key", key)
	return env.Payload, true
}
Open data scored by Repobility · https://repobility.com
cache.Cache.Set method · go · L89-L133 (45 LOC)
internal/cache/cache.go
func (c *Cache) Set(key string, payload string) {
	if c == nil || c.ttl == 0 {
		return
	}

	if err := os.MkdirAll(c.dir, 0o755); err != nil {
		c.log.Debug("cache: mkdir failed", "error", err)
		return
	}

	env := envelope{
		CreatedAt: time.Now(),
		Key:       key,
		Payload:   payload,
	}
	data, err := json.Marshal(env)
	if err != nil {
		c.log.Debug("cache: marshal failed", "error", err)
		return
	}

	tmp, err := os.CreateTemp(c.dir, ".tmp-*")
	if err != nil {
		c.log.Debug("cache: temp file failed", "error", err)
		return
	}
	tmpName := tmp.Name()

	if _, err := tmp.Write(data); err != nil {
		_ = tmp.Close()
		_ = os.Remove(tmpName)
		c.log.Debug("cache: write failed", "error", err)
		return
	}
	if err := tmp.Close(); err != nil {
		_ = os.Remove(tmpName)
		c.log.Debug("cache: close failed", "error", err)
		return
	}

	if err := os.Rename(tmpName, c.path(key)); err != nil {
		_ = os.Remove(tmpName)
		c.log.Debug("cache: rename failed", "error", err)
	}
}
cache.Cache.Clear method · go · L136-L154 (19 LOC)
internal/cache/cache.go
func (c *Cache) Clear() error {
	if c == nil {
		return nil
	}
	entries, err := os.ReadDir(c.dir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return fmt.Errorf("failed to read cache dir: %w", err)
	}
	var errs []error
	for _, e := range entries {
		if err := os.Remove(filepath.Join(c.dir, e.Name())); err != nil && !os.IsNotExist(err) {
			errs = append(errs, err)
		}
	}
	return errors.Join(errs...)
}
config.Config.Validate method · go · L20-L43 (24 LOC)
internal/config/config.go
func (c Config) Validate() error {
	if c.Git.Timeout < 0 {
		return errors.New("git.timeout cannot be negative")
	}
	if c.GitHub.PreviewCacheTTL < 0 {
		return errors.New("github.preview_cache_ttl cannot be negative")
	}
	if c.PullRequest.WorktreePrefix == "" {
		return errors.New("pull_request.worktree_prefix cannot be empty")
	}
	if c.Slugify.HashLength < 0 {
		return errors.New("slugify.hash_length cannot be negative")
	}
	if c.Slugify.MaxLength < 0 {
		return errors.New("slugify.max_length cannot be negative")
	}
	if c.Slugify.MaxLength > 0 && c.Slugify.HashLength > c.Slugify.MaxLength-2 {
		return errors.New("slugify.hash_length must be at least 2 less than slugify.max_length")
	}
	if len(c.Workspace.PrimaryBranches) == 0 {
		return errors.New("workspace.primary_branches cannot be empty")
	}
	return nil
}
config.DefaultConfig function · go · L6-L35 (30 LOC)
internal/config/defaults.go
func DefaultConfig() Config {
	return Config{
		Git: GitConfig{
			Timeout: 5 * time.Second,
		},
		GitHub: GitHubConfig{
			PreviewCacheTTL: 5 * time.Minute,
		},
		LocalBranch: LocalBranchConfig{
			BranchPrefix:      "feature/",
			StripBranchPrefix: []string{"feature/"},
			WorktreePrefix:    "wt-",
		},
		PullRequest: PullRequestConfig{
			BranchTemplate: "{{.BranchName}}",
			WorktreePrefix: "pr-",
		},
		Slugify: SlugifyConfig{
			CollapseDashes:     true,
			HashLength:         4,
			Lowercase:          true,
			MaxLength:          50,
			ReplaceNonAlphanum: true,
			TrimDashes:         true,
		},
		Workspace: WorkspaceConfig{
			PrimaryBranches: []string{"main", "develop", "master"},
		},
	}
}
config.ConfigPaths function · go · L23-L75 (53 LOC)
internal/config/discovery.go
func ConfigPaths(cwd, worktreeRoot, gitRoot, homeDir string) []string {
	var paths []string
	seen := make(map[string]bool)

	addPath := func(dir string) {
		if dir == "" {
			return
		}
		path := filepath.Join(dir, configFileName)
		if !seen[path] {
			seen[path] = true
			paths = append(paths, path)
		}
	}

	if xdgConfigDir := os.Getenv("XDG_CONFIG_HOME"); xdgConfigDir != "" {
		addPath(filepath.Join(xdgConfigDir, "grove"))
	} else if homeDir != "" {
		addPath(filepath.Join(homeDir, ".config", "grove"))
	}

	if gitRoot != "" && homeDir != "" {
		// Collect ancestors from gitRoot's parent up to home.
		// Skip cwd so it appears only via addPath(cwd) below (highest priority),
		// but always include homeDir since it's the walk boundary.
		var ancestors []string
		current := filepath.Dir(gitRoot)
		for current != "" && len(current) >= len(homeDir) {
			if current != cwd || current == homeDir {
				ancestors = append(ancestors, current)
			}
			if current == homeDir {
				break
			}
			pa
config.OSFileSystem.Exists method · go · L27-L33 (7 LOC)
internal/config/loader.go
func (OSFileSystem) Exists(path string) bool {
	info, err := os.Stat(path)
	if err != nil {
		return false
	}
	return !info.IsDir()
}
config.NewLoader function · go · L42-L47 (6 LOC)
internal/config/loader.go
func NewLoader(fs FileSystem) *Loader {
	return &Loader{
		fs:  fs,
		log: log.Default().WithPrefix("config"),
	}
}
config.Loader.Load method · go · L57-L86 (30 LOC)
internal/config/loader.go
func (l *Loader) Load(paths []string) (LoadResult, error) {
	cfg := DefaultConfig()
	var sourcePaths []string

	for _, path := range paths {
		if !l.fs.Exists(path) {
			continue // Skip missing files
		}

		metadata, err := toml.DecodeFile(path, &cfg)
		if err != nil {
			return LoadResult{}, fmt.Errorf("failed to parse %s: %w", path, err)
		}

		if undecoded := metadata.Undecoded(); len(undecoded) > 0 {
			l.log.Warn("unknown config keys", "path", path, "keys", undecoded)
		}

		sourcePaths = append(sourcePaths, path)
	}

	if err := cfg.Validate(); err != nil {
		return LoadResult{}, fmt.Errorf("invalid config: %w", err)
	}

	return LoadResult{
		Config:      cfg,
		SourcePaths: sourcePaths,
	}, nil
}
Repobility analyzer · published findings · https://repobility.com
git.New function · go · L27-L34 (8 LOC)
internal/git/git_cli.go
func New(dryRun bool, workingDir string, timeout time.Duration) Git {
	return &GitCli{
		dryRun:     dryRun,
		log:        clog.Default().WithPrefix("git"),
		timeout:    timeout,
		workingDir: workingDir,
	}
}
git.GitCli.executeGitCommand method · go · L36-L62 (27 LOC)
internal/git/git_cli.go
func (g *GitCli) executeGitCommand(args ...string) (string, error) {
	g.log.Debug("Executing git command", "cmd", "git", "args", args, "workingDir", g.workingDir)

	ctx, cancel := context.WithTimeout(context.Background(), g.timeout)
	defer cancel()

	cmd := exec.CommandContext(ctx, "git", args...)
	cmd.Dir = g.workingDir
	cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")

	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("git command timed out", "args", args, "timeout", g.timeout, "error", err)
			return "", fmt.Errorf("git %s timed out after %s", strings.Join(args, " "), g.timeout)
		}
		g.log.Debug("Git command failed", "args", args, "stderr", stderr.String(), "error", err)
		return "", fmt.Errorf("git %s failed: %w: %s", strings.Join(args, " "), err, stderr.String())
	}

	output := strings.TrimSpace(stdout.String())
	g.log.Debug("Git command succeeded", "ar
git.GitCli.executeMutatingGitCommand method · go · L65-L74 (10 LOC)
internal/git/git_cli.go
func (g *GitCli) executeMutatingGitCommand(errContext string, args ...string) error {
	if g.dryRun {
		g.log.Info("Would execute git command", "cmd", "git", "args", args)
		return nil
	}
	if _, err := g.executeGitCommand(args...); err != nil {
		return fmt.Errorf("%s: %w", errContext, err)
	}
	return nil
}
‹ prevpage 2 / 4next ›