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
14 changes: 7 additions & 7 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "loki-cli"
version = "2.0.0"
version = "2.1.0"
authors = ["Kyle W. Rader"]
description = "Loki: 🚀 A Git productivity tool"
homepage = "https://github.com/kyle-rader/loki-cli"
Expand Down
101 changes: 92 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ Loki: 🚀 A Git productivity tool
Usage: lk <COMMAND>

Commands:
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]
push Push the current branch to origin with --set-upstream [aliases: p]
pull Pull with --prune deleting local branches pruned from the remote
fetch Fetch with --prune deleting local branches pruned from the remote
save Add, commit, and push using a timestamp based commit message [aliases: s]
commit Commit local changes [aliases: c]
rebase Rebase the current branch onto the target branch after fetching
no-hooks Run any command without triggering any hooks [aliases: x]
help Print this message or the help of the given subcommand(s)
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]
push Push the current branch to origin with --set-upstream [aliases: p]
pull Pull with --prune deleting local branches pruned from the remote
fetch Fetch with --prune deleting local branches pruned from the remote
save Add, commit, and push using a timestamp based commit message [aliases: s]
commit Commit local changes [aliases: c]
rebase Rebase the current branch onto the target branch after fetching
worktree Manage git worktrees [aliases: w]
no-hooks Run any command without triggering any hooks [aliases: x]
help Print this message or the help of the given subcommand(s)

Options:
-h, --help Print help
Expand Down Expand Up @@ -90,6 +91,88 @@ Execute a git commit without running any hooks
lk x -- commit -m "Update Readme without running hooks"
```

### `worktree`
Alias: `w`

Manage git worktrees for parallel development workflows. Subcommands:

#### `worktree add <name>` (alias: `a`)
Create a new worktree as a sibling directory and set up a branch with upstream tracking.

```sh
# Creates worktree at ../my-project_fix-auth, creates and pushes branch
❯ lk w a fix-auth

# With a custom base ref
❯ lk w a fix-auth --base origin/develop

# With a branch prefix (via flag or LOKI_NEW_PREFIX env var)
❯ lk w a fix-auth --prefix users/danigon/
```

The worktree is created at `<parent>/<repo>_<name>` (e.g., `~/repos/my-project_fix-auth`).

**Flags:**
- `--base` / `-b` — Base ref (default: `origin/main`, env: `LOKI_WORKTREE_BASE`)
- `--prefix` — Branch name prefix (env: `LOKI_NEW_PREFIX`)

#### `worktree remove [name]` (alias: `r`)
Remove a worktree and delete its local branch.

```sh
# From inside the worktree — name is inferred from the directory
~/repos/my-project_fix-auth ❯ lk w r

# Explicit name from anywhere
~/repos/my-project ❯ lk w r fix-auth

# Force remove a dirty worktree
❯ lk w r fix-auth --force
```

**Flags:**
- `--force` / `-f` — Force removal of dirty worktrees
- `--prefix` — Branch name prefix used during creation (env: `LOKI_NEW_PREFIX`)

#### `worktree list` (alias: `l`)
List all worktrees. The current worktree is highlighted in green, and other
worktrees show a copy-paste command to switch to them:

```
❯ lk w l
* my-project [main]
fix-auth [users/dan/fix-auth] lk w s fix-auth | iex
```

#### `worktree switch [name]` (alias: `s`)
Print a `cd` command for switching to a worktree. Designed for use with `eval`:

```bash
# bash/zsh — switch to a named worktree
eval "$(lk w s fix-auth)"

# PowerShell
lk w s fix-auth | iex

# Switch to the main worktree (no name)
eval "$(lk w s)"
```

#### Shell Wrappers

All worktree commands output `cd <path>` to stdout (info goes to stderr), so you can
pipe to your shell for automatic directory switching:

```powershell
# PowerShell
lk w a fix-auth | iex
```

```bash
# bash/zsh
eval "$(lk w a fix-auth)"
```

### `repo stats`
Analyze commits reachable from HEAD to see who has been landing work in a repository. All of the filtering flags operate on commit dates.

Expand Down
76 changes: 71 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod git;
pub mod pruning;
pub mod vars;
pub mod worktree;

use std::{
collections::HashMap,
Expand Down Expand Up @@ -31,7 +33,7 @@ fn styles() -> clap::builder::Styles {
.placeholder(AnsiColor::Cyan.on_default())
}

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

#[derive(Debug, Parser)]
struct CommitOptions {
Expand Down Expand Up @@ -94,6 +96,53 @@ enum RepoSubcommand {
Stats(RepoStatsOptions),
}

#[derive(Debug, Subcommand)]
enum WorktreeSubcommand {
/// Create a new worktree and branch.
#[clap(visible_alias = "a")]
Add {
/// Optional prefix to prepend to the branch name.
#[clap(long, env = LOKI_NEW_PREFIX)]
prefix: Option<String>,

/// Base ref to create the worktree from.
#[clap(short, long, default_value = "origin/main", env = LOKI_WORKTREE_BASE)]
base: String,

/// Name parts joined with dashes to form the worktree and branch name.
name: Vec<String>,
},

/// Remove a worktree and its associated branch.
#[clap(visible_alias = "r")]
Remove {
/// Optional prefix used when the branch was created.
#[clap(long, env = LOKI_NEW_PREFIX)]
prefix: Option<String>,

/// Force removal of a dirty worktree.
#[clap(short, long)]
force: bool,

/// Worktree name. If omitted, inferred from the current directory.
name: Vec<String>,
},

/// List all worktrees.
#[clap(visible_alias = "l")]
List,

/// Print a cd command for switching to a worktree (use with eval).
///
/// bash/zsh: eval "$(lk w s <name>)"
/// PowerShell: lk w s <name> | Invoke-Expression
#[clap(visible_alias = "s")]
Switch {
/// Worktree name. If omitted, switches to the main worktree.
name: Vec<String>,
},
}

#[derive(Parser)]
#[clap(version, about, author, color = clap::ColorChoice::Auto, styles = styles())]
enum Cli {
Expand All @@ -102,7 +151,7 @@ enum Cli {
#[clap(visible_alias = "n")]
New {
/// Optional prefix to prepend to the generated branch name.
#[clap(long, env = "LOKI_NEW_PREFIX")]
#[clap(long, env = LOKI_NEW_PREFIX)]
prefix: Option<String>,

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

/// Start an interactive rebase.
Expand All @@ -159,13 +208,18 @@ enum Cli {
command: RepoSubcommand,
},

/// Manage git worktrees.
#[clap(visible_alias = "w")]
Worktree {
#[clap(subcommand)]
command: WorktreeSubcommand,
},

/// Push the main branch to the release branch.
#[clap(visible_alias = "r")]
Release,
}

const LOKI_NEW_PREFIX: &str = "LOKI_NEW_PREFIX";

fn main() -> Result<(), String> {
let cli = Cli::parse();

Expand All @@ -181,6 +235,18 @@ fn main() -> Result<(), String> {
Cli::Repo {
command: RepoSubcommand::Stats(options),
} => repo_stats(options),
Cli::Worktree { command } => match command {
WorktreeSubcommand::Add { name, base, prefix } => {
worktree::worktree_add(name, base, prefix.as_deref())
}
WorktreeSubcommand::Remove {
name,
force,
prefix,
} => worktree::worktree_remove(name, *force, prefix.as_deref()),
WorktreeSubcommand::List => worktree::worktree_list(),
WorktreeSubcommand::Switch { name } => worktree::worktree_switch(name),
},
Cli::Release => release(),
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/vars.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Environment variable for the branch name prefix used by `new` and `worktree add`.
pub const LOKI_NEW_PREFIX: &str = "LOKI_NEW_PREFIX";

/// Environment variable for the base ref used by `worktree add`.
pub const LOKI_WORKTREE_BASE: &str = "LOKI_WORKTREE_BASE";

/// Environment variable for the rebase target branch.
pub const LOKI_REBASE_TARGET: &str = "LOKI_REBASE_TARGET";

/// Git config override that disables all hooks.
pub const NO_HOOKS: &str = "core.hooksPath=/dev/null";
Loading
Loading