Skip to content

Record clone-time commit SHA in state; add mepo changed command #381

Description

@mathomp4

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

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions