Authoritative scope gate for git-parity (umbrella #246, scope-definition milestone #247). This file defines exactly what "usable like git" means for mkit v1 — the scope statement, non-goals, machine-output contract, and tracked divergences. The live, per-command status matrix now lives on the web
/paritypage (see the pointer below). A command is in scope only if it is covered by this scope gate. If schedule pressure appears, the lever is renegotiating scope here — never the safety principles.
mkit targets CLI/UX parity with Git: every in-matrix command and flag a
human or AI agent would type behaves like Git's, while mkit keeps its own
improvements — BLAKE3 content addressing, native attestation, and data-loss
safety guards. This is not on-disk or wire interop with real .git
repositories; mkit's object/wire formats remain native (a BLAKE3 object store
cannot share bytes with Git's SHA-1/SHA-256 store).
Scope amendment (git-bridge): one deliberate, renegotiated exception to
the paragraph above — the translation bridge: a deterministic export to
git mirrors (mkit git export, SPEC-GIT-BRIDGE) and
an importer-signed import from git remotes (mkit git import,
SPEC-GIT-IMPORT). Neither shares bytes between the
stores (every object is translated, never aliased); bidirectional sync
remains a permanent non-goal. Safety principles are untouched.
- Hash length — mkit object IDs are 64-hex BLAKE3, Git's are 40-hex SHA-1. A literal Git SHA pasted into mkit will never resolve. mkit matches the UX shape (short prefixes, abbreviated display) but not the length.
- Repo marker — mkit uses
.mkit/, not.git/. Repo detection by.git/is not added to core; the opt-ingitalias shim (issue #254 — shipped atcontrib/git-shim/mkit-git, never installed by default) is the only bridge.
These are out of scope for v1 parity. Listing them here keeps scope from creeping; revisit post-v1 if demand warrants.
- Submodules / subtrees
- Hooks (
.git/hooks,core.hooksPath) - Full refspec grammar (
+a:b, wildcard push/fetch maps beyond current remotes) - Wire protocol v2 / smart-HTTP negotiation flags
git notes- Partial / shallow clone beyond what
clonealready exposes .git/-format on-disk interop, SHA-1/SHA-256 objects,git fsck-compat (exception: the translation bridge, see the scope amendment above — export mirroring and importer-signed import, never byte-sharing or bidirectional sync)- Shadowing the real
gitbinary onPATHby default log --graphASCII commit-graph rendering (the flag is accepted as a no-op for script compatibility). Full byte-parity is unachievable because mkit's defaultlogbody already diverges from git'scommit/Author/Date(mkitIdentityplus 64-hex ids); an--oneline --graphrenderer for the linear/DAG case is an optional post-v1 follow-up, not a v1 blocker.
Scope amendment (worktrees, #493): multiple working trees left the
non-goals list — mkit worktree add/list/remove/prune ships with git's
core semantics (linked trees share one object store and the shared
refs; each tree has its own HEAD/index/op-state; a branch can be
checked out in at most one tree; gc never prunes objects reachable
from a sibling tree's state). Documented divergences: the stash is
per-worktree (git shares refs/stash across trees) — a deliberate
consequence of mkit's worktree-state stash manifest; and
worktree move/lock/repair remain follow-ups outside the minimal
set. On-disk format and discovery are specified in
SPEC-WORKTREE.md.
The live, per-command git-parity matrix — every porcelain, plumbing, remote,
and config-convention row with its shipped / divergent / non-goal status — is
rendered on the web /parity page from
apps/web/src/lib/parity-data.ts. That
page is the single source of truth for which commands and flags behave like
git today; keep it current when a command's behavior changes.
apps/web/src/lib/parity-sync.test.ts
parses the "Deferred flags" list below and fails CI if a command listed there
is rendered with an unqualified 'parity' status on the web page.
This document keeps the surrounding contract that the web page distills out (it carries user-facing notes only): the scope gate above, and the machine-output / safety / divergence sections below.
mkit deliberately refuses Git's silent data-loss defaults. These are features, not gaps, and stay even when the command reaches parity:
rm/restore/reset --hard/clean/stash pop— refuse to destroy locally-modified or untracked content without an explicit-f/--force.mv— refuses to overwrite an existing destination without-f(matches git'smvclobber guard).worktree remove— refuses to delete a tree holding local changes, untracked files, or an in-progress operation without--force, and never removes the main tree or the tree the caller stands in.checkout— refuses to clobber dirty tracked files or colliding untracked files; non-colliding untracked files are preserved (git semantics).- Repo-local config —
user.identityand other security-sensitive keys are inREPO_FORBIDDEN_KEYSso a hostile clone cannot spoof the signed author or redirect signing/transport trust. Git-styleuser.name/user.emailaliases must respect this (user-scope / non-authoritative only). - History-rewriting commands (
commit --amend,reset,rebase) record the superseded commit in the recovery log sogckeeps it recoverable within the retention window (#260 plus #233, shipped).
Outputs that tools parse must be stable and Git-shaped, normalized only by hash length:
status --porcelain=v1—XY <path>, newline-delimited; special-byte paths are C-style quoted (gitcore.quotePath), and-zemits raw, NUL-terminated records. mkit addsTfor mode-change as the sole extension.status --porcelain=v2— git's richer per-path format:1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>for tracked changes (octal modes; full 64-hex BLAKE3 object ids, masked to git's length in the differential harness;<sub>alwaysN...) and? <path>for untracked.-zraw NUL-terminated. Exact renames emit a2R100record; no--branchheader lines. The tracked-side columns match git, includingmW = 000000when a tracked file is shadowed by a directory.- Untracked-walk collision (#288, resolved) — when a tracked path is
shadowed on disk by a directory (for example, a tracked file replaced by a
directory),
status(v1 and v2) andcleannow match git: they report only the tracked-side deletion and suppress the directory's contents as untracked. Note the behavior is not uniform across consumers — git'sls-files --othersis raw plumbing and still lists the shadowed directory's contents; mkit matches that too. - Plumbing (
rev-parse,cat-file,ls-files,ls-tree,show-ref,for-each-ref) — exact flag contracts defined by the plumbing-commands milestone (#251); output matches Git modulo 64-hex vs 40-hex hashes.
The differential parity harness (rust/crates/mkit-cli/tests/git_parity_harness.rs)
runs the same script under real git and mkit and asserts these contracts
modulo hash length. Cases for not-yet-implemented rows are #[ignore]d with
their phase/issue so CI stays green until the feature lands; each phase
un-ignores its rows as it implements them.
Beyond the machine contract, mkit's human output is shaped like git's so people and agents who learned git's surface aren't surprised:
- Sync —
push:Everything up-to-date/To <url>plus ref-update summary (* [new branch],<old>..<new>,+ …(forced update),! [rejected] …);pull:Already up to date./Updating <a>..<b>plusFast-forwardplus diffstat;fetch:From <url>plus per-ref summary (silent on no-op);clone:Cloning into '<dir>'....fetch --all/pull --allrepeat the same per-remote summary once per configured remote rather than inventing a combined format.clone/push/pull/fetchalso stream a live, honest transfer-progress line on stderr while the network transfer runs (Writing objects: N objects, B bytes/Unpacking objects: N objects, #711) — real counts derived from objects actually staged into the outgoing pack / unpacked from a downloaded one, shown only when stderr is a tty (--quiet/-qorMKIT_PROGRESS=neversuppress it;MKIT_PROGRESS=alwaysforces it). mkit deliberately does not fabricate git's object-count / delta-compression progress lines (Enumerating/Counting/Compressing objects,Total N (delta D)) — mkit's transport is one-object-per-pack and computes no cross-branch delta graph, so those specific numbers would be invented. - Local mutation —
commit/cherry-pick/revert:[<branch> <hash>] <subject>+ diffstat +create/delete mode;merge:Merge made by the 'ort' strategy./CONFLICT (content): …+Automatic merge failed; …;checkout/switch:Switched to (a new) branch '<n>'/Already on '<n>';reset --hard:HEAD is now at …;branch/tagdelete confirmations;stashSaved working directory …/Dropped …;rebaseSuccessfully rebased …. - status (long format) —
On branch,No commits yet, word labels (new file:/modified:/deleted:/typechange:), a separateUntracked files:section, and(use "mkit …")hints. - color —
diff --color[=always|auto|never]/--no-colorcolorizes the patch with git's palette (bold meta, cyan hunk, green/red lines); defaultauto(tty-only, honoringNO_COLOR/CLICOLOR_FORCE). Color forstatus/log/branchis a tracked follow-up (theterm::ColorChoicegating is in place).
Channel divergence (intentional): all of the above goes to stderr —
mkit reserves stdout for porcelain/data so mkit status stays empty-on-clean
in a pipeline. git puts some of these on stdout; mkit keeps its stderr
convention (documented in docs/CLI.md). Object ids stay 64-hex BLAKE3
prefixes (the inherent hash-length divergence).
These commonly-typed flags are recognized as in-scope for full parity but are not yet implemented; they are larger feature additions best done as their own change, and are listed here so the gap is explicit:
log -p/--stat/--decorate/--all— needs the log renderer plus walk extensions. (--author/--grep/--since/--until/--no-merges/--first-parentshipped: #712 —--author/--grepare substring matches rather than regexes, and--since/--untiluse a small explicit date grammar rather than git'sapproxidate; both are documented divergences, not gaps.)branch -r/-a/-u/--unset-upstream— remote-tracking listing and upstream config.add -n/--dry-run— needs the path-selection pass without the index write (a no-op flag would incorrectly stage, so it is deferred rather than faked).reset --mixed"Unstaged changes after reset:" file list — needs a worktree-vs-target-tree diff;--hard'sHEAD is now at …ships,--mixedis otherwise silent.- color for
status/log/branch(see above).
- Empty commit — when the index is populated but identical to HEAD (no
change to record),
mkit commitsilently creates an empty commit, whereas git refuses without--allow-empty. (A truly empty index — nothing ever staged — still errors, like git.) Left unchanged this PR to avoid surprising existing mkit workflows; revisit with--allow-empty. - Default remote name
default— mkit's flat default remote isdefault, not git'sorigin(tracking refs live underrefs/remotes/default/). Renaming is a load-bearing migration, deferred. mkit blame --ignore-rev-precise— opt-in, content-addressed refinement of--ignore-revfall-through (#496). git's--ignore-revdecides which parent line a fallen-through line should inherit blame from by position: within a changed hunk it pairs the k-th added line with the k-th removed line, purely by offset, because a textual diff is all it has. mkit hashes line content, so for a reformat/reorder commit it can often identify the line's true surviving origin directly — including cases where the positional pass finds no in-hunk counterpart at all and git would credit the noise commit itself. This is the same shape as mkit's content-addressed rename detection giving a more reliableRthan git's similarity heuristic instatus/diff. The default--ignore-revfall-through is git's exact positional guess, byte-for-byte — parity is the contract there;--ignore-rev-preciseis a conscious, documented better-than-git mode, not a changed default.mkit remote renameand prefix-nested remote names (#660/#789) — with prefix-nested remote names configured (aanda/bboth exist), mkit's rename moves only the named remote's own tracking refs and bridge state, leaving a nested sibling's exactly where they are; git's own rename (verified on 2.50.1) silently drags the sibling's refs to the new name while its config still points at the old one.remote removewas already per-ref-filtered the same way in git, so mkit only reaches parity there. This protection is keyed on the source name, not the destination: a rename whose destination nests under an unrelated configured sibling (rename a/b awhilea/xis configured, orrename a a/bwhilea/b/cis configured) is outside what the source-side check can see, so it fails closed with the standard warning rather than merging into the sibling or dragging it along — nothing moves, nothing is lost, the sibling is untouched. Deliberate boundary, not a bug; seemove_state_dir's doc inmkit-cli/src/commands/remote.rs.