Skip to content

Full-opacity redesign: no gitlinks, encrypted manifest holds all submodule state #16

Description

@ikma-ricon

Full-opacity redesign: no gitlinks, encrypted manifest holds all submodule state

Motivation

The current design hashes directory names and encrypts the manifest, but still leaves a gitlink (mode 160000 + SHA) visible in the git tree. A public observer of a repo using modules sees:

  • submodules/ directory (reveals the feature is in use)
  • One hashed dir per module (reveals count of modules)
  • A gitlink SHA per module (reveals the pinned commit of each module)

The SHA is the leaky bit. If the upstream repo is public, anyone can search GitHub for that commit hash and resolve it back to the upstream repo — the hashed dir name buys nothing.

Target use case: public home repo with private dependency graph. An observer should learn nothing about dependencies beyond "this repo uses modules."

Proposal

Git tracks nothing under submodules/. No gitlinks, no hashed dirs. All submodule state lives in the encrypted manifest. submodules/ becomes pure local state.

What's in git

  • .modules/manifest — encrypted JSON (git-crypt). The only file that reveals modules are in use.
  • .gitignore entry for submodules/.

What's in the manifest

{
  "fold": {
    "url": "https://github.com/ricon-family/fold.git",
    "pin": "4feed004a2ae5ba710b7ae643847ab92d5669086"
  },
  "den": {
    "url": "https://github.com/ricon-family/den.git",
    "pin": "321bf53..."
  }
}

The path field is gone — path is always submodules/<name>/ locally.

What's on disk (after modules unlock && modules init)

submodules/
├── fold/     ← real clone, readable, gitignored
└── den/      ← real clone, readable, gitignored

No hashing. No symlinks. cd submodules/fold just works.

Command semantics

Command Behavior
modules setup creates .modules/manifest, gitignores submodules/, installs hooks, assigns manifest to git-crypt via rudi
modules add <url> [--name n] [--ref r] clones to submodules/<name>/, records url+pin in manifest
modules init reads manifest, clones each entry + checks out pin
modules update [name] fetches + bumps manifest pin to local HEAD
modules status compares local HEADs to manifest pins (drift detection, replaces what gitlinks gave us for free)
modules remove <name> rm clone dir, drop from manifest
modules lock / unlock thin wrappers around rudi lock / unlock

What GitHub observers see (locked)

.modules/manifest     ← encrypted, unreadable
.gitignore            ← contains "submodules/"

Presence of the feature leaks. Nothing else — no names, no SHAs, no count.

What we lose + how we replace it

Git's built-in "submodule pin drift" signal in git status. Replacement: pre-commit hook runs modules status and warns if any submodules/<name>/ HEAD ≠ manifest pin. Same safety net, implemented by the tool.

Migration

Existing repos (den, fold clones in the wild) need a one-time modules migrate task:

  1. Read old manifest (with path + pin).
  2. For each entry: move submodules/<hash>/submodules/<name>/, remove gitlink from index.
  3. Rewrite manifest to drop path field.
  4. Move manifest: submodules/.manifest.modules/manifest.
  5. Update .gitignore: add submodules/.
  6. Update .gitattributes (rudi assign): repoint git-crypt from submodules/.manifest to .modules/manifest.

One commit. Reversible if we keep the old manifest format as a backup.

Open questions

  1. Manifest location.modules/manifest (dedicated namespace, mirrors .git/) vs modules.lock at root vs .modules.encrypted. Leaning .modules/manifest.
  2. Pin drift enforcement — pre-commit hook should warn by default, with a strict mode available as an opt-in. Alternative: always block, with an explicit override flag.
  3. Name conflicts — today hashing saves you from modules add fold twice. With readable names, we error on duplicate. Calling it out; fine as-is.
  4. Breaking change — only den and fold use this in production. Migration task makes it a single commit. Propose landing v1.0.0 with the new design + migration in the same release.

Non-goals

  • Hiding the presence of modules (the encrypted manifest itself is a signal). Accepted — can't hide that a feature is in use, only its contents.
  • Supporting both old (hashed + gitlink) and new (manifest-only) modes. Two modes is long-term complexity. Clean break + migration.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions