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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **`gwm sync [<pattern>] [--merge]`** ([#24](https://github.com/kbrdn1/gwm-cli/issues/24)). Fetch a worktree's upstream and rebase its branch onto it — or merge with `--merge`. Resolves the target worktree by fuzzy pattern (defaults to the CWD worktree). Refuses a dirty working tree and a branch with no upstream; a conflicting rebase/merge is aborted so the worktree stays usable, with an actionable error. Read-side inspection uses libgit2; the fetch/rebase/merge steps shell out to `git` so the user's configured credentials are honoured.
- **`cargo-binstall` support** ([#27](https://github.com/kbrdn1/gwm-cli/issues/27)). `[package.metadata.binstall]` in `Cargo.toml` lets `cargo binstall gwm` pull the prebuilt archive (`gwm-v{version}-{target}.tar.gz`, `.zip` on Windows) straight from the GitHub Release — no Rust toolchain or libgit2 compile at install time. Pinned against artefact-naming drift by `tests/binstall_metadata_tests.rs`.

## Past releases
Expand Down
20 changes: 20 additions & 0 deletions docs/3.cli/1.reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,26 @@ gwm bootstrap auth # on a fuzzy-matched name

Useful after editing `.gwm.toml` or adding new `[[bootstrap.copy]]` rules. Same `✓ / ! / ✗` report as `gwm create`.

## `gwm sync [<pattern>] [--merge]`

Fetch a worktree's upstream and bring its branch up to date — rebase by default, or merge with `--merge`.

```bash
gwm sync # the CWD worktree, rebase onto upstream
gwm sync auth # a fuzzy-matched worktree
gwm sync auth --merge # merge the upstream instead of rebasing
```

Resolves the target like `gwm bootstrap` (fuzzy pattern, defaults to the worktree containing the CWD — which may be the main worktree, so you can sync trunk too). It runs `git fetch` for the upstream's remote, recomputes how far behind the branch is, then integrates only when there's something to integrate. Reports a single `✓` line (`already up to date` / `rebased N commit(s)` / `merged N commit(s)`).

Guard rails:

- **Dirty working tree** → refuses before touching the remote (`commit or stash`). A rebase/merge on top of uncommitted work is how changes get lost.
- **No upstream configured** → errors with the `git branch --set-upstream-to=<remote>/<branch>` fix.
- **Conflict** → the rebase/merge is **aborted** so the worktree is left usable, and you're told to reconcile by hand.

The fetch / rebase / merge steps shell out to your `git` (so SSH keys, credential helpers, and `insteadOf` rules all apply); the dirty / upstream / ahead-behind inspection uses libgit2.

## `gwm remove <pattern> [--delete-branch]`

Remove a worktree by fuzzy match. The branch survives by default.
Expand Down
2 changes: 1 addition & 1 deletion docs/3.cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ navigation:

`gwm <subcommand>` is the scriptable side of gwm — designed to be safe in pipelines, shell completions, and pre-commit hooks. Every subcommand exits with a meaningful code (`0` ok, `1` warning, `2` failure) so you can wire `gwm doctor` into CI without parsing stdout.

- **[Reference](/cli/reference)** — every subcommand, exhaustive (`init`, `create`, `list`, `path`, `cd`, `switch`, `bootstrap`, `remove`, `prune`, `link`, `unlink`, `open`, `status`, `doctor`, `tmux`, `zellij`, `completions`, `shell-init`).
- **[Reference](/cli/reference)** — every subcommand, exhaustive (`init`, `create`, `list`, `path`, `cd`, `switch`, `bootstrap`, `sync`, `remove`, `prune`, `link`, `unlink`, `open`, `status`, `doctor`, `tmux`, `zellij`, `completions`, `shell-init`).
- **[Shell completions](/cli/completions)** — generate completion scripts for zsh / bash / fish / PowerShell / elvish, plus dynamic worktree-name completion via `gwm list --format=names`.
- **[Multiplexer integration](/cli/multiplexer)** — `gwm tmux` / `gwm zellij` to open a worktree in a new tab or pane of the current session.

Expand Down
6 changes: 5 additions & 1 deletion skills/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Source: https://github.com/kbrdn1/gwm-cli — current version: `0.6.0`.

## When to use this skill

- User runs or asks about any `gwm <subcommand>`: `init`, `list`, `create`, `remove`, `path` / `cd`, `bootstrap`, `prune`, `doctor`, `types`, `completions`, `shell-init`, `switch` (alias `s`), `tmux`, `zellij`, `link`, `unlink`, `open`, `status`.
- User runs or asks about any `gwm <subcommand>`: `init`, `list`, `create`, `remove`, `path` / `cd`, `bootstrap`, `sync`, `prune`, `doctor`, `types`, `completions`, `shell-init`, `switch` (alias `s`), `tmux`, `zellij`, `link`, `unlink`, `open`, `status`.
- User opens the TUI by running `gwm` alone in a repo, or the picker via `gwm switch` / `gwm s`.
- User mentions `.gwm.toml` (per-repo config) or any of its sections: `[worktree]`, `[doctor]`, `[tui]`, `[tui.open]`, `[git_tui]`, `[review]`, `[[bootstrap.copy]]`, `[[bootstrap.guard]]`, `[[bootstrap.no_symlink]]`, `[[bootstrap.command]]`, `[bootstrap.fallback.*]`.
- User asks about composable `when` predicates (`file_exists:`, `cmd_exists:`, `env_set:`, `env_eq:`, `glob_exists:`) and the `!` / `&&` / `||` operators.
Expand Down Expand Up @@ -88,6 +88,9 @@ gwm path <pattern> # print path (fuzzy match) → use $(g
gwm cd <pattern> # alias of `gwm path`
gwm bootstrap # re-run bootstrap on cwd worktree
gwm bootstrap <pattern> # ...or on a named worktree
gwm sync # fetch + rebase the cwd worktree onto its upstream
gwm sync <pattern> # ...or a fuzzy-matched worktree
gwm sync <pattern> --merge # merge the upstream instead of rebasing
gwm remove <pattern> # remove (fuzzy). Keeps the branch.
gwm remove <pattern> --delete-branch # also drop the local branch
gwm prune # clean stale .git/worktrees entries
Expand Down Expand Up @@ -700,6 +703,7 @@ gwm list # list worktrees
gwm path|cd <pat> # print path
gwm switch | gwm s | gcd # interactive picker (cd via shell wrapper)
gwm bootstrap [pat] # re-run bootstrap
gwm sync [pat] [--merge] # fetch + rebase (or merge) onto upstream
gwm remove <pat> [-b] # remove (-b drops branch)
gwm prune # clean stale refs
gwm types # show branch types
Expand Down
80 changes: 80 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::multiplexer::{
};
use crate::naming::{parse_branch, BranchSpec};
use crate::pr_templates::{self, PrTemplateContext};
use crate::sync::{self, SyncAction, SyncReport, SyncStrategy};
use crate::trust::{self, TrustLedger, TrustMode, TrustOutcome};
use crate::worktree;
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
Expand Down Expand Up @@ -187,6 +188,25 @@ pub enum Command {
#[arg(long, value_name = "PHASES")]
skip_hooks: Option<String>,
},
/// Fetch + rebase (or merge) a worktree's branch onto its upstream.
///
/// Resolves the target worktree (defaults to the CWD worktree when
/// no pattern is given), runs `git fetch` for its upstream's remote,
/// then rebases the branch onto the upstream — or merges with
/// `--merge`. Reports the outcome with the same ✓ / ! / ✗ sigils as
/// the rest of gwm.
///
/// Refuses up front on a dirty working tree (commit or stash first)
/// and on a branch with no upstream configured. A conflicting
/// rebase/merge is aborted so the worktree stays usable, and the
/// user is told to reconcile by hand. Issue #24.
Sync {
/// Worktree name/pattern; defaults to the worktree containing the CWD.
pattern: Option<String>,
/// Merge the upstream instead of rebasing onto it.
#[arg(long)]
merge: bool,
},
/// Prune stale worktree references (admin files without a working dir).
Prune {
/// List the prunable worktrees (name + path + reason) without
Expand Down Expand Up @@ -738,6 +758,7 @@ pub fn run(cli: Cli) -> Result<()> {
} => cmd_remove(pattern, delete_branch, dry_run, force, skip_hooks, mode),
Command::Path { pattern } => cmd_path(pattern),
Command::Bootstrap { target, skip_hooks } => cmd_bootstrap(target, skip_hooks, mode),
Command::Sync { pattern, merge } => cmd_sync(pattern, merge),
Command::Prune { dry_run } => cmd_prune(dry_run),
Command::Doctor => cmd_doctor(),
Command::Types { gitmoji } => cmd_types(gitmoji),
Expand Down Expand Up @@ -1503,6 +1524,65 @@ fn cmd_bootstrap(target: Option<String>, skip_hooks: Option<String>, trust_mode:
Ok(())
}

fn cmd_sync(pattern: Option<String>, merge: bool) -> Result<()> {
// Resolve the target worktree. With a pattern, fuzzy-match against the
// main repo's worktree list like the rest of gwm. Without one, default
// to the worktree *containing* the CWD — which, unlike `find_fuzzy`,
// may legitimately be the main worktree (syncing trunk is valid). We
// discover that worktree's own workdir (not the CWD basename) so a
// `gwm sync` from a subdirectory still names and targets the worktree
// root rather than the subdir.
let (target_path, name) = match pattern {
Some(p) => {
let repo = worktree::discover_repo(None)?;
let found = worktree::find_fuzzy(&repo, &p)?;
(found.path, found.name)
}
None => {
let cwd = std::env::current_dir()?;
let repo = Repository::discover(&cwd).map_err(|_| GwmError::NotInGitRepo)?;
let workdir = repo.workdir().ok_or(GwmError::NotInGitRepo)?.to_path_buf();
let name = workdir
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "worktree".into());
(workdir, name)
}
};

let strategy = if merge {
SyncStrategy::Merge
} else {
SyncStrategy::Rebase
};
let report = sync::sync(&target_path, strategy)?;
print!("{}", format_sync_report(&name, &report));
Ok(())
}

/// Render a successful [`SyncReport`] as a single ✓ status line. The
/// error paths (dirty tree, missing upstream, conflicts) surface as
/// `GwmError` and are printed by `main`'s top-level handler, so this
/// only ever formats the success cases.
pub fn format_sync_report(name: &str, report: &SyncReport) -> String {
match report.action {
SyncAction::UpToDate => {
format!("✓ {} already up to date with {}\n", name, report.upstream)
}
SyncAction::Integrated => {
let verb = match report.strategy {
SyncStrategy::Rebase => "rebased",
SyncStrategy::Merge => "merged",
};
let plural = if report.behind_before == 1 { "" } else { "s" };
format!(
"✓ {} {} {} commit{} from {}\n",
name, verb, report.behind_before, plural, report.upstream
)
}
}
}

fn cmd_prune(dry_run: bool) -> Result<()> {
let repo = worktree::discover_repo(None)?;
if dry_run {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod milestones;
pub mod multiplexer;
pub mod naming;
pub mod pr_templates;
pub mod sync;
pub mod templating;
pub mod trust;
pub mod tui;
Expand Down
Loading
Loading