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: 9 additions & 5 deletions .specify/memory/roadmap.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Product Roadmap: Subtree CLI

**Version:** v1.5.0
**Last Updated:** 2025-11-27
**Version:** v1.7.0
**Last Updated:** 2025-11-30

## Vision & Goals

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

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

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

## Global Risks & Assumptions

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

## Change Log

- **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)
- **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)
- **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)
- **v1.4.0** (2025-10-29): Phase 2 complete — Remove Command delivered with idempotent behavior (191 tests passing)
- **v1.3.0** (2025-10-28): Phase 2 progress — Add Command and Update Command marked complete
Expand Down
47 changes: 41 additions & 6 deletions .specify/memory/roadmap/phase-3-advanced-operations.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Phase 3 — Advanced Operations & Safety

**Status:** ACTIVE
**Last Updated:** 2025-11-29
**Last Updated:** 2025-11-30

## Goal

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

### 5. Lint Command ⏳ PLANNED
### 5. Brace Expansion: Embedded Path Separators ✅ COMPLETE

- **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
- **Success metrics**:
- Patterns like `Sources/{A,B/C}.swift` correctly match files at different directory depths
- Multiple brace groups expand as cartesian product (bash behavior)
- 100% backward compatible with existing patterns
- **Dependencies**: Multi-Pattern Extraction, Extract Command (existing GlobMatcher)
- **Notes**:
- GlobMatcher already supports basic `{a,b}` for extensions; this adds pre-expansion for path separators
- Pre-expansion at CLI level via `BraceExpander` utility (bash semantics)
- Only applies to `--from` and `--exclude` patterns (`--to` is destination path, not glob)
- Example: `Sources/Crypto/{PrettyBytes,SecureBytes,BoringSSL/RNG_boring}.swift` → 3 patterns
- Nested braces, escaping, numeric ranges deferred to backlog
- **Delivered**: All 4 user stories (basic expansion, multiple groups, pass-through, empty alternative errors), 526 tests passing

### 6. Multi-Destination Extraction (Fan-Out) ⏳ PLANNED

- **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
- **Success metrics**:
- Multiple `--to` flags supported in single command
- Each matched file copied to every `--to` destination (fan-out)
- `--from` and `--to` counts independent (no positional pairing)
- Works with all existing extract modes (ad-hoc, bulk, clean)
- **Dependencies**: Multi-Pattern Extraction
- **Notes**:
- Fan-out semantics: N files × M destinations = N×M copy operations
- Directory structure preserved at each destination
- YAML schema: `to: ["path1/", "path2/"]` for persisted mappings
- Atomic per-destination: all files to one destination succeed or fail together

### 7. Lint Command ⏳ PLANNED

- **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
- **Success metrics**:
Expand All @@ -84,17 +115,19 @@ Enable portable configuration validation, selective file extraction with compreh
2. Extract Command ✅
3. Multi-Pattern Extraction ✅
4. Extract Clean Mode ✅
5. Lint Command ⏳ (final Phase 3 feature)
- **Rationale**: Lint command validates all previous operations and completes Phase 3
5. Brace Expansion in Patterns ✅
6. Multi-Destination Extraction ⏳
7. Lint Command ⏳ (final Phase 3 feature)
- **Rationale**: Brace Expansion and Multi-Destination extend pattern capabilities before Lint validates all operations
- **Cross-phase dependencies**: Requires Phase 2 Add Command for subtrees to exist

## Phase-Specific Metrics & Success Criteria

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

## Risks & Assumptions

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

## Phase Notes

- 2025-11-30: Brace Expansion complete (011-brace-expansion) with 526 tests; 4 user stories delivered
- 2025-11-29: Added Brace Expansion and Multi-Destination Extraction features
- 2025-11-29: Extract Clean Mode complete (010-extract-clean) with 477 tests; dry-run/preview mode deferred to Phase 5 backlog
- 2025-11-27: Added Multi-Pattern Extraction and Extract Clean Mode features before Lint Command
- 2025-10-29: Case-Insensitive Names added to Phase 3
Expand Down
29 changes: 28 additions & 1 deletion .specify/memory/roadmap/phase-5-backlog.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Phase 5 — Future Features (Backlog)

**Status:** FUTURE
**Last Updated:** 2025-11-27
**Last Updated:** 2025-11-29

## Goal

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

### 11. Brace Expansion: Backslash Escaping

- **Purpose & user value**: Allow users to escape literal braces in patterns using `\{` and `\}` (bash-style escaping)
- **Success metrics**:
- Users can match files with literal `{` or `}` characters in names
- `path/\{literal\}.txt` matches file named `{literal}.txt`
- **Dependencies**: Brace Expansion (011)
- **Notes**: MVP workaround is character class syntax `[{]` and `[}]`. Backslash escaping provides more intuitive syntax.

### 12. Brace Expansion: Nested Braces

- **Purpose & user value**: Support nested brace patterns like `{a,{b,c}}` expanding to `a`, `b`, `c`
- **Success metrics**:
- Nested braces expand recursively matching bash behavior
- **Dependencies**: Brace Expansion (011)
- **Notes**: Adds complexity; evaluate user demand before implementing

### 13. Brace Expansion: Numeric Ranges

- **Purpose & user value**: Support numeric range patterns like `{1..10}` expanding to `1`, `2`, ..., `10`
- **Success metrics**:
- Numeric ranges expand to sequential numbers
- Supports zero-padding `{01..10}` → `01`, `02`, ..., `10`
- **Dependencies**: Brace Expansion (011)
- **Notes**: Bash feature; useful for numbered files but lower priority than core expansion

## Dependencies & Sequencing

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

## Phase Notes

- 2025-11-29: Added Brace Expansion deferred features (backslash escaping, nested braces, numeric ranges)
- 2025-11-27: Initial backlog created from roadmap refactor
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ subtree extract --name mylib \
--from "src/**/*.c" \
--to vendor/

# Brace expansion (011) - compact patterns with {alternatives}
subtree extract --name mylib --from "*.{h,c,cpp}" --to Sources/
subtree extract --name mylib --from "{src,test}/*.swift" --to Sources/

# Brace expansion with embedded path separators (different directory depths)
subtree extract --name crypto-lib \
--from "Sources/{PrettyBytes,SecureBytes,BoringSSL/RNG}.swift" \
--to Crypto/

# With exclusions (applies to all patterns)
subtree extract --name mylib --from "src/**/*.c" --to Sources/ --exclude "**/test/**"

Expand Down
68 changes: 60 additions & 8 deletions Sources/SubtreeLib/Commands/ExtractCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,17 +220,23 @@ public struct ExtractCommand: AsyncParsableCommand {
// T069: Destination path validation
let normalizedDest = try validateDestination(destinationValue, gitRoot: gitRoot)

// T039: Expand brace patterns in --from before matching (011-brace-expansion)
let expandedFromPatterns = expandBracePatterns(from)

// T040: Expand brace patterns in --exclude before matching (011-brace-expansion)
let expandedExcludePatterns = expandBracePatterns(exclude)

// T023-T025 + T040: Multi-pattern matching with deduplication and per-pattern tracking
// Process all --from patterns and collect unique files
var allMatchedFiles: [(sourcePath: String, relativePath: String)] = []
var seenPaths = Set<String>() // T024: Deduplicate by relative path
var patternMatchCounts: [(pattern: String, count: Int)] = [] // T040: Per-pattern tracking

for pattern in from {
for pattern in expandedFromPatterns {
let matchedFiles = try await findMatchingFiles(
in: subtree.prefix,
pattern: pattern,
excludePatterns: exclude,
excludePatterns: expandedExcludePatterns,
gitRoot: gitRoot
)

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

// 011-brace-expansion: Expand brace patterns before matching
let expandedFromPatterns = expandBracePatterns(mapping.from)
let expandedExcludePatterns = expandBracePatterns(mapping.exclude ?? [])

// Find files to clean
let filesToClean = try await findFilesToClean(
patterns: mapping.from,
excludePatterns: mapping.exclude ?? [],
patterns: expandedFromPatterns,
excludePatterns: expandedExcludePatterns,
subtreePrefix: subtree.prefix,
destinationPath: fullDestPath,
gitRoot: gitRoot
Expand Down Expand Up @@ -664,10 +674,14 @@ public struct ExtractCommand: AsyncParsableCommand {
let normalizedDest = try validateDestination(destinationValue, gitRoot: gitRoot)
let fullDestPath = gitRoot + "/" + normalizedDest

// 011-brace-expansion: Expand brace patterns before matching
let expandedFromPatterns = expandBracePatterns(from)
let expandedExcludePatterns = expandBracePatterns(exclude)

// T025: Find files to clean in destination
let filesToClean = try await findFilesToClean(
patterns: from,
excludePatterns: exclude,
patterns: expandedFromPatterns,
excludePatterns: expandedExcludePatterns,
subtreePrefix: subtree.prefix,
destinationPath: fullDestPath,
gitRoot: gitRoot
Expand Down Expand Up @@ -828,15 +842,19 @@ public struct ExtractCommand: AsyncParsableCommand {
// Validate destination
let normalizedDest = try validateDestination(mapping.to, gitRoot: gitRoot)

// 011-brace-expansion: Expand brace patterns before matching
let expandedFromPatterns = expandBracePatterns(mapping.from)
let expandedExcludePatterns = expandBracePatterns(mapping.exclude ?? [])

// T026: Find matching files from ALL patterns (multi-pattern support)
var allMatchedFiles: [(sourcePath: String, relativePath: String)] = []
var seenPaths = Set<String>() // Deduplicate by relative path

for pattern in mapping.from {
for pattern in expandedFromPatterns {
let matchedFiles = try await findMatchingFiles(
in: subtree.prefix,
pattern: pattern,
excludePatterns: mapping.exclude ?? [],
excludePatterns: expandedExcludePatterns,
gitRoot: gitRoot
)

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

// MARK: - T038: Brace Expansion Helper (011-brace-expansion)

/// Expand brace patterns in a list of patterns
///
/// Applies `BraceExpander` to each pattern, handling errors with user-friendly messages.
/// Returns flattened array of all expanded patterns.
///
/// - Parameter patterns: Array of patterns potentially containing braces
/// - Returns: Array of expanded patterns
/// - Throws: Never (exits with error code on failure)
private func expandBracePatterns(_ patterns: [String]) -> [String] {
var expandedPatterns: [String] = []

for pattern in patterns {
do {
let expanded = try BraceExpander.expand(pattern)
expandedPatterns.append(contentsOf: expanded)
} catch BraceExpanderError.emptyAlternative(let invalidPattern) {
// T041: User-friendly error message
writeStderr("❌ Error: Invalid brace pattern '\(invalidPattern)'\n")
writeStderr(" Empty alternatives like {a,} or {,b} are not supported.\n\n")
writeStderr("Suggestions:\n")
writeStderr(" • Remove trailing/leading commas: {a,b} instead of {a,}\n")
writeStderr(" • Use separate --from flags for different patterns\n")
Foundation.exit(1)
} catch {
writeStderr("❌ Error: Failed to expand pattern '\(pattern)': \(error)\n")
Foundation.exit(1)
}
}

return expandedPatterns
}

// MARK: - T070: Glob Pattern Matching

/// Find all files matching the glob pattern
Expand Down
Loading