diff --git a/CLAUDE.md b/CLAUDE.md index f0c4341..5472075 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,6 +64,7 @@ The `specs/` directory contains detailed design documents that describe each fea | `specs/014-continue-abort.md` | Continue or abort a paused loom operation | | `specs/015-swap.md` | Swap two commits or two branch sections | | `specs/016-diff.md` | Diff: short-ID–aware wrapper around git diff | +| `specs/017-switch.md` | Switch to any branch for testing without weaving | ## Build & Run Commands diff --git a/README.md b/README.md index 54b1699..c9b6d44 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Commits: Branches: branch, br Manage feature branches (create, merge, unmerge) + switch, sw Switch to any branch for testing (without weaving) Inspection: status Show the branch-aware status (default command) diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 0c29b8a..2f1d926 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -34,6 +34,7 @@ - [reword](commands/reword.md) - [drop](commands/drop.md) - [branch](commands/branch.md) +- [switch](commands/switch.md) - [status](commands/status.md) - [show](commands/show.md) - [diff](commands/diff.md) diff --git a/docs/src/commands/README.md b/docs/src/commands/README.md index 0ec0a6f..f0302ac 100644 --- a/docs/src/commands/README.md +++ b/docs/src/commands/README.md @@ -19,6 +19,7 @@ Commits: Branches: branch, br Manage feature branches (create, merge, unmerge) + switch, sw Switch to any branch for testing (without weaving) Inspection: status Show the branch-aware status (default command) diff --git a/docs/src/commands/switch.md b/docs/src/commands/switch.md new file mode 100644 index 0000000..6fc3280 --- /dev/null +++ b/docs/src/commands/switch.md @@ -0,0 +1,75 @@ +# switch + +Check out any branch for testing without weaving it into the integration branch. + +## Usage + +``` +git-loom switch [] +git-loom sw [] +``` + +If no branch is given, an interactive picker lists all local branches and all remote-only branches. + +### Arguments + +| Argument | Description | +|----------|-------------| +| `` | Branch to switch to: local branch name, remote-tracking name (e.g. `origin/feature-x`), or short ID of a woven branch. Optional — omit to pick interactively. | + +## What It Does + +### When Target is a Local Branch + +HEAD moves to the named local branch (attached, not detached). No branch refs or commit history are changed. + +### When Target is a Remote-Only Branch + +A remote-only branch is a remote-tracking ref (e.g. `origin/colleague-work`) with no local counterpart. HEAD is detached at that ref's commit. No local tracking branch is created, so there is nothing to clean up afterward. + +### Interactive Picker (no argument) + +Shows all local branches except the current one, followed by all remote-only branches. Selecting a local branch switches normally; selecting a remote-only branch detaches HEAD. + +## Target Resolution + +1. **Local branch name** — exact match against local branches +2. **Remote branch name** — exact match against remote-tracking refs (e.g. `origin/feature-x`) +3. **Short ID** — best-effort lookup via the woven-branch graph (requires being on an integration branch with upstream tracking configured; silently skipped otherwise) + +## Examples + +### Switch to a local branch + +```bash +git-loom switch feature-x +# ✓ Switched to `feature-x` +``` + +### Switch using a short ID + +```bash +git-loom switch fx +# ✓ Switched to `feature-x` +``` + +### Inspect a remote-only branch + +```bash +git-loom switch origin/colleague-work +# ✓ Detached HEAD at `origin/colleague-work` +# No local branch is created. +``` + +### Return to the integration branch + +```bash +git-loom switch integration +# ✓ Switched to `integration` +``` + +## Prerequisites + +- Must be in a git repository with a working tree (not bare) +- Working tree must be clean: no staged changes and no unstaged modifications to tracked files (untracked files are allowed) +- Blocked while a loom operation is paused — run [`continue`](continue.md) or [`abort`](abort.md) first diff --git a/specs/017-switch.md b/specs/017-switch.md new file mode 100644 index 0000000..503cf77 --- /dev/null +++ b/specs/017-switch.md @@ -0,0 +1,241 @@ +# Spec 017: Switch + +## Overview + +`git loom switch` lets you check out any branch — local or remote — for +quick inspection or testing, without weaving it into the integration branch. +Remote-only branches (those that exist on the remote but have no local +counterpart) detach HEAD at the remote ref rather than creating a tracking +branch. The command refuses to run when the working tree has staged or +unstaged changes to tracked files. + +## Why Switch? + +While working inside an integration branch, you sometimes need to briefly +inspect or test another branch — a colleague's remote branch, an upstream +fix, or a local feature branch — without permanently merging it into your +integration topology. + +Raw git requires extra steps, especially for remote branches: + +```bash +# To inspect a remote branch with no local counterpart: +git fetch origin +git switch -c colleague-work --track origin/colleague-work +# ... look around ... +git switch integration +git branch -D colleague-work # manual cleanup +``` + +`git-loom switch` condenses this to a single command and leaves no local +branch behind when testing remote-only refs: + +```bash +git-loom switch origin/colleague-work +# Detaches HEAD at the remote ref — no local branch created +``` + +For local branches, it is a one-step alternative to `git switch` that +integrates with loom's short ID system. + +## CLI + +```bash +git-loom switch [] +git-loom sw [] # alias +``` + +**Arguments:** + +- `` *(optional)*: branch to switch to. Accepted forms: + - Local branch name (e.g. `feature-x`) + - Remote branch name (e.g. `origin/feature-x`) + - Loom short ID for a woven branch (e.g. `fx`) — best-effort; see + [Target Resolution](#target-resolution) + - If omitted, shows an interactive picker listing all local branches and + all remote-only branches + +## What Happens + +### When the Target is a Local Branch + +HEAD moves to the named local branch. The branch pointer is not changed. + +**What changes:** + +- HEAD now points to the local branch (attached, not detached) + +**What stays the same:** + +- All branch refs and commit history +- The integration branch topology (the switch does not weave or unweave anything) +- The working tree (git refuses to switch if files conflict with the target) + +Success message: `✓ Switched to ` + +### When the Target is a Remote-Only Branch + +A remote-only branch is a remote-tracking ref (e.g. `origin/feature-x`) +with no local branch of the same short name. HEAD is detached at the +remote ref's commit. No local tracking branch is created. + +**What changes:** + +- HEAD is detached at the remote ref's OID + +**What stays the same:** + +- All local branch refs (no new branch is created) +- The integration branch topology +- The working tree + +Success message: `✓ Detached HEAD at ` + +To return to normal branch mode, run `git-loom switch ` or +`git switch `. + +### Interactive Picker (no argument) + +When no branch name is provided, an interactive menu is shown with: + +1. All local branches, except the current branch +2. All remote-only branches (remote-tracking refs that have no local + counterpart, excluding `/HEAD` pointers) + +Selecting a local branch switches as described above. Selecting a +remote-only branch detaches HEAD as described above. + +If there are no branches to show (e.g. the repo has only the current +branch and no remotes), the command errors with +`"No branches available to switch to"`. + +## Target Resolution + +When a `` argument is supplied, resolution is attempted in this order: + +1. **Local branch name** — exact match against local branches +2. **Remote branch name** — exact match against remote-tracking refs + (e.g. `origin/feature-x`) +3. **Loom short ID** — best-effort lookup via the woven-branch graph + (see Spec 002). This only succeeds when loom is on an integration + branch with upstream tracking configured. If it fails (e.g. HEAD is + detached or no upstream is set), it is silently skipped. + +Short IDs resolve only to **local** branches (those woven into the +integration branch visible in `git-loom status`). To switch to a +remote-only branch, use its full remote-tracking name (e.g. +`origin/feature-x`). + +If none of the above match, the command errors with +`"Branch '' not found"`. + +## Conflict Recovery + +`switch` never performs a rebase, so it has no conflict recovery. There is +no `.git/loom/state.json` written. `switch` is blocked (like most commands) +when a loom operation is already paused — run `loom continue` or +`loom abort` first. + +## Prerequisites + +- Must be run inside a git repository with a working directory (not a bare + repository). +- The working tree must be clean: no staged changes and no unstaged + modifications to tracked files. Untracked (new) files are allowed. + If dirty, the command errors with: + ``` + Working tree has uncommitted changes. + Stash or commit your changes before switching branches. + ``` +- `switch` is blocked while a loom operation is paused (state file exists). + +## Examples + +### Switch to a local feature branch + +``` +git-loom status +# ● (upstream) +# │╮─ fx [feature-x] +# │● a1b2 Add widget + +git-loom switch feature-x +# ✓ Switched to `feature-x` +# HEAD is now on feature-x +``` + +### Switch using a short ID + +``` +git-loom status +# │╮─ fx [feature-x] + +git-loom switch fx +# ✓ Switched to `feature-x` +``` + +### Inspect a remote-only branch (no local counterpart) + +``` +git fetch origin + +git-loom switch origin/colleague-work +# ✓ Detached HEAD at `origin/colleague-work` +# No local branch is created. + +# ... test, review ... + +git-loom switch integration +# ✓ Switched to `integration` +``` + +### Switch back from detached HEAD + +``` +git-loom switch integration +# ✓ Switched to `integration` +``` + +## Design Decisions + +### Detach HEAD for Remote-Only Branches Instead of Creating a Tracking Branch + +When the target is a remote-only branch, loom detaches HEAD at the remote +ref rather than creating a local tracking branch (`git switch -c name +--track origin/name`). + +Creating a local branch would require the user to clean it up manually +after testing, and it implies ongoing ownership (pushes, tracking status) +that is not intended for a temporary inspection. Detaching HEAD makes the +temporary intent explicit: you are looking at a commit, not claiming a +branch. + +### Clean Working Tree Required + +The command refuses to run when tracked files have staged or unstaged +changes. This prevents a silent loss of context: after switching and +switching back, locally-staged work might appear to be in a different +state relative to the new HEAD. Requiring a clean tree also matches the +mental model that `switch` is for observation, not for carrying work +across branches. + +Untracked files are permitted because git itself allows switching with +untracked files as long as they do not conflict with the target branch. + +### Short IDs Are Best-Effort + +Short ID resolution requires loading the integration branch graph +(`gather_repo_info`), which in turn requires being on a branch with +upstream tracking configured. If the resolution fails for any reason +(detached HEAD, no upstream, not an integration branch), it is silently +skipped and the command falls through to a "not found" error. + +This keeps `switch` usable in any repository state while still supporting +the convenient short-ID workflow when the full loom context is available. + +### Remote-Only Branches Always Shown in Picker + +The interactive picker always includes remote-only branches without +requiring a flag (contrast with `branch merge`, which requires `--all`). +For `switch`, the primary motivation is testing remote branches, so +hiding them by default would undermine the command's purpose. diff --git a/src/completions/git-loom.lua b/src/completions/git-loom.lua index 49aef4a..bfb36b1 100644 --- a/src/completions/git-loom.lua +++ b/src/completions/git-loom.lua @@ -55,6 +55,7 @@ clink.argmatcher("git-loom") "push", "continue", "abort", - "swap" + "swap", + "switch" ) :addflags("--no-color", "--help", "-h") diff --git a/src/completions/git-loom.ps1 b/src/completions/git-loom.ps1 index cd14d33..abbe5f3 100644 --- a/src/completions/git-loom.ps1 +++ b/src/completions/git-loom.ps1 @@ -20,7 +20,8 @@ $_gitLoomCompleter = { @{ Name = 'split'; Description = 'Split a commit into two sequential commits' }, @{ Name = 'continue'; Description = 'Continue a paused loom operation after resolving conflicts' }, @{ Name = 'abort'; Description = 'Abort a paused loom operation and restore original state' }, - @{ Name = 'swap'; Description = 'Swap two commits' } + @{ Name = 'swap'; Description = 'Swap two commits' }, + @{ Name = 'switch'; Description = 'Switch to any branch for testing' } ) $globalFlags = @( diff --git a/src/git_commands/git_branch.rs b/src/git_commands/git_branch.rs index 32aee62..f4d555b 100644 --- a/src/git_commands/git_branch.rs +++ b/src/git_commands/git_branch.rs @@ -47,6 +47,24 @@ pub fn delete(workdir: &Path, name: &str) -> Result<()> { Ok(()) } +/// Switch to an existing local branch. +/// +/// Wraps `git switch `. +pub fn switch(workdir: &Path, name: &str) -> Result<()> { + run_git(workdir, &["switch", name]) + .with_context(|| format!("Failed to switch to branch '{}'", name))?; + Ok(()) +} + +/// Detach HEAD at a ref without creating a local branch. +/// +/// Wraps `git switch --detach `. +pub fn switch_detach(workdir: &Path, refspec: &str) -> Result<()> { + run_git(workdir, &["switch", "--detach", refspec]) + .with_context(|| format!("Failed to detach HEAD at '{}'", refspec))?; + Ok(()) +} + /// Create a new branch at a remote tracking ref and switch to it. /// /// Wraps `git switch -c --track `. diff --git a/src/main.rs b/src/main.rs index 3c4f7b4..af4f8fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ mod show; mod split; mod status; mod swap; +mod switch; mod trace; mod transaction; mod update; @@ -66,6 +67,7 @@ const GROUPED_COMMANDS: &str = "\ \x1b[1;33mBranches:\x1b[0m \x1b[32mbranch\x1b[0m, \x1b[32mbr\x1b[0m Manage feature branches (create, merge, unmerge) + \x1b[32mswitch\x1b[0m, \x1b[32msw\x1b[0m Switch to any branch for testing (without weaving) \x1b[1;33mInspection:\x1b[0m \x1b[32mstatus\x1b[0m Show the branch-aware status (\x1b[34mdefault\x1b[0m command) @@ -206,6 +208,13 @@ enum Command { #[command(visible_alias = "br")] Branch(BranchCmd), + /// Switch to any branch for testing without weaving it into the integration branch + #[command(visible_alias = "sw")] + Switch { + /// Branch name or short ID (if not provided, shows interactive picker) + branch: Option, + }, + // -- Inspection -- /// Show the branch-aware status Status { @@ -382,6 +391,7 @@ fn main() { all, }) => status::run(files, context, all, theme), Some(Command::Init { name }) => init::run(name), + Some(Command::Switch { branch }) => switch::run(branch), Some(Command::Branch(cmd)) => match cmd.action { Some(BranchAction::New(args)) => branch::new::run(args.name, args.target), Some(BranchAction::Merge { branch, all }) => branch::merge::run(branch, all), diff --git a/src/switch.rs b/src/switch.rs new file mode 100644 index 0000000..1bfa721 --- /dev/null +++ b/src/switch.rs @@ -0,0 +1,119 @@ +use std::collections::HashSet; + +use anyhow::{Result, bail}; +use git2::{BranchType, Repository, StatusOptions}; + +use crate::git; +use crate::git_commands::git_branch; +use crate::msg; + +/// Switch to any branch (local or remote) for testing without weaving it into +/// the integration branch. Remote-only branches detach HEAD at the remote ref. +/// Fails if the working tree has staged or unstaged changes to tracked files. +pub fn run(branch: Option) -> Result<()> { + let repo = git::open_repo()?; + let workdir = git::require_workdir(&repo, "switch")?; + + check_clean(&repo)?; + + let (branch_name, is_remote) = match branch { + Some(arg) => resolve_branch(&repo, &arg)?, + None => pick_branch(&repo)?, + }; + + if is_remote { + git_branch::switch_detach(workdir, &branch_name)?; + msg::success(&format!("Detached HEAD at `{}`", branch_name)); + } else { + git_branch::switch(workdir, &branch_name)?; + msg::success(&format!("Switched to `{}`", branch_name)); + } + + Ok(()) +} + +/// Fail if the working tree has staged or unstaged changes to tracked files. +fn check_clean(repo: &Repository) -> Result<()> { + let mut opts = StatusOptions::new(); + opts.include_untracked(false); + let statuses = repo.statuses(Some(&mut opts))?; + if !statuses.is_empty() { + bail!( + "Working tree has uncommitted changes.\n\ + Stash or commit your changes before switching branches." + ); + } + Ok(()) +} + +/// Resolve a branch argument to a name and whether it is remote-only. +/// +/// Tries in order: local branch name → remote branch name → short ID +/// (best-effort, only works when on an integration branch). +fn resolve_branch(repo: &Repository, arg: &str) -> Result<(String, bool)> { + if repo.find_branch(arg, BranchType::Local).is_ok() { + return Ok((arg.to_string(), false)); + } + + if repo.find_branch(arg, BranchType::Remote).is_ok() { + return Ok((arg.to_string(), true)); + } + + // Short ID resolution via gather_repo_info (best-effort) + if let Ok(git::Target::Branch(name)) = git::resolve_arg(repo, arg, &[git::TargetKind::Branch]) { + return Ok((name, false)); + } + + bail!("Branch '{}' not found", arg) +} + +/// Interactive picker: all local branches (except current) plus remote-only +/// branches (those without a local counterpart). +/// +/// Returns `(branch_name, is_remote_only)`. +fn pick_branch(repo: &Repository) -> Result<(String, bool)> { + let current = repo + .head() + .ok() + .and_then(|h| h.shorthand().map(|s| s.to_string())); + + let mut items: Vec<(String, bool)> = Vec::new(); + let mut local_names: HashSet = HashSet::new(); + + // Local branches, skip the current branch; collect all names for deduplication + for branch_result in repo.branches(Some(BranchType::Local))? { + let (branch, _) = branch_result?; + if let Some(name) = branch.name()? { + local_names.insert(name.to_string()); + if Some(name) != current.as_deref() { + items.push((name.to_string(), false)); + } + } + } + + // Remote-only branches (no local counterpart, skip /HEAD pointers) + for branch_result in repo.branches(Some(BranchType::Remote))? { + let (branch, _) = branch_result?; + if let Some(name) = branch.name()? { + if name.ends_with("/HEAD") { + continue; + } + let short_name = git::upstream_local_branch(name); + if !local_names.contains(&short_name) { + items.push((name.to_string(), true)); + } + } + } + + if items.is_empty() { + bail!("No branches available to switch to"); + } + + let names: Vec = items.iter().map(|(n, _)| n.clone()).collect(); + let selected = msg::select("Select branch to switch to", names)?; + let is_remote = items + .iter() + .find(|(n, _)| n == &selected) + .is_some_and(|(_, r)| *r); + Ok((selected, is_remote)) +} diff --git a/tests/integration/test_switch.sh b/tests/integration/test_switch.sh new file mode 100644 index 0000000..d844382 --- /dev/null +++ b/tests/integration/test_switch.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# Integration tests for: gl switch +set -euo pipefail +source "$(dirname "$0")/helpers.sh" +trap 'rm -rf "$TMPROOT"' EXIT + +# ══════════════════════════════════════════════════════════════════════════════ +# PRECONDITIONS +# ══════════════════════════════════════════════════════════════════════════════ + +describe "precond: staged changes block switch" +setup_repo_with_remote +create_feature_branch "g-target" +write_file "staged.txt" "staged content" +git -C "$WORK" add staged.txt +gl_capture switch g-target +assert_exit_fail "$CODE" "staged_blocks" +assert_contains "$OUT" "uncommitted changes" "staged_blocks_msg" + +describe "precond: unstaged changes to tracked file block switch" +setup_repo_with_remote +commit_file "Tracked" "tracked.txt" +create_feature_branch "g-target" +write_file "tracked.txt" "modified" +gl_capture switch g-target +assert_exit_fail "$CODE" "unstaged_blocks" +assert_contains "$OUT" "uncommitted changes" "unstaged_blocks_msg" + +describe "precond: branch not found errors" +setup_repo_with_remote +gl_capture switch no-such-branch +assert_exit_fail "$CODE" "not_found_exit" +assert_contains "$OUT" "not found" "not_found_msg" + +# ══════════════════════════════════════════════════════════════════════════════ +# SWITCHING TO A LOCAL BRANCH +# ══════════════════════════════════════════════════════════════════════════════ + +describe "switch to local branch by name moves HEAD onto that branch" +setup_repo_with_remote +create_feature_branch "g-feat" +out=$(gl switch g-feat) +assert_exit_ok $? "local_switch_ok" +current=$(git -C "$WORK" rev-parse --abbrev-ref HEAD) +assert_eq "$current" "g-feat" "local_switch_head_on_branch" +assert_contains "$out" "Switched to" "local_switch_msg" +assert_contains "$out" "g-feat" "local_switch_msg_name" + +describe "HEAD is not detached after switching to a local branch" +setup_repo_with_remote +create_feature_branch "g-local" +commit_file "Local commit" "local.txt" +switch_to integration +gl switch g-local +assert_exit_ok $? "local_not_detached_ok" +detached=$(git -C "$WORK" symbolic-ref --short HEAD 2>/dev/null || echo "DETACHED") +assert_eq "$detached" "g-local" "local_not_detached_head" + +describe "untracked files do not block switch" +setup_repo_with_remote +create_feature_branch "g-untracked" +write_file "untracked.txt" "not staged" +out=$(gl switch g-untracked) +assert_exit_ok $? "untracked_ok" +assert_contains "$out" "Switched to" "untracked_ok_msg" + +# ══════════════════════════════════════════════════════════════════════════════ +# SWITCHING TO A REMOTE-ONLY BRANCH +# ══════════════════════════════════════════════════════════════════════════════ + +describe "switch to remote-only branch detaches HEAD at the remote ref" +setup_repo_with_remote +# Create, push, then delete the local branch so only origin/g-remote exists +create_feature_branch "g-remote" +switch_to g-remote +commit_file "Remote commit" "remote.txt" +git -C "$WORK" push -q origin g-remote >/dev/null +remote_oid=$(git -C "$WORK" rev-parse origin/g-remote) +git -C "$WORK" checkout -q integration +git -C "$WORK" branch -D g-remote +# Now origin/g-remote exists but g-remote local branch does not +assert_branch_not_exists "g-remote" "remote_only_setup" +out=$(gl switch origin/g-remote) +assert_exit_ok $? "remote_only_ok" +assert_contains "$out" "Detached HEAD" "remote_only_msg" +assert_contains "$out" "origin/g-remote" "remote_only_msg_name" +head_oid=$(git -C "$WORK" rev-parse HEAD) +assert_eq "$head_oid" "$remote_oid" "remote_only_at_correct_oid" + +describe "switch to remote-only branch does not create a local branch" +setup_repo_with_remote +create_feature_branch "g-remote2" +switch_to g-remote2 +commit_file "Remote2 commit" "remote2.txt" +git -C "$WORK" push -q origin g-remote2 >/dev/null +git -C "$WORK" checkout -q integration +git -C "$WORK" branch -D g-remote2 +gl switch origin/g-remote2 +assert_exit_ok $? "remote_only_no_local_ok" +assert_branch_not_exists "g-remote2" "remote_only_no_local_branch" + +describe "HEAD is detached after switching to a remote-only branch" +setup_repo_with_remote +create_feature_branch "g-detach" +switch_to g-detach +commit_file "Detach commit" "detach.txt" +git -C "$WORK" push -q origin g-detach >/dev/null +git -C "$WORK" checkout -q integration +git -C "$WORK" branch -D g-detach +gl switch origin/g-detach +assert_exit_ok $? "remote_detach_ok" +# symbolic-ref fails on detached HEAD — that's what we expect +if git -C "$WORK" symbolic-ref HEAD >/dev/null 2>&1; then + fail "remote_detach_head: HEAD should be detached but is on a branch" +fi + +# ══════════════════════════════════════════════════════════════════════════════ +# SHORT ID RESOLUTION +# ══════════════════════════════════════════════════════════════════════════════ + +describe "switch by branch short ID resolves to the correct local branch" +setup_repo_with_remote +create_feature_branch "g-shortid" +switch_to g-shortid +commit_file "Shortid commit" "shortid.txt" +switch_to integration +weave_branch "g-shortid" +sid=$(branch_sid_from_status "g-shortid") +# Also verify the full-name form works +out_full=$(gl switch g-shortid) +assert_exit_ok $? "shortid_full_ok" +assert_contains "$out_full" "Switched to" "shortid_full_msg" +# Switch back and use the short ID +switch_to integration +out_sid=$(gl switch "$sid") +assert_exit_ok $? "shortid_sid_ok" +assert_contains "$out_sid" "Switched to" "shortid_sid_msg" +assert_contains "$out_sid" "g-shortid" "shortid_sid_name" +current=$(git -C "$WORK" rev-parse --abbrev-ref HEAD) +assert_eq "$current" "g-shortid" "shortid_sid_head" + +# ══════════════════════════════════════════════════════════════════════════════ +# ALIAS +# ══════════════════════════════════════════════════════════════════════════════ + +describe "gl sw is an alias for gl switch" +setup_repo_with_remote +create_feature_branch "g-alias" +out=$(gl sw g-alias) +assert_exit_ok $? "alias_ok" +current=$(git -C "$WORK" rev-parse --abbrev-ref HEAD) +assert_eq "$current" "g-alias" "alias_head" +assert_contains "$out" "Switched to" "alias_msg" + +pass