Skip to content

Commit 8624a51

Browse files
committed
refactor: consolidate hardcoded values and eliminate code redundancy
1 parent fc7a422 commit 8624a51

File tree

8 files changed

+125
-75
lines changed

8 files changed

+125
-75
lines changed

src/commands.rs

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ use std::time::Duration;
2626
use unicode_width::UnicodeWidthStr;
2727

2828
use crate::config::Config;
29-
use crate::constants::{section_header, CONFIG_FILE_NAME, GIT_REMOTE_PREFIX, WORKTREES_SUBDIR};
29+
use crate::constants::{
30+
section_header, CONFIG_FILE_NAME, GIT_CRITICAL_DIRS, GIT_REMOTE_PREFIX, GIT_RESERVED_NAMES,
31+
INVALID_FILESYSTEM_CHARS, MAX_WORKTREE_NAME_LENGTH, WINDOWS_RESERVED_CHARS, WORKTREES_SUBDIR,
32+
};
3033
use crate::file_copy;
3134
use crate::git::{GitWorktreeManager, WorktreeInfo};
3235
use crate::hooks::{self, HookContext};
@@ -1847,20 +1850,19 @@ pub fn validate_worktree_name(name: &str) -> Result<String> {
18471850
}
18481851

18491852
// Check for invalid filesystem characters
1850-
const INVALID_CHARS: &[char] = &['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'];
1851-
if name.chars().any(|c| INVALID_CHARS.contains(&c)) {
1853+
if name.chars().any(|c| INVALID_FILESYSTEM_CHARS.contains(&c)) {
18521854
return Err(anyhow!(
18531855
"Worktree name contains invalid characters: {}",
1854-
INVALID_CHARS.iter().collect::<String>()
1856+
INVALID_FILESYSTEM_CHARS.iter().collect::<String>()
18551857
));
18561858
}
18571859

18581860
// Check for names that could conflict with git internals (case insensitive)
1859-
const RESERVED_NAMES: &[&str] = &[".git", "HEAD", "refs", "hooks", "info", "objects", "logs"];
18601861
let name_lower = name.to_lowercase();
1861-
if RESERVED_NAMES
1862-
.iter()
1863-
.any(|&reserved| reserved.to_lowercase() == name_lower)
1862+
if name_lower == ".git"
1863+
|| GIT_RESERVED_NAMES
1864+
.iter()
1865+
.any(|&reserved| reserved.to_lowercase() == name_lower)
18641866
{
18651867
return Err(anyhow!("Worktree name '{name}' is reserved by git"));
18661868
}
@@ -1896,8 +1898,10 @@ pub fn validate_worktree_name(name: &str) -> Result<String> {
18961898
}
18971899

18981900
// Check for extremely long names
1899-
if name.len() > 255 {
1900-
return Err(anyhow!("Worktree name is too long (max 255 characters)"));
1901+
if name.len() > MAX_WORKTREE_NAME_LENGTH {
1902+
return Err(anyhow!(
1903+
"Worktree name is too long (max {MAX_WORKTREE_NAME_LENGTH} characters)"
1904+
));
19011905
}
19021906

19031907
Ok(name.to_string())
@@ -1981,11 +1985,10 @@ pub fn validate_custom_path(path: &str) -> Result<()> {
19811985
}
19821986

19831987
// Check for Windows reserved characters (for cross-platform compatibility)
1984-
const RESERVED_CHARS: &[char] = &['<', '>', ':', '"', '|', '?', '*'];
1985-
if path.chars().any(|c| RESERVED_CHARS.contains(&c)) {
1988+
if path.chars().any(|c| WINDOWS_RESERVED_CHARS.contains(&c)) {
19861989
return Err(anyhow!(
19871990
"Custom path contains reserved characters: {}",
1988-
RESERVED_CHARS.iter().collect::<String>()
1991+
WINDOWS_RESERVED_CHARS.iter().collect::<String>()
19891992
));
19901993
}
19911994

@@ -2015,15 +2018,34 @@ pub fn validate_custom_path(path: &str) -> Result<()> {
20152018

20162019
// Check for reserved names in path components
20172020
if let Some(name_str) = name.to_str() {
2018-
const RESERVED_NAMES: &[&str] =
2019-
&[".git", "HEAD", "refs", "hooks", "info", "objects", "logs"];
20202021
let name_lower = name_str.to_lowercase();
2021-
if RESERVED_NAMES
2022+
if GIT_RESERVED_NAMES
20222023
.iter()
20232024
.any(|&reserved| reserved.to_lowercase() == name_lower)
20242025
{
20252026
return Err(anyhow!("Path component '{name_str}' is reserved by git"));
20262027
}
2028+
2029+
// Special check for .git paths - only prevent critical Git metadata directories
2030+
if name_str == ".git" {
2031+
let components: Vec<_> = path_obj.components().collect();
2032+
if components.len() > 1 {
2033+
// Check if trying to create in critical Git directories
2034+
if let Some(std::path::Component::Normal(next)) = components.get(1) {
2035+
if let Some(next_str) = next.to_str() {
2036+
// These directories contain critical Git metadata and should not be used for worktrees
2037+
if GIT_CRITICAL_DIRS.contains(&next_str) {
2038+
return Err(anyhow!(
2039+
"Cannot create worktree in .git/{} directory. \
2040+
This directory contains critical Git metadata.",
2041+
next_str
2042+
));
2043+
}
2044+
}
2045+
}
2046+
}
2047+
// Allow other .git subdirectories (like .git/worktrees-custom, etc.)
2048+
}
20272049
}
20282050
}
20292051
std::path::Component::ParentDir => {

src/constants.rs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,6 @@ pub const TIME_FORMAT: &str = "%Y-%m-%d %H:%M";
2626
// Switch Marker
2727
pub const SWITCH_TO_PREFIX: &str = "SWITCH_TO:";
2828

29-
// Common Messages - Not used currently but available for future refactoring
30-
#[allow(dead_code)]
31-
pub const MSG_NO_WORKTREES_FOUND: &str = "• No worktrees found.";
32-
#[allow(dead_code)]
33-
pub const MSG_NO_WORKTREES_TO_SEARCH: &str = "• No worktrees to search.";
34-
#[allow(dead_code)]
35-
pub const MSG_NO_WORKTREES_TO_DELETE: &str = "• No worktrees to delete.";
36-
#[allow(dead_code)]
37-
pub const MSG_NO_WORKTREES_AVAILABLE: &str = "• No worktrees available for deletion.";
38-
#[allow(dead_code)]
39-
pub const MSG_NO_WORKTREES_SELECTED: &str = "• No worktrees selected.";
40-
#[allow(dead_code)]
41-
pub const MSG_NO_WORKTREES_TO_RENAME: &str = "• No worktrees to rename.";
42-
#[allow(dead_code)]
43-
pub const MSG_CURRENT_WORKTREE: &str = "• Already in this worktree.";
44-
#[allow(dead_code)]
45-
pub const MSG_CANNOT_DELETE_CURRENT: &str = " (Cannot delete the current worktree)";
46-
#[allow(dead_code)]
47-
pub const MSG_CANNOT_RENAME_CURRENT: &str = " (Cannot rename the current worktree)";
48-
4929
/// Creates a section header with title and separator
5030
pub fn section_header(title: &str) -> String {
5131
let title_formatted = title.bright_cyan().bold();
@@ -72,3 +52,25 @@ pub const DEFAULT_BRANCH_MASTER: &str = "master";
7252

7353
// Configuration
7454
pub const CONFIG_FILE_NAME: &str = ".git-workers.toml";
55+
56+
// Git internals
57+
pub const GIT_RESERVED_NAMES: &[&str] = &["HEAD", "refs", "hooks", "info", "objects", "logs"];
58+
pub const GIT_CRITICAL_DIRS: &[&str] = &["objects", "refs", "hooks", "info", "logs", "worktrees"];
59+
60+
// Filesystem limits
61+
pub const MAX_WORKTREE_NAME_LENGTH: usize = 255;
62+
pub const MAX_FILE_SIZE_MB: u64 = 100;
63+
64+
// Special characters
65+
pub const INVALID_FILESYSTEM_CHARS: &[char] = &['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'];
66+
pub const WINDOWS_RESERVED_CHARS: &[char] = &['<', '>', ':', '"', '|', '?', '*'];
67+
68+
// Timeouts
69+
pub const STALE_LOCK_TIMEOUT_SECS: u64 = 300; // 5 minutes
70+
71+
// Git constants
72+
pub const COMMIT_ID_SHORT_LENGTH: usize = 8;
73+
pub const LOCK_FILE_NAME: &str = "git-workers-worktree.lock";
74+
75+
// Directory depth limits
76+
pub const MAX_DIRECTORY_DEPTH: usize = 50;

src/file_copy.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::fs;
1010
use std::path::{Path, PathBuf};
1111

1212
use crate::config::FilesConfig;
13-
use crate::constants::WORKTREES_SUBDIR;
13+
use crate::constants::{MAX_DIRECTORY_DEPTH, MAX_FILE_SIZE_MB, WORKTREES_SUBDIR};
1414
use crate::git::{GitWorktreeManager, WorktreeInfo};
1515

1616
/// Copies configured files from source to destination worktree
@@ -232,11 +232,8 @@ fn find_source_in_regular_repo(manager: &GitWorktreeManager) -> Result<PathBuf>
232232
Ok(repo_workdir.to_path_buf())
233233
}
234234

235-
/// Maximum directory recursion depth to prevent infinite loops
236-
const MAX_DIRECTORY_DEPTH: usize = 50;
237-
238-
/// Maximum file size for automatic copying (100MB)
239-
const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024;
235+
/// Maximum file size for automatic copying in bytes
236+
const MAX_FILE_SIZE: u64 = MAX_FILE_SIZE_MB * 1024 * 1024;
240237

241238
/// Calculates the total size of a file or directory
242239
///

src/git.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,13 @@ use std::path::{Path, PathBuf};
4040
use std::time::Duration;
4141

4242
use crate::constants::{
43-
DEFAULT_AUTHOR_UNKNOWN, DEFAULT_BRANCH_DETACHED, DEFAULT_BRANCH_UNKNOWN, DEFAULT_MESSAGE_NONE,
44-
GIT_DEFAULT_MAIN_WORKTREE, GIT_REMOTE_PREFIX, TIME_FORMAT,
43+
COMMIT_ID_SHORT_LENGTH, DEFAULT_AUTHOR_UNKNOWN, DEFAULT_BRANCH_DETACHED,
44+
DEFAULT_BRANCH_UNKNOWN, DEFAULT_MESSAGE_NONE, GIT_DEFAULT_MAIN_WORKTREE, GIT_REMOTE_PREFIX,
45+
LOCK_FILE_NAME, STALE_LOCK_TIMEOUT_SECS, TIME_FORMAT,
4546
};
4647

47-
// Git-specific constants
48-
const COMMIT_ID_SHORT_LENGTH: usize = 8;
49-
50-
// Lock file name used in .git directory
51-
const LOCK_FILE_NAME: &str = "git-workers-worktree.lock";
52-
53-
// Maximum age for a lock file before it's considered stale (5 minutes)
54-
const STALE_LOCK_TIMEOUT: Duration = Duration::from_secs(300);
48+
// Create Duration from constant for stale lock timeout
49+
const STALE_LOCK_TIMEOUT: Duration = Duration::from_secs(STALE_LOCK_TIMEOUT_SECS);
5550

5651
/// Simple lock structure for worktree operations
5752
struct WorktreeLock {

tests/create_worktree_integration_test.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,20 @@ fn test_create_worktree_internal_with_first_pattern() -> Result<()> {
5959

6060
// Verify it's at the same level as the repository
6161
// The worktree should be a sibling to the test-repo directory
62-
let current_dir = std::env::current_dir()?.canonicalize()?;
62+
let current_dir = std::env::current_dir()?;
6363
let repo_parent = current_dir.parent().unwrap();
6464

6565
// Both should have the same parent directory
66+
// Use canonicalize to resolve any symlinks for comparison
67+
let worktree_parent = worktree_path
68+
.canonicalize()?
69+
.parent()
70+
.unwrap()
71+
.to_path_buf();
72+
let expected_parent = repo_parent.canonicalize()?;
73+
6674
assert_eq!(
67-
worktree_path.canonicalize()?.parent().unwrap(),
68-
repo_parent,
75+
worktree_parent, expected_parent,
6976
"Worktree should be at the same level as the repository"
7077
);
7178

tests/integration_test.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ use std::process::{Command, Stdio};
44
#[test]
55
#[ignore = "Interactive test - requires terminal and is time-consuming"]
66
fn test_esc_key_handling() {
7-
// テスト用のgitリポジトリを作成
7+
// Create a git repository for testing
88
let temp_dir = tempfile::tempdir().unwrap();
99
let repo_path = temp_dir.path();
1010

11-
// gitリポジトリを初期化
11+
// Initialize git repository
1212
Command::new("git")
1313
.args(["init"])
1414
.current_dir(repo_path)
1515
.output()
1616
.expect("Failed to init git repo");
1717

18-
// 初期コミットを作成
18+
// Create initial commit
1919
std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
2020
Command::new("git")
2121
.args(["add", "."])
@@ -29,10 +29,10 @@ fn test_esc_key_handling() {
2929
.output()
3030
.expect("Failed to commit");
3131

32-
// バイナリのパスを取得
32+
// Get binary path
3333
let binary_path = std::env::current_dir().unwrap().join("target/debug/gw");
3434

35-
// プロセスを起動
35+
// Start process
3636
let mut child = Command::new(&binary_path)
3737
.current_dir(repo_path)
3838
.stdin(Stdio::piped())
@@ -43,13 +43,13 @@ fn test_esc_key_handling() {
4343

4444
let stdin = child.stdin.as_mut().expect("Failed to get stdin");
4545

46-
// 1. Create worktreeを選択 (4番目の項目)
46+
// 1. Select Create worktree (4th item)
4747
stdin.write_all(b"\x1b[B\x1b[B\x1b[B\r").unwrap(); // 下矢印3回 + Enter
4848

49-
// 2. ESCキーを送信してキャンセル
49+
// 2. Send ESC key to cancel
5050
stdin.write_all(b"\x1b").unwrap(); // ESC
5151

52-
// 3. Exitを選択
52+
// 3. Select Exit
5353
stdin
5454
.write_all(b"\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\r")
5555
.unwrap(); // 下矢印5回 + Enter
@@ -67,7 +67,7 @@ fn test_esc_key_handling() {
6767
println!("STDOUT:\n{}", stdout);
6868
println!("STDERR:\n{}", stderr);
6969

70-
// ESCキーでキャンセルされたことを確認
70+
// Confirm that it was cancelled with ESC key
7171
assert!(stdout.contains("Operation cancelled") || stderr.contains("Operation cancelled"));
7272
}
7373

@@ -77,7 +77,7 @@ fn test_search_esc_handling() {
7777
let temp_dir = tempfile::tempdir().unwrap();
7878
let repo_path = temp_dir.path();
7979

80-
// gitリポジトリを初期化
80+
// Initialize git repository
8181
Command::new("git")
8282
.args(["init"])
8383
.current_dir(repo_path)
@@ -109,13 +109,13 @@ fn test_search_esc_handling() {
109109

110110
let stdin = child.stdin.as_mut().expect("Failed to get stdin");
111111

112-
// 1. Search worktreesを選択 (3番目の項目)
112+
// 1. Select Search worktrees (3rd item)
113113
stdin.write_all(b"\x1b[B\x1b[B\r").unwrap(); // 下矢印2回 + Enter
114114

115-
// 2. ESCキーを送信してキャンセル
115+
// 2. Send ESC key to cancel
116116
stdin.write_all(b"\x1b").unwrap(); // ESC
117117

118-
// 3. Exitを選択
118+
// 3. Select Exit
119119
stdin
120120
.write_all(b"\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\x1b[B\r")
121121
.unwrap(); // 下矢印6回 + Enter
@@ -133,6 +133,6 @@ fn test_search_esc_handling() {
133133
println!("STDOUT:\n{}", stdout);
134134
println!("STDERR:\n{}", stderr);
135135

136-
// ESCキーでキャンセルされたことを確認
136+
// Confirm that it was cancelled with ESC key
137137
assert!(stdout.contains("Search cancelled") || stderr.contains("Search cancelled"));
138138
}

tests/validate_custom_path_test.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::Result;
22
use git_workers::commands::validate_custom_path;
3+
use git_workers::constants::GIT_RESERVED_NAMES;
34

45
#[test]
56
fn test_validate_custom_path_valid_paths() -> Result<()> {
@@ -133,9 +134,7 @@ fn test_validate_custom_path_path_traversal() -> Result<()> {
133134

134135
#[test]
135136
fn test_validate_custom_path_reserved_names() -> Result<()> {
136-
let reserved_names = vec![".git", "HEAD", "refs", "hooks", "info", "objects", "logs"];
137-
138-
for name in reserved_names {
137+
for name in GIT_RESERVED_NAMES {
139138
// Test reserved name as path component
140139
let path = format!("test/{}/worktree", name);
141140
let result = validate_custom_path(&path);
@@ -163,6 +162,33 @@ fn test_validate_custom_path_edge_cases() -> Result<()> {
163162
// Test path that goes up then down (should be valid)
164163
assert!(validate_custom_path("../sibling/worktrees/feature").is_ok());
165164

165+
// Test .git directory - allowed for custom subdirectories but not Git's reserved ones
166+
assert!(validate_custom_path(".git/my-custom-worktrees/feature").is_ok());
167+
assert!(validate_custom_path(".git/feature-branches/test").is_ok());
168+
169+
// But critical Git directories should still be protected
170+
let result = validate_custom_path(".git/objects/test");
171+
assert!(result.is_err());
172+
assert!(result
173+
.unwrap_err()
174+
.to_string()
175+
.contains("critical Git metadata"));
176+
177+
let result = validate_custom_path(".git/refs/worktree");
178+
assert!(result.is_err());
179+
assert!(result
180+
.unwrap_err()
181+
.to_string()
182+
.contains("critical Git metadata"));
183+
184+
// Test that .git/worktrees is protected
185+
let result = validate_custom_path(".git/worktrees/test");
186+
assert!(result.is_err());
187+
assert!(result
188+
.unwrap_err()
189+
.to_string()
190+
.contains("critical Git metadata"));
191+
166192
Ok(())
167193
}
168194

tests/validate_worktree_name_test.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::Result;
22
use git_workers::commands;
3+
use git_workers::constants::MAX_WORKTREE_NAME_LENGTH;
34

45
#[test]
56
fn test_validate_worktree_name_with_ascii() -> Result<()> {
@@ -59,11 +60,11 @@ fn test_validate_worktree_name_reserved() -> Result<()> {
5960
#[test]
6061
fn test_validate_worktree_name_length() -> Result<()> {
6162
// Very long names should fail
62-
let long_name = "a".repeat(256);
63+
let long_name = "a".repeat(MAX_WORKTREE_NAME_LENGTH + 1);
6364
assert!(commands::validate_worktree_name(&long_name).is_err());
6465

65-
// Max length (255) should pass
66-
let max_name = "a".repeat(255);
66+
// Max length should pass
67+
let max_name = "a".repeat(MAX_WORKTREE_NAME_LENGTH);
6768
assert_eq!(commands::validate_worktree_name(&max_name)?, max_name);
6869

6970
Ok(())

0 commit comments

Comments
 (0)