Skip to content

Commit cc88fdb

Browse files
committed
refactor: improve code quality
1 parent 39cd4bd commit cc88fdb

8 files changed

Lines changed: 190 additions & 90 deletions

File tree

CLAUDE.md

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ source /path/to/git-workers/shell/gw.sh
6969

7070
## Recent Changes
7171

72+
### v0.3.0 File Copy Feature
73+
74+
- Automatically copy gitignored files (like `.env`) from main worktree to new worktrees
75+
- Configurable via `[files]` section in `.git-workers.toml`
76+
- Security validation to prevent path traversal attacks
77+
- Follows same discovery priority as configuration files
78+
7279
### Branch Option Simplification
7380

7481
- Reduced from 3 options to 2: "Create from current HEAD" and "Select branch (smart mode)"
@@ -79,6 +86,7 @@ source /path/to/git-workers/shell/gw.sh
7986
- **`get_branch_worktree_map()`**: Maps branch names to worktree names, including main worktree detection
8087
- **`list_all_branches()`**: Returns both local and remote branches (remote without "origin/" prefix)
8188
- **`create_worktree_with_new_branch()`**: Creates worktree with new branch from base branch (supports git-flow style workflows)
89+
- **`copy_configured_files()`**: Copies files specified in config to new worktrees
8290

8391
## Architecture
8492

@@ -96,6 +104,7 @@ src/
96104
├── repository_info.rs # Repository information display
97105
├── input_esc_raw.rs # Custom input handling with ESC support
98106
├── constants.rs # Centralized constants (strings, formatting)
107+
├── file_copy.rs # File copy functionality for gitignored files
99108
└── utils.rs # Common utilities (error display, etc.)
100109
```
101110

@@ -194,7 +203,7 @@ Since Git lacks native rename functionality:
194203
1. Current directory (current worktree)
195204
2. Main/master worktree directories (fallback)
196205

197-
## v0.3.0 File Copy Feature (Planning)
206+
## v0.3.0 File Copy Feature (Implemented)
198207

199208
### Overview
200209

@@ -209,15 +218,18 @@ copy = [".env", ".env.local", "config/local.json"]
209218

210219
# Optional: source directory (defaults to main worktree)
211220
# source = "path/to/source"
212-
213-
# Optional: destination directory (defaults to worktree root)
214-
# destination = "path/to/dest"
215221
```
216222

217-
### Implementation Plan
218-
219-
1. **Config Structure**: Add `FilesConfig` struct with `copy`, `source`, and `destination` fields
220-
2. **File Detection**: Find main worktree directory for source files
221-
3. **Copy Logic**: In `post-create` hook phase, copy specified files
222-
4. **Error Handling**: Warn on missing files but don't fail worktree creation
223-
5. **Security**: Validate paths to prevent directory traversal attacks
223+
### Implementation Details
224+
225+
1. **Config Structure**: `FilesConfig` struct with `copy` and `source` fields (destination is always worktree root)
226+
2. **File Detection**: Uses same priority as config file discovery for finding source files
227+
3. **Copy Logic**: Executes after worktree creation but before post-create hooks
228+
4. **Error Handling**: Warns on missing files but continues with worktree creation
229+
5. **Security**: Validates paths to prevent directory traversal attacks
230+
6. **Features**:
231+
- Supports both files and directories
232+
- Recursive directory copying
233+
- Symlink detection with warnings
234+
- Maximum directory depth limit (50 levels)
235+
- Preserves file permissions

src/commands.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1811,8 +1811,12 @@ pub fn edit_hooks() -> Result<()> {
18111811
// Check if we're in a worktree structure like /path/to/repo/branch/worktree-name
18121812
if let Some(parent) = cwd.parent() {
18131813
// Look for main or master directories in the parent
1814-
let main_path = parent.join("main").join(CONFIG_FILE_NAME);
1815-
let master_path = parent.join("master").join(CONFIG_FILE_NAME);
1814+
let main_path = parent
1815+
.join(crate::constants::DEFAULT_BRANCH_MAIN)
1816+
.join(CONFIG_FILE_NAME);
1817+
let master_path = parent
1818+
.join(crate::constants::DEFAULT_BRANCH_MASTER)
1819+
.join(CONFIG_FILE_NAME);
18161820

18171821
if main_path.exists() {
18181822
main_path
@@ -1830,14 +1834,14 @@ pub fn edit_hooks() -> Result<()> {
18301834
let workdir = repo
18311835
.workdir()
18321836
.ok_or_else(|| anyhow::anyhow!("No working directory"))?;
1833-
workdir.join(".git-workers.toml")
1837+
workdir.join(CONFIG_FILE_NAME)
18341838
}
18351839
} else {
18361840
// Can't get current directory, use workdir
18371841
let workdir = repo
18381842
.workdir()
18391843
.ok_or_else(|| anyhow::anyhow!("No working directory"))?;
1840-
workdir.join(".git-workers.toml")
1844+
workdir.join(CONFIG_FILE_NAME)
18411845
}
18421846
} else {
18431847
utils::print_error("Not in a git repository");

src/config.rs

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,12 @@ impl Config {
244244
// For bare repositories:
245245
// Get the default branch name from HEAD
246246
let default_branch = if let Ok(head) = repo.head() {
247-
head.shorthand().unwrap_or("main").to_string()
247+
head.shorthand()
248+
.unwrap_or(crate::constants::DEFAULT_BRANCH_MAIN)
249+
.to_string()
248250
} else {
249251
// Fallback to common default branch names
250-
"main".to_string()
252+
crate::constants::DEFAULT_BRANCH_MAIN.to_string()
251253
};
252254

253255
if let Ok(cwd) = std::env::current_dir() {
@@ -264,17 +266,12 @@ impl Config {
264266
}
265267

266268
// Also check main/master if different from default
267-
if default_branch != "main" {
268-
let main_config = cwd.join("main").join(CONFIG_FILE_NAME);
269-
if main_config.exists() {
270-
return Self::load_from_file(&main_config, repo);
271-
}
272-
}
273-
if default_branch != "master" {
274-
let master_config = cwd.join("master").join(CONFIG_FILE_NAME);
275-
if master_config.exists() {
276-
return Self::load_from_file(&master_config, repo);
277-
}
269+
if let Some(config_path) = crate::utils::find_config_in_default_branches(
270+
&cwd,
271+
&default_branch,
272+
CONFIG_FILE_NAME,
273+
) {
274+
return Self::load_from_file(&config_path, repo);
278275
}
279276

280277
// 2. Try to detect worktree pattern by listing existing worktrees
@@ -312,27 +309,25 @@ impl Config {
312309
}
313310

314311
// Fallback to main/master
315-
if default_branch != "main" {
316-
let main_config =
317-
first_parent.join("main").join(CONFIG_FILE_NAME);
318-
if main_config.exists() {
319-
return Self::load_from_file(&main_config, repo);
320-
}
321-
}
322-
if default_branch != "master" {
323-
let master_config =
324-
first_parent.join("master").join(CONFIG_FILE_NAME);
325-
if master_config.exists() {
326-
return Self::load_from_file(&master_config, repo);
327-
}
312+
if let Some(config_path) =
313+
crate::utils::find_config_in_default_branches(
314+
first_parent,
315+
&default_branch,
316+
CONFIG_FILE_NAME,
317+
)
318+
{
319+
return Self::load_from_file(&config_path, repo);
328320
}
329321
}
330322
}
331323
}
332324
}
333325

334326
// 3. Fallback: Check common subdirectories
335-
for subdir in &["branch", "worktrees"] {
327+
for subdir in &[
328+
crate::constants::BRANCH_SUBDIR,
329+
crate::constants::WORKTREES_SUBDIR,
330+
] {
336331
let branch_path = cwd
337332
.join(subdir)
338333
.join(&default_branch)
@@ -364,7 +359,10 @@ impl Config {
364359

365360
// 2. Then check main/master default branch worktree
366361
// Check if current directory is named "worktrees" or inside a worktree
367-
if cwd.file_name().is_some_and(|n| n == "worktrees") {
362+
if cwd
363+
.file_name()
364+
.is_some_and(|n| n == crate::constants::WORKTREES_SUBDIR)
365+
{
368366
// We're in the worktrees directory itself
369367
if let Some(parent) = cwd.parent() {
370368
let main_config = parent.join(CONFIG_FILE_NAME);
@@ -378,7 +376,10 @@ impl Config {
378376
// This is a linked worktree, find the main/master worktree
379377
if let Some(parent) = cwd.parent() {
380378
// Check if we're in a worktrees subdirectory
381-
if parent.file_name().is_some_and(|n| n == "worktrees") {
379+
if parent
380+
.file_name()
381+
.is_some_and(|n| n == crate::constants::WORKTREES_SUBDIR)
382+
{
382383
// Go up one more level to repository root
383384
if let Some(repo_root) = parent.parent() {
384385
// Check for main worktree
@@ -388,25 +389,32 @@ impl Config {
388389
}
389390

390391
// Also check main/master subdirectories
391-
let main_path = repo_root.join("main").join(CONFIG_FILE_NAME);
392+
let main_path = repo_root
393+
.join(crate::constants::DEFAULT_BRANCH_MAIN)
394+
.join(CONFIG_FILE_NAME);
392395
if main_path.exists() {
393396
return Self::load_from_file(&main_path, repo);
394397
}
395398

396-
let master_path =
397-
repo_root.join("master").join(CONFIG_FILE_NAME);
399+
let master_path = repo_root
400+
.join(crate::constants::DEFAULT_BRANCH_MASTER)
401+
.join(CONFIG_FILE_NAME);
398402
if master_path.exists() {
399403
return Self::load_from_file(&master_path, repo);
400404
}
401405
}
402406
} else {
403407
// Not in worktrees subdirectory, check parent for main/master
404-
let main_path = parent.join("main").join(CONFIG_FILE_NAME);
408+
let main_path = parent
409+
.join(crate::constants::DEFAULT_BRANCH_MAIN)
410+
.join(CONFIG_FILE_NAME);
405411
if main_path.exists() {
406412
return Self::load_from_file(&main_path, repo);
407413
}
408414

409-
let master_path = parent.join("master").join(CONFIG_FILE_NAME);
415+
let master_path = parent
416+
.join(crate::constants::DEFAULT_BRANCH_MASTER)
417+
.join(CONFIG_FILE_NAME);
410418
if master_path.exists() {
411419
return Self::load_from_file(&master_path, repo);
412420
}

src/constants.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ pub const GIT_DEFAULT_MAIN_WORKTREE: &str = "main";
6666

6767
// Directory patterns
6868
pub const WORKTREES_SUBDIR: &str = "worktrees";
69+
pub const BRANCH_SUBDIR: &str = "branch";
70+
71+
// Git branches
72+
pub const DEFAULT_BRANCH_MAIN: &str = "main";
73+
pub const DEFAULT_BRANCH_MASTER: &str = "master";
6974

7075
// Configuration
7176
pub const CONFIG_FILE_NAME: &str = ".git-workers.toml";

src/file_copy.rs

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,10 @@ fn determine_source_directory(
115115
}
116116
}
117117

118-
/// Finds the source directory for file copying
118+
/// Finds the main worktree directory for file copying
119119
///
120-
/// Uses the same priority as .git-workers.toml discovery:
121-
/// - For bare repositories: follows the same search pattern
122-
/// - For non-bare repositories: follows the same search pattern
120+
/// This function follows the same discovery logic as configuration file loading,
121+
/// ensuring consistency between config and file copy operations.
123122
fn find_main_worktree(manager: &GitWorktreeManager) -> Result<PathBuf> {
124123
let repo = manager.repo();
125124

@@ -140,7 +139,7 @@ fn find_source_in_bare_repo(repo: &git2::Repository) -> Result<PathBuf> {
140139
.head()
141140
.ok()
142141
.and_then(|h| h.shorthand().map(String::from))
143-
.unwrap_or_else(|| "main".to_string());
142+
.unwrap_or_else(|| crate::constants::DEFAULT_BRANCH_MAIN.to_string());
144143

145144
if let Ok(cwd) = std::env::current_dir() {
146145
// 1. First check current directory
@@ -155,17 +154,8 @@ fn find_source_in_bare_repo(repo: &git2::Repository) -> Result<PathBuf> {
155154
}
156155

157156
// Also check main/master if different from default
158-
if default_branch != "main" {
159-
let main_dir = cwd.join("main");
160-
if main_dir.exists() && main_dir.is_dir() {
161-
return Ok(main_dir);
162-
}
163-
}
164-
if default_branch != "master" {
165-
let master_dir = cwd.join("master");
166-
if master_dir.exists() && master_dir.is_dir() {
167-
return Ok(master_dir);
168-
}
157+
if let Some(dir) = crate::utils::find_default_branch_directory(&cwd, &default_branch) {
158+
return Ok(dir);
169159
}
170160

171161
// 3. Try to detect worktree pattern
@@ -199,25 +189,22 @@ fn find_source_in_bare_repo(repo: &git2::Repository) -> Result<PathBuf> {
199189
}
200190

201191
// Fallback to main/master
202-
if default_branch != "main" {
203-
let main_dir = first_parent.join("main");
204-
if main_dir.exists() && main_dir.is_dir() {
205-
return Ok(main_dir);
206-
}
207-
}
208-
if default_branch != "master" {
209-
let master_dir = first_parent.join("master");
210-
if master_dir.exists() && master_dir.is_dir() {
211-
return Ok(master_dir);
212-
}
192+
if let Some(dir) = crate::utils::find_default_branch_directory(
193+
first_parent,
194+
&default_branch,
195+
) {
196+
return Ok(dir);
213197
}
214198
}
215199
}
216200
}
217201
}
218202

219203
// 4. Fallback: Check common subdirectories
220-
for subdir in &["branch", "worktrees"] {
204+
for subdir in &[
205+
crate::constants::BRANCH_SUBDIR,
206+
crate::constants::WORKTREES_SUBDIR,
207+
] {
221208
let branch_dir = cwd.join(subdir).join(&default_branch);
222209
if branch_dir.exists() && branch_dir.is_dir() {
223210
return Ok(branch_dir);
@@ -265,32 +252,35 @@ fn find_source_in_regular_repo(repo: &git2::Repository) -> Result<PathBuf> {
265252

266253
// 3. Look for main/master in parent directories
267254
if let Some(parent) = cwd.parent() {
268-
if parent.file_name().is_some_and(|n| n == "worktrees") {
255+
if parent
256+
.file_name()
257+
.is_some_and(|n| n == crate::constants::WORKTREES_SUBDIR)
258+
{
269259
// We're in worktrees subdirectory
270260
if let Some(repo_root) = parent.parent() {
271261
if repo_root.join(".git").is_dir() {
272262
return Ok(repo_root.to_path_buf());
273263
}
274264

275265
// Check main/master subdirectories
276-
let main_dir = repo_root.join("main");
266+
let main_dir = repo_root.join(crate::constants::DEFAULT_BRANCH_MAIN);
277267
if main_dir.exists() && main_dir.is_dir() {
278268
return Ok(main_dir);
279269
}
280270

281-
let master_dir = repo_root.join("master");
271+
let master_dir = repo_root.join(crate::constants::DEFAULT_BRANCH_MASTER);
282272
if master_dir.exists() && master_dir.is_dir() {
283273
return Ok(master_dir);
284274
}
285275
}
286276
} else {
287277
// Check parent for main/master
288-
let main_dir = parent.join("main");
278+
let main_dir = parent.join(crate::constants::DEFAULT_BRANCH_MAIN);
289279
if main_dir.exists() && main_dir.is_dir() {
290280
return Ok(main_dir);
291281
}
292282

293-
let master_dir = parent.join("master");
283+
let master_dir = parent.join(crate::constants::DEFAULT_BRANCH_MASTER);
294284
if master_dir.exists() && master_dir.is_dir() {
295285
return Ok(master_dir);
296286
}

0 commit comments

Comments
 (0)