@senad-d/branchme 0.3.16 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.17 - Unreleased
4
+
5
+ - `revert_changes` refuses only ignored files at the paths the reverted commit touches, which Git could overwrite. Before, any ignored file in the checkout (`.env`, `.pi/`, `node_modules`) refused the revert, and in a repository with installed dependencies the whole-checkout listing passed the 4 MiB read cap first ("Local Git inspection exceeded the 4 MiB safety limit"), so the tool could not run in a working project.
6
+
3
7
  ## 0.3.16 - Unreleased
4
8
 
5
9
  - Added `commit_changes`, ported from CommitMe with the same behavior: `action: "gather"` returns read-only bounded commit context; `action: "commit"` commits an explicit subject or drafts one with the active model; `paths`, `steeringPrompt`, `confirm`, and unsafe-file approval work as before. The tool uses BranchMe's Git runner, lock retry, redacted Git errors, 30-second mutation timeout, worktree routing, tool mutation coordinator, and blocker guidance. Commits now stage and commit from the repository root, fixing CommitMe's failure when Pi runs in a subdirectory. The `/commitme` command is not included. Uninstall `@senad-d/commitme` to avoid two commit tools.
package/SECURITY.md CHANGED
@@ -50,7 +50,7 @@ Outside `commit_changes`, BranchMe does not create user-authored commits or acce
50
50
 
51
51
  `restore_paths` previews selected tracked paths and applies only with explicit discard authorization, fresh HEAD/index expectations, and matching per-file fingerprints. An explicit source must be pinned to a full commit when applying. The index's staged entries stay unchanged (Git may refresh stat-cache bytes), while approved working paths can be replaced or deleted. Untracked/ignored content, symlink ancestors, unsupported index modes, and directory expansion are protected/refused. No reset/clean or automatic destructive rollback exists; a partial failure reports that selected paths may already have changed.
52
52
 
53
- `revert_changes` requires one contained non-merge full commit and a clean idle checkout with current expectations, and refuses ignored residue. It uses `git revert --no-commit` with rerere disabled. Conflicts use owned `REVERT_HEAD` evidence before abort and verified restoration. A successful inverse may leave operation markers; owned state is cleared by `git revert --quit`, preserving the staged inverse without creating a commit. Unknown ownership or uncertain cleanup blocks a success claim. No generic sequencer continuation or hidden commit call exists.
53
+ `revert_changes` requires one contained non-merge full commit and a clean idle checkout with current expectations, and refuses ignored local-only files at the paths the commit touches (Git could overwrite them); ignored files elsewhere, such as `.env` or installed dependencies, do not block it. It uses `git revert --no-commit` with rerere disabled. Conflicts use owned `REVERT_HEAD` evidence before abort and verified restoration. A successful inverse may leave operation markers; owned state is cleared by `git revert --quit`, preserving the staged inverse without creating a commit. Unknown ownership or uncertain cleanup blocks a success claim. No generic sequencer continuation or hidden commit call exists.
54
54
 
55
55
  `list_remotes` returns bounded sanitized destination previews and a configuration fingerprint, not arbitrary config contents. Add-only `add_remote` requires an unused non-overlapping name, credential-free HTTPS/SSH destination, current config expectation, regular repository config, and no URL rewriting. `set_upstream` changes only one local branch's remote/merge keys for an existing direct remote-tracking ref or explicit unset; inherited/included/ambiguous/worktree-specific settings are refused. Config mutations are common-directory queued and verified, never change global/system config, and never fetch automatically. Failed verification reports uncertain config rather than rolling it back blindly.
56
56
 
@@ -59,7 +59,7 @@ Restoration is limited to currently index-tracked files. Untracked/ignored files
59
59
 
60
60
  ## Prepare a no-commit revert
61
61
 
62
- `revert_changes({ commit, expectedHead, expectedIndex, worktreePath? })` applies the inverse of one full-ID non-merge commit already contained in HEAD. It requires clean idle state and refuses ignored local-only files. HEAD, branch, refs, and configuration remain unchanged; inverse changes are staged and ordinary commit creation remains a separate explicit workflow.
62
+ `revert_changes({ commit, expectedHead, expectedIndex, worktreePath? })` applies the inverse of one full-ID non-merge commit already contained in HEAD. It requires clean idle state and refuses ignored local-only files at the paths the commit touches; ignored files elsewhere do not block it. HEAD, branch, refs, and configuration remain unchanged; inverse changes are staged and ordinary commit creation remains a separate explicit workflow.
63
63
 
64
64
  Conflicts attempt `git revert --abort` and verify restoration. A successful `--no-commit` application may still leave REVERT_HEAD; BranchMe verifies ownership, then uses `git revert --quit` to clear only that operation while preserving its index/worktree result. This is not a general sequencer continuation API. Uncertain recovery requires a person; no reset, stash, commit, or private recovery state is introduced. Returned paths may be partial; enumerate `git_status` for every resulting path.
65
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senad-d/branchme",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "type": "module",
5
5
  "description": "Pi extension for verified Git repository initialization, commit, branch, worktree, integration, retirement, push, and pull request workflows.",
6
6
  "license": "MIT",
package/src/git-revert.ts CHANGED
@@ -8,13 +8,13 @@ async function revertInside(host: GitAPI, reader: GitReader, input: RevertInput)
8
8
  await requireSimpleIndex(reader);
9
9
  const before = await getGitStatus(reader.pi, reader.ctx, {}, reader.signal, reader.tokens);
10
10
  if (before.head !== input.expectedHead || before.index !== input.expectedIndex || before.hasChanges) throw new Error("No-commit revert requires fresh HEAD/index expectations and a clean idle checkout.");
11
- if ((await readGit(reader, ["ls-files", "--others", "--ignored", "--exclude-standard", "-z"])).stdout) throw new Error("No-commit revert refuses ignored local-only files; preserve them outside the checkout first.");
12
11
  const metadata = (await readGit(reader, ["rev-list", "--parents", "-n", "1", input.commit, "--"])).stdout.trim().split(" ");
13
12
  if (metadata[0] !== input.commit || metadata.length > 2 || !metadata.every(validOid) || !(await isCommitAncestor(reader.pi, reader.ctx, input.commit, input.expectedHead, reader.signal))) throw new Error("Revert requires one non-merge commit already contained in current HEAD.");
14
13
  if (metadata[1]) {
15
14
  const tree = await readGit(reader, ["ls-tree", "-r", "--format=%(objectmode)", metadata[1]]);
16
15
  if (tree.stdout.split("\n").includes("160000")) throw new Error("Reverts introducing gitlinks are unsupported.");
17
16
  }
17
+ await refuseIgnoredTouched(reader, input.commit);
18
18
  const branch = (await getCurrentBranch(reader.pi, reader.ctx, reader.signal)).currentBranch;
19
19
  const refs = (await readGit(reader, ["show-ref"])).stdout;
20
20
  const config = (await readGit(reader, ["config", "--null", "--list"])).stdout;
@@ -58,6 +58,16 @@ async function verifyRevert(host: GitAPI, final: GitReader, input: RevertInput,
58
58
  if (after.index !== applied.index || after.head !== before.head || after.operations.length || (await getCurrentBranch(final.pi, final.ctx)).currentBranch !== branch || (await readGit(final, ["show-ref"])).stdout !== refs || (await readGit(final, ["config", "--null", "--list"])).stdout !== config) throw new Error("Revert postconditions are uncertain; inspect git_status before another mutation.");
59
59
  return { action: "revert_changes", commit: input.commit, head: after.head, index: after.index, committed: false, stagedPaths: after.entries.map((entry) => entry.path), pathsComplete: after.complete, note: "Inverse changes are staged, not committed. Enumerate git_status for all paths; ordinary commit creation is a separate explicit workflow." };
60
60
  }
61
+ // Git may overwrite an ignored file when a revert brings a tracked path back, so
62
+ // only ignored files at paths the commit touches block it; .env, .pi/ and
63
+ // installed dependencies elsewhere do not (a whole-checkout listing passed the
64
+ // 4 MiB read cap in a repository with node_modules).
65
+ async function refuseIgnoredTouched(reader: GitReader, commit: string) {
66
+ const touched = (await readGit(reader, ["diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "--root", commit, "--"])).stdout.split("\0").filter(Boolean);
67
+ if (!touched.length) return;
68
+ const ignored = (await readGit(reader, ["ls-files", "--others", "--ignored", "--exclude-standard", "-z", "--", ...touched.map((path) => `:(literal)${path}`)])).stdout.split("\0").filter(Boolean);
69
+ if (ignored.length) throw new Error(`No-commit revert refuses ignored local-only files at paths the commit touches (${ignored.slice(0, 5).join(", ")}); preserve them outside the checkout first.`);
70
+ }
61
71
  function validOid(value: string): boolean { return OBJECT_ID.test(value); }
62
72
  export async function revertChanges(host: GitAPI, ctx: GitCommandContext, input: RevertInput, signal?: AbortSignal, tokens: readonly string[] = []) {
63
73
  validateKeys(input, ["commit", "expectedHead", "expectedIndex", "worktreePath"]);