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
9 changes: 3 additions & 6 deletions .specify/memory/roadmap/phase-5-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,10 @@ Post-1.0 enhancements for advanced workflows, improved onboarding, and enterpris
- **Dependencies**: Extract Command
- **Notes**: Supports advanced monorepo scenarios with nested subtree structures

### 6. Extract Flatten Mode (`--flatten` flag)
### 6. ~~Extract Flatten Mode (`--flatten` flag)~~ IMPLEMENTED as `base: match`

- **Purpose & user value**: Strip pattern prefix from extracted paths (e.g., `src/**/*.c` extracts `src/foo.c` to `dest/foo.c` instead of `dest/src/foo.c`)
- **Success metrics**:
- Users can flatten path structure when needed without manual post-processing
- **Dependencies**: Extract Command
- **Notes**: Default behavior (009-multi-pattern-extraction) preserves full relative paths per industry best practices. `--flatten` restores pre-009 behavior for users who prefer prefix stripping. Handles filename conflicts with clear errors.
- **Status**: Implemented
- **Notes**: The `base` field (`root`/`match`) on extraction mappings replaces the proposed `--flatten` flag. `base: match` strips the literal prefix before the first glob/brace character from destination paths. `--base match` CLI flag for ad-hoc use. Default (`base: root` or omitted) preserves full relative paths for backward compatibility.

### 7. Extract Dry-Run Mode (`--dry-run` flag)

Expand Down
332 changes: 246 additions & 86 deletions Sources/SubtreeLib/Commands/ExtractCommand.swift

Large diffs are not rendered by default.

37 changes: 31 additions & 6 deletions Sources/SubtreeLib/Configuration/ExtractionMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@ public struct ExtractionMapping: Equatable, Sendable {

/// Optional array of glob patterns to exclude from matches
public let exclude: [String]?


/// Optional base path mode for controlling destination path structure
/// - "root" (default when nil): preserves full relative path from subtree root
/// - "match": strips literal prefix before first glob/brace character
public let base: String?

// MARK: - CodingKeys

private enum CodingKeys: String, CodingKey {
case from
case to
case exclude
case base
}

// MARK: - Initializers
Expand All @@ -38,10 +44,11 @@ public struct ExtractionMapping: Equatable, Sendable {
/// - from: Single glob pattern matching source files (e.g., "docs/**/*.md")
/// - to: Single destination path for copied files (e.g., "project-docs/")
/// - exclude: Optional array of glob patterns to exclude (e.g., ["docs/internal/**"])
public init(from: String, to: String, exclude: [String]? = nil) {
public init(from: String, to: String, exclude: [String]? = nil, base: String? = nil) {
self.from = [from]
self.to = [to]
self.exclude = exclude
self.base = base
}

/// Initialize an extraction mapping with multiple patterns and single destination
Expand All @@ -61,10 +68,11 @@ public struct ExtractionMapping: Equatable, Sendable {
/// - fromPatterns: Array of glob patterns matching source files (processed as union)
/// - to: Single destination path for copied files (relative to repository root)
/// - exclude: Optional array of glob patterns to exclude (applies to all patterns)
public init(fromPatterns: [String], to: String, exclude: [String]? = nil) {
public init(fromPatterns: [String], to: String, exclude: [String]? = nil, base: String? = nil) {
self.from = fromPatterns
self.to = [to]
self.exclude = exclude
self.base = base
}

/// Initialize an extraction mapping with a single pattern and multiple destinations (012-multi-destination)
Expand All @@ -84,10 +92,11 @@ public struct ExtractionMapping: Equatable, Sendable {
/// - from: Single glob pattern matching source files
/// - toDestinations: Array of destination paths (each receives all matched files)
/// - exclude: Optional array of glob patterns to exclude
public init(from: String, toDestinations: [String], exclude: [String]? = nil) {
public init(from: String, toDestinations: [String], exclude: [String]? = nil, base: String? = nil) {
self.from = [from]
self.to = toDestinations
self.exclude = exclude
self.base = base
}

/// Initialize an extraction mapping with multiple patterns and multiple destinations (012-multi-destination)
Expand All @@ -107,10 +116,11 @@ public struct ExtractionMapping: Equatable, Sendable {
/// - fromPatterns: Array of glob patterns (processed as union)
/// - toDestinations: Array of destination paths (each receives all matched files)
/// - exclude: Optional array of glob patterns to exclude
public init(fromPatterns: [String], toDestinations: [String], exclude: [String]? = nil) {
public init(fromPatterns: [String], toDestinations: [String], exclude: [String]? = nil, base: String? = nil) {
self.from = fromPatterns
self.to = toDestinations
self.exclude = exclude
self.base = base
}
}

Expand Down Expand Up @@ -157,6 +167,20 @@ extension ExtractionMapping: Codable {
}

self.exclude = try container.decodeIfPresent([String].self, forKey: .exclude)

// Decode `base`: optional string, validate allowed values
if let baseValue = try container.decodeIfPresent(String.self, forKey: .base) {
guard baseValue == "root" || baseValue == "match" else {
throw DecodingError.dataCorruptedError(
forKey: .base,
in: container,
debugDescription: "base must be 'root' or 'match', got '\(baseValue)'"
)
}
self.base = baseValue
} else {
self.base = nil
}
}

/// Custom encoder that outputs string for single value, array for multiple
Expand All @@ -178,5 +202,6 @@ extension ExtractionMapping: Codable {
}

try container.encodeIfPresent(exclude, forKey: .exclude)
try container.encodeIfPresent(base, forKey: .base)
}
}
Loading
Loading