feat(cli): add export-patch command to export local spec edits as upstream patches#2162
Open
edenfunf wants to merge 4 commits into
Open
feat(cli): add export-patch command to export local spec edits as upstream patches#2162edenfunf wants to merge 4 commits into
edenfunf wants to merge 4 commits into
Conversation
…tream patches (microsoft#2118) Replays the locked install (same machinery as the audit drift check) and re-expresses every locally modified managed file as a unified diff against the package source file that deployed it, one .patch file per package, applicable with 'git apply' at the base recorded in the patch header (resolved commit for git deps, version/URL for registry deps). Reverse mapping is content-addressed: a modified finding is exportable iff the replayed content is byte-identical (after drift normalization) to exactly one file in the owning package's source tree. Transformed deployments (rule-dir frontmatter rewrites, compiled or aggregated outputs, resolved links), ambiguous matches, local path deps, and project-local content are skipped with an explicit reason instead of producing a patch that would not apply. The command consumes the replay/diff APIs read-only and never mutates the project tree, cache, or lockfile; patch files are the only output. Local deletions (unintegrated findings) are never exported since they do not imply upstream removal intent. Covered by unit tests for mapping/skip/diff semantics and an e2e closed loop: edit -> export -> git apply in the package source -> updated snapshot -> nothing left to export.
Address correctness and robustness gaps found in review of the initial export-patch implementation: - Gate exports on raw-bytes cleanliness of the matched source: a CRLF-, BOM-, or build-id-bearing source digest-matches in normalized space but would reject every hunk at git-apply time. Skipped with an accurate reason instead of exporting an unappliable patch. - Disambiguate colliding patch filenames with a digest suffix and use an ASCII-only sanitizer (isalnum accepted Unicode) plus Windows reserved-name handling; previously one package's patch could silently overwrite another's. - Deduplicate edits when one source file is deployed to several targets: identical edits export as a single diff, divergent edits are reported as a conflict instead of emitting clashing hunks. - Resolve diff-phase targets with apm.yml's explicit target, matching the replay's own resolution; auto-detection alone silently hid findings for declared targets whose root directory does not exist. - Split diff lines on newline only: str.splitlines also breaks on bare CR / form feed / U+2028 and desyncs hunk counts from git's line model. - Sanitize lockfile-sourced header fields; an embedded newline could break out of the comment block and inject applyable diff content. - Add an outer exception net (exit 1 with a message instead of a traceback), reject --out paths inside apm_modules/, distinguish an unparsable lockfile from a missing one, and short-circuit all-local lockfiles before the replay. - Exclude .git/__pycache__/symlinks from the reverse-mapping index and track unindexed files (size cap, read errors) so skip reasons never misreport them as transformed; write patch files atomically. Every fix carries a regression test (17 new). Also adds the changelog entry and syncs the apm-guide command reference and the docs page.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new apm export-patch CLI command that turns local edits to APM-managed deployed files into git apply-compatible patch files against the originating package sources, using the drift replay/diff machinery as a read-only signal.
Changes:
- Implement
export-patchcommand + core reverse-mapping/diff generation logic with per-package patch output. - Add unit and integration coverage for patch generation, edge cases, and a closed-loop
git applyconvergence test. - Document the new command (docs site + apm-guide command reference) and add a changelog entry.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/commands/export_patch.py |
New command implementation: replay/diff integration, reverse mapping, patch/header generation, and patch file output. |
src/apm_cli/cli.py |
Wires export-patch into the root CLI. |
tests/unit/commands/test_export_patch.py |
Unit tests for filename sanitization, diff shape, reverse mapping, skip reasons, and CLI error handling. |
tests/integration/test_export_patch_e2e.py |
End-to-end closed-loop test exercising export → git apply → convergence. |
docs/src/content/docs/reference/cli/export-patch.md |
New CLI reference page for apm export-patch. |
packages/apm-guide/.apm/skills/apm-usage/commands.md |
Updates the apm-guide command list to include export-patch. |
CHANGELOG.md |
Adds an Unreleased entry for the new command. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Private-registry and authenticated-source URLs can embed credentials (user:token@host); patch files are made to be shared, so source and resolved-URL header fields now pass through a shared _strip_userinfo helper before being written. Query-string '@' and plain owner/repo sources are unaffected (covered by regression tests). Also appends the PR number to the changelog entry per repo convention.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2118
What
New
apm export-patchcommand: exports local edits to APM-managed files as unified diffs against the packages that deployed them -- onegit apply-ready.patchfile per package, with the locked base (resolved commit for git deps, version/URL for registry deps) recorded in the patch header.How it works
install/drift.py) read-only: replay the locked install into a scratch tree, diff against the project, and take themodifiedfindings.unintegrated) are never exported -- deleting a deployed copy does not imply upstream removal intent.Scope notes
commands/export_patch.py: it consumes replay/diff APIs read-only and adds no install/lockfile semantics, so no normative critical path is touched (Mode B detector is clean).target:, matching the replay's own resolution._check_driftinpolicy/ci_checks.pyhas the same auto-detect-only pattern; left untouched here to keep scope contained -- can file a follow-up.Testing
apm export-patch->git applyin the package source -> update the snapshot -> the same command reports nothing left to export.microsoft/apm-sample-package: exported patch applies cleanly withgit applyin a fresh clone at the recorded base commit.Docs page, changelog entry, and the apm-guide command reference are included.