Skip to content

Commit bfc1252

Browse files
authored
feat: update brace expansion (#7)
Updated roadmap and Phase 3 documentation to mark 011-brace-expansion as complete with 526 tests passing. Added Multi-Destination Extraction as next Phase 3 feature. Moved advanced brace expansion features (backslash escaping, nested braces, numeric ranges) to Phase 5 backlog.
1 parent a5493c3 commit bfc1252

File tree

17 files changed

+1884
-24
lines changed

17 files changed

+1884
-24
lines changed

.specify/memory/roadmap.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Product Roadmap: Subtree CLI
22

3-
**Version:** v1.5.0
4-
**Last Updated:** 2025-11-27
3+
**Version:** v1.7.0
4+
**Last Updated:** 2025-11-30
55

66
## Vision & Goals
77

@@ -31,8 +31,10 @@ Simplify git subtree management through declarative YAML configuration with safe
3131

3232
- ✅ Case-Insensitive Names & Validation
3333
- ✅ Extract Command (5 user stories, 411 tests)
34-
-**Multi-Pattern Extraction** — Multiple `--from` patterns in single extraction
35-
-**Extract Clean Mode**`--clean` flag to remove extracted files safely
34+
- ✅ Multi-Pattern Extraction (5 user stories, 439 tests)
35+
- ✅ Extract Clean Mode (5 user stories, 477 tests)
36+
-**Brace Expansion: Embedded Path Separators** (4 user stories, 526 tests)
37+
-**Multi-Destination Extraction** — Fan-out to multiple `--to` paths
3638
- ⏳ Lint Command — Configuration integrity validation
3739

3840
## Product-Level Metrics & Success Criteria
@@ -62,7 +64,7 @@ Simplify git subtree management through declarative YAML configuration with safe
6264
1. **Phase 1 → Phase 2**: Core operations depend on config foundation
6365
2. **Phase 2 → Phase 3**: Extract and Lint require subtrees to exist (Add command)
6466
3. **Phase 3 → Phase 4**: Packaging requires all commands feature-complete
65-
4. **Multi-Pattern → Clean Mode**: Clean mode benefits from array pattern support
67+
4. **Multi-Pattern → Brace Expansion → Multi-Destination → Lint**: Pattern enhancements before validation
6668

6769
## Global Risks & Assumptions
6870

@@ -78,6 +80,8 @@ Simplify git subtree management through declarative YAML configuration with safe
7880

7981
## Change Log
8082

83+
- **v1.7.0** (2025-11-30): Brace Expansion complete (011-brace-expansion) with 526 tests; embedded path separators, cartesian product, bash pass-through semantics (MINOR — feature complete)
84+
- **v1.6.0** (2025-11-29): Added Brace Expansion and Multi-Destination Extraction to Phase 3; marked Multi-Pattern Extraction and Extract Clean Mode complete (MINOR — new features)
8185
- **v1.5.0** (2025-11-27): Roadmap refactored to multi-file structure; added Multi-Pattern Extraction and Extract Clean Mode to Phase 3 (MINOR — new features, structural improvement)
8286
- **v1.4.0** (2025-10-29): Phase 2 complete — Remove Command delivered with idempotent behavior (191 tests passing)
8387
- **v1.3.0** (2025-10-28): Phase 2 progress — Add Command and Update Command marked complete

.specify/memory/roadmap/phase-3-advanced-operations.md

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Phase 3 — Advanced Operations & Safety
22

33
**Status:** ACTIVE
4-
**Last Updated:** 2025-11-29
4+
**Last Updated:** 2025-11-30
55

66
## Goal
77

@@ -66,7 +66,38 @@ Enable portable configuration validation, selective file extraction with compreh
6666
- Continue-on-error for bulk operations with failure summary
6767
- **Delivered**: All 5 user stories (ad-hoc clean, force override, bulk clean, multi-pattern, error handling), 477 tests passing
6868

69-
### 5. Lint Command ⏳ PLANNED
69+
### 5. Brace Expansion: Embedded Path Separators ✅ COMPLETE
70+
71+
- **Purpose & user value**: Extends existing brace expansion (`*.{h,c}`) to support embedded path separators (e.g., `Sources/{A,B/C}.swift`), enabling extraction from directories at different depths with a single pattern
72+
- **Success metrics**:
73+
- Patterns like `Sources/{A,B/C}.swift` correctly match files at different directory depths
74+
- Multiple brace groups expand as cartesian product (bash behavior)
75+
- 100% backward compatible with existing patterns
76+
- **Dependencies**: Multi-Pattern Extraction, Extract Command (existing GlobMatcher)
77+
- **Notes**:
78+
- GlobMatcher already supports basic `{a,b}` for extensions; this adds pre-expansion for path separators
79+
- Pre-expansion at CLI level via `BraceExpander` utility (bash semantics)
80+
- Only applies to `--from` and `--exclude` patterns (`--to` is destination path, not glob)
81+
- Example: `Sources/Crypto/{PrettyBytes,SecureBytes,BoringSSL/RNG_boring}.swift` → 3 patterns
82+
- Nested braces, escaping, numeric ranges deferred to backlog
83+
- **Delivered**: All 4 user stories (basic expansion, multiple groups, pass-through, empty alternative errors), 526 tests passing
84+
85+
### 6. Multi-Destination Extraction (Fan-Out) ⏳ PLANNED
86+
87+
- **Purpose & user value**: Allows extracting matched files to multiple destinations simultaneously (e.g., `--to Lib/ --to Vendor/`), enabling distribution of extracted files to multiple locations without repeated commands
88+
- **Success metrics**:
89+
- Multiple `--to` flags supported in single command
90+
- Each matched file copied to every `--to` destination (fan-out)
91+
- `--from` and `--to` counts independent (no positional pairing)
92+
- Works with all existing extract modes (ad-hoc, bulk, clean)
93+
- **Dependencies**: Multi-Pattern Extraction
94+
- **Notes**:
95+
- Fan-out semantics: N files × M destinations = N×M copy operations
96+
- Directory structure preserved at each destination
97+
- YAML schema: `to: ["path1/", "path2/"]` for persisted mappings
98+
- Atomic per-destination: all files to one destination succeed or fail together
99+
100+
### 7. Lint Command ⏳ PLANNED
70101

71102
- **Purpose & user value**: Validates subtree integrity and synchronization state offline and with remote checks, enabling users to detect configuration drift, missing subtrees, or desync between config and repository state
72103
- **Success metrics**:
@@ -84,17 +115,19 @@ Enable portable configuration validation, selective file extraction with compreh
84115
2. Extract Command ✅
85116
3. Multi-Pattern Extraction ✅
86117
4. Extract Clean Mode ✅
87-
5. Lint Command ⏳ (final Phase 3 feature)
88-
- **Rationale**: Lint command validates all previous operations and completes Phase 3
118+
5. Brace Expansion in Patterns ✅
119+
6. Multi-Destination Extraction ⏳
120+
7. Lint Command ⏳ (final Phase 3 feature)
121+
- **Rationale**: Brace Expansion and Multi-Destination extend pattern capabilities before Lint validates all operations
89122
- **Cross-phase dependencies**: Requires Phase 2 Add Command for subtrees to exist
90123

91124
## Phase-Specific Metrics & Success Criteria
92125

93126
This phase is successful when:
94-
- All five features complete and tested
127+
- All seven features complete and tested
95128
- Extract supports multiple patterns and cleanup operations
96129
- Lint provides comprehensive integrity validation
97-
- 475+ tests pass on macOS and Ubuntu
130+
- 600+ tests pass on macOS and Ubuntu (currently 526, growing)
98131

99132
## Risks & Assumptions
100133

@@ -105,6 +138,8 @@ This phase is successful when:
105138

106139
## Phase Notes
107140

141+
- 2025-11-30: Brace Expansion complete (011-brace-expansion) with 526 tests; 4 user stories delivered
142+
- 2025-11-29: Added Brace Expansion and Multi-Destination Extraction features
108143
- 2025-11-29: Extract Clean Mode complete (010-extract-clean) with 477 tests; dry-run/preview mode deferred to Phase 5 backlog
109144
- 2025-11-27: Added Multi-Pattern Extraction and Extract Clean Mode features before Lint Command
110145
- 2025-10-29: Case-Insensitive Names added to Phase 3

.specify/memory/roadmap/phase-5-backlog.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Phase 5 — Future Features (Backlog)
22

33
**Status:** FUTURE
4-
**Last Updated:** 2025-11-27
4+
**Last Updated:** 2025-11-29
55

66
## Goal
77

@@ -91,6 +91,32 @@ Post-1.0 enhancements for advanced workflows, improved onboarding, and enterpris
9191
- **Dependencies**: Update Command
9292
- **Notes**: Configurable retry count, distinguishes transient from permanent failures
9393

94+
### 11. Brace Expansion: Backslash Escaping
95+
96+
- **Purpose & user value**: Allow users to escape literal braces in patterns using `\{` and `\}` (bash-style escaping)
97+
- **Success metrics**:
98+
- Users can match files with literal `{` or `}` characters in names
99+
- `path/\{literal\}.txt` matches file named `{literal}.txt`
100+
- **Dependencies**: Brace Expansion (011)
101+
- **Notes**: MVP workaround is character class syntax `[{]` and `[}]`. Backslash escaping provides more intuitive syntax.
102+
103+
### 12. Brace Expansion: Nested Braces
104+
105+
- **Purpose & user value**: Support nested brace patterns like `{a,{b,c}}` expanding to `a`, `b`, `c`
106+
- **Success metrics**:
107+
- Nested braces expand recursively matching bash behavior
108+
- **Dependencies**: Brace Expansion (011)
109+
- **Notes**: Adds complexity; evaluate user demand before implementing
110+
111+
### 13. Brace Expansion: Numeric Ranges
112+
113+
- **Purpose & user value**: Support numeric range patterns like `{1..10}` expanding to `1`, `2`, ..., `10`
114+
- **Success metrics**:
115+
- Numeric ranges expand to sequential numbers
116+
- Supports zero-padding `{01..10}``01`, `02`, ..., `10`
117+
- **Dependencies**: Brace Expansion (011)
118+
- **Notes**: Bash feature; useful for numbered files but lower priority than core expansion
119+
94120
## Dependencies & Sequencing
95121

96122
- Features are independent and can be prioritized based on user demand
@@ -112,4 +138,5 @@ This phase is successful when:
112138

113139
## Phase Notes
114140

141+
- 2025-11-29: Added Brace Expansion deferred features (backslash escaping, nested braces, numeric ranges)
115142
- 2025-11-27: Initial backlog created from roadmap refactor

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,15 @@ subtree extract --name mylib \
165165
--from "src/**/*.c" \
166166
--to vendor/
167167

168+
# Brace expansion (011) - compact patterns with {alternatives}
169+
subtree extract --name mylib --from "*.{h,c,cpp}" --to Sources/
170+
subtree extract --name mylib --from "{src,test}/*.swift" --to Sources/
171+
172+
# Brace expansion with embedded path separators (different directory depths)
173+
subtree extract --name crypto-lib \
174+
--from "Sources/{PrettyBytes,SecureBytes,BoringSSL/RNG}.swift" \
175+
--to Crypto/
176+
168177
# With exclusions (applies to all patterns)
169178
subtree extract --name mylib --from "src/**/*.c" --to Sources/ --exclude "**/test/**"
170179

Sources/SubtreeLib/Commands/ExtractCommand.swift

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,17 +220,23 @@ public struct ExtractCommand: AsyncParsableCommand {
220220
// T069: Destination path validation
221221
let normalizedDest = try validateDestination(destinationValue, gitRoot: gitRoot)
222222

223+
// T039: Expand brace patterns in --from before matching (011-brace-expansion)
224+
let expandedFromPatterns = expandBracePatterns(from)
225+
226+
// T040: Expand brace patterns in --exclude before matching (011-brace-expansion)
227+
let expandedExcludePatterns = expandBracePatterns(exclude)
228+
223229
// T023-T025 + T040: Multi-pattern matching with deduplication and per-pattern tracking
224230
// Process all --from patterns and collect unique files
225231
var allMatchedFiles: [(sourcePath: String, relativePath: String)] = []
226232
var seenPaths = Set<String>() // T024: Deduplicate by relative path
227233
var patternMatchCounts: [(pattern: String, count: Int)] = [] // T040: Per-pattern tracking
228234

229-
for pattern in from {
235+
for pattern in expandedFromPatterns {
230236
let matchedFiles = try await findMatchingFiles(
231237
in: subtree.prefix,
232238
pattern: pattern,
233-
excludePatterns: exclude,
239+
excludePatterns: expandedExcludePatterns,
234240
gitRoot: gitRoot
235241
)
236242

@@ -569,10 +575,14 @@ public struct ExtractCommand: AsyncParsableCommand {
569575
let normalizedDest = try validateDestination(mapping.to, gitRoot: gitRoot)
570576
let fullDestPath = gitRoot + "/" + normalizedDest
571577

578+
// 011-brace-expansion: Expand brace patterns before matching
579+
let expandedFromPatterns = expandBracePatterns(mapping.from)
580+
let expandedExcludePatterns = expandBracePatterns(mapping.exclude ?? [])
581+
572582
// Find files to clean
573583
let filesToClean = try await findFilesToClean(
574-
patterns: mapping.from,
575-
excludePatterns: mapping.exclude ?? [],
584+
patterns: expandedFromPatterns,
585+
excludePatterns: expandedExcludePatterns,
576586
subtreePrefix: subtree.prefix,
577587
destinationPath: fullDestPath,
578588
gitRoot: gitRoot
@@ -664,10 +674,14 @@ public struct ExtractCommand: AsyncParsableCommand {
664674
let normalizedDest = try validateDestination(destinationValue, gitRoot: gitRoot)
665675
let fullDestPath = gitRoot + "/" + normalizedDest
666676

677+
// 011-brace-expansion: Expand brace patterns before matching
678+
let expandedFromPatterns = expandBracePatterns(from)
679+
let expandedExcludePatterns = expandBracePatterns(exclude)
680+
667681
// T025: Find files to clean in destination
668682
let filesToClean = try await findFilesToClean(
669-
patterns: from,
670-
excludePatterns: exclude,
683+
patterns: expandedFromPatterns,
684+
excludePatterns: expandedExcludePatterns,
671685
subtreePrefix: subtree.prefix,
672686
destinationPath: fullDestPath,
673687
gitRoot: gitRoot
@@ -828,15 +842,19 @@ public struct ExtractCommand: AsyncParsableCommand {
828842
// Validate destination
829843
let normalizedDest = try validateDestination(mapping.to, gitRoot: gitRoot)
830844

845+
// 011-brace-expansion: Expand brace patterns before matching
846+
let expandedFromPatterns = expandBracePatterns(mapping.from)
847+
let expandedExcludePatterns = expandBracePatterns(mapping.exclude ?? [])
848+
831849
// T026: Find matching files from ALL patterns (multi-pattern support)
832850
var allMatchedFiles: [(sourcePath: String, relativePath: String)] = []
833851
var seenPaths = Set<String>() // Deduplicate by relative path
834852

835-
for pattern in mapping.from {
853+
for pattern in expandedFromPatterns {
836854
let matchedFiles = try await findMatchingFiles(
837855
in: subtree.prefix,
838856
pattern: pattern,
839-
excludePatterns: mapping.exclude ?? [],
857+
excludePatterns: expandedExcludePatterns,
840858
gitRoot: gitRoot
841859
)
842860

@@ -982,6 +1000,40 @@ public struct ExtractCommand: AsyncParsableCommand {
9821000
return trimmed.hasSuffix("/") ? String(trimmed.dropLast()) : trimmed
9831001
}
9841002

1003+
// MARK: - T038: Brace Expansion Helper (011-brace-expansion)
1004+
1005+
/// Expand brace patterns in a list of patterns
1006+
///
1007+
/// Applies `BraceExpander` to each pattern, handling errors with user-friendly messages.
1008+
/// Returns flattened array of all expanded patterns.
1009+
///
1010+
/// - Parameter patterns: Array of patterns potentially containing braces
1011+
/// - Returns: Array of expanded patterns
1012+
/// - Throws: Never (exits with error code on failure)
1013+
private func expandBracePatterns(_ patterns: [String]) -> [String] {
1014+
var expandedPatterns: [String] = []
1015+
1016+
for pattern in patterns {
1017+
do {
1018+
let expanded = try BraceExpander.expand(pattern)
1019+
expandedPatterns.append(contentsOf: expanded)
1020+
} catch BraceExpanderError.emptyAlternative(let invalidPattern) {
1021+
// T041: User-friendly error message
1022+
writeStderr("❌ Error: Invalid brace pattern '\(invalidPattern)'\n")
1023+
writeStderr(" Empty alternatives like {a,} or {,b} are not supported.\n\n")
1024+
writeStderr("Suggestions:\n")
1025+
writeStderr(" • Remove trailing/leading commas: {a,b} instead of {a,}\n")
1026+
writeStderr(" • Use separate --from flags for different patterns\n")
1027+
Foundation.exit(1)
1028+
} catch {
1029+
writeStderr("❌ Error: Failed to expand pattern '\(pattern)': \(error)\n")
1030+
Foundation.exit(1)
1031+
}
1032+
}
1033+
1034+
return expandedPatterns
1035+
}
1036+
9851037
// MARK: - T070: Glob Pattern Matching
9861038

9871039
/// Find all files matching the glob pattern

0 commit comments

Comments
 (0)