• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

umputun / ralphex / 21466423009

29 Jan 2026 05:07AM UTC coverage: 79.724% (+1.5%) from 78.255%
21466423009

push

github

umputun
refactor: simplify main.go using extracted packages

main.go reduced from 1100 to ~670 lines by using:
- pkg/plan.Selector for plan file selection
- pkg/git.IsMainBranch and EnsureIgnored for git operations
- pkg/web.Dashboard for web dashboard management

unified printStartupInfo to handle both execution and plan modes

94 of 213 new or added lines in 3 files covered. (44.13%)

108 existing lines in 4 files now uncovered.

3873 of 4858 relevant lines covered (79.72%)

61.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.34
/pkg/git/workflow.go
1
package git
2

3
import (
4
        "errors"
5
        "fmt"
6
        "os"
7
        "path/filepath"
8

9
        "github.com/umputun/ralphex/pkg/plan"
10
)
11

12
// Workflow provides plan-aware git operations built on top of Repo.
13
type Workflow struct {
14
        repo *Repo
15
        log  func(string, ...any) (int, error)
16
}
17

18
// NewWorkflow creates a Workflow wrapping the given Repo.
19
// logFn is called to report progress (compatible with color.Color.Printf).
20
func NewWorkflow(repo *Repo, logFn func(string, ...any) (int, error)) *Workflow {
15✔
21
        return &Workflow{repo: repo, log: logFn}
15✔
22
}
15✔
23

24
// Repo returns the underlying Repo for direct access when needed.
25
func (w *Workflow) Repo() *Repo {
1✔
26
        return w.repo
1✔
27
}
1✔
28

29
// CreateBranchForPlan creates or switches to a feature branch for plan execution.
30
// If already on a feature branch (not main/master), returns nil immediately.
31
// If on main/master, extracts branch name from plan file and creates/switches to it.
32
// If plan file has uncommitted changes and is the only dirty file, auto-commits it.
33
func (w *Workflow) CreateBranchForPlan(planFile string) error {
7✔
34
        isMain, err := w.repo.IsMainBranch()
7✔
35
        if err != nil {
7✔
NEW
36
                return fmt.Errorf("check main branch: %w", err)
×
NEW
37
        }
×
38

39
        if !isMain {
8✔
40
                return nil // already on feature branch
1✔
41
        }
1✔
42

43
        branchName := plan.ExtractBranchName(planFile)
6✔
44
        currentBranch, err := w.repo.CurrentBranch()
6✔
45
        if err != nil {
6✔
NEW
46
                return fmt.Errorf("get current branch: %w", err)
×
NEW
47
        }
×
48

49
        // check for uncommitted changes to files other than the plan
50
        hasOtherChanges, err := w.repo.HasChangesOtherThan(planFile)
6✔
51
        if err != nil {
6✔
NEW
52
                return fmt.Errorf("check uncommitted files: %w", err)
×
NEW
53
        }
×
54

55
        if hasOtherChanges {
7✔
56
                // other files have uncommitted changes - show helpful error
1✔
57
                return fmt.Errorf("cannot create branch %q: worktree has uncommitted changes\n\n"+
1✔
58
                        "ralphex needs to create a feature branch from %s to isolate plan work.\n\n"+
1✔
59
                        "options:\n"+
1✔
60
                        "  git stash && ralphex %s && git stash pop   # stash changes temporarily\n"+
1✔
61
                        "  git commit -am \"wip\"                       # commit changes first\n"+
1✔
62
                        "  ralphex --review                           # skip branch creation (review-only mode)",
1✔
63
                        branchName, currentBranch, planFile)
1✔
64
        }
1✔
65

66
        // check if plan file needs to be committed (untracked, modified, or staged)
67
        planHasChanges, err := w.repo.FileHasChanges(planFile)
5✔
68
        if err != nil {
5✔
NEW
69
                return fmt.Errorf("check plan file status: %w", err)
×
NEW
70
        }
×
71

72
        // create or switch to branch
73
        if w.repo.BranchExists(branchName) {
6✔
74
                w.log("switching to existing branch: %s\n", branchName)
1✔
75
                if err := w.repo.CheckoutBranch(branchName); err != nil {
1✔
NEW
76
                        return fmt.Errorf("checkout branch %s: %w", branchName, err)
×
NEW
77
                }
×
78
        } else {
4✔
79
                w.log("creating branch: %s\n", branchName)
4✔
80
                if err := w.repo.CreateBranch(branchName); err != nil {
4✔
NEW
81
                        return fmt.Errorf("create branch %s: %w", branchName, err)
×
NEW
82
                }
×
83
        }
84

85
        // auto-commit plan file if it was the only uncommitted file
86
        if planHasChanges {
9✔
87
                w.log("committing plan file: %s\n", filepath.Base(planFile))
4✔
88
                if err := w.repo.Add(planFile); err != nil {
4✔
NEW
89
                        return fmt.Errorf("stage plan file: %w", err)
×
NEW
90
                }
×
91
                if err := w.repo.Commit("add plan: " + branchName); err != nil {
4✔
NEW
92
                        return fmt.Errorf("commit plan file: %w", err)
×
NEW
93
                }
×
94
        }
95

96
        return nil
5✔
97
}
98

99
// MovePlanToCompleted moves a plan file to the completed/ subdirectory and commits.
100
// Creates the completed/ directory if it doesn't exist.
101
// Uses git mv if the file is tracked, falls back to os.Rename for untracked files.
102
func (w *Workflow) MovePlanToCompleted(planFile string) error {
3✔
103
        // create completed directory
3✔
104
        completedDir := filepath.Join(filepath.Dir(planFile), "completed")
3✔
105
        if err := os.MkdirAll(completedDir, 0o750); err != nil {
3✔
NEW
106
                return fmt.Errorf("create completed dir: %w", err)
×
NEW
107
        }
×
108

109
        // destination path
110
        destPath := filepath.Join(completedDir, filepath.Base(planFile))
3✔
111

3✔
112
        // use git mv
3✔
113
        if err := w.repo.MoveFile(planFile, destPath); err != nil {
4✔
114
                // fallback to regular move for untracked files
1✔
115
                if renameErr := os.Rename(planFile, destPath); renameErr != nil {
1✔
NEW
116
                        return fmt.Errorf("move plan: %w", renameErr)
×
NEW
117
                }
×
118
                // stage the new location - log if fails but continue
119
                if addErr := w.repo.Add(destPath); addErr != nil {
1✔
NEW
120
                        fmt.Fprintf(os.Stderr, "warning: failed to stage moved plan: %v\n", addErr)
×
NEW
121
                }
×
122
        }
123

124
        // commit the move
125
        commitMsg := "move completed plan: " + filepath.Base(planFile)
3✔
126
        if err := w.repo.Commit(commitMsg); err != nil {
3✔
NEW
127
                return fmt.Errorf("commit plan move: %w", err)
×
NEW
128
        }
×
129

130
        w.log("moved plan to %s\n", destPath)
3✔
131
        return nil
3✔
132
}
133

134
// EnsureHasCommits checks that the repository has at least one commit.
135
// If the repository is empty, calls promptFn to ask user whether to create initial commit.
136
// promptFn should return true to create the commit, false to abort.
137
// Returns error if repo is empty and user declined or promptFn returned false.
138
func (w *Workflow) EnsureHasCommits(promptFn func() bool) error {
4✔
139
        hasCommits, err := w.repo.HasCommits()
4✔
140
        if err != nil {
4✔
NEW
141
                return fmt.Errorf("check commits: %w", err)
×
NEW
142
        }
×
143
        if hasCommits {
5✔
144
                return nil
1✔
145
        }
1✔
146

147
        // prompt user to create initial commit
148
        if !promptFn() {
4✔
149
                return errors.New("no commits - please create initial commit manually")
1✔
150
        }
1✔
151

152
        // create the commit
153
        if err := w.repo.CreateInitialCommit("initial commit"); err != nil {
3✔
154
                return fmt.Errorf("create initial commit: %w", err)
1✔
155
        }
1✔
156
        return nil
1✔
157
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc