# sgit-ai 0.26.8: history, blame and search; renames found as Git finds them, cli.sgit.ai

> Dev build of 28 October 2026, from the CLI's dev at bc904c1b, and what a plain pip install gives today. sgit history blame, sgit grep (the working copy or any revision), history log -S and -G, rename detection in history show, diff and log, history log --follow, and a patch text that is git diff's byte for byte. Only the files a commit changed are opened, so these cost what changed, not the vault's size. One change for scripts and agents: history log --json lists a renamed file under files_renamed, no longer under files_added and files_deleted. Reviewed by cli.build. 5,436 unit and security tests pass. Against git on random histories, blame matched on 25 of 25 files, renames on 90 of 90 pairs, grep on 120 of 120, and log -S and -G on 40 of 40 against git --no-renames. Vaults made by 0.20.0 to 0.26.0 open. Maturity good. One low finding that breaks a script's exit code (BR-5), two differences from Git to document (BR-6, BR-7), and BR-1 still open.

*Source: <https://cli.sgit.ai/builds/0-26-8.html> · generated from the same content as the page, so the two cannot drift.*

---
# sgit-ai 0.26.8

```text
┌─ sgit-ai 0.26.8 ── dev build ── 28 Oct 2026, 13:34 UTC ─────────────────────────
│ tag v0.26.8 = bc904c1b (dev) · PyPI package == tag · a plain install gets this
│ new     history blame · grep · history log -S/-G · renames in show/diff/log · log --follow
│ change  history log --json: a rename is in files_renamed, not files_added + files_deleted
│ tests   5,436 unit+security ✓ · 18/18 end-to-end ✓ · 0.20.0–0.26.0 vaults open ✓
│ vs git  blame 25/25 · renames 90/90 · grep 120/120 · log -S/-G 40/40 (git --no-renames)
│ open    BR-1 (high, every build) · BR-5 (low) · BR-6, BR-7 (differences) · R3-1
│ maturity  ########--  good: use it; read BR-5 if a script tests grep's exit code
└──────────────────────────────────────────────────────────────────────────────────
```

```text
$ pip install sgit-ai==0.26.8        # or: sgit update, which takes the newest build
$ sgit version
sgit-ai v0.26.8
```

## What is new {#new}

This build is design 04 of the [Git-parity roadmap](../research/git-parity-roadmap.md): reading history the way Git does. It adds no vault format and no gate.

### Blame

```text
$ sgit history blame -L 10,+5 src/app.py          # who last changed each line
$ sgit history blame HEAD~3 docs/guide.md          # as of a revision
$ sgit history blame --porcelain notes.md          # Git's machine format
$ sgit history blame --json notes.md               # history_blame_v1
```

- **Each line is attributed to the commit that last changed it,** through renames and merges.
- **Options as in Git:** `-L N,M`, `N,+K`, `N,-K`, `/regex/,+K` (repeatable), `-w`, `--first-parent`, `-s`, `--abbrev`, `--porcelain`, `--line-porcelain`. Also `--json`, `-e` (the signing key id in place of the branch name) and `--text` (a binary file, line by line).
- **The author is the commit's branch name,** dates are UTC, and a boundary (the root, a shallow clone's edge) is marked `^`.

### Search

```text
$ sgit grep -n -i todo                              # the working copy (an uncommitted edit is what is searched)
$ sgit grep -n todo HEAD~5 -- '*.py'                # a revision, decrypted in memory: HEAD~5:path:line:text
$ sgit history log -S needle --oneline              # commits where the count of "needle" changes
$ sgit history log -G 'def \w+' --file src/ -n 5    # commits whose diff adds or removes a matching line
```

- **`sgit grep` takes Git's options** (`-i -w -v -n -c -l -L -o -q -h -H -I -a -F -e -A -B -C -m`) and exit codes: 0 a match, 1 none, 2 an error. `grep` is a new top-level command; `sgit file grep` is the same.
- **The pattern is a Python regular expression** (see [BR-6](#br-6)); it goes through the same guard as `--grep`: at most 1,000 characters, valid, and no repetition of a repetition.
- **A path from the vault is quoted as Git quotes it** (`"jos\303\251.md"`), so a file name cannot forge an output line.
- **`log -S` and `-G`** combine with `--file`, `--grep`, `--since`, `--until`, `--author`, `-n`, `--oneline` and `--stat`; `--pickaxe-regex` and `-i` as in Git. They stop at the `-n`-th match.

### Renames, found as Git finds them

- **`history show`, `history diff` and `history log` (`--files`, `--patch`, `--json`)** report `R096  docs/guide.md -> manual/guide-v2.md`, and in a patch Git's `similarity index`, `rename from`, `rename to` header. On by default.
- **The detection is a port of Git's:** exact, then same basename, then a similarity matrix.
- **Options as in Git:** `-M[<n>]` (`-M90`, `-M90%`, `-M0.9`), `--no-renames`, `-l` / `--rename-limit`.
- **`sgit history log --file <path> --follow`** continues past a rename.

### Faster, and a patch that is `git diff`'s

- **`history show`, `history diff` and `history log --patch` open only the folders whose ids differ** and decrypt only the changed files. The CLI team measured a 3,000-file vault with one change at 439 ms and 6,000 decrypted blobs before, and 7 ms and 2 after.
- **The patch text is `git diff`'s, byte for byte,** including `\ No newline at end of file`. The old output glued the last line of a file with no final newline to the next `+` line.
- **A re-encrypted file whose bytes did not change** is no longer listed as modified.

### Changes for scripts and agents {#script-changes}

| What | Before | Now |
|---|---|---|
| `history log --json`, `history show --json`, `history diff --json` | a renamed file is in `files_added` and `files_deleted` | it is in `files_renamed` (`old_path`, `new_path`, `score`, `exact`, `lines_added`, `lines_removed`); `--no-renames` restores the old lists |
| patch text | the old format | `git diff`'s |
| `history log --stat` | a rename shown as `R old -> new` | the same, preferring a same-basename source, as Git does |

- **A script that reads `files_added` to find new files** now misses a renamed file. That is correct, and it will be noticed only if the script is run on a history that has a rename.

## Compatibility {#compat}

- **No change to the vault format, and no new gate.** Everything here reads; nothing writes a new kind of object.
- **Checked on old vaults** made by 0.20.0, 0.22.0, 0.24.0 and 0.26.0 (pack mode too, from 0.24.0), each opened by 0.26.8:
  - the old working copy: status, fsck, a commit and a push;
  - a fresh clone: fsck clean, every commit's signature verified, the same tree;
  - the old client pulls what 0.26.8 pushed, and its fsck is clean.
- **Blame (through a rename), grep and `-S`** gave the right answers on the vaults of 0.20.0 and 0.24.0 (pack mode).

## Review by cli.build {#review}

**Verdict: good.**
- The new commands matched Git, used as the oracle, on every case I built, and where they differ the difference is a documented design choice (BR-6, BR-7).
- The hostile inputs I tried were refused or quoted: a pattern that backtracks without end, a quote or non-ASCII characters in a name (a newline in a name is rewritten to `_` by the vault, and `sgit ls` prints it raw, see below), a path outside the vault, a binary file.
- One finding changes a script's exit code (BR-5). One high finding is not this build's own (BR-1).

```text
┌─ checks on the PyPI package 0.26.8 ───────────────────────────────────────────────
│ ✓     package == tag v0.26.8 (only the version file differs); wheel == sdist
│ ✓     5,436 unit+security tests pass (38 skipped); 4 serve tests fail only when two suites run at once
│ ✓     everything from 0.26.1–0.26.7 (G1–G2, M1–M5, L1–L2, X1–X4, C1–C5) still holds
│ ✓ H1  blame, 25 files over random histories with edits, moves, a rename: every line's commit equals git blame --porcelain
│ ✓ H2  grep: 104 working-copy runs (13 option sets) and 16 revision runs: output and exit code equal git grep (-E syntax)
│ ✓ H3  log -S / -G / -i -S / --pickaxe-regex: 40 runs: the commits selected equal git log --no-renames
│ ✓ H4  rename detection -M50: 90 consecutive pairs: scores and paths equal git diff -M50 --name-status
│ ✓ H5  hostile input: nested repetition, 1,000 and 1,001 characters, a quote and non-ASCII in a name, ../ pathspec, a binary file
│ ✓ H6  vaults of 0.20.0 · 0.22.0 · 0.24.0 · 0.26.0, plain and pack: open, commit, push; old client pulls
│ ✗ P1  one push of 150 commits: fresh clone's fsck fails  ── BR-1, unchanged from 0.26.6
│ ✗     R3-1 not re-run; no change in the code it concerns
└──────────────────────────────────────────────────────────────────────────────────────
```

### BR-5 (low): `sgit grep` with a pattern of 1,001 characters or more exits 1 with a traceback message {#br-5}

**What happens.**
- `history log --grep` refuses a long pattern cleanly: `error: --grep pattern is 1001 characters; the limit is 1000`.
- `sgit grep` does not reach that check. The pattern becomes a `Safe_Str__Grep_Pattern` first, and its own length limit fails with `error: TypeError in "grep" — … value exceeds maximum length of 1000 characters (was 1001)`.
- **The exit code is 1,** which for `grep` means "no match", not "error" (2).

**Why it matters:** a script that treats 1 as "nothing found" will treat an oversized pattern as a clean result. 1,000 characters is a long pattern, so this is rare.

**For the CLI team's agent:** check the length (and run the guard) before the pattern is turned into the safe type, or catch the type error in `CLI__Grep` and exit 2 with the guard's message. The repro: `sgit grep -c "$(printf 'y%.0s' $(seq 1 1001))"; echo $?`.

### BR-6 (difference from Git, to document): the pattern is a Python expression, and Git's default is a basic one {#br-6}

- `git grep` reads a pattern as a basic regular expression: `a\|b` is an alternation and `a|b` is literal. `sgit grep` reads it like `git grep -E`: `a|b` is an alternation and `a\|b` is literal.
- A Git user's `sgit grep 'alpha\|gamma'` finds nothing and exits 1. `sgit grep -E 'alpha|gamma'` and `sgit grep 'alpha|gamma'` behave as in Git with `-E`.
- The CHANGELOG says `-E` and `-P` are accepted and the pattern is a Python regular expression; it does not say that the default differs from Git. Say it in the release notes, and in [sgit and Git](../docs/sgit-and-git.md).
- **In my differential** the 104 working-copy runs used patterns that mean the same in both dialects; the one run that did not (`ne+dle`) is where this showed.

### BR-7 (difference from Git, to document): `log -S` and `-G` do not follow renames {#br-7}

- Git's `log -S` runs after rename detection, which `git log` turns on by default. A commit that only renames (or renames and edits) a file holding the string does not change its count, so it is not listed.
- `sgit history log -S` lists it: a rename counts as a delete and an add. Its answers equal `git log --no-renames -S` on all 40 runs, and differ from default Git, which has rename detection on, in 16 of the 40 runs.
- This is a choice, not a bug, and the CHANGELOG's "exactly git log's" is true of `--no-renames`. Rename detection is on by default for `show`, `diff` and `log`, so a reader will expect `-S` to follow it. Either follow renames in the pickaxe, or say so.

### Also noted

- **`history blame` on a 200,000-line file took 24 s** (two commits, 3.7 MB, one edit). `grep -c` over the same vault took 1.4 s, `log -S` 0.9 s, `history show HEAD` 0.8 s. The CLI team's own figures (0.44 s on a vault of 67,742 files) use small files. That is one point; I did not measure how it grows.
- **`sgit ls` prints a path with a newline in it raw,** so the name can forge a line. It does in 0.26.6 too. The new `Vault__Path_Quote` is used by grep and not by `ls`; one call would fix it.
- **`blame -L 0,1` says "file has only 3 lines"** when the range, not the file, is wrong. A CR in a line is shown as `?`.
- **`history blame HEAD~9` on a vault with fewer commits** says "has 0 parent(s), not 1", which does not say the revision is out of range.
- **The serve tests share ports,** so two suites at once fail four of them; run alone they pass (9 of 9).
- **Not tested here:** `-p` and the interactive paths, a merge-heavy history for `blame --first-parent`, and a vault on a real host (everything ran on the in-memory host).

## What changed since 0.26.6, for the release notes of 0.27.0 {#for-release-notes}

A list the CLI team can paste into the release notes, from this and the three builds before it:

1. **`sgit commit -m "msg" y`:** `y` is a path now; it was ignored (0.26.6).
2. **`history log/show/diff --json`:** renames are in `files_renamed` (0.26.8).
3. **Patch text** is `git diff`'s (0.26.8).
4. **Rename detection** is on by default in `show`, `diff` and `log` (0.26.8); `--no-renames` turns it off.
5. **`sgit grep` patterns** read like `git grep -E`, not Git's default (0.26.8).
6. **`sgit push`** pushes with uncommitted changes (0.26.6).
7. **`sgit update`** takes the newest build; `--stable` takes the newest release (0.26.3).
8. **User signatures can be checked,** and none can be made yet (0.26.7).

*Scripts: [`diff_history.py`](https://github.com/SGit-AI/SGit-AI__Website__Cli/tree/dev/admin/research/builds-0-26-7-0-26-8-2026-10-28/scripts) (`NORENAMES=1` for the `-S` comparison), `legacy_vault.py`, `push_gap.py`, `check_build.py`. Results: `results/check_0.26.8.json`, `results/suite-v0.26.8.txt`, `results/diff_history_seeds_1-8_*.txt`, `results/legacy_vaults_0.20.0_to_0.26.0.txt`. All builds: [the builds page](../builds/index.md).*

---

*[Site index for agents](../llms.txt) · [HTML version](https://cli.sgit.ai/builds/0-26-8.html)*
