Class: Ace::Git::Worktree::Atoms::PathExpander

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/git/worktree/atoms/path_expander.rb

Overview

Path expansion and validation for worktree operations

Simplified implementation focused on worktree-specific needs without over-engineered security patterns.

Examples:

Expand a user path

PathExpander.expand("~") # => "/home/user"

Check if a directory is writable

PathExpander.writable?("/tmp/worktree") # => true/false

Class Method Summary collapse

Class Method Details

.expand(path, base = nil) ⇒ String

Expand a path using standard Ruby path expansion

Parameters:

  • path (String)

    Path to expand

  • base (String, nil) (defaults to: nil)

    Base directory for relative paths (default: current directory)

Returns:

  • (String)

    Expanded absolute path



26
27
28
29
# File 'lib/ace/git/worktree/atoms/path_expander.rb', line 26

def expand(path, base = nil)
  return "" if path.nil? || path.empty?
  base ? File.expand_path(path, base) : File.expand_path(path)
end

.relative_to_git_root(path, git_root) ⇒ String

Get a relative path from git root

Parameters:

  • path (String)

    Absolute path

  • git_root (String)

    Git repository root

Returns:

  • (String)

    Relative path from git root



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/ace/git/worktree/atoms/path_expander.rb', line 148

def relative_to_git_root(path, git_root)
  expanded_path = expand(path)
  expanded_root = expand(git_root)

  # Use File.expand_path for consistent comparison
  normalized_path = File.expand_path(expanded_path)
  normalized_root = File.expand_path(expanded_root)

  if normalized_path.start_with?(normalized_root + "/") || normalized_path == normalized_root
    if normalized_path == normalized_root
      "."
    else
      relative_path = normalized_path[normalized_root.length..-1]
      relative_path.start_with?("/") ? relative_path[1..-1] : relative_path
    end
  else
    expanded_path
  end
end

.resolve(path, base = Dir.pwd) ⇒ String

Resolve a path relative to a base directory

Parameters:

  • path (String)

    Path to resolve

  • base (String) (defaults to: Dir.pwd)

    Base directory (default: current directory)

Returns:

  • (String)

    Resolved absolute path



36
37
38
39
40
41
42
43
44
45
# File 'lib/ace/git/worktree/atoms/path_expander.rb', line 36

def resolve(path, base = Dir.pwd)
  expand_path = expand(path)
  expanded_base = expand(base)

  if File.absolute_path?(expand_path)
    expand_path
  else
    File.expand_path(path, expanded_base)
  end
end

.safe_path?(path) ⇒ Boolean

Simple path safety validation Only checks for obviously dangerous patterns

Parameters:

  • path (String)

    Path to validate

Returns:

  • (Boolean)

    true if path appears safe



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/ace/git/worktree/atoms/path_expander.rb', line 173

def safe_path?(path)
  return false if path.nil? || path.empty?

  path_str = path.to_s

  # Check for null bytes and obviously dangerous patterns
  return false if path_str.include?("\x00")
  return false if path_str.include?("../../../")

  true
end

.validate_for_worktree(path, git_root = nil) ⇒ Hash

Validate a path for worktree creation

Parameters:

  • path (String)

    Path to validate

  • git_root (String) (defaults to: nil)

    Git repository root

Returns:

  • (Hash)

    Validation result with :valid, :error, :expanded_path keys



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/ace/git/worktree/atoms/path_expander.rb', line 82

def validate_for_worktree(path, git_root = nil)
  expanded_path = expand(path)

  # Check if parent directory exists and is writable
  parent_dir = File.dirname(expanded_path)
  unless File.exist?(parent_dir)
    return {
      valid: false,
      error: "Parent directory does not exist: #{parent_dir}",
      expanded_path: expanded_path
    }
  end

  unless writable?(parent_dir)
    return {
      valid: false,
      error: "Parent directory is not writable: #{parent_dir}",
      expanded_path: expanded_path
    }
  end

  # Check if worktree is being created directly in git root (not allowed)
  # Allow worktrees in subdirectories or outside git root entirely
  if git_root
    git_root_expanded = File.expand_path(git_root)
    expanded_path_abs = File.expand_path(expanded_path)

    # Only prevent creation directly at git root, not in subdirectories
    if expanded_path_abs == git_root_expanded
      return {
        valid: false,
        error: "Worktree cannot be created at git repository root. Use a subdirectory or different location.",
        expanded_path: expanded_path
      }
    end
  end

  # Check if path already exists
  if File.exist?(expanded_path)
    if File.directory?(expanded_path) && !Dir.empty?(expanded_path)
      return {
        valid: false,
        error: "Directory already exists and is not empty: #{expanded_path}",
        expanded_path: expanded_path
      }
    elsif File.file?(expanded_path)
      return {
        valid: false,
        error: "Path exists but is a file: #{expanded_path}",
        expanded_path: expanded_path
      }
    end
  end

  {
    valid: true,
    error: nil,
    expanded_path: expanded_path
  }
end

.writable?(path, create_if_missing: false) ⇒ Boolean

Check if a path is writable

Parameters:

  • path (String)

    Path to check

  • create_if_missing (Boolean) (defaults to: false)

    Create directory if it doesn’t exist

Returns:

  • (Boolean)

    true if path is writable



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/ace/git/worktree/atoms/path_expander.rb', line 52

def writable?(path, create_if_missing: false)
  expanded_path = expand(path)

  begin
    # Check if path exists
    unless File.exist?(expanded_path)
      if create_if_missing
        FileUtils.mkdir_p(expanded_path)
      else
        return false
      end
    end

    # Test writability by trying to create a temp file
    temp_file = File.join(expanded_path, ".ace_git_worktree_test_#{Time.now.to_i}_#{$$}")
    File.write(temp_file, "test")
    File.delete(temp_file)
    true
  rescue Errno::EACCES, Errno::EPERM
    false
  rescue
    false
  end
end