Skip to content

feat: add group fetch and fast-forward-only pull actions - #70

Merged
cheat2001 merged 10 commits into
cheat2001:mainfrom
ChrisLauinger77:feat/group-fetch-fast-forward-pull
Oct 16, 2026
Merged

cheat2001 merged 10 commits into
cheat2001:mainfrom
ChrisLauinger77:feat/group-fetch-fast-forward-pull

Conversation

@ChrisLauinger77

@ChrisLauinger77 ChrisLauinger77 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What & why

Updating a repository group currently requires opening each repository separately. The start-page group header menu now offers Fetch all and Pull all (fast-forward only), with a live per-repository summary of moved refs, old/new commit IDs, commit counts, unchanged repositories, skips with reasons, and failures. Operations run sequentially and continue after individual failures. Fetch pruning respects remote.<name>.prune first, falling back to fetch.prune only when the per-remote key is absent; an explicit remote false prevents pruning.

Cancel run cancels the native run token, stops queued repositories, releases the app's busy state, and enables closing the dialog without waiting for a stalled network read. Downloading objects is separated from applying refs; ref updates and fast-forward checkout share a cancellation mutex. After cancellation acknowledges, no further ref or working-tree updates are allowed. Completed changes are retained. A blocked transport can still wind down in the background; this is cooperative cancellation, not a hard socket timeout. Cancellation intentionally waits for a running local checkout/ref-write phase to finish, which preserves the acknowledgement guarantee; the code documents this wait. The native guard records completed checkout outcomes and lightweight fetch progress before releasing the mutex. Moved-ref discovery and commit counting happen outside the write guard when the final response or cancellation receipt is built, rather than after every remote. One open repository handle serves the fetch loop and its final summary. Receipt inspection failures are isolated to the affected row, preserving its fetch counts and earlier errors plus an inspection diagnostic, and retaining all other completed receipts. Cancellation still inspects moved refs and counts commits before returning; making that optional work asynchronous remains outside this fix. Fetch receipts retain successful remotes and error details together; cancelling cannot turn earlier failures into a generic Cancelled row. Queued repositories explicitly say they never started. Cancel returns completed-result receipts, so a checkout that finishes while cancellation waits still counts as Updated even if its original IPC response is delayed. Each frontend row settles once from the response or cancellation acknowledgement; late successes and errors are consumed without changing it. Leaving the page also cancels the run. Registry entries record last use; one-hour stale entries are cancelled and pruned on later registry activity without blocking or interrupting an active checkout.

Rust returns an explicit partial status when some remotes succeed and others fail, including when successful remotes move no refs. A shared classify() function drives both badges and counters, so each repository is counted exactly once. Changes and failure details remain visible.

Pull uses the checked-out branch's configured upstream and never merges or rebases. It reads the merge source through git2's branch_upstream_merge API. Named upstreams resolve the tracking ref independently of its existence and fetch the exact source into it, and verifies the ref-update callback including unchanged refs. URL/path upstreams use anonymous remotes with a temporary private ref removed in the same guarded write phase, preserving branch configuration. Named upstreams outside the configured fetch refspec use that same temporary destination while keeping both the fetch configuration and existing tracking refs unchanged. A missing local tracking ref can therefore be restored, while a deleted remote branch is reported as a missing configured upstream rather than “no upstream”. Detached/unborn HEADs, bare repositories, unfinished operations, and divergent branches are skipped. Safe checkout preserves unrelated untracked files and staged or unstaged edits, and refuses conflicting changes. Safe checkout precedes the branch-ref update, after checking for changes during fetch.

Group pull uses the same libgit2 checkout behavior as toolbar Pull, including its existing handling of hooks, submodules, and external filters such as Git LFS. There are no additional compatibility guards or blanket clean-tree requirements. If checkout succeeds but updating the branch ref fails, the error explicitly describes that state and includes both commit IDs for inspection. Integration-test fixtures explicitly use unsigned commits and the expected branch name to isolate developer Git defaults.

Unchanged fetch badges use the same wording as the counts. The summary uses the app's flat layout: borderless rows, green Updated and red Failed badges, and a single non-wrapping counts line. The pull menu entry uses the toolbar's ArrowDownToLine icon. Cancellation's local-write wait is documented in CLAUDE.md, with no inline source comment.

Closes #69. Intended to land after 0.23.0, as requested by the maintainer.

Screenshots

Uses existing design tokens without changing the theme palette.

Group pull summary — dark

Group pull summary — light

Partial fetch and cancelled repositories

Validation

  • TypeScript checks for desktop, core, and design system passed; all 274 frontend unit tests passed. Used local tsc/vitest executables because the available pnpm launcher could not resolve the pinned version.
  • Desktop production build passed.
  • cargo fmt --check and cargo clippy --all-targets -- -D warnings passed.
  • All 109 Git integration tests passed, including a real stalled HTTP socket cancelled before it closes, cancelled-token lifetime, missing/deleted upstreams, preservation of unrelated untracked/unstaged/staged changes, real checkout conflicts including an untracked-file collision, toolbar parity with hooks/submodule metadata/LFS attributes, and a real branch-ref lock failure after successful checkout. Prune precedence is covered for explicit true/false and missing remote settings. A real controlled fast-forward verifies the engine itself saves the completed checkout receipt. A multi-remote fixture fetches successfully, fails while applying a later remote, and stalls on a socket; cancellation keeps both the changes and prior error. An error-only version verifies failed status survives cancellation. URL upstream coverage checks fast-forwarding, unchanged/deleted upstreams, unchanged configuration, and temporary-ref cleanup. A named upstream outside the remote fetch refspec fast-forwards without changing fetch configuration or existing tracking refs and still skips a deleted upstream. A real stalled-fetch fixture opens a gitdir alias, invalidates its pointer before cancellation, and verifies cancellation succeeds with both a diagnostic partial row and the unaffected completed receipt. This preserves error-isolation coverage without renaming a directory containing open pack files on Windows. File-URL fixtures use Url::from_file_path for portable drive letters, slashes, and escaping. All 109 integration tests, formatting, and clippy pass locally on Linux; the Windows CI rerun is pending. The multi-remote cancellation bound is five seconds, with the socket kept open and the worker still running, to allow slower CI hosts. Both registry-expiry unit tests passed.
  • All 4 relevant Playwright tests passed: repository-group menu actions, mixed pull results, and partial fetch classification plus cancellation of a hanging operation (including releasing busy and preventing late results from reopening the dialog), plus assertions for badge tones, borderless rows, non-wrapping counts, the toolbar pull icon, and dark/light screenshots. The new cancellation regression uses an acknowledged successful checkout with a delayed inactive-run error and verifies the Updated count and row remain stable.
  • The initial native desktop feature was manually verified by the requester.
  • Known limitation from the original full Rust unit-test run: 72 passed and 3 existing signing tests failed because they inherit this machine's global Git signing configuration (signing_is_off_unless_config_enables_it, ssh_format_without_a_key_is_a_clear_error, openpgp_falls_back_to_the_committer_identity). Signing code is unchanged.

Checklist

  • Frontend typechecks and unit tests pass (equivalent local commands noted above)
  • cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test pass (full unit-test limitation noted above)
  • New engine functions have integration tests in tests/git_engine.rs
  • Mutating operations go through the undo tracked() wrapper where undoable (fetch/pull follow the existing network-operation behavior)
  • Colors use design tokens only (no hex in components)
  • CLAUDE.md updated if conventions/architecture changed
  • Commits carry no trailers from coding tools

@cheat2001 cheat2001 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for building the whole thing out, and the engine tests are thorough, the stalled socket one included. Three things before it goes in.

Pull all skips any repository that has an untracked file. The toolbar Pull fast-forwards fine with unrelated local changes, and the safe checkout already refuses a real conflict, so in a normal work folder most repos would come back as "local changes". Drop the clean tree gate and let the checkout decide.

The hooks, submodule and LFS skips are stricter than the rest of the app. The toolbar Pull runs none of those either, and nothing tells the user why this one is different. Align it with Pull or drop the guards.

The summary dialog needs the app's flat look: tone on the badges (failed red, updated green), rows without a border each, a counts line that doesn't wrap, and the pull entry should use the same arrow icon the toolbar uses, not the pull request one. There's also one inline comment in group_update.rs, the codebase keeps those in CLAUDE.md.

As the issue said, this one lands after 0.23.0, so no rush.

@ChrisLauinger77

Copy link
Copy Markdown
Contributor Author

I take care about your findings in the evening. After 0.23 is perfectly fine. Happy I could help

@ChrisLauinger77
ChrisLauinger77 force-pushed the feat/group-fetch-fast-forward-pull branch from 7c91b4f to a7ae152 Compare October 13, 2026 15:42
@ChrisLauinger77

ChrisLauinger77 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback, with the latest fixes in d8866bc, 32eb84d, and 2c47839.

  • Pull behavior: removed the blanket clean-tree and hooks/submodule/LFS guards to match toolbar Pull. Safe checkout preserves unrelated edits and refuses conflicts. Pull now uses branch_upstream_merge, supports URL/path upstreams, and handles upstreams outside the remote's fetch refspec through a temporary ref that is removed in the same guarded write phase. Fetch and branch configuration stay unchanged. Missing upstreams are distinguished from unconfigured ones, and branch-update failures after checkout explain the working-tree/HEAD mismatch.
  • Fetch results: per-remote prune settings override fetch.prune, including explicit false. Partial fetches have their own status, badges and counters share classification, and unchanged fetch badges now say “Unchanged”.
  • Cancellation: completed checkouts remain counted as updated; fetch receipts retain successful remotes and earlier errors. One unreadable repository no longer makes cancellation fail or discards other completed results. Late responses cannot overwrite settled rows, and queued repositories explicitly say they never started. Ref calculations run outside the write lock, repository handles are reused between remotes, and read-only cancellation checks use ensure_active().
  • Summary styling: flat rows, green Updated/red Failed badges, a non-wrapping counts line, and the same pull arrow icon as the toolbar. The inline source comment was moved into CLAUDE.md.
  • Windows tests: the unreadable-path regression now invalidates a gitdir pointer instead of renaming a repository with open pack files. File URLs use Url::from_file_path. The cancellation timing bound was relaxed for slower CI hosts.

Validation at 2c47839: the Rust jobs on Windows, macOS, and Linux are green, as are commit-message and frontend typecheck/unit/build checks. Locally, all 109 engine integration tests, formatting, and clippy pass.

@cheat2001

Copy link
Copy Markdown
Owner

Took it for a spin in the installed app, this is good now. The pull behaves like the toolbar one, the summary reads cleanly in both themes, and the cancel receipts are a nice touch.

Two small things and I merge:

  1. Fetch all uses the Download icon, the toolbar's Fetch uses RefreshCw. Same icon for the same action.
  2. The CLAUDE.md section for this feature is about 70 lines, most of it implementation narration (the Windows fixture, the Arc of initial refs, which lock each step holds). Trim it to what someone touching the code later needs: the menu entries, the dialog, the runId handshake, the cancel rule, and the "pull behaves exactly like toolbar Pull" line.

No need to touch anything else.

@ChrisLauinger77

Copy link
Copy Markdown
Contributor Author

Nice - I take care in the afternoon

@ChrisLauinger77

Copy link
Copy Markdown
Contributor Author

done

@ChrisLauinger77

Copy link
Copy Markdown
Contributor Author

The latest CI run at c58ecfa has five passing checks and one failure in the Windows Rust job.

The failure is in the existing signing unit test core::sign::tests::reads_ssh_signing_setup_from_git_config: libgit2 could not acquire the temporary repository's .git/config.lock while writing configuration. The unit-test phase reports 68 passed, 1 failed.

The likely cause is a temporary-directory collision between parallel signing tests. Their fixture names use only the process ID and timestamp, so repeated timestamps on Windows can give two tests the same directory. This is an inference from the fixture and lock error; the log does not prove the collision. An atomic sequence number in the fixture name would guarantee uniqueness.

This test helper is unchanged by this PR, and the latest commit only changes the Fetch all icon and trims CLAUDE.md. The failure is separate from the earlier Windows group-update fixture issues.

Failed Windows job

@cheat2001
cheat2001 merged commit cca8ab0 into cheat2001:main Oct 16, 2026
11 of 12 checks passed
cheat2001 added a commit that referenced this pull request Oct 16, 2026
The fixture named its folder by process id and timestamp alone, and two signing tests running in parallel on the Windows CI runner got the same folder and fought over .git/config.lock (the red Windows job on PR #70's last run). An atomic counter in the name makes it unique per call.
@cheat2001

Copy link
Copy Markdown
Owner

Released in 0.24.0. Thanks for sticking with the review rounds, the cancel receipts and the summary turned out really nice.

@ChrisLauinger77
ChrisLauinger77 deleted the feat/group-fetch-fast-forward-pull branch October 16, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Start page: group context menu with fetch all and fast-forward-only pull

2 participants