1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
use anyhow::Context;
use std::path::PathBuf;
use worktrunk::config::{LoadedConfigs, UserConfig};
use worktrunk::git::Repository;
use super::command_executor::CommandContext;
/// Shared execution context for command handlers that operate on the current worktree.
///
/// Centralizes the common "repo + branch + config + cwd" setup so individual handlers
/// can focus on their core logic while sharing consistent error messaging.
///
/// This helper is used for commands that explicitly act on "where the user is standing"
/// (e.g., `beta` and `merge`) and therefore need all of these pieces together. Commands that
/// inspect multiple worktrees or run without a config/branch requirement (`list`, the picker,
/// some `worktree` helpers) still call `Repository::current()` directly so they can operate in
/// broader contexts without forcing config loads or branch resolution.
pub struct CommandEnv {
pub repo: Repository,
/// Current branch name, if on a branch (None in detached HEAD state).
pub branch: Option<String>,
pub config: UserConfig,
/// Canonical absolute path to the worktree root (via `git rev-parse --show-toplevel`).
pub worktree_path: PathBuf,
}
impl CommandEnv {
/// Load the command environment for the current worktree.
///
/// Resolves the worktree path from the current directory. The branch is
/// populated when available but not required — commands that need a branch
/// should call `require_branch()` after construction.
pub fn for_action(config: UserConfig) -> anyhow::Result<Self> {
let repo = Repository::current()?;
let current_wt = repo.current_worktree();
let worktree_path = current_wt.root()?;
let branch = current_wt
.branch()
.context("Failed to determine current branch")?;
Ok(Self {
repo,
branch,
config,
worktree_path,
})
}
/// Load the command environment for a named worktree (by branch name).
///
/// Resolves the worktree path from the branch name rather than using
/// the current working directory.
pub fn for_branch(config: UserConfig, branch: &str) -> anyhow::Result<Self> {
let repo = Repository::current()?;
let worktree_path = repo
.worktree_for_branch(branch)?
.ok_or_else(|| anyhow::anyhow!("no worktree for branch '{branch}'"))?;
Ok(Self {
repo,
branch: Some(branch.to_string()),
config,
worktree_path,
})
}
/// Load the command environment without requiring a branch.
///
/// Use this for commands that can operate in detached HEAD state,
/// such as running hooks (where `{{ branch }}` expands to "HEAD" if detached).
pub fn for_action_branchless() -> anyhow::Result<Self> {
let repo = Repository::current()?;
let current_wt = repo.current_worktree();
let worktree_path = current_wt.root()?;
// Propagate git errors (broken repo, missing git) but allow None for detached HEAD
let branch = current_wt
.branch()
.context("Failed to determine current branch")?;
// `wt hook`-style callers always read project config downstream
// (`repo.load_project_config()`); use the bundle loader to warm
// both in parallel. `CommandEnv` only carries `UserConfig` —
// downstream picks up project from the cache.
let config = LoadedConfigs::load(&repo)
.context("Failed to load config")?
.user;
Ok(Self {
repo,
branch,
config,
worktree_path,
})
}
/// Build a `CommandContext` tied to this environment.
pub fn context(&self, yes: bool) -> CommandContext<'_> {
CommandContext::new(
&self.repo,
&self.config,
self.branch.as_deref(),
&self.worktree_path,
yes,
)
}
/// Get branch name, returning error if in detached HEAD state.
pub fn require_branch(&self, action: &str) -> anyhow::Result<&str> {
self.branch.as_deref().ok_or_else(|| {
worktrunk::git::GitError::DetachedHead {
action: Some(action.into()),
}
.into()
})
}
/// Get the project identifier for per-project config lookup.
///
/// Uses the remote URL if available, otherwise the canonical repository path.
/// Returns None only if the path is not valid UTF-8.
pub fn project_id(&self) -> Option<String> {
self.repo.project_identifier().ok()
}
/// Get all resolved config with defaults applied.
pub fn resolved(&self) -> worktrunk::config::ResolvedConfig {
self.config.resolved(self.project_id().as_deref())
}
}