vcs-git 0.8.0

Automate Git from Rust: a typed, async wrapper that drives the real git CLI with its exact behavior, config, and credentials.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# Changelog — vcs-git

All notable changes to the `vcs-git` crate are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this crate adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
This crate is versioned and published independently of the other workspace
crates; tag releases as `vcs-git-v<version>`.

## [Unreleased]

### Added
-

### Changed
-

### Fixed
-

## [0.8.0] - 2026-07-03

### Added
- `MergeCheck` (+ its partial builder `MergeCheckPartial`) — the spec that `is_merged`
  now takes.

### Changed
- **`GitApi::is_merged` takes a `MergeCheck` spec, not two bare `&str` refs
  (breaking).** `is_merged(dir, branch, target)` had two adjacent same-typed refs that
  compiled when transposed and **inverted** the answer (asking "is `target` merged into
  `branch`"). It's now `is_merged(dir, MergeCheck::branch("feature").into_base("main"))`
  — the branch and the base are named across two builder steps, so a swap can't compile
  silently. Emitted `git branch --merged <base>` is unchanged. (`docs/audit-2026-07.md`
  A5.)

### Fixed
-

## [0.7.0] - 2026-07-03

### Added
- Re-export of `processkit::ProcessRunner` and `JobRunner` (`vcs_git::{ProcessRunner,
  JobRunner}`) — so a consumer naming the client's runner type parameter (for
  `with_runner`, or to write a custom `ProcessRunner`) needn't add a direct `processkit`
  dependency. Joins the existing `Error`/`Result`/`ProcessResult` re-exports.
- **`is_am_in_progress`** (a `git am` mailbox-apply is paused — `rebase-apply/applying`)
  and **`am_abort`** (`git am --abort`). A `git am` shares the `rebase-apply/` dir with
  an apply-backend rebase but marks it `applying`; these let a caller detect and abort
  it distinctly. (`docs/audit-2026-07.md` M20.)

### Changed
- Bumped `processkit` to **1.1.0** (workspace floor now `"1"`, was `0.11.0`). Crossing
  processkit's 1.0 makes the re-exported `processkit` types (`Error`/`ProcessResult`/…)
  1.x — **breaking** for a downstream that pins `processkit` `0.x` directly. No
  behaviour change (processkit's text-capture verb is now `output_string`, used
  internally). processkit is semver-stable from 1.0, so future 1.x updates are non-breaking.
- `DiffSpec` is now a re-export of `vcs_diff::DiffSpec` (hoisted to the shared
  crate so `vcs-git`/`vcs-jj` share one definition; `vcs_git::DiffSpec` still
  resolves) and is no longer `#[non_exhaustive]`, so a `match` over it can be
  exhaustive. Requires `vcs-diff` ≥ the version that introduces `DiffSpec`.
- **Renamed `GitApi::fetch_remote_branch``fetch_branch` (breaking).** The
  single-branch fetch (and its `at(dir)` bound form) is now `fetch_branch`, so
  git exposes a consistent `fetch`/`fetch_from`/`fetch_branch` family; the emitted
  `git fetch --quiet origin <refspec>` command is unchanged. Update callers of
  `fetch_remote_branch` to `fetch_branch`.
- **Every git client now scrubs the inherited repo-redirector env vars**
  (`GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE`, `GIT_COMMON_DIR`,
  `GIT_OBJECT_DIRECTORY`, `GIT_ALTERNATE_OBJECT_DIRECTORIES`, `GIT_NAMESPACE`), not
  just [`harden()`]Git::harden. A `GIT_DIR` leaking from the parent process (e.g.
  running inside a git hook) can no longer silently retarget commands at a
  *different* repository than the bound `dir`. (`docs/audit-2026-07.md` H4.)
- **`harden()` now documents its git ≥ 2.31 requirement prominently.** On older git
  the hook/`fsmonitor`/`sshCommand` config-pins silently no-op (they ride
  `GIT_CONFIG_COUNT`, added in 2.31); the doc now says so and tells you to check
  `capabilities().version` (major/minor) yourself, since there is no built-in 2.31
  gate yet. (`docs/audit-2026-07.md` H3.)

### Fixed
- **`checkout` can no longer silently discard unstaged edits.** It now passes a
  trailing `--`, so a `reference` that doesn't resolve as a ref but names a tracked
  path errors instead of falling into git's *pathspec* mode and restoring that path
  from the index (which reverted unstaged edits and returned `Ok`).
  (`docs/audit-2026-07.md` C2.)
- **`conflict::parse_conflicts` no longer rejects marker-like content.** A
  `=======`/`>>>>>>>` run *outside* a conflict region (a Markdown/RST setext
  underline, a divider banner, a quoted email) is kept as text instead of erroring,
  so programmatic conflict resolution works on files that merely contain
  marker-like lines. Only a genuinely broken region (an opener with no
  separator/terminator) still errors. (`docs/audit-2026-07.md` H6.)
- **`with_retry` lock-contention retry now fires on a non-English runner.** The git
  `is_lock_contention` marker is the locale-stable `index.lock` path fragment (not
  the translated `': File exists'` suffix), with a `refs/` guard that still excludes
  per-ref locks (unsafe to retry mid-way through a multi-ref push/fetch).
  (`docs/audit-2026-07.md` H2.)
- **`show_file` and `diff_text` no longer strip trailing bytes.** They return git's
  output **verbatim** (via `run_untrimmed`) instead of `trim_end`, so a blob's
  trailing newline(s) survive a read-modify-write round-trip and a diff's last hunk
  stays in sync with its `@@` line count. (Behavior change: a caller that relied on
  the old trimming should trim itself.) (`docs/audit-2026-07.md` H7.)
- **A `fetch` that times out is no longer retried** (inherited from cli-support's
  `is_transient_fetch_error` change). A timeout already spent the per-client deadline,
  so the old 3× fetch-retry blocked ≈ 3× the configured ceiling against a black-holed
  remote; a timeout now surfaces immediately. Fast transient failures (DNS, dropped
  connection) still retry. (`docs/audit-2026-07.md` R6.)
- **A failed `clone_repo` cleans up its partial `dest`.** A clone that fails midway
  (timeout, network, auth) left a partial, non-empty `dest` that blocked a retry with
  "destination path already exists and is not empty" (`timeout_grace` can't prevent it
  — Windows' job-kill is atomic, the Unix grace too short for a large partial). It now
  removes a `dest` it could have created (absent, or an empty directory) on failure,
  but **never** a non-empty pre-existing directory (git refuses to clone into one, so
  the caller's data is untouched). (`docs/audit-2026-07.md` R7.)
- **`switch_with_stash` no longer pops an unrelated stash or flattens the index.** If
  `stash push` exited 0 having saved **nothing** (e.g. a submodule-only change that
  `status` still reports as dirty), the following bare `stash pop` splatted an older,
  unrelated pre-existing stash — data loss. It now checks the stash-list depth around
  the push and pops only when the push actually saved, and pops with **`--index`** so
  the staged/unstaged split is restored faithfully (a bare `pop` returned everything
  unstaged). Documents the single-actor contract. (`docs/audit-2026-07.md` M12.)
- **`status` no longer emits a phantom entry for a worktree rename.** `parse_porcelain`
  only consumed a rename/copy's source record when `R`/`C` sat in the **index** column
  (`R `); git also emits it in the **worktree** column (` R`, ` C`), whose source path
  then leaked out as a bogus `StatusEntry` with a garbage code/path. Both columns are
  now checked. (`docs/audit-2026-07.md` M11.)
- **`rev_parse` now passes `--verify`, so a non-revision errors instead of resolving
  to a fake id.** `git rev-parse Makefile` (a filename, not a rev) exited 0 echoing
  `"Makefile"` back; `rev_parse` now requires `rev` to name exactly one object (a valid
  revision still resolves to the same full hash). Matches `rev_parse_short` /
  `resolve_commit`, which already verify. (`docs/audit-2026-07.md` M13.)
- **Docs:** `GitApi::run` now documents that it (and the `run*`/`run_args` escape
  hatches) execute in the **process's current directory** — the `at(dir)` bound view
  does *not* re-bind them, unlike every modelled `GitAt` method. Pass `-C <dir>` to
  target the bound repo. (`docs/audit-2026-07.md` M15.)
- **`is_rebase_in_progress` no longer reports a `git am` as a rebase.** `git am` uses
  the same `rebase-apply/` dir as an apply-backend rebase but adds an `applying` marker;
  `is_rebase_in_progress` now excludes that case (it's an am — see `is_am_in_progress`),
  so a facade won't abort an in-progress `git am` with `rebase --abort`.
  (`docs/audit-2026-07.md` M20.)

### Security
- **Per-operation credentials are scoped to the clone URL's host.** With a
  credential provider set, `clone_repo` now binds the inline `credential.helper` to
  the target URL's host, so an HTTP redirect or a submodule fetch to a *different*
  host during the clone can't extract the token. Other remote ops (fetch/push)
  remain host-ungated for now (they target a configured remote). (`docs/audit-2026-07.md` H5.)
- **`harden()` scrubs more env code-execution vectors.** Added `GIT_PROXY_COMMAND`
  (runs an arbitrary program for a `git://` connection), `GIT_EXEC_PATH` (relocates
  where git finds its own sub-commands), and `GIT_TEMPLATE_DIR` (seeds hooks/config
  into a repo on `init`/`clone`) to the scrub list, plus the pathspec-mode vars
  (`GIT_LITERAL_PATHSPECS` / `GIT_GLOB_PATHSPECS` / `GIT_NOGLOB_PATHSPECS` /
  `GIT_ICASE_PATHSPECS`), which silently change which paths a command matches.
  (`docs/audit-2026-07.md` M14.)
- **`push` refuses a force (`+`) or multi-ref (`:`) metacharacter smuggled into a
  branch name.** `GitPush::branch("+main")` (or `"a:b:c"`) previously rode through the
  argv guard and became a **force-push** / a push to an unexpected ref. `push` now
  rejects a leading `+` and more than one `:` before spawning; a legitimate
  `local:remote` refspec still works, and a real force-push must be explicit via
  `run(["push", "--force", …])`. (`docs/audit-2026-07.md` M16.)

## [0.6.0] - 2026-06-27

### Added
- **Per-operation HTTPS credentials (opt-in).** `Git::with_credentials(provider)`
  accepts a `CredentialProvider` (re-exported from `vcs-cli-support`, with
  `Credential`/`Secret`/`StaticCredential`/`EnvToken`/`provider_fn`), plus the
  convenience `Git::with_token(token)` / `with_env_token(var)` for the common cases.
  When the provider yields a credential, every remote op (`fetch`/`fetch_from`/
  `fetch_remote_branch`/`push`/`clone_repo`/`remote_branch_exists`/
  `remote_branches`) runs with a leading inline `credential.helper` that feeds the
  secret from an environment variable — so the token never appears in `argv`.
  Default is no provider → ambient git credential helpers / SSH agent, unchanged.
- `Git::with_retry(RetryPolicy)` — opt-in retry of **whole-repo lock-contention**
  failures (another process holds the repo's `index.lock`), with exponential,
  jittered backoff. Off by default; safe even for mutating commands because that
  lock is acquired pre-write (the command never ran). Per-ref lock failures are
  *not* retried (a multi-ref op can fail a ref lock mid-way). Re-exports `RetryPolicy`.
  (Internally `Git` now wraps a `ManagedClient` instead of a bare `CliClient`  no change to existing methods.)

### Changed
- **`GitApi::log` unified (breaking).** `log(dir, max)` + `log_range(dir, range, max)`
  collapse into one `log(dir, revspec, max)` — pass `"HEAD"` for the current branch
  or a range like `"main..HEAD"`. Mirrors `JjApi::log`'s revset argument so
  cross-backend code shares one signature; the `revspec` is guarded against being
  parsed as a flag.
- **`StatusEntry::orig_path` renamed to `old_path` (breaking)** — matches
  `vcs_jj::ChangedPath::old_path`, so the rename source reads the same on both wrappers.
- **`GitApi::current_branch` now returns `Result<Option<String>>` (breaking)**  `None` on a detached HEAD instead of the literal string `"HEAD"`. Mirrors
  `JjApi::current_bookmark`'s `Option` shape, so cross-backend code treats "no named
  branch/bookmark" identically (and the `vcs-core` facade forwards it directly
  instead of remapping `"HEAD"``None`). Now backed by
  `git symbolic-ref --quiet --short HEAD` (exit 0 → branch, exit 1 → detached →
  `None`), which **also returns the branch name on an unborn repo** — a fresh
  `init`/`clone` before the first commit, where the previous
  `rev-parse --abbrev-ref HEAD` instead errored with exit 128.
- **`harden()` also scrubs the env-based command hooks**`GIT_SSH_COMMAND`/
  `GIT_SSH`, `GIT_ASKPASS`, `GIT_EXTERNAL_DIFF`, `GIT_PAGER`, and
  `GIT_EDITOR`/`GIT_SEQUENCE_EDITOR` — closing a second arbitrary-code-execution
  path (a poisoned environment making git spawn a helper) alongside the existing
  repo-redirector and config scrubbing. The opt-in `with_credentials` auth seam is
  unaffected (it injects a `credential.helper` / token env, not these variables); an
  operator who relies on an ambient `GIT_SSH_COMMAND`/`GIT_ASKPASS` for a hardened
  run should inject it per-call rather than inherit it.
- **`harden()` also pins `core.sshCommand` empty** — the *config-key* twin of the
  scrubbed `GIT_SSH_COMMAND` env var, so a poisoned **repo-local** `.git/config`
  can't run an arbitrary program for the SSH transport (env-config overrides
  repo-local config; empty falls back to the default `ssh`). The hardening docs now
  also scope the guarantee honestly: repo-local `.gitattributes`-driven
  `filter.*` smudge/clean and `diff.*.textconv` keys are *not* neutralized, so a
  fully untrusted repo still needs an OS sandbox for checkout/diff — `harden()` is
  hardening, not a sandbox.
- Bumped `processkit` to **0.11.0** (from 0.9.1), a major breaking release ahead
  of processkit's 1.0 freeze. Breaking for downstream via the re-exported
  `processkit::Error`: `Error::Timeout`/`Signalled` now carry partial
  `stdout`/`stderr`, `Error::Signalled`/`NotFound`/`CassetteMiss` are first-class
  variants, the blanket `From<io::Error>` is gone, and `Invocation::cwd` is now
  `Option<PathBuf>`.

### Removed
- The **`cancellation`** feature — cancellation is always available now
  (processkit 0.10 made it core), so the `cli_client!`-generated
  `default_cancel_on(token)` and the re-exported `CancellationToken` no longer sit
  behind a feature. Downstream that enabled `vcs-git/cancellation` should drop it.

### Fixed
- `push` and `clone_repo` now apply the same `timeout_grace` window as `fetch`:
  on a per-client timeout, the process tree is terminated gracefully (then
  hard-killed after the grace window) so a timed-out push releases its lock /
  doesn't half-update the remote ref, and a timed-out clone can clean up its
  partial destination. A no-op when no `default_timeout` is set.
- `config_get` strips only git's trailing line terminator (`\n`/`\r\n`) instead of
  all trailing whitespace, so a config value that legitimately ends in spaces or a
  tab is returned intact.
- **`blame` works on SHA-256 repositories.** The blame-porcelain header parser only
  recognised a **40-hex** (SHA-1) commit id, so on a SHA-256 repo (64-hex object ids)
  no header matched and `blame` silently returned an **empty `Vec`**. It now accepts
  both 40- and 64-hex ids.
- **`remote_head_branch` and `upstream` surface a timeout/signal instead of reporting
  it as "absent".** Both mapped *any* non-success outcome to `None`, so a timed-out or
  signal-killed run read as "no default branch"/"no upstream" rather than an error.
  `remote_head_branch` now maps exit 0 → the branch, exit 1 (the `--quiet` "unset"
  signal) → `None`, and anything else (a real failure / no exit code) errors via
  `ensure_success`; `upstream` keeps a non-zero **exit** as `None` (git uses exit 128
  for both "no upstream" and a real failure, indistinguishable by code) but surfaces a
  no-exit-code timeout/signal — matching `config_get`/`current_branch`.

## [0.5.0] - 2026-06-08

### Added
- `branch_status(dir) -> BranchStatus` — a combined branch + working-tree
  snapshot in **one** spawn (`status --porcelain=v2 --branch -z`): HEAD, branch,
  upstream, ahead/behind, and tracked/untracked/conflict counts. The cheap
  primitive behind the facade's `Repo::snapshot`. `BranchStatus` is re-exported.
- `fetch_from(dir, remote)` — fetch from a *named* remote (`fetch --quiet
  <remote>`), with the same terminal-prompt-off and transient-retry behaviour as
  `fetch`.
- `conflicted_files(dir)` — paths with unresolved merge conflicts
  (`diff --name-only --diff-filter=U -z`); empty when there are none.
- `status_tracked(dir)``status` minus untracked files
  (`--untracked-files=no`): "is the *tracked* tree dirty", staged or not.
- `Git::switch_with_stash(dir, branch)` (also on `GitAt`) — switch branches
  carrying uncommitted changes across via `stash push -u``checkout`  `stash pop`; a clean tree skips the stash round-trip, and a failed checkout
  pops the stash back where it was. Inherent (a composed operation, not a 1:1
  CLI verb).
- `clone_repo(url, dest, CloneSpec)``git clone` with a `CloneSpec` builder
  (`.branch()`, `.depth()`, `.bare()`). Runs without a working directory; pass
  an absolute `dest`. Note: git silently ignores `--depth` for a plain
  local-path source.
- Tag operations: `tag_create` (lightweight, optional rev),
  `tag_create_annotated` (`-a -m`), `tag_list`, `tag_delete`.
- `show_file(dir, rev, path)` — file content at a revision
  (`git show <rev>:<path>`); backslash separators are normalised to `/` (git
  requires it), binary content decodes lossily rather than erroring.
- `config_get(dir, key)``Option<String>` (`config --get`; exit 1 → `None`  git lumps "unset" and "no such section" together) and
  `config_set(dir, key, value)`.
- `remote_add(dir, name, url)` and `remote_set_url(dir, name, url)`.
- `blame(dir, path, rev)``Vec<BlameLine>` (`blame --line-porcelain`):
  per-line commit, author, epoch timestamp + tz, and content.
- Sequencer: `cherry_pick(dir, rev)`, `revert(dir, rev)` (`--no-edit` +
  headless editor backstop), and `rebase_skip(dir)` (`rebase --skip`) — mainly
  for the `apply` backend's "nothing to commit" stop; the default `merge`
  backend auto-drops emptied patches on `--continue`.
- `capabilities()``GitCapabilities { version: GitVersion }` — the installed
  binary's parsed version (tolerates `2.54.0.windows.1`/`-rc` shapes), with
  `is_supported()` / `ensure_supported()` gating on the major floor only
  (validated on 2.54; expected ≥ 2.30 — an untested minor is not hard-gated).
  A value type: probe once and keep it.
- Injection guards on every exposed positional argument — names, revisions,
  ranges, remotes, and **URLs** (`clone_repo`/`remote_*`: a leading-`-` url
  like `--upload-pack=<cmd>` is an RCE-class flag, refused). A caller-supplied
  value with a leading `-` (or an empty one) is rejected **before** anything
  spawns — git would parse it as a flag (`git checkout -evil` → "unknown
  switch", verified). Flag-value positions (`-m <msg>`) are unaffected.
- `RefName` and `RevSpec` validating newtypes — optional up-front validation
  for untrusted input (`check-ref-format`-shaped rules / minimal flag-shape
  rejection). Method signatures stay `&str`; the internal guards make the
  smuggling impossible either way.
- `Git::harden()` / `Git::hardened()` — an untrusted-repo execution profile
  applied to every command: hooks disabled (`core.hooksPath=/dev/null` via
  git's env-based config; verified to suppress hooks on Windows),
  `core.fsmonitor=false`, repo-redirecting `GIT_*` env scrubbed
  (`GIT_DIR`/`GIT_WORK_TREE`/config overrides/…), system config skipped,
  terminal prompts off.
- `conflict` module — a typed model of conflict markers: `parse_conflicts`
  `Text`/`Conflict` segments (`merge`/`diff3`/`zdiff3` styles, variable
  marker size, CRLF preserved), byte-exact `render`, and
  `resolve(…, ResolutionSide::{Ours,Base,Theirs})`. Pure functions; also
  parses files materialized by jj's `git` conflict-marker style.

### Changed
- **Breaking:** four multi-option `GitApi` methods now take a spec/builder
  argument instead of positional flags, mirroring `push(GitPush)` /
  `clone_repo(.., CloneSpec)`:
  - `commit_paths(dir, paths, message, amend)``commit_paths(dir, CommitPaths)`
    (`CommitPaths::new(paths, message).amend()`).
  - `merge_commit(dir, branch, no_ff, message)``merge_commit(dir, MergeCommit)`
    (`MergeCommit::branch(name).no_ff().message(m)`).
  - `merge_no_commit(dir, branch, squash, no_ff)`    `merge_no_commit(dir, MergeNoCommit)`
    (`MergeNoCommit::branch(name).squash().no_ff()`).
  - `tag_create_annotated(dir, name, message, rev)`    `tag_create_annotated(dir, AnnotatedTag)` (`AnnotatedTag::new(name, message).rev(r)`).

  The built argv and behaviour are unchanged — only the call shape moves to the
  builder style. New types `CommitPaths`, `MergeCommit`, `MergeNoCommit`, and
  `AnnotatedTag` are exported (each `#[non_exhaustive]`).
- Bumped `processkit` to **0.8** — the re-exported `Error`/`ProcessResult` carry
  through 0.8 (`Error` still `#[non_exhaustive]` with `NotReady`/`Unsupported` and
  feature-gated `Cancelled`/`ResourceLimit`; `Error::Exit` Display gained a
  stderr-tail suffix; `Command` is `#[must_use]`). **Breaking** for consumers that
  match the re-exported types exhaustively, or that bump their own direct
  `processkit` separately — caret `"0.7"` does not span 0.8, so bump together.
- Internal: the `CliClient` verbs the wrapper bodies call were renamed to one
  shared vocabulary (`text``run`, `capture``output`, `unit``run_unit`,
  `code``exit_code`); no public-API or built-argv change.
- New off-by-default **`cancellation`** feature: pulls in processkit's
  `cancellation`, so `cli_client!` emits `default_cancel_on(token)` on the client —
  build a cancellable client (every command it runs dies when the token fires) and
  pass it through the facade. No new vcs-* API; `CancellationToken` is re-exported
  from `processkit`.
- Internal: the diff model + parser (`ChangeKind`/`DiffLine`/`Hunk`/`FileDiff`/
  `DiffStat`/`parse_diff`) and the version type now come from the shared
  `vcs-diff` crate, and the error classifiers (`is_merge_conflict`/
  `is_nothing_to_commit`/`is_transient_fetch_error`) + the argv injection guard
  from `vcs-cli-support` — both re-exported, so the public API is unchanged
  (`vcs_git::FileDiff`, `vcs_git::is_merge_conflict`, … still resolve; `GitVersion`
  is now an alias of `vcs_diff::Version`). Removes the byte-identical duplication
  with `vcs-jj`. `parse_diff` is now part of the public surface.

### Fixed
- `diff`/`diff_text` pin the `a/``b/` diff prefixes (`--src-prefix`/`--dst-prefix`),
  so a user's global `diff.noprefix` / `diff.mnemonicPrefix` config can no longer
  make every parsed file silently vanish from the result.
- `branches`/`is_merged`/`tag_list` pass `--no-column`, so a user's
  `column.ui = always` (which columnates output even when piped) can no longer
  corrupt the line parsing or yield a false "not merged".
- Commands whose failure output feeds the error classifiers (the `commit`,
  `merge`, `rebase`, `cherry-pick`/`revert`, and `fetch` families) force
  `LC_ALL=C`, so a non-English locale can no longer defeat
  `is_merge_conflict`/`is_nothing_to_commit` or the transient-fetch retry.
- `show_file` normalises `\``/` only on Windows — on Unix a backslash is a
  legal filename byte, and the unconditional rewrite made such paths unresolvable.
- `branch_status` runs with `GIT_OPTIONAL_LOCKS=0`, so the snapshot/poll
  primitive no longer opportunistically rewrites `.git/index` — a filesystem
  watcher re-querying through it (vcs-watch) had its own query re-trigger the
  watch for a couple of extra rounds per change burst.
- `conflict::parse_conflicts`: a repeated `|`-run line inside a diff3 region is
  base **content**, not a replacement base marker — the overwrite dropped a
  line on `render`, breaking the byte-exact roundtrip (found by the roundtrip
  proptest; its seed is now committed under `proptest-regressions/`).

## [0.4.0] - 2026-06-04

### Added
- `Git::at(dir)``GitAt`, a cwd-bound view whose methods omit the leading `dir`
  argument (`git.at(dir).status()`), so a caller needn't thread `dir` through every
  call. The dir-taking `GitApi` stays for driving many directories from one client.
- `rev_parse_short` (`rev-parse --short <rev>`) — e.g. to label a detached HEAD.
- `push(dir, GitPush)` (git had no push): a `GitPush` builder — `branch(name)` /
  `refspec(local, remote_branch)`, `.remote(_)`, `.set_upstream()`.
- `upstream` (`@{u}`, `None` when unset), `set_upstream`, and `remote_branches`
  (`ls-remote --heads`) — the remote-tracking surface vcs-flow hand-rolled.
- `FileDiff.raw` — the verbatim per-file diff section, so a consumer can show the
  raw text without re-parsing.
- Sync `blocking::worktree_remove` for `Drop`-time cleanup that can't `.await`.

### Changed
- `merge_commit` with no message now passes `--no-edit`, and `rebase` /
  `rebase_continue` force a no-op editor (`GIT_EDITOR`/`GIT_SEQUENCE_EDITOR`), so
  a headless caller never hangs on `$EDITOR`.
- `remote_branch_exists` now queries the fully-qualified `refs/heads/<name>` — a
  bare `foo` could tail-match `bar/foo`.
- `fetch` now runs with `GIT_TERMINAL_PROMPT=0`, matching the other remote ops, so
  a credentials-needing remote fails fast instead of blocking on a prompt.
- Bumped `processkit` to 0.6. `fetch` / `fetch_remote_branch` now retry transient
  failures (3 attempts, 500 ms backoff) — the retry that consumers hand-rolled.
- The exit-code predicates (`diff_is_empty`, `diff_range_is_empty`,
  `staged_is_empty`, `branch_exists`, `is_unborn`) use processkit's `probe()` — no
  API change, but an unexpected exit code now carries the real captured output.

### Fixed
- `merge_no_commit` no longer builds the mutually-exclusive `--squash --no-ff`
  pair (which git rejects); `squash` takes precedence (it never fast-forwards).

## [0.3.1] - 2026-06-03

### Added

- feat(diff): typed diff (raw + parsed) for git and jj
- feat(git,jj): fill Phase 1 API gaps
- feat: Step B + 1d + 1e — error classifiers, status/diff_stat consistency, &[&str] ergonomics


### Changed

- review: fix potential issues across vcs-git/vcs-jj expansion
- deps: bump processkit 0.4 -> 0.5; absorb breaking API changes
- Release: vcs-git v0.3.0, vcs-jj v0.3.0, vcs-github v0.3.0


### Changed

- Release: vcs-git v0.2.1, vcs-jj v0.2.1, vcs-github v0.2.1


### Added

- feat(git,jj): expand clients with worktree/workspace, discovery, diff, merge ops for agent-workspace


### Changed

- Release: vcs-git v0.2.0, vcs-jj v0.2.0, vcs-github v0.2.0


### Added

- feat(process): job-backed spawn (JobObject/cgroup) + publish setup
- feat: typed command wrappers, exec options, integration tests
- feat: mockable trait-based API + Runner injection
- feat: async (tokio) API, timeouts, structured errors, richer models
- feat: non_exhaustive result structs, optional tracing, cli_client! macro


### Changed

- Scaffold vcs-toolkit-rs workspace from rust-repo-template
- review: harden whole solution, fix potential issues
- refactor: portable Output model, CliClient core, richer test seam, -z git parsing
- refactor: replace internal vcs-process with external processkit 0.3
- ci: release workflow picks major/minor/patch with auto-increment (+ all-crates, first-release)
- Release: vcs-git v0.1.0, vcs-jj v0.1.0, vcs-github v0.1.0

## [0.3.0] - 2026-06-02

### Added
- Typed diff: `diff_text(dir, DiffSpec)` returns the raw git-format unified diff
  (`diff <spec> --no-color --no-ext-diff -M`), and `diff(dir, DiffSpec)` returns
  a parsed `Vec<FileDiff>` (change kind, path, rename old-path, and `@@` hunks
  with per-line `DiffLine`s). The pure parser `parse::parse_diff` is public for
  parsing externally-obtained diff text. `DiffSpec::WorkingTree` diffs the working
  tree vs `HEAD`; `DiffSpec::Rev(_)` diffs a revision/range.
- API gaps consumers previously hand-rolled via `run()`: `checkout_detach`,
  `commit_paths` (partial `commit --only`, with optional `--amend`),
  `last_commit_message`, `is_unborn`, `log_range`, and `stash_push`/`stash_pop`.
  `WorktreeAdd` gains a `no_checkout()` builder (`worktree add --no-checkout`).
- Error classifiers `is_merge_conflict`, `is_nothing_to_commit`, and
  `is_transient_fetch_error` — inspect both captured streams of an `Error::Exit`
  (git writes `CONFLICT (…)` to stdout, `Automatic merge failed` to stderr) so
  callers stop string-scraping. Enabled by processkit 0.5's `Error::Exit.stdout`.
- `status_text` — raw `git status --porcelain=v1` text, the unparsed counterpart
  of `status`, mirroring `vcs_jj`.
- Inherent `Git::run_args` / `run_raw_args` taking `&[&str]`, so callers needn't
  allocate a `Vec<String>` for the `run` escape hatch.

### Changed
- Renamed `diff_shortstat``diff_stat` to match `vcs_jj::JjApi::diff_stat`
  (both return `DiffStat`).
- Bumped `processkit` to 0.5 and absorbed its breaking changes: exit-code probes
  now read `ProcessResult::code() -> Option<i32>` (the removed `exit_code() -> i32`
  with its `-1` timeout sentinel is gone), and synthetic `Error::Exit` values carry
  the new `stdout` field. No change to this crate's public API.

### Fixed
- `remote_head_branch` now keeps a slashed default-branch name intact (e.g.
  `release/v2`) instead of returning only its last path segment.

## [0.2.1] - 2026-06-01

### Added
-

### Changed
- Bumped `processkit` to 0.4 — macOS/BSD process trees are now contained via a
  POSIX process group (`killpg` on drop) instead of an uncontained spawn.

### Fixed
-

## [0.2.0] - 2026-06-01

### Added
- **Worktree management:** `worktree_list` (new `Worktree` struct),
  `worktree_add` (`WorktreeAdd` options), `worktree_remove`, `worktree_move`,
  `worktree_prune`.
- **Discovery:** `common_dir`, `git_dir`, `resolve_commit`, `remote_head_branch`,
  `branch_exists`, `remote_branch_exists` (no credential prompt, 10s timeout),
  `remote_url`.
- **Branches & diff:** `is_merged`, `delete_branch`, `rename_branch`,
  `rev_list_count`, `diff_range_is_empty`, `diff_shortstat` (new `DiffStat` struct).
- **In-progress state:** `staged_is_empty`, `is_rebase_in_progress`,
  `is_merge_in_progress`.
- **Mutations:** `fetch`, `fetch_remote_branch`, `merge_squash`, `merge_commit`,
  `merge_no_commit`, `merge_abort`, `merge_continue`, `reset_merge`, `reset_hard`,
  `rebase`, `rebase_abort`, `rebase_continue`.

## [0.1.0] - 2026-06-01

### Added
- `GitApi` trait + `Git` client with typed, repo-scoped commands returning parsed
  structs: `status` (`StatusEntry`), `log`/`current_branch`/`branches`/`rev_parse`,
  `init`/`add`/`commit`, `diff_is_empty`. New `Commit`/`Branch`/`StatusEntry` types.
- **Mockable by design:** consumers code against `GitApi`; `Git::with_runner`
  injects a fake process runner (e.g. `processkit::ScriptedRunner`), and the
  `mock` feature generates `MockGitApi` (via `mockall`) for stubbing whole methods.
- `create_branch`, `checkout`, and raw `run`/`run_raw` escape hatches on `GitApi`.
- `Commit` gained `short_hash` and `date` (ISO-8601 `%aI`).
- `Git::default_timeout` kills any command exceeding the deadline.

### Changed
- The API is now the `Git` client + `GitApi` trait — the original free functions
  (`run`/`version`/`status`/…) are gone. Commands launch `git` inside an OS job
  (Windows Job Object / Linux cgroup v2) via `processkit`, killed on close.
- **Now async (tokio):** every `GitApi` method is `async`. Errors are the typed
  `processkit::Error` (exit code, stderr, …) instead of `io::Error`.
  Adds `async-trait`.
- `status` now runs `git status --porcelain=v1 -z` (NUL-delimited records, raw
  unescaped paths — robust to spaces and special characters) and `log` uses `-z`
  record separation (robust to multi-line fields). `StatusEntry` gained
  `orig_path`, the source path for a rename/copy (`R`/`C`).
- Built on the external **`processkit`** crate (the `CliClient` core, the
  `cli_client!` macro, the `ProcessRunner` seam, and the structured `Error`) —
  replacing the prototype internal `vcs-process` crate. No public API change
  beyond `run_raw` now returning `processkit::ProcessResult<String>`.
- `StatusEntry`/`Commit`/`Branch` are now `#[non_exhaustive]` — future fields
  won't be breaking changes.
- Optional `tracing` feature (forwards to `processkit/tracing`): a `debug` event
  per `git` command.

### Fixed
- `status`/`branches` parsing no longer corrupts the first entry: output is parsed
  raw instead of being trimmed, which had stripped leading `--porcelain` status
  spaces and `branch` markers.

[Unreleased]: https://github.com/ZelAnton/vcs-toolkit-rs/compare/vcs-git-v0.8.0...HEAD
[0.8.0]: https://github.com/ZelAnton/vcs-toolkit-rs/compare/vcs-git-v0.7.0...vcs-git-v0.8.0
[0.7.0]: https://github.com/ZelAnton/vcs-toolkit-rs/compare/vcs-git-v0.6.0...vcs-git-v0.7.0
[0.6.0]: https://github.com/ZelAnton/vcs-toolkit-rs/compare/vcs-git-v0.5.0...vcs-git-v0.6.0
[0.5.0]: https://github.com/ZelAnton/vcs-toolkit-rs/compare/vcs-git-v0.4.0...vcs-git-v0.5.0
[0.4.0]: https://github.com/ZelAnton/vcs-toolkit-rs/compare/vcs-git-v0.3.1...vcs-git-v0.4.0
[0.3.1]: https://github.com/ZelAnton/vcs-toolkit-rs/compare/vcs-git-v0.3.0...vcs-git-v0.3.1
[0.3.0]: https://github.com/ZelAnton/vcs-toolkit-rs/compare/vcs-git-v0.2.1...vcs-git-v0.3.0
[0.2.1]: https://github.com/ZelAnton/vcs-toolkit-rs/compare/vcs-git-v0.2.0...vcs-git-v0.2.1
[0.2.0]: https://github.com/ZelAnton/vcs-toolkit-rs/compare/vcs-git-v0.1.0...vcs-git-v0.2.0
[0.1.0]: https://github.com/ZelAnton/vcs-toolkit-rs/releases/tag/vcs-git-v0.1.0