Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ func mergeInto(dst, src *Config) {
// computeSources compares a resolved config against global and repo raw configs
// to determine which source provided each final value.
func computeSources(resolved, global, repo *Config) ConfigSource {
defaults := Default()
src := ConfigSource{
AgentCommand: SourceDefault,
AgentArgs: SourceDefault,
Expand All @@ -189,14 +188,6 @@ func computeSources(resolved, global, repo *Config) ConfigSource {
GitHubAutoPush: SourceDefault,
}

// Helper: check global then repo for each field
if global != nil && global.Agent.Command != "" && global.Agent.Command != defaults.Agent.Command {
src.AgentCommand = SourceGlobal
}
if repo != nil && repo.Agent.Command != "" && repo.Agent.Command != defaults.Agent.Command {
src.AgentCommand = SourceRepo
}
// If resolved value equals default but global/repo both set it, still annotate
if global != nil && global.Agent.Command != "" {
src.AgentCommand = SourceGlobal
}
Expand Down Expand Up @@ -239,7 +230,6 @@ func computeSources(resolved, global, repo *Config) ConfigSource {
src.GitHubAutoPush = SourceRepo
}

_ = resolved
return src
}

Expand Down
10 changes: 9 additions & 1 deletion pkg/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,15 @@ func (pm *PRManager) FetchPRReviews(branch string) ([]ReviewComment, error) {
cmd := exec.Command("gh", "pr", "view", branch, "--json", "url,reviews")
output, err := cmd.CombinedOutput()
if err != nil {
return nil, nil // no PR for this branch
// gh exits 4 when the resource (PR) is not found; also guard on output text
// for older gh versions that may use exit code 1 with a message.
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 4 {
return nil, nil
}
if strings.Contains(string(output), "no pull requests found") {
return nil, nil
}
return nil, fmt.Errorf("gh pr view failed: %w", err)
}

var prData struct {
Expand Down
13 changes: 8 additions & 5 deletions pkg/tui/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ func (m Model) loadWorkspacesCmd() tea.Msg {
go func(i int, ws *state.Workspace) {
defer wg.Done()

diff, fileChanges, _ := m.worktreeMgr.DiffStats(ws.Branch, ws.BaseBranch)
diff, fileChanges, err := m.worktreeMgr.DiffStats(ws.Branch, ws.BaseBranch)
diffStat := "No changes"
lines := strings.Split(strings.TrimSpace(diff), "\n")
if len(lines) > 0 && lines[len(lines)-1] != "" {
diffStat = lines[len(lines)-1]
if err != nil {
diffStat = "diff unavailable"
} else {
lines := strings.Split(strings.TrimSpace(diff), "\n")
if len(lines) > 0 && lines[len(lines)-1] != "" {
diffStat = lines[len(lines)-1]
}
}

win, exists := windowMap[ws.Name]
Expand All @@ -49,7 +53,6 @@ func (m Model) loadWorkspacesCmd() tea.Msg {
Workspace: ws,
DiffStat: diffStat,
Active: exists && win.Active,
WindowID: "",
FileChanges: fileChanges,
}
if exists {
Expand Down
50 changes: 21 additions & 29 deletions pkg/tui/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,24 +217,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if branchName == "" {
return m, nil
}
m.creating = false
m.remoteBranchMode = false
m.remoteBranches = nil
m.filteredBranches = nil
m.branchSuggestionCursor = 0
m.input.SetValue("")
m.input.Placeholder = "New branch name"
m.resetCreateMode()
m.workspaceCreating = true
m.workspaceCreatingName = branchName
return m, tea.Batch(m.createWorkspaceFromRemoteCmd(branchName), spinnerTickCmd())
case "esc":
m.creating = false
m.remoteBranchMode = false
m.remoteBranches = nil
m.filteredBranches = nil
m.branchSuggestionCursor = 0
m.input.SetValue("")
m.input.Placeholder = "New branch name"
m.resetCreateMode()
return m, nil
default:
m.input, cmd = m.input.Update(msg)
Expand All @@ -251,10 +239,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
if m.issueMode {
m.creating = false
m.issueMode = false
m.input.SetValue("")
m.input.Placeholder = "New branch name"
m.resetCreateMode()
m.workspaceCreating = true
m.workspaceCreatingName = "issue " + val
return m, tea.Batch(m.createWorkspaceFromIssueCmd(val), spinnerTickCmd())
Expand All @@ -268,21 +253,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
branchName := m.newBranchName
baseBranch := val
m.creating = false
m.createStep = 0
m.newBranchName = ""
m.input.SetValue("")
m.input.Placeholder = "New branch name"
m.resetCreateMode()
m.workspaceCreating = true
m.workspaceCreatingName = branchName
return m, tea.Batch(m.createWorkspaceCmd(branchName, baseBranch), spinnerTickCmd())
case "esc":
m.creating = false
m.issueMode = false
m.createStep = 0
m.newBranchName = ""
m.input.SetValue("")
m.input.Placeholder = "New branch name"
m.resetCreateMode()
return m, nil
}
m.input, cmd = m.input.Update(msg)
Expand Down Expand Up @@ -603,6 +579,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.workspaceCreating = false
m.workspaceDeleting = false
m.workspaceDeletingNames = make(map[string]bool)
m.creating = false
m.filtering = false
m.prCreating = false
m.err = msg.err
m.appendErrLog(msg.err.Error())
return m, tea.Tick(3*time.Second, func(t time.Time) tea.Msg {
Expand All @@ -628,6 +607,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
}

func (m *Model) resetCreateMode() {
m.creating = false
m.remoteBranchMode = false
m.remoteBranches = nil
m.filteredBranches = nil
m.branchSuggestionCursor = 0
m.issueMode = false
m.createStep = 0
m.newBranchName = ""
m.input.SetValue("")
m.input.Placeholder = "New branch name"
}

func (m *Model) appendErrLog(msg string) {
ts := time.Now().Format("15:04:05")
entry := fmt.Sprintf("[%s] %s", ts, msg)
Expand Down
47 changes: 26 additions & 21 deletions pkg/tui/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import (
"github.com/axelgar/opentree/pkg/config"
)

const (
headerFooterHeight = 8
minDiffHeight = 5
defaultPreviewWidth = 60
minPreviewWidth = 20
)

func (m Model) View() string {
// Error log overlay
if m.showErrLog {
Expand Down Expand Up @@ -70,9 +77,9 @@ func (m Model) View() string {
// Diff view overlay
if m.diffViewing {
lines := strings.Split(m.diffContent, "\n")
availHeight := m.height - 8
if availHeight < 5 {
availHeight = 5
availHeight := m.height - headerFooterHeight
if availHeight < minDiffHeight {
availHeight = minDiffHeight
}
// clamp scroll
maxScroll := len(lines) - availHeight
Expand Down Expand Up @@ -294,26 +301,12 @@ func (m Model) View() string {
case ws.PRStatus == "open" && ws.MergeConflicts:
title += " " + conflictsBadgeStyle.Render("PR open · conflicts")
if ci, ok := m.ciStatus[ws.Name]; ok {
switch ci {
case "success":
title += " " + ciSuccessStyle.Render("✓ CI")
case "failure":
title += " " + ciFailureStyle.Render("✗ CI")
case "pending":
title += " " + ciPendingStyle.Render("⟳ CI")
}
title += renderCIBadge(ci)
}
case ws.PRStatus == "open":
title += " " + prOpenBadgeStyle.Render("PR open")
if ci, ok := m.ciStatus[ws.Name]; ok {
switch ci {
case "success":
title += " " + ciSuccessStyle.Render("✓ CI")
case "failure":
title += " " + ciFailureStyle.Render("✗ CI")
case "pending":
title += " " + ciPendingStyle.Render("⟳ CI")
}
title += renderCIBadge(ci)
}
case ws.BranchPushed:
title += " " + pushedBadgeStyle.Render("pushed")
Expand Down Expand Up @@ -384,8 +377,8 @@ func (m Model) View() string {
if m.agentPreview != "" && m.cursor < len(visible) {
wsName := visible[m.cursor].Name
previewWidth := m.width - 8
if previewWidth < 20 {
previewWidth = 60
if previewWidth < minPreviewWidth {
previewWidth = defaultPreviewWidth
}
content := previewTitleStyle.Render("Agent Output: "+wsName) + "\n" +
previewLineStyle.Render(m.agentPreview)
Expand Down Expand Up @@ -502,3 +495,15 @@ func (m Model) sortedWorkspaces() []WorkspaceItem {
}
return ws
}

func renderCIBadge(ci string) string {
switch ci {
case "success":
return " " + ciSuccessStyle.Render("✓ CI")
case "failure":
return " " + ciFailureStyle.Render("✗ CI")
case "pending":
return " " + ciPendingStyle.Render("⟳ CI")
}
return ""
}
1 change: 1 addition & 0 deletions pkg/workspace/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func (s *Service) Create(name, baseBranch string) (*state.Workspace, error) {

agentCmd := s.cfg.Agent.Command
if err := s.process.CreateWindow(name, worktreePath, agentCmd, s.cfg.Agent.Args...); err != nil {
_ = s.worktrees.Delete(name, true) // cleanup orphaned worktree
return nil, fmt.Errorf("failed to create tmux window: %w", err)
}

Expand Down
9 changes: 7 additions & 2 deletions pkg/worktree/worktree.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/axelgar/opentree/pkg/gitutil"
Expand Down Expand Up @@ -384,10 +385,14 @@ func parseNumstat(output string) []FileChange {
added := 0
removed := 0
if parts[0] != "-" {
_, _ = fmt.Sscanf(parts[0], "%d", &added)
if n, err := strconv.Atoi(parts[0]); err == nil {
added = n
}
}
if parts[1] != "-" {
_, _ = fmt.Sscanf(parts[1], "%d", &removed)
if n, err := strconv.Atoi(parts[1]); err == nil {
removed = n
}
}
files = append(files, FileChange{
FileName: parts[2],
Expand Down
Loading