Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Improved

- **`remote_repo` names the repository as the remote spells it**: `repo` is the directory on disk, so a renamed clone reports the new name. `{{ remote_repo }}` takes it from the primary remote's URL, available everywhere `owner` is and unset when no remote parses. ([#3745](https://github.com/max-sixty/worktrunk/pull/3745), thanks @canac)

## 0.73.0

### Fixed
Expand Down
1 change: 1 addition & 0 deletions dev/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# - `{{ repo_path }}` — absolute path to the repository root (e.g., `/Users/me/code/myproject`. Or for bare repos, the bare directory itself)
# - `{{ repo }}` — repository directory name (e.g., `myproject`)
# - `{{ owner }}` — primary remote owner path (may include subgroups like `group/subgroup`)
# - `{{ remote_repo }}` — repository name in the primary remote URL, without `.git` (e.g., `myproject`); differs from `{{ repo }}`, the directory on disk, when a clone was renamed
# - `{{ branch }}` — raw branch name (e.g., `feature/auth`)
# - `{{ branch | sanitize }}` — filesystem-safe: `/` and `\` become `-` (e.g., `feature-auth`)
# - `{{ branch | sanitize_db }}` — database-safe: lowercase, underscores, hash suffix (e.g., `feature_auth_x7k`)
Expand Down
1 change: 1 addition & 0 deletions docs/content/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Controls where new worktrees are created.
- `{{ repo_path }}` — absolute path to the repository root (e.g., `/Users/me/code/myproject`. Or for bare repos, the bare directory itself)
- `{{ repo }}` — repository directory name (e.g., `myproject`)
- `{{ owner }}` — primary remote owner path (may include subgroups like `group/subgroup`)
- `{{ remote_repo }}` — repository name in the primary remote URL, without `.git` (e.g., `myproject`); differs from `{{ repo }}`, the directory on disk, when a clone was renamed
- `{{ branch }}` — raw branch name (e.g., `feature/auth`)
- `{{ branch | sanitize }}` — filesystem-safe: `/` and `\` become `-` (e.g., `feature-auth`)
- `{{ branch | sanitize_db }}` — database-safe: lowercase, underscores, hash suffix (e.g., `feature_auth_x7k`)
Expand Down
3 changes: 2 additions & 1 deletion docs/content/hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ Hooks can use template variables that expand at runtime:
| repo | `{{ repo }}` | Repository directory name |
| | `{{ repo_path }}` | Absolute path to repository root |
| | `{{ owner }}` | Primary remote owner path (may include subgroups) |
| | `{{ remote_repo }}` | Repository name from the primary remote URL, without `.git` |
| | `{{ primary_worktree_path }}` | Primary worktree path |
| | `{{ default_branch }}` | Default branch name |
| | `{{ remote }}` | Primary remote name |
Expand All @@ -148,7 +149,7 @@ Hooks can use template variables that expand at runtime:
| | `{{ args }}` | Tokens forwarded from the CLI — see [Running Hooks Manually](#running-hooks-manually) |
| user | `{{ vars.<key> }}` | Per-branch variables from [`wt config state vars`](@/config.md#wt-config-state-vars) |

The `repo` variables (`repo`, `repo_path`, `owner`, `primary_worktree_path`, `default_branch`, `remote`, `remote_url`) are constant across the whole repository — `default_branch` is the same in every worktree. The `active` variables (`branch`, `worktree_path`, `worktree_name`, `commit`, `short_commit`, `upstream`) vary per worktree.
The `repo` variables (`repo`, `repo_path`, `owner`, `remote_repo`, `primary_worktree_path`, `default_branch`, `remote`, `remote_url`) are constant across the whole repository — `default_branch` is the same in every worktree. The `active` variables (`branch`, `worktree_path`, `worktree_name`, `commit`, `short_commit`, `upstream`) vary per worktree.

Bare variables (`branch`, `worktree_path`, `commit`) refer to the branch the operation acts on: the destination for switch/create, the source for merge/remove. `base` and `target` give the other side:

Expand Down
1 change: 1 addition & 0 deletions plugins/worktrunk/skills/worktrunk/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Controls where new worktrees are created.
- `{{ repo_path }}` — absolute path to the repository root (e.g., `/Users/me/code/myproject`. Or for bare repos, the bare directory itself)
- `{{ repo }}` — repository directory name (e.g., `myproject`)
- `{{ owner }}` — primary remote owner path (may include subgroups like `group/subgroup`)
- `{{ remote_repo }}` — repository name in the primary remote URL, without `.git` (e.g., `myproject`); differs from `{{ repo }}`, the directory on disk, when a clone was renamed
- `{{ branch }}` — raw branch name (e.g., `feature/auth`)
- `{{ branch | sanitize }}` — filesystem-safe: `/` and `\` become `-` (e.g., `feature-auth`)
- `{{ branch | sanitize_db }}` — database-safe: lowercase, underscores, hash suffix (e.g., `feature_auth_x7k`)
Expand Down
3 changes: 2 additions & 1 deletion plugins/worktrunk/skills/worktrunk/reference/hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Hooks can use template variables that expand at runtime:
| repo | `{{ repo }}` | Repository directory name |
| | `{{ repo_path }}` | Absolute path to repository root |
| | `{{ owner }}` | Primary remote owner path (may include subgroups) |
| | `{{ remote_repo }}` | Repository name from the primary remote URL, without `.git` |
| | `{{ primary_worktree_path }}` | Primary worktree path |
| | `{{ default_branch }}` | Default branch name |
| | `{{ remote }}` | Primary remote name |
Expand All @@ -139,7 +140,7 @@ Hooks can use template variables that expand at runtime:
| | `{{ args }}` | Tokens forwarded from the CLI — see [Running Hooks Manually](#running-hooks-manually) |
| user | `{{ vars.<key> }}` | Per-branch variables from [`wt config state vars`](https://worktrunk.dev/config/#wt-config-state-vars) |

The `repo` variables (`repo`, `repo_path`, `owner`, `primary_worktree_path`, `default_branch`, `remote`, `remote_url`) are constant across the whole repository — `default_branch` is the same in every worktree. The `active` variables (`branch`, `worktree_path`, `worktree_name`, `commit`, `short_commit`, `upstream`) vary per worktree.
The `repo` variables (`repo`, `repo_path`, `owner`, `remote_repo`, `primary_worktree_path`, `default_branch`, `remote`, `remote_url`) are constant across the whole repository — `default_branch` is the same in every worktree. The `active` variables (`branch`, `worktree_path`, `worktree_name`, `commit`, `short_commit`, `upstream`) vary per worktree.

Bare variables (`branch`, `worktree_path`, `commit`) refer to the branch the operation acts on: the destination for switch/create, the source for merge/remove. `base` and `target` give the other side:

Expand Down
1 change: 1 addition & 0 deletions skills/worktrunk/reference/config.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion skills/worktrunk/reference/hook.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,7 @@ Hooks can use template variables that expand at runtime:
| repo | `{{ repo }}` | Repository directory name |
| | `{{ repo_path }}` | Absolute path to repository root |
| | `{{ owner }}` | Primary remote owner path (may include subgroups) |
| | `{{ remote_repo }}` | Repository name from the primary remote URL, without `.git` |
| | `{{ primary_worktree_path }}` | Primary worktree path |
| | `{{ default_branch }}` | Default branch name |
| | `{{ remote }}` | Primary remote name |
Expand All @@ -1682,7 +1683,7 @@ Hooks can use template variables that expand at runtime:
| | `{{ args }}` | Tokens forwarded from the CLI — see [Running Hooks Manually](#running-hooks-manually) |
| user | `{{ vars.<key> }}` | Per-branch variables from [`wt config state vars`](@/config.md#wt-config-state-vars) |

The `repo` variables (`repo`, `repo_path`, `owner`, `primary_worktree_path`, `default_branch`, `remote`, `remote_url`) are constant across the whole repository — `default_branch` is the same in every worktree. The `active` variables (`branch`, `worktree_path`, `worktree_name`, `commit`, `short_commit`, `upstream`) vary per worktree.
The `repo` variables (`repo`, `repo_path`, `owner`, `remote_repo`, `primary_worktree_path`, `default_branch`, `remote`, `remote_url`) are constant across the whole repository — `default_branch` is the same in every worktree. The `active` variables (`branch`, `worktree_path`, `worktree_name`, `commit`, `short_commit`, `upstream`) vary per worktree.

Bare variables (`branch`, `worktree_path`, `commit`) refer to the branch the operation acts on: the destination for switch/create, the source for merge/remove. `base` and `target` give the other side:

Expand Down Expand Up @@ -1968,6 +1969,7 @@ Controls where new worktrees are created.
- `{{ repo_path }}` — absolute path to the repository root (e.g., `/Users/me/code/myproject`. Or for bare repos, the bare directory itself)
- `{{ repo }}` — repository directory name (e.g., `myproject`)
- `{{ owner }}` — primary remote owner path (may include subgroups like `group/subgroup`)
- `{{ remote_repo }}` — repository name in the primary remote URL, without `.git` (e.g., `myproject`); differs from `{{ repo }}`, the directory on disk, when a clone was renamed
- `{{ branch }}` — raw branch name (e.g., `feature/auth`)
- `{{ branch | sanitize }}` — filesystem-safe: `/` and `\` become `-` (e.g., `feature-auth`)
- `{{ branch | sanitize_db }}` — database-safe: lowercase, underscores, hash suffix (e.g., `feature_auth_x7k`)
Expand Down
1 change: 1 addition & 0 deletions src/commands/command_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ pub fn build_hook_context(

if let Some(parsed_remote) = ctx.repo.primary_remote_parsed_url() {
map.insert("owner".into(), parsed_remote.owner().to_string());
map.insert("remote_repo".into(), parsed_remote.repo().to_string());
}

// Default branch
Expand Down
16 changes: 16 additions & 0 deletions src/config/expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub const REPO_VARS: &[&str] = &[
"repo",
"repo_path",
"owner",
"remote_repo",
"primary_worktree_path",
"default_branch",
"remote",
Expand Down Expand Up @@ -2931,6 +2932,20 @@ mod tests {
.is_ok()
);

// Remote-derived repo vars validate in every scope
assert!(
validate_template("{{ owner }}/{{ remote_repo }}", hook, &test.repo, "test").is_ok()
);
assert!(
validate_template(
"{{ remote_repo }}",
ValidationScope::Alias,
&test.repo,
"test"
)
.is_ok()
);

// Deprecated vars still valid in every scope
assert!(validate_template("{{ main_worktree }}", hook, &test.repo, "test").is_ok());

Expand Down Expand Up @@ -3155,6 +3170,7 @@ mod tests {
repo = demo
repo_path = /tmp/demo
owner = (unset)
remote_repo = (unset)
primary_worktree_path = (unset)
default_branch = (unset)
remote = (unset)
Expand Down
11 changes: 5 additions & 6 deletions src/config/user/accessors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ impl UserConfig {
/// `{{ main_worktree }}` and `{{ repo }}`
/// * `branch` - Branch name (replaces {{ branch }} in template; use `{{ branch | sanitize }}` for paths)
/// * `repo` - Repository, for template function access and for the
/// `{{ repo_path }}` and `{{ owner }}` values read off it —
/// `{{ repo_path }}`, `{{ owner }}`, and {{ remote_repo }} values read off it —
/// `{{ owner }}` is absent when the primary remote has no parseable URL
/// * `project` - Optional project identifier (e.g., "github.com/user/repo") to look up
/// project-specific worktree-path template
Expand All @@ -259,11 +259,10 @@ impl UserConfig {
vars.insert("repo", main_worktree);
vars.insert("branch", branch);
vars.insert("repo_path", repo_path.as_str());
let owner = repo
.primary_remote_parsed_url()
.map(|parsed_remote| parsed_remote.owner().to_string());
if let Some(ref owner) = owner {
vars.insert("owner", owner.as_str());
let parsed_remote = repo.primary_remote_parsed_url();
if let Some(ref parsed_remote) = parsed_remote {
vars.insert("owner", parsed_remote.owner());
vars.insert("remote_repo", parsed_remote.repo());
}
Ok(expand_template(
&template,
Expand Down
23 changes: 23 additions & 0 deletions src/config/user/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,29 @@ fn test_worktrunk_config_format_path_owner_variable() {
assert_eq!(path, "max-sixty/myrepo/feature/branch");
}

#[test]
fn test_worktrunk_config_format_path_remote_repo_variable() {
let mut test = TestRepo::with_initial_commit();
test.setup_remote("main");
test.run_git(&[
"remote",
"set-url",
"origin",
"git@github.com:company-org/project.git",
]);

let config = UserConfig {
worktree_path: Some("{{ remote_repo }}/{{ repo }}/{{ branch }}".to_string()),
..Default::default()
};

let path = config
.format_path("myrepo", "feature/branch", &test.repo, None)
.unwrap();

assert_eq!(path, "project/myrepo/feature/branch");
}

#[test]
fn test_worktrunk_config_format_path_owner_uses_full_namespace() {
let mut test = TestRepo::with_initial_commit();
Expand Down
26 changes: 26 additions & 0 deletions tests/integration_tests/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,32 @@ fn test_eval_owner(repo: TestRepo) {
);
}

#[rstest]
fn test_eval_remote_repo(repo: TestRepo) {
repo.run_git(&[
"remote",
"set-url",
"origin",
"git@github.com:company-org/project.git",
]);

let output = repo
.wt_command()
.args(["step", "eval", "{{ remote_repo }}/{{ repo }}"])
.output()
.expect("Failed to run wt step eval");

assert!(
output.status.success(),
"wt step eval should succeed, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"project/repo"
);
}

#[rstest]
fn test_eval_conditional(repo: TestRepo) {
assert_cmd_snapshot!(make_snapshot_cmd(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ exit_code: 0
  repo = repo
  repo_path = _REPO_
  owner = (unset)
  remote_repo = (unset)
  primary_worktree_path = _REPO_
  default_branch = main
  remote = origin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ exit_code: 0
  repo = repo
  repo_path = _REPO_
  owner = (unset)
  remote_repo = (unset)
  primary_worktree_path = _REPO_
  default_branch = main
  remote = origin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Creates ~/.config/worktrunk/config.toml with the following content:
  # - `{{ repo_path }}` — absolute path to the repository root (e.g., `/Users/me/code/myproject`. Or for bare repos, the bare directory itself)
  # - `{{ repo }}` — repository directory name (e.g., `myproject`)
  # - `{{ owner }}` — primary remote owner path (may include subgroups like `group/subgroup`)
  # - `{{ remote_repo }}` — repository name in the primary remote URL, without `.git` (e.g., `myproject`); differs from `{{ repo }}`, the directory on disk, when a clone was renamed
  # - `{{ branch }}` — raw branch name (e.g., `feature/auth`)
  # - `{{ branch | sanitize }}` — filesystem-safe: `/` and `\` become `-` (e.g., `feature-auth`)
  # - `{{ branch | sanitize_db }}` — database-safe: lowercase, underscores, hash suffix (e.g., `feature_auth_x7k`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Controls where new worktrees are created.
- {{ repo_path }} — absolute path to the repository root (e.g., /Users/me/code/myproject. Or for bare repos, the bare directory itself)
- {{ repo }} — repository directory name (e.g., myproject)
- {{ owner }} — primary remote owner path (may include subgroups like group/subgroup)
- {{ remote_repo }} — repository name in the primary remote URL, without .git (e.g., myproject); differs from {{ repo }}, the directory on disk, when a clone was renamed
- {{ branch }} — raw branch name (e.g., feature/auth)
- {{ branch | sanitize }} — filesystem-safe: / and \ become - (e.g., feature-auth)
- {{ branch | sanitize_db }} — database-safe: lowercase, underscores, hash suffix (e.g., feature_auth_x7k)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ exit_code: 0
  repo = repo
  repo_path = _REPO_
  owner = (unset)
  remote_repo = (unset)
  primary_worktree_path = _REPO_
  default_branch = main
  remote = origin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ exit_code: 0
  repo = repo
  repo_path = _REPO_
  owner = (unset)
  remote_repo = (unset)
  primary_worktree_path = _REPO_
  default_branch = main
  remote = origin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ hello foo bar baz
  repo = repo
  repo_path = _REPO_
  owner = (unused)
  remote_repo = (unused)
  primary_worktree_path = (unused)
  default_branch = (unused)
  remote = (unused)
Expand Down
Loading
Loading