Skip to content

feat: add commit_files for multi-path commits and fix repo-relative path resolution for nested config dirs - #679

Merged
cooper (czxtm) merged 2 commits into
mainfrom
09-04-scott-git-repo-rel-path
Nov 9, 2026
Merged

cooper (czxtm) merged 2 commits into
mainfrom
09-04-scott-git-repo-rel-path

Conversation

@scottmcmaster

@scottmcmaster Scott McMaster (scottmcmaster) commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This is to improve/harden the git layer to make it easier and less bug-prone supporting nested config dirs, based on this comment from Alex Shabalin (@alex-sparus) here: #655 (comment)

It also proactively adds the commit_files function which was in the PR that inspired said comment.

The main thing to look at is the repo_relative_path method -- the correctness of its logic as well as its call sites.

Test Plan

New unit tests.

Docs

  • Docs updated (companion PR in darkmatter/nixmac-web: #___)
  • No docs update needed

Copy link
Copy Markdown
Collaborator Author

@scottmcmaster Scott McMaster (scottmcmaster) changed the title scott-git-repo-rel-path feat: add commit_files for multi-path commits and fix repo-relative path resolution for nested config dirs Sep 4, 2026
@darkmatter

darkmatter Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

🎨 Storybook preview

Open Storybook preview

Updated for 0e785af


⚠️ Detected UI changes (5)

These stories' HTML snapshots changed. I've added screenshots + links to the changed stories below. Review them carefully then accept the changes to regenerate baselines and include them in this PR:

Flows/Evolve › Playground

Flows/Evolve › Playground

Flows/Evolve › 1. Begin (idle)

Flows/Evolve › 1. Begin (idle)

Flows/Evolve › 2. Evolving (progress)

Flows/Evolve › 2. Evolving (progress)

Flows/Evolve › Evolving With Error Event

Flows/Evolve › Evolving With Error Event

Flows/Evolve › 3. Review (changes generated)

Flows/Evolve › 3. Review (changes generated)


Accept UI changes

  • Click here to accept these changes

Alternatively, you can run bun run test:update-snapshots locally to re-generate the baselines and then push the changes to this PR.

What does this do?

The screenshots above show UI changes detected by the Storybook
snapshot tests run on this PR. Each image is the rendered output of
a Storybook story from the code in this PR branch; the snapshot
test compared it against the committed baseline in
__snapshots__/ and flagged the difference.

Checking the box tells the darkmatter[bot] to regenerate the
baselines from this PR's current code and commit them directly to
this branch. The new baselines become the source of truth for
future runs — only accept after confirming the visual changes are
intentional.

Comparison baseline: the committed __snapshots__/ files on this
PR branch (carried forward from develop). Accept updates them in
place on this branch.

@darkmatteragent darkmatteragent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — APPROVE

69e2e8d2c82f · 0 findings

Approve: solid hardening — every producer/consumer boundary checks out. - All app callers pass the git toplevel plus repo-relative filenames, which repo_relative_path reproduces unchanged; the nested-config-dir case is fixed as intended. - Per-hunk commit/discard keeps hunk granularity through find_hunk_by_hash; no sibling-row mutation exists. - Escape/symlink paths fail closed before mutation, and the index.write() reorder preserves intent-to-add entries, with new tests locking both in.

No findings.

@darkmatteragent

Copy link
Copy Markdown
Reviewer · darkmatter agent VERDICT
APPROVE
Approve: solid hardening — every producer/consumer boundary checks out. - All app callers pass the git toplevel plus repo-relative filenames, which `repo_relative_path` reproduces unchanged; the nested-config-dir case is fixed as intended. - Per-hunk commit/discard keeps hunk granularity through `find_hunk_by_hash`; no sibling-row mutation exists. - Escape/symlink paths fail closed before mutation, and the `index.write()` reorder preserves intent-to-add entries, with new tests locking both in.

No findings.

reviewed: 69e2e8d — 2026-09-04T07:53Z verdict: approve findings: 0

Reviewer avatar Reviewer
darkmatter agent
Run details
Metadata Value
Agent darkmatter/agent/reviewer
Session pr:darkmatter/nixmac#679
Kind pr
Runtime platform-runtime
Configured model litellm/glm-5.2-fp8
Target darkmatter/nixmac#679
Revision 69e2e8d2c82fe2af0ff501d8f13b678545dc336c
Verdict approve
Reviewed at 2026-09-04T07:53Z
Logs Open Braintrust session

@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor
Warnings
⚠️

No Linear issue ID found in this PR's title, description, or branch name (expected something like ENG-123). Add one so this work is traceable in Linear, or add #no-linear to the PR description to acknowledge it's intentionally untracked.

📋 PR Overview

Lines changed 425 (+396 / -29)
Files 0 added, 2 modified, 0 deleted
Draft / WIP no
Has Test Plan yes
Linear issue no
No Test Plan Needed no
New UI components no
New Storybook stories no
New Rust modules no
New TS source files no
New tests no
package.json touched no
Cargo.toml touched no
Infra / CI touched no

🔬 Coverage

Report Lines Statements Functions Branches
apps/native/coverage/coverage-summary.json 35.0% 34.4% 29.8% 29.6%

Generated by 🚫 dangerJS against 0e785af

@@ -425,8 +482,7 @@ pub fn restore_hunk(dir: &str, path: &str, hunk: &str) -> Result<()> {
/// as an empty blob instead.
pub fn commit_hunk(dir: &str, path: &str, hunk: &str, message: &str) -> Result<CommitInfo> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity 2 — not blocking

commit_hunk still has the write-before-bail shape this PR fixes in commit_files: further down (line 550) the index is written before the nothing to commit check. It's practically unreachable today (find_hunk_by_hash just found the hunk in the live diff), but the lookup→commit window is a check-then-act race, and when the bail does fire it clobbers intent-to-add entries the same way. Same two-line move as in commit_files?

@@ -318,8 +376,7 @@ fn index_entry_differs_from_head(
/// drift row alive. Purely unstaged drift leaves the index untouched.
pub fn restore_hunk(dir: &str, path: &str, hunk: &str) -> Result<()> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity 2 — not blocking

Now that paths here are dir-relative, restore_hunk/commit_hunk are only correct with dir = repo root: find_hunk_by_hash and file_diff_contents still match workdir-relative names, so a future caller passing a nested config dir would look a hunk up under one convention and resolve the file under the other. Worth a one-line comment on these two functions ("callers must pass the repo root until the query side is dir-relative too") so the first nested-dir caller finds out at review time instead of at runtime?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh damn! I guess I missed #655 (comment) before writing my review comment in #655 ... I think this probably solves my issues! Looking forward to re-try once this is in :)

@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High impact] Adds multi-file commit and path resolution to git operations.

The PR appears safe for current callers, with a non-blocking empty-selection guard needed for the new batch API.

Findings

  1. P2 Empty selection creates a commit ▶

Summary

Adds commit_files and makes file and hunk operations resolve paths relative to the caller’s directory.

  • Adds tests for nested directories, selected-file commits, deletions, and outside paths.
  • Preserves the saved index when a selected-file commit reports no changes.
  • An empty batch should be rejected before it can create an empty first commit.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["Caller directory and selected paths"] --> B["Discover repository"]
  B --> C["Resolve paths relative to caller directory"]
  C --> D["Reject paths that escape repository"]
  D --> E["Stage selected files or deletions"]
  E --> F["Build commit tree"]
  F --> G{"Same tree as existing HEAD?"}
  G -->|Yes| H["Return without writing index"]
  G -->|No| I["Write index and create commit"]
Loading

Reviews (1) · Last reviewed commit: "scott-git-repo-rel-path" · Reviewed by Greptile


/// Commit only the requested files, interpreting each path relative to `dir`
/// and leaving unrelated worktree changes out of the commit.
pub fn commit_files(dir: &str, paths: &[&str], message: &str) -> Result<CommitInfo> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Empty selection creates a commit

The new commit_files API accepts an empty paths slice. In a repository without a first commit, this skips staging, writes an empty index, and creates an empty commit. The “nothing to commit” check only runs when parent exists.

Current callers always select one file, so this is non-blocking. Reject an empty selection before touching the index, and add a test that checks that staged files survive.

commit_files with an empty path list on an unborn HEAD cleared the index,
wrote it to disk and recorded an empty root commit. It now rejects an empty
path list up front and, on an unborn HEAD, bails when nothing ended up
staged, mirroring commit_all.

commit_hunk wrote the reset index to disk before checking whether the hunk
changed anything, so a no-op hunk unstaged the user's other changes and then
failed. The index is now written only once a commit will be created.

Adds regression tests for all three cases.
@czxtm

Copy link
Copy Markdown
Member

Pushed 0e785af with two small fixes so this can land ahead of the secrets stack:

  • commit_files now rejects an empty path list, and on an unborn HEAD it bails when nothing ended up staged (the same guard commit_all uses). Before, an empty list on a fresh repo cleared the index and recorded an empty root commit.
  • commit_hunk writes the index only after the no-op check, so a hunk that changes nothing no longer unstages the user's other changes on its way to failing.

Three regression tests cover these; they fail without the fix and all 42 git::exec tests pass with it. I'll merge once Rust Check and the Linux tests are green. build will still fail at notarization until the Apple agreement is renewed, so this goes in with an admin merge.

@sonarqubecloud

sonarqubecloud Bot commented Nov 9, 2026

Copy link
Copy Markdown

@prelint

prelint Bot commented Nov 9, 2026

Copy link
Copy Markdown

Ship it Git layer resolves paths from the config folder and adds multi-file commits

Product decisions in this change

Agree 1. A config folder inside a larger git repository now works like a root folder. File paths are read relative to the config folder, and nixmac translates them to the repository layout.

Users who keep their nix config in a subfolder (for example nix/os in a dotfiles repository) are a real group. The repo already tests against such a setup in the eval corpus. Before this change, commit and discard actions could look in the wrong place. After it, the user sees the same behavior as a user with a config at the repository root.

The path rule is also stable for users. They never see it, and the root-folder case works as before. Changing it later would be an internal change only.

Agree 2. A path that points outside the repository is refused, and nothing changes. This includes `..` escapes.

Commit and discard are destructive actions. A discard that could delete a file outside the user's repository would be a serious trust failure. Failing before any change is the right default. The tests show that a file outside the repository survives a failed discard.

One small cost is the error text. A user who sees "path is outside repository" may not know why it happened. This is rare, so it is acceptable.

Agree 3. nixmac gains a commit of several chosen files in one commit. Other uncommitted changes stay in the working tree.

Users often have a mix of changes. One commit for the files tied to one request matches how people think about a change set. It also avoids the all-or-nothing choice between one file and everything. The same rule as the single-file commit applies, so there is no new surprise.

The feature has no visible user interface in this PR. The product value depends on the later PR that uses it.

Agree 4. A commit with no paths, or with no real change, is refused. This applies to a new repository with no commits as well. The user's staged work stays as it was.

Without this guard, an empty request on a fresh repository would record an empty first commit and clear what the user had staged. That would silently destroy the user's own staging work. The follow-up fix and the tests close this gap. A clear "nothing to commit" message is the expected result.

Agree 5. A failed hunk commit or a failed single-file commit now leaves the user's staged changes and intent-to-add entries alone.

A user who stages files by hand expects a failed nixmac action to leave that work as it was. Before this change, a no-op action could unstage it. The new order keeps the user's state on failure, so the tool is safer to use next to normal git habits. There is no user-facing cost.

Open questions

  • Do users with a config folder inside a larger repository see a clear message when a path is refused as outside the repository?

  • Which user action will first call the multi-file commit, and does the interface tell the user which files it will include?

Recommendation

Ship it
All decisions protect user data and match how users expect git to behave, and the nested config folder case is a real need. The two open questions concern later interface work and do not block this change.

@prelint prelint Bot added the ship it label Nov 9, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack

View guided diff | Turn on auto-fix | Re-trigger cubic

@czxtm
cooper (czxtm) merged commit 3ee94d4 into main Nov 9, 2026
13 of 14 checks passed
@czxtm
cooper (czxtm) deleted the 09-04-scott-git-repo-rel-path branch November 9, 2026 15:38
@linear-code

linear-code Bot commented Nov 9, 2026

Copy link
Copy Markdown

ENG-1445

cooper (czxtm) added a commit that referenced this pull request Nov 9, 2026
Picks up #582 (vitest 3.2.6) and #679. bun.lock was re-resolved from main's
lockfile with bun 1.3.14 and bun.nix regenerated with bun2nix 2.0.8, so the
Storybook 10.6.2 packages and the vitest 3.2.6 tree resolve together.

This branch was successfully deployed

1 active deployment
storybook-preview — 0e785af3 Deployed Nov 9, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants