@@ -395,26 +395,36 @@ fn search_worktrees_internal(manager: &GitWorktreeManager) -> Result<bool> {
395395/// 2. **Location Selection** (first worktree only):
396396/// - Same level as repository: `../worktree-name`
397397/// - In subdirectory (recommended): `../repo/worktrees/worktree-name`
398+ /// - Custom path: User-specified relative path (e.g., `../custom/path`)
398399/// 3. **Branch Selection**:
399400/// - Create from current HEAD
400401/// - Create from existing branch (shows branch list)
401402/// 4. **Creation**: Creates the worktree with progress indication
402- /// 5. **Post-create Hooks**: Executes any configured post-create hooks
403- /// 6. **Switch Option**: Asks if user wants to switch to the new worktree
403+ /// 5. **File Copy**: Copies configured files (e.g., `.env`) from main worktree
404+ /// 6. **Post-create Hooks**: Executes any configured post-create hooks
405+ /// 7. **Switch Option**: Asks if user wants to switch to the new worktree
404406///
405407/// # Worktree Patterns
406408///
407409/// The first worktree establishes the pattern for subsequent worktrees.
408410/// If the first worktree is created at the same level as the repository,
409- /// all future worktrees follow that pattern.
411+ /// all future worktrees follow that pattern. The custom path option allows
412+ /// breaking this pattern for special cases.
410413///
411414/// # Path Handling
412415///
413416/// - "Same level" paths (`../name`) are canonicalized for clean display without `..`
414417/// - "In subdirectory" paths create worktrees in `worktrees/` folder within the repository
418+ /// - "Custom path" allows any relative path, validated for security and compatibility
415419/// - All paths are resolved to absolute canonical paths before creation
416420/// - Parent directories are created automatically if needed
417421///
422+ /// # Custom Path Examples
423+ ///
424+ /// - `../experiments/feature-x` - Organize experiments separately
425+ /// - `temp/quick-fix` - Temporary worktrees in project
426+ /// - `../../shared-worktrees/project-a` - Shared location (max one level up from parent)
427+ ///
418428/// # Returns
419429///
420430/// Returns `true` if the user created and switched to a new worktree,
@@ -426,6 +436,7 @@ fn search_worktrees_internal(manager: &GitWorktreeManager) -> Result<bool> {
426436/// - Git repository operations fail
427437/// - File system operations fail
428438/// - User input is invalid
439+ /// - Custom path validation fails
429440pub fn create_worktree ( ) -> Result < bool > {
430441 let manager = GitWorktreeManager :: new ( ) ?;
431442 create_worktree_internal ( & manager)
@@ -443,15 +454,32 @@ pub fn create_worktree() -> Result<bool> {
443454/// - Detects existing worktree patterns for consistency
444455/// - Handles both branch and HEAD-based creation
445456/// - Executes lifecycle hooks at appropriate times
457+ /// - Supports custom path input for flexible worktree organization
446458///
447459/// # Path Handling
448460///
449- /// For first-time worktree creation, offers two location patterns:
450- /// - Same level as repository (`../name`): Creates worktrees as siblings
451- /// - In subdirectory (`worktrees/name`): Creates within repository structure
461+ /// For first-time worktree creation, offers three location patterns:
462+ /// 1. **Same level as repository** (`../name`): Creates worktrees as siblings to the main repository
463+ /// 2. **In subdirectory** (`worktrees/name`): Creates within repository structure (recommended)
464+ /// 3. **Custom path**: Allows users to specify any relative path, validated by `validate_custom_path()`
452465///
453466/// The chosen pattern is then used for subsequent worktrees when simple names
454467/// are provided, ensuring consistent organization.
468+ ///
469+ /// # Custom Path Feature
470+ ///
471+ /// When users select "Custom path", they can specify any relative path for the worktree.
472+ /// This enables flexible project organization such as:
473+ /// - Grouping by feature type: `features/ui/new-button`, `features/api/auth`
474+ /// - Temporary locations: `../temp/experiment-123`
475+ /// - Project-specific conventions: `workspaces/team-a/feature`
476+ ///
477+ /// All custom paths are validated for security and compatibility before use.
478+ ///
479+ /// # Returns
480+ ///
481+ /// * `true` - If a worktree was created and the user switched to it
482+ /// * `false` - If the operation was cancelled or user chose not to switch
455483fn create_worktree_internal ( manager : & GitWorktreeManager ) -> Result < bool > {
456484 println ! ( ) ;
457485 println ! ( "{}" , section_header( "Create New Worktree" ) ) ;
@@ -497,6 +525,7 @@ fn create_worktree_internal(manager: &GitWorktreeManager) -> Result<bool> {
497525 let options = vec ! [
498526 format!( "Same level as repository (../{name})" ) ,
499527 format!( "In subdirectory ({repo_name}/{}/{name})" , WORKTREES_SUBDIR ) ,
528+ "Custom path (specify relative to project root)" . to_string( ) ,
500529 ] ;
501530
502531 let selection = match Select :: with_theme ( & get_theme ( ) )
@@ -511,7 +540,38 @@ fn create_worktree_internal(manager: &GitWorktreeManager) -> Result<bool> {
511540
512541 match selection {
513542 0 => format ! ( "../{}" , name) , // Same level
514- _ => format ! ( "{}/{}" , WORKTREES_SUBDIR , name) , // Subdirectory pattern
543+ 1 => format ! ( "{}/{}" , WORKTREES_SUBDIR , name) , // Subdirectory pattern
544+ 2 => {
545+ // Custom path input
546+ println ! ( ) ;
547+ println ! (
548+ "{}" ,
549+ "Enter custom path (relative to project root):" . bright_cyan( )
550+ ) ;
551+ println ! (
552+ "{}" ,
553+ "Examples: ../custom-dir/worktree-name, temp/worktrees/name" . dimmed( )
554+ ) ;
555+
556+ let custom_path = match input_esc ( "Custom path" ) {
557+ Some ( path) => path. trim ( ) . to_string ( ) ,
558+ None => return Ok ( false ) ,
559+ } ;
560+
561+ if custom_path. is_empty ( ) {
562+ utils:: print_error ( "Custom path cannot be empty" ) ;
563+ return Ok ( false ) ;
564+ }
565+
566+ // Validate custom path
567+ if let Err ( e) = validate_custom_path ( & custom_path) {
568+ utils:: print_error ( & format ! ( "Invalid custom path: {}" , e) ) ;
569+ return Ok ( false ) ;
570+ }
571+
572+ custom_path
573+ }
574+ _ => format ! ( "{}/{}" , WORKTREES_SUBDIR , name) , // Default fallback
515575 }
516576 } else {
517577 name. clone ( )
@@ -1847,6 +1907,151 @@ pub fn validate_worktree_name(name: &str) -> Result<String> {
18471907 Ok ( name. to_string ( ) )
18481908}
18491909
1910+ /// Validates a custom path for worktree creation
1911+ ///
1912+ /// This function ensures that the custom path is safe and valid for use
1913+ /// as a worktree path, preventing potential security issues and file system
1914+ /// incompatibilities. It performs comprehensive validation to ensure the path
1915+ /// works across different operating systems and doesn't conflict with Git internals.
1916+ ///
1917+ /// # Arguments
1918+ ///
1919+ /// * `path` - The custom path to validate (must be relative to project root)
1920+ ///
1921+ /// # Returns
1922+ ///
1923+ /// * `Ok(())` - The path is valid and safe to use
1924+ /// * `Err` - The path violates one or more validation rules
1925+ ///
1926+ /// # Validation Rules
1927+ ///
1928+ /// 1. **Empty paths**: Path cannot be empty or contain only whitespace
1929+ /// 2. **Null bytes**: Path cannot contain null bytes (`\0`)
1930+ /// 3. **Reserved characters**: Cannot contain Windows reserved characters for cross-platform compatibility:
1931+ /// - `<` `>` `:` `"` `|` `?` `*`
1932+ /// 4. **Absolute paths**: Must be relative, not absolute (e.g., `/path` or `C:\path` are invalid)
1933+ /// 5. **Path traversal**: Limited to one level above project root (e.g., `../sibling` is ok, `../../parent` is not)
1934+ /// 6. **Path format**: Cannot contain consecutive slashes (`//`) or start/end with slash
1935+ /// 7. **Reserved names**: Path components cannot be Git reserved names (case-insensitive):
1936+ /// - `.git`, `HEAD`, `refs`, `hooks`, `info`, `objects`, `logs`
1937+ ///
1938+ /// # Security Considerations
1939+ ///
1940+ /// This function is designed to prevent:
1941+ /// - Path traversal attacks that could access system files
1942+ /// - Creation of worktrees in inappropriate locations
1943+ /// - Conflicts with Git's internal directory structure
1944+ /// - File system incompatibilities across platforms
1945+ ///
1946+ /// # Examples
1947+ ///
1948+ /// ```no_run
1949+ /// use git_workers::commands::validate_custom_path;
1950+ ///
1951+ /// // Valid paths
1952+ /// assert!(validate_custom_path("../my-worktree").is_ok());
1953+ /// assert!(validate_custom_path("temp/feature-branch").is_ok());
1954+ /// assert!(validate_custom_path("worktrees/experiment").is_ok());
1955+ /// assert!(validate_custom_path("./local-test").is_ok());
1956+ ///
1957+ /// // Invalid paths
1958+ /// assert!(validate_custom_path("/absolute/path").is_err()); // Absolute path
1959+ /// assert!(validate_custom_path("../../too-far").is_err()); // Too many parent dirs
1960+ /// assert!(validate_custom_path("path/with//double").is_err()); // Consecutive slashes
1961+ /// assert!(validate_custom_path("ends/with/").is_err()); // Trailing slash
1962+ /// assert!(validate_custom_path("has:colon").is_err()); // Reserved character
1963+ /// assert!(validate_custom_path("path/.git/config").is_err()); // Contains .git
1964+ /// ```
1965+ ///
1966+ /// # Usage in Worktree Creation
1967+ ///
1968+ /// This function is called when users select the "Custom path" option during
1969+ /// worktree creation. It ensures that user-provided paths are safe before
1970+ /// passing them to Git's worktree creation commands.
1971+ pub fn validate_custom_path ( path : & str ) -> Result < ( ) > {
1972+ use std:: path:: Path ;
1973+
1974+ // Trim the path
1975+ let path = path. trim ( ) ;
1976+
1977+ // Check for empty path
1978+ if path. is_empty ( ) {
1979+ return Err ( anyhow ! ( "Custom path cannot be empty" ) ) ;
1980+ }
1981+
1982+ // Check for null bytes
1983+ if path. contains ( '\0' ) {
1984+ return Err ( anyhow ! ( "Custom path cannot contain null bytes" ) ) ;
1985+ }
1986+
1987+ // Check for Windows reserved characters (for cross-platform compatibility)
1988+ const RESERVED_CHARS : & [ char ] = & [ '<' , '>' , ':' , '"' , '|' , '?' , '*' ] ;
1989+ if path. chars ( ) . any ( |c| RESERVED_CHARS . contains ( & c) ) {
1990+ return Err ( anyhow ! (
1991+ "Custom path contains reserved characters: {}" ,
1992+ RESERVED_CHARS . iter( ) . collect:: <String >( )
1993+ ) ) ;
1994+ }
1995+
1996+ // Check if path is absolute
1997+ if Path :: new ( path) . is_absolute ( ) {
1998+ return Err ( anyhow ! ( "Custom path must be relative, not absolute" ) ) ;
1999+ }
2000+
2001+ // Check for consecutive slashes
2002+ if path. contains ( "//" ) {
2003+ return Err ( anyhow ! ( "Custom path cannot contain consecutive slashes" ) ) ;
2004+ }
2005+
2006+ // Check for starting or ending with slash
2007+ if path. starts_with ( '/' ) || path. ends_with ( '/' ) {
2008+ return Err ( anyhow ! ( "Custom path cannot start or end with slash" ) ) ;
2009+ }
2010+
2011+ // Check for dangerous path traversal
2012+ let path_obj = Path :: new ( path) ;
2013+ let mut depth = 0i32 ;
2014+
2015+ for component in path_obj. components ( ) {
2016+ match component {
2017+ std:: path:: Component :: Normal ( name) => {
2018+ depth += 1 ;
2019+
2020+ // Check for reserved names in path components
2021+ if let Some ( name_str) = name. to_str ( ) {
2022+ const RESERVED_NAMES : & [ & str ] =
2023+ & [ ".git" , "HEAD" , "refs" , "hooks" , "info" , "objects" , "logs" ] ;
2024+ let name_lower = name_str. to_lowercase ( ) ;
2025+ if RESERVED_NAMES
2026+ . iter ( )
2027+ . any ( |& reserved| reserved. to_lowercase ( ) == name_lower)
2028+ {
2029+ return Err ( anyhow ! ( "Path component '{}' is reserved by git" , name_str) ) ;
2030+ }
2031+ }
2032+ }
2033+ std:: path:: Component :: ParentDir => {
2034+ depth -= 1 ;
2035+ // Allow going up to project parent, but not further
2036+ // depth can be -1 (one level up from project root) but not -2 or less
2037+ if depth < -1 {
2038+ return Err ( anyhow ! (
2039+ "Custom path cannot go above project root (too many '../')"
2040+ ) ) ;
2041+ }
2042+ }
2043+ std:: path:: Component :: CurDir => {
2044+ // ./ is allowed but not very useful
2045+ }
2046+ _ => {
2047+ return Err ( anyhow ! ( "Custom path contains invalid components" ) ) ;
2048+ }
2049+ }
2050+ }
2051+
2052+ Ok ( ( ) )
2053+ }
2054+
18502055/// Finds the configuration file path using the same logic as Config::load()
18512056///
18522057/// This function follows the exact same discovery order as Config::load_from_main_repository_only()
0 commit comments