Skip to content

Commit 481c4a0

Browse files
authored
Merge pull request #36 from dggsax/worktree-commands
2 parents a03582d + ab02084 commit 481c4a0

6 files changed

Lines changed: 539 additions & 22 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "loki-cli"
3-
version = "2.0.0"
3+
version = "2.1.0"
44
authors = ["Kyle W. Rader"]
55
description = "Loki: 🚀 A Git productivity tool"
66
homepage = "https://github.com/kyle-rader/loki-cli"

README.md

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@ Loki: 🚀 A Git productivity tool
2222
Usage: lk <COMMAND>
2323

2424
Commands:
25-
new Create a new branch from HEAD and push it to origin. Set a prefix with --prefix or the LOKI_NEW_PREFIX env var [aliases: n]
26-
push Push the current branch to origin with --set-upstream [aliases: p]
27-
pull Pull with --prune deleting local branches pruned from the remote
28-
fetch Fetch with --prune deleting local branches pruned from the remote
29-
save Add, commit, and push using a timestamp based commit message [aliases: s]
30-
commit Commit local changes [aliases: c]
31-
rebase Rebase the current branch onto the target branch after fetching
32-
no-hooks Run any command without triggering any hooks [aliases: x]
33-
help Print this message or the help of the given subcommand(s)
25+
new Create a new branch from HEAD and push it to origin. Set a prefix with --prefix or the LOKI_NEW_PREFIX env var [aliases: n]
26+
push Push the current branch to origin with --set-upstream [aliases: p]
27+
pull Pull with --prune deleting local branches pruned from the remote
28+
fetch Fetch with --prune deleting local branches pruned from the remote
29+
save Add, commit, and push using a timestamp based commit message [aliases: s]
30+
commit Commit local changes [aliases: c]
31+
rebase Rebase the current branch onto the target branch after fetching
32+
worktree Manage git worktrees [aliases: w]
33+
no-hooks Run any command without triggering any hooks [aliases: x]
34+
help Print this message or the help of the given subcommand(s)
3435

3536
Options:
3637
-h, --help Print help
@@ -90,6 +91,88 @@ Execute a git commit without running any hooks
9091
lk x -- commit -m "Update Readme without running hooks"
9192
```
9293

94+
### `worktree`
95+
Alias: `w`
96+
97+
Manage git worktrees for parallel development workflows. Subcommands:
98+
99+
#### `worktree add <name>` (alias: `a`)
100+
Create a new worktree as a sibling directory and set up a branch with upstream tracking.
101+
102+
```sh
103+
# Creates worktree at ../my-project_fix-auth, creates and pushes branch
104+
❯ lk w a fix-auth
105+
106+
# With a custom base ref
107+
❯ lk w a fix-auth --base origin/develop
108+
109+
# With a branch prefix (via flag or LOKI_NEW_PREFIX env var)
110+
❯ lk w a fix-auth --prefix users/danigon/
111+
```
112+
113+
The worktree is created at `<parent>/<repo>_<name>` (e.g., `~/repos/my-project_fix-auth`).
114+
115+
**Flags:**
116+
- `--base` / `-b` — Base ref (default: `origin/main`, env: `LOKI_WORKTREE_BASE`)
117+
- `--prefix` — Branch name prefix (env: `LOKI_NEW_PREFIX`)
118+
119+
#### `worktree remove [name]` (alias: `r`)
120+
Remove a worktree and delete its local branch.
121+
122+
```sh
123+
# From inside the worktree — name is inferred from the directory
124+
~/repos/my-project_fix-auth ❯ lk w r
125+
126+
# Explicit name from anywhere
127+
~/repos/my-project ❯ lk w r fix-auth
128+
129+
# Force remove a dirty worktree
130+
❯ lk w r fix-auth --force
131+
```
132+
133+
**Flags:**
134+
- `--force` / `-f` — Force removal of dirty worktrees
135+
- `--prefix` — Branch name prefix used during creation (env: `LOKI_NEW_PREFIX`)
136+
137+
#### `worktree list` (alias: `l`)
138+
List all worktrees. The current worktree is highlighted in green, and other
139+
worktrees show a copy-paste command to switch to them:
140+
141+
```
142+
❯ lk w l
143+
* my-project [main]
144+
fix-auth [users/dan/fix-auth] lk w s fix-auth | iex
145+
```
146+
147+
#### `worktree switch [name]` (alias: `s`)
148+
Print a `cd` command for switching to a worktree. Designed for use with `eval`:
149+
150+
```bash
151+
# bash/zsh — switch to a named worktree
152+
eval "$(lk w s fix-auth)"
153+
154+
# PowerShell
155+
lk w s fix-auth | iex
156+
157+
# Switch to the main worktree (no name)
158+
eval "$(lk w s)"
159+
```
160+
161+
#### Shell Wrappers
162+
163+
All worktree commands output `cd <path>` to stdout (info goes to stderr), so you can
164+
pipe to your shell for automatic directory switching:
165+
166+
```powershell
167+
# PowerShell
168+
lk w a fix-auth | iex
169+
```
170+
171+
```bash
172+
# bash/zsh
173+
eval "$(lk w a fix-auth)"
174+
```
175+
93176
### `repo stats`
94177
Analyze commits reachable from HEAD to see who has been landing work in a repository. All of the filtering flags operate on commit dates.
95178

src/main.rs

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub mod git;
22
pub mod pruning;
3+
pub mod vars;
4+
pub mod worktree;
35

46
use std::{
57
collections::HashMap,
@@ -31,7 +33,7 @@ fn styles() -> clap::builder::Styles {
3133
.placeholder(AnsiColor::Cyan.on_default())
3234
}
3335

34-
const NO_HOOKS: &str = "core.hooksPath=/dev/null";
36+
use vars::{LOKI_NEW_PREFIX, LOKI_REBASE_TARGET, LOKI_WORKTREE_BASE, NO_HOOKS};
3537

3638
#[derive(Debug, Parser)]
3739
struct CommitOptions {
@@ -94,6 +96,53 @@ enum RepoSubcommand {
9496
Stats(RepoStatsOptions),
9597
}
9698

99+
#[derive(Debug, Subcommand)]
100+
enum WorktreeSubcommand {
101+
/// Create a new worktree and branch.
102+
#[clap(visible_alias = "a")]
103+
Add {
104+
/// Optional prefix to prepend to the branch name.
105+
#[clap(long, env = LOKI_NEW_PREFIX)]
106+
prefix: Option<String>,
107+
108+
/// Base ref to create the worktree from.
109+
#[clap(short, long, default_value = "origin/main", env = LOKI_WORKTREE_BASE)]
110+
base: String,
111+
112+
/// Name parts joined with dashes to form the worktree and branch name.
113+
name: Vec<String>,
114+
},
115+
116+
/// Remove a worktree and its associated branch.
117+
#[clap(visible_alias = "r")]
118+
Remove {
119+
/// Optional prefix used when the branch was created.
120+
#[clap(long, env = LOKI_NEW_PREFIX)]
121+
prefix: Option<String>,
122+
123+
/// Force removal of a dirty worktree.
124+
#[clap(short, long)]
125+
force: bool,
126+
127+
/// Worktree name. If omitted, inferred from the current directory.
128+
name: Vec<String>,
129+
},
130+
131+
/// List all worktrees.
132+
#[clap(visible_alias = "l")]
133+
List,
134+
135+
/// Print a cd command for switching to a worktree (use with eval).
136+
///
137+
/// bash/zsh: eval "$(lk w s <name>)"
138+
/// PowerShell: lk w s <name> | Invoke-Expression
139+
#[clap(visible_alias = "s")]
140+
Switch {
141+
/// Worktree name. If omitted, switches to the main worktree.
142+
name: Vec<String>,
143+
},
144+
}
145+
97146
#[derive(Parser)]
98147
#[clap(version, about, author, color = clap::ColorChoice::Auto, styles = styles())]
99148
enum Cli {
@@ -102,7 +151,7 @@ enum Cli {
102151
#[clap(visible_alias = "n")]
103152
New {
104153
/// Optional prefix to prepend to the generated branch name.
105-
#[clap(long, env = "LOKI_NEW_PREFIX")]
154+
#[clap(long, env = LOKI_NEW_PREFIX)]
106155
prefix: Option<String>,
107156

108157
/// List of names to join with dashes to form a valid branch name.
@@ -138,7 +187,7 @@ enum Cli {
138187
/// Rebase the current branch onto the target branch after fetching.
139188
Rebase {
140189
/// The branch to rebase onto.
141-
#[clap(default_value = "main", env = "LOKI_REBASE_TARGET")]
190+
#[clap(default_value = "main", env = LOKI_REBASE_TARGET)]
142191
target: String,
143192

144193
/// Start an interactive rebase.
@@ -159,13 +208,18 @@ enum Cli {
159208
command: RepoSubcommand,
160209
},
161210

211+
/// Manage git worktrees.
212+
#[clap(visible_alias = "w")]
213+
Worktree {
214+
#[clap(subcommand)]
215+
command: WorktreeSubcommand,
216+
},
217+
162218
/// Push the main branch to the release branch.
163219
#[clap(visible_alias = "r")]
164220
Release,
165221
}
166222

167-
const LOKI_NEW_PREFIX: &str = "LOKI_NEW_PREFIX";
168-
169223
fn main() -> Result<(), String> {
170224
let cli = Cli::parse();
171225

@@ -181,6 +235,18 @@ fn main() -> Result<(), String> {
181235
Cli::Repo {
182236
command: RepoSubcommand::Stats(options),
183237
} => repo_stats(options),
238+
Cli::Worktree { command } => match command {
239+
WorktreeSubcommand::Add { name, base, prefix } => {
240+
worktree::worktree_add(name, base, prefix.as_deref())
241+
}
242+
WorktreeSubcommand::Remove {
243+
name,
244+
force,
245+
prefix,
246+
} => worktree::worktree_remove(name, *force, prefix.as_deref()),
247+
WorktreeSubcommand::List => worktree::worktree_list(),
248+
WorktreeSubcommand::Switch { name } => worktree::worktree_switch(name),
249+
},
184250
Cli::Release => release(),
185251
}
186252
}

src/vars.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// Environment variable for the branch name prefix used by `new` and `worktree add`.
2+
pub const LOKI_NEW_PREFIX: &str = "LOKI_NEW_PREFIX";
3+
4+
/// Environment variable for the base ref used by `worktree add`.
5+
pub const LOKI_WORKTREE_BASE: &str = "LOKI_WORKTREE_BASE";
6+
7+
/// Environment variable for the rebase target branch.
8+
pub const LOKI_REBASE_TARGET: &str = "LOKI_REBASE_TARGET";
9+
10+
/// Git config override that disables all hooks.
11+
pub const NO_HOOKS: &str = "core.hooksPath=/dev/null";

0 commit comments

Comments
 (0)