Packages

fnord

0.8.61

AI code archaeology

Current section

Files

Jump to
fnord lib resolve_project.ex
Raw

lib/resolve_project.ex

defmodule ResolveProject do
@moduledoc """
Project resolution from cwd or git worktree root.
Resolution order (most-specific to least-specific):
- From CWD: choose the configured project whose root contains CWD with the deepest (longest) root.
- Git fallback: build candidate roots from worktree and repo.
- Prefer exact matches first (worktree exact beats repo exact; ties break by depth then name).
- Otherwise, pick the deepest configured project whose root lies within any candidate (ties by name).
Returns `{:ok, project_name}` or `{:error, :not_in_project}`.
"""
@type project_name :: binary
@spec resolve_from_cwd(cwd :: binary | nil) :: {:ok, project_name} | {:error, :not_in_project}
def resolve_from_cwd(cwd \\ nil) do
projects =
Settings.new()
|> Settings.get_projects()
|> Enum.flat_map(fn {name, %{"root" => root}} ->
case root do
nil -> []
root_str -> [{Path.absname(root_str), name}]
end
end)
cwd_abs =
case cwd do
nil -> Path.absname(File.cwd!())
dir -> Path.absname(dir)
end
matching_projects =
projects
|> Enum.filter(fn {root_abs, _name} -> path_contains?(root_abs, cwd_abs) end)
case matching_projects do
[] ->
{:error, :not_in_project}
_ ->
{_, project_name} =
Enum.max_by(matching_projects, fn {root_abs, _} -> String.length(root_abs) end)
{:ok, project_name}
end
end
@spec resolve_from_worktree() :: {:ok, project_name} | {:error, :not_in_project}
def resolve_from_worktree() do
# Gather possible roots from git worktree and repo
candidates =
[GitCli.worktree_root(), GitCli.repo_root()]
|> Enum.filter(& &1)
|> Enum.map(&Path.absname/1)
|> Enum.uniq()
# Load configured project roots
projects =
Settings.new()
|> Settings.get_projects()
|> Enum.flat_map(fn {name, %{"root" => root}} ->
case root do
nil -> []
root_str -> [{Path.absname(root_str), name}]
end
end)
# Attempt direct match of candidate to configured roots
# Two-phase ranked project selection: exact then nested within worktree candidates
# Phase 1: exact matches
selected =
projects
|> Enum.filter(fn {root_abs, _} -> root_abs in candidates end)
result =
if selected != [] do
worktree_abs = GitCli.worktree_root() && Path.absname(GitCli.worktree_root())
repo_abs = GitCli.repo_root() && Path.absname(GitCli.repo_root())
cond do
worktree_abs && Enum.any?(selected, fn {root_abs, _} -> root_abs == worktree_abs end) ->
{_, name} = Enum.find(selected, fn {root_abs, _} -> root_abs == worktree_abs end)
{:ok, name}
repo_abs && Enum.any?(selected, fn {root_abs, _} -> root_abs == repo_abs end) ->
{_, name} = Enum.find(selected, fn {root_abs, _} -> root_abs == repo_abs end)
{:ok, name}
true ->
{_, name} =
Enum.max_by(selected, fn {root_abs, name} -> {String.length(root_abs), name} end)
{:ok, name}
end
else
# Phase 2: nested matches
nested =
projects
|> Enum.filter(fn {root_abs, _} ->
Enum.any?(candidates, fn cand -> Util.path_within_root?(root_abs, cand) end)
end)
if nested != [] do
{_, name} =
Enum.max_by(nested, fn {root_abs, name} -> {String.length(root_abs), name} end)
{:ok, name}
else
{:error, :not_in_project}
end
end
result
end
# Determines if `child` is equal to or contained within `parent`.
defp path_contains?(parent, child) do
parent_abs = Path.expand(parent)
child_abs = Path.expand(child)
Util.path_within_root?(child_abs, parent_abs)
end
end