Full Code of bartman/git-wip for AI

master 022fa8909442 cached
66 files
229.4 KB
61.7k tokens
142 symbols
1 requests
Download .txt
Showing preview only (247K chars total). Download the full file or copy to clipboard to get everything.
Repository: bartman/git-wip
Branch: master
Commit: 022fa8909442
Files: 66
Total size: 229.4 KB

Directory structure:
gitextract_lv7ob25m/

├── .codecov.yml
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── AGENTS.md
├── Attic/
│   ├── README.markdown
│   ├── git-wip
│   └── tests/
│       └── test-git-wip.sh
├── CMakeLists.txt
├── Dockerfile-deb
├── LICENSE
├── README.md
├── cmake/
│   ├── GitVersion.cmake
│   └── GitVersion.sh
├── dependencies.sh
├── dev.sh
├── doc/
│   └── git-wip.txt
├── emacs/
│   ├── git-wip-mode.el
│   └── git-wip.el
├── flake.nix
├── lua/
│   └── git-wip/
│       └── init.lua
├── src/
│   ├── CMakeLists.txt
│   ├── cmd_delete.cpp
│   ├── cmd_delete.hpp
│   ├── cmd_list.cpp
│   ├── cmd_list.hpp
│   ├── cmd_log.cpp
│   ├── cmd_log.hpp
│   ├── cmd_save.cpp
│   ├── cmd_save.hpp
│   ├── cmd_status.cpp
│   ├── cmd_status.hpp
│   ├── color.cpp
│   ├── color.hpp
│   ├── command.hpp
│   ├── git_guards.hpp
│   ├── git_helpers.hpp
│   ├── main.cpp
│   ├── print_compat.hpp
│   ├── string_helpers.hpp
│   └── wip_helpers.hpp
├── sublime/
│   └── gitwip.py
├── test/
│   ├── cli/
│   │   ├── CMakeLists.txt
│   │   ├── lib.sh
│   │   ├── profile.sh
│   │   ├── test_delete.sh
│   │   ├── test_help.sh
│   │   ├── test_legacy.sh
│   │   ├── test_list.sh
│   │   ├── test_save_file.sh
│   │   ├── test_save_subdir.sh
│   │   ├── test_spaces.sh
│   │   ├── test_status.sh
│   │   ├── test_status2.sh
│   │   └── test_status_ref.sh
│   ├── nvim/
│   │   ├── CMakeLists.txt
│   │   ├── lib.sh
│   │   ├── test_nvim_background.sh
│   │   ├── test_nvim_buffers.sh
│   │   ├── test_nvim_single.sh
│   │   └── test_nvim_windows.sh
│   └── unit/
│       ├── CMakeLists.txt
│       ├── test_git_helpers.cpp
│       ├── test_repo_fixture.hpp
│       ├── test_string_helpers.cpp
│       └── test_wip_helpers.cpp
└── vim/
    └── plugin/
        └── git-wip.vim

================================================
FILE CONTENTS
================================================

================================================
FILE: .codecov.yml
================================================
codecov:
  branch: master
  require_ci_to_pass: yes
  notify:
    wait_for_ci: yes

coverage:
  precision: 2
  round: down
  range: "70...100"
  status:
    project:
      default:
        target: auto
        threshold: 1%
    patch:
      default:
        target: auto
        threshold: 1%

ignore:
  - "build/_deps/**/*"
  - "test/**/*"

comment:
  layout: "reach,diff,flags,files,footer"
  behavior: default
  require_changes: no


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches: ["**"]
  pull_request:
    branches: ["**"]

jobs:
  build:
    name: ${{ matrix.os }} / ${{ matrix.compiler }} / ${{ matrix.build_type }}

    strategy:
      fail-fast: false   # let all matrix cells run even if one fails
      matrix:
        include:
          # ── Debian stable ──────────────────────────────────────────────────
          # static: true — libgit2-dev ships libgit2.a on Debian
          - os: debian:stable
            runner: ubuntu-latest
            compiler: gcc
            cc: gcc
            cxx: g++
            build_type: Release
            static: true

          - os: debian:stable
            runner: ubuntu-latest
            compiler: gcc
            cc: gcc
            cxx: g++
            build_type: Debug
            static: true

          - os: debian:stable
            runner: ubuntu-latest
            compiler: clang
            cc: clang
            cxx: clang++
            build_type: Release
            static: true

          - os: debian:stable
            runner: ubuntu-latest
            compiler: clang
            cc: clang
            cxx: clang++
            build_type: Debug
            static: true

          # ── Ubuntu LTS (24.04) ─────────────────────────────────────────────
          # static: true — libgit2-dev ships libgit2.a on Ubuntu
          - os: ubuntu:24.04
            runner: ubuntu-latest
            compiler: gcc
            cc: gcc
            cxx: g++
            build_type: Release
            static: true

          - os: ubuntu:24.04
            runner: ubuntu-latest
            compiler: gcc
            cc: gcc
            cxx: g++
            build_type: Debug
            static: true

          - os: ubuntu:24.04
            runner: ubuntu-latest
            compiler: clang
            cc: clang
            cxx: clang++
            build_type: Release
            static: true

          - os: ubuntu:24.04
            runner: ubuntu-latest
            compiler: clang
            cc: clang
            cxx: clang++
            build_type: Debug
            static: true

          # ── Fedora (latest) ───────────────────────────────────────────────────
          # static: false — Fedora does not ship libgit2.a / libssh2.a / libssl.a
          - os: fedora:latest
            runner: ubuntu-latest
            compiler: gcc
            cc: gcc
            cxx: g++
            build_type: Release
            static: false

          - os: fedora:latest
            runner: ubuntu-latest
            compiler: gcc
            cc: gcc
            cxx: g++
            build_type: Debug
            static: false

          - os: fedora:latest
            runner: ubuntu-latest
            compiler: clang
            cc: clang
            cxx: clang++
            build_type: Release
            static: false

          - os: fedora:latest
            runner: ubuntu-latest
            compiler: clang
            cc: clang
            cxx: clang++
            build_type: Debug
            static: false

          # ── Arch Linux (stable) ───────────────────────────────────────────────
          # static: false — Arch does not ship libgit2.a
          - os: archlinux:latest
            runner: ubuntu-latest
            compiler: gcc
            cc: gcc
            cxx: g++
            build_type: Release
            static: false

          - os: archlinux:latest
            runner: ubuntu-latest
            compiler: gcc
            cc: gcc
            cxx: g++
            build_type: Debug
            static: false

          - os: archlinux:latest
            runner: ubuntu-latest
            compiler: clang
            cc: clang
            cxx: clang++
            build_type: Release
            static: false

          - os: archlinux:latest
            runner: ubuntu-latest
            compiler: clang
            cc: clang
            cxx: clang++
            build_type: Debug
            static: false

    runs-on: ${{ matrix.runner }}

    container:
      image: ${{ matrix.os }}

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Install dependencies
        env:
          DEBIAN_FRONTEND: noninteractive
        run: bash dependencies.sh --compiler=${{ matrix.compiler }}

      - name: Cache CMake FetchContent (spdlog, clipp, fmt)
        uses: actions/cache@v5
        with:
          path: build-so/_deps
          key: fetchcontent-${{ matrix.os }}-${{ matrix.compiler }}-${{ hashFiles('CMakeLists.txt') }}
          restore-keys: |
            fetchcontent-${{ matrix.os }}-${{ matrix.compiler }}-
            fetchcontent-${{ matrix.os }}-

      - name: Configure git identity (needed by tests that create commits)
        run: |
          git config --global user.email "ci@github-actions"
          git config --global user.name  "GitHub Actions"
          git config --global init.defaultBranch master

      - name: Build dynamic
        env:
          CC:  ${{ matrix.cc }}
          CXX: ${{ matrix.cxx }}
          CI:  "1"
        run: make BUILD=build-so TYPE=${{ matrix.build_type }}

      - name: Test dynamic
        env:
          CC:  ${{ matrix.cc }}
          CXX: ${{ matrix.cxx }}
          CI:  "1"
        run: make BUILD=build-so TYPE=${{ matrix.build_type }} test

      - name: Install static dependencies
        if: matrix.static
        env:
          DEBIAN_FRONTEND: noninteractive
        run: bash dependencies.sh --compiler=${{ matrix.compiler }} --static

      - name: Build static
        if: matrix.static
        env:
          CC:  ${{ matrix.cc }}
          CXX: ${{ matrix.cxx }}
          CI:  "1"
        run: make BUILD=build-a STATIC=1 TYPE=${{ matrix.build_type }}

      - name: Test static
        if: matrix.static
        env:
          CC:  ${{ matrix.cc }}
          CXX: ${{ matrix.cxx }}
          CI:  "1"
        run: make BUILD=build-a STATIC=1 TYPE=${{ matrix.build_type }} test

      - name: Compute artifact name
        if: failure()
        id: artifact-name
        shell: bash
        run: |
          # Colons and slashes are not allowed in artifact names; replace with dashes.
          raw="test-artifacts-${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.build_type }}"
          echo "name=${raw//[:\\/]/-}" >> "$GITHUB_OUTPUT"

      - name: Upload test artifacts on failure
        if: failure()
        uses: actions/upload-artifact@v7
        with:
          name: ${{ steps.artifact-name.outputs.name }}
          path: |
            build-so/test/
            build-so/Testing/
            build-a/test/
            build-a/Testing/
          retention-days: 7

  coverage:
    name: Coverage (debian:stable / gcc / Debug)

    runs-on: ubuntu-latest

    container:
      image: debian:stable

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Install dependencies
        env:
          DEBIAN_FRONTEND: noninteractive
        run: bash dependencies.sh --compiler=gcc --coverage

      - name: Cache CMake FetchContent (spdlog, clipp, fmt)
        uses: actions/cache@v5
        with:
          path: build/_deps
          key: fetchcontent-coverage-${{ hashFiles('CMakeLists.txt') }}
          restore-keys: |
            fetchcontent-coverage-

      - name: Configure git identity (needed by tests that create commits)
        run: |
          git config --global user.email "ci@github-actions"
          git config --global user.name  "GitHub Actions"
          git config --global init.defaultBranch master

      - name: Build, test, and collect coverage
        env:
          CC:  gcc
          CXX: g++
          CI:  "1"
        run: make CC=gcc CXX=g++ TYPE=Debug coverage REBUILD=1

      - name: Upload to Codecov
        uses: codecov/codecov-action@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage.info
          flags: unittests
          name: debian-stable-gcc-debug
          verbose: true
          fail_ci_if_error: true


================================================
FILE: .gitignore
================================================
*~
build/
.cache/
.opencode/
/git-wip

coverage-report/
coverage.xml
coverage.info

### C++
# Prerequisites
*.d

# Compiled Object files
*.slo
*.lo
*.o
*.obj

# Precompiled Headers
*.gch
*.pch

# Compiled Dynamic libraries
*.so
*.dylib
*.dll

# Fortran module files
*.mod
*.smod

# Compiled Static libraries
*.lai
*.la
*.a
*.lib

# Executables
*.exe
*.out
*.app

### CMake
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
Testing
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
flake.lock



================================================
FILE: AGENTS.md
================================================
# AGENTS.md - git-wip C++ Rewrite

## Guidance from user

- Use c++23 best practices.  Use CamelCase for classes, use snake_case for method names, variable names, etc.  Use #pragma once in headers.  Use m_ prefix for member variables.
- use manual arg parsing, use spdlog for debug logging (set `WIP_DEBUG=1` to see debug), use libgit2 for git functionality
- build with `make`, test with `make test`
- manage/install dependencies with `dependencies.sh` script
- unit tests go into `test/unit/test_*.cpp`
- CLI integration tests go into `test/cli/test_*.sh` — source `test/cli/lib.sh`, must be executable
- old scripts are in `Attic/` subdirectory, we try to be backward compatible (at least for vim/ emacs/ sublime/ plugins)
- agent will update `AGENTS.md` and `README.md` files with new information, as needed
- `lua/git-wip/init.lua` is the plugin for Neovim written in Lua
- `vim/plugin/git-wip.vim` is the legacy plugin for Vim written in VimL -- maintained, but not actively improved

## Lua Plugin Configuration

The Neovim Lua plugin supports the following configuration options (set via `opts` in lazy.nvim or passed to `setup()`):

- `git_wip_path`: Path to the git-wip binary (default: "git-wip")
- `gpg_sign`: nil (default), true (--gpg-sign), false (--no-gpg-sign)
- `untracked`: nil (default), true (--untracked), false (--no-untracked)
- `ignored`: nil (default), true (--ignored), false (--no-ignored)
- `background`: false (default, sync execution), true (async if Neovim 0.10+, else sync with warning)
- `filetypes`: Array of filetypes to enable (default: { "*" } for all)

Async execution uses Neovim's `vim.system` with `on_exit` callback for non-blocking saves.

## Test Infrastructure

### test/cli/lib.sh

Shared bash harness sourced by every CLI test script.  Provides:

- `RUN <cmd>` — run a command, fail the test if it exits non-zero
- `_RUN <cmd>` — run a command without checking exit code (use before `EXP_text`)
- `EXP_none` — assert that the last command produced no output
- `EXP_text <string>` — assert that the first line of output equals `<string>`
- `EXP_grep [opts] <pattern>` — assert that output matches (or with `-v`, does not match) a grep pattern
- `create_test_repo` — init a fresh git repo in `$REPO`, checkout branch `master`
- `handle_error` — print diagnostics and exit 1

Required env vars (set by ctest via `CMakeLists.txt`):
- `GIT_WIP` — path to the binary under test
- `TEST_TREE` — base dir for artifacts; each test gets `$TEST_TREE/$TEST_NAME/`

`TEST_NAME` is derived automatically from `$(basename "$0" .sh)`.

Each test cleans its own subdirectory before running and leaves artifacts after for debugging.

### Adding a new CLI test

1. Create `test/cli/test_<name>.sh` (executable, `chmod +x`)
2. First two lines: `#!/usr/bin/env bash` then `source "$(dirname "$0")/lib.sh"`
3. Write test body using `RUN`, `EXP_*`, `create_test_repo`
4. End with `echo "OK: $TEST_NAME"`
5. Add `test_<name>` to the `foreach` list in `test/cli/CMakeLists.txt`

### Quoting in test scripts

All commands go through `eval "$@"` inside `_RUN`.  Multi-word arguments
(commit messages, file names with spaces) must be wrapped in escaped quotes:

```bash
RUN "$GIT_WIP" save "\"message with spaces\""   # correct
RUN git commit -m "\"my commit message\""        # correct
RUN "$GIT_WIP" save "message with spaces"        # WRONG — splits into tokens
```

## Source Layout

```
src/
  main.cpp          # arg dispatch; no-args → save "WIP"
  command.hpp       # abstract Command base class
  git_guards.hpp    # RAII wrappers for libgit2 handles + git_error_str()
  cmd_save.hpp/cpp  # save command
  cmd_log.hpp/cpp   # log command
  cmd_status.hpp/cpp# status command
  cmd_delete.hpp/cpp# delete command (skeleton)
test/cli/
  lib.sh            # shared test harness (not executable)
  test_legacy.sh    # legacy compatibility tests
  test_spaces.sh    # filenames with spaces
  test_status.sh    # status command tests
  test_status2.sh   # status after work-branch advance
  test_save_file.sh # save with explicit file arguments
  CMakeLists.txt    # registers each test_*.sh with ctest
```

## Old Shell Script Analysis (Attic/git-wip)

This section captures the analysis of the original shell script implementation and the requirements for backward compatibility with editor plugins.

### Commands Implemented (original script)

| Command | In old script | Notes |
|---------|--------------|-------|
| (no args) | Yes | Defaults to `save "WIP"` |
| save | Yes | Core functionality |
| log | Yes | Shows WIP history |
| info | **No** | Dies with "info not implemented" |
| delete | **No** | Dies with "delete not implemented" |
| help | Yes | Shows usage |

### Command Syntax (from old script)

```
Usage: git wip [ info | save <message> [ --editor | --untracked | --no-gpg-sign ] | log [ --pretty ] | delete ] [ [--] <file>... ]
```

#### save command

```
git wip save <message> [options] [--] [files...]
```

Options:
- `-e`, `--editor` - Be less verbose, assume called from an editor (suppresses errors when no changes)
- `-u`, `--untracked` - Capture also untracked files
- `-i`, `--ignored` - Capture also ignored files
- `--no-gpg-sign` - Do not sign commit (overrides commit.gpgSign=true)

If no files are specified, all changes to tracked files are saved. If files are specified, only those files are saved.

#### log command

```
git wip log [options] [files...]
```

Options:
- `-p`, `--pretty` - Show a pretty graph with colors
- `-s`, `--stat` - Show diffstat
- `-r`, `--reflog` - Show changes in reflog instead of regular log

### Internal Implementation Details

1. **WIP Branch Naming**: `refs/wip/<branch_name>` where `<branch_name>` is the current local branch (e.g., `refs/wip/master`, `refs/wip/feature`)

2. **First Run Behavior**:
   - Creates a new commit on `wip/<branch>` starting from the current branch HEAD
   - Captures all changes to tracked files
   - Can optionally capture untracked and/or ignored files

3. **Subsequent Run Behavior**:
   - If the work branch has new commits since last WIP:
     - Creates a new WIP commit as a child of the work branch
     - This "resets" the WIP branch to follow the work branch
   - If no new commits on work branch:
     - Continues from the last WIP commit (adds new changes on top)

4. **Tree Building Process** (build_new_tree function):
   - Creates a temporary index file `$GIT_DIR/.git-wip.$$-INDEX`
   - Copies the main git index to the temp index
   - Uses `git read-tree` to populate the index with the parent tree
   - Uses `git add` to stage changes:
     - Default: `git add --update .` (updates tracked files)
     - With `--untracked`: `git add .` (includes untracked)
     - With `--ignored`: `git add -f -A .` (includes ignored)
   - Uses `git write-tree` to create the new tree object

5. **Commit Creation**:
   - Uses `git commit-tree` to create an orphan commit
   - Parent is the determined wip_parent (either last wip commit or work branch HEAD)
   - Commit message is the user-provided message

6. **Reference Update**:
   - Uses `git update-ref` to update the wip branch ref
   - Message format: `git-wip: <first line of message>`
   - Old ref value is passed for safe update (prevent overwriting)

7. **Reflog**:
   - Enables reflog for the wip branch
   - Creates `$GIT_DIR/logs/refs/wip/<branch>` if it doesn't exist
   - Allows recovery of "orphaned" WIP commits via `git reflog`

8. **Editor Mode** (`--editor`):
   - In editor mode, if there are no changes, the script exits quietly (exit code 0)
   - Without editor mode, it reports an error "no changes"

9. **Error Handling**:
   - Requires a working tree (uses `git-sh-setup` functions)
   - Requires being on a local branch (not detached HEAD)
   - Reports soft errors in editor mode (exits 0), hard errors otherwise

## Editor Plugin Commands (Backward Compatibility)

The C++ implementation MUST accept these exact command formats:

### vim (vim/plugin/git-wip.vim, line 53)

```vim
let out = system('cd "' . dir . '" && git wip save "WIP from vim (' . file . ')" ' . wip_opts . ' -- "' . file . '" 2>&1')
```

Full command example:
```
git wip save "WIP from vim (filename)" --editor --no-gpg-sign -- "filename"
```

Key observations:
- Uses `git wip` (space, not hyphen)
- Message format: `WIP from vim (filename)`
- Options: `--editor` and optionally `--no-gpg-sign`
- File argument after `--` delimiter

### emacs (emacs/git-wip.el, line 4)

```lisp
(shell-command (concat "git-wip save \"WIP from emacs: " (buffer-file-name) "\" --editor -- " file-arg))
```

Full command example:
```
git-wip save "WIP from emacs: /path/to/file.el" --editor -- '/path/to/file.el'
```

Key observations:
- Uses `git-wip` (hyphen, not space) - DIFFERENT FROM OTHERS
- Message format: `WIP from emacs: /path/to/file`
- Option: `--editor`
- File argument after `--` delimiter

### sublime (sublime/gitwip.py, line 12-14)

```python
p = Popen(["git", "wip", "save",
           "WIP from ST3: saving %s" % fname,
           "--editor", "--", fname],
```

Full command example:
```
git wip save "WIP from ST3: saving filename" --editor -- filename
```

Key observations:
- Uses `git wip` (space, not hyphen)
- Message format: `WIP from ST3: saving filename`
- Option: `--editor`
- File argument after `--` delimiter

### vim check for git-wip (vim/plugin/git-wip.vim, line 24)

```vim
silent! !git wip -h >/dev/null 2>&1
```

The vim plugin runs `git wip -h` to check if git-wip is installed. This should:
- Either show help and exit 0
- Or at least not error out (the script checks `v:shell_error`)

## Implementation Status (C++)

| Command | Header | Implementation | Status |
|---------|--------|----------------|--------|
| save | cmd_save.hpp | cmd_save.cpp | **Implemented** — full libgit2, passes all tests |
| log | cmd_log.hpp | cmd_log.cpp | **Implemented** — libgit2 range, spawns `git log` |
| status | cmd_status.hpp | cmd_status.cpp | **Implemented** — libgit2 revwalk, `-l`/`-f` flags |
| delete | cmd_delete.hpp | cmd_delete.cpp | **Implemented** — delete one/current/cleanup orphaned wip refs |
| config | — | — | Not implemented |

### git_guards.hpp — RAII wrappers

All libgit2 handle types have lightweight RAII guards in `src/git_guards.hpp`:
`RepoGuard`, `IndexGuard`, `TreeGuard`, `CommitGuard`, `ReferenceGuard`,
`SignatureGuard`, `RevwalkGuard`.  Each exposes `get()` and `ptr()`.
Also provides `inline git_error_str()` for the last libgit2 error message.

Arg parsing is done **manually** in all commands because the
`save` command has a "first positional = message, rest = files" pattern that
is awkward in declarative parsers.  The same manual style was adopted for
consistency across `log` and `status`.

### save command (cmd_save.cpp)

Uses libgit2 exclusively (no subprocess spawning).

**Algorithm:**
1. Parse args manually — first non-option positional = message, remainder after `--` (or bare paths) = files
2. Open repo with `git_repository_open_ext`
3. Resolve HEAD → work branch short name → `refs/wip/<branch>`
4. Ensure `$GIT_DIR/logs/refs/wip/<branch>` exists (for reflog)
5. Resolve `work_last` from HEAD
6. Determine `wip_parent`:
   - If `refs/wip/<branch>` exists: compute merge-base; if `work_last == base` use `wip_last`, else use `work_last`
   - Otherwise: use `work_last`
7. Build new tree (in-memory, never touches the on-disk index):
   - `git_repository_index` → `git_index_read_tree` from parent commit tree
   - Stage changes: `git_index_update_all` (default) / `git_index_add_all` with `DEFAULT` (untracked) / `FORCE` (ignored or specific files)
   - `git_index_write_tree` → OID of new tree object
   - `git_index_read(force=1)` to restore the real on-disk index
8. Compare new tree OID vs parent tree OID — equal → "no changes" (exit 0 in editor mode, exit 1 otherwise)
9. `git_commit_create` with `ref=NULL` (does not update any ref yet)
10. `git_reference_create_matching` with `current_id=wip_last` (NULL on first run) and reflog message `"git-wip: <first line>"`

**Specific-file behaviour:** when files are listed after `--`, `git_index_add_all`
is called with `GIT_INDEX_ADD_FORCE` and a pathspec of exactly those files.
Only the listed files are updated in the wip tree; all others reflect the
parent wip commit state.  Untracked files can be captured this way too.

### log command (cmd_log.cpp)

Uses libgit2 to compute the range, then spawns `git log` via `std::system()`.

**Algorithm:**
1. Resolve work branch and wip branch; look up `work_last` and `wip_last`
2. `git_merge_base` → `base`; stop = `base~1` if base has parents, else `base`
3. `std::system("git log [--graph] [--stat] [--pretty=...] <wip_last> <work_last> ^<stop>")`

### status command (cmd_status.cpp)

Uses libgit2 for all commit enumeration; spawns `git diff --stat` via
`std::system()` for the `-f`/`--files` output.

**Algorithm:**
1. Resolve `work_last` and `wip_last`; if no wip ref → print "no wip commits", exit 0
2. `git_merge_base(wip_last, work_last)` → `base`
3. If `base != work_last`: the work branch has advanced past the wip branch — there are **0 current wip commits** (the next `save` would reset).  Print "0 wip commits", exit 0.
4. Otherwise walk from `wip_last` hiding `work_last` (== base) to collect the wip-only commits (newest first via `GIT_SORT_TOPOLOGICAL`)
5. Print summary: `branch <name> has <N> wip commit(s) on refs/wip/<name>`
6. `-l`/`--list`: for each commit print `<sha7> - <subject> (<age>)`
7. `-f`/`--files`: `git diff --stat <work_last> <wip_last>`
8. `-l -f` combined: per-commit `git diff --stat <commit>^ <commit>` after each list line

**Key bug that was fixed:** originally hid only `work_last` in the revwalk.
When the work branch advances (new real commit), `work_last` is no longer an
ancestor of `wip_last`, so hiding it has no effect and stale wip commits
remain visible.  The fix: hide the merge-base, and short-circuit to "0 commits"
when `merge_base != work_last`.

### main.cpp — No-args behavior

When `argc < 2`, synthesises `argv = ["save", "WIP"]` and invokes `SaveCmd`
directly, matching the old shell script's default behaviour.

## Required Implementation for Backward Compatibility

### 1. save command must support:

- Positional message argument (e.g., `git wip save "message"`)
- `--editor` / `-e` flag
- `--untracked` / `-u` flag
- `--ignored` / `-i` flag
- `--no-gpg-sign` flag
- `--` delimiter for file arguments
- File arguments (optional, multiple allowed)

### 2. log command must support:

- `--pretty` / `-p` flag
- `--stat` / `-s` flag
- `--reflog` / `-r` flag
- File arguments (optional, multiple allowed)

### 3. status command must support:

- `--list` / `-l` flag — one line per wip commit
- `--files` / `-f` flag — diff --stat of wip changes
- Combination of `-l -f` — per-commit diff interleaved with list
- Optional `<ref>` argument (defaults to current branch), where `<ref>` may be:
  - `<branch>`
  - `wip/<branch>`
  - `refs/heads/<branch>`
  - `refs/wip/<branch>`

### 4. Main program must support:

- No arguments: invoke save with default message "WIP"
- `help`, `--help`, `-h`: show help
- Command dispatch to subcommands

### 5. Must handle edge cases:

- No changes to save (exit 0 quietly with `--editor`, print "no changes" + exit 1 otherwise)
- Not on a branch (detached HEAD) — error
- No commits on current branch — error
- WIP branch unrelated to work branch — error
- Work branch has advanced past wip branch — status shows 0 wip commits


================================================
FILE: Attic/README.markdown
================================================
# About

git-wip is a script that will manage Work In Progress (or WIP) branches.
WIP branches are mostly throw away but identify points of development
between commits.  The intent is to tie this script into your editor so
that each time you save your file, the git-wip script captures that
state in git.  git-wip also helps you return back to a previous state of
development.

Latest git-wip can be obtained from [github.com](http://github.com/bartman/git-wip).
git-wip was written by [Bart Trojanowski](mailto:bart@jukie.net).
You can find out more from the original [blog post](http://www.jukie.net/bart/blog/save-everything-with-git-wip).

# WIP branches

Wip branches are named after the branch that is being worked on, but are
prefixed with 'wip/'.  For example if you are working on a branch named
'feature' then the git-wip script will only manipulate the 'wip/feature'
branch.

When you run git-wip for the first time, it will capture all changes to
tracked files and all untracked (but not ignored) files, create a
commit, and make a new wip/*topic* branch point to it.

    --- * --- * --- *          <-- topic
                     \
                      *        <-- wip/topic

The next invocation of git-wip after a commit is made will continue to
evolve the work from the last wip/*topic* point.

    --- * --- * --- *          <-- topic
                     \
                      *
                       \
                        *      <-- wip/topic

When git-wip is invoked after a commit is made, the state of the
wip/*topic* branch will be reset back to your *topic* branch and the new
changes to the working tree will be caputred on a new commit.

    --- * --- * --- * --- *    <-- topic
                     \     \
                      *     *  <-- wip/topic
                       \
                        *

While the old wip/*topic* work is no longer accessible directly, it can
always be recovered from git-reflog.  In the above example you could use
`wip/topic@{1}` to access the dangling references.

# git-wip command

The git-wip command can be invoked in several different ways.

* `git wip`
  
  In this mode, git-wip will create a new commit on the wip/*topic*
  branch (creating it if needed) as described above.

* `git wip save "description"`
  
  Similar to `git wip`, but allows for a custom commit message.

* `git wip log`
  
  Show the list of the work that leads upto the last WIP commit.  This
  is similar to invoking:
  
  `git log --stat wip/$branch...$(git merge-base wip/$branch $branch)`

# Installation

Download the script from the GitHub page:

    git clone git://github.com/bartman/git-wip.git

Add `git-wip` to your `$PATH`:

    mkdir -p ~/bin
    cp git-wip/git-wip ~/bin/

# editor hooking

To use git-wip effectively, you should tie it into your editor so you
don't have to remember to run git-wip manually.

## vim

To add git-wip support to vim you can install the provided vim plugin.  There
are a few ways to do this.

**(1)** If you're using [Vundle](https://github.com/gmarik/Vundle.vim), you
just need to include the following line in your `.vimrc`.

    Bundle 'bartman/git-wip', {'rtp': 'vim/'}

**(2)** You can slo copy the `git-wip.vim` into your vim runtime:

    cp vim/plugin/git-wip ~/.vim/plugin/git-wip

**(3)** Alternatively, you can add the following to your `.vimrc`.  Doing so
will make it be invoked after every `:w` operation.

    augroup git-wip
      autocmd!
      autocmd BufWritePost * :silent !cd "`dirname "%"`" && git wip save "WIP from vim" --editor -- "`basename "%"`"
    augroup END

The `--editor` option puts git-wip into a special mode that will make it
more quiet and not report errors if there were no changes made to the
file.

## emacs

To add git-wip support to emacs add the following to your `.emacs`. Doing
so will make it be invoked after every `save-buffer` operation.

    (load "/{path_to_git-wip}/emacs/git-wip.el")

Or you may also copy the content of git-wip.el in your `.emacs`.

## sublime

A sublime plugin was contributed as well.  You will find it in the `sublime`
directory.

# recovery

Should you discover that you made some really bad changes in your code,
from which you want to recover, here is what to do.

First we need to find the commit we are interested in.  If it's the most recent
then it can be referenced with `wip/master` (assuming your branch is `master`),
otherwise you may need to find the one you want using:

    git reflog show wip/master

I personally prefer to inspect the reflog with `git log -g`, and sometimes 
with `-p` also:

    git log -g -p wip/master

Once you've picked a commit, you need to checkout the files, note that we are not
switching the commit that your branch points to (HEAD will continue to reference
the last real commit on the branch).  We are just checking out the files:

    git checkout ref -- .

Here `ref` could be a SHA1 or `wip/master`.  If you only want to recover one file,
then use it's path instead of the *dot*.

The changes will be staged in the index and checked out into the working tree, to
review what the differences are between the last commit, use:

    git diff --cached

If you want, you can unstage all or some with `git reset`, optionally specifying a
filename to unstage.  You can then stage them again using `git add` or `git add -p`.
Finally, when you're happy with the changes, commit them.

# related

- [wip.rs](https://github.com/dlight/wip.rs) is a Rust executable that watches for changes in your work, and invokes `git-wip` as needed. (by [dlight](https://github.com/dlight)).

<!-- vim: set ft=markdown -->


================================================
FILE: Attic/git-wip
================================================
#!/usr/bin/env bash
#
# Copyright Bart Trojanowski <bart@jukie.net>
#
# git-wip is a script that will manage Work In Progress (or WIP) branches.
# WIP branches are mostly throw away but identify points of development
# between commits.  The intent is to tie this script into your editor so
# that each time you save your file, the git-wip script captures that
# state in git.  git-wip also helps you return back to a previous state of
# development.
#
# See also http://github.com/bartman/git-wip
#
# The code is licensed as GPL v2 or, at your option, any later version.
# Please see http://www.gnu.org/licenses/gpl-2.0.txt
#

USAGE='[ info | save <message> [ --editor | --untracked | --no-gpg-sign ] | log [ --pretty ] | delete ] [ [--] <file>... ]'
LONG_USAGE="Manage Work In Progress branches

Commands:
        git wip                   - create a new WIP commit
        git wip save <message>    - create a new WIP commit with custom message
        git wip info [<branch>]   - brief WIP info
        git wip log [<branch>]    - show changes on the WIP branch
        git wip delete [<branch>] - delete a WIP branch

Options for save:
        -e --editor               - be less verbose, assume called from an editor
        -u --untracked            - capture also untracked files
        -i --ignored              - capture also ignored files
        --no-gpg-sign             - do not sign commit; that is, countermand
                                    'commit.gpgSign = true'

Options for log:
        -p --pretty               - show a pretty graph
	-r --reflog               - show changes in reflog
	-s --stat                 - show diffstat
"

SUBDIRECTORY_OK=Yes
OPTIONS_SPEC=

. "$(git --exec-path)/git-sh-setup"

require_work_tree

TMP="$GIT_DIR/.git-wip.$$"
trap 'rm -f "$TMP-*"' 0

WIP_INDEX="$TMP-INDEX"

WIP_PREFIX=refs/wip/
WIP_COMMAND=
WIP_MESSAGE=WIP
EDITOR_MODE=false

dbg() {
	if test -n "$WIP_DEBUG"
	then
		printf '# %s\n' "$*"
	fi
}

# some errors are not worth reporting in --editor mode
report_soft_error () {
	$EDITOR_MODE && exit 0
	die "$@"
}

cleanup () {
	rm -f "$TMP-*"
}

get_work_branch () {
	ref=$(git symbolic-ref -q HEAD) \
	|| report_soft_error "git-wip requires a branch"


	branch=${ref#refs/heads/}
	if [ $branch = $ref ] ; then
		die "git-wip requires a local branch"
	fi

	echo $branch
}

get_wip_branch () {
	return 0
}

check_files () {
	local -a files=( "$@" )

	for f in "${files[@]}"
	do
		[ -f "$f" -o -d "$f" ] || die "$f: No such file or directory."
	done
}

build_new_tree () {
	local untracked=$1 ; shift
	local ignored=$1 ; shift
	local -a files=( "$@" )

	(
	set -e
	rm -f "$WIP_INDEX"
	cp -p "$GIT_DIR/index" "$WIP_INDEX"
	export GIT_INDEX_FILE="$WIP_INDEX"
	git read-tree $wip_parent
	if test "${#files[@]}" -gt 0
	then
		git add -f "${files[@]}"
	else
		git add --update .
	fi
	[ -n "$untracked" ] && git add .
	[ -n "$ignored" ] && git add -f -A .
	git write-tree
	rm -f "$WIP_INDEX"
	)
}

do_save () {
	local msg="$1" ; shift
	local add_untracked=
	local add_ignored=
	local no_sign=

	while test $# != 0
	do
		case "$1" in
		-e|--editor)
			EDITOR_MODE=true
			;;
		-u|--untracked)
			add_untracked=t
			;;
		-i|--ignored)
			add_ignored=t
			;;
		--no-gpg-sign)
			no_sign=--no-gpg-sign
			;;
		--)
			shift
			break
			;;
		*)
			[ -f "$1" ] && break
			die "Unknown option '$1'."
			;;
		esac
		shift
	done
	local -a files=( "$@" )
	local "add_untracked=$add_untracked"
	local "add_ignored=$add_ignored"

	if test "${#files[@]}" -gt 0
	then
		dbg "check_files ${files[@]}"
		check_files "${files[@]}"
	fi

	dbg "msg=$msg"
	dbg "files=$files"

	local work_branch=$(get_work_branch)
	local wip_branch="$WIP_PREFIX$work_branch"

	dbg "work_branch=$work_branch"
	dbg "wip_branch=$wip_branch"

	# enable reflog
	local wip_branch_reflog="$GIT_DIR/logs/$wip_branch"
	dbg "wip_branch_reflog=$wip_branch_reflog"
	mkdir -p "$(dirname "$wip_branch_reflog")"
	: >>"$wip_branch_reflog"

	if ! work_last=$(git rev-parse --verify $work_branch)
	then
		report_soft_error "'$work_branch' branch has no commits."
	fi

	dbg "work_last=$work_last"

	if wip_last=$(git rev-parse --quiet --verify $wip_branch)
	then
		local base=$(git merge-base $wip_last $work_last) \
		|| die "'work_branch' and '$wip_branch' are unrelated."

		if [ $base = $work_last ] ; then
			wip_parent=$wip_last
		else
			wip_parent=$work_last
		fi
	else
		# remove empty/corrupt wip branch file
		local wip_branch_file="$GIT_DIR/$wip_branch"
		if test -e "$wip_branch_file"
		then
			dbg "removing $wip_branch_file"
			rm -f "$wip_branch_file"
		fi
		# use the working branch for parent
		wip_parent=$work_last
	fi

	dbg "wip_parent=$wip_parent"

	new_tree=$( build_new_tree "$add_untracked" "$add_ignored" "${files[@]}" ) \
	|| die "Cannot save the current worktree state."

	dbg "new_tree=$new_tree"

	if git diff-tree --exit-code --quiet $new_tree $wip_parent ; then
		report_soft_error "no changes"
	fi

	dbg "... has changes"

	new_wip=$(printf '%s\n' "$msg" | git commit-tree $no_sign $new_tree -p $wip_parent 2>/dev/null) \
	|| die "Cannot record working tree state"

	dbg "new_wip=$new_wip"

	msg1=$(printf '%s\n' "$msg" | sed -e 1q)
	git update-ref -m "git-wip: $msg1" $wip_branch $new_wip $wip_last

	dbg "SUCCESS"
}

do_info () {
	local branch=$1

	die "info not implemented"
}

do_log () {
	local work_branch=$1
	[ -z $branch ] && work_branch=$(get_work_branch)
	local wip_branch="$WIP_PREFIX$work_branch"

	local log_cmd="log"
	local graph=""
	local pretty=""
	local stat=""
	while [ -n "$1" ]
	do
		case "$1" in
			-p|--pretty)
				graph="--graph"
				pretty="--pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset' --abbrev-commit --date=relative"
				;;
			-s|--stat)
				stat="--stat"
				;;
			-r|--reflog)
				log_cmd="reflog"
				;;
			*)
				break
				;;
		esac
		shift
	done

	if [ $log_cmd = reflog ]
	then
		echo git reflog $stat $pretty $wip_branch | sh
		return $?
	fi

	if ! work_last=$(git rev-parse --verify $work_branch)
	then
		die "'$work_branch' branch has no commits."
	fi

	dbg work_last=$work_last

	if ! wip_last=$(git rev-parse --quiet --verify $wip_branch)
	then
		die "'$work_branch' branch has no commits."
	fi

	dbg wip_last=$wip_last

	local base=$(git merge-base $wip_last $work_last)

	dbg base=$base

	local stop=$base
	if git cat-file commit $base | grep -q '^parent' ; then
		stop="$base~1"
	fi

	dbg stop=$stop

	echo git log $graph $stat $pretty "$@" $wip_last $work_last "^$stop" | sh
}

do_delete () {
	local branch=$1

	die "delete not implemented"
}

do_help () {
	local rc=$1

	cat <<END
Usage: git wip $USAGE

$LONG_USAGE
END
	exit $rc
}


if test $# -eq 0
then
	dbg "no arguments"

	do_save "WIP"
	exit $?
fi

dbg "args: $@"

case "$1" in
save)
	WIP_COMMAND=$1
	shift
	if [ -n "$1" ] && [[ "$1" != -* ]]
	then
		WIP_MESSAGE="$1"
		shift
	fi
	;;
info|log|delete)
	WIP_COMMAND=$1
	shift
	;;
help)
	do_help 0
	;;
--*)
	;;
*)
	[ -f "$1" ] || die "Unknown command '$1'."
	;;
esac

case $WIP_COMMAND in
save)
	do_save "$WIP_MESSAGE" "$@"
	;;
info)
	do_info "$@"
	;;
log)
	do_log "$@"
	;;
delete)
	do_delete "$@"
	;;
*)
	usage
	exit 1
	;;
esac

# vim: set noet sw=8


================================================
FILE: Attic/tests/test-git-wip.sh
================================================
#!/bin/bash

set -e

# split the script name into BASE directory, and the name it calls itSELF
BASE=$(realpath ${0%/*})
SELF=${0##*/}

# this tells git where to find our copy of git-wip script
export PATH="$BASE:$PATH"

# keep state for each test in this temporary tree
TEST_TREE=/tmp/git-wip-test-$$
REPO="$TEST_TREE/repo"
CMD="$TEST_TREE/cmd"
OUT="$TEST_TREE/out"
RC="$TEST_TREE/rc"

# ------------------------------------------------------------------------
# some helpful utility functions

die() { echo >&2 "ERROR: $@" ; exit 1 ; }
warn() { echo >&2 "WARNING: $@" ; }
comment() { echo >&2 "# $@" ; }

_RUN() {
	comment "$@"
	[ `pwd` = "$REPO" ] || die "expecting to be in $REPO, not `pwd`"

	set +e
	echo "$@" >"$CMD"
	eval "$@" >"$OUT" 2>&1
	echo "$?" >"$RC"
	set -e
}

RUN() {
	_RUN "$@"
	local rc="`cat $RC`"
	[ "$rc" = 0 ] || handle_error
}

EXP_none() {
	local out="$(head -n1 "$OUT")"
	if [ -n "$out" ] ; then
		warn "out: $out"
		handle_error
	fi
}

EXP_text() {
	local exp="$1"
	local out="$(head -n1 "$OUT")"
	if [ "$out" != "$exp" ] ; then
		warn "exp: $exp"
		warn "out: $out"
		handle_error
	fi
}

EXP_grep() {
	if ! grep -q "$@" < "$OUT" ; then
		warn "grep: $@"
		handle_error
	fi
}

create_test_tree() {
	rm -rf "$REPO"
	mkdir -p "$REPO"
	cd "$REPO"
	RUN git init
}

cleanup_test_tree() {
	rm -rf "$TEST_TREE"
}

handle_error() {
	set +e
	warn "CMD='`cat $CMD`' RC=`cat $RC`"
	cat >&1 "$OUT"
	cleanup_test_tree
	exit 1
}

trap cleanup_test_tree EXIT
trap handle_error INT TERM ERR

# ------------------------------------------------------------------------
# tests

test_general() {
	# init

	create_test_tree
	RUN "echo 1 >README"
	RUN git add README
	RUN git commit -m README

	# run wip w/o changes

	_RUN git wip save
	EXP_text "no changes"

	RUN git wip save --editor
	EXP_none

	RUN git wip save -e
	EXP_none

	# expecting a master branch

	RUN git branch
	EXP_grep "^\* master$"
	EXP_grep -v "wip"

	# not expecting  wip ref at this time

	RUN git for-each-ref
	EXP_grep -v "commit.refs/wip/master$"

	# make changes, store wip

	RUN "echo 2 >README"
	RUN git wip save --editor
	EXP_none

	# expecting a wip ref

	RUN git for-each-ref
	EXP_grep "commit.refs/wip/master$"

	# expecting a log entry

	RUN git wip log
	EXP_grep "^commit "
	EXP_grep "^\s\+WIP$"

	# there should be no wip branch

	RUN git branch
	EXP_grep -v "wip"

	# make changes, store wip

	RUN "echo 3 >README"
	RUN git wip save "\"message2\""
	EXP_none

	# expecting both log entries

	RUN git wip log
	EXP_grep "^commit "
	EXP_grep "^\s\+WIP$"
	EXP_grep "^\s\+message2$"

	# make a commit

	RUN git add -u README
	RUN git commit -m README.2

	# make changes, store wip

	RUN "echo 4 >UNTRACKED"
	RUN "echo 4 >README"
	RUN git wip save "\"message3\""
	EXP_none

	# expecting message3, not message2 or original WIP

	RUN git wip log
	EXP_grep "^commit "
	EXP_grep -v "^\s\+WIP$"
	EXP_grep -v "^\s\+message2$"
	EXP_grep "^\s\+message3$"

	# expecting file changes to README, not UNTRACKED

	RUN git wip log --stat
	EXP_grep "^commit "
	EXP_grep "^ README | 2"
	EXP_grep -v "UNTRACKED"

	# need to be able to extract latest data from git wip branch

	RUN git show HEAD:README
	EXP_grep '^3$'
	EXP_grep -v '^4$'

	RUN git show wip/master:README
	EXP_grep -v '^3$'
	EXP_grep '^4$'
}

test_spaces() {
	# init

	create_test_tree
	RUN "echo 1 >\"s p a c e s\""
	RUN git add "\"s p a c e s\""
	RUN git commit -m "\"s p a c e s\""

	# make changes, store wip

	RUN "echo 2 >\"s p a c e s\""
	RUN git wip save "\"message with spaces\""
	EXP_none

	# expecting a wip ref

	RUN git for-each-ref
	EXP_grep "commit.refs/wip/master$"

	# expecting a log entry

	RUN git wip log
	EXP_grep "^commit "
	EXP_grep "^\s\+message with spaces$"
}

# ------------------------------------------------------------------------
# run tests

TESTS=( test_general test_spaces )

for TEST in "${TESTS[@]}" ; do
	echo "-- $TEST"
	$TEST
	echo "OK"
done

trap - INT TERM ERR
cleanup_test_tree
echo "DONE"


================================================
FILE: CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.26)
project(git-wip LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Enable generation of compile_commands.json
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

option(WIP_COVERAGE "Enable code coverage instrumentation" OFF)
option(WIP_STATIC  "Build a fully static binary"           OFF)

include(FetchContent)
include(CheckCXXSourceCompiles)

FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG v1.15.3
)

FetchContent_MakeAvailable(spdlog)

# Code coverage options — applied AFTER FetchContent so that third-party
# dependencies (spdlog, fmt, …) are NOT instrumented.  Mixing gcov versions
# between the host compiler and a pre-built dep causes "version mismatch"
# errors at runtime that pollute test output.
if(WIP_COVERAGE)
    add_compile_options(-fprofile-arcs -ftest-coverage)
    add_link_options(-fprofile-arcs -ftest-coverage)
endif()

# Detect whether the compiler's standard library ships <print> (C++23 P2093).
# GCC < 14 and some older clangs lack it even with -std=c++23.
check_cxx_source_compiles("
    #include <print>
    int main() { std::println(\"ok\"); }
" WIP_HAVE_STD_PRINT)

if(WIP_HAVE_STD_PRINT)
    message(STATUS "std::print available — using <print>")
else()
    message(STATUS "std::print not available — fetching {fmt} as fallback")
    FetchContent_Declare(
        fmt
        GIT_REPOSITORY https://github.com/fmtlib/fmt.git
        GIT_TAG 11.1.4
    )
    FetchContent_MakeAvailable(fmt)
endif()

include(FindPkgConfig)

if(WIP_STATIC)
    message(STATUS "WIP_STATIC=ON — building a mostly-static binary (libgit2 and all its deps statically linked; glibc stays shared)")
    pkg_check_modules(LIBGIT2 REQUIRED libgit2)

    # Collect the full transitive static link flags from pkg-config.
    # We split them into two groups so CMake can handle each token cleanly:
    #   LIBGIT2_STATIC_LIBS  — -lfoo tokens (library names, no -L prefix)
    #   LIBGIT2_STATIC_DIRS  — -L/path tokens (search paths)
    execute_process(
        COMMAND pkg-config --static --libs libgit2
        OUTPUT_VARIABLE _git2_raw
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    execute_process(
        COMMAND pkg-config --static --libs-only-l libgit2
        OUTPUT_VARIABLE _git2_libs_raw
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    execute_process(
        COMMAND pkg-config --static --libs-only-L libgit2
        OUTPUT_VARIABLE _git2_dirs_raw
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    # Also pull in the transitive deps of libssh2 (uses OpenSSL on Debian)
    execute_process(
        COMMAND pkg-config --static --libs-only-l libssh2
        OUTPUT_VARIABLE _ssh2_libs_raw
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    string(REPLACE "-l" "" _git2_libs_stripped "${_git2_libs_raw}")
    string(REPLACE "-L" "" _git2_dirs_stripped "${_git2_dirs_raw}")
    string(REPLACE "-l" "" _ssh2_libs_stripped "${_ssh2_libs_raw}")
    separate_arguments(LIBGIT2_STATIC_LIBS UNIX_COMMAND "${_git2_libs_stripped}")
    separate_arguments(LIBGIT2_STATIC_DIRS UNIX_COMMAND "${_git2_dirs_stripped}")
    separate_arguments(SSH2_STATIC_LIBS    UNIX_COMMAND "${_ssh2_libs_stripped}")
    # Deduplicate (SSH2 libs are a subset)
    list(APPEND LIBGIT2_STATIC_LIBS ${SSH2_STATIC_LIBS})
    list(REMOVE_DUPLICATES LIBGIT2_STATIC_LIBS)
else()
    pkg_check_modules(LIBGIT2 REQUIRED libgit2)
endif()

# Optional: GTest/GMock for unit tests (only available on some distros)
find_package(GTest QUIET)

enable_testing()

if(GTest_FOUND)
    message(STATUS "GTest found — building unit tests")
    add_subdirectory(test/unit)
else()
    message(STATUS "GTest not found — skipping unit tests")
endif()

add_subdirectory(test/cli)
add_subdirectory(test/nvim)

# the executable
add_subdirectory(src)


================================================
FILE: Dockerfile-deb
================================================
# vim: set ft=dockerfile
FROM debian:testing

WORKDIR /home/git-wip
COPY dependencies.sh dependencies.sh

# Install necessary packages
RUN apt-get update \
    && apt-get install -y sudo procps bash \
    && bash dependencies.sh \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# this lets us have the same UID:GID in the container
ARG UID
ARG GID
ARG USERNAME
RUN getent group users || groupadd -g $GID users
RUN useradd -u $UID -g $GID -m -s /bin/bash $USERNAME
RUN echo "$USERNAME  ALL=(ALL:ALL)  NOPASSWD:SETENV: ALL" > "/etc/sudoers.d/$USERNAME"
USER $USERNAME

# Create ~/.local/bin directory, add it to the PATH
RUN mkdir -p /home/${USERNAME}/.local/bin
RUN echo 'export PATH=$HOME/.local/bin:$PATH' >> /home/${USERNAME}/.bashrc

# Make git usable inside container
RUN git config --global --add safe.directory /home/git-wip

ARG GIT_EMAIL
ARG GIT_NAME
ENV GIT_EMAIL=${GIT_EMAIL}
ENV GIT_NAME=${GIT_NAME}
RUN git config --global user.email "${GIT_EMAIL}"
RUN git config --global user.name "${GIT_NAME}"

# Map the current directory to the container's ~/git-wip
WORKDIR /home/git-wip

# Keep running
CMD ["tail", "-f", "/dev/null"]



================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 2, June 1991

 Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it.  (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.)  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must show them these terms so they know their
rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  Finally, any free program is threatened constantly by software
patents.  We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary.  To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

  The precise terms and conditions for copying, distribution and
modification follow.

                    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License.  The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language.  (Hereinafter, translation is included without limitation in
the term "modification".)  Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

  1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

  2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices
    stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in
    whole or in part contains or is derived from the Program or any
    part thereof, to be licensed as a whole at no charge to all third
    parties under the terms of this License.

    c) If the modified program normally reads commands interactively
    when run, you must cause it, when started running for such
    interactive use in the most ordinary way, to print or display an
    announcement including an appropriate copyright notice and a
    notice that there is no warranty (or else, saying that you provide
    a warranty) and that users may redistribute the program under
    these conditions, and telling the user how to view a copy of this
    License.  (Exception: if the Program itself is interactive but
    does not normally print such an announcement, your work based on
    the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of Sections
    1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three
    years, to give any third party, for a charge no more than your
    cost of physically performing source distribution, a complete
    machine-readable copy of the corresponding source code, to be
    distributed under the terms of Sections 1 and 2 above on a medium
    customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer
    to distribute corresponding source code.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form with such
    an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it.  For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable.  However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

  4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License.  Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

  5. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Program or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

  7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all.  For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded.  In such case, this License incorporates
the limitation as if written in the body of this License.

  9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number.  If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

  10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
  `Gnomovision' (which makes passes at compilers) written by James Hacker.

  <signature of Ty Coon>, 1 April 1989
  Ty Coon, President of Vice

This General Public License does not permit incorporating your program into
proprietary programs.  If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.


================================================
FILE: README.md
================================================
# About

[![CI](https://github.com/bartman/git-wip/actions/workflows/ci.yml/badge.svg)](https://github.com/bartman/git-wip/actions)
[![Codecov](https://codecov.io/gh/bartman/git-wip/branch/master/graph/badge.svg)](https://codecov.io/gh/bartman/git-wip)

`git-wip` manages **Work In Progress** (or **WIP**) branches.
WIP branches are mostly throw-away but capture points of development
between commits.  The intent is to tie `git-wip` into your editor so
that each time you save a file the current working-tree state is
snapshotted in git.  `git-wip` also helps you return to a previous
state of development.

Latest `git-wip` can be obtained from [github.com](http://github.com/bartman/git-wip).
`git-wip` was written by [Bart Trojanowski](mailto:bart@jukie.net).
You can find out more from the original [blog post](http://www.jukie.net/bart/blog/save-everything-with-git-wip).

> **Note:** `git-wip` was originally a bash script (2009).
> This repository is a [C++ rewrite](http://www.jukie.net/bart/blog/git-wip-cpp-rewrite/);
> tag [v0.2](https://github.com/bartman/git-wip/releases/tag/v0.2) is the last bash version.
> The script was moved to `Attic/` and is no longer maintained.

---

## TL;DR

```sh
$ make
$ ./dependencies.sh         # assumes you're on a Debian-based distro, and you have `sudo`
$ make install              # installs to ~/.local by default
$ cd my-project
$ git wip                   # snapshot working tree → wip/master
```

---

## How WIP branches work

WIP branches are named after the current branch, prefixed with `wip/`.
If you are working on `feature`, the WIP branch is `wip/feature` (stored
as `refs/wip/feature`).

**First snapshot** — creates a commit on `wip/<branch>` rooted at the
current branch HEAD:

```
--- * --- * --- *          ← feature
                 \
                  W        ← wip/feature
```

**Subsequent snapshots** — stacks new WIP commits on top:

```
--- * --- * --- *          ← feature
                 \
                  W
                   \
                    W      ← wip/feature
```

**After a real commit** — the next `git wip` detects that the work branch
has advanced and resets the WIP branch to start from the new HEAD:

```
--- * --- * --- * --- *    ← feature
                 \     \
                  W     W  ← wip/feature
                   \
                    W  (reachable via reflog as wip/feature@{1})
```

Old WIP commits are never deleted; they remain reachable through
`git reflog show wip/<branch>`.

---

## Commands

### `git wip`

Snapshot the working tree with the default message `"WIP"`.
Equivalent to `git wip save "WIP"`.

### `git wip [--version | -v | version]`

Show the version string (from `git describe --tags --dirty=-dirty` at build time).

```
$ git wip --version
v0.2-83-g95a6648-dirty
```

### `git wip save [<message>] [options] [-- <file>...]`

Create a new WIP commit.

| Option | Description |
|---|---|
| `-e`, `--editor` | Quiet mode — exit 0 silently when there are no changes (for editor hooks) |
| `-u`, `--untracked` | Also capture untracked files |
| `-i`, `--ignored` | Also capture ignored files |
| `--no-gpg-sign` | Do not GPG-sign the commit (overrides `commit.gpgSign = true`) |

If `<file>...` arguments are given, only those files are snapshotted.
Otherwise all tracked files are updated.

### `git wip status [-l] [-f] [<ref>]`

Show the status of the WIP branch for the current work branch, or another branch.

```
$ git wip status
branch master has 5 wip commits on refs/wip/master
```

| Option | Description |
|---|---|
| `-l`, `--list` | List each WIP commit: short SHA, subject, and age |
| `-f`, `--files` | Show a `git diff --stat` of changes relative to the work branch HEAD |

`<ref>` is optional and can be any of:

- `<branch>`
- `wip/<branch>`
- `refs/heads/<branch>`
- `refs/wip/<branch>`

If `<ref>` is omitted, `git wip status` uses the current branch.

`-l` and `-f` can be combined; each commit line is then followed by its
own per-commit diff stat.

```
$ git wip status -l
branch master has 5 wip commits on refs/wip/master
1d146bf - WIP (53 minutes ago)
a3f901c - save file.c (50 minutes ago)
...

$ git wip status -f
branch master has 5 wip commits on refs/wip/master
 src/main.c | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

$ git wip status -l -f
branch master has 5 wip commits on refs/wip/master
1d146bf - WIP (53 minutes ago)
 src/main.c |  2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
...
```

### `git wip log [options]`

Show the git log for the current WIP branch.

| Option | Description |
|---|---|
| `-p`, `--pretty` | Compact colourised graph output |
| `-s`, `--stat` | Include per-commit diff stat |
| `-r`, `--reflog` | Show the reflog instead of the commit graph (useful for recovering old WIP stacks) |

### `git wip list [-v]`

List all WIP refs in the repository.

```
$ git wip list
wip/master
wip/feature
```

| Option | Description |
|---|---|
| `-v`, `--verbose` | Show how many WIP commits are ahead of the matching work branch; report orphaned WIP refs |

Verbose output example:

```
$ git wip list -v
wip/master has 5 commits ahead of master
wip/feature has 1 commit ahead of feature
wip/old-branch is orphaned
```

### `git wip delete [--yes] [<ref>]`

Delete WIP refs.

- With `<ref>`, deletes that branch's WIP ref (same ref formats as `status`):
  - `<branch>`
  - `wip/<branch>`
  - `refs/heads/<branch>`
  - `refs/wip/<branch>`
- With no `<ref>`, deletes the current branch's WIP ref and asks for confirmation.
- `--yes` skips the confirmation prompt (only relevant when `<ref>` is omitted).

```
$ git wip delete
About to delete wip/master [Y/n]
```

### `git wip delete --cleanup`

Delete orphaned WIP refs (any `refs/wip/<branch>` where `refs/heads/<branch>`
does not exist).

---

## Building

Requires: a C++23 compiler, CMake ≥ 3.26, Ninja, and `libgit2-dev`.

```sh
$ ./dependencies.sh # install packages needed to build
$ make              # Release build → build/src/git-wip
$ make TYPE=Debug   # Debug build
$ make test         # Build + run all tests (unit + CLI integration)
$ make install      # Install to ~/.local  (override with PREFIX=...)
```

Dependencies (`spdlog`) are fetched automatically by CMake via
`FetchContent`.  `libgit2` must be installed system-wide (e.g.
`apt install libgit2-dev`).

If you'd rather build a static binary (tested on Debian/Ubuntu), you can use:

```sh
$ ./dependencies.sh --static  # install extra dependencies
$ make STATIC=1               # build the static binary
```

---

## Installation

```sh
$ git clone https://github.com/bartman/git-wip.git
$ cd git-wip
$ ./dependencies.sh
$ make
$ make install          # → ~/.local/bin/git-wip
```

Or copy the binary manually:

```sh
$ cp build/src/git-wip ~/bin/
```

---

## Editor integration

### vim

The vim plugin shells out to `git wip` on every file save, so the `git-wip`
binary must be installed and on your `PATH` before the plugin will do anything.
Easiest way to do this is to run `make install`.

Verify you're ready with:

```sh
$ git wip -h
```

**(1)** With [lazy.nvim](https://github.com/folke/lazy.nvim) (Neovim only):

```lua
{
    "bartman/git-wip",
    opts = {
        gpg_sign = false,    -- true enables GPG signing of commits
        untracked = true,    -- true to include untracked files
        ignored = false,     -- true to include files ignored by .gitignore
        background = false,  -- true for async execution if supported (Neovim 0.10+), false for sync
        filetypes = { "*" }, -- list of vim file types to call git-wip on
    },
},
```

**(2)** With [Vundle](https://github.com/gmarik/Vundle.vim) (Vim only):

```vim
Bundle 'bartman/git-wip', {'rtp': 'vim/'}
```

**(3)** Copy the plugin directly:

Not really recommended (or supported), but if you want to you could copy it to your Neovim plugin directory...

```sh
$ cp lua/git-wip/init.vim ~/.config/nvim/plugin/lua/git-wip.lua
```
and then in your `~/.config/nvim/init.lua` you will need to:
```lua
require("git-wip").setup({})
```

Meanwhile if you use Vim, you can do this:
```
$ cp vim/plugin/git-wip.vim ~/.vim/plugin/
```
and Vim will pick it up from there.

**(4)** Or add an autocommand to your `.vimrc`:

This bypasses a plugin, just uses the executable.

```vim
augroup git-wip
  autocmd!
  autocmd BufWritePost * :silent !cd "`dirname "%"`" && git wip save "WIP from vim" --editor -- "`basename "%"`"
augroup END
```

The `--editor` flag makes `git-wip` silent when there are no changes.

### emacs

Add to your `.emacs`:

```lisp
(load "/{path_to_git-wip}/emacs/git-wip.el")
```

Or copy the contents of `emacs/git-wip.el` directly into your `.emacs`.

### Sublime Text

A Sublime Text plugin is provided in the `sublime/` directory.

---

## Recovery

Find the commit you want to recover:

```sh
$ git wip status -l           # show current WIP stack
$ git reflog show wip/master  # show full history including reset points
$ git log -g -p wip/master    # inspect with diffs
```

Check out the files from a WIP commit (HEAD stays where it is):

```sh
$ git checkout wip/master -- .   # restore entire tree
$ git checkout <sha> -- path/to/file  # restore a single file
```

The changes land in the index and working tree.  Review with:

```sh
$ git diff --cached
```

Adjust with `git reset` / `git add -p` as needed, then commit.

# Appendix

## related projects

- [wip.rs](https://github.com/dlight/wip.rs) is a Rust executable that watches for changes in your work, and invokes `git-wip` as needed.
  (by [dlight](https://github.com/dlight)).

## star history

<a href="https://www.star-history.com/?repos=bartman%2Fgit-wip&type=date&legend=top-left">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=bartman/git-wip&type=date&theme=dark&legend=top-left" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=bartman/git-wip&type=date&legend=top-left" />
   <img alt="Star History Chart" src="https://api.star-history.com/image?repos=bartman/git-wip&type=date&legend=top-left" />
 </picture>
</a>


================================================
FILE: cmake/GitVersion.cmake
================================================
# GitVersion.cmake - Integration for GitVersion.sh
#
# Usage:
#   include(GitVersion)
#   gitversion_generate(PREFIX GIT_WIP_ OUTPUT ${CMAKE_BINARY_DIR}/git_wip_version.h)

function(gitversion_generate)
    set(options)
    set(oneValueArgs PREFIX OUTPUT)
    set(multiValueArgs)

    cmake_parse_arguments(GV "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    if(NOT GV_PREFIX)
        message(FATAL_ERROR "gitversion_generate: PREFIX is required")
    endif()
    if(NOT GV_OUTPUT)
        message(FATAL_ERROR "gitversion_generate: OUTPUT is required")
    endif()

    # Run GitVersion.sh to generate the version header
    execute_process(
        COMMAND ${CMAKE_SOURCE_DIR}/cmake/GitVersion.sh ${GV_PREFIX} ${GV_OUTPUT}
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        RESULT_VARIABLE GV_RESULT
    )

    if(NOT GV_RESULT EQUAL 0)
        message(WARNING "gitversion_generate: Failed to run GitVersion.sh")
    endif()

    # Add a custom command to regenerate the version header
    add_custom_command(
        OUTPUT ${GV_OUTPUT}
        COMMAND ${CMAKE_SOURCE_DIR}/cmake/GitVersion.sh ${GV_PREFIX} ${GV_OUTPUT}
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        DEPENDS ${CMAKE_SOURCE_DIR}/cmake/GitVersion.sh
        COMMENT "Generating version header: ${GV_OUTPUT}"
        VERBATIM
    )

    # Add a custom target that depends on the version header
    add_custom_target(gitversion DEPENDS ${GV_OUTPUT})
endfunction()


================================================
FILE: cmake/GitVersion.sh
================================================
#!/usr/bin/env bash
# GitVersion.sh - Generate version header from git describe
# Usage: GitVersion.sh PREFIX OUTPUT

set -e

PREFIX="$1"
OUTPUT="$2"

if [ -z "$PREFIX" ] || [ -z "$OUTPUT" ]; then
    echo "Usage: $0 PREFIX OUTPUT" >&2
    exit 1
fi

# Get git describe output
DESCRIBE="$(git describe --tags --dirty=-dirty 2>/dev/null || echo "unknown")"

# Generate temporary output file
OUTPUT_TMP="${OUTPUT}.tmp"

cat > "$OUTPUT_TMP" << EOF
#pragma once
#define ${PREFIX}VERSION "${DESCRIBE}"
EOF

# Only update the file if it changed
if [ -f "$OUTPUT" ] && cmp -s "$OUTPUT" "$OUTPUT_TMP"; then
    rm -f "$OUTPUT_TMP"
else
    mv "$OUTPUT_TMP" "$OUTPUT"
fi


================================================
FILE: dependencies.sh
================================================
#!/bin/bash
set -e

SUDO=sudo
[ "$(id -u)" = 0 ] && SUDO=

function die() {
    echo >&2 "ERROR: $*"
    exit 1
}

# ---------------------------------------------------------------------------
# Detect package manager
# ---------------------------------------------------------------------------

if command -v nix &>/dev/null && [ -e ~/.nix-profile/etc/profile.d/nix.sh ]; then
    # shellcheck disable=SC1091
    source ~/.nix-profile/etc/profile.d/nix.sh
fi

if command -v nix-shell &>/dev/null || command -v nix &>/dev/null; then
    if [ -f shell.nix ] || [ -f default.nix ]; then
        die "Nix detected. Run 'nix develop' to enter a dev shell."
    fi
fi

pkg_mgr=""
if command -v apt &>/dev/null; then
    pkg_mgr=apt
elif command -v dnf &>/dev/null; then
    pkg_mgr=dnf
elif command -v pacman &>/dev/null; then
    pkg_mgr=pacman
elif command -v nix &>/dev/null; then
    die "Nix detected but no shell.nix found. Run 'nix develop' to start a dev shell."
else
    die "Unsupported system: no apt, dnf, pacman, or nix found. dependencies.sh does not support this OS."
fi

echo "Detected package manager: $pkg_mgr"

# sync the package database once at the start (if needed)
case "$pkg_mgr" in
    apt)
        $SUDO apt update
        ;;
    pacman)
        $SUDO pacman -Sy --noconfirm
        ;;
esac

# ---------------------------------------------------------------------------
# Package manager helpers
# ---------------------------------------------------------------------------

function must_have_one_of() {
    local pkg_names=("$@")
    for n in "${pkg_names[@]}" ; do
        case "$pkg_mgr" in
            apt)
                # apt-cache show exits non-zero when the package is unknown;
                # apt policy always exits 0 so it cannot be used for existence checks.
                if apt-cache show "$n" >/dev/null 2>&1 ; then
                    echo "$n"
                    return
                fi
                ;;
            dnf)
                if dnf list available "$n" >/dev/null 2>&1 ; then
                    echo "$n"
                    return
                fi
                ;;
            pacman)
                # Check if package exists (installed or available in repos)
                if pacman -Si "$n" >/dev/null 2>&1 ; then
                    echo "$n"
                    return
                fi
                ;;
        esac
    done
    die "$pkg_mgr cannot find any of these packages: ${pkg_names[*]}"
}

function want_one_of() {
    local pkg_names=("$@")
    for n in "${pkg_names[@]}" ; do
        case "$pkg_mgr" in
            apt)
                # apt-cache show exits non-zero when the package is unknown;
                # apt policy always exits 0 so it cannot be used for existence checks.
                if apt-cache show "$n" >/dev/null 2>&1 ; then
                    echo "$n"
                    return
                fi
                ;;
            dnf)
                if dnf list available "$n" >/dev/null 2>&1 ; then
                    echo "$n"
                    return
                fi
                ;;
            pacman)
                # Check if package exists (installed or available in repos)
                if pacman -Si "$n" >/dev/null 2>&1 ; then
                    echo "$n"
                    return
                fi
                ;;
        esac
    done
}

# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------

compiler=""   # empty → auto-select via must_have_one_of
coverage=0    # --coverage → install lcov, curl, gpg
static=0      # --static  → install static libs needed for STATIC=1 builds

for arg in "$@" ; do
    case "$arg" in
        --compiler=gcc)   compiler=gcc   ;;
        --compiler=gnu)   compiler=gnu   ;;
        --compiler=g++)   compiler=g++   ;;
        --compiler=clang) compiler=clang ;;
        --compiler=*)
            die "unknown --compiler value '${arg#--compiler=}' (expected gcc or clang)" ;;
        --coverage)
            coverage=1 ;;
        --static)
            static=1 ;;
        -h|--help)
            cat <<'EOF'
Usage: dependencies.sh [--compiler=<gcc|clang>] [--coverage] [--static] [-h|--help]

Install build dependencies for git-wip.

Options:
  --compiler=gcc    Install gcc and g++
  --compiler=clang  Install clang
  (no flag)         Install whichever of clang/gcc is available (auto-select)

  --coverage        Also install coverage tools (lcov, curl, gpg)

  --static          Also install static libraries required for `make STATIC=1`
                    (libllhttp-dev and any other missing static .a files)

  -h, --help        Show this help and exit
EOF
            exit 0
            ;;
        *)
            die "unknown argument '$arg'" ;;
    esac
done

# ---------------------------------------------------------------------------
# Compiler packages
# ---------------------------------------------------------------------------

compiler_packages=()

case "$compiler" in
    gcc|gnu|g++)
        # On Arch, gcc includes both C and C++ compilers (no separate g++ package)
        if [ "$pkg_mgr" = "pacman" ]; then
            compiler_packages+=( gcc )
        else
            compiler_packages+=( gcc g++ )
        fi
        ;;
    clang)
        compiler_packages+=( "$(must_have_one_of clang)" )
        ;;
    "")
        # No preference — pick whatever is available, prefer clang for the
        # C compiler slot and gcc for the C++ slot (matches the old behaviour).
        if [ "$pkg_mgr" = "pacman" ]; then
            # On Arch, gcc package includes both C and C++ compilers
            compiler_packages+=(
                "$(must_have_one_of clang gcc)"
            )
        else
            compiler_packages+=(
                "$(must_have_one_of clang gcc)"
                "$(must_have_one_of clang g++)"
            )
        fi
        ;;
esac

# ---------------------------------------------------------------------------
# Full package list
# ---------------------------------------------------------------------------

packages=(
    cmake
    git
    make
    ninja-build
    pkg-config
    python3
)

# Optional packages (only install if available)
want_one_of clangd && packages+=($(want_one_of clangd))
#want_one_of valgrind && packages+=($(want_one_of valgrind))

# Compiler packages
packages+=("${compiler_packages[@]}")

# Build tools (different package names between apt, dnf, and pacman)
case "$pkg_mgr" in
    apt)
        packages+=(
            googletest
            libgmock-dev
            libgtest-dev
            libgit2-dev
        )
        ;;
    dnf)
        packages+=(
            gtest-devel
            gmock-devel
            libgit2-devel
        )
        ;;
    pacman)
        # Arch uses different package names
        packages+=(
            libgit2
        )
        # Replace base packages with Arch equivalents
        packages=( "${packages[@]/ninja-build/ninja}" )
        packages=( "${packages[@]/pkg-config/pkgconf}" )
        packages=( "${packages[@]/python3/python}" )
        # For clang, ensure C++ standard library is available
        if [ "$compiler" = "clang" ] || [ "$compiler" = "" ]; then
            packages+=( gcc-libs )
        fi
        ;;
esac

# Coverage tools (only when --coverage is requested)
if [ "$coverage" = 1 ]; then
    case "$pkg_mgr" in
        apt)
            packages+=( lcov curl gpg )
            ;;
        dnf)
            packages+=( lcov curl gnupg2 )
            ;;
        pacman)
            packages+=( lcov curl gnupg )
            ;;
    esac
fi

# Static-build extra libs (only when --static is requested)
# Most static .a files come from the -dev packages already installed above.
# The extras needed on Debian/Ubuntu:
#   libgpg-error-dev → libgpg-error.a (libssh2 transitive dep)
#   libzstd-dev      → libzstd.a     (libssh2 transitive dep)
#   libkrb5-dev      → provides libgssapi_krb5.so stubs for the dynamic link
#   libllhttp-dev    → libllhttp.a   (libgit2 HTTP parser)
#                      Present on Debian stable; Ubuntu 24.04 (noble) embeds
#                      llhttp statically inside libgit2.a so the package does
#                      not exist there — detected and skipped automatically.
# Fedora and Arch do not ship libgit2.a / libssh2.a / libssl.a, so the static
# build is not supported on those distros; nothing extra to install.
if [ "$static" = 1 ]; then
    case "$pkg_mgr" in
        apt)
            packages+=( libgpg-error-dev libzstd-dev libkrb5-dev )
            # libllhttp-dev is optional — present on Debian stable, absent on
            # Ubuntu 24.04 (noble embeds llhttp statically inside libgit2.a)
            llhttp_pkg=$(want_one_of libllhttp-dev)
            [ -n "$llhttp_pkg" ] && packages+=( "$llhttp_pkg" )
            ;;
        dnf)
            # Fedora does not ship libgit2.a / libssh2.a / libssl.a, so a
            # fully static build is not supported there.  Nothing to install.
            ;;
        pacman)
            # Arch does not ship libgit2.a either; nothing to install.
            ;;
    esac
fi

set -e -x

case "$pkg_mgr" in
    apt)
        $SUDO apt update
        $SUDO apt install -y "${packages[@]}"
        ;;
    dnf)
        $SUDO dnf install -y "${packages[@]}"
        ;;
    pacman)
        $SUDO pacman -Sy --noconfirm "${packages[@]}"
        ;;
esac


================================================
FILE: dev.sh
================================================
#!/usr/bin/env bash
set -e

PROJECT=git-wip
TARGET=

HOSTNAME="$(hostname)"
USERNAME="$(id -u -n)"
USER_UID="$(id -u)"
USER_GID="$(id -g)"

# terminal colors
BLACK='\033[0;30m' RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[0;37m'
BRIGHT_BLACK='\033[1;30m' BRIGHT_RED='\033[1;31m' BRIGHT_GREEN='\033[1;32m' BRIGHT_YELLOW='\033[1;33m' BRIGHT_BLUE='\033[1;34m' BRIGHT_MAGENTA='\033[1;35m' BRIGHT_CYAN='\033[1;36m' BRIGHT_WHITE='\033[1;37m'
RESET='\033[0m'

function die {
    echo >&2 "ERROR: $*"
    exit 1
}

function available_targets {
    ls Dockerfile-* 2>/dev/null | sed -n 's/^Dockerfile-\([A-Za-z0-9]\+\)$/\1/p'
}

declare TARGET= DOCKERFILE= NAME=

function set_target {
    TARGET="$1"
    [ -z "$TARGET" ] && die "Target not specified"
    DOCKERFILE="Dockerfile-$TARGET"
    [ -f "$DOCKERFILE" ] || die "$DOCKERFILE: no such file"
    [ -r "$DOCKERFILE" ] || die "$DOCKERFILE: cannot be read"
    NAME="$PROJECT-builder-$USERNAME-$TARGET"
}

declare -a BUILD_ARGS=()

function set_build_args {
    local value

    # transfer host user.email to docker
    value="$(git config user.email)"
    [ -z "$value" ] && value="$USERNAME@$HOSTNAME"
    [ -n "$value" ] && BUILD_ARGS+=( "--build-arg=GIT_EMAIL=$value" )

    # transfer host user.name to docker
    value="$(git config user.name)"
    [ -z "$value" ] && value="$USERNAME"
    [ -n "$value" ] && BUILD_ARGS+=( "--build-arg=GIT_NAME=$value" )

    # Add UID, GID, and USERNAME as build arguments to match host user
    BUILD_ARGS+=( "--build-arg=UID=$USER_UID" )
    BUILD_ARGS+=( "--build-arg=GID=$USER_GID" )
    BUILD_ARGS+=( "--build-arg=USERNAME=$USERNAME" )
}

function show_status {
    local images=( $( docker images | awk "\$1 ~ /\<${NAME}\>/ { print \$3 }" ) )
    if [ "${#images[@]}" -gt 0 ] ; then
        echo -e "${NAME}: image is ${BRIGHT_GREEN}built${RESET}:    ${BRIGHT_BLUE}${images[*]}${RESET}"
    else
        echo -e "${NAME}: image is ${BRIGHT_RED}not built${RESET}."
    fi

    # Check if container is running
    local running=( $( docker ps | awk "\$2 ~ /\<${NAME}\>/ { print \$1 }" ) )
    if [ "${#running[@]}" -gt 0 ] ; then
        echo -e "${NAME}: cntnr is ${BRIGHT_GREEN}running${RESET}:  ${BRIGHT_BLUE}${running[*]}${RESET}"
    fi

    local stopped=( $( docker ps -a | awk "\$2 ~ /\<${NAME}\>/ && /Exited / { print \$1 }" ) )
    if [ "${#stopped[@]}" -gt 0 ] ; then
        echo -e "${NAME}: cntnr is ${BRIGHT_YELLOW}stopped${RESET}:  ${BRIGHT_BLUE}${stopped[*]}${RESET}"
    fi

    if [ "${#running[@]}" -eq 0 ] && [ "${#stopped[@]}" -eq 0 ] ; then
        echo -e "${NAME}: cntnr does ${BRIGHT_RED}not exist${RESET}."
    fi
}

# Function to display help
show_help() {
    cat <<END
${0##*/} <target> <command>[,<command>,...] [...]

    Available commands:

        build          - create a dev image
        remove         - remove a dev image
        start          - start the container
        stop           - stop the container
        status         - check if built/running
        connect        - get a shell in the container
        run     <cmd>  - run a command in the container

    Available targets:

END
    for t in $(available_targets); do
        echo "        $t"
    done
    echo
}

target="$1" ; shift
case "$target" in
    help|--help|-h)
        show_help
        exit 0
        ;;
    status|--status|-s)
        for t in $(available_targets) ; do
            set_target "$t"
            show_status
            echo
        done
        exit 0
        ;;
esac

set_target "$target"
cmd="$1" ; shift

case "$cmd" in
    help|--help|-h)
        show_help
        exit 0
        ;;
    *,*)
        re="^[a-z,]+$"
        [[ "$cmd" =~ $re ]] || die "bad"
        for x in ${cmd//,/ } ; do
            echo -e >&2 "# ${BRIGHT_YELLOW}$0 $x $*${RESET}"
            if ! $0 "$target" "$x" "$@" ; then
                die "failed: $0 $x $*"
            fi
        done
        exit 0
        ;;
    build)
        set_build_args
        ( set -x
        docker build "${@}" "${BUILD_ARGS[@]}" -t "${NAME}" -f "Dockerfile-$TARGET" .
        )
        exit $?
        ;;
    rm|remove)
        ( set -x
        docker image rm -f "${NAME}"
        )
        exit $?
        ;;
    up|start)
        ( set -x
        docker run -d --init -v "$(pwd):/home/$PROJECT" --user "$USER_UID:$USER_GID" --name "${NAME}" "${NAME}"
        )
        exit $?
        ;;
    down|stop)
        container_ids=( $(docker ps -a --filter "name=${NAME}" --format='{{.ID}}') )
        if [ "${#container_ids[@]}" = 0 ]; then
            echo "Container ${NAME} is not running."
        else
            for container_id in "${container_ids[@]}" ; do
                ( set -x
                docker stop "$container_id"
                )
            done
            for container_id in "${container_ids[@]}" ; do
                ( set -x
                docker rm "$container_id"
                )
            done
        fi
        exit 0
        ;;
    status)
        show_status
        exit $?
        ;;
    connect|enter)
        container_id="$(docker ps -a --filter "name=${NAME}" --format='{{.ID}}')"
        if [ -z "$container_id" ]; then
            echo "Container ${NAME} is not running."
        else
            ( set -x
            docker exec -it "$container_id" bash
            )
        fi
        ;;
    run|exec)
        container_id="$(docker ps -a --filter "name=${NAME}" --format='{{.ID}}')"
        if [ -z "$container_id" ]; then
            echo "Container ${NAME} is not running."
            exit 1
        elif [ $# -eq 0 ]; then
            echo "No command provided to run in the container."
            exit 1
        else
            ( set -x
            #docker exec -w /home/${PROJECT} "$container_id" "$@"
            docker exec -i -w /home/${PROJECT} "$container_id" /bin/bash -l -c "$*"
            )
        fi
        ;;
    *)
        die "${0##*/} [ build | remove | start | stop | status | connect ] <target>"
        ;;
esac


================================================
FILE: doc/git-wip.txt
================================================
*git-wip.txt*           Git WIP (Work In Progress) for Neovim

==============================================================================
Table of Contents                                      *git-wip-toc*

    1. Introduction ................ |git-wip-intro|
    2. Requirements ................ |git-wip-requirements|
    3. Installation ................ |git-wip-installation|
    4. Configuration ............... |git-wip-configuration|
    5. Commands .................... |git-wip-commands|
    6. Lua API ..................... |git-wip-lua-api|
    7. Vim Compatibility ........... |git-wip-vim|
    8. Examples .................... |git-wip-examples|

==============================================================================
Introduction                                           *git-wip-intro*

`git-wip` automatically creates Work-In-Progress snapshots using the
`git-wip` tool every time you save a buffer in Neovim.

It works together with the C++ rewrite of `git-wip` to maintain a stack of
temporary commits on `wip/<branch>` refs.

==============================================================================
Requirements                                        *git-wip-requirements*

- Neovim >= 0.8
- The `git-wip` binary must be in your $PATH
- The current working directory (or its parent) must be a git repository

==============================================================================
Installation                                        *git-wip-installation*

*lazy.nvim* (recommended):

```lua
{
  "bartman/git-wip",
  opts = {
    -- see |git-wip-configuration|
    untracked = true,
    ignored = false,
  },
}
```

Lazy will automatically call `require("git-wip").setup(opts)`.

==============================================================================
Configuration                                       *git-wip-configuration*

All options are passed in the opts table: >

    gpg_sign  (boolean|nil)
    untracked (boolean|nil)
    ignored   (boolean|nil)
    filetypes (string[])

*gpg_sign* (boolean|nil) ~
    Override GPG signing behavior (currently not implemented in git-wip).
    true    → git-wip save --gpg-sign
    false   → git-wip save --no-gpg-sign
    nil     → use git-wip default

*untracked* (boolean|nil) ~
    Capture untracked files.
    true    → git-wip save --untracked
    false   → git-wip save --no-untracked
    nil     → use git-wip default

*ignored* (boolean|nil) ~
    Capture ignored files.
    true    → git-wip save --ignored
    false   → git-wip save --no-ignored
    nil     → use git-wip default

*filetypes* (string[]) ~
    List of filetypes where the plugin should run.
    Use `"*"` for all files.
    Default: `{"*"}`

================================================================================
Commands                                               *git-wip-commands*

:Wip                                                           *:Wip*
    Save a WIP snapshot for the current buffer.
    The file's directory and filename are extracted from the current
    buffer's path and passed to git-wip.

    This command is useful when you want to save a snapshot without
    writing the buffer, or when working with files outside of git.

:WipAll                                                        *:WipAll*
    Save a WIP snapshot for all changes in the current working directory.
    The filename is omitted, so git-wip captures all changes (similar to
    `git-wip save` without specifying files).

================================================================================
Lua API                                             *git-wip-lua-api*

setup({opts})                                        *git-wip.setup()*
    Configure the plugin and register the BufWritePost autocmd.
    Automatically called by lazy.nvim.

GitWipBufWritePost()                        *git-wip.GitWipBufWritePost()*
    Manually trigger a snapshot (the same function used by the autocmd).

==============================================================================
Vim Compatibility                                   *git-wip-vim*

This plugin also ships `vim/plugin/git-wip.vim` for classic Vim users.
That file is ignored in Neovim.

==============================================================================
Examples                                            *git-wip-examples*

```lua
require("git-wip").setup({
  gpg_sign = false,
  untracked = true,
  ignored = false,
  filetypes = { "lua", "c", "cpp", "python" },
})
```

On every `:w` you will see a notification like: >
    [git-wip] saved main.cpp in 0.037 sec

==============================================================================
vim:tw=78:ts=8:noet:ft=help:norl:


================================================
FILE: emacs/git-wip-mode.el
================================================
;;; git-wip-mode.el --- Use git-wip to record every buffer save

;; Copyright (C) 2013  Jerome Baum

;; Author: Jerome Baum <jerome@jeromebaum.com>
;; Version: 0.1
;; Keywords: vc

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;;; Code:

(eval-when-compile
  (require 'cl))

(require 'vc)

(defvar git-wip-buffer-name " *git-wip*"
  "Name of the buffer to which git-wip's output will be echoed")

(defvar git-wip-path
  (or
   ;; Internal copy of git-wip; preferred because it will be
   ;; version-matched
   (expand-file-name
    "../git-wip"
    (file-name-directory
     (or load-file-name
         (locate-library "git-wip-mode"))))
   ;; Look in $PATH and git exec-path
   (let ((exec-path
          (append
           exec-path
           (parse-colon-path
            (replace-regexp-in-string
             "[ \t\n\r]+\\'" ""
             (shell-command-to-string "git --exec-path"))))))
     (executable-find "git-wip"))))

(defun git-wip-after-save ()
  (when (and (string= (vc-backend (buffer-file-name)) "Git")
             git-wip-path)
    (start-process "git-wip" git-wip-buffer-name
                   git-wip-path "save" (concat "WIP from emacs: "
                                               (file-name-nondirectory
                                                buffer-file-name))
                   "--editor" "--"
                   (file-name-nondirectory buffer-file-name))
    (message (concat "Wrote and git-wip'd " (buffer-file-name)))))

;;;###autoload
(define-minor-mode git-wip-mode
  "Toggle git-wip mode.
With no argument, this command toggles the mode.
Non-null prefix argument turns on the mode.
Null prefix argument turns off the mode.

When git-wip mode is enabled, git-wip will be called every time
you save a buffer."
  ;; The initial value.
  nil
  ;; The indicator for the mode line.
  " WIP"
  :group 'git-wip

  ;; (de-)register our hook
  (if git-wip-mode
      (add-hook 'after-save-hook 'git-wip-after-save nil t)
    (remove-hook 'after-save-hook 'git-wip-after-save t)))

(defun git-wip-mode-if-git ()
  (when (string= (vc-backend (buffer-file-name)) "Git")
    (git-wip-mode t)))

(add-hook 'find-file-hook 'git-wip-mode-if-git)

(provide 'git-wip-mode)
;;; git-wip-mode.el ends here


================================================
FILE: emacs/git-wip.el
================================================
(defun git-wip-wrapper () 
  (interactive)
  (let ((file-arg (shell-quote-argument (buffer-file-name))))
    (shell-command (concat "git-wip save \"WIP from emacs: " (buffer-file-name) "\" --editor -- " file-arg))
    (message (concat "Wrote and git-wip'd " (buffer-file-name)))))

(defun git-wip-if-git ()
  (interactive)
  (when (string= (vc-backend (buffer-file-name)) "Git")
    (git-wip-wrapper)))

(add-hook 'after-save-hook 'git-wip-if-git)


================================================
FILE: flake.nix
================================================
{
  description = "git-wip — Work In Progress branch manager";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in
      {
        devShells.default = pkgs.mkShell {
          name = "git-wip-dev";

          # Build tools and dependencies
          # Nix name          ↔  apt name
          # cmake             ↔  cmake
          # ninja             ↔  ninja-build
          # pkg-config        ↔  pkg-config
          # gnumake           ↔  make
          # gcc / stdenv      ↔  gcc / g++
          # clang-tools       ↔  clangd
          # clang             ↔  clang
          # libgit2           ↔  libgit2-dev
          # gtest             ↔  googletest / libgmock-dev / libgtest-dev
          # git               ↔  git
          packages = with pkgs; [
            # build system
            cmake
            ninja
            pkg-config
            gnumake

            # compilers
            gcc
            clang
            clang-tools   # provides clangd

            # runtime library (required at link time)
            libgit2

            # test framework
            gtest

            # version control (needed by cmake FetchContent and tests)
            git

            # python is used by test/runner.py
            python3
          ];

          # Ensure pkg-config can find libgit2
          PKG_CONFIG_PATH = "${pkgs.libgit2}/lib/pkgconfig";

          shellHook = ''
            echo "git-wip dev shell"
            echo "  compiler:  $(c++ --version | head -1)"
            echo "  cmake:     $(cmake --version | head -1)"
            echo "  libgit2:   $(pkg-config --modversion libgit2)"
            echo ""
            echo "  build:     make"
            echo "  test:      make test"
          '';
        };
      }
    );
}


================================================
FILE: lua/git-wip/init.lua
================================================
-- this is a Neovim plugin that launches git-wip save on every buffer write
local vim = vim
local M = {}

-- Detect Neovim version for API compatibility
-- vim.system is available in Neovim 0.10+
local has_vim_system = vim.system ~= nil
local has_loop_spawn = vim.loop.spawn ~= nil
local has_loop_hrtime = vim.loop and vim.loop.hrtime ~= nil

-- Configuration
M.defaults = {
  git_wip_path = "git-wip",  -- path to git-wip binary (can be absolute)
  gpg_sign     = nil,        -- true for --gpg-sign, false for --no-gpg-sign
  untracked    = nil,        -- true for --untracked, false for --no-untracked
  ignored      = nil,        -- true for --ignored, false for --no-ignored
  background   = nil,        -- true for async execution if supported, false for sync
  filetypes    = { "*" },
}

---@type table
M.config = M.defaults

---Wrapper for vim.loop.hrtime(), returns 0 if not available
local function get_hrtime()
  if has_loop_hrtime then
    return vim.loop.hrtime()
  else
    return 0
  end
end

---Helper for tri-state flags
local function add_tri_flag(cmd, value, positive, negative)
  if value == true then
    table.insert(cmd, positive)
  elseif value == false then
    table.insert(cmd, negative)
  end
  -- nil = do nothing (git-wip default)
end

---Helper: Build command array
local function build_command(display_name, filename)
  local cmd = { M.config.git_wip_path, "save", string.format("WIP from neovim for %s", display_name) }
  add_tri_flag(cmd, M.config.gpg_sign, "--gpg-sign", "--no-gpg-sign")
  add_tri_flag(cmd, M.config.untracked, "--untracked", "--no-untracked")
  add_tri_flag(cmd, M.config.ignored, "--ignored", "--no-ignored")
  table.insert(cmd, "--editor")
  if filename ~= nil then
    table.insert(cmd, "--")
    table.insert(cmd, filename)
  end
  return cmd
end

---Helper: Notify result
local function notify_result(display_name, code, elapsed)
  if code == 0 then
    local msg = "[git-wip] saved " .. display_name
    if elapsed and elapsed > 0 then
      msg = msg .. string.format(" in %.3f sec", elapsed)
    end
    vim.notify(msg, vim.log.levels.INFO)
  else
    local msg = "[git-wip] failed for " .. display_name .. " (exit " .. code .. ")"
    if M.config.background and not has_loop_spawn then
      msg = msg .. " (async not supported, ran sync)"
    end
    vim.notify(msg, vim.log.levels.WARN)
  end
end

---Helper: Run sync
local function run_sync(cmd, dir, display_name)
  local start = get_hrtime()
  local code = 0
  if has_vim_system then
    local job = vim.system(cmd, { cwd = dir, text = true })
    local result = job:wait()
    code = result.code
  else
    local shell_cmd = "cd " .. vim.fn.shellescape(dir) .. " && " .. cmd[1]
    for i = 2, #cmd do
      shell_cmd = shell_cmd .. " " .. vim.fn.shellescape(cmd[i])
    end
    vim.fn.system(shell_cmd)
    code = vim.v.shell_error
  end
  local elapsed = (get_hrtime() - start) / 1e9
  notify_result(display_name, code, elapsed)
end

---Helper: Run async
local function run_async(cmd, dir, display_name)
  local unpack = table.unpack or unpack -- unpack is deprecated
  local start = get_hrtime()
  local handle
  handle = vim.loop.spawn(cmd[1], {
    args = {unpack(cmd, 2)},
    cwd = dir,
    stdio = {nil, nil, nil},
  }, function(code, signal)
    local elapsed = has_loop_hrtime and (get_hrtime() - start) / 1e9 or 0
    notify_result(display_name, code, elapsed)
    handle:close()
  end)
  if not handle then
    vim.notify("Failed to spawn git-wip process", vim.log.levels.ERROR)
  end
end

---Setup function (automatically called by Lazy.nvim)
---@param opts? table
function M.setup(opts)
  -- Merge user config
  M.config = vim.tbl_deep_extend("force", M.defaults, opts or {})

  -- Register the BufWritePost autocmd once
  vim.api.nvim_create_autocmd("BufWritePost", {
    group = vim.api.nvim_create_augroup("GitWip", { clear = true }),
    pattern = "*",
    callback = M.GitWipBufWritePost,
    desc = "git-wip: run after buffer write",
  })

  -- Register commands
  vim.api.nvim_create_user_command("Wip", function()
    local fullpath = vim.api.nvim_buf_get_name(0)
    if fullpath == "" then
      vim.notify("[git-wip] no file name for current buffer", vim.log.levels.ERROR)
      return
    end
    local dir = vim.fn.fnamemodify(fullpath, ":h")
    local filename = vim.fn.fnamemodify(fullpath, ":t")
    M.RunGitWip(dir, filename)
  end, {
    desc = "Save WIP snapshot for the current buffer",
  })

  vim.api.nvim_create_user_command("WipAll", function()
    local dir = vim.fn.getcwd()
    M.RunGitWip(dir, nil)
  end, {
    desc = "Save WIP snapshot for all changes in the current directory",
  })
end

function M.RunGitWip(dir, filename)
  local display_name = filename or '*'
  local cmd = build_command(display_name, filename)
  if M.config.background and has_loop_spawn then
    run_async(cmd, dir, display_name)
  else
    run_sync(cmd, dir, display_name)
  end
end

function M.GitWipBufWritePost()
  local fullpath = vim.api.nvim_buf_get_name(0)
  if fullpath == "" then
    return
  end

  -- Respect config.filetypes
  local ft = vim.bo.filetype
  local enabled = vim.tbl_contains(M.config.filetypes, "*")
    or vim.tbl_contains(M.config.filetypes, ft)
  if not enabled then
    return
  end

  local dir = vim.fn.fnamemodify(fullpath, ":h")      -- directory part
  local filename = vim.fn.fnamemodify(fullpath, ":t") -- just the filename

  M.RunGitWip(dir, filename)
end

return M


================================================
FILE: src/CMakeLists.txt
================================================
# Generate version header
include(${CMAKE_SOURCE_DIR}/cmake/GitVersion.cmake)
gitversion_generate(PREFIX GIT_WIP_ OUTPUT ${CMAKE_BINARY_DIR}/git_wip_version.h)

add_executable(git-wip
    color.cpp
    main.cpp
    cmd_delete.cpp
    cmd_list.cpp
    cmd_log.cpp
    cmd_save.cpp
    cmd_status.cpp
)

# Ensure the executable is rebuilt when the version header changes
add_dependencies(git-wip gitversion)

install(TARGETS git-wip
    RUNTIME DESTINATION bin
)

target_include_directories(git-wip PRIVATE
    ${CMAKE_BINARY_DIR}
    ${LIBGIT2_INCLUDE_DIRS}
)

if(WIP_STATIC)
    # Link libgit2 and all its transitive deps as static archives.
    # Strategy:
    #   1. -Wl,-Bstatic wraps the static block.
    #   2. --start-group / --end-group around all static libs resolves any
    #      circular or out-of-order symbol dependencies (e.g. libcrypto → libz).
    #   3. -Wl,-Bdynamic switches back to dynamic for the remainder
    #      (glibc, gssapi_krb5 and its kerberos chain have no static .a here).
    #   4. GSSAPI / krb5 are added explicitly as dynamic-only after the switch.
    #
    # spdlog is always a static archive (built by FetchContent) and does NOT
    # need to be inside -Bstatic because CMake links it by full path anyway.
    target_link_directories(git-wip PRIVATE
        ${LIBGIT2_STATIC_DIRS}
    )
    set(_static_lib_args
        "-Wl,-Bstatic"
        "-Wl,--start-group"
    )
    foreach(_lib IN LISTS LIBGIT2_STATIC_LIBS)
        list(APPEND _static_lib_args "-l${_lib}")
    endforeach()
    list(APPEND _static_lib_args
        "-Wl,--end-group"
        "-Wl,-Bdynamic"
        # GSSAPI / Kerberos — only available as shared libs on most distros
        "-lgssapi_krb5"
        "-lkrb5"
        "-lk5crypto"
        "-lkrb5support"
        "-lcom_err"
    )
    target_link_libraries(git-wip PRIVATE
        spdlog::spdlog
        ${_static_lib_args}
    )
else()
    target_link_libraries(git-wip PRIVATE
        spdlog::spdlog
        ${LIBGIT2_LIBRARIES}
    )
    target_link_directories(git-wip PRIVATE
        ${LIBGIT2_LIBRARY_DIRS}
    )
endif()

if(WIP_HAVE_STD_PRINT)
    target_compile_definitions(git-wip PRIVATE WIP_HAVE_STD_PRINT)
else()
    target_link_libraries(git-wip PRIVATE fmt::fmt)
endif()


================================================
FILE: src/cmd_delete.cpp
================================================
#include "cmd_delete.hpp"
#include "color.hpp"
#include "git_guards.hpp"
#include "git_helpers.hpp"
#include "string_helpers.hpp"

#include "spdlog/spdlog.h"

#include "print_compat.hpp"
#include <iostream>
#include <optional>
#include <string>

namespace {

int delete_ref(git_repository *repo, const std::string &wip_ref) {
    if (!resolve_oid(repo, wip_ref)) {
        spdlog::error("'{}' has no WIP commits", strip_prefix(wip_ref, "refs/wip/"));
        return 1;
    }

    if (git_reference_remove(repo, wip_ref.c_str()) < 0) {
        spdlog::error("cannot delete ref '{}': {}", wip_ref, git_error_str());
        return 1;
    }

    std::println("deleted {}", color_wip_branch(strip_prefix(wip_ref, "refs/")));
    return 0;
}

} // namespace

int DeleteCmd::run(int argc, char *argv[]) {
    bool cleanup_mode = false;
    bool yes_mode = false;
    std::optional<std::string> ref_arg;

    for (int i = 1; i < argc; ++i) {
        std::string a(argv[i]);
        if (a == "--cleanup") {
            cleanup_mode = true;
        } else if (a == "--yes") {
            yes_mode = true;
        } else if (a == "--help" || a == "-h") {
            std::println("Usage: git-wip delete [--yes] [<ref>]");
            std::println("       git-wip delete --cleanup\n");
            //                -                     #
            std::println("    --yes                 # skip confirmation when deleting current branch wip ref");
            std::println("    --cleanup             # delete orphaned refs/wip/* entries\n");
            return 0;
        } else if (!a.empty() && a[0] == '-') {
            spdlog::error("git-wip delete: unknown option '{}'", a);
            return 1;
        } else if (ref_arg.has_value()) {
            spdlog::error("git-wip delete: only one ref argument is allowed");
            return 1;
        } else {
            ref_arg = a;
        }
    }

    if (cleanup_mode && ref_arg.has_value()) {
        spdlog::error("git-wip delete: --cleanup cannot be used with <ref>");
        return 1;
    }

    GitLibGuard git_lib_guard;
    GitRepoGuard repo_guard;
    if (git_repository_open_ext(repo_guard.ptr(), ".", 0, nullptr) < 0) {
        spdlog::error("not a git repository: {}", git_error_str());
        return 1;
    }
    git_repository *repo = repo_guard.get();

    if (cleanup_mode) {
        auto wip_refs = find_refs(repo, "refs/wip/");
        std::size_t removed = 0;

        for (const auto &wip_ref : wip_refs) {
            auto bn = resolve_branch_names(repo, wip_ref);
            if (!bn) continue;

            if (resolve_oid(repo, bn->work_ref))
                continue; // matching branch exists

            if (git_reference_remove(repo, wip_ref.c_str()) < 0) {
                spdlog::error("cannot delete ref '{}': {}", wip_ref, git_error_str());
                return 1;
            }
            ++removed;
            std::println("deleted {}", color_wip_branch(strip_prefix(wip_ref, "refs/")));
        }

        std::println("deleted {} orphaned wip ref{}",
                     removed,
                     removed == 1 ? "" : "s");
        return 0;
    }

    std::optional<BranchNames> bn;
    if (ref_arg.has_value())
        bn = resolve_branch_names(repo, ref_arg);
    else
        bn = resolve_branch_names(repo);

    if (!bn) {
        spdlog::error("not on a local branch");
        return 1;
    }

    if (!ref_arg.has_value() && !yes_mode) {
        std::print("About to delete {} [Y/n] ", color_wip_branch(strip_prefix(bn->wip_ref, "refs/")));
        std::cout.flush();

        std::string input;
        if (!std::getline(std::cin, input))
            return 1;

        if (!(input.empty() || input == "y" || input == "Y"))
            return 1;
    }

    return delete_ref(repo, bn->wip_ref);
}


================================================
FILE: src/cmd_delete.hpp
================================================
#pragma once

#include "command.hpp"
#include <string>

class DeleteCmd : public Command {
public:
    std::string name() const override {
        return "delete";
    }

    std::string desc() const override {
        return "delete wip refs";
    }

    int run(int argc, char *argv[]) override;
};


================================================
FILE: src/cmd_list.cpp
================================================
#include "cmd_list.hpp"

#include "color.hpp"
#include "git_guards.hpp"
#include "git_helpers.hpp"
#include "wip_helpers.hpp"

#include "spdlog/spdlog.h"

#include "print_compat.hpp"

#include <string>

int ListCmd::run(int argc, char *argv[]) {
    bool verbose = false;

    for (int i = 1; i < argc; ++i) {
        std::string a(argv[i]);
        if (a == "-v" || a == "--verbose") {
            verbose = true;
        } else if (a == "--help" || a == "-h") {
            std::println("Usage: git-wip list [-v|--verbose]\n");
            //                -                     #
            std::println("    -v, --verbose         # show ahead counts and orphaned refs\n");
            return 0;
        } else {
            spdlog::error("git-wip list: unknown option '{}'", a);
            return 1;
        }
    }

    GitLibGuard git_lib_guard;

    GitRepoGuard repo_guard;
    if (git_repository_open_ext(repo_guard.ptr(), ".", 0, nullptr) < 0) {
        spdlog::error("not a git repository: {}", git_error_str());
        return 1;
    }
    git_repository *repo = repo_guard.get();

    auto wip_refs = find_refs(repo, "refs/wip/");
    spdlog::debug("list: found {} refs under refs/wip/", wip_refs.size());

    for (const auto &wip_ref : wip_refs) {
        const std::string wip_name = strip_prefix(wip_ref, "refs/");
        const std::string wip_name_colored = color_wip_branch(wip_name);

        if (!verbose) {
            std::println("{}", wip_name_colored);
            continue;
        }

        const std::string branch_name = strip_prefix(wip_ref, "refs/wip/");
        auto bn = resolve_branch_names(repo, branch_name);
        if (!bn) {
            std::println("{} is orphaned", wip_name_colored);
            continue;
        }

        auto work_last = resolve_oid(repo, bn->work_ref);
        auto wip_last = resolve_oid(repo, bn->wip_ref);

        // The wip ref came from find_refs(), so wip_last should always resolve.
        // If either lookup fails (or histories are unrelated), report orphaned.
        if (!work_last || !wip_last) {
            std::println("{} is orphaned", wip_name_colored);
            continue;
        }

        auto wip_commits = collect_wip_commits(repo, *wip_last, *work_last);
        if (!wip_commits) {
            std::println("{} is orphaned", wip_name_colored);
            continue;
        }

        std::println("{} has {} commit{} ahead of {}",
                     wip_name_colored,
                     wip_commits->size(),
                     wip_commits->size() == 1 ? "" : "s",
                     color_branch(bn->work_branch));
    }

    return 0;
}


================================================
FILE: src/cmd_list.hpp
================================================
#pragma once

#include "command.hpp"

#include <string>

class ListCmd : public Command {
public:
    std::string name() const override {
        return "list";
    }

    std::string desc() const override {
        return "list wip branches";
    }

    int run(int argc, char *argv[]) override;
};


================================================
FILE: src/cmd_log.cpp
================================================
#include "cmd_log.hpp"
#include "git_guards.hpp"
#include "git_helpers.hpp"

#include "spdlog/spdlog.h"

#include <cstdlib>
#include "print_compat.hpp"
#include <string>
#include <vector>

int LogCmd::run(int argc, char *argv[]) {
    // -----------------------------------------------------------------------
    // 1. Parse arguments
    // -----------------------------------------------------------------------
    bool pretty     = false;
    bool stat       = false;
    bool reflog_mode = false;
    std::vector<std::string> files;

    std::vector<std::string> args;
    for (int i = 1; i < argc; ++i)
        args.emplace_back(argv[i]);

    bool past_dashdash = false;
    for (const auto &a : args) {
        if (past_dashdash) {
            files.push_back(a);
        } else if (a == "--") {
            past_dashdash = true;
        } else if (a == "--pretty" || a == "-p") {
            pretty = true;
        } else if (a == "--stat" || a == "-s") {
            stat = true;
        } else if (a == "--reflog" || a == "-r") {
            reflog_mode = true;
        } else if (a == "--help" || a == "-h") {
            std::println("Usage: git-wip log [--pretty|-p] [--stat|-s] [--reflog|-r] [-- <file>...]\n");
            //                -                     #
            std::println("    -p, --pretty          # use pretty oneline log (default full log)");
            std::println("    -s, --stat            # show file changes in log");
            std::println("    -r, --reflog          # invoke reflog (shows historical entries)");
            std::println("    <file>...             # filter on changes to specific file(s)\n");
            return 0;
        } else if (!a.empty() && a[0] == '-') {
            spdlog::error("git-wip log: unknown option '{}'", a);
            return 1;
        } else {
            files.push_back(a);
        }
    }

    spdlog::debug("log: pretty={} stat={} reflog={} files={}", pretty, stat, reflog_mode, files.size());

    // -----------------------------------------------------------------------
    // 2. Open repository
    // -----------------------------------------------------------------------
    GitLibGuard git_lib_guard;

    GitRepoGuard repo_guard;
    if (git_repository_open_ext(repo_guard.ptr(), ".", 0, nullptr) < 0) {
        spdlog::error("not a git repository: {}", git_error_str());
        return 1;
    }
    git_repository *repo = repo_guard.get();

    // -----------------------------------------------------------------------
    // 3. Resolve branch names
    // -----------------------------------------------------------------------
    auto bn = resolve_branch_names(repo);
    if (!bn) {
        spdlog::error("not on a local branch");
        return 1;
    }

    spdlog::debug("log: work_branch='{}' wip_ref='{}'", bn->work_branch, bn->wip_ref);

    // -----------------------------------------------------------------------
    // 4. Resolve work_last and wip_last OIDs
    // -----------------------------------------------------------------------
    auto work_last = resolve_oid(repo, bn->work_ref);
    if (!work_last) {
        spdlog::error("'{}' branch has no commits.", bn->work_branch);
        return 1;
    }

    auto wip_last = resolve_oid(repo, bn->wip_ref);
    if (!wip_last) {
        spdlog::error("'{}' has no WIP commits.", bn->work_branch);
        return 1;
    }

    spdlog::debug("log: work_last={} wip_last={}",
                  oid_to_hex(&*work_last), oid_to_hex(&*wip_last));

    // -----------------------------------------------------------------------
    // 5. Build and exec git log / git reflog
    // -----------------------------------------------------------------------
    const std::string pretty_fmt =
        " --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset'"
        " --abbrev-commit --date=relative";

    if (reflog_mode) {
        std::string cmd = "git reflog";
        if (stat)   cmd += " --stat";
        if (pretty) cmd += pretty_fmt;
        cmd += " " + bn->wip_ref;
        spdlog::debug("log: running: {}", cmd);
        return std::system(cmd.c_str());
    }

    // Compute merge-base to determine the stop point.
    git_oid base_oid{};
    if (git_merge_base(&base_oid, repo, &*wip_last, &*work_last) < 0) {
        spdlog::error("cannot find merge base: {}", git_error_str());
        return 1;
    }

    spdlog::debug("log: base={}", oid_to_hex(&base_oid));

    // stop = base~1 if base has parents, else base itself
    std::string stop_arg;
    {
        GitCommitGuard base_commit;
        std::string base_hex = oid_to_hex(&base_oid);
        if (git_commit_lookup(base_commit.ptr(), repo, &base_oid) == 0 &&
            git_commit_parentcount(base_commit.get()) > 0)
            stop_arg = base_hex + "~1";
        else
            stop_arg = base_hex;
    }

    spdlog::debug("log: stop={}", stop_arg);

    std::string cmd = "git log";
    if (pretty) cmd += " --graph" + pretty_fmt;
    if (stat)   cmd += " --stat";
    for (const auto &f : files) cmd += " -- " + f;
    cmd += " " + oid_to_hex(&*wip_last);
    cmd += " " + oid_to_hex(&*work_last);
    cmd += " ^" + stop_arg;

    spdlog::debug("log: running: {}", cmd);
    return std::system(cmd.c_str());
}


================================================
FILE: src/cmd_log.hpp
================================================
#pragma once

#include "command.hpp"
#include <string>

class LogCmd : public Command {
public:
    std::string name() const override {
        return "log";
    }

    std::string desc() const override {
        return "look at WIP history";
    }

    int run(int argc, char *argv[]) override;
};


================================================
FILE: src/cmd_save.cpp
================================================
#include "cmd_save.hpp"
#include "git_guards.hpp"
#include "git_helpers.hpp"
#include "string_helpers.hpp"
#include "wip_helpers.hpp"

#include "spdlog/spdlog.h"

#include <cstdlib>
#include <filesystem>
#include <format>
#include "print_compat.hpp"
#include <string>
#include <vector>

int SaveCmd::run(int argc, char *argv[]) {
    // -----------------------------------------------------------------------
    // 1. Parse arguments
    // -----------------------------------------------------------------------
    bool editor_mode   = false;
    bool add_untracked = false;
    bool add_ignored   = false;
    bool gpg_sign      = false; // TODO: not implemented yet
    std::string message = "WIP";
    std::vector<std::string> files;

    std::vector<std::string> args;
    for (int i = 1; i < argc; ++i)
        args.emplace_back(argv[i]);

    bool got_message   = false;
    bool past_dashdash = false;
    for (const auto &a : args) {
        if (past_dashdash) {
            files.push_back(a);
        } else if (a == "--") {
            past_dashdash = true;
        } else if (a == "--editor" || a == "-e") {
            editor_mode = true;
        } else if (a == "--untracked" || a == "-u") {
            add_untracked = true;
        } else if (a == "--no-untracked" || a == "-U") {
            add_untracked = false;
        } else if (a == "--ignored" || a == "-i") {
            add_ignored = true;
        } else if (a == "--no-ignored" || a == "-I") {
            add_ignored = false;
        } else if (a == "--gpg-sign") {
            gpg_sign = true;
        } else if (a == "--no-gpg-sign") {
            gpg_sign = false;
        } else if (a == "--help" || a == "-h") {
            std::println("Usage: git-wip save [<message>] [--editor|-e] [--[no-]untracked|-u|-U] [--[no-]ignored|-i|-I] [--[no-]gpg-sign] [-- <file>...]\n");
            //                -                     #
            std::println("    <message>             # use this message (defaults to \"WIP\")");
            std::println("    -e, --editor          # queit when there are no changes (called from editor)");
            std::println("    -u, --untracked       # enable capture of changes to untracked files");
            std::println("    -U, --no-untracked    # disable capture of changes to untracked files");
            std::println("    -i, --ignored         # enable capture of changes to ignored files");
            std::println("    -I, --no-ignored      # disable capture of changes to ignored files");
            std::println("    --gpg-sign            # enable signing of commits");
            std::println("    --no-gpg-sign         # disable signing of commits");
            std::println("    <file>...             # filter on changes to specific file(s)\n");
            return 0;
        } else if (!a.empty() && a[0] == '-') {
            spdlog::error("git-wip save: unknown option '{}'", a);
            return 1;
        } else if (!got_message) {
            message     = a;
            got_message = true;
        } else {
            files.push_back(a);
        }
    }

    spdlog::debug("save: message='{}' editor={} untracked={} ignored={} gpg_sign={} files={}",
                  message, editor_mode, add_untracked, add_ignored, gpg_sign, files.size());

    if (gpg_sign)
        spdlog::warn("git-wip sign --gpg-sign is not implemented yet");

    // -----------------------------------------------------------------------
    // 2. Open repository
    // -----------------------------------------------------------------------
    GitLibGuard git_lib_guard;

    GitRepoGuard repo_guard;
    if (git_repository_open_ext(repo_guard.ptr(), ".", 0, nullptr) < 0) {
        spdlog::error("not a git repository: {}", git_error_str());
        return 1;
    }
    git_repository *repo = repo_guard.get();

    // -----------------------------------------------------------------------
    // 2b. Normalise file paths to be relative to the workdir root.
    //
    //     git_index_add_all() resolves pathspecs against the workdir root,
    //     but the paths supplied on the command line are relative to the
    //     current working directory (which may be a subdirectory).  Convert
    //     each path: cwd/file → absolute → workdir-relative.
    // -----------------------------------------------------------------------
    if (!files.empty()) {
        const char *workdir_cstr = git_repository_workdir(repo);
        if (workdir_cstr) {
            std::filesystem::path workdir = std::filesystem::canonical(workdir_cstr);
            std::filesystem::path cwd     = std::filesystem::current_path();
            for (auto &f : files) {
                std::filesystem::path abs = std::filesystem::weakly_canonical(cwd / f);
                // Make relative to workdir; weakly_canonical handles non-existent files too
                std::error_code ec;
                auto rel = std::filesystem::relative(abs, workdir, ec);
                if (!ec && !rel.empty())
                    f = rel.string();
                spdlog::debug("save: file '{}' → repo-relative '{}'",
                              (cwd / f).string(), f);
            }
        }
    }

    if (git_repository_is_bare(repo)) {
        spdlog::error("cannot use in a bare repository");
        return 1;
    }

    // -----------------------------------------------------------------------
    // 3. Resolve branch names
    // -----------------------------------------------------------------------
    auto bn = resolve_branch_names(repo);
    if (!bn) {
        if (editor_mode) { return 0; }
        spdlog::error("git-wip requires a local branch");
        return 1;
    }

    spdlog::debug("save: work_branch='{}' wip_ref='{}'", bn->work_branch, bn->wip_ref);

    // -----------------------------------------------------------------------
    // 4. Ensure reflog directory exists for the wip branch
    // -----------------------------------------------------------------------
    ensure_reflog_dir(repo, bn->wip_ref);

    // -----------------------------------------------------------------------
    // 5. Resolve work_last
    // -----------------------------------------------------------------------
    auto work_last = resolve_oid(repo, bn->work_ref);
    if (!work_last) {
        if (editor_mode) { return 0; }
        spdlog::error("'{}' branch has no commits.", bn->work_branch);
        return 1;
    }

    spdlog::debug("save: work_last={}", oid_to_hex(&*work_last));

    // -----------------------------------------------------------------------
    // 6. Determine wip_parent
    // -----------------------------------------------------------------------
    auto wip_last = resolve_oid(repo, bn->wip_ref); // nullopt if no wip branch yet

    if (wip_last)
        spdlog::debug("save: wip_last={}", oid_to_hex(&*wip_last));

    auto wip_parent = wip_parent_oid(repo, *work_last, wip_last);
    if (!wip_parent) {
        spdlog::error("'{}' and '{}' are unrelated.",
                      bn->work_branch, bn->wip_ref);
        return 1;
    }

    spdlog::debug("save: wip_parent={}", oid_to_hex(&*wip_parent));

    // -----------------------------------------------------------------------
    // 7. Build new tree (in-memory; never writes to the on-disk index)
    //
    //    Steps:
    //      a) Load the parent commit's tree into the repo's in-memory index
    //      b) Stage changes from the working directory on top
    //      c) Write the tree object to the ODB
    //      d) Restore the real on-disk index
    // -----------------------------------------------------------------------
    git_oid new_tree_oid{};
    {
        GitCommitGuard parent_commit;
        if (git_commit_lookup(parent_commit.ptr(), repo, &*wip_parent) < 0) {
            spdlog::error("cannot look up parent commit: {}", git_error_str());
            return 1;
        }

        GitTreeGuard parent_tree;
        if (git_commit_tree(parent_tree.ptr(), parent_commit.get()) < 0) {
            spdlog::error("cannot get parent tree: {}", git_error_str());
            return 1;
        }

        GitIndexGuard idx_guard;
        if (git_repository_index(idx_guard.ptr(), repo) < 0) {
            spdlog::error("cannot get repo index: {}", git_error_str());
            return 1;
        }
        git_index *idx = idx_guard.get();

        if (git_index_read_tree(idx, parent_tree.get()) < 0) {
            spdlog::error("cannot read parent tree into index: {}", git_error_str());
            return 1;
        }

        // Stage changes
        int stage_rc = 0;
        if (!files.empty()) {
            std::vector<const char *> c_files;
            c_files.reserve(files.size());
            for (const auto &f : files) c_files.push_back(f.c_str());
            git_strarray ps{const_cast<char **>(c_files.data()), c_files.size()};
            stage_rc = git_index_add_all(idx, &ps, GIT_INDEX_ADD_FORCE, nullptr, nullptr);
        } else if (add_ignored) {
            git_strarray dot{nullptr, 0};
            stage_rc = git_index_add_all(idx, &dot, GIT_INDEX_ADD_FORCE, nullptr, nullptr);
        } else if (add_untracked) {
            git_strarray dot{nullptr, 0};
            stage_rc = git_index_add_all(idx, &dot, GIT_INDEX_ADD_DEFAULT, nullptr, nullptr);
        } else {
            git_strarray dot{nullptr, 0};
            stage_rc = git_index_update_all(idx, &dot, nullptr, nullptr);
        }

        if (stage_rc < 0) {
            spdlog::error("cannot stage changes: {}", git_error_str());
            git_index_read(idx, 1);
            return 1;
        }

        if (git_index_write_tree(&new_tree_oid, idx) < 0) {
            spdlog::error("cannot write tree: {}", git_error_str());
            git_index_read(idx, 1);
            return 1;
        }

        spdlog::debug("save: new_tree={}", oid_to_hex(&new_tree_oid));

        git_index_read(idx, /*force=*/1); // restore on-disk index
    }

    // -----------------------------------------------------------------------
    // 8. Check for changes
    // -----------------------------------------------------------------------
    {
        GitCommitGuard parent_commit;
        git_commit_lookup(parent_commit.ptr(), repo, &*wip_parent);
        git_oid parent_tree_oid = *git_commit_tree_id(parent_commit.get());

        if (git_oid_equal(&new_tree_oid, &parent_tree_oid)) {
            spdlog::debug("save: no changes");
            if (editor_mode) { return 0; }
            std::println("no changes");
            return 1;
        }
    }

    spdlog::debug("save: has changes, creating commit");

    // -----------------------------------------------------------------------
    // 9. Create the WIP commit
    // -----------------------------------------------------------------------
    GitTreeGuard new_tree_obj;
    if (git_tree_lookup(new_tree_obj.ptr(), repo, &new_tree_oid) < 0) {
        spdlog::error("cannot look up new tree: {}", git_error_str());
        return 1;
    }

    GitCommitGuard parent_commit_obj;
    if (git_commit_lookup(parent_commit_obj.ptr(), repo, &*wip_parent) < 0) {
        spdlog::error("cannot look up parent commit: {}", git_error_str());
        return 1;
    }

    GitSignatureGuard sig;
    if (git_signature_default(sig.ptr(), repo) < 0)
        git_signature_now(sig.ptr(), "git-wip", "git-wip@localhost");

    git_oid new_commit_oid{};
    {
        const git_commit *parents[] = {parent_commit_obj.get()};
        if (git_commit_create(&new_commit_oid, repo, nullptr,
                              sig.get(), sig.get(), nullptr,
                              message.c_str(), new_tree_obj.get(),
                              1, parents) < 0) {
            spdlog::error("cannot create commit: {}", git_error_str());
            return 1;
        }
    }

    spdlog::debug("save: new_wip={}", oid_to_hex(&new_commit_oid));

    // -----------------------------------------------------------------------
    // 10. Update the wip ref
    // -----------------------------------------------------------------------
    {
        std::string reflog_msg = "git-wip: " + first_line(message.c_str());
        const git_oid *current_id = wip_last ? &*wip_last : nullptr;

        GitReferenceGuard new_ref;
        if (git_reference_create_matching(new_ref.ptr(), repo,
                                          bn->wip_ref.c_str(),
                                          &new_commit_oid, /*force=*/1,
                                          current_id,
                                          reflog_msg.c_str()) < 0) {
            spdlog::error("cannot update ref '{}': {}",
                          bn->wip_ref, git_error_str());
            return 1;
        }
    }

    spdlog::debug("save: SUCCESS");
    return 0;
}


================================================
FILE: src/cmd_save.hpp
================================================
#pragma once

#include "command.hpp"
#include <string>

class SaveCmd : public Command {
public:
    std::string name() const override {
        return "save";
    }

    std::string desc() const override {
        return "save current work";
    }

    int run(int argc, char *argv[]) override;
};


================================================
FILE: src/cmd_status.cpp
================================================
#include "cmd_status.hpp"
#include "color.hpp"
#include "git_guards.hpp"
#include "git_helpers.hpp"
#include "string_helpers.hpp"
#include "wip_helpers.hpp"

#include "spdlog/spdlog.h"

#include <cstdlib>
#include <iostream>
#include "print_compat.hpp"
#include <string>
#include <vector>

int StatusCmd::run(int argc, char *argv[]) {
    // -----------------------------------------------------------------------
    // 1. Parse arguments
    // -----------------------------------------------------------------------
    bool list_mode  = false;
    bool files_mode = false;
    std::optional<std::string> ref_arg;

    for (int i = 1; i < argc; ++i) {
        std::string a(argv[i]);
        if (a == "-l" || a == "--list") {
            list_mode = true;
        } else if (a == "-f" || a == "--files") {
            files_mode = true;
        } else if (a == "--help" || a == "-h") {
            std::println("Usage: git-wip status [-l|--list] [-f|--files] [<ref>]\n");
            //                -                     #
            std::println("    -l, --list            # show each wip commit (short sha, subject, age)");
            std::println("    -f, --files           # show diff --stat of wip changes\n");
            return 0;
        } else if (!a.empty() && a[0] == '-') {
            spdlog::error("git-wip status: unknown option '{}'", a);
            return 1;
        } else if (ref_arg.has_value()) {
            spdlog::error("git-wip status: only one ref argument is allowed");
            return 1;
        } else {
            ref_arg = a;
        }
    }

    spdlog::debug("status: list={} files={}", list_mode, files_mode);

    // -----------------------------------------------------------------------
    // 2. Open repository
    // -----------------------------------------------------------------------
    GitLibGuard git_lib_guard;

    GitRepoGuard repo_guard;
    if (git_repository_open_ext(repo_guard.ptr(), ".", 0, nullptr) < 0) {
        spdlog::error("not a git repository: {}", git_error_str());
        return 1;
    }
    git_repository *repo = repo_guard.get();

    // -----------------------------------------------------------------------
    // 3. Resolve branch names
    // -----------------------------------------------------------------------
    auto bn = resolve_branch_names(repo, ref_arg);
    if (!bn) {
        spdlog::error("not on a local branch");
        return 1;
    }

    spdlog::debug("status: work_branch='{}' wip_ref='{}'", bn->work_branch, bn->wip_ref);

    // -----------------------------------------------------------------------
    // 4. Resolve work_last and wip_last OIDs
    // -----------------------------------------------------------------------
    auto work_last = resolve_oid(repo, bn->work_ref);
    if (!work_last) {
        spdlog::error("branch '{}' has no commits", bn->work_branch);
        return 1;
    }

    auto wip_last = resolve_oid(repo, bn->wip_ref);
    if (!wip_last) {
        std::println("branch {} has no wip commits", color_branch(bn->work_branch));
        return 0;
    }

    spdlog::debug("status: work_last={} wip_last={}",
                  oid_to_hex(&*work_last), oid_to_hex(&*wip_last));

    // -----------------------------------------------------------------------
    // 5. Collect the WIP-only commits (newest first)
    // -----------------------------------------------------------------------
    auto wip_commits = collect_wip_commits(repo, *wip_last, *work_last);
    if (!wip_commits) {
        spdlog::error("cannot enumerate wip commits: {}", git_error_str());
        return 1;
    }

    spdlog::debug("status: {} wip commit(s)", wip_commits->size());

    // -----------------------------------------------------------------------
    // 6. Summary line
    // -----------------------------------------------------------------------
    std::println("branch {} has {} wip commit{} on {}",
                 color_branch(bn->work_branch),
                 wip_commits->size(),
                 wip_commits->size() == 1 ? "" : "s",
                 color_wip_branch(bn->wip_ref));
    std::cout.flush();

    // -----------------------------------------------------------------------
    // 7. Optional detail modes
    // -----------------------------------------------------------------------
    if (list_mode) {
        for (const auto &oid : *wip_commits) {
            GitCommitGuard commit;
            if (git_commit_lookup(commit.ptr(), repo, &oid) < 0) continue;

            const git_signature *author = git_commit_author(commit.get());
            std::println("{} - {} ({})",
                         color_commit_hash(oid_to_short_hex(&oid)),
                         first_line(git_commit_message(commit.get())),
                         relative_time(author->when.time));
            std::cout.flush();

            if (files_mode) {
                // per-commit diff --stat against its parent
                std::string hex = oid_to_hex(&oid);
                std::system(("git diff --stat " + hex + "^ " + hex).c_str());
            }
        }
    } else if (files_mode) {
        // diff --stat from work branch HEAD to latest wip tip
        std::system(("git diff --stat " +
                     oid_to_hex(&*work_last) + " " +
                     oid_to_hex(&*wip_last)).c_str());
    }

    return 0;
}


================================================
FILE: src/cmd_status.hpp
================================================
#pragma once

#include "command.hpp"
#include <string>

class StatusCmd : public Command {
public:
    std::string name() const override {
        return "status";
    }

    std::string desc() const override {
        return "inspect changes";
    }

    int run(int argc, char *argv[]) override;
};


================================================
FILE: src/color.cpp
================================================
#include "color.hpp"

#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <format>

#include <unistd.h>

bool g_wip_color = true;

namespace {

std::string lower_copy(std::string s) {
    std::ranges::transform(s, s.begin(), [](unsigned char c) {
        return static_cast<char>(std::tolower(c));
    });
    return s;
}

bool stdout_is_tty() {
    return isatty(fileno(stdout)) == 1;
}

} // namespace

void color_init() {
    // safe fallback
    g_wip_color = false;

    const char *env = std::getenv("WIP_COLOR");
    if (env == nullptr) {
        g_wip_color = stdout_is_tty();
        return;
    }

    std::string mode = lower_copy(env);
    if (mode == "1" || mode == "on" || mode == "always") {
        g_wip_color = true;
        return;
    }

    if (mode == "0" || mode == "off" || mode == "never") {
        g_wip_color = false;
        return;
    }

    if (mode.empty() || mode == "auto") {
        g_wip_color = stdout_is_tty();
        return;
    }
}

std::string Color::red() {
    return g_wip_color ? "\x1b[31m" : "";
}

std::string Color::green() {
    return g_wip_color ? "\x1b[32m" : "";
}

std::string Color::yellow() {
    return g_wip_color ? "\x1b[33m" : "";
}

std::string Color::reset() {
    return g_wip_color ? "\x1b[0m" : "";
}

std::string Color::rgb(int r, int g, int b) {
    r = std::clamp(r, 0, 255);
    g = std::clamp(g, 0, 255);
    b = std::clamp(b, 0, 255);
    return g_wip_color ? std::format("\x1b[38;2;{};{};{}m", r, g, b) : "";
}

std::string color_branch(std::string_view branch_name) {
    return Color::green() + std::string(branch_name) + Color::reset();
}

std::string color_wip_branch(std::string_view wip_branch_name) {
    return Color::red() + std::string(wip_branch_name) + Color::reset();
}

std::string color_commit_hash(std::string_view commit_hash) {
    return Color::yellow() + std::string(commit_hash) + Color::reset();
}


================================================
FILE: src/color.hpp
================================================
#pragma once

#include <string>
#include <string_view>

extern bool g_wip_color;

void color_init();

class Color {
public:
    static std::string red();
    static std::string green();
    static std::string yellow();
    static std::string reset();
    static std::string rgb(int r, int g, int b);
};

std::string color_branch(std::string_view branch_name);
std::string color_wip_branch(std::string_view wip_branch_name);
std::string color_commit_hash(std::string_view commit_hash);


================================================
FILE: src/command.hpp
================================================
#pragma once

#include <string>
#include <vector>

class Command {
public:
    // Pure virtual methods to be implemented by derived classes
    virtual std::string name() const = 0;
    virtual std::string desc() const = 0;
    virtual int run(int argc, char *argv[]) = 0;

    // Virtual destructor to ensure proper cleanup of derived classes
    virtual ~Command() = default;
};


================================================
FILE: src/git_guards.hpp
================================================
#pragma once

#include <git2.h>
#include <string>

// ---------------------------------------------------------------------------
// RAII wrappers for libgit2 objects
//
// Each guard owns the pointed-to object and frees it on destruction.
// Use ptr() to obtain the address to pass to libgit2 "out" parameters,
// and get() to retrieve the wrapped pointer for subsequent API calls.
// ---------------------------------------------------------------------------

struct GitLibGuard {
    GitLibGuard() { git_libgit2_init(); }
    ~GitLibGuard() { git_libgit2_shutdown(); }

    GitLibGuard(const GitLibGuard &) = delete;
    GitLibGuard &operator=(const GitLibGuard &) = delete;
};

struct GitRepoGuard {
    git_repository *m_repo = nullptr;
    ~GitRepoGuard() { if (m_repo) git_repository_free(m_repo); }
    git_repository       *get()       { return m_repo; }
    git_repository const *get() const { return m_repo; }
    git_repository      **ptr()       { return &m_repo; }
};

struct GitIndexGuard {
    git_index *m_idx = nullptr;
    ~GitIndexGuard() { if (m_idx) git_index_free(m_idx); }
    git_index       *get()       { return m_idx; }
    git_index const *get() const { return m_idx; }
    git_index      **ptr()       { return &m_idx; }
};

struct GitTreeGuard {
    git_tree *m_tree = nullptr;
    ~GitTreeGuard() { if (m_tree) git_tree_free(m_tree); }
    git_tree       *get()       { return m_tree; }
    git_tree const *get() const { return m_tree; }
    git_tree      **ptr()       { return &m_tree; }
};

struct GitCommitGuard {
    git_commit *m_commit = nullptr;
    ~GitCommitGuard() { if (m_commit) git_commit_free(m_commit); }
    git_commit       *get()       { return m_commit; }
    git_commit const *get() const { return m_commit; }
    git_commit      **ptr()       { return &m_commit; }
};

struct GitReferenceGuard {
    git_reference *m_ref = nullptr;
    ~GitReferenceGuard() { if (m_ref) git_reference_free(m_ref); }
    git_reference       *get()       { return m_ref; }
    git_reference const *get() const { return m_ref; }
    git_reference      **ptr()       { return &m_ref; }
};

struct GitSignatureGuard {
    git_signature *m_sig = nullptr;
    ~GitSignatureGuard() { if (m_sig) git_signature_free(m_sig); }
    git_signature       *get()       { return m_sig; }
    git_signature const *get() const { return m_sig; }
    git_signature      **ptr()       { return &m_sig; }
};

struct GitRevwalkGuard {
    git_revwalk *m_walk = nullptr;
    ~GitRevwalkGuard() { if (m_walk) git_revwalk_free(m_walk); }
    git_revwalk       *get()       { return m_walk; }
    git_revwalk const *get() const { return m_walk; }
    git_revwalk      **ptr()       { return &m_walk; }
};

// ---------------------------------------------------------------------------
// Convenience helper: return the last libgit2 error message as a std::string.
// ---------------------------------------------------------------------------
inline std::string git_error_str() {
    const git_error *e = git_error_last();
    return e ? e->message : "(unknown error)";
}


================================================
FILE: src/git_helpers.hpp
================================================
#pragma once

// git_helpers.hpp — thin wrappers around libgit2 operations that are repeated
// across multiple commands.  All functions return by value and report errors
// via std::expected-style: an empty optional/string signals failure, and the
// caller can retrieve the last libgit2 error with git_error_str().

#include "git_guards.hpp"
#include "string_helpers.hpp"

#include <filesystem>
#include <fstream>
#include <algorithm>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

// ---------------------------------------------------------------------------
// oid_to_hex  — convert a git_oid to its full lowercase hex string
// ---------------------------------------------------------------------------
inline std::string oid_to_hex(const git_oid *oid) {
    char buf[GIT_OID_MAX_HEXSIZE + 1];
    git_oid_tostr(buf, sizeof(buf), oid);
    return buf;
}

// ---------------------------------------------------------------------------
// oid_to_short_hex  — first 7 characters of the hex OID
// ---------------------------------------------------------------------------
inline std::string oid_to_short_hex(const git_oid *oid) {
    char buf[GIT_OID_MAX_HEXSIZE + 1];
    git_oid_tostr(buf, sizeof(buf), oid);
    buf[7] = '\0';
    return buf;
}

// ---------------------------------------------------------------------------
// BranchNames
//
// Holds the three names every command needs after resolving the current branch:
//   work_branch  — short name, e.g. "master"
//   work_ref     — full ref,   e.g. "refs/heads/master"
//   wip_ref      — full ref,   e.g. "refs/wip/master"
// ---------------------------------------------------------------------------
struct BranchNames {
    std::string work_branch; // short, e.g. "master"
    std::string work_ref;    // e.g. "refs/heads/master"
    std::string wip_ref;     // e.g. "refs/wip/master"
};

// ---------------------------------------------------------------------------
// resolve_branch_names
//
// Derive BranchNames from `repo`.
//
// If `branch_name` is provided, build names for that branch directly.
// Otherwise read HEAD from `repo` and derive the current branch names.
//
// Returns std::nullopt if HEAD is unborn or detached (not on a local branch)
// when no explicit branch name is given.
// ---------------------------------------------------------------------------
inline std::optional<BranchNames> resolve_branch_names(
    git_repository *repo,
    const std::optional<std::string> &branch_name = std::nullopt) {
    BranchNames bn;

    if (branch_name.has_value() && branch_name != "HEAD") {
        bn.work_branch = *branch_name;
        if (!strip_prefix_inplace(bn.work_branch, "refs/heads/") &&
            !strip_prefix_inplace(bn.work_branch, "refs/wip/") &&
            !strip_prefix_inplace(bn.work_branch, "wip/")) {
            // No prefix matched: treat argument as bare branch name.
        }
        bn.work_ref = "refs/heads/" + bn.work_branch;
    } else {
        GitReferenceGuard head_ref;
        if (git_repository_head(head_ref.ptr(), repo) < 0)
            return std::nullopt;
        if (!git_reference_is_branch(head_ref.get()))
            return std::nullopt;

        bn.work_ref = git_reference_name(head_ref.get()); // e.g. "refs/heads/master"
        bn.work_branch = strip_prefix(bn.work_ref, "refs/heads/");
    }

    bn.wip_ref     = "refs/wip/" + bn.work_branch;
    return bn;
}

// ---------------------------------------------------------------------------
// find_refs
//
// Enumerate references whose names begin with `prefix`.
// Example: prefix="refs/wip/" behaves like `git for-each-ref refs/wip/`.
//
// Returns an empty vector when no refs match OR if iteration fails.
// ---------------------------------------------------------------------------
inline std::vector<std::string> find_refs(
    git_repository *repo,
    const std::string_view prefix) {
    std::vector<std::string> refs;

    git_reference_iterator *iter = nullptr;
    if (git_reference_iterator_new(&iter, repo) < 0)
        return refs;

    git_reference *ref = nullptr;
    while (git_reference_next(&ref, iter) == 0) {
        const char *name = git_reference_name(ref);
        if (name != nullptr && std::string_view(name).starts_with(prefix))
            refs.emplace_back(name);
        git_reference_free(ref);
        ref = nullptr;
    }

    git_reference_iterator_free(iter);
    std::ranges::sort(refs);
    return refs;
}

// ---------------------------------------------------------------------------
// resolve_oid
//
// Look up the OID for a named ref.  Returns std::nullopt on failure.
// ---------------------------------------------------------------------------
inline std::optional<git_oid> resolve_oid(git_repository *repo,
                                          const std::string &ref_name) {
    git_oid oid{};
    if (git_reference_name_to_id(&oid, repo, ref_name.c_str()) < 0)
        return std::nullopt;
    return oid;
}

// ---------------------------------------------------------------------------
// ensure_reflog_dir
//
// Create the directory tree and empty file needed for libgit2 to maintain a
// reflog for `wip_ref` (e.g. "refs/wip/master").
// Silently ignores errors — a missing reflog is not fatal.
// ---------------------------------------------------------------------------
inline void ensure_reflog_dir(git_repository *repo, const std::string &wip_ref) {
    const char *git_dir = git_repository_path(repo); // ends with "/"
    std::filesystem::path reflog_path =
        std::filesystem::path(git_dir) / "logs" / wip_ref;
    std::error_code ec;
    std::filesystem::create_directories(reflog_path.parent_path(), ec);
    if (!ec && !std::filesystem::exists(reflog_path, ec))
        std::ofstream{reflog_path, std::ios::app}; // touch
}


================================================
FILE: src/main.cpp
================================================
#include "command.hpp"
#include "color.hpp"
#include "cmd_delete.hpp"
#include "cmd_list.hpp"
#include "cmd_log.hpp"
#include "cmd_save.hpp"
#include "cmd_status.hpp"

#include <cstdlib>
#include <iostream>
#include "git_wip_version.h"
#include "print_compat.hpp"
#include <format>
#include <map>
#include <memory>
#include <vector>
#include "spdlog/spdlog.h"

bool g_wip_debug = false;

void print_main_help(const std::vector<std::unique_ptr<Command>>& commands, std::ostream &os = std::cout) {
    std::println(os, "Manage Work In Progress\n");
    std::println(os, "git-wip <command> [ --help | --version | command options ]\n");
    for (const auto& cmd : commands) {
        std::println("    git-wip {:20} # {}", cmd->name(), cmd->desc());
    }
    std::println(os, "\nUse git-wip <command> --help to see command options.\n");
}

int main(int argc, char *argv[]) {
    color_init();

    // Check WIP_DEBUG environment variable for debug logging
    const char* wip_debug = std::getenv("WIP_DEBUG");
    if (wip_debug != nullptr && wip_debug[0] != '\0' && wip_debug[0] != '0') {
        spdlog::set_level(spdlog::level::debug);
        spdlog::debug("Debug logging enabled via WIP_DEBUG environment variable");
        g_wip_debug = true;
    }

    std::vector<std::unique_ptr<Command>> commands;
    commands.push_back(std::make_unique<SaveCmd>());
    commands.push_back(std::make_unique<StatusCmd>());
    commands.push_back(std::make_unique<ListCmd>());
    commands.push_back(std::make_unique<LogCmd>());
    commands.push_back(std::make_unique<DeleteCmd>());

    std::map<std::string, Command*> command_map;
    for (const auto& cmd : commands) {
        command_map[cmd->name()] = cmd.get();
    }

    // No arguments: default to "save WIP" (matches old shell script behaviour)
    if (argc < 2) {
        spdlog::debug("no arguments, defaulting to 'save WIP'");
        auto it = command_map.find("save");
        if (it != command_map.end()) {
            // Build a synthetic argv: ["save", "WIP"]
            static const char *default_argv[] = {"save", "WIP", nullptr};
            return it->second->run(2, const_cast<char **>(default_argv));
        }
        spdlog::error("internal error, 'save' not implemented");
        return 1;
    }

    std::string command_name = argv[1];

    if (command_name == "help" || command_name == "--help" || command_name == "-h") {
        print_main_help(commands);
        return 0;
    }

    if (command_name == "version" || command_name == "--version" || command_name == "-v") {
        std::cout << GIT_WIP_VERSION << std::endl;
        return 0;
    }

    // If the first argument looks like a file (not a known command and not an
    // option), treat the whole invocation as "save WIP [files...]" — matching
    // the old script behaviour where bare file paths fall through to save.
    auto it = command_map.find(command_name);
    if (it != command_map.end()) {
        Command* cmd = it->second;
        // Pass remaining arguments to the command, skipping argv[0] (program name),
        // so that argv[1] (command name) becomes argv[0] inside the command parser
        return cmd->run(argc - 1, argv + 1);
    } else {
        spdlog::error("Unknown command '{}'", command_name);
        print_main_help(commands, std::cerr);
        return 1;
    }

    return 0;
}


================================================
FILE: src/print_compat.hpp
================================================
#pragma once

// print_compat.hpp — portable std::print / std::println
//
// C++23's <print> (P2093) is not available on all compilers we support:
//   - GCC 14+   ✓
//   - GCC 13    ✗  (Ubuntu 24.04)
//   - GCC 12    ✗  (Debian stable)
//   - Clang 17+ ✓  (with libc++)
//
// When WIP_HAVE_STD_PRINT is defined by CMake (via check_cxx_source_compiles),
// we use the real <print>.  Otherwise we shim std::print / std::println on top
// of {fmt}, which is fetched by FetchContent and has an identical API.

#ifdef WIP_HAVE_STD_PRINT

#include <print>

#else // fallback: map std::print / std::println → fmt::print / fmt::println

#include <fmt/core.h>
#include <fmt/ostream.h>

#include <ostream>
#include <utility>

namespace std {

template <typename... Args>
void print(fmt::format_string<Args...> fmt_str, Args &&...args) {
    fmt::print(fmt_str, std::forward<Args>(args)...);
}

template <typename... Args>
void print(std::ostream &os, fmt::format_string<Args...> fmt_str, Args &&...args) {
    fmt::print(os, fmt_str, std::forward<Args>(args)...);
}

template <typename... Args>
void println(fmt::format_string<Args...> fmt_str, Args &&...args) {
    fmt::println(fmt_str, std::forward<Args>(args)...);
}

template <typename... Args>
void println(std::ostream &os, fmt::format_string<Args...> fmt_str, Args &&...args) {
    fmt::println(os, fmt_str, std::forward<Args>(args)...);
}

} // namespace std

#endif // WIP_HAVE_STD_PRINT


================================================
FILE: src/string_helpers.hpp
================================================
#pragma once

// string_helpers.hpp — pure string/time utility functions with no git dependency.
// All functions are inline so this header is self-contained and unit-testable
// without linking against libgit2.

#include <chrono>
#include <format>
#include <string>
#include <string_view>

// ---------------------------------------------------------------------------
// strip_prefix
//
// If `s` starts with `prefix`, return the remainder.  Otherwise return `s`
// unchanged.
// ---------------------------------------------------------------------------
inline std::string strip_prefix(std::string_view s, std::string_view prefix) {
    if (s.substr(0, prefix.size()) == prefix)
        return std::string(s.substr(prefix.size()));
    return std::string(s);
}

// ---------------------------------------------------------------------------
// strip_prefix_inplace
//
// If `s` starts with `prefix`, remove it in-place and return true.
// Otherwise leave `s` unchanged and return false.
// ---------------------------------------------------------------------------
inline bool strip_prefix_inplace(std::string &s, std::string_view prefix) {
    if (!std::string_view(s).starts_with(prefix))
        return false;
    s.erase(0, prefix.size());
    return true;
}

// ---------------------------------------------------------------------------
// first_line
//
// Return the text up to (but not including) the first newline.
// Returns an empty string if `msg` is null.
// ---------------------------------------------------------------------------
inline std::string first_line(const char *msg) {
    if (!msg) return {};
    std::string_view sv(msg);
    auto pos = sv.find('\n');
    return std::string(pos == std::string_view::npos ? sv : sv.substr(0, pos));
}

// ---------------------------------------------------------------------------
// relative_time
//
// Format a point-in-time (seconds since epoch) as a human-readable relative
// string, e.g. "5 minutes ago".  Mirrors git's approximate relative-date
// output.
// ---------------------------------------------------------------------------
inline std::string relative_time(std::int64_t epoch_seconds) {
    using namespace std::chrono;
    auto commit_tp = system_clock::from_time_t(static_cast<time_t>(epoch_seconds));
    auto now       = system_clock::now();
    auto secs      = duration_cast<seconds>(now - commit_tp).count();
    if (secs < 0) secs = 0;

    if (secs < 90)
        return std::format("{} second{} ago", secs, secs == 1 ? "" : "s");

    auto mins = secs / 60;
    if (mins < 90)
        return std::format("{} minute{} ago", mins, mins == 1 ? "" : "s");

    auto hours = mins / 60;
    if (hours < 36)
        return std::format("{} hour{} ago", hours, hours == 1 ? "" : "s");

    auto days = hours / 24;
    if (days < 14)
        return std::format("{} day{} ago", days, days == 1 ? "" : "s");

    auto weeks = days / 7;
    if (weeks < 8)
        return std::format("{} week{} ago", weeks, weeks == 1 ? "" : "s");

    auto months = days / 30;
    if (months < 24)
        return std::format("{} month{} ago", months, months == 1 ? "" : "s");

    auto years = days / 365;
    return std::format("{} year{} ago", years, years == 1 ? "" : "s");
}


================================================
FILE: src/wip_helpers.hpp
================================================
#pragma once

// wip_helpers.hpp — higher-level helpers that encode the core git-wip
// branching logic shared between the save, log, and status commands.

#include "git_guards.hpp"
#include "git_helpers.hpp"

#include <optional>
#include <vector>

// ---------------------------------------------------------------------------
// wip_parent_oid
//
// Determine the commit that a new WIP commit should be parented on, given:
//   work_last  — current HEAD of the work branch
//   wip_last   — current tip of refs/wip/<branch> (nullopt if none)
//
// Rules (matching the original shell script):
//   • No wip branch yet  → parent = work_last
//   • merge_base(wip_last, work_last) == work_last
//     (work branch hasn't advanced)  → parent = wip_last  (stack on top)
//   • Otherwise (work branch has new commits) → parent = work_last  (reset)
//
// Returns std::nullopt if the two branches are completely unrelated (no
// common ancestor), which is an error the caller should report.
// ---------------------------------------------------------------------------
inline std::optional<git_oid> wip_parent_oid(
    git_repository         *repo,
    const git_oid          &work_last,
    const std::optional<git_oid> &wip_last)
{
    if (!wip_last.has_value())
        return work_last;   // first save — root from work branch HEAD

    git_oid base{};
    if (git_merge_base(&base, repo, &*wip_last, &work_last) < 0)
        return std::nullopt; // unrelated histories

    // If work_last IS the merge-base, the work branch hasn't moved since
    // the last wip save — keep stacking.
    if (git_oid_equal(&base, &work_last))
        return *wip_last;

    // Work branch has advanced — reset the wip stack.
    return work_last;
}

// ---------------------------------------------------------------------------
// collect_wip_commits
//
// Walk from `wip_last` backwards, stopping at (but not including) `work_last`,
// and return the OIDs in topological order (newest first).
//
// Returns an empty vector when the work branch has advanced past the wip
// branch (merge_base != work_last), mirroring the status-command logic:
// there are 0 "current" wip commits in that situation.
//
// Returns std::nullopt on a libgit2 error.
// ---------------------------------------------------------------------------
inline std::optional<std::vector<git_oid>> collect_wip_commits(
    git_repository *repo,
    const git_oid  &wip_last,
    const git_oid  &work_last)
{
    // Compute merge-base to decide whether the wip stack is still "live".
    git_oid base{};
    if (git_merge_base(&base, repo, &wip_last, &work_last) < 0)
        return std::nullopt;

    // Work branch has advanced past the wip branch → 0 current commits.
    if (!git_oid_equal(&base, &work_last))
        return std::vector<git_oid>{};

    GitRevwalkGuard walk;
    if (git_revwalk_new(walk.ptr(), repo) < 0)
        return std::nullopt;

    git_revwalk_sorting(walk.get(), GIT_SORT_TOPOLOGICAL);
    git_revwalk_push(walk.get(), &wip_last);
    git_revwalk_hide(walk.get(), &work_last);

    std::vector<git_oid> result;
    git_oid oid{};
    while (git_revwalk_next(&oid, walk.get()) == 0)
        result.push_back(oid);

    return result;
}


================================================
FILE: sublime/gitwip.py
================================================
import sublime_plugin
from subprocess import Popen, PIPE, STDOUT
import os
import sublime
import copy

class GitWipAutoCommand(sublime_plugin.EventListener):

    def on_post_save_async(self, view):
        dirname, fname = os.path.split(view.file_name())

        p = Popen(["git", "wip", "save",
                   "WIP from ST3: saving %s" % fname,
                   "--editor", "--", fname],
                  cwd=dirname, universal_newlines=True,
                  bufsize=8096, stdout=PIPE, stderr=STDOUT)

        def finish_callback():
            rcode = p.poll()

            if rcode is None: # not terminated yet
                sublime.set_timeout_async(finish_callback, 20)
                return

            if rcode != 0:
                print ("git command returned code {}".format(rcode))

            for line in p.stdout:
                print(line)

        finish_callback()



================================================
FILE: test/cli/CMakeLists.txt
================================================
# test/cli/CMakeLists.txt
#
# Registers each cli integration test as a ctest entry.
#
# TEST_TREE is placed inside the cmake binary directory so ctest manages it.
# Artifacts are NOT removed after the test runs (useful for debugging).
# Before each test its own subdirectory is wiped and re-created by the script.

set(LEGACY_TEST_TREE "${CMAKE_CURRENT_BINARY_DIR}/test-artifacts")
set(GIT_WIP_BIN "$<TARGET_FILE:git-wip>")

foreach(TEST_NAME IN ITEMS test_legacy test_spaces test_status test_status2 test_status_ref test_save_file test_save_subdir test_list test_delete test_help)
    add_test(
        NAME    "cli/${TEST_NAME}"
        COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/${TEST_NAME}.sh"
    )
    set_tests_properties("cli/${TEST_NAME}" PROPERTIES
        ENVIRONMENT "GIT_WIP=${GIT_WIP_BIN};TEST_TREE=${LEGACY_TEST_TREE}"
        TIMEOUT 60
    )
endforeach()


================================================
FILE: test/cli/lib.sh
================================================
# lib.sh -- shared helpers for git-wip legacy integration tests
#
# Source this file from each test script; do NOT execute directly.
#
# Required environment variables (set by ctest via CMakeLists.txt):
#   GIT_WIP      path to the git-wip binary under test
#   TEST_TREE    base directory for test artifacts (one subdir per test)
#
# TEST_NAME is derived from the sourcing script's filename (basename without .sh).

set -e

die()  { echo >&2 "ERROR: $*"   ; exit 1 ; }
warn() { echo >&2 "WARNING: $*" ; }
note() { echo >&2 "# $*"        ; }

# ------------------------------------------------------------------------
# Validate environment

[ -z "${GIT_WIP}"   ] && die "GIT_WIP is not set"
[ -x "${GIT_WIP}"   ] || die "GIT_WIP=${GIT_WIP} is not executable"
[ -z "${TEST_TREE}" ] && die "TEST_TREE is not set"

# Derive test name from the calling script's filename
TEST_NAME="$(basename "$0" .sh)"

# ------------------------------------------------------------------------
# Per-test paths

REPO="$TEST_TREE/$TEST_NAME/repo"
CMD="$TEST_TREE/$TEST_NAME/cmd"
OUT="$TEST_TREE/$TEST_NAME/out"
RC="$TEST_TREE/$TEST_NAME/rc"

# Clean before running so each run starts fresh; leave artifacts after for debugging
rm -rf "$TEST_TREE/$TEST_NAME"
mkdir -p "$TEST_TREE/$TEST_NAME"

note "Running $TEST_NAME (artifacts in $TEST_TREE/$TEST_NAME)"

# ------------------------------------------------------------------------
# Test helpers

# Current working directory for _RUN/_RUN_IN; starts at $REPO.
_RUN_CWD=""

_RUN() {
    note "$@"
    [ "$(pwd)" = "$REPO" ] || die "expected cwd=$REPO, got $(pwd)"

    set +e
    printf '%s' "$*" >"$CMD"
    eval "$@" >"$OUT" 2>&1
    printf '%s' "$?" >"$RC"
    set -e
}

RUN() {
    _RUN "$@"
    local rc
    rc="$(cat "$RC")"
    [ "$rc" = 0 ] || handle_error
}

# CD <subdir> — change the working directory for subsequent RUN_IN calls.
# Use CD "" or CD_ROOT to return to $REPO.
CD() {
    local target
    if [ -z "$1" ]; then
        target="$REPO"
    else
        target="$REPO/$1"
    fi
    cd "$target" || die "CD: cannot cd to $target"
    _RUN_CWD="$target"
    note "CD → $(pwd)"
}

CD_ROOT() { CD ""; }

# _RUN_IN — like _RUN but allows cwd to be a subdirectory of $REPO.
_RUN_IN() {
    note "$@"
    local expected="${_RUN_CWD:-$REPO}"
    [ "$(pwd)" = "$expected" ] || die "expected cwd=$expected, got $(pwd)"

    set +e
    printf '%s' "$*" >"$CMD"
    eval "$@" >"$OUT" 2>&1
    printf '%s' "$?" >"$RC"
    set -e
}

# RUN_IN — like RUN but for subdirectory context (set via CD).
RUN_IN() {
    _RUN_IN "$@"
    local rc
    rc="$(cat "$RC")"
    [ "$rc" = 0 ] || handle_error
}

EXP_none() {
    local out
    out="$(head -n1 "$OUT")"
    if [ -n "$out" ]; then
        warn "expected no output, got: $out"
        handle_error
    fi
}

EXP_text() {
    local exp="$1"
    local out
    out="$(head -n1 "$OUT")"
    if [ "$out" != "$exp" ]; then
        warn "exp: $exp"
        warn "out: $out"
        handle_error
    fi
}

EXP_grep() {
    if ! grep -q "$@" <"$OUT"; then
        warn "grep $* — not matched"
        handle_error
    fi
}

create_test_repo() {
    rm -rf "$REPO"
    mkdir -p "$REPO"
    cd "$REPO"
    RUN git init
    # Force branch name to "master" regardless of init.defaultBranch config
    RUN git checkout -b master
}

handle_error() {
    set +e
    warn "CMD='$(cat "$CMD")' RC=$(cat "$RC")"
    cat >&2 "$OUT"
    exit 1
}


================================================
FILE: test/cli/profile.sh
================================================
#!/usr/bin/env bash
# profile.sh -- performance profiling test for git-wip save
# Run manually: GIT_WIP=/path/to/git-wip TEST_TREE=/tmp/test-tree ./profile.sh
#
# This test builds a large git repository and measures time for different
# git-wip save scenarios to identify performance bottlenecks.

if [ -z "$GIT_WIP" ] && [ -z "$TEST_TREE" ] ; then
    echo "# GIT_WIP and TEST_TREE not specified, using defaults"
    GIT_WIP=$(pwd)/build/src/git-wip
    TEST_TREE=/tmp/wip-profile
fi

source "$(dirname "$0")/lib.sh"

# -------------------------------------------------------------------------
# Helper: time a command and print its duration

time_cmd() {
    local label="$1"
    shift

    note "=== $label ==="
    local start end duration
    start=$(date +%s%3N)
    eval "$@"
    end=$(date +%s%3N)
    duration=$((end - start))
    printf '%d ms\n' "$duration"
    note ""
}

# -------------------------------------------------------------------------
# Create the test repository with nested directories

create_test_repo
RUN git config user.email "test@example.com"
RUN git config user.name "Test User"

note "Creating nested directory structure (2560 files)..."
cd "$REPO"

# Create directories {a,b,c,d}/{a,b,c,d}/{a,b,c,d}/{a,b,c,d}
for d1 in a b c d; do
    for d2 in a b c d; do
        for d3 in a b c d; do
            for d4 in a b c d; do
                dir="$REPO/$d1/$d2/$d3/$d4"
                mkdir -p "$dir"
                # Create 10 files in each directory
                for f in 0 1 2 3 4 5 6 7 8 9; do
                    echo "content" > "$dir/$f"
                done
                # Commit each directory separately
                git add -A "$dir"/ >/dev/null
                git commit -m "\"$dir\"" >/dev/null
            done
        done
    done
done

note "Initial commit structure ready:"
RUN git log --oneline
RUN git rev-list --count HEAD
RUN git ls-files
lines="$(wc -l <"$OUT")"
if ! [ "$lines" = 2560 ] ; then
    die "expecting 2560 files but found $files, check $OUT"
fi

# -------------------------------------------------------------------------
# Scenario 1: wip save on all files (all changed)

note "=== SCENARIO 1: wip save on all files (all changed) ==="
cd "$REPO"
for f in $(find . -path ./.git -prune -o -type f -print); do
    echo "test1" > "$f"
done
sync

time_cmd "git-wip save (all files changed)" "$GIT_WIP" save "\"WIP-1\""

# -------------------------------------------------------------------------
# Scenario 2: wip save on one file, all changed (but only save one path)

note "=== SCENARIO 2: wip save on one path, all files changed ==="
git reset --hard
for f in $(find . -path ./.git -prune -o -type f -print); do
    echo "test2" > "$f"
done
sync

time_cmd "git-wip save -- a/b/c/d/0 (one path)" "$GIT_WIP" save "\"WIP-2\"" -- a/b/c/d/0

# -------------------------------------------------------------------------
# Scenario 3: wip save on one file, one changed

note "=== SCENARIO 3: wip save on one file, one changed ==="
git reset --hard
echo "test3" > a/b/c/d/0
sync

time_cmd "git-wip save -- a/b/c/d/0 (single file)" "$GIT_WIP" save "\"WIP-3\"" -- a/b/c/d/0

# -------------------------------------------------------------------------
# Summary

note "=== PROFILING COMPLETE ==="
note "Repository: $REPO"
note "Files: 2560 in 256 directories"
note ""
note "Run 'du -sh $REPO/.git' to see git object database size"

echo "OK: $TEST_NAME"


================================================
FILE: test/cli/test_delete.sh
================================================
#!/usr/bin/env bash
source "$(dirname "$0")/lib.sh"

create_test_repo
RUN git config user.email "test@example.com"
RUN git config user.name "Test User"

RUN "echo v1 >file.txt"
RUN git add file.txt
RUN git commit -m initial

# master wip
RUN "echo m2 >file.txt"
RUN "$GIT_WIP" save "\"master one\""

# foo wip
RUN git checkout -b foo
RUN "echo f1 >foo.txt"
RUN git add foo.txt
RUN git commit -m "\"foo base\""
RUN "echo f2 >>foo.txt"
RUN "$GIT_WIP" save "\"foo one\""

# orphaned wip ref
RUN git update-ref refs/wip/orphan HEAD

# cleanup removes only orphaned refs
RUN "$GIT_WIP" delete --cleanup
EXP_grep "deleted wip/orphan"
EXP_grep "deleted 1 orphaned wip ref"

RUN git for-each-ref
EXP_grep "refs/wip/master$"
EXP_grep "refs/wip/foo$"
EXP_grep -v "refs/wip/orphan$"

# delete a single wip ref by explicit ref name
RUN "$GIT_WIP" delete refs/wip/foo
EXP_grep "deleted wip/foo"
RUN git for-each-ref
EXP_grep -v "refs/wip/foo$"
EXP_grep "refs/wip/master$"

# delete current branch wip with confirmation: 'n' aborts
RUN git checkout -f master
_RUN "printf 'n\n' | \"$GIT_WIP\" delete"
EXP_grep "About to delete wip/master \[Y/n\]"
RUN git for-each-ref
EXP_grep "refs/wip/master$"

# empty response confirms delete
RUN "printf '\n' | \"$GIT_WIP\" delete"
EXP_grep "deleted wip/master"
RUN git for-each-ref
EXP_grep -v "refs/wip/master$"

# recreate and delete with --yes (no prompt)
RUN "echo m3 >file.txt"
RUN "$GIT_WIP" save "\"master two\""
RUN "$GIT_WIP" delete --yes
EXP_grep "deleted wip/master"
EXP_grep -v "About to delete"
RUN git for-each-ref
EXP_grep -v "refs/wip/master$"

echo "OK: $TEST_NAME"


================================================
FILE: test/cli/test_help.sh
================================================
#!/usr/bin/env bash

source "$(dirname "$0")/lib.sh"

create_test_repo

# Test main help commands

_RUN "$GIT_WIP" help 2>/dev/null

EXP_grep "."

_RUN "$GIT_WIP" --help 2>/dev/null

EXP_grep "."

_RUN "$GIT_WIP" -h 2>/dev/null

EXP_grep "."

_RUN "$GIT_WIP" --version 2>/dev/null

EXP_grep "."

# Test per-command help

for cmd in save status log delete ; do

  _RUN "$GIT_WIP" $cmd --help 2>/dev/null

  EXP_grep "."

  _RUN "$GIT_WIP" $cmd -h 2>/dev/null

  EXP_grep "."

done

echo "OK: $TEST_NAME"

================================================
FILE: test/cli/test_legacy.sh
================================================
#!/usr/bin/env bash
source "$(dirname "$0")/lib.sh"

# these tests are here to make sure we behave the same way as the legacy git-wip shell implementation
# do not add anymore tests here.  Create new tests in test/cli/test_*.sh instead.

create_test_repo
RUN "echo 1 >README"
RUN git add README
RUN git commit -m README

# run wip w/o changes
_RUN "$GIT_WIP" save
EXP_text "no changes"

RUN "$GIT_WIP" save --editor
EXP_none

RUN "$GIT_WIP" save -e
EXP_none

# expecting a master branch
RUN git branch
EXP_grep "^\* master$"
EXP_grep -v "wip"

# not expecting a wip ref at this time
RUN git for-each-ref
EXP_grep -v "commit.refs/wip/master$"

# make changes, store wip
RUN "echo 2 >README"
RUN "$GIT_WIP" save --editor
EXP_none

# expecting a wip ref
RUN git for-each-ref
EXP_grep "commit.refs/wip/master$"

# expecting a log entry
RUN "$GIT_WIP" log
EXP_grep "^commit "
EXP_grep "^\s\+WIP$"

# there should be no wip branch
RUN git branch
EXP_grep -v "wip"

# make changes, store wip
RUN "echo 3 >README"
RUN "$GIT_WIP" save "\"message2\""
EXP_none

# expecting both log entries
RUN "$GIT_WIP" log
EXP_grep "^commit "
EXP_grep "^\s\+WIP$"
EXP_grep "^\s\+message2$"

# make a commit
RUN git add -u README
RUN git commit -m README.2

# make changes, store wip
RUN "echo 4 >UNTRACKED"
RUN "echo 4 >README"
RUN "$GIT_WIP" save "\"message3\""
EXP_none

# expecting message3, not message2 or original WIP
RUN "$GIT_WIP" log
EXP_grep "^commit "
EXP_grep -v "^\s\+WIP$"
EXP_grep -v "^\s\+message2$"
EXP_grep "^\s\+message3$"

# expecting file changes to README, not UNTRACKED
RUN "$GIT_WIP" log --stat
EXP_grep "^commit "
EXP_grep "^ README | 2"
EXP_grep -v "UNTRACKED"

# need to be able to extract latest data from git wip branch
RUN git show HEAD:README
EXP_grep '^3$'
EXP_grep -v '^4$'

RUN git show wip/master:README
EXP_grep -v '^3$'
EXP_grep '^4$'

echo "OK: $TEST_NAME"


================================================
FILE: test/cli/test_list.sh
================================================
#!/usr/bin/env bash
source "$(dirname "$0")/lib.sh"

create_test_repo
RUN git config user.email "test@example.com"
RUN git config user.name "Test User"

RUN "echo v1 >file.txt"
RUN git add file.txt
RUN git commit -m initial

# one wip commit on master
RUN "echo master-wip >file.txt"
RUN "$GIT_WIP" save "\"master snapshot\""

# one wip commit on foo
RUN git checkout -b foo
RUN "echo foo-wip >file.txt"
RUN "$GIT_WIP" save "\"foo snapshot\""

# add an orphaned wip ref (no refs/heads/baz branch)
RUN git update-ref refs/wip/baz HEAD

# list all wip refs (short form)
RUN "$GIT_WIP" list
EXP_grep "^wip/master$"
EXP_grep "^wip/foo$"
EXP_grep "^wip/baz$"

# verbose output includes ahead counts and orphaned refs
RUN "$GIT_WIP" list -v
EXP_grep "^wip/master has 1 commit ahead of master$"
EXP_grep "^wip/foo has 1 commit ahead of foo$"
EXP_grep "^wip/baz is orphaned$"

echo "OK: $TEST_NAME"


================================================
FILE: test/cli/test_save_file.sh
================================================
#!/usr/bin/env bash
source "$(dirname "$0")/lib.sh"

create_test_repo
RUN git config user.email "test@example.com"
RUN git config user.name "Test User"

RUN "echo base >file_a"
RUN "echo base >file_b"
RUN "echo base >file_c"
RUN git add file_a file_b file_c
RUN git commit -m initial

# -------------------------------------------------------------------------
# modify two files, save only one — only the specified file appears in WIP

RUN "echo mod_a >file_a"
RUN "echo mod_b >file_b"

RUN "$GIT_WIP" save "\"save-a-only\"" -- file_a
RUN git show wip/master --stat

# file_a changed, file_b must not appear
EXP_grep "file_a"
EXP_grep -v "file_b"
EXP_grep -v "file_c"

# verify content in wip tree
RUN git show wip/master:file_a
EXP_grep "^mod_a$"

RUN git show wip/master:file_b
EXP_grep "^base$"   # unchanged in wip tree

# -------------------------------------------------------------------------
# save the other modified file — stacks on previous WIP commit

RUN "$GIT_WIP" save "\"save-b-only\"" -- file_b
RUN git show wip/master --stat

EXP_grep "file_b"
EXP_grep -v "file_a"   # file_a was already at mod_a in the parent; no delta
EXP_grep -v "file_c"

RUN git show wip/master:file_a
EXP_grep "^mod_a$"   # persisted from previous wip commit

RUN git show wip/master:file_b
EXP_grep "^mod_b$"

# -------------------------------------------------------------------------
# save multiple files at once with --

RUN "echo mod2_a >file_a"
RUN "echo mod2_c >file_c"

RUN "$GIT_WIP" save "\"save-a-and-c\"" -- file_a file_c
RUN git show wip/master --stat

EXP_grep "file_a"
EXP_grep "file_c"
EXP_grep -v "file_b"

RUN git show wip/master:file_a
EXP_grep "^mod2_a$"

RUN git show wip/master:file_c
EXP_grep "^mod2_c$"

# -------------------------------------------------------------------------
# untracked file saved explicitly with --

RUN "echo untracked >file_d"

RUN "$GIT_WIP" save "\"save-untracked-file\"" -- file_d
RUN git show wip/master --stat

EXP_grep "file_d"
EXP_grep -v "file_a"
EXP_grep -v "file_b"
EXP_grep -v "file_c"

RUN git show wip/master:file_d
EXP_grep "^untracked$"

# -------------------------------------------------------------------------
# saving an unchanged (already-at-baseline) file reports "no changes"

_RUN "$GIT_WIP" save "\"save-c-nochange\"" -- file_c
EXP_text "no changes"

# -------------------------------------------------------------------------
# --editor mode with unchanged file is silent and exits 0

RUN "$GIT_WIP" save --editor "\"save-c-editor\"" -- file_c
EXP_none

# -------------------------------------------------------------------------
# --editor mode with a changed file succeeds silently

RUN "echo editor_change >file_c"
RUN "$GIT_WIP" save --editor "\"save-c-via-editor\"" -- file_c
EXP_none

RUN git show wip/master:file_c
EXP_grep "^editor_change$"

# -------------------------------------------------------------------------
# after a real commit, a new -- file save starts a fresh WIP stack

RUN git add file_a file_b file_c file_d
RUN git commit -m "\"commit all changes\""

RUN "echo post_commit >file_a"
RUN "$GIT_WIP" save "\"post-commit-a\"" -- file_a

# new wip commit is rooted at the new work HEAD — only file_a in delta
RUN git show wip/master --stat
EXP_grep "file_a"
EXP_grep -v "file_b"
EXP_grep -v "file_c"
EXP_grep -v "file_d"

# status shows exactly 1 wip commit
RUN "$GIT_WIP" status -l
EXP_grep "branch master has 1 wip commit on refs/wip/master"
EXP_grep "post-commit-a"
EXP_grep -v "save-a-only"

echo "OK: $TEST_NAME"


================================================
FILE: test/cli/test_save_subdir.sh
================================================
#!/usr/bin/env bash
source "$(dirname "$0")/lib.sh"

create_test_repo
RUN git config user.email "test@example.com"
RUN git config user.name "Test User"

# -------------------------------------------------------------------------
# Set up: a/b/file committed at baseline
RUN mkdir -p a/b
RUN "echo base >a/b/file"
RUN git add a/b/file
RUN git commit -m "\"initial\""

# -------------------------------------------------------------------------
# Save from repo root using full relative path a/b/file

RUN "echo 'from root' >a/b/file"
RUN_IN "$GIT_WIP" save "\"from root\"" -- a/b/file

RUN_IN "$GIT_WIP" status -l -f
EXP_grep "1 wip commit"
EXP_grep "from root"
EXP_grep "a/b/file"

RUN git show wip/master:a/b/file
EXP_grep "^from root$"

# -------------------------------------------------------------------------
# Save from subdirectory a/ using path b/file

CD a
RUN_IN "echo 'from a' >b/file"
RUN_IN "$GIT_WIP" save "\"from a\"" -- b/file

CD_ROOT
RUN_IN "$GIT_WIP" status -l -f
EXP_grep "2 wip commits"
EXP_grep "from a"
EXP_grep "from root"
EXP_grep "a/b/file"

RUN git show wip/master:a/b/file
EXP_grep "^from a$"

# -------------------------------------------------------------------------
# Save from subdirectory a/b/ using bare filename file

CD a/b
RUN_IN "echo 'from b' >file"
RUN_IN "$GIT_WIP" save "\"from b\"" -- file

CD_ROOT
RUN_IN "$GIT_WIP" status -l -f
EXP_grep "3 wip commits"
EXP_grep "from b"
EXP_grep "from a"
EXP_grep "from root"
EXP_grep "a/b/file"

RUN git show wip/master:a/b/file
EXP_grep "^from b$"

# -------------------------------------------------------------------------
# Verify --editor mode from a subdirectory (no changes → silent exit 0)

CD a/b
RUN_IN "$GIT_WIP" save --editor "\"no-change-editor\"" -- file
EXP_none

# -------------------------------------------------------------------------
# Verify that saving from subdir does NOT corrupt the real index

CD_ROOT
RUN git status --short
# a/b/file should show as modified (M) in the working tree, not staged
EXP_grep " M a/b/file"

echo "OK: $TEST_NAME"


================================================
FILE: test/cli/test_spaces.sh
================================================
#!/usr/bin/env bash
source "$(dirname "$0")/lib.sh"

create_test_repo
RUN "echo 1 >\"s p a c e s\""
RUN git add "\"s p a c e s\""
RUN git commit -m "\"s p a c e s\""

# make changes, store wip
RUN "echo 2 >\"s p a c e s\""
RUN "$GIT_WIP" save "\"message with spaces\""
EXP_none

# expecting a wip ref
RUN git for-each-ref
EXP_grep "commit.refs/wip/master$"

# expecting a log entry
RUN "$GIT_WIP" log
EXP_grep "^commit "
EXP_grep "^\s\+message with spaces$"

echo "OK: $TEST_NAME"


================================================
FILE: test/cli/test_status.sh
================================================
#!/usr/bin/env bash
source "$(dirname "$0")/lib.sh"

create_test_repo
RUN git config user.email "test@example.com"
RUN git config user.name "Test User"

RUN "echo v1 >file.txt"
RUN git add file.txt
RUN git commit -m initial

# -------------------------------------------------------------------------
# no wip branch yet — status should report zero wip commits (exit 0)

RUN "$GIT_WIP" status
EXP_grep "no wip commits"

# -------------------------------------------------------------------------
# create 3 wip commits with distinct file changes

RUN "echo v2 >file.txt"
RUN "$GIT_WIP" save "\"WIP one\""

RUN "echo v3 >file.txt"
RUN "$GIT_WIP" save "\"WIP two\""

RUN "echo v4 >file.txt"
RUN "$GIT_WIP" save "\"WIP three\""

# -------------------------------------------------------------------------
# status (no flags) — summary line only

RUN "$GIT_WIP" status
EXP_grep "branch master has 3 wip commits on refs/wip/master"

# -------------------------------------------------------------------------
# status -l — summary + one line per commit (newest first)

RUN "$GIT_WIP" status -l
EXP_grep "branch master has 3 wip commits on refs/wip/master"
# each commit line: <sha7> - <subject> (<age>)
EXP_grep " - WIP three ("
EXP_grep " - WIP two ("
EXP_grep " - WIP one ("
# order: newest first — line number of "WIP three" must be less than "WIP one"
three_line=$(grep -n "WIP three" "$OUT" | cut -d: -f1)
one_line=$(grep -n "WIP one" "$OUT" | cut -d: -f1)
[ "$three_line" -lt "$one_line" ] || { warn "WIP three not before WIP one in output"; handle_error; }

# -------------------------------------------------------------------------
# status --list is synonymous with -l

RUN "$GIT_WIP" status --list
EXP_grep "branch master has 3 wip commits on refs/wip/master"
EXP_grep " - WIP three ("

# -------------------------------------------------------------------------
# status -f — summary + diff --stat from HEAD to latest wip

RUN "$GIT_WIP" status -f
EXP_grep "branch master has 3 wip commits on refs/wip/master"
# git diff --stat output includes the filename and change counts
EXP_grep "file.txt"
EXP_grep "changed"

# -------------------------------------------------------------------------
# status --files is synonymous with -f

RUN "$GIT_WIP" status --files
EXP_grep "branch master has 3 wip commits on refs/wip/master"
EXP_grep "file.txt"

# -------------------------------------------------------------------------
# status -l -f — per-commit diff --stat interleaved with list lines

RUN "$GIT_WIP" status -l -f
EXP_grep "branch master has 3 wip commits on refs/wip/master"
EXP_grep " - WIP three ("
EXP_grep " - WIP two ("
EXP_grep " - WIP one ("
# each commit's diff --stat should show file.txt changed
EXP_grep "file.txt"
EXP_grep "changed"
# summary comes before any commit lines
summary_line=$(grep -n "3 wip commits" "$OUT" | cut -d: -f1)
first_commit_line=$(grep -n "WIP three" "$OUT" | cut -d: -f1)
[ "$summary_line" -lt "$first_commit_line" ] || { warn "summary not before commit lines"; handle_error; }

echo "OK: $TEST_NAME"


================================================
FILE: test/cli/test_status2.sh
================================================
#!/usr/bin/env bash
source "$(dirname "$0")/lib.sh"

create_test_repo
RUN git config user.email "test@example.com"
RUN git config user.name "Test User"

RUN touch file
RUN git add file
RUN git commit -m initial

# save one wip commit
RUN "echo one >file"
RUN "$GIT_WIP" save "\"one\""

# should see "one"
RUN "$GIT_WIP" status -l
EXP_grep "branch master has 1 wip commit on refs/wip/master"
EXP_grep " - one ("

# advance the work branch past the wip
RUN git add file
RUN git commit -m one

# wip branch is now behind (or at) the work branch — no wip entries visible
RUN "$GIT_WIP" status -l
EXP_grep "branch master has 0 wip commits on refs/wip/master"

# save two more wip commits on the new work branch HEAD
RUN "echo two >file"
RUN "$GIT_WIP" save "\"two\""

# should see only "two"
RUN "$GIT_WIP" status -l
EXP_grep "branch master has 1 wip commit on refs/wip/master"
EXP_grep " - two ("
EXP_grep -v " - one ("

RUN "echo three >file"
RUN "$GIT_WIP" save "\"three\""

# should see "two" and "three", not "one"
RUN "$GIT_WIP" status -l
EXP_grep "branch master has 2 wip commits on refs/wip/master"
EXP_grep " - two ("
EXP_grep " - three ("
EXP_grep -v " - one ("

echo "OK: $TEST_NAME"


================================================
FILE: test/cli/test_status_ref.sh
================================================
#!/usr/bin/env bash
source "$(dirname "$0")/lib.sh"

create_test_repo
RUN git config user.email "test@example.com"
RUN git config user.name "Test User"

RUN "echo v1 >file.txt"
RUN git add file.txt
RUN git commit -m initial

# master: two wip commits
RUN "echo m2 >file.txt"
RUN "$GIT_WIP" save "\"master one\""
RUN "echo m3 >file.txt"
RUN "$GIT_WIP" save "\"master two\""

# foo: one wip commit
RUN git checkout -b foo
RUN "echo f1 >foo.txt"
RUN git add foo.txt
RUN git commit -m "\"foo base\""
RUN "echo f2 >>foo.txt"
RUN "$GIT_WIP" save "\"foo one\""

# Status on master from foo branch, across all accepted ref syntaxes
RUN "$GIT_WIP" status master
EXP_grep "branch master has 2 wip commits on refs/wip/master"

RUN "$GIT_WIP" status wip/master
EXP_grep "branch master has 2 wip commits on refs/wip/master"

RUN "$GIT_WIP" status refs/heads/master
EXP_grep "branch master has 2 wip commits on refs/wip/master"

RUN "$GIT_WIP" status refs/wip/master
EXP_grep "branch master has 2 wip commits on refs/wip/master"

# Status on foo across all accepted ref syntaxes
RUN "$GIT_WIP" status foo
EXP_grep "branch foo has 1 wip commit on refs/wip/foo"

RUN "$GIT_WIP" status wip/foo
EXP_grep "branch foo has 1 wip commit on refs/wip/foo"

RUN "$GIT_WIP" status refs/heads/foo
EXP_grep "branch foo has 1 wip commit on refs/wip/foo"

RUN "$GIT_WIP" status refs/wip/foo
EXP_grep "branch foo has 1 wip commit on refs/wip/foo"

echo "OK: $TEST_NAME"


================================================
FILE: test/nvim/CMakeLists.txt
================================================
# test/nvim/CMakeLists.txt
#
# Registers each Neovim integration test as a ctest entry.
#
# TEST_TREE is placed inside the cmake binary directory so ctest manages it.
# Artifacts are NOT removed after the test runs (useful for debugging).
# Before each test its own subdirectory is wiped and re-created by the script.
#
# Tests are skipped if nvim is not found in PATH.

set(NVIM_TEST_TREE "${CMAKE_CURRENT_BINARY_DIR}/test-artifacts")
set(GIT_WIP_BIN "$<TARGET_FILE:git-wip>")

# Find nvim
find_program(NVIM_EXECUTABLE nvim)

if(NVIM_EXECUTABLE)
    message(STATUS "Found nvim: ${NVIM_EXECUTABLE}")

    foreach(TEST_NAME IN ITEMS test_nvim_single test_nvim_buffers test_nvim_windows test_nvim_background)
        add_test(
            NAME    "nvim/${TEST_NAME}"
            COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/${TEST_NAME}.sh"
        )
        set_tests_properties("nvim/${TEST_NAME}" PROPERTIES
            ENVIRONMENT "GIT_WIP=${GIT_WIP_BIN};NVIM=${NVIM_EXECUTABLE};TEST_TREE=${NVIM_TEST_TREE}"
            TIMEOUT 120
        )
    endforeach()
else()
    message(STATUS "nvim not found - Neovim tests will be skipped")
endif()


================================================
FILE: test/nvim/lib.sh
================================================
# lib.sh -- shared helpers for git-wip Neovim integration tests
#
# Source this file from each test script; do NOT execute directly.
#
# Required environment variables (set by ctest via CMakeLists.txt):
#   GIT_WIP     path to the git-wip binary under test
#   NVIM        path to the nvim binary
#   TEST_TREE   base directory for test artifacts (one subdir per test)
#
# TEST_NAME is derived from the sourcing script's filename (basename without .sh).

set -e

die()  { echo >&2 "ERROR: $*"   ; exit 1 ; }
warn() { echo >&2 "WARNING: $*" ; }
note() { echo >&2 "# $*"        ; }

# ------------------------------------------------------------------------
# Validate environment

[ -z "${GIT_WIP}"   ] && die "GIT_WIP is not set"
[ -x "${GIT_WIP}"   ] || die "GIT_WIP=${GIT_WIP} is not executable"
[ -z "${NVIM}"      ] && die "NVIM is not set"

# Check if nvim is available, skip if not
if ! command -v "${NVIM}" &>/dev/null; then
    note "nvim not found at ${NVIM} - skipping test"
    exit 0
fi

# Get the repo root (parent of test/nvim/)
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"

# Derive test name from the calling script's filename
TEST_NAME="$(basename "$0" .sh)"

# ------------------------------------------------------------------------
# Per-test paths

REPO="$TEST_TREE/$TEST_NAME/repo"
NVIM_CMD="$TEST_TREE/$TEST_NAME/nvim_cmd"
NVIM_OUT="$TEST_TREE/$TEST_NAME/nvim_out"
NVIM_RC="$TEST_TREE/$TEST_NAME/nvim_rc"
NVIM_LOG="$TEST_TREE/$TEST_NAME/nvim_log"

# Clean before running so each run starts fresh; leave artifacts after for debugging
rm -rf "$TEST_TREE/$TEST_NAME"
mkdir -p "$TEST_TREE/$TEST_NAME"

note "Running $TEST_NAME (artifacts in $TEST_TREE/$TEST_NAME)"

# ------------------------------------------------------------------------
# Git helpers (from cli lib.sh)

create_test_repo() {
    rm -rf "$REPO"
    mkdir -p "$REPO"
    cd "$REPO"
    git init
    git config user.email "test@example.com"
    git config user.name "Test User"
    # Force branch name to "master" regardless of init.defaultBranch config
    git checkout -b master
}

# Git helper functions (simplified from cli/lib.sh)
_run() {
    note "$@"
    [ "$(pwd)" = "$REPO" ] || die "expected cwd=$REPO, got $(pwd)"

    set +e
    eval "$@"
    local rc=$?
    set -e
    return $rc
}

run() {
    _run "$@" || die "command failed: $*"
}

# ------------------------------------------------------------------------
# Neovim helpers

# _run_nvim -- Run nvim with sandboxed config, capturing output and exit code
# Usage: _run_nvim [ex-command]..
Download .txt
gitextract_lv7ob25m/

├── .codecov.yml
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── AGENTS.md
├── Attic/
│   ├── README.markdown
│   ├── git-wip
│   └── tests/
│       └── test-git-wip.sh
├── CMakeLists.txt
├── Dockerfile-deb
├── LICENSE
├── README.md
├── cmake/
│   ├── GitVersion.cmake
│   └── GitVersion.sh
├── dependencies.sh
├── dev.sh
├── doc/
│   └── git-wip.txt
├── emacs/
│   ├── git-wip-mode.el
│   └── git-wip.el
├── flake.nix
├── lua/
│   └── git-wip/
│       └── init.lua
├── src/
│   ├── CMakeLists.txt
│   ├── cmd_delete.cpp
│   ├── cmd_delete.hpp
│   ├── cmd_list.cpp
│   ├── cmd_list.hpp
│   ├── cmd_log.cpp
│   ├── cmd_log.hpp
│   ├── cmd_save.cpp
│   ├── cmd_save.hpp
│   ├── cmd_status.cpp
│   ├── cmd_status.hpp
│   ├── color.cpp
│   ├── color.hpp
│   ├── command.hpp
│   ├── git_guards.hpp
│   ├── git_helpers.hpp
│   ├── main.cpp
│   ├── print_compat.hpp
│   ├── string_helpers.hpp
│   └── wip_helpers.hpp
├── sublime/
│   └── gitwip.py
├── test/
│   ├── cli/
│   │   ├── CMakeLists.txt
│   │   ├── lib.sh
│   │   ├── profile.sh
│   │   ├── test_delete.sh
│   │   ├── test_help.sh
│   │   ├── test_legacy.sh
│   │   ├── test_list.sh
│   │   ├── test_save_file.sh
│   │   ├── test_save_subdir.sh
│   │   ├── test_spaces.sh
│   │   ├── test_status.sh
│   │   ├── test_status2.sh
│   │   └── test_status_ref.sh
│   ├── nvim/
│   │   ├── CMakeLists.txt
│   │   ├── lib.sh
│   │   ├── test_nvim_background.sh
│   │   ├── test_nvim_buffers.sh
│   │   ├── test_nvim_single.sh
│   │   └── test_nvim_windows.sh
│   └── unit/
│       ├── CMakeLists.txt
│       ├── test_git_helpers.cpp
│       ├── test_repo_fixture.hpp
│       ├── test_string_helpers.cpp
│       └── test_wip_helpers.cpp
└── vim/
    └── plugin/
        └── git-wip.vim
Download .txt
SYMBOL INDEX (142 symbols across 20 files)

FILE: src/cmd_delete.cpp
  function delete_ref (line 16) | int delete_ref(git_repository *repo, const std::string &wip_ref) {

FILE: src/cmd_delete.hpp
  class DeleteCmd (line 6) | class DeleteCmd : public Command {
    method name (line 8) | std::string name() const override {
    method desc (line 12) | std::string desc() const override {

FILE: src/cmd_list.hpp
  class ListCmd (line 7) | class ListCmd : public Command {
    method name (line 9) | std::string name() const override {
    method desc (line 13) | std::string desc() const override {

FILE: src/cmd_log.hpp
  class LogCmd (line 6) | class LogCmd : public Command {
    method name (line 8) | std::string name() const override {
    method desc (line 12) | std::string desc() const override {

FILE: src/cmd_save.hpp
  class SaveCmd (line 6) | class SaveCmd : public Command {
    method name (line 8) | std::string name() const override {
    method desc (line 12) | std::string desc() const override {

FILE: src/cmd_status.hpp
  class StatusCmd (line 6) | class StatusCmd : public Command {
    method name (line 8) | std::string name() const override {
    method desc (line 12) | std::string desc() const override {

FILE: src/color.cpp
  function lower_copy (line 15) | std::string lower_copy(std::string s) {
  function stdout_is_tty (line 22) | bool stdout_is_tty() {
  function color_init (line 28) | void color_init() {
  function color_branch (line 78) | std::string color_branch(std::string_view branch_name) {
  function color_wip_branch (line 82) | std::string color_wip_branch(std::string_view wip_branch_name) {
  function color_commit_hash (line 86) | std::string color_commit_hash(std::string_view commit_hash) {

FILE: src/color.hpp
  class Color (line 10) | class Color {

FILE: src/command.hpp
  class Command (line 6) | class Command {

FILE: src/git_guards.hpp
  type GitLibGuard (line 14) | struct GitLibGuard {
    method GitLibGuard (line 15) | GitLibGuard() { git_libgit2_init(); }
    method GitLibGuard (line 18) | GitLibGuard(const GitLibGuard &) = delete;
    method GitLibGuard (line 19) | GitLibGuard &operator=(const GitLibGuard &) = delete;
  type GitRepoGuard (line 22) | struct GitRepoGuard {
    method git_repository (line 25) | git_repository       *get()       { return m_repo; }
    method git_repository (line 26) | git_repository const *get() const { return m_repo; }
    method git_repository (line 27) | git_repository      **ptr()       { return &m_repo; }
  type GitIndexGuard (line 30) | struct GitIndexGuard {
    method git_index (line 33) | git_index       *get()       { return m_idx; }
    method git_index (line 34) | git_index const *get() const { return m_idx; }
    method git_index (line 35) | git_index      **ptr()       { return &m_idx; }
  type GitTreeGuard (line 38) | struct GitTreeGuard {
    method git_tree (line 41) | git_tree       *get()       { return m_tree; }
    method git_tree (line 42) | git_tree const *get() const { return m_tree; }
    method git_tree (line 43) | git_tree      **ptr()       { return &m_tree; }
  type GitCommitGuard (line 46) | struct GitCommitGuard {
    method git_commit (line 49) | git_commit       *get()       { return m_commit; }
    method git_commit (line 50) | git_commit const *get() const { return m_commit; }
    method git_commit (line 51) | git_commit      **ptr()       { return &m_commit; }
  type GitReferenceGuard (line 54) | struct GitReferenceGuard {
    method git_reference (line 57) | git_reference       *get()       { return m_ref; }
    method git_reference (line 58) | git_reference const *get() const { return m_ref; }
    method git_reference (line 59) | git_reference      **ptr()       { return &m_ref; }
  type GitSignatureGuard (line 62) | struct GitSignatureGuard {
    method git_signature (line 65) | git_signature       *get()       { return m_sig; }
    method git_signature (line 66) | git_signature const *get() const { return m_sig; }
    method git_signature (line 67) | git_signature      **ptr()       { return &m_sig; }
  type GitRevwalkGuard (line 70) | struct GitRevwalkGuard {
    method git_revwalk (line 73) | git_revwalk       *get()       { return m_walk; }
    method git_revwalk (line 74) | git_revwalk const *get() const { return m_walk; }
    method git_revwalk (line 75) | git_revwalk      **ptr()       { return &m_walk; }
  function git_error_str (line 81) | inline std::string git_error_str() {

FILE: src/git_helpers.hpp
  function oid_to_hex (line 22) | inline std::string oid_to_hex(const git_oid *oid) {
  function oid_to_short_hex (line 31) | inline std::string oid_to_short_hex(const git_oid *oid) {
  type BranchNames (line 46) | struct BranchNames {
  function resolve_branch_names (line 63) | inline std::optional<BranchNames> resolve_branch_names(
  function find_refs (line 99) | inline std::vector<std::string> find_refs(
  function resolve_oid (line 127) | inline std::optional<git_oid> resolve_oid(git_repository *repo,
  function ensure_reflog_dir (line 142) | inline void ensure_reflog_dir(git_repository *repo, const std::string &w...

FILE: src/main.cpp
  function print_main_help (line 21) | void print_main_help(const std::vector<std::unique_ptr<Command>>& comman...
  function main (line 30) | int main(int argc, char *argv[]) {

FILE: src/print_compat.hpp
  type std (line 27) | namespace std {
    function print (line 30) | void print(fmt::format_string<Args...> fmt_str, Args &&...args) {
    function print (line 35) | void print(std::ostream &os, fmt::format_string<Args...> fmt_str, Args...
    function println (line 40) | void println(fmt::format_string<Args...> fmt_str, Args &&...args) {
    function println (line 45) | void println(std::ostream &os, fmt::format_string<Args...> fmt_str, Ar...

FILE: src/string_helpers.hpp
  function strip_prefix (line 18) | inline std::string strip_prefix(std::string_view s, std::string_view pre...
  function strip_prefix_inplace (line 30) | inline bool strip_prefix_inplace(std::string &s, std::string_view prefix) {
  function first_line (line 43) | inline std::string first_line(const char *msg) {
  function relative_time (line 57) | inline std::string relative_time(std::int64_t epoch_seconds) {

FILE: src/wip_helpers.hpp
  function wip_parent_oid (line 28) | inline std::optional<git_oid> wip_parent_oid(
  function collect_wip_commits (line 61) | inline std::optional<std::vector<git_oid>> collect_wip_commits(

FILE: sublime/gitwip.py
  class GitWipAutoCommand (line 7) | class GitWipAutoCommand(sublime_plugin.EventListener):
    method on_post_save_async (line 9) | def on_post_save_async(self, view):

FILE: test/unit/test_git_helpers.cpp
  function TEST (line 14) | TEST(OidToHex, FullLengthIs40) {
  function TEST (line 25) | TEST(OidToHex, ShortLengthIs7) {
  function TEST (line 35) | TEST(OidToHex, ShortIsPrefixOfFull) {
  function TEST (line 49) | TEST(ResolveBranchNames, FreshRepoUnbornHead) {
  function TEST (line 57) | TEST(ResolveBranchNames, AfterFirstCommit) {
  function TEST (line 69) | TEST(ResolveBranchNames, DetachedHead) {
  function TEST (line 81) | TEST(ResolveBranchNames, ExplicitBranchName) {
  function TEST (line 93) | TEST(ResolveBranchNames, ExplicitWipShortName) {
  function TEST (line 105) | TEST(ResolveBranchNames, ExplicitHeadsRef) {
  function TEST (line 117) | TEST(ResolveBranchNames, ExplicitWipRef) {
  function TEST (line 129) | TEST(ResolveBranchNames, StripsOnlyOnePrefix) {
  function TEST (line 145) | TEST(ResolveOid, ExistingRef) {
  function TEST (line 155) | TEST(ResolveOid, MissingRefReturnsNullopt) {
  function TEST (line 164) | TEST(ResolveOid, WipRefAfterCreation) {
  function TEST (line 180) | TEST(EnsureReflogDir, CreatesDirectoryAndFile) {
  function TEST (line 195) | TEST(EnsureReflogDir, IdempotentSecondCall) {
  function TEST (line 210) | TEST(EnsureReflogDir, NestedBranchName) {
  function TEST (line 227) | TEST(FindRefs, FindsWipRefsByPrefix) {
  function TEST (line 244) | TEST(FindRefs, FindsHeadsRefsByPrefix) {

FILE: test/unit/test_repo_fixture.hpp
  class TestRepo (line 39) | class TestRepo {
    method TestRepo (line 44) | TestRepo() {
    method TestRepo (line 57) | explicit TestRepo(const std::string &name) { init(name); }
    method init (line 60) | void init(const std::string &name) {
    method git_repository (line 94) | git_repository *repo() { return m_repo.get(); }
    method write_file (line 99) | void write_file(const std::string &rel_path, const std::string &conten...
    method git_oid (line 108) | git_oid commit(const std::string &message) {
    method create_ref (line 156) | void create_ref(const std::string &ref_name, const git_oid &oid) {
    method advance_head (line 165) | void advance_head(const git_oid &oid) {
    method last_error (line 174) | static std::string last_error() {

FILE: test/unit/test_string_helpers.cpp
  function TEST (line 12) | TEST(StripPrefix, RemovesPresentPrefix) {
  function TEST (line 16) | TEST(StripPrefix, LeavesStringUnchangedWhenPrefixAbsent) {
  function TEST (line 20) | TEST(StripPrefix, EmptyPrefix) {
  function TEST (line 24) | TEST(StripPrefix, PrefixEqualsString) {
  function TEST (line 28) | TEST(StripPrefix, EmptyString) {
  function TEST (line 32) | TEST(StripPrefix, WipRefUnchanged) {
  function TEST (line 40) | TEST(StripPrefixInplace, RemovesPresentPrefixAndReturnsTrue) {
  function TEST (line 46) | TEST(StripPrefixInplace, LeavesStringUnchangedWhenPrefixAbsent) {
  function TEST (line 56) | TEST(FirstLine, NullReturnsEmpty) {
  function TEST (line 60) | TEST(FirstLine, SingleLine) {
  function TEST (line 64) | TEST(FirstLine, MultiLine) {
  function TEST (line 68) | TEST(FirstLine, EmptyString) {
  function TEST (line 72) | TEST(FirstLine, LeadingNewline) {
  function TEST (line 76) | TEST(FirstLine, TrailingNewline) {
  function seconds_ago (line 91) | std::int64_t seconds_ago(std::int64_t delta) {
  function TEST (line 99) | TEST(RelativeTime, JustNow) {
  function TEST (line 104) | TEST(RelativeTime, OneMinuteAgo) {
  function TEST (line 109) | TEST(RelativeTime, OneHourAgo) {
  function TEST (line 114) | TEST(RelativeTime, OneDayAgo) {
  function TEST (line 119) | TEST(RelativeTime, TwoWeeksAgo) {
  function TEST (line 124) | TEST(RelativeTime, ThreeMonthsAgo) {
  function TEST (line 129) | TEST(RelativeTime, TwoYearsAgo) {
  function TEST (line 134) | TEST(RelativeTime, FutureTimestampClampsToZero) {
  function TEST (line 140) | TEST(RelativeTime, SingularSecond) {
  function TEST (line 146) | TEST(RelativeTime, SingularMinute) {

FILE: test/unit/test_wip_helpers.cpp
  function TEST (line 11) | TEST(WipParentOid, NoWipBranch) {
  function TEST (line 22) | TEST(WipParentOid, WorkBranchUnchanged) {
  function TEST (line 40) | TEST(WipParentOid, WorkBranchAdvanced) {
  function TEST (line 64) | TEST(WipParentOid, UnrelatedHistories) {
  function TEST (line 86) | TEST(CollectWipCommits, SingleCommitIsWorkHead) {
  function TEST (line 98) | TEST(CollectWipCommits, OneWipCommit) {
  function TEST (line 115) | TEST(CollectWipCommits, ThreeWipCommits) {
  function TEST (line 135) | TEST(CollectWipCommits, WorkBranchAdvanced) {
  function TEST (line 157) | TEST(CollectWipCommits, WipSameAsWork) {
Condensed preview — 66 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (251K chars).
[
  {
    "path": ".codecov.yml",
    "chars": 435,
    "preview": "codecov:\n  branch: master\n  require_ci_to_pass: yes\n  notify:\n    wait_for_ci: yes\n\ncoverage:\n  precision: 2\n  round: do"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 8013,
    "preview": "name: CI\n\non:\n  push:\n    branches: [\"**\"]\n  pull_request:\n    branches: [\"**\"]\n\njobs:\n  build:\n    name: ${{ matrix.os "
  },
  {
    "path": ".gitignore",
    "chars": 550,
    "preview": "*~\nbuild/\n.cache/\n.opencode/\n/git-wip\n\ncoverage-report/\ncoverage.xml\ncoverage.info\n\n### C++\n# Prerequisites\n*.d\n\n# Compi"
  },
  {
    "path": "AGENTS.md",
    "chars": 15460,
    "preview": "# AGENTS.md - git-wip C++ Rewrite\n\n## Guidance from user\n\n- Use c++23 best practices.  Use CamelCase for classes, use sn"
  },
  {
    "path": "Attic/README.markdown",
    "chars": 5617,
    "preview": "# About\n\ngit-wip is a script that will manage Work In Progress (or WIP) branches.\nWIP branches are mostly throw away but"
  },
  {
    "path": "Attic/git-wip",
    "chars": 7162,
    "preview": "#!/usr/bin/env bash\n#\n# Copyright Bart Trojanowski <bart@jukie.net>\n#\n# git-wip is a script that will manage Work In Pro"
  },
  {
    "path": "Attic/tests/test-git-wip.sh",
    "chars": 3966,
    "preview": "#!/bin/bash\n\nset -e\n\n# split the script name into BASE directory, and the name it calls itSELF\nBASE=$(realpath ${0%/*})\n"
  },
  {
    "path": "CMakeLists.txt",
    "chars": 3851,
    "preview": "cmake_minimum_required(VERSION 3.26)\nproject(git-wip LANGUAGES CXX)\n\nset(CMAKE_CXX_STANDARD 23)\nset(CMAKE_CXX_STANDARD_R"
  },
  {
    "path": "Dockerfile-deb",
    "chars": 1150,
    "preview": "# vim: set ft=dockerfile\nFROM debian:testing\n\nWORKDIR /home/git-wip\nCOPY dependencies.sh dependencies.sh\n\n# Install nece"
  },
  {
    "path": "LICENSE",
    "chars": 18092,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Fr"
  },
  {
    "path": "README.md",
    "chars": 10136,
    "preview": "# About\n\n[![CI](https://github.com/bartman/git-wip/actions/workflows/ci.yml/badge.svg)](https://github.com/bartman/git-w"
  },
  {
    "path": "cmake/GitVersion.cmake",
    "chars": 1454,
    "preview": "# GitVersion.cmake - Integration for GitVersion.sh\n#\n# Usage:\n#   include(GitVersion)\n#   gitversion_generate(PREFIX GIT"
  },
  {
    "path": "cmake/GitVersion.sh",
    "chars": 662,
    "preview": "#!/usr/bin/env bash\n# GitVersion.sh - Generate version header from git describe\n# Usage: GitVersion.sh PREFIX OUTPUT\n\nse"
  },
  {
    "path": "dependencies.sh",
    "chars": 9467,
    "preview": "#!/bin/bash\nset -e\n\nSUDO=sudo\n[ \"$(id -u)\" = 0 ] && SUDO=\n\nfunction die() {\n    echo >&2 \"ERROR: $*\"\n    exit 1\n}\n\n# ---"
  },
  {
    "path": "dev.sh",
    "chars": 6061,
    "preview": "#!/usr/bin/env bash\nset -e\n\nPROJECT=git-wip\nTARGET=\n\nHOSTNAME=\"$(hostname)\"\nUSERNAME=\"$(id -u -n)\"\nUSER_UID=\"$(id -u)\"\nU"
  },
  {
    "path": "doc/git-wip.txt",
    "chars": 4737,
    "preview": "*git-wip.txt*           Git WIP (Work In Progress) for Neovim\n\n========================================================="
  },
  {
    "path": "emacs/git-wip-mode.el",
    "chars": 2848,
    "preview": ";;; git-wip-mode.el --- Use git-wip to record every buffer save\n\n;; Copyright (C) 2013  Jerome Baum\n\n;; Author: Jerome B"
  },
  {
    "path": "emacs/git-wip.el",
    "chars": 448,
    "preview": "(defun git-wip-wrapper () \n  (interactive)\n  (let ((file-arg (shell-quote-argument (buffer-file-name))))\n    (shell-comm"
  },
  {
    "path": "flake.nix",
    "chars": 1997,
    "preview": "{\n  description = \"git-wip — Work In Progress branch manager\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nix"
  },
  {
    "path": "lua/git-wip/init.lua",
    "chars": 5438,
    "preview": "-- this is a Neovim plugin that launches git-wip save on every buffer write\nlocal vim = vim\nlocal M = {}\n\n-- Detect Neov"
  },
  {
    "path": "src/CMakeLists.txt",
    "chars": 2251,
    "preview": "# Generate version header\ninclude(${CMAKE_SOURCE_DIR}/cmake/GitVersion.cmake)\ngitversion_generate(PREFIX GIT_WIP_ OUTPUT"
  },
  {
    "path": "src/cmd_delete.cpp",
    "chars": 3804,
    "preview": "#include \"cmd_delete.hpp\"\n#include \"color.hpp\"\n#include \"git_guards.hpp\"\n#include \"git_helpers.hpp\"\n#include \"string_hel"
  },
  {
    "path": "src/cmd_delete.hpp",
    "chars": 301,
    "preview": "#pragma once\n\n#include \"command.hpp\"\n#include <string>\n\nclass DeleteCmd : public Command {\npublic:\n    std::string name("
  },
  {
    "path": "src/cmd_list.cpp",
    "chars": 2646,
    "preview": "#include \"cmd_list.hpp\"\n\n#include \"color.hpp\"\n#include \"git_guards.hpp\"\n#include \"git_helpers.hpp\"\n#include \"wip_helpers"
  },
  {
    "path": "src/cmd_list.hpp",
    "chars": 300,
    "preview": "#pragma once\n\n#include \"command.hpp\"\n\n#include <string>\n\nclass ListCmd : public Command {\npublic:\n    std::string name()"
  },
  {
    "path": "src/cmd_log.cpp",
    "chars": 5275,
    "preview": "#include \"cmd_log.hpp\"\n#include \"git_guards.hpp\"\n#include \"git_helpers.hpp\"\n\n#include \"spdlog/spdlog.h\"\n\n#include <cstdl"
  },
  {
    "path": "src/cmd_log.hpp",
    "chars": 299,
    "preview": "#pragma once\n\n#include \"command.hpp\"\n#include <string>\n\nclass LogCmd : public Command {\npublic:\n    std::string name() c"
  },
  {
    "path": "src/cmd_save.cpp",
    "chars": 12807,
    "preview": "#include \"cmd_save.hpp\"\n#include \"git_guards.hpp\"\n#include \"git_helpers.hpp\"\n#include \"string_helpers.hpp\"\n#include \"wip"
  },
  {
    "path": "src/cmd_save.hpp",
    "chars": 299,
    "preview": "#pragma once\n\n#include \"command.hpp\"\n#include <string>\n\nclass SaveCmd : public Command {\npublic:\n    std::string name() "
  },
  {
    "path": "src/cmd_status.cpp",
    "chars": 5370,
    "preview": "#include \"cmd_status.hpp\"\n#include \"color.hpp\"\n#include \"git_guards.hpp\"\n#include \"git_helpers.hpp\"\n#include \"string_hel"
  },
  {
    "path": "src/cmd_status.hpp",
    "chars": 301,
    "preview": "#pragma once\n\n#include \"command.hpp\"\n#include <string>\n\nclass StatusCmd : public Command {\npublic:\n    std::string name("
  },
  {
    "path": "src/color.cpp",
    "chars": 1927,
    "preview": "#include \"color.hpp\"\n\n#include <algorithm>\n#include <cctype>\n#include <cstdio>\n#include <cstdlib>\n#include <format>\n\n#in"
  },
  {
    "path": "src/color.hpp",
    "chars": 485,
    "preview": "#pragma once\n\n#include <string>\n#include <string_view>\n\nextern bool g_wip_color;\n\nvoid color_init();\n\nclass Color {\npubl"
  },
  {
    "path": "src/command.hpp",
    "chars": 381,
    "preview": "#pragma once\n\n#include <string>\n#include <vector>\n\nclass Command {\npublic:\n    // Pure virtual methods to be implemented"
  },
  {
    "path": "src/git_guards.hpp",
    "chars": 3082,
    "preview": "#pragma once\n\n#include <git2.h>\n#include <string>\n\n// ------------------------------------------------------------------"
  },
  {
    "path": "src/git_helpers.hpp",
    "chars": 5829,
    "preview": "#pragma once\n\n// git_helpers.hpp — thin wrappers around libgit2 operations that are repeated\n// across multiple commands"
  },
  {
    "path": "src/main.cpp",
    "chars": 3346,
    "preview": "#include \"command.hpp\"\n#include \"color.hpp\"\n#include \"cmd_delete.hpp\"\n#include \"cmd_list.hpp\"\n#include \"cmd_log.hpp\"\n#in"
  },
  {
    "path": "src/print_compat.hpp",
    "chars": 1439,
    "preview": "#pragma once\n\n// print_compat.hpp — portable std::print / std::println\n//\n// C++23's <print> (P2093) is not available on"
  },
  {
    "path": "src/string_helpers.hpp",
    "chars": 3245,
    "preview": "#pragma once\n\n// string_helpers.hpp — pure string/time utility functions with no git dependency.\n// All functions are in"
  },
  {
    "path": "src/wip_helpers.hpp",
    "chars": 3221,
    "preview": "#pragma once\n\n// wip_helpers.hpp — higher-level helpers that encode the core git-wip\n// branching logic shared between t"
  },
  {
    "path": "sublime/gitwip.py",
    "chars": 900,
    "preview": "import sublime_plugin\nfrom subprocess import Popen, PIPE, STDOUT\nimport os\nimport sublime\nimport copy\n\nclass GitWipAutoC"
  },
  {
    "path": "test/cli/CMakeLists.txt",
    "chars": 867,
    "preview": "# test/cli/CMakeLists.txt\n#\n# Registers each cli integration test as a ctest entry.\n#\n# TEST_TREE is placed inside the c"
  },
  {
    "path": "test/cli/lib.sh",
    "chars": 3409,
    "preview": "# lib.sh -- shared helpers for git-wip legacy integration tests\n#\n# Source this file from each test script; do NOT execu"
  },
  {
    "path": "test/cli/profile.sh",
    "chars": 3421,
    "preview": "#!/usr/bin/env bash\n# profile.sh -- performance profiling test for git-wip save\n# Run manually: GIT_WIP=/path/to/git-wip"
  },
  {
    "path": "test/cli/test_delete.sh",
    "chars": 1608,
    "preview": "#!/usr/bin/env bash\nsource \"$(dirname \"$0\")/lib.sh\"\n\ncreate_test_repo\nRUN git config user.email \"test@example.com\"\nRUN g"
  },
  {
    "path": "test/cli/test_help.sh",
    "chars": 502,
    "preview": "#!/usr/bin/env bash\n\nsource \"$(dirname \"$0\")/lib.sh\"\n\ncreate_test_repo\n\n# Test main help commands\n\n_RUN \"$GIT_WIP\" help "
  },
  {
    "path": "test/cli/test_legacy.sh",
    "chars": 1871,
    "preview": "#!/usr/bin/env bash\nsource \"$(dirname \"$0\")/lib.sh\"\n\n# these tests are here to make sure we behave the same way as the l"
  },
  {
    "path": "test/cli/test_list.sh",
    "chars": 891,
    "preview": "#!/usr/bin/env bash\nsource \"$(dirname \"$0\")/lib.sh\"\n\ncreate_test_repo\nRUN git config user.email \"test@example.com\"\nRUN g"
  },
  {
    "path": "test/cli/test_save_file.sh",
    "chars": 3509,
    "preview": "#!/usr/bin/env bash\nsource \"$(dirname \"$0\")/lib.sh\"\n\ncreate_test_repo\nRUN git config user.email \"test@example.com\"\nRUN g"
  },
  {
    "path": "test/cli/test_save_subdir.sh",
    "chars": 2051,
    "preview": "#!/usr/bin/env bash\nsource \"$(dirname \"$0\")/lib.sh\"\n\ncreate_test_repo\nRUN git config user.email \"test@example.com\"\nRUN g"
  },
  {
    "path": "test/cli/test_spaces.sh",
    "chars": 481,
    "preview": "#!/usr/bin/env bash\nsource \"$(dirname \"$0\")/lib.sh\"\n\ncreate_test_repo\nRUN \"echo 1 >\\\"s p a c e s\\\"\"\nRUN git add \"\\\"s p a"
  },
  {
    "path": "test/cli/test_status.sh",
    "chars": 3048,
    "preview": "#!/usr/bin/env bash\nsource \"$(dirname \"$0\")/lib.sh\"\n\ncreate_test_repo\nRUN git config user.email \"test@example.com\"\nRUN g"
  },
  {
    "path": "test/cli/test_status2.sh",
    "chars": 1190,
    "preview": "#!/usr/bin/env bash\nsource \"$(dirname \"$0\")/lib.sh\"\n\ncreate_test_repo\nRUN git config user.email \"test@example.com\"\nRUN g"
  },
  {
    "path": "test/cli/test_status_ref.sh",
    "chars": 1439,
    "preview": "#!/usr/bin/env bash\nsource \"$(dirname \"$0\")/lib.sh\"\n\ncreate_test_repo\nRUN git config user.email \"test@example.com\"\nRUN g"
  },
  {
    "path": "test/nvim/CMakeLists.txt",
    "chars": 1136,
    "preview": "# test/nvim/CMakeLists.txt\n#\n# Registers each Neovim integration test as a ctest entry.\n#\n# TEST_TREE is placed inside t"
  },
  {
    "path": "test/nvim/lib.sh",
    "chars": 4670,
    "preview": "# lib.sh -- shared helpers for git-wip Neovim integration tests\n#\n# Source this file from each test script; do NOT execu"
  },
  {
    "path": "test/nvim/test_nvim_background.sh",
    "chars": 1787,
    "preview": "#!/usr/bin/env bash\n# test_nvim_background.sh -- Test git-wip with background=true (async execution)\nsource \"$(dirname \""
  },
  {
    "path": "test/nvim/test_nvim_buffers.sh",
    "chars": 2869,
    "preview": "#!/usr/bin/env bash\n# test_nvim_buffers.sh -- Test git-wip with multiple buffers\nsource \"$(dirname \"$0\")/lib.sh\"\n\n# ----"
  },
  {
    "path": "test/nvim/test_nvim_single.sh",
    "chars": 2634,
    "preview": "#!/usr/bin/env bash\n# test_nvim_single.sh -- Test git-wip with single file saves\nsource \"$(dirname \"$0\")/lib.sh\"\n\n# ----"
  },
  {
    "path": "test/nvim/test_nvim_windows.sh",
    "chars": 2369,
    "preview": "#!/usr/bin/env bash\n# test_nvim_windows.sh -- Test git-wip with multiple windows\n# Tests window-related operations in Ne"
  },
  {
    "path": "test/unit/CMakeLists.txt",
    "chars": 2281,
    "preview": "find_package(GTest REQUIRED)\n\n# Directory where test repos are created (inside the cmake binary tree)\nset(UNIT_TEST_TMPD"
  },
  {
    "path": "test/unit/test_git_helpers.cpp",
    "chars": 8156,
    "preview": "#include \"git_helpers.hpp\"\n#include \"test_repo_fixture.hpp\"\n\n#include <gtest/gtest.h>\n\n#include <filesystem>\n#include <s"
  },
  {
    "path": "test/unit/test_repo_fixture.hpp",
    "chars": 6832,
    "preview": "#pragma once\n\n// test_repo_fixture.hpp — helpers for creating throwaway git repositories\n// inside the cmake binary dire"
  },
  {
    "path": "test/unit/test_string_helpers.cpp",
    "chars": 4789,
    "preview": "#include \"string_helpers.hpp\"\n\n#include <gtest/gtest.h>\n\n#include <chrono>\n#include <ctime>\n\n// ------------------------"
  },
  {
    "path": "test/unit/test_wip_helpers.cpp",
    "chars": 5780,
    "preview": "#include \"wip_helpers.hpp\"\n#include \"test_repo_fixture.hpp\"\n\n#include <gtest/gtest.h>\n\n// ------------------------------"
  },
  {
    "path": "vim/plugin/git-wip.vim",
    "chars": 2262,
    "preview": "\" ---------------------------------------------------------------------------\n\" this is a Vim plugin that launches git-w"
  }
]

About this extraction

This page contains the full source code of the bartman/git-wip GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 66 files (229.4 KB), approximately 61.7k tokens, and a symbol index with 142 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!