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: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- `[worktree]` config section with `shared_cache` and `cache_strategy` for
selective build cache sharing between worktrees. New worktrees can now reuse
`target/`, `node_modules/`, `.venv/`, etc. from the main repo via symlink
(default) or recursive copy, eliminating cold-build cost on `parsec start`
(#207).

## [0.3.3] - 2026-04-22

### Added
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,17 @@ layout = "sibling"
base_dir = ".parsec/workspaces"
branch_prefix = "feature/"

[worktree]
# Directories to share from the main repo into new worktrees so that
# `parsec start` doesn't trigger a cold rebuild. Default is empty (no sharing).
shared_cache = ["target", "node_modules", ".venv"]
# "symlink" (default): fast, zero-disk overhead. All worktrees and the main
# repo share one cache — running parallel builds of the
# same artifact may race.
# "copy": full copy at start time. Each worktree gets an independent cache,
# no race risk, but uses more disk and the initial copy takes time.
cache_strategy = "symlink"

[tracker]
# "jira" | "github" | "gitlab" | "none"
provider = "jira"
Expand Down
19 changes: 19 additions & 0 deletions schema/parsec-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@
},
"additionalProperties": false
},
"worktree": {
"type": "object",
"description": "Build cache sharing for new worktrees",
"properties": {
"shared_cache": {
"type": "array",
"items": { "type": "string" },
"default": [],
"description": "Directories to share from the main repo into new worktrees (e.g. target, node_modules, .venv)"
},
"cache_strategy": {
"type": "string",
"enum": ["symlink", "copy"],
"default": "symlink",
"description": "How to share cache directories: symlink (fast, shared state) or copy (independent state)"
}
},
"additionalProperties": false
},
"tracker": {
"type": "object",
"description": "Issue tracker integration settings",
Expand Down
1 change: 1 addition & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod settings;

pub use settings::CacheStrategy;
pub use settings::ParsecConfig;
pub use settings::TrackerProvider;
pub use settings::WorktreeLayout;
102 changes: 102 additions & 0 deletions src/config/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,44 @@ impl std::fmt::Display for WorktreeLayout {
}
}

// ---------------------------------------------------------------------------
// CacheStrategy
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum CacheStrategy {
#[default]
Symlink,
Copy,
}

impl std::fmt::Display for CacheStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CacheStrategy::Symlink => write!(f, "symlink"),
CacheStrategy::Copy => write!(f, "copy"),
}
}
}

// ---------------------------------------------------------------------------
// WorktreeConfig
// ---------------------------------------------------------------------------

/// Build cache sharing settings for new worktrees.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WorktreeConfig {
/// Directories to share from the main repo into new worktrees
/// (e.g. ["target", "node_modules", ".venv"]).
#[serde(default)]
pub shared_cache: Vec<String>,
/// How to share the directories: symlink (default) or copy.
#[serde(default)]
pub cache_strategy: CacheStrategy,
}

// ---------------------------------------------------------------------------
// WorkspaceConfig
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -356,6 +394,8 @@ pub struct ParsecConfig {
#[serde(default)]
pub workspace: WorkspaceConfig,
#[serde(default)]
pub worktree: WorktreeConfig,
#[serde(default)]
pub tracker: TrackerConfig,
#[serde(default)]
pub ship: ShipConfig,
Expand Down Expand Up @@ -586,3 +626,65 @@ impl ParsecConfig {
Ok(config)
}
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn worktree_config_defaults_when_section_missing() {
let config: ParsecConfig = toml::from_str("").unwrap();
assert!(config.worktree.shared_cache.is_empty());
assert_eq!(config.worktree.cache_strategy, CacheStrategy::Symlink);
}

#[test]
fn worktree_config_parses_full_section() {
let toml_str = r#"
[worktree]
shared_cache = ["target", ".venv"]
cache_strategy = "copy"
"#;
let config: ParsecConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
config.worktree.shared_cache,
vec!["target".to_string(), ".venv".to_string()]
);
assert_eq!(config.worktree.cache_strategy, CacheStrategy::Copy);
}

#[test]
fn worktree_config_partial_fields_take_defaults() {
let toml_str = r#"
[worktree]
shared_cache = ["target"]
"#;
let config: ParsecConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.worktree.shared_cache, vec!["target".to_string()]);
assert_eq!(config.worktree.cache_strategy, CacheStrategy::Symlink);
}

#[test]
fn worktree_config_unknown_strategy_is_error() {
let toml_str = r#"
[worktree]
cache_strategy = "hardlink"
"#;
let err = toml::from_str::<ParsecConfig>(toml_str).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("hardlink") || msg.to_lowercase().contains("variant"),
"expected error to mention bad variant, got: {msg}"
);
}

#[test]
fn cache_strategy_symlink_is_default() {
let strategy = CacheStrategy::default();
assert_eq!(strategy, CacheStrategy::Symlink);
}
}
Loading
Loading