Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,51 @@ jobs:
build-a/Testing/
retention-days: 7

nix:
name: Nix (dev shell + flake build)

runs-on: ubuntu-latest

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

- name: Install Nix
uses: cachix/install-nix-action@v31
with:
extra_nix_config: |
experimental-features = nix-command flakes
accept-flake-config = true

- 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 inside dev shell (make / make test)
run: |
nix develop --command bash -c '
set -e
make
make test
'

- name: Build via flake (nix build)
run: nix build --print-build-logs

- name: Verify --version of nix-built binary
run: |
set -e
./result/bin/git-wip --version
# Sanity-check: must start with the contents of the VERSION file.
ver="$(./result/bin/git-wip --version)"
base="$(cat VERSION)"
case "$ver" in
"$base"-*) echo "OK: version '$ver' starts with '$base-'" ;;
*) echo "FAIL: version '$ver' does not start with '$base-'" >&2; exit 1 ;;
esac

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

Expand Down
144 changes: 144 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,150 @@ The Neovim Lua plugin supports the following configuration options (set via `opt

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

## Versioning

The `git-wip --version` string is generated at build time and embedded in the
binary as the C string macro `GIT_WIP_VERSION` (defined in
`build/git_wip_version.h`, consumed in `src/main.cpp`).

### Version string format

```
{VERSION}-{YYYYMMDD}-g{HASH}[-dirty]
```

- `VERSION` — contents of the committed `VERSION` file at the top of the
source tree (currently `v0.3`). This is the canonical, human-managed
version and is bumped by `tag-new-release.sh`.
- `YYYYMMDD` — committer date of `HEAD` (for `make`) or
`self.lastModifiedDate` truncated to a date (for `nix build`).
- `HASH` — short hash of `HEAD` / `self.shortRev`.
- `-dirty` — appended when the working tree has uncommitted changes (`make`
path) or when the flake source is dirty (Nix path: `self.rev` absent).

Special fallback for `make` builds with no `.git` directory available:

```
{VERSION}-unknown
```

The format intentionally does **not** match `git describe`'s
`vX.Y-<commits-since-tag>-g<hash>` shape, because `git describe` cannot run
inside the Nix sandbox (`.git` is stripped from the source tree). The
date+hash form is monotonic, encodes the commit, and works identically under
all three build paths.

### Files involved

| File | Role |
|---|---|
| `VERSION` | Source of truth. One line, e.g. `v0.3`. Committed. |
| `tag-new-release.sh` | Bumps `VERSION`, commits, creates annotated tag. |
| `cmake/GitVersion.sh` | Generates `git_wip_version.h` for `make` builds. Reads `VERSION`, queries git. |
| `cmake/GitVersion.cmake` | CMake wrapper around `GitVersion.sh`. Adds custom command + target `gitversion`. |
| `src/CMakeLists.txt` | Wires `gitversion` into the `git-wip` executable. Honours `USE_GIT_WIP_VERSION_H=<path>` to use a pre-generated header (Nix path). |
| `flake.nix` | Computes the version string from `lib.fileContents ./VERSION`, `self.lastModifiedDate`, and `self.shortRev` / `self.dirtyShortRev`. Passes it to `nix/package.nix`. |
| `nix/package.nix` | In `preConfigure`, writes `build/git_wip_version.h` from the injected `version` argument and points CMake at it via `-DUSE_GIT_WIP_VERSION_H=...`. |

### Build paths and what they produce

| Build invocation | Where version comes from | Example output |
|---|---|---|
| `make` (Debian / NixOS dev shell) | `cmake/GitVersion.sh` runs `git log -1 --format=%cd` + `git rev-parse --short HEAD` | `v0.3-20260518-g93b99ef` |
| `make` with no `.git` (extracted tarball) | `cmake/GitVersion.sh` fallback | `v0.3-unknown` |
| `nix build` (flake) | `flake.nix` computes from `self.*`, passed via `package.nix` `preConfigure` | `v0.3-20260518-g93b99ef` |

All three paths produce byte-identical output for the same clean commit.

### Cutting a release

```
./tag-new-release.sh v0.4
```

The script:
1. Refuses to run on a dirty tree.
2. Validates `v0.4` is strictly greater (per `sort -V`) than both the current
`VERSION` file contents and the most recent git tag.
3. Writes `v0.4` to `VERSION`.
4. Commits with message `Release v0.4`.
5. Creates annotated tag `git tag -a -m v0.4 v0.4`.

After running the script, push commit + tag with `git push --follow-tags`.

### Implementation notes

- **No use of `git describe`.** The previous scheme used it; the rewrite
avoids it because (a) it cannot run in the Nix sandbox, and (b) it is
unstable when a tarball is extracted without `.git`.
- **`self.revCount` is intentionally unused.** The GitHub flake fetcher
(`github:` URL) does not populate it; the date+hash scheme works under
both `github:` and `git+https://` fetchers without conditional logic.
- **Dirty detection in Nix** relies on `self ? rev`. A dirty flake source
sets `dirtyRev`/`dirtyShortRev` but not `rev`, so the check
`if self ? rev then "" else "-dirty"` is correct.
- **`USE_GIT_WIP_VERSION_H`** is a CMake variable, not an env var. The
Nix derivation appends `-DUSE_GIT_WIP_VERSION_H=$PWD/build/git_wip_version.h`
to `cmakeFlagsArray` in `preConfigure`. When set and the file exists,
`src/CMakeLists.txt` skips the `GitVersion.sh` invocation entirely.

## Continuous Integration

`.github/workflows/ci.yml` defines three jobs:

### `build` (matrix)

Distro × compiler × build-type matrix. Runs in distro-native containers on
the `ubuntu-latest` runner:

| Distro | Compilers | Build types | Static |
|---|---|---|---|
| `debian:stable` | gcc, clang | Release, Debug | yes |
| `ubuntu:24.04` | gcc, clang | Release, Debug | yes |
| `fedora:latest` | gcc, clang | Release, Debug | no |
| `archlinux:latest` | gcc, clang | Release, Debug | no |

For each cell: `dependencies.sh` installs deps, `make BUILD=build-so` builds
dynamic, `make BUILD=build-so test` runs tests; then (only on distros where
libgit2.a is available) it repeats with `STATIC=1 BUILD=build-a`. On
failure, test artifacts from `build-so/` and `build-a/` are uploaded.

### `nix` (single job, no matrix)

Runs on stock `ubuntu-latest` (no container). Installs Nix via
`cachix/install-nix-action@v31` with `nix-command` + `flakes` enabled.
Compiler and dependency versions are pinned by the flake, so a matrix is
unnecessary. Steps:

1. **Dev shell make build** — `nix develop --command bash -c 'make && make test'`.
Exercises the `cmake/GitVersion.sh` path with the flake's pinned toolchain.
2. **Flake build** — `nix build --print-build-logs`. Exercises the
`flake.nix` → `package.nix` → `preConfigure` version-injection path.
3. **Version sanity check** — runs `./result/bin/git-wip --version` and asserts
the output starts with `<contents of VERSION>-`. Catches regressions in
either the flake's version computation or `package.nix`'s header generation.

This job is the canonical guard against the three known regressions in the
Nix path: (a) `git describe` creeping back in, (b) `.git` being needed inside
the sandbox, (c) `VERSION` not being read.

### `coverage`

Single job on `debian:stable` / gcc / Debug. Runs `make TYPE=Debug coverage`
and uploads the resulting `coverage.info` to Codecov.

### Why no `nixos/nix` container

GitHub Actions' `container:` mechanism requires a node-capable image to host
the runner agent and the JavaScript-based actions (`actions/checkout`,
`actions/cache`, `actions/upload-artifact`). The official `nixos/nix` image
is intentionally minimal and lacks `node` and an FHS layout, breaking those
actions. The supported pattern — used by NixOS/nix itself — is to run on
stock `ubuntu-latest` and install Nix via `cachix/install-nix-action`.
Functionally this is equivalent to a NixOS container for our purposes
because the flake's `mkShell` and `mkDerivation` pin their toolchain
independently of the host distro.

## Test Infrastructure

### test/cli/lib.sh
Expand Down
19 changes: 13 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ 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
)
# use spdlog from system if possible
find_package(spdlog 1.15 QUIET CONFIG)

FetchContent_MakeAvailable(spdlog)
if(spdlog_FOUND)
message(STATUS "Using system spdlog ${spdlog_VERSION}")
else()

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

# Code coverage options — applied AFTER FetchContent so that third-party
# dependencies (spdlog, fmt, …) are NOT instrumented. Mixing gcov versions
Expand Down
67 changes: 65 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,15 @@ Equivalent to `git wip save "WIP"`.

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

Show the version string (from `git describe --tags --dirty=-dirty` at build time).
Show the version string baked in at build time. Format:
`{VERSION}-{YYYYMMDD}-g{HASH}[-dirty]`, where `VERSION` comes from the
committed `VERSION` file, `YYYYMMDD` is the commit date, and `HASH` is the
short commit hash. `-dirty` is appended if the working tree had uncommitted
changes at build time.

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

### `git wip save [<message>] [options] [-- <file>...]`
Expand Down Expand Up @@ -248,6 +252,65 @@ Or copy the binary manually:
$ cp build/src/git-wip ~/bin/
```

### NixOS

`git-wip` ships a flake (`flake.nix`) and a self-contained package definition
(`nix/package.nix`). To install it system-wide on NixOS **without touching
your existing `flake.nix`**, drop a `git-wip.nix` file next to your
`configuration.nix` and add it to `imports`.

In `configuration.nix`:

```nix
imports = [
# ...your other imports...
./git-wip.nix
];
```

In `git-wip.nix`:

```nix
{ config, pkgs, lib, ... }:

let
# Pin to the most recent release. Update both `ref` and `rev` when bumping.
# `rev` must be the full 40-character commit hash; look it up with:
# git ls-remote https://github.com/bartman/git-wip.git refs/tags/v0.3
src = builtins.fetchGit {
url = "https://github.com/bartman/git-wip.git";
ref = "refs/tags/v0.3";
rev = "0000000000000000000000000000000000000000"; # replace with v0.3's full sha
};

# Reproduce the version string shape used by the upstream flake:
# {VERSION}-{YYYYMMDD}-g{HASH}
baseVersion = lib.fileContents (src + "/VERSION");
buildDate = builtins.substring 0 8 (src.lastModifiedDate or "00000000");
shortHash = src.shortRev or "unknown";
version = "${baseVersion}-${buildDate}-g${shortHash}";

git-wip = pkgs.callPackage (src + "/nix/package.nix") {
inherit pkgs version;
};
in
{
environment.systemPackages = [ git-wip ];
}
```

Apply with `sudo nixos-rebuild switch`, then verify:

```sh
$ git-wip --version
v0.3-20260518-g93b99ef
```

To upgrade to a newer release later, bump `ref` to the new tag (e.g.
`refs/tags/v0.4`) and replace `rev` with the full hash of that tag's commit.
Leaving the `rev` value invalid will cause Nix to error out — useful, since
it forces you to consciously pin each release.

---

## Editor integration
Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v0.4
35 changes: 32 additions & 3 deletions cmake/GitVersion.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
#!/usr/bin/env bash
# GitVersion.sh - Generate version header from git describe
# GitVersion.sh - Generate version header from VERSION file + git metadata.
#
# Usage: GitVersion.sh PREFIX OUTPUT
#
# Output version string format:
# {VERSION}-{YYYYMMDD}-g{HASH}[-dirty] when .git is available
# {VERSION}-unknown when .git is not available
#
# VERSION is read from the VERSION file at the top of the source tree.
# YYYYMMDD is the committer date of HEAD.
# HASH is the short hash of HEAD.
# -dirty is appended if the working tree has uncommitted changes.

set -e

Expand All @@ -12,8 +22,27 @@ if [ -z "$PREFIX" ] || [ -z "$OUTPUT" ]; then
exit 1
fi

# Get git describe output
DESCRIBE="$(git describe --tags --dirty=-dirty 2>/dev/null || echo "unknown")"
# Locate the source tree (the directory containing the VERSION file). This
# script normally runs with CWD = source root (set by CMake), but be defensive.
SRC_DIR="${SRC_DIR:-$PWD}"
if [ ! -f "$SRC_DIR/VERSION" ]; then
SRC_DIR="$(cd "$(dirname "$0")/.." && pwd)"
fi

VERSION="$(cat "$SRC_DIR/VERSION" 2>/dev/null || echo unknown)"

if git -C "$SRC_DIR" rev-parse --git-dir >/dev/null 2>&1; then
DATE="$(git -C "$SRC_DIR" log -1 --format=%cd --date=format:%Y%m%d)"
HASH="$(git -C "$SRC_DIR" rev-parse --short HEAD)"
DIRTY=""
if ! git -C "$SRC_DIR" diff --quiet 2>/dev/null \
|| ! git -C "$SRC_DIR" diff --cached --quiet 2>/dev/null; then
DIRTY="-dirty"
fi
DESCRIBE="${VERSION}-${DATE}-g${HASH}${DIRTY}"
else
DESCRIBE="${VERSION}-unknown"
fi

# Generate temporary output file
OUTPUT_TMP="${OUTPUT}.tmp"
Expand Down
3 changes: 3 additions & 0 deletions default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{ pkgs ? import <nixpkgs> {} }:

pkgs.callPackage ./nix/package.nix { }
Loading
Loading