Skip to content

Repository files navigation

gitty icon

gitty

diligent backup machine

land everything that can land, every run

GitHub npm



Path You get Verdict
git
  • git commit all-or-nothing
  • one conflict / huge file / submodule
  • whole checkpoint dies
  • remote unchanged


stalls the backup
rebase
  • pull --rebase
  • same conflict N times
  • dirty tree mid-flight
  • other host still stranded


sync theater
gitty
  • gitty drip
  • push the clean subset
  • hold blockers locally (size / submodule / hook)
  • park only if nothing can land


backup continues
flowchart LR
  T[tree] --> Q{can this path land?}
  Q -->|yes| P[commit + push]
  Q -->|conflict / too big / submodule / hook reject| H[hold back]
  P --> R[keep going]
  H --> R
  R --> F{anything still blocked?}
  F -->|held some| D[remote has the rest]
  F -->|nothing landed| K[park bak/pending-merge]
Loading

Issue

❌ All-or-nothing stall

  • one conflicted, oversized, or submodule path parks the whole checkpoint

  • the rest of the tree sits unbacked on that machine

  • drip: push the clean subset; hold the rest; never stall the backup

❌ Dual-machine strand

  • laptop + desktop both edit

  • N local commits replay against remote -> same conflict N times

  • other machine's work never left that machine

❌ Checkpoint friction

  • agents touch many repos per session

  • add + commit + push per repo burns time

  • one blocker used to abort the whole run

❌ Hook-block stalls the clean subset

  • pre-commit rejects one path (context-pollution, tilde, policy)

  • rest of the staged tree was fine

  • drip: eliminate the offender, land the rest, hold with a specific reason

❌ Append ledger false conflicts

  • two hosts append different .ndjson / .jsonl lines

  • git sees one slot, two lines -> conflict

  • nothing was edited; both lines should keep


Options

Bin Job When
gitty gitty drip + holdback -> commit -> push default backup
lfs gittylfs same + LFS track big binaries
git gittyembedded submodules then parent dirty nested repos
health gittyhealth report only inspect / mid-op
snap gittysnap snap -> merge -> abort clean solo multi-machine
union gittyunion NDJSON union driver append ledgers

Separate bins. Same package. Plain gitty != gittysnap.


Setup

# scoped package; same bin names
npm i -g @vd7/gitty

Optional resolvers (missing file -> empty allowlist -> drip/park, no auto-resolve):

per_host:
  paths:
    - registry/system-resource/manifest.json
  globs:
    - snapshots/$(hostname)/**
semantic_union:
  globs:
    - "**/*.ndjson"
    - "**/*.jsonl"
  key_field: ts

Usage

path do this get this
A gitty drip + holdback checkpoint on remote
B gittysnap snapshot-first solo sync
C gittyunion install NDJSON union merge driver
D gitty-dispatch config-driven Git operation routing

Path A · gitty

# land whatever can land; hold blockers; park only if nothing can
gitty "checkpoint" "$HOME/Documents/a/gitty"
gitty -v

GITTY_PARTIAL=1 default. Park-only: GITTY_PARTIAL=0. Bulldoze: GITTY_FORCE=1.

Integration preserves configured Git clean/smudge filters. If Git's internal merge snapshot fails on a filtered worktree, gitty computes the clean merge tree from Git objects, runs merge hooks against that tree, and restores only incoming paths after proving they do not overlap local worktree changes.

Holdback reasons:

  • optional .gitty/holdback-reasons.json or GITTY_HOLDBACK_CATALOG

  • maps hook stderr → stable [CODE] reason

  • missing catalog → offender stderr (never mute constant)

Stale remote tip: autoheal fetch+rebase unless GITTY_NO_STALE_BASE_HEAL=1.

Path B · gittysnap

# both hosts on remote first; rc=2 on conflict; worktree clean
gittysnap

Path C · gittyunion

# union + dedupe + stable-sort for .ndjson / .jsonl
gittyunion install

Path D · gitty-dispatch

gitty-dispatch --config /absolute/path/dispatch.json --require-match -- push origin main

Configuration supplies policy; the binary supplies exact dispatch:

{
  "dispatch": {"version": 1, "real_git": "/usr/bin/git"},
  "guarded_repos": [{
    "match": {"remote_urls": ["https://example.invalid/team/repo.git"]},
    "dispatch": {
      "mode": "redirect",
      "intercept_ops": ["push"],
      "passthrough_ops": ["status", "log", "diff"],
      "driver_argv": ["/absolute/path/sync-driver", "{repo_root}", "{operation}"],
      "authorized_ancestor": "/absolute/path/sync-driver"
    }
  }]
}

The driver runs when an intercepted operation has no exact canonical driver in its process ancestry. Descendant Git calls pass through. Environment markers, substrings, basenames, and shell evaluation never authorize a request. Rules, driver paths, and intercepted operations remain consumer-owned configuration. Driver paths may be absolute or start with {config_dir}/; both are canonicalized before comparison. Use the standalone {git_args} element when a driver needs the original Git argument vector. Git, Zsh, and jq are required. Linux supports whitespace in driver paths; macOS ancestry verification fails closed for such paths because ps does not expose exact argument boundaries. Matched repositories are default-deny: every literal Git operation belongs in either intercept_ops or passthrough_ops. Configuration and configured executables are trusted inputs; process ancestry prevents accidental or agentic route-around, not attacks by another process already running as the same user.

Output

signal meaning
partial integrate resolved subset committed; held paths stay local
holdback oversized / submodule / push-reject / hook-reject left unstaged
[CODE] reason catalog match for pre-commit reject (else offender stderr line)
elimination scan hook blocked commit; drip retries without each staged path
park nothing could land; bak/pending-merge-* + remote-snapshot-*
merge error original Git diagnostic; exit 2; no conflict parking

Local branch stays put on park. Nothing lost.

Other merge or merge-commit failures stop with exit 2 rather than masquerading as parked conflicts. Any in-progress merge is left in place for manual recovery.

Gotchas

problem fix stability why
one unresolvable file parks whole merge drip (GITTY_PARTIAL=1): commit resolved subset stable backup as much as possible
oversized path blocks push leave GITTY_PARTIAL=1 stable hold back; push the rest
pre-commit rejects one staged path leave GITTY_PARTIAL=1; elimination drip holds that path stable clean subset still lands
mute pre-commit hook rejection reason .gitty/holdback-reasons.json or GITTY_HOLDBACK_CATALOG stable stable [CODE] + fix hint
remote ahead / stale-base guard leave autoheal on; GITTY_NO_STALE_BASE_HEAL=1 to skip stable fetch+rebase then retry
N-commit rebase replays same conflict N times merge --ff-only then --no-ff once stable one merge, one conflict set
per-host artifact conflicts every sync .gitty/additive-resolvers.yaml per_host stable ours on that host; --no-ff keeps both parents
.ndjson append conflict semantic_union or gittyunion install stable union + dedupe + stable-sort
fresh clone drops union driver re-run gittyunion install stable attrs alone insufficient
nothing can land park bak/pending-merge-* stable first-class ref; nothing lost; non-fatal
foreign-owned .git (old sudo git) sudo chown -R "$(whoami)" .git stable fail-fast; GITTY_SKIP_PERM_CHECK=1 opt out

Regression tests

npm run test:integrate (also included in npm test) requires Git, zsh, and git-crypt. It exercises integration using disposable local repositories and local remotes only.

Docs

Doc Topic
docs/README index
docs/commands all bins
docs/gittysnap snapshot-first sync
docs/gittyunion NDJSON union driver
docs/partial-commit holdback + drip
docs/environment env
docs/hooks repo hooks

npm tarball = bin/ + README only. docs/ is GitHub.

Contact

vd7.io   /vdutts7

About

git add, commit, and push in one command from any dir, for any repo

Topics

Resources

Stars

10 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages