Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3bc0e6f
feat(cli): add restore/record/export provisioning commands
Shinrai Jul 13, 2026
9cdccb0
fix(cli): link accepts an empty gitlink dir and records its URL
Shinrai Jul 13, 2026
b68f5d4
test: end-to-end provisioning tests
Shinrai Jul 13, 2026
fa9f54c
docs: document provisioning commands and update coverage matrix
Shinrai Jul 13, 2026
f947db3
fix: harden provisioning target guards (review)
Shinrai Jul 14, 2026
6db4b13
fix: symlink-safe target guards + scp-root convention (review round 2)
Shinrai Jul 14, 2026
425e547
test: gate symlink-guard cases behind a creation-capability probe
Shinrai Jul 14, 2026
b454487
fix(cli): correct restore summary buckets; docs: four resolution sources
Shinrai Jul 14, 2026
f9f5dbe
fix: manifest shape hardening + accurate link docs (review round 4)
Shinrai Jul 14, 2026
2dfb3ef
chore: merge next into feat/provisioning-cli (bring workflow suite in…
Shinrai Jul 14, 2026
0625a0b
fix: git end-of-options guards + link registry-key normalization (rev…
Shinrai Jul 14, 2026
91edfe2
fix(restore): normalize --skip / path filters to gitlink-path spellin…
Shinrai Jul 14, 2026
bc849cd
feat(cli): branch-aware restore + day-2 sync command
Shinrai Jul 17, 2026
9496c09
fix(embedded): symlink-safe restore, normalized record filters, sync …
Shinrai Jul 18, 2026
9aa8bf5
fix(embedded): refuse a symlinked gitlink path in sync (review round 8)
Shinrai Jul 18, 2026
4be400c
fix(embedded): refuse unreadable targets, surface fetch failures (rev…
Shinrai Jul 18, 2026
e35b01c
fix(embedded): surface git-branch failure in sync; -- guard in branch…
Shinrai Jul 18, 2026
59e1f72
fix(embedded): anchor clone to repo root; split merge-base error from…
Shinrai Jul 18, 2026
6c037fb
fix(embedded): surface git stderr in detached-checkout failure notes …
Shinrai Jul 18, 2026
8fb1b64
chore: merge next into feat/provisioning-cli
Shinrai Jul 19, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules/
dist/
coverage/
reference/
tmp/
.idea/
.vscode/
*.tsbuildinfo
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,88 @@ git config advice.addEmbeddedRepo false

The committed parent tree now contains a gitlink at `embedded-child` pinning the child's current HEAD. No `.gitmodules` is created; the child's URL never lands in the public repo.

`link` clones into a missing **or empty** target directory (a fresh clone of a parent materializes each gitlink as an empty dir, so `link` works to fill one in); it refuses anything else β€” a non-empty directory, a file, a symlink (even to an empty dir), or an unreadable path. After staging, it also records the child's URL and branch into this clone's local registry (see below).

## Restoring embedded children (machine-B bootstrap)

The parent commits only anonymous gitlinks β€” a path and a pinned SHA, never a URL. So a fresh clone of the parent materializes each embedded child as an _empty directory_: git knows the pin but has nowhere to fetch it from. `git embedded restore` fills those directories in.

```bash
git clone <parent-url> myproject
cd myproject
git embedded restore # clone every embedded child and check out its pinned SHA
```

`restore` resolves each child's clone URL from up to four **optional** sources, strictest first, stopping at the first that yields a URL:

1. **Local config registry** β€” `embedded.<path>.url` (and `embedded.<path>.branch`, see below) in _this clone's_ `.git/config`. Per-clone, never committed. Written automatically after a successful restore, and by `record` / `link`.
2. **Manifest file** (`--from <file>`) β€” a JSON transfer file carried out-of-band (never committed). See `export` below.
3. **`--base <url-base>`** β€” derives `<url-base>/<basename>.git` for each child.
4. **Convention** (zero state) β€” the child is a sibling of wherever the parent was cloned from: the parent's origin with its last path segment replaced by `<basename>.git`. A URL- or path-style origin splits on the final `/` (`https://host/org/parent.git` β†’ `https://host/org/tests.git`); a scp-style origin whose repo sits at the path root has no `/`, so the sibling is taken after the last `:` instead (`git@host:parent.git` β†’ `git@host:tests.git`). No configuration, but it only resolves when the child's repository is actually named after the gitlink path and sits beside the parent. A convention guess can only ever name strings already derivable from the committed tree, so it discloses nothing new.

Every clone is **SHA-verified**: the parent's pinned commit must exist in the freshly cloned child (a `git fetch` is attempted first). If it doesn't β€” e.g. a convention guess resolved to the wrong repository β€” the clone `restore` created is removed and the child is reported `pinned-mismatch`. A wrong guess fails closed; it never plants the wrong code.

Per-child outcomes are `restored`, `already-present`, `unresolved`, `pinned-mismatch`, or `skipped`, and `restore` exits non-zero if any child ends `unresolved` or `pinned-mismatch`. Use `--dry-run` to report resolution without cloning.

### Branch-aware checkout

A restored child does not have to end up detached. `restore` resolves a **branch** for each child with the same layering as the URL β€” `embedded.<path>.branch` in the local registry, then the manifest β€” and when neither supplies one, it infers the branch from the pin: if exactly **one** `origin` branch contains the pinned commit, that branch is used. With a branch, the child ends ON it at the pin (`checkout -B`), with upstream tracking set to `origin/<branch>` when it exists, and the branch is auto-registered like the URL. An ambiguous pin (on several branches) or an unmatchable one keeps today's detached checkout β€” inference never guesses.

**Partial restore is the normal case.** A public contributor without access to a private child simply skips it:

```bash
git embedded restore --skip tests # comma-separate several: --skip tests,vendor/foo
```

### Obscured children

A child whose repository name does not match its gitlink path β€” the intended state for a hidden private child β€” is deliberately _not_ convention-resolvable. Provide its URL once (via `link` into the empty gitlink dir, or `record` if it is already cloned) and this clone's registry remembers it for every later restore:

```bash
git embedded link tests git@example.com:org/private-tests.git
# ...or, if the child is already present on disk:
git embedded record
```

### Sharing URLs between machines: `export` / `record`

`record` writes the origin URL (and current branch) of every present child into the local registry. `export` serializes that registry to a manifest another machine can consume:

```bash
git embedded export --scan -o children.json # record present children, then write the manifest
```

On the other machine:

```bash
git clone <parent-url> myproject && cd myproject
git embedded restore --from children.json
```

> **Never commit the manifest.** It contains the very URLs the anonymous-gitlink design keeps out of the tree. When `export -o` writes inside the worktree it appends the filename to `.git/info/exclude` as a courtesy, but keeping the manifest out-of-band is your responsibility.

## Day-2: syncing pins

When the parent pulls commits that move gitlink pins, the children on disk are still at the old SHAs. `git embedded sync` moves them β€” and only them; sync never touches the parent, so pulling the parent first is your step:

```bash
git pull
git embedded sync
```

Per child, sync is deliberately conservative β€” a clean child follows the pin, anything that looks like your work is reported and left alone:

- **already at the pin** β€” nothing to do (`in-sync`).
- **uncommitted changes** β€” left alone (`dirty`).
- **on the registered branch** (`embedded.<path>.branch`), clean β€” the branch is moved to the pin **fast-forward only**: the child's HEAD must be an ancestor of the pin. Commits beyond the pin are your work (`ahead`, left alone).
- **on any other branch** β€” left alone (`unregistered-branch`).
- **detached**, clean β€” snapped to the pin, staying detached (`synced`).
- **pin not present locally** β€” one `git fetch origin` inside the child; if the pin still cannot be found the child is reported `pin-unavailable` and sync exits non-zero.

Only `pin-unavailable` (and an unexpected checkout failure) fail the run β€” the left-alone outcomes protect in-progress work and exit zero. `sync` takes the same `[paths…]`, `--skip`, and `--dry-run` surface as `restore`.

If the hooks from this package are installed, most parent operations already update the children automatically (detached, like standard submodules). `sync` covers the rest: hook-less clones, the `git reset --hard` gap, and keeping a child _on its branch_ as pins advance.

## Manual install (no CLI)

If you'd rather wire things up by hand:
Expand Down
Loading
Loading