Class: Ace::Support::Fs::Molecules::ProjectRootFinder

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/support/fs/molecules/project_root_finder.rb

Overview

Find project root directory based on markers

Constant Summary collapse

DEFAULT_MARKERS =

Common project root markers in order of preference

%w[
  .git
  Gemfile
  package.json
  Cargo.toml
  pyproject.toml
  go.mod
  .hg
  .svn
  Rakefile
  Makefile
].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(markers: DEFAULT_MARKERS, start_path: nil) ⇒ ProjectRootFinder

Initialize finder with optional custom markers

Parameters:

  • markers (Array<String>) (defaults to: DEFAULT_MARKERS)

    Project root markers to look for

  • start_path (String) (defaults to: nil)

    Path to start searching from (default: current directory)

Raises:

  • (ArgumentError)

    if markers is nil or empty



42
43
44
45
46
47
48
# File 'lib/ace/support/fs/molecules/project_root_finder.rb', line 42

def initialize(markers: DEFAULT_MARKERS, start_path: nil)
  if markers.nil? || markers.empty?
    raise ArgumentError, "markers cannot be nil or empty"
  end
  @markers = markers
  @start_path = start_path ? Atoms::PathExpander.expand(start_path) : Dir.pwd
end

Class Attribute Details

.cache_mutexObject (readonly)

Returns the value of attribute cache_mutex.



31
32
33
# File 'lib/ace/support/fs/molecules/project_root_finder.rb', line 31

def cache_mutex
  @cache_mutex
end

Class Method Details

.cacheObject



33
34
35
# File 'lib/ace/support/fs/molecules/project_root_finder.rb', line 33

def cache
  @cache ||= {}
end

.clear_cache!Object

Clear the cache (thread-safe)



105
106
107
108
109
# File 'lib/ace/support/fs/molecules/project_root_finder.rb', line 105

def self.clear_cache!
  cache_mutex.synchronize do
    cache.clear
  end
end

.find(start_path: nil, markers: DEFAULT_MARKERS) ⇒ String?

Class method for convenience

Returns:

  • (String, nil)

    Project root path



113
114
115
# File 'lib/ace/support/fs/molecules/project_root_finder.rb', line 113

def self.find(start_path: nil, markers: DEFAULT_MARKERS)
  new(start_path: start_path, markers: markers).find
end

.find_or_current(start_path: nil, markers: DEFAULT_MARKERS) ⇒ String

Class method to find or use current directory

Returns:

  • (String)

    Project root path or current directory



119
120
121
# File 'lib/ace/support/fs/molecules/project_root_finder.rb', line 119

def self.find_or_current(start_path: nil, markers: DEFAULT_MARKERS)
  new(start_path: start_path, markers: markers).find_or_current
end

Instance Method Details

#findString?

Find project root directory with caching

Returns:

  • (String, nil)

    Project root path or nil if not found



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/ace/support/fs/molecules/project_root_finder.rb', line 52

def find
  # Check environment variable first
  project_root_env = env_project_root
  if project_root_env && !project_root_env.empty?
    project_root = Atoms::PathExpander.expand(project_root_env)
    if Dir.exist?(project_root) && path_within_root?(@start_path, project_root)
      return project_root
    end
  end

  cache_key = "#{@start_path}:#{@markers.join(",")}"

  # Thread-safe cache access
  self.class.cache_mutex.synchronize do
    # Return cached result if available
    return self.class.cache[cache_key] if self.class.cache.key?(cache_key)

    # Find and cache the result
    result = find_without_cache
    self.class.cache[cache_key] = result
    result
  end
end

#find_or_currentString

Find project root or fall back to current directory

Returns:

  • (String)

    Project root path or current directory



78
79
80
# File 'lib/ace/support/fs/molecules/project_root_finder.rb', line 78

def find_or_current
  find || Dir.pwd
end

#in_project?Boolean

Check if we’re in a project directory

Returns:

  • (Boolean)

    true if project root is found



84
85
86
# File 'lib/ace/support/fs/molecules/project_root_finder.rb', line 84

def in_project?
  !find.nil?
end

#relative_path(path) ⇒ String?

Get the relative path from project root to a given path

Parameters:

  • path (String)

    Path to make relative

Returns:

  • (String, nil)

    Relative path or nil if not in project



91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/ace/support/fs/molecules/project_root_finder.rb', line 91

def relative_path(path)
  root = find
  return nil unless root

  # Use realpath to handle symlinks and resolve paths
  real_root = File.realpath(root)
  real_path = File.realpath(Atoms::PathExpander.expand(path))

  return nil unless real_path.start_with?(real_root)

  Pathname.new(real_path).relative_path_from(Pathname.new(real_root)).to_s
end