Background
Currently mepo clone reads components.yaml and records each component's version (branch name, tag name, or hash) in state.0.json, but does not record the actual resolved commit SHA at the moment of cloning. This means there is no reliable baseline against which to later compare what a user has changed in their working tree — especially for components pinned to a branch, where the branch tip may have moved since the clone.
Part 1 — Record clone-time SHA in state.json
Problem: For a component pinned to a branch (e.g. branch: main), the exact commit at clone time is lost once the upstream branch advances. For a tag the tag name is stable, but recording the SHA alongside it is still valuable as a concrete comparison baseline. There is currently no field in MepoVersion or the serialized state for this.
Proposed change: When mepo clone (or mepo init) clones each component, immediately after the clone/checkout call git rev-parse HEAD and store the resulting SHA in the state alongside the existing version info.
Likely implementation touches:
- Add a
clone_hash field to MepoComponent (separate from the existing version namedtuple, since version is mutable via mepo save but the clone hash should be immutable).
- In
clone.py, after cloning each component call git.get_local_latest_commit_id() and store it on the component before the first write_state().
- Update
serialize() / deserialize() in component.py to round-trip the new field.
state.0.json thus becomes the authoritative, immutable record of the exact commits present at clone time.
Part 2 — mepo changed command
A new command that iterates over all components and, for each one where the current state differs from the clone-time baseline, produces a complete diff in patch format.
What counts as "changed"
Any of:
- Current
HEAD SHA differs from the recorded clone-time SHA (new commits made, or branch advanced via pull)
- A different branch or tag is now checked out
- Uncommitted staged or unstaged modifications to tracked files
- Untracked new files not yet committed (e.g. a new Fortran source file mid-development)
Output format
A single unified diff / patch combining all of the above for each changed repo, suitable for use as a patch file. The goal is that a recipient could, in principle, reproduce the user's exact source-code state on top of the original clone baseline using git apply or patch.
Implementation approaches for capturing untracked files
Plain git diff <clone_sha> covers committed changes and uncommitted modifications to tracked files, but silently omits untracked new files. Two approaches are worth benchmarking:
Option A — git add --intent-to-add (preferred)
git add --intent-to-add . # register untracked files as empty→new in the index
git diff <clone_sha> # one command: commits + uncommitted + new files, all in proper git format
git restore --staged . # undo the intent-to-add staging, leaving working tree untouched
--intent-to-add (or -N) marks each untracked file as a staged empty file, making git diff treat it as a new-file addition with a proper diff --git a/... b/... header. The output is fully git apply-compatible. git restore --staged . cleanly removes the staging without touching the working tree.
Option B — synthetic diff per untracked file (shell loop)
git diff <clone_sha> # committed + tracked uncommitted changes
git ls-files --others --exclude-standard | while read f; do
diff --unified /dev/null "$f"
done
Produces a valid unified diff for new files, but the output uses --- /dev/null / +++ <file> headers rather than the diff --git format, so may be less reliably parsed by git apply. Avoids any temporary index mutation, which could be relevant in large repos or under concurrent use.
Both options should be tested for correctness and performance across repos of varying size.
Suggested CLI flags
--output <file> — write combined patch to a file instead of stdout
--repo <name> — limit output to a specific component
--no-untracked — skip untracked new files (faster; useful if only committed/staged changes are of interest)
Implementation notes
- The baseline SHA lives in
state.0.json (the first-ever written state), not state.json (which may have been updated by mepo save). The command should read state.0.json directly rather than via the current read_state() which always follows the state.json symlink.
- Repos where current SHA == clone SHA and working tree is clean should be silently skipped.
- The existing
--hashes flag on mepo status already shows current SHAs; mepo changed is the natural complement.
Related issues
Background
Currently
mepo clonereadscomponents.yamland records each component's version (branch name, tag name, or hash) instate.0.json, but does not record the actual resolved commit SHA at the moment of cloning. This means there is no reliable baseline against which to later compare what a user has changed in their working tree — especially for components pinned to a branch, where the branch tip may have moved since the clone.Part 1 — Record clone-time SHA in
state.jsonProblem: For a component pinned to a branch (e.g.
branch: main), the exact commit at clone time is lost once the upstream branch advances. For a tag the tag name is stable, but recording the SHA alongside it is still valuable as a concrete comparison baseline. There is currently no field inMepoVersionor the serialized state for this.Proposed change: When
mepo clone(ormepo init) clones each component, immediately after the clone/checkout callgit rev-parse HEADand store the resulting SHA in the state alongside the existing version info.Likely implementation touches:
clone_hashfield toMepoComponent(separate from the existingversionnamedtuple, sinceversionis mutable viamepo savebut the clone hash should be immutable).clone.py, after cloning each component callgit.get_local_latest_commit_id()and store it on the component before the firstwrite_state().serialize()/deserialize()incomponent.pyto round-trip the new field.state.0.jsonthus becomes the authoritative, immutable record of the exact commits present at clone time.Part 2 —
mepo changedcommandA new command that iterates over all components and, for each one where the current state differs from the clone-time baseline, produces a complete diff in patch format.
What counts as "changed"
Any of:
HEADSHA differs from the recorded clone-time SHA (new commits made, or branch advanced via pull)Output format
A single unified diff / patch combining all of the above for each changed repo, suitable for use as a patch file. The goal is that a recipient could, in principle, reproduce the user's exact source-code state on top of the original clone baseline using
git applyorpatch.Implementation approaches for capturing untracked files
Plain
git diff <clone_sha>covers committed changes and uncommitted modifications to tracked files, but silently omits untracked new files. Two approaches are worth benchmarking:Option A —
git add --intent-to-add(preferred)--intent-to-add(or-N) marks each untracked file as a staged empty file, makinggit difftreat it as a new-file addition with a properdiff --git a/... b/...header. The output is fullygit apply-compatible.git restore --staged .cleanly removes the staging without touching the working tree.Option B — synthetic diff per untracked file (shell loop)
Produces a valid unified diff for new files, but the output uses
--- /dev/null/+++ <file>headers rather than thediff --gitformat, so may be less reliably parsed bygit apply. Avoids any temporary index mutation, which could be relevant in large repos or under concurrent use.Both options should be tested for correctness and performance across repos of varying size.
Suggested CLI flags
--output <file>— write combined patch to a file instead of stdout--repo <name>— limit output to a specific component--no-untracked— skip untracked new files (faster; useful if only committed/staged changes are of interest)Implementation notes
state.0.json(the first-ever written state), notstate.json(which may have been updated bymepo save). The command should readstate.0.jsondirectly rather than via the currentread_state()which always follows thestate.jsonsymlink.--hashesflag onmepo statusalready shows current SHAs;mepo changedis the natural complement.Related issues
mepo status --hashes