Skip to content
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Use `--hook <name>` for project-specific hooks, or `--hook` alone to list availa
| `ez create <name>` | Create worktree + branch (default). `--from main` for independent work. `--no-worktree` for branch only. |
| `ez list` | Dashboard for all local branches: PRs, CI, age, ports, and working tree state. `--json` for machine output. |
| `ez delete [name]` | Delete branch + worktree. Auto-detects worktrees and best-effort stops listeners on the branch dev port. `--yes` for agents. |
| `ez push` | Push + create/update PR. `-am "msg"` to stage+commit+push in one step. |
| `ez push` | Push + create/update PR. `-am "msg"` to stage+commit+push in one step. `--no-pr` skips PR updates, `--pr` overrides `no_pr` config. |

### Committing

Expand Down Expand Up @@ -194,7 +194,9 @@ Intended workflow:

| Command | Description |
|---------|-------------|
| `ez init --yes` | Initialize ez and accept recommended non-interactive defaults |
| `ez setup --yes` | Configure shell integration |
| `ez config list/get/set` | View or update repo settings such as `default_from`, `draft`, `no_pr`, and `rerere` |
| `ez skill install` | Install the ez-workflow skill for AI agents |
| `ez update` | Update to latest version |

Expand Down
4 changes: 3 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ ez status # stack info + working tree state
ez push -am "feat: done" # stage tracked changes + commit + push + create PR
ez push -Am "feat: done" # include untracked files too
ez push --title "feat: auth" --body "..." # with PR metadata
ez push --no-pr # push branch only
ez push --pr # create/update PR even if no_pr config is true
ez submit # push entire stack
ez merge --yes # merge bottom PR non-interactively
ez merge --stack --yes # merge the current linear stack bottom-to-top
Expand Down Expand Up @@ -165,4 +167,4 @@ Check `redundant_commits > 0` after sync/restack — means commits were auto-dro

## Advanced Commands

See [reference.md](reference.md) for the full command reference: `ez adopt`, `ez commit`, `ez amend`, `ez diff`, `ez status`, `ez restack`, `ez log`, `ez move`, `ez merge`, `ez switch`, `ez pr-edit`, `ez draft`/`ez ready`, `ez pr-link`, `ez update`, `ez setup`, `ez skill install`.
See [reference.md](reference.md) for the full command reference: `ez adopt`, `ez commit`, `ez amend`, `ez diff`, `ez status`, `ez restack`, `ez log`, `ez move`, `ez merge`, `ez switch`, `ez pr-edit`, `ez draft`/`ez ready`, `ez pr-link`, `ez config`, `ez update`, `ez setup`, `ez skill install`.
7 changes: 7 additions & 0 deletions reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ Preferred workflow:

| Intent | Command |
|--------|---------|
| Push current branch and create/update PR | `ez push` |
| Push without creating/updating PR | `ez push --no-pr` |
| Force PR creation when `no_pr` config is true | `ez push --pr` |
| Print PR URL to stdout | `ez pr-link` |
| Edit PR title/body | `ez pr-edit --title "..." --body "..."` |
| Mark PR as draft / ready | `ez draft` / `ez ready` |
Expand Down Expand Up @@ -81,8 +84,12 @@ Preferred workflow:

| Intent | Command |
|--------|---------|
| Initialize ez non-interactively | `ez init --yes` |
| Install skill in repo | `ez skill install` |
| Shell integration | `ez setup --yes` |
| List repo config | `ez config list` |
| Read repo config | `ez config get default_from` |
| Update repo config | `ez config set draft true` |
| Update ez | `ez update` |
| Check for updates | `ez update --check` |

Expand Down
167 changes: 164 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ pub enum Commands {
#[command(after_help = "\
Examples:
ez init
ez init --yes
ez init --trunk main
ez init --trunk develop")]
Init {
/// Trunk branch name (auto-detected if not provided)
#[arg(long)]
trunk: Option<String>,

/// Accept recommended defaults without prompting (for agents and scripts)
#[arg(short, long)]
yes: bool,
},

/// Adopt branches from GitHub PRs into the local stack
Expand Down Expand Up @@ -151,14 +156,28 @@ Examples:
ez push
ez push --title \"feat: add auth\" --body \"Adds login/logout.\"
ez push --draft
ez push --pr
ez push --no-pr
ez push --stack
ez push -am \"feat: add auth\"
ez push -Am \"feat: add auth and new snapshots\"")]
Push {
/// Create a draft PR
#[arg(long)]
#[arg(long, conflicts_with = "no_pr")]
draft: bool,

/// Override draft config to create a ready-for-review PR
#[arg(long, conflicts_with = "no_pr")]
no_draft: bool,

/// Push the branch without creating or updating a PR
#[arg(long, conflicts_with_all = ["draft", "no_draft", "title", "body", "body_file"])]
no_pr: bool,

/// Create/update a PR even when config no_pr is true
#[arg(long, conflicts_with = "no_pr")]
pr: bool,

/// PR title (defaults to first commit message)
#[arg(long)]
title: Option<String>,
Expand Down Expand Up @@ -201,12 +220,19 @@ Examples:
#[command(after_help = "\
Examples:
ez submit
ez submit --draft")]
ez submit --draft

Note: --draft only affects newly created PRs. Existing PRs are not changed.
Use `ez ready` to undraft an existing PR.")]
Submit {
/// Create draft PRs
/// Create draft PRs (only affects new PRs, not existing ones)
#[arg(long)]
draft: bool,

/// Override draft config to create ready-for-review PRs
#[arg(long)]
no_draft: bool,

/// PR title (defaults to first commit message)
#[arg(long)]
title: Option<String>,
Expand Down Expand Up @@ -456,6 +482,9 @@ Examples:
eval \"$(ez shell-init)\"")]
ShellInit,

/// View and update ez settings for the current repo
Config(ConfigArgs),

/// Manage git worktrees
Worktree(WorktreeArgs),
}
Expand Down Expand Up @@ -511,6 +540,49 @@ Examples:
Clear,
}

#[derive(Args)]
pub struct ConfigArgs {
#[command(subcommand)]
pub command: ConfigCommands,
}

#[derive(Subcommand)]
pub enum ConfigCommands {
/// List all config settings
#[command(after_help = "\
Examples:
ez config list")]
List,

/// Get the value of a config key
#[command(after_help = "\
Examples:
ez config get trunk
ez config get remote")]
Get {
/// Config key to read
key: String,
},

/// Set a config key to a new value
#[command(after_help = "\
Examples:
ez config set trunk develop
ez config set remote fork
ez config set default_from dev
ez config set repo owner/name
ez config set draft true
ez config set no_pr true
ez config set rerere true")]
Set {
/// Config key to update
key: String,

/// New value
value: String,
},
}

#[derive(Args)]
pub struct WorktreeArgs {
#[command(subcommand)]
Expand Down Expand Up @@ -582,6 +654,19 @@ mod tests {
use super::*;
use clap::Parser;

#[test]
fn parses_init_yes_flag() {
let cli = Cli::try_parse_from(["ez", "init", "--yes"]).expect("parse init --yes");

match cli.command {
Commands::Init { yes, trunk } => {
assert!(yes);
assert!(trunk.is_none());
}
_ => panic!("expected init command"),
}
}

#[test]
fn parses_commit_with_paths_after_double_dash() {
let cli = Cli::try_parse_from([
Expand Down Expand Up @@ -696,16 +781,92 @@ mod tests {
message,
stage_all,
stage_all_files,
no_pr,
no_draft,
..
} => {
assert_eq!(message.as_deref(), Some("feat: ship new files"));
assert!(!stage_all);
assert!(stage_all_files);
assert!(!no_pr);
assert!(!no_draft);
}
_ => panic!("expected push command"),
}
}

#[test]
fn parses_push_no_pr_flag() {
let cli = Cli::try_parse_from(["ez", "push", "--no-pr"]).expect("parse push --no-pr");

match cli.command {
Commands::Push {
no_pr, pr, draft, ..
} => {
assert!(no_pr);
assert!(!pr);
assert!(!draft);
}
_ => panic!("expected push command"),
}
}

#[test]
fn parses_push_pr_flag() {
let cli = Cli::try_parse_from(["ez", "push", "--pr"]).expect("parse push --pr");

match cli.command {
Commands::Push { pr, no_pr, .. } => {
assert!(pr);
assert!(!no_pr);
}
_ => panic!("expected push command"),
}
}

#[test]
fn push_no_pr_conflicts_with_draft() {
let result = Cli::try_parse_from(["ez", "push", "--no-pr", "--draft"]);
assert!(result.is_err(), "--no-pr and --draft should conflict");
}

#[test]
fn push_pr_conflicts_with_no_pr() {
let result = Cli::try_parse_from(["ez", "push", "--pr", "--no-pr"]);
assert!(result.is_err(), "--pr and --no-pr should conflict");
}

#[test]
fn parses_push_no_draft_flag() {
let cli = Cli::try_parse_from(["ez", "push", "--no-draft"]).expect("parse push --no-draft");

match cli.command {
Commands::Push {
no_draft, draft, ..
} => {
assert!(no_draft);
assert!(!draft);
}
_ => panic!("expected push command"),
}
}

#[test]
fn parses_submit_no_draft_flag() {
let cli =
Cli::try_parse_from(["ez", "submit", "--no-draft"]).expect("parse submit --no-draft");

match cli.command {
Commands::Submit {
no_draft, draft, ..
} => {
assert!(no_draft);
assert!(!draft);
}
_ => panic!("expected submit command"),
}
}

#[test]
fn parses_worktree_delete_yes_flag() {
let cli =
Expand Down
5 changes: 5 additions & 0 deletions src/cmd/checkout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ mod tests {
StackState {
trunk: "main".to_string(),
remote: "origin".to_string(),
default_from: None,
repo: None,
draft: None,
no_pr: None,
rerere: None,
branches,
}
}
Expand Down
Loading
Loading