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
14 changes: 1 addition & 13 deletions internal/app/add_repo_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,20 +232,8 @@ func (s *AddRepoService) Run(input domain.AddRepoInput) (domain.AddRepoResult, e
})
}

rootPlan := domain.PlannedWorktree{
Repo: domain.DiscoveredFlutterRepo{
Name: filepath.Base(rootRecord.Path),
RepoRoot: rootRecord.RepoRoot,
PackageName: readPackageNameFromWorktree(rootRecord.Path),
},
Role: "root",
Path: rootRecord.Path,
Branch: rootRecord.Branch,
}

overridePath := filepath.Join(rootRecord.Path, overrideFileName)
overrideContent := buildOverrideContent(rootPlan, overridePackages)
if err := os.WriteFile(overridePath, []byte(overrideContent), 0o644); err != nil {
if err := writeOverrideFile(overridePath, rootRecord.Path, overridePackages); err != nil {
rollback()
return domain.AddRepoResult{}, domain.NewError(domain.CategoryPersistence, 5, "Failed to update pubspec_overrides.yaml.", overridePath, err)
}
Expand Down
149 changes: 142 additions & 7 deletions internal/app/create_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (s *CreateService) Apply(plan domain.CreateDryPlan, options domain.CreateAp
created = append(created, pkg)
}

if err := os.WriteFile(plan.OverridePath, []byte(plan.OverrideContent), 0o644); err != nil {
if err := writeOverrideFile(plan.OverridePath, plan.Root.Path, plan.Packages); err != nil {
rollback()
return domain.CreateResult{}, domain.NewError(domain.CategoryPersistence, 5, "Failed to write pubspec_overrides.yaml.", plan.OverridePath, err)
}
Expand Down Expand Up @@ -408,18 +408,153 @@ func repoLabel(repo domain.DiscoveredFlutterRepo) string {
}

func buildOverrideContent(root domain.PlannedWorktree, packages []domain.PlannedWorktree) string {
lines := []string{"dependency_overrides:"}
if len(packages) == 0 {
lines = append(lines, " {}")
return strings.Join(lines, "\n") + "\n"
entries := buildOverrideEntriesMap(root, packages, nil)
return formatOverrideYAML(entries)
}

// OverrideEntry represents a single dependency_override entry.
type OverrideEntry struct {
PackageName string
RelativePath string
}

// buildOverrideEntriesMap merges new packages with existing entries.
// existingEntries may be nil. When package name collides, new wins.
func buildOverrideEntriesMap(root domain.PlannedWorktree, packages []domain.PlannedWorktree, existingEntries map[string]string) map[string]string {
entries := make(map[string]string)
// Seed with existing entries first (will be overwritten by new if duplicate).
for pkg, relPath := range existingEntries {
entries[pkg] = relPath
}
// Add new packages (overwrites any existing with same package name).
for _, pkg := range packages {
rel, err := filepath.Rel(root.Path, pkg.Path)
if err != nil {
rel = pkg.Path
}
lines = append(lines, " "+pkg.Repo.PackageName+":")
lines = append(lines, " path: "+filepath.ToSlash(rel))
entries[pkg.Repo.PackageName] = filepath.ToSlash(rel)
}
return entries
}

// Merge existing overrides from pubspec.yaml into the overrides map.
// Returns the updated entries map with pubspec overrides included.
// Pubspec overrides are only moved if not already present in entries (new wins on conflict).
func mergePubspecOverrides(entries map[string]string, pubspecPath string) map[string]string {
content, err := os.ReadFile(pubspecPath)
if err != nil {
return entries
}
parsing := false
var currentPkg string
for _, rawLine := range strings.Split(string(content), "\n") {
line := strings.TrimSpace(rawLine)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "dependency_overrides:") {
parsing = true
currentPkg = ""
continue
}
if parsing && strings.HasPrefix(line, ":") && !strings.HasPrefix(line, " ") {
break
}
if parsing && strings.HasPrefix(line, " ") && strings.TrimSpace(line) != "" {
pkgLine := strings.TrimSpace(line)
if strings.HasSuffix(pkgLine, ":") {
currentPkg = strings.TrimSpace(strings.TrimSuffix(pkgLine, ":"))
continue
}
if strings.HasPrefix(pkgLine, "path:") && currentPkg != "" {
relPath := strings.TrimSpace(strings.TrimPrefix(pkgLine, "path:"))
relPath = strings.Trim(relPath, "\"'")
if relPath != "" {
if _, exists := entries[currentPkg]; !exists {
entries[currentPkg] = relPath
}
}
currentPkg = ""
}
}
}
return entries
}

// readExistingOverrides reads the current pubspec_overrides.yaml and returns
// a map of package name -> relative path. Returns nil if file doesn't exist.
func readExistingOverrides(overridePath string) map[string]string {
content, err := os.ReadFile(overridePath)
if err != nil {
return nil
}
entries := make(map[string]string)
var currentPkg string
for _, rawLine := range strings.Split(string(content), "\n") {
line := strings.TrimSpace(rawLine)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if line == "dependency_overrides:" || strings.TrimSpace(line) == "{}" {
continue
}
if strings.HasPrefix(line, " ") && !strings.HasPrefix(line, " ") {
if strings.HasSuffix(line, ":") {
currentPkg = strings.TrimSpace(strings.TrimSuffix(line, ":"))
continue
}
if strings.HasPrefix(line, "path:") && currentPkg != "" {
pathVal := strings.TrimSpace(strings.TrimPrefix(line, "path:"))
pathVal = strings.Trim(pathVal, "\"'")
if pathVal != "" {
entries[currentPkg] = pathVal
}
currentPkg = ""
}
}
}
return entries
}

// writeOverrideFile reads existing overrides, merges pubspec.yaml overrides,
// adds new packages, and writes the merged result. Skips write if content unchanged.
func writeOverrideFile(overridePath, rootPath string, newPackages []domain.PlannedWorktree) error {
pubspecPath := filepath.Join(rootPath, "pubspec.yaml")
entries := readExistingOverrides(overridePath)
if entries == nil {
entries = make(map[string]string)
}
entries = mergePubspecOverrides(entries, pubspecPath)
for _, pkg := range newPackages {
rel, err := filepath.Rel(rootPath, pkg.Path)
if err != nil {
rel = pkg.Path
}
entries[pkg.Repo.PackageName] = filepath.ToSlash(rel)
}
newContent := formatOverrideYAML(entries)
existingContent, err := os.ReadFile(overridePath)
if err == nil && string(existingContent) == newContent {
return nil
}
return os.WriteFile(overridePath, []byte(newContent), 0o644)
}

// formatOverrideYAML generates the YAML content from entries map,
// sorted alphabetically for deterministic output.
func formatOverrideYAML(entries map[string]string) string {
if len(entries) == 0 {
return "dependency_overrides:\n {}\n"
}
var sorted []string
for pkg := range entries {
sorted = append(sorted, pkg)
}
sort.Strings(sorted)
lines := []string{"dependency_overrides:"}
for _, pkg := range sorted {
lines = append(lines, " "+pkg+":")
lines = append(lines, " path: "+entries[pkg])
}
return strings.Join(lines, "\n") + "\n"
}
Expand Down
31 changes: 22 additions & 9 deletions internal/ui/add_repo_wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ui
import (
"fmt"
"sort"
"strconv"
"strings"

"github.com/EndersonPro/flutree/internal/domain"
Expand Down Expand Up @@ -308,15 +309,27 @@ func (m addRepoWizardModel) updateReview(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}

func (m addRepoWizardModel) progressLabel() string {
labels := []string{"1.Select repos", "2.Review", "3.Branches"}
for i := range labels {
if i == int(m.step) {
labels[i] = wizardProgressActiveStyle.Render(labels[i])
continue
type stepInfo struct{ num int; label string }
steps := []stepInfo{
{1, "Select repos"},
{2, "Review"},
{3, "Branches"},
}
var out strings.Builder
out.WriteString("\n")
for _, s := range steps {
prefix := "○"
style := wizardProgressIdleStyle
if int(m.step)+1 == s.num {
prefix = "●"
style = wizardProgressActiveStyle
}
out.WriteString(style.Render(prefix+" Step "+strconv.Itoa(s.num)+": "+s.label))
if s.num < len(steps) {
out.WriteString(" ")
}
labels[i] = wizardProgressIdleStyle.Render(labels[i])
}
return "\n" + strings.Join(labels, " ")
return out.String()
}

func (m addRepoWizardModel) selectReposView() string {
Expand All @@ -328,9 +341,9 @@ func (m addRepoWizardModel) selectReposView() string {
if i == m.cursor {
cursor = ">"
}
marker := "[ ]"
marker := ""
if m.selected[i] {
marker = "[x]"
marker = ""
}
b.WriteString(fmt.Sprintf("%s %s %s [%s] (%s)\n", cursor, marker, repo.Name, repo.PackageName, repo.RepoRoot))
}
Expand Down
37 changes: 26 additions & 11 deletions internal/ui/create_wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ui
import (
"fmt"
"sort"
"strconv"
"strings"

"github.com/EndersonPro/flutree/internal/domain"
Expand Down Expand Up @@ -264,9 +265,9 @@ func (m createWizardModel) View() string {
if i == m.pkgCursor {
cursor = ">"
}
marker := "[ ]"
marker := ""
if m.pkgSelected[i] {
marker = "[x]"
marker = ""
}
b.WriteString(fmt.Sprintf("%s %s %s [%s]\n", cursor, marker, pkg.Name, pkg.PackageName))
}
Expand Down Expand Up @@ -522,15 +523,29 @@ func (m *createWizardModel) refreshPackageCandidates(preselected []string) {
}

func (m createWizardModel) progressLabel() string {
labels := []string{"1.Name", "2.Root", "3.Packages", "4.Branches", "5.Review"}
for i := range labels {
if i == int(m.step) {
labels[i] = wizardProgressActiveStyle.Render(labels[i])
continue
type stepInfo struct{ num int; label string }
steps := []stepInfo{
{1, "Name"},
{2, "Root"},
{3, "Packages"},
{4, "Branches"},
{5, "Review"},
}
var out strings.Builder
out.WriteString("\n")
for _, s := range steps {
prefix := "○"
style := wizardProgressIdleStyle
if int(m.step)+1 == s.num {
prefix = "●"
style = wizardProgressActiveStyle
}
out.WriteString(style.Render(prefix+" Step "+strconv.Itoa(s.num)+": "+s.label))
if s.num < len(steps) {
out.WriteString(" ")
}
labels[i] = wizardProgressIdleStyle.Render(labels[i])
}
return "\n" + strings.Join(labels, " ")
return out.String()
}

func (m createWizardModel) branchesView() string {
Expand Down Expand Up @@ -569,7 +584,7 @@ func (m createWizardModel) reviewView() string {
var b strings.Builder
b.WriteString(wizardSectionStyle.Render("Step 5 - Final review"))
b.WriteString("\n")
b.WriteString(renderTable(
b.WriteString(renderStyledTableNoZebra(
[]string{"Setting", "Value"},
[][]string{
{"Workspace", m.name},
Expand All @@ -579,7 +594,7 @@ func (m createWizardModel) reviewView() string {
))
b.WriteString("\n")

b.WriteString(renderTable(
b.WriteString(renderStyledTableNoZebra(
[]string{"Role", "Repository", "Package", "Branch", "Base Branch"},
m.reviewRows(),
))
Expand Down
4 changes: 2 additions & 2 deletions internal/ui/create_wizard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,10 @@ func TestCreateWizardReviewViewUsesEnglishChoices(t *testing.T) {
if !strings.Contains(view, "Apply changes") {
t.Fatalf("expected english apply action in review view, got: %q", view)
}
if !regexp.MustCompile(`\|\s*Role\s*\|\s*Repository\s*\|\s*Package\s*\|\s*Branch\s*\|\s*Base Branch\s*\|`).MatchString(view) {
if !regexp.MustCompile(`[|│]\s*Role\s*[|│]\s*Repository\s*[|│]\s*Package\s*[|│]\s*Branch\s*[|│]\s*Base Branch\s*[|│]`).MatchString(view) {
t.Fatalf("expected review table header with role/branch/base columns, got: %q", view)
}
if !regexp.MustCompile(`\|\s*package\s*\|\s*core\s*\|\s*core\s*\|\s*feature/feature\s*\|\s*develop\s*\|`).MatchString(view) {
if !regexp.MustCompile(`[|│]\s*package\s*[|│]\s*core\s*[|│]\s*core\s*[|│]\s*feature/feature\s*[|│]\s*develop\s*[|│]`).MatchString(view) {
t.Fatalf("expected package detail row in review table, got: %q", view)
}
}
19 changes: 16 additions & 3 deletions internal/ui/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ func terminalWidth() int {
// colors, status-aware cell rendering, zebra striping, and terminal-width
// awareness.
func renderStyledTable(headers []string, rows [][]string, listRows []domain.ListRow) string {
return renderStyledTableWithZebra(headers, rows, listRows, true)
}

// renderStyledTableNoZebra renders a styled table without zebra striping.
// Used by create wizard review step for cleaner visual output.
func renderStyledTableNoZebra(headers []string, rows [][]string) string {
return renderStyledTableWithZebra(headers, rows, nil, false)
}

func renderStyledTableWithZebra(headers []string, rows [][]string, listRows []domain.ListRow, zebra bool) string {
if len(headers) == 0 {
return ""
}
Expand Down Expand Up @@ -59,10 +69,13 @@ func renderStyledTable(headers []string, rows [][]string, listRows []domain.List
if row == table.HeaderRow {
return uiTableHeaderStyle
}
if row%2 == 0 {
return uiTableRowStyle
if zebra {
if row%2 == 0 {
return uiTableRowStyle
}
return uiTableRowAltStyle
}
return uiTableRowAltStyle
return uiTableRowStyle
})

return t.Render()
Expand Down
Loading