diff --git a/apps/labfox/lib/features/commits/data/history_repository.dart b/apps/labfox/lib/features/commits/data/history_repository.dart index 4196e91e..92946d7e 100644 --- a/apps/labfox/lib/features/commits/data/history_repository.dart +++ b/apps/labfox/lib/features/commits/data/history_repository.dart @@ -75,6 +75,35 @@ class HistoryRepository { return List.unmodifiable(items); } + /// Reads fixed-head detail through this captured account and project. + /// The caller chooses the target from fresh authoritative MR context. + /// Null means the caller's origin expired; late results/errors are discarded. + /// Recheck currency after awaiting, then validate membership and original + /// references separately. This read does not make a private write safe. + Future originalCommit({ + required int projectId, + required String commitId, + required bool Function() isCurrent, + }) async { + if (projectId < 1) throw ArgumentError.value(projectId, 'projectId'); + if (commitId.length != 40 || RegExp(r'[^0-9a-f]').hasMatch(commitId)) { + throw ArgumentError.value(commitId, 'commitId'); + } + if (!isCurrent()) return null; + final Commit result; + try { + result = await _client.repository.fixedCommit( + projectId, + commitId: commitId, + ); + } on GitLabException { + if (!isCurrent()) return null; + rethrow; + } + if (!isCurrent()) return null; + return result; + } + Future commit({required int projectId, required String sha}) { return _client.repository.commit(projectId, sha: sha); } diff --git a/apps/labfox/test/original_commit_repository_test.dart b/apps/labfox/test/original_commit_repository_test.dart new file mode 100644 index 00000000..7ace7437 --- /dev/null +++ b/apps/labfox/test/original_commit_repository_test.dart @@ -0,0 +1,127 @@ +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gitlab_api/gitlab_api.dart'; +import 'package:labfox/features/commits/data/history_repository.dart'; +import '../../../packages/gitlab_api/test/mr_commits_api_test.dart' as b; + +void main() { + for (final parents in [ + null, + [], + [b.parent], + [b.second, b.parent], + ]) { + test('captured fixed detail preserves parent context $parents', () async { + var calls = 0; + final c = b.client((o) { + calls++; + expect(o.path, '/projects/8/repository/commits/${b.head}'); + expect(o.method, 'GET'); + return b.response({...b.commit(), 'parent_ids': parents}); + }); + addTearDown(c.close); + final result = await HistoryRepository( + c, + ).originalCommit(projectId: 8, commitId: b.head, isCurrent: () => true); + expect(result!.id, b.head); + expect(result.parentIds, parents); + expect(calls, 1); + }); + } + for (final (project, sha) in [ + (0, b.head), + (-1, b.head), + (8, 'main'), + (8, ''), + (8, b.head.toUpperCase()), + ]) { + test('invalid identity never dispatches $project $sha', () async { + var calls = 0; + final c = b.client((o) { + calls++; + return b.response(b.commit()); + }); + addTearDown(c.close); + await expectLater( + HistoryRepository(c).originalCommit( + projectId: project, + commitId: sha, + isCurrent: () => true, + ), + throwsArgumentError, + ); + expect(calls, 0); + }); + } + test('obsolete origin performs no read', () async { + var calls = 0; + final c = b.client((o) { + calls++; + return b.response(b.commit()); + }); + addTearDown(c.close); + expect( + await HistoryRepository( + c, + ).originalCommit(projectId: 8, commitId: b.head, isCurrent: () => false), + isNull, + ); + expect(calls, 0); + }); + for (final status in [200, 403, 404, 429, 500]) { + test('obsolete late success or failure is discarded $status', () async { + final started = Completer(), answer = Completer(); + var current = true; + final c = b.client((o) async { + started.complete(); + await answer.future; + return b.response(b.commit(), status: status); + }); + addTearDown(c.close); + final result = HistoryRepository(c).originalCommit( + projectId: 8, + commitId: b.head, + isCurrent: () => current, + ); + await Future.any([started.future, result]); + expect(started.isCompleted, isTrue); + current = false; + answer.complete(); + expect(await result, isNull); + }); + } + test('final exposure rechecks currency', () async { + var checks = 0; + final c = b.client((o) => b.response(b.commit())); + addTearDown(c.close); + expect( + await HistoryRepository(c).originalCommit( + projectId: 8, + commitId: b.head, + isCurrent: () => ++checks < 2, + ), + isNull, + ); + expect(checks, 2); + }); + test('current head mismatch stays a typed failure', () async { + final c = b.client((o) => b.response(b.commit(id: b.parent))); + addTearDown(c.close); + await expectLater( + HistoryRepository( + c, + ).originalCommit(projectId: 8, commitId: b.head, isCurrent: () => true), + throwsA(isA()), + ); + }); + test('current unavailable target commit stays not found', () async { + final c = b.client((o) => b.response('invalid', status: 404, raw: true)); + addTearDown(c.close); + await expectLater( + HistoryRepository( + c, + ).originalCommit(projectId: 8, commitId: b.head, isCurrent: () => true), + throwsA(isA()), + ); + }); +} diff --git a/docs/fixed-commit-review-reads.md b/docs/fixed-commit-review-reads.md new file mode 100644 index 00000000..645a837b --- /dev/null +++ b/docs/fixed-commit-review-reads.md @@ -0,0 +1,56 @@ +# Fixed commit detail reads + +Issue [#632](https://github.com/Theorvane/LabFox/issues/632) adds strict commit +detail reads for original review context. Existing general `commit` reads retain +branch/tag compatibility. No original diff coordinates or private-write UI is +exposed. + +## Exact identity and parent metadata + +The [official single commit endpoint](https://docs.gitlab.com/api/commits/#retrieve-a-commit) +uses `GET /projects/:id/repository/commits/:sha` and permits a commit hash or a +branch/tag reference. `RepositoryApi.fixedCommit` deliberately requires a full +lowercase SHA-1 and a positive numeric project or nonblank encoded path. It +requests `stats=false`, uses the captured instance/client, disables redirects +and checks HTTP 200 before decoding plain JSON. Returned `id` must exactly equal +the captured head. Missing/mismatched identities and malformed payloads fail +with sanitized domain errors; status/transport errors retain typed mapping. +Existing captured-account read-only OAuth recovery remains available on that +same fixed route. + +Nullable generated `Commit.parentIds` retains its existing contract: absent or +null parents stay unknown, an empty array identifies a reported root commit, and +multiple parents retain server order. Every reported parent must be a full +lowercase SHA-1, distinct from the head and from other parents. Text is preserved +without trimming. The returned parent list is immutable through the existing +generated model; no serializer or model changes are required. + +## Captured repository read + +`HistoryRepository.originalCommit` requires a captured numeric project and head +plus a caller currency guard. It performs one strict detail GET through its +captured account client. The guard runs before dispatch and before successful +exposure or propagating a typed failure. Obsolete results/errors return null; +current failures stay typed. A guard cannot abort a dispatched GET or its +account-bound authentication recovery, and callers must recheck after awaiting. + +The caller selects the target from fresh MR identity, not from a screen project, +source branch or list metadata. Availability of a fixed commit in that repository +does not establish current MR membership, original parent/reference semantics, +literal coordinates, complete diff coverage, permissions or an atomic snapshot. +Root/merge metadata is preserved without synthesizing blank/empty-tree references +or choosing a parent. MR-wide `diff_refs` are not a fallback. A future controller +must combine fresh identity/membership and exact original literal selection with +shared private-write reservations, settlement and explicit recovery. + +## Validation + +Test-first API/repository regressions cover encoded captured routes, exact head +and text preservation, unknown/root/ordered parents, invalid/self/duplicate +parents, input guards, immutable metadata, sanitized status-first failures, +redirect options, read-only OAuth recovery and origin replacement. A general +branch-reference read still works. Full workspace format, analysis and all five +package test gates apply. No UI/localization, dependencies, persistence, telemetry +or private-write behavior changes; live private-instance/device validation is +not claimed. Original reference validation and guarded review UI remain +follow-ups; MW-07 and image/file draft creation remain in progress. diff --git a/docs/mobile-web-parity.md b/docs/mobile-web-parity.md index a5682098..cb3338b9 100644 --- a/docs/mobile-web-parity.md +++ b/docs/mobile-web-parity.md @@ -57,7 +57,7 @@ issues and PRs. Keep it open until its full acceptance boundary is verified. | MW-04 | P1 | Member invitations, role/expiry changes, and removal for groups/projects. | Queued | Issue needed | | MW-05 | P0 | Issue/work-item editing, metadata, types, and validated state transitions. | In progress | [#329](https://github.com/Theorvane/labfox/issues/329), [PR #330](https://github.com/Theorvane/labfox/pull/330) shipped title and description editing; [#339](https://github.com/Theorvane/labfox/issues/339), [PR #340](https://github.com/Theorvane/labfox/pull/340) shipped due-date editing; [#341](https://github.com/Theorvane/labfox/issues/341), [PR #342](https://github.com/Theorvane/labfox/pull/342) shipped issue-label editing; [#343](https://github.com/Theorvane/labfox/issues/343), [PR #344](https://github.com/Theorvane/labfox/pull/344) shipped assignee editing; [#345](https://github.com/Theorvane/labfox/issues/345), [PR #346](https://github.com/Theorvane/labfox/pull/346) shipped milestone editing; [#347](https://github.com/Theorvane/labfox/issues/347), [PR #348](https://github.com/Theorvane/labfox/pull/348) shipped confidentiality; [#349](https://github.com/Theorvane/labfox/issues/349), [PR #350](https://github.com/Theorvane/labfox/pull/350) shipped discussion locking. Type-conversion compatibility and remaining work-item actions need separate issues. | | MW-06 | P1 | Boards and iterations: discover mobile-web behavior, then list/detail/mutations. | Queued | Issue needed | -| MW-07 | P0 | Advanced MR review: audit inline discussions/suggestions and finish missing review/approval flows. | In progress | [#552](https://github.com/Theorvane/labfox/issues/552), [PR #553](https://github.com/Theorvane/labfox/pull/553) prevent overlapping approval, merge, and other MR commands per project/IID, including while the repository resolves; failures release the reservation for explicit retry. [#554](https://github.com/Theorvane/labfox/issues/554), [PR #555](https://github.com/Theorvane/labfox/pull/555) add repository-session observation, pre-dispatch cancellation and late-result/error isolation, success-only current-session refresh/analytics/inbox effects, cancelled to-do outcomes without current-account toasts, and generation-owned duplicate reservations. Already dispatched server writes cannot be undone. [#556](https://github.com/Theorvane/labfox/issues/556), [PR #557](https://github.com/Theorvane/labfox/pull/557) add localized approval-read loading/error states, disabled approval toggles until successful reads, read-only retry, retained-data refresh protection, reactive new-session reads with stale-result isolation, and current-state toggle selection while preserving unavailable endpoint compatibility and independent merge controls. [#558](https://github.com/Theorvane/labfox/issues/558), [PR #559](https://github.com/Theorvane/labfox/pull/559) derives approval ownership from the authenticated account user ID and documented `approved_by` membership, ignores conflicting legacy current-user flags, and refreshes membership on account replacement. Regression coverage checks real toggle dispatch on mobile/desktop in both themes. [#560](https://github.com/Theorvane/labfox/issues/560), [PR #561](https://github.com/Theorvane/labfox/pull/561) validates successful approval responses before model parsing: explicit approver lists, complete user wrappers, positive integral user IDs, and nonnegative integral counts when present. Malformed reads become sanitized domain errors; status mapping precedes payload parsing. End-to-end widget tests verify blocked approval toggles and read-only recovery in both widths/themes, while unavailable endpoint compatibility remains intact. [#562](https://github.com/Theorvane/labfox/issues/562), [PR #563](https://github.com/Theorvane/labfox/pull/563) adds an account-bound, one-page MR discussion reader with generated grouped-note models and nullable resolution metadata. It preserves next-page headers without assuming totals or fetching ahead, rejects incomplete groups and nonadvancing cursors, and keeps HTTP failures distinct from sanitized parsing errors. Building on the API/models foundation, [#564](https://github.com/Theorvane/labfox/issues/564), [PR #565](https://github.com/Theorvane/labfox/pull/565) connects grouped MR replies to the detail-screen comments flow and aggregates resolution across all resolvable notes without claiming unknown states are resolved. The shared top-level composer refreshes discussion page one after posting; explicit load-more and read/page retries preserve groups and cursors. Account-bound reads/posts ignore late results, drafts reset when the session or resource changes, and resizing preserves the current draft. Five locales and narrow/compact/wide light/dark layouts are covered. [#566](https://github.com/Theorvane/labfox/issues/566), [PR #567](https://github.com/Theorvane/labfox/pull/567) adds the MR discussion reply API foundation: encoded thread/project paths, explicit IID routing, exact nonempty Markdown bodies, validated generated note responses, sanitized typed errors and no automatic authentication replay for the write. [#568](https://github.com/Theorvane/labfox/issues/568), [PR #569](https://github.com/Theorvane/labfox/pull/569) connects a localized inline reply composer to the selected discussion. One active reply preserves the top-level draft; cancel and write reservations prevent accidental overlaps. Replies preserve Markdown, retain failed drafts for explicit retry, refresh authoritative discussion page one and ignore old-session completions. Account/MR replacement resets the selection, resizing and pagination preserve current drafts, and a removed target restores top-level submission. Five locales and three widths in both themes are covered. [#570](https://github.com/Theorvane/labfox/issues/570), [PR #571](https://github.com/Theorvane/labfox/pull/571) adds known-state Resolve/Reopen controls and a strict account-bound mutation: encoded identity, explicit resolution, no redirects/authentication replay, and updated thread/state confirmation. Resolution shares comment/reply write reservations, preserves the top-level draft, retains localized permission/failure feedback for explicit retry, and refreshes discussions and MR detail only on current-session success. Unknown/nonresolvable states offer no toggle, and an active reply draft prevents resolution. [#572](https://github.com/Theorvane/labfox/issues/572), [PR #573](https://github.com/Theorvane/labfox/pull/573) preserves generated diff-note position metadata: original version SHAs, renamed paths, old/new text lines, multiline range endpoints and image/file types. Discussion reads, replies and resolution responses reject malformed or fractional coordinates before parsing; absent legacy fields remain unknown. [#574](https://github.com/Theorvane/labfox/issues/574), [PR #575](https://github.com/Theorvane/labfox/pull/575) adds paginated diff-version reads and an explicitly selected original unified-diff snapshot. Generated version/file models retain SHA triplets, renamed paths, raw text and nullable omission flags; absent files differ from empty snapshots. Identity/payload validation, status-first typed failures and no eager pagination or current-diff fallback are covered. [#576](https://github.com/Theorvane/labfox/issues/576) / [PR #577](https://github.com/Theorvane/labfox/pull/577) adds an expandable original-diff context beside positioned notes. It matches complete SHA triplets, both file paths and exact old/new single-line text coordinates; explicit older-page reads and read-only retries preserve drafts. The matched original hunk/line has an accessible non-color marker. Unsupported, omitted, truncated or ambiguous contexts remain unavailable without a current-diff fallback; account/resource replacement resets panels and stale reads cannot dispatch follow-up snapshots or affect the new session. Five locales, three widths and both themes are covered. Multiline context, positioned thread creation and suggestion application remain separate slices. [#578](https://github.com/Theorvane/labfox/issues/578), [PR #580](https://github.com/Theorvane/labfox/pull/580) adds the single-line positioned discussion creation API foundation: exact original SHA/path/line anchors, Markdown preservation, pre-dispatch argument validation, no redirects or authentication replay, created-thread confirmation and sanitized typed failures. The line selector/composer, multiline creation and suggestions remain separate. [#579](https://github.com/Theorvane/labfox/issues/579), [PR #581](https://github.com/Theorvane/labfox/pull/581) connects the latest authoritative diff-version snapshot to localized single-line selection and discussion creation. Exact original coordinates, indexed eligibility, shared write reservations, retained drafts, session/resource isolation and visible authoritative discussion inspection before uncertain-write retry are covered. The existing MR Changes route restores from project/IID; commit diffs stay read-only. Multiline context/creation and suggestion application remain separate. [#582](https://github.com/Theorvane/labfox/issues/582) / [PR #583](https://github.com/Theorvane/labfox/pull/583) adds typed immutable suggestion metadata to MR discussion responses, preserving original/replacement code, inclusive ranges, applied status and both documented applicability spellings. Missing states remain unknown; positive integral identities/coordinates, range order, response-wide uniqueness, content/flag types and alias agreement are validated before generated parsing. Suggestion presentation/application and multiline context/creation remain separate. [#584](https://github.com/Theorvane/labfox/issues/584) / [PR #585](https://github.com/Theorvane/labfox/pull/585) adds read-only suggestion previews beside user notes with exact selectable original/replacement code, nullable application/applicability states, localized ranges and empty/unavailable content, bounded code panes, retained drafts across resize/theme, and account/resource/metadata isolation. Suggestion application and batching remain separate. [#586](https://github.com/Theorvane/labfox/issues/586) / [PR #587](https://github.com/Theorvane/labfox/pull/587) adds the single-suggestion application API foundation with a positive global identity, exact optional commit messages, strict HTTP 200 and matching applied=true confirmation, shared metadata validation, typed sanitized failures, and no redirect/authentication replay. Application UI/session reservations and batch application remain separate. [#588](https://github.com/Theorvane/labfox/issues/588) / [PR #589](https://github.com/Theorvane/labfox/pull/589) adds explicit single-suggestion confirmation with literal code and optional commit messages, a fresh exact-discussion preflight, shared reservations including pagination, current-view/account isolation, confirmed-success detail/discussion/review-snapshot refreshes, and draft-preserving read-only recovery before uncertain-write retry. Five locales, three widths, both themes, keyboard insets and large text are covered. Batch application and multiline context/creation remain separate. [#590](https://github.com/Theorvane/labfox/issues/590) / [PR #591](https://github.com/Theorvane/labfox/pull/591) adds the batch suggestion API foundation: one account-bound request with immutable positive unique global IDs, exact optional messages, strict complete matching applied-state confirmation, preserved server order/nullable metadata, status-first sanitized failures, and no redirects, authentication replay or single-write fallback. Building on that API, [#592](https://github.com/Theorvane/labfox/issues/592) / [PR #593](https://github.com/Theorvane/labfox/pull/593) adds explicit loaded-target selection and multi-patch confirmation, deduplicated all-thread preflight before one batch, immutable selected identities, shared reservations, current-account/view isolation, complete staged read-only recovery, exact retained drafts and success-only refreshes. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [the batch UI](mr-suggestion-batch-ui.md). [#594](https://github.com/Theorvane/labfox/issues/594) / [PR #595](https://github.com/Theorvane/labfox/pull/595) adds exact original multiline context with inclusive side-aware per-line markers, full filename-hash/raw-counter endpoint identity, optional coordinate support, complete contiguous hunk checks, duplicate/reversed/gapped-span rejection and retained draft/session behavior. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [original context](mr-discussion-context.md). [#596](https://github.com/Theorvane/labfox/issues/596) / [PR #597](https://github.com/Theorvane/labfox/pull/597) adds explicit original multiline selection and positioned creation, indexed complete contiguous range eligibility, exact endpoint request/response confirmation, inclusive accessible markers, retained drafts and complete-range recovery inspection. Repository identity is checked before dispatch and after completion to isolate replaced-session outcomes. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [multiline creation](mr-multiline-discussions.md). [#598](https://github.com/Theorvane/labfox/issues/598) / [PR #599](https://github.com/Theorvane/labfox/pull/599) adds the read-only pending-review draft foundation: generated private note/global MR/author identities, exact Markdown and nullable original position/reply/resolution metadata; strict one-page draft_notes reads with immutable server order, header pagination, sanitized malformed/status failures and normal read-only OAuth refresh. Account-bound repositories reject other-author pages; auto-disposed page consumers prevent stale pre-dispatch reads and ignore late outcomes. Presentation removes retained private rows immediately during account/client/instance changes, sign-out, loading and failed refresh. Draft mutations, aggregation and pending-review UI remain separate; no live GitLab/device validation is claimed. [#600](https://github.com/Theorvane/labfox/issues/600) / [PR #601](https://github.com/Theorvane/labfox/pull/601) adds the unpublished regular/original text-range draft creation API prerequisite: exact Markdown and recursive null omission, strict HTTP 201 and original-position/body confirmation, no redirect/OAuth replay/automatic retry/public-comment fallback, and sanitized typed/status-preserving failures. Account-bound repositories require and confirm captured author plus explicit global MR identity separately from route IID. Shared text/range validation preserves published discussion behavior. Replies, commit/image/file drafts, editing/deletion, publication and the pending-review controller/UI remain follow-up work; uncertain writes require authoritative inspection before an explicit retry, and no live GitLab/device validation is claimed. [#602](https://github.com/Theorvane/labfox/issues/602) / [PR #603](https://github.com/Theorvane/labfox/pull/603) adds read-only pending-review pagination with an authoritative global MR identity separate from route IID, immutable server order, explicit cursors through empty pages, optional totals and duplicate-load prevention. Mismatched author/MR identities, duplicate drafts and backward cursors reject the full result. Account/repository generations cancel obsolete dispatch and isolate initial/pagination success/errors. Presentation immediately removes retained private rows during account/client/instance/sign-out, refresh and failed reads; explicit retry restarts page one. Offset traversal is not an atomic snapshot and cannot authorize publication. The controller exposes no UI or mutation; see [private pagination](mr-pending-review-reader.md). [#604](https://github.com/Theorvane/labfox/issues/604) / [PR #605](https://github.com/Theorvane/labfox/pull/605) adds a read-only private pending-review panel on MR detail with exact selectable saved Markdown, known original metadata, localized loading/empty/partial/error states and explicit paging/refresh/retry. Guarded queries await fresh MR detail and verify route/global identity before dispatch, closing account/instance/client replacement requests ahead of detail; old private rows disappear during detail refresh and failed reads. Five locales, three widths, both themes, large text, long content and resize/theme preservation are covered with six synthetic captures. The panel sends no mutation or embedded image request and never infers/re-anchors original positions; see [private review panel](mr-pending-review-panel.md). Creation orchestration, uncertain-write recovery and publication remain follow-ups; no live GitLab/device validation is claimed. [#606](https://github.com/Theorvane/labfox/issues/606) / [PR #607](https://github.com/Theorvane/LabFox/pull/607) adds regular private review save orchestration with fresh account-bound detail/global identity, shared discussion write/pagination reservations, exact Markdown and one confirmed create before route-scoped private-reader refresh. Uncertain writes block replay across ordinary refreshes and same-account client/repository replacement; explicit complete read-only inspection follows empty-page cursors, validates ownership/identity/uniqueness/order and waits for any dispatched write to settle. Old-session/view results and errors cannot expose private rows or dispatch further pages. The controller has no composer UI, positioned/reply save orchestration, editing/deletion or publication; see [guarded saves](mr-pending-review-save.md). Recovery is in-memory and offset inspection cannot promise exactly-once creation after a client timeout; no live-instance/device validation is claimed. [#608](https://github.com/Theorvane/LabFox/issues/608) / [PR #609](https://github.com/Theorvane/LabFox/pull/609) adds a localized regular private-note composer on MR detail with exact Markdown, guarded one-save orchestration, retained failed input, complete visible inspection and separate acknowledgement before manual retry. Account/client/repository/resource replacement discards private input and snapshots; obsolete results cannot toast or expose data. Public drafts stay intact, and same-account reopen retains uncertain-write gating. Five locales, three widths, both themes and keyboard/large-text recovery are covered with twelve synthetic captures; see [private composer](mr-pending-review-composer.md). Positioned/reply/commit saves, editing/deletion and publication remain separate; no exactly-once or live-instance/device validation is claimed. [#610](https://github.com/Theorvane/LabFox/issues/610) / [PR #611](https://github.com/Theorvane/LabFox/pull/611) adds private draft update/delete API and account-bound repository foundations: exact Markdown, explicit original text-position preservation, strict 200 target/body/position confirmation and 204 deletion acknowledgement, positive project/IID/draft identities, captured author/global-MR checks and unchanged non-body metadata. Status-first plain-wire decoding preserves typed failures even for malformed JSON; redirects, authentication replay, fallback writes and automatic retry are disabled. Original line-code-only and unsupported update anchors fail before dispatch; owned opaque/image/file drafts can be deleted by identity. See [maintenance contract](mr-draft-note-maintenance.md). Fresh target/controller reservations, localized edit/delete confirmation/recovery and publication remain follow-ups; no live-instance/device validation is claimed. [#612](https://github.com/Theorvane/LabFox/issues/612) connects private draft editing and confirmed deletion to the existing pending review panel. Shared discussion reservations, fresh detail/global identity and complete private-page comparison refuse stale or missing selected targets before one write. Uncertain results gate every private write; complete visible inspection and acknowledgement adopt a current target only for explicit retry, with obsolete session/view input and outcomes discarded. Five locales, keyboard/large text and twelve synthetic captures cover three widths and both themes; see [maintenance UI](mr-pending-review-maintenance.md). Publication and positioned/reply/commit creation remain separate; no atomic snapshot, conditional-write, durable recovery or live-instance/device guarantee is claimed. [#614](https://github.com/Theorvane/LabFox/issues/614) adds whole-review publication: complete visible private-note confirmation and consent, a fresh full-set comparison before one strict-204 account-scoped bulk POST, and uncertainty that blocks private mutations until complete private/public inspection. Manual retries require new consent; session/view changes discard old confirmation and outcomes. Five locales and three widths in both themes are covered; see [publication flow](mr-pending-review-publication.md). Single-note publication, summary/reviewer-state controls and positioned/reply/commit creation remain separate. No automatic approval/merge, atomic server snapshot, durable recovery or live-instance/device guarantee is claimed. [#616](https://github.com/Theorvane/labfox/issues/616) adds private text line/range draft creation from the MR changes screen, fresh authoritative diff selection checks, immutable selected text, session/snapshot/origin isolation, preserved public input and explicit uncertain-save inspection. See [private inline review](mr-pending-review-inline.md). [#618](https://github.com/Theorvane/LabFox/issues/618) adds selected saved-note publication through one strict-204 PUT, fresh exact target comparison, explicit consent and shared two-sided recovery. Unrelated saved notes remain private; a missing selected ID cannot publish another note. See [single-note publication](mr-pending-review-single-publication.md). [#620](https://github.com/Theorvane/LabFox/issues/620) adds whole-review submission with an exact public summary, explicit reviewed/request-changes outcomes, complete current-reviewer reads, fresh reviewer preflight comparison and postwrite outcome verification. Partial or obsolete outcomes require private/public/reviewer inspection after actual write settlement and renewed consent, including selected-note recovery entry. Five locales, three widths, both themes and keyboard/large text are covered; see [review submission](mr-review-submission.md). Formal approval remains separate; additional draft creation types are follow-ups. No atomic, durable recovery or live-instance/device guarantee is claimed. [#622](https://github.com/Theorvane/LabFox/issues/622) adds private reply drafts on existing public discussion groups, exact target/body and non-resolution confirmation, a fresh selected-thread preflight, shared command reservations, and actual-settlement recovery that stages every private page together with the original public target. Missing targets stay missing; scoped recovery cannot clear publication/reviewer uncertainty. The localized shared composer preserves unsent public comments/replies and survives its own detail refresh or target removal. Five locales, keyboard/large text and twelve synthetic captures cover three widths and both themes; see [private replies](mr-private-review-replies.md). Commit/image/file draft creation remains separate; no atomic, durable recovery or live-instance/device guarantee is claimed. [#624](https://github.com/Theorvane/LabFox/issues/624) adds the private commit-text draft API/repository foundation: exact commit/body/original text position, matching head SHA, strict private acknowledgement and captured author/global MR validation, with no redirect/auth replay/retry. No user action is exposed; guarded commit selection, membership/freshness, shared reservations and recovery remain follow-ups. See [commit-text draft API](mr-commit-text-draft-api.md). Image/file creation and MW-07 remain in progress. [#626](https://github.com/Theorvane/LabFox/issues/626) adds MR-scoped commit reads and complete advertised-page traversal, preserving unknown/root/ordered parent metadata. Strict SHA-1 identities, offset and same-resource Link cursor checks, cross-page duplicate rejection, immutable results and origin currency guards prevent partial or obsolete membership exposure. Read-only OAuth refresh remains account-bound. See [MR commit membership reads](mr-commit-membership-reads.md). Original commit diff selection and guarded private-save/recovery UI remain follow-ups; no atomic snapshot or live-instance guarantee is claimed. [#628](https://github.com/Theorvane/LabFox/issues/628) adds fixed-SHA literal commit diff pages and guarded advertised-page traversal, retaining raw paths/text and unknown omission flags. Shared offset validation preserves MR membership and confirms unified format plus exact route echoes. Diff file limits can stop pagination before full coverage; no original coordinate, membership or private-write preflight is inferred. See [original commit diff reads](original-commit-diff-reads.md). Authoritative fork/parent context and guarded literal selection/save/recovery UI remain follow-ups. [#630](https://github.com/Theorvane/LabFox/issues/630) preserves separate nullable fork project identities and adds a fresh captured MR detail context read. Strict integer and route/global identity checks reject malformed or mismatched responses; unknown target context is never inferred, while unknown source metadata stays unknown. Currency guards discard obsolete results and typed failures; read-only authentication remains captured. See [MR commit review context](mr-commit-review-context.md). Original parent/reference validation and guarded literal selection/save/recovery UI remain follow-ups; no atomic snapshot, original-coordinate or write-eligibility guarantee is claimed. | +| MW-07 | P0 | Advanced MR review: audit inline discussions/suggestions and finish missing review/approval flows. | In progress | [#552](https://github.com/Theorvane/labfox/issues/552), [PR #553](https://github.com/Theorvane/labfox/pull/553) prevent overlapping approval, merge, and other MR commands per project/IID, including while the repository resolves; failures release the reservation for explicit retry. [#554](https://github.com/Theorvane/labfox/issues/554), [PR #555](https://github.com/Theorvane/labfox/pull/555) add repository-session observation, pre-dispatch cancellation and late-result/error isolation, success-only current-session refresh/analytics/inbox effects, cancelled to-do outcomes without current-account toasts, and generation-owned duplicate reservations. Already dispatched server writes cannot be undone. [#556](https://github.com/Theorvane/labfox/issues/556), [PR #557](https://github.com/Theorvane/labfox/pull/557) add localized approval-read loading/error states, disabled approval toggles until successful reads, read-only retry, retained-data refresh protection, reactive new-session reads with stale-result isolation, and current-state toggle selection while preserving unavailable endpoint compatibility and independent merge controls. [#558](https://github.com/Theorvane/labfox/issues/558), [PR #559](https://github.com/Theorvane/labfox/pull/559) derives approval ownership from the authenticated account user ID and documented `approved_by` membership, ignores conflicting legacy current-user flags, and refreshes membership on account replacement. Regression coverage checks real toggle dispatch on mobile/desktop in both themes. [#560](https://github.com/Theorvane/labfox/issues/560), [PR #561](https://github.com/Theorvane/labfox/pull/561) validates successful approval responses before model parsing: explicit approver lists, complete user wrappers, positive integral user IDs, and nonnegative integral counts when present. Malformed reads become sanitized domain errors; status mapping precedes payload parsing. End-to-end widget tests verify blocked approval toggles and read-only recovery in both widths/themes, while unavailable endpoint compatibility remains intact. [#562](https://github.com/Theorvane/labfox/issues/562), [PR #563](https://github.com/Theorvane/labfox/pull/563) adds an account-bound, one-page MR discussion reader with generated grouped-note models and nullable resolution metadata. It preserves next-page headers without assuming totals or fetching ahead, rejects incomplete groups and nonadvancing cursors, and keeps HTTP failures distinct from sanitized parsing errors. Building on the API/models foundation, [#564](https://github.com/Theorvane/labfox/issues/564), [PR #565](https://github.com/Theorvane/labfox/pull/565) connects grouped MR replies to the detail-screen comments flow and aggregates resolution across all resolvable notes without claiming unknown states are resolved. The shared top-level composer refreshes discussion page one after posting; explicit load-more and read/page retries preserve groups and cursors. Account-bound reads/posts ignore late results, drafts reset when the session or resource changes, and resizing preserves the current draft. Five locales and narrow/compact/wide light/dark layouts are covered. [#566](https://github.com/Theorvane/labfox/issues/566), [PR #567](https://github.com/Theorvane/labfox/pull/567) adds the MR discussion reply API foundation: encoded thread/project paths, explicit IID routing, exact nonempty Markdown bodies, validated generated note responses, sanitized typed errors and no automatic authentication replay for the write. [#568](https://github.com/Theorvane/labfox/issues/568), [PR #569](https://github.com/Theorvane/labfox/pull/569) connects a localized inline reply composer to the selected discussion. One active reply preserves the top-level draft; cancel and write reservations prevent accidental overlaps. Replies preserve Markdown, retain failed drafts for explicit retry, refresh authoritative discussion page one and ignore old-session completions. Account/MR replacement resets the selection, resizing and pagination preserve current drafts, and a removed target restores top-level submission. Five locales and three widths in both themes are covered. [#570](https://github.com/Theorvane/labfox/issues/570), [PR #571](https://github.com/Theorvane/labfox/pull/571) adds known-state Resolve/Reopen controls and a strict account-bound mutation: encoded identity, explicit resolution, no redirects/authentication replay, and updated thread/state confirmation. Resolution shares comment/reply write reservations, preserves the top-level draft, retains localized permission/failure feedback for explicit retry, and refreshes discussions and MR detail only on current-session success. Unknown/nonresolvable states offer no toggle, and an active reply draft prevents resolution. [#572](https://github.com/Theorvane/labfox/issues/572), [PR #573](https://github.com/Theorvane/labfox/pull/573) preserves generated diff-note position metadata: original version SHAs, renamed paths, old/new text lines, multiline range endpoints and image/file types. Discussion reads, replies and resolution responses reject malformed or fractional coordinates before parsing; absent legacy fields remain unknown. [#574](https://github.com/Theorvane/labfox/issues/574), [PR #575](https://github.com/Theorvane/labfox/pull/575) adds paginated diff-version reads and an explicitly selected original unified-diff snapshot. Generated version/file models retain SHA triplets, renamed paths, raw text and nullable omission flags; absent files differ from empty snapshots. Identity/payload validation, status-first typed failures and no eager pagination or current-diff fallback are covered. [#576](https://github.com/Theorvane/labfox/issues/576) / [PR #577](https://github.com/Theorvane/labfox/pull/577) adds an expandable original-diff context beside positioned notes. It matches complete SHA triplets, both file paths and exact old/new single-line text coordinates; explicit older-page reads and read-only retries preserve drafts. The matched original hunk/line has an accessible non-color marker. Unsupported, omitted, truncated or ambiguous contexts remain unavailable without a current-diff fallback; account/resource replacement resets panels and stale reads cannot dispatch follow-up snapshots or affect the new session. Five locales, three widths and both themes are covered. Multiline context, positioned thread creation and suggestion application remain separate slices. [#578](https://github.com/Theorvane/labfox/issues/578), [PR #580](https://github.com/Theorvane/labfox/pull/580) adds the single-line positioned discussion creation API foundation: exact original SHA/path/line anchors, Markdown preservation, pre-dispatch argument validation, no redirects or authentication replay, created-thread confirmation and sanitized typed failures. The line selector/composer, multiline creation and suggestions remain separate. [#579](https://github.com/Theorvane/labfox/issues/579), [PR #581](https://github.com/Theorvane/labfox/pull/581) connects the latest authoritative diff-version snapshot to localized single-line selection and discussion creation. Exact original coordinates, indexed eligibility, shared write reservations, retained drafts, session/resource isolation and visible authoritative discussion inspection before uncertain-write retry are covered. The existing MR Changes route restores from project/IID; commit diffs stay read-only. Multiline context/creation and suggestion application remain separate. [#582](https://github.com/Theorvane/labfox/issues/582) / [PR #583](https://github.com/Theorvane/labfox/pull/583) adds typed immutable suggestion metadata to MR discussion responses, preserving original/replacement code, inclusive ranges, applied status and both documented applicability spellings. Missing states remain unknown; positive integral identities/coordinates, range order, response-wide uniqueness, content/flag types and alias agreement are validated before generated parsing. Suggestion presentation/application and multiline context/creation remain separate. [#584](https://github.com/Theorvane/labfox/issues/584) / [PR #585](https://github.com/Theorvane/labfox/pull/585) adds read-only suggestion previews beside user notes with exact selectable original/replacement code, nullable application/applicability states, localized ranges and empty/unavailable content, bounded code panes, retained drafts across resize/theme, and account/resource/metadata isolation. Suggestion application and batching remain separate. [#586](https://github.com/Theorvane/labfox/issues/586) / [PR #587](https://github.com/Theorvane/labfox/pull/587) adds the single-suggestion application API foundation with a positive global identity, exact optional commit messages, strict HTTP 200 and matching applied=true confirmation, shared metadata validation, typed sanitized failures, and no redirect/authentication replay. Application UI/session reservations and batch application remain separate. [#588](https://github.com/Theorvane/labfox/issues/588) / [PR #589](https://github.com/Theorvane/labfox/pull/589) adds explicit single-suggestion confirmation with literal code and optional commit messages, a fresh exact-discussion preflight, shared reservations including pagination, current-view/account isolation, confirmed-success detail/discussion/review-snapshot refreshes, and draft-preserving read-only recovery before uncertain-write retry. Five locales, three widths, both themes, keyboard insets and large text are covered. Batch application and multiline context/creation remain separate. [#590](https://github.com/Theorvane/labfox/issues/590) / [PR #591](https://github.com/Theorvane/labfox/pull/591) adds the batch suggestion API foundation: one account-bound request with immutable positive unique global IDs, exact optional messages, strict complete matching applied-state confirmation, preserved server order/nullable metadata, status-first sanitized failures, and no redirects, authentication replay or single-write fallback. Building on that API, [#592](https://github.com/Theorvane/labfox/issues/592) / [PR #593](https://github.com/Theorvane/labfox/pull/593) adds explicit loaded-target selection and multi-patch confirmation, deduplicated all-thread preflight before one batch, immutable selected identities, shared reservations, current-account/view isolation, complete staged read-only recovery, exact retained drafts and success-only refreshes. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [the batch UI](mr-suggestion-batch-ui.md). [#594](https://github.com/Theorvane/labfox/issues/594) / [PR #595](https://github.com/Theorvane/labfox/pull/595) adds exact original multiline context with inclusive side-aware per-line markers, full filename-hash/raw-counter endpoint identity, optional coordinate support, complete contiguous hunk checks, duplicate/reversed/gapped-span rejection and retained draft/session behavior. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [original context](mr-discussion-context.md). [#596](https://github.com/Theorvane/labfox/issues/596) / [PR #597](https://github.com/Theorvane/labfox/pull/597) adds explicit original multiline selection and positioned creation, indexed complete contiguous range eligibility, exact endpoint request/response confirmation, inclusive accessible markers, retained drafts and complete-range recovery inspection. Repository identity is checked before dispatch and after completion to isolate replaced-session outcomes. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [multiline creation](mr-multiline-discussions.md). [#598](https://github.com/Theorvane/labfox/issues/598) / [PR #599](https://github.com/Theorvane/labfox/pull/599) adds the read-only pending-review draft foundation: generated private note/global MR/author identities, exact Markdown and nullable original position/reply/resolution metadata; strict one-page draft_notes reads with immutable server order, header pagination, sanitized malformed/status failures and normal read-only OAuth refresh. Account-bound repositories reject other-author pages; auto-disposed page consumers prevent stale pre-dispatch reads and ignore late outcomes. Presentation removes retained private rows immediately during account/client/instance changes, sign-out, loading and failed refresh. Draft mutations, aggregation and pending-review UI remain separate; no live GitLab/device validation is claimed. [#600](https://github.com/Theorvane/labfox/issues/600) / [PR #601](https://github.com/Theorvane/labfox/pull/601) adds the unpublished regular/original text-range draft creation API prerequisite: exact Markdown and recursive null omission, strict HTTP 201 and original-position/body confirmation, no redirect/OAuth replay/automatic retry/public-comment fallback, and sanitized typed/status-preserving failures. Account-bound repositories require and confirm captured author plus explicit global MR identity separately from route IID. Shared text/range validation preserves published discussion behavior. Replies, commit/image/file drafts, editing/deletion, publication and the pending-review controller/UI remain follow-up work; uncertain writes require authoritative inspection before an explicit retry, and no live GitLab/device validation is claimed. [#602](https://github.com/Theorvane/labfox/issues/602) / [PR #603](https://github.com/Theorvane/labfox/pull/603) adds read-only pending-review pagination with an authoritative global MR identity separate from route IID, immutable server order, explicit cursors through empty pages, optional totals and duplicate-load prevention. Mismatched author/MR identities, duplicate drafts and backward cursors reject the full result. Account/repository generations cancel obsolete dispatch and isolate initial/pagination success/errors. Presentation immediately removes retained private rows during account/client/instance/sign-out, refresh and failed reads; explicit retry restarts page one. Offset traversal is not an atomic snapshot and cannot authorize publication. The controller exposes no UI or mutation; see [private pagination](mr-pending-review-reader.md). [#604](https://github.com/Theorvane/labfox/issues/604) / [PR #605](https://github.com/Theorvane/labfox/pull/605) adds a read-only private pending-review panel on MR detail with exact selectable saved Markdown, known original metadata, localized loading/empty/partial/error states and explicit paging/refresh/retry. Guarded queries await fresh MR detail and verify route/global identity before dispatch, closing account/instance/client replacement requests ahead of detail; old private rows disappear during detail refresh and failed reads. Five locales, three widths, both themes, large text, long content and resize/theme preservation are covered with six synthetic captures. The panel sends no mutation or embedded image request and never infers/re-anchors original positions; see [private review panel](mr-pending-review-panel.md). Creation orchestration, uncertain-write recovery and publication remain follow-ups; no live GitLab/device validation is claimed. [#606](https://github.com/Theorvane/labfox/issues/606) / [PR #607](https://github.com/Theorvane/LabFox/pull/607) adds regular private review save orchestration with fresh account-bound detail/global identity, shared discussion write/pagination reservations, exact Markdown and one confirmed create before route-scoped private-reader refresh. Uncertain writes block replay across ordinary refreshes and same-account client/repository replacement; explicit complete read-only inspection follows empty-page cursors, validates ownership/identity/uniqueness/order and waits for any dispatched write to settle. Old-session/view results and errors cannot expose private rows or dispatch further pages. The controller has no composer UI, positioned/reply save orchestration, editing/deletion or publication; see [guarded saves](mr-pending-review-save.md). Recovery is in-memory and offset inspection cannot promise exactly-once creation after a client timeout; no live-instance/device validation is claimed. [#608](https://github.com/Theorvane/LabFox/issues/608) / [PR #609](https://github.com/Theorvane/LabFox/pull/609) adds a localized regular private-note composer on MR detail with exact Markdown, guarded one-save orchestration, retained failed input, complete visible inspection and separate acknowledgement before manual retry. Account/client/repository/resource replacement discards private input and snapshots; obsolete results cannot toast or expose data. Public drafts stay intact, and same-account reopen retains uncertain-write gating. Five locales, three widths, both themes and keyboard/large-text recovery are covered with twelve synthetic captures; see [private composer](mr-pending-review-composer.md). Positioned/reply/commit saves, editing/deletion and publication remain separate; no exactly-once or live-instance/device validation is claimed. [#610](https://github.com/Theorvane/LabFox/issues/610) / [PR #611](https://github.com/Theorvane/LabFox/pull/611) adds private draft update/delete API and account-bound repository foundations: exact Markdown, explicit original text-position preservation, strict 200 target/body/position confirmation and 204 deletion acknowledgement, positive project/IID/draft identities, captured author/global-MR checks and unchanged non-body metadata. Status-first plain-wire decoding preserves typed failures even for malformed JSON; redirects, authentication replay, fallback writes and automatic retry are disabled. Original line-code-only and unsupported update anchors fail before dispatch; owned opaque/image/file drafts can be deleted by identity. See [maintenance contract](mr-draft-note-maintenance.md). Fresh target/controller reservations, localized edit/delete confirmation/recovery and publication remain follow-ups; no live-instance/device validation is claimed. [#612](https://github.com/Theorvane/LabFox/issues/612) connects private draft editing and confirmed deletion to the existing pending review panel. Shared discussion reservations, fresh detail/global identity and complete private-page comparison refuse stale or missing selected targets before one write. Uncertain results gate every private write; complete visible inspection and acknowledgement adopt a current target only for explicit retry, with obsolete session/view input and outcomes discarded. Five locales, keyboard/large text and twelve synthetic captures cover three widths and both themes; see [maintenance UI](mr-pending-review-maintenance.md). Publication and positioned/reply/commit creation remain separate; no atomic snapshot, conditional-write, durable recovery or live-instance/device guarantee is claimed. [#614](https://github.com/Theorvane/LabFox/issues/614) adds whole-review publication: complete visible private-note confirmation and consent, a fresh full-set comparison before one strict-204 account-scoped bulk POST, and uncertainty that blocks private mutations until complete private/public inspection. Manual retries require new consent; session/view changes discard old confirmation and outcomes. Five locales and three widths in both themes are covered; see [publication flow](mr-pending-review-publication.md). Single-note publication, summary/reviewer-state controls and positioned/reply/commit creation remain separate. No automatic approval/merge, atomic server snapshot, durable recovery or live-instance/device guarantee is claimed. [#616](https://github.com/Theorvane/labfox/issues/616) adds private text line/range draft creation from the MR changes screen, fresh authoritative diff selection checks, immutable selected text, session/snapshot/origin isolation, preserved public input and explicit uncertain-save inspection. See [private inline review](mr-pending-review-inline.md). [#618](https://github.com/Theorvane/LabFox/issues/618) adds selected saved-note publication through one strict-204 PUT, fresh exact target comparison, explicit consent and shared two-sided recovery. Unrelated saved notes remain private; a missing selected ID cannot publish another note. See [single-note publication](mr-pending-review-single-publication.md). [#620](https://github.com/Theorvane/LabFox/issues/620) adds whole-review submission with an exact public summary, explicit reviewed/request-changes outcomes, complete current-reviewer reads, fresh reviewer preflight comparison and postwrite outcome verification. Partial or obsolete outcomes require private/public/reviewer inspection after actual write settlement and renewed consent, including selected-note recovery entry. Five locales, three widths, both themes and keyboard/large text are covered; see [review submission](mr-review-submission.md). Formal approval remains separate; additional draft creation types are follow-ups. No atomic, durable recovery or live-instance/device guarantee is claimed. [#622](https://github.com/Theorvane/LabFox/issues/622) adds private reply drafts on existing public discussion groups, exact target/body and non-resolution confirmation, a fresh selected-thread preflight, shared command reservations, and actual-settlement recovery that stages every private page together with the original public target. Missing targets stay missing; scoped recovery cannot clear publication/reviewer uncertainty. The localized shared composer preserves unsent public comments/replies and survives its own detail refresh or target removal. Five locales, keyboard/large text and twelve synthetic captures cover three widths and both themes; see [private replies](mr-private-review-replies.md). Commit/image/file draft creation remains separate; no atomic, durable recovery or live-instance/device guarantee is claimed. [#624](https://github.com/Theorvane/LabFox/issues/624) adds the private commit-text draft API/repository foundation: exact commit/body/original text position, matching head SHA, strict private acknowledgement and captured author/global MR validation, with no redirect/auth replay/retry. No user action is exposed; guarded commit selection, membership/freshness, shared reservations and recovery remain follow-ups. See [commit-text draft API](mr-commit-text-draft-api.md). Image/file creation and MW-07 remain in progress. [#626](https://github.com/Theorvane/LabFox/issues/626) adds MR-scoped commit reads and complete advertised-page traversal, preserving unknown/root/ordered parent metadata. Strict SHA-1 identities, offset and same-resource Link cursor checks, cross-page duplicate rejection, immutable results and origin currency guards prevent partial or obsolete membership exposure. Read-only OAuth refresh remains account-bound. See [MR commit membership reads](mr-commit-membership-reads.md). Original commit diff selection and guarded private-save/recovery UI remain follow-ups; no atomic snapshot or live-instance guarantee is claimed. [#628](https://github.com/Theorvane/LabFox/issues/628) adds fixed-SHA literal commit diff pages and guarded advertised-page traversal, retaining raw paths/text and unknown omission flags. Shared offset validation preserves MR membership and confirms unified format plus exact route echoes. Diff file limits can stop pagination before full coverage; no original coordinate, membership or private-write preflight is inferred. See [original commit diff reads](original-commit-diff-reads.md). Authoritative fork/parent context and guarded literal selection/save/recovery UI remain follow-ups. [#630](https://github.com/Theorvane/LabFox/issues/630) preserves separate nullable fork project identities and adds a fresh captured MR detail context read. Strict integer and route/global identity checks reject malformed or mismatched responses; unknown target context is never inferred, while unknown source metadata stays unknown. Currency guards discard obsolete results and typed failures; read-only authentication remains captured. See [MR commit review context](mr-commit-review-context.md). Original parent/reference validation and guarded literal selection/save/recovery UI remain follow-ups; no atomic snapshot, original-coordinate or write-eligibility guarantee is claimed. [#632](https://github.com/Theorvane/LabFox/issues/632) adds strict fixed-head commit detail and guarded captured-account reads. Exact SHA and ordered nullable parent checks preserve unknown/root/merge metadata without synthesizing references; status-first failures, disabled redirects and obsolete-result isolation retain honest context. General branch/tag reads remain compatible. See [fixed commit review reads](fixed-commit-review-reads.md). Original reference/coordinate validation and guarded private-save/recovery UI remain follow-ups; no membership, atomic snapshot or write-eligibility guarantee is inferred. | | MW-08 | P1 | CI/CD configuration: pipeline editor, variables, triggers, schedule editing, and execution history. | In progress | [#423](https://github.com/Theorvane/labfox/issues/423), [PR #424](https://github.com/Theorvane/labfox/pull/424) cover timing editing; [#425](https://github.com/Theorvane/labfox/issues/425), [PR #426](https://github.com/Theorvane/labfox/pull/426) cover deletion; [#427](https://github.com/Theorvane/labfox/issues/427), [PR #428](https://github.com/Theorvane/labfox/pull/428) cover creation; [#429](https://github.com/Theorvane/labfox/issues/429), [PR #430](https://github.com/Theorvane/labfox/pull/430) cover ownership transfer; [#431](https://github.com/Theorvane/labfox/issues/431), [PR #432](https://github.com/Theorvane/labfox/pull/432) cover ref/active editing. These slices await review or maintainer merge. [#433](https://github.com/Theorvane/labfox/issues/433), [PR #434](https://github.com/Theorvane/labfox/pull/434) cover schedule-specific execution history in review: newest-first header pagination, retained rows/cursor on failed next pages, retry, run-now/refresh reload with inline errors, stale-request protection, and identifier-only pipeline navigation. History filters, variables/inputs, pipeline configuration editing, and triggers remain separate slices. | | MW-09 | P1 | Wiki creation/editing/deletion and history with conflict handling. | In progress | [#351](https://github.com/Theorvane/labfox/issues/351), [PR #352](https://github.com/Theorvane/labfox/pull/352) shipped Markdown page creation; [#353](https://github.com/Theorvane/labfox/issues/353), [PR #354](https://github.com/Theorvane/labfox/pull/354) shipped editing with best-effort stale-draft detection; [#373](https://github.com/Theorvane/labfox/issues/373), [PR #374](https://github.com/Theorvane/labfox/pull/374) shipped deletion with confirmation and best-effort stale-page detection; [#375](https://github.com/Theorvane/labfox/issues/375), [PR #376](https://github.com/Theorvane/labfox/pull/376) shipped Markdown template selection. [#377](https://github.com/Theorvane/labfox/issues/377), [PR #378](https://github.com/Theorvane/labfox/pull/378) cover template listing and creation in review. Page history and atomic conflict protection remain. The documented wiki REST API can retrieve a page by version SHA but does not list page history. | | MW-10 | P1 | Snippet creation/editing/deletion, files, and visibility. | In progress | [#355](https://github.com/Theorvane/labfox/issues/355), [PR #356](https://github.com/Theorvane/labfox/pull/356) shipped single-file project snippet creation; [#357](https://github.com/Theorvane/labfox/issues/357), [PR #358](https://github.com/Theorvane/labfox/pull/358) shipped deletion with confirmation; [#359](https://github.com/Theorvane/labfox/issues/359), [PR #360](https://github.com/Theorvane/labfox/pull/360) shipped title and description editing; [#361](https://github.com/Theorvane/labfox/issues/361), [PR #362](https://github.com/Theorvane/labfox/pull/362) shipped single-file content editing; [#363](https://github.com/Theorvane/labfox/issues/363), [PR #364](https://github.com/Theorvane/labfox/pull/364) shipped private/public visibility editing; [#365](https://github.com/Theorvane/labfox/issues/365), [PR #366](https://github.com/Theorvane/labfox/pull/366) shipped multi-file content editing; [#367](https://github.com/Theorvane/labfox/issues/367), [PR #368](https://github.com/Theorvane/labfox/pull/368) shipped adding a file; [#369](https://github.com/Theorvane/labfox/issues/369), [PR #370](https://github.com/Theorvane/labfox/pull/370) shipped deleting a file; [#371](https://github.com/Theorvane/labfox/issues/371), [PR #372](https://github.com/Theorvane/labfox/pull/372) shipped moving and renaming a file. Broader visibility management remains. | diff --git a/packages/gitlab_api/lib/src/repository/repository_api.dart b/packages/gitlab_api/lib/src/repository/repository_api.dart index 828afb08..b6128fff 100644 --- a/packages/gitlab_api/lib/src/repository/repository_api.dart +++ b/packages/gitlab_api/lib/src/repository/repository_api.dart @@ -276,6 +276,64 @@ class RepositoryApi { } } + /// Reads an exact commit identity and preserves ordered, nullable parents. + /// General branch/tag detail reads use [commit] instead. This method does not + /// choose original diff references or prove current MR membership. + Future fixedCommit( + Object projectId, { + required String commitId, + }) async { + if (!((projectId is int && projectId > 0) || + (projectId is String && projectId.trim().isNotEmpty))) { + throw ArgumentError.value(projectId, 'projectId'); + } + if (!_validCommitId(commitId)) { + throw ArgumentError.value(commitId, 'commitId'); + } + try { + final response = await _dio.get( + '/projects/${_enc(projectId)}/repository/commits/$commitId', + queryParameters: {'stats': false}, + options: Options( + responseType: ResponseType.plain, + followRedirects: false, + ), + ); + if (response.statusCode != 200) { + throw mapStatus( + response.statusCode, + response.headers.map, + context: 'reading a fixed commit', + ); + } + try { + final data = jsonDecode(response.data ?? ''); + if (data is! Map || data['id'] != commitId) { + throw const FormatException(); + } + final parents = data['parent_ids']; + if (parents != null && + (parents is! List || + parents.any((p) => !_validCommitId(p) || p == commitId) || + parents.toSet().length != parents.length)) { + throw const FormatException(); + } + return Commit.fromJson(data); + } on FormatException { + throw const GitLabServerException('Invalid fixed commit response.'); + } on TypeError { + throw const GitLabServerException('Invalid fixed commit response.'); + } + } on DioException catch (error) { + throw mapError(error, context: 'reading a fixed commit'); + } + } + + static bool _validCommitId(Object? value) => + value is String && + value.length == 40 && + !RegExp(r'[^0-9a-f]').hasMatch(value); + /// Reads one page of literal original diff files for a fixed commit identity. /// Preserves unknown text/omission metadata without inferring file coverage. /// GitLab diff limits can stop pagination before every changed file is exposed. diff --git a/packages/gitlab_api/test/fixed_commit_api_test.dart b/packages/gitlab_api/test/fixed_commit_api_test.dart new file mode 100644 index 00000000..f672aff3 --- /dev/null +++ b/packages/gitlab_api/test/fixed_commit_api_test.dart @@ -0,0 +1,223 @@ +import 'package:dio/dio.dart'; +import 'package:gitlab_api/gitlab_api.dart'; +import 'package:test/test.dart'; +import 'mr_commits_api_test.dart' as b; + +void main() { + for (final project in [8, 'group/project +']) { + test( + 'fixed detail preserves exact identity, content and route $project', + () async { + late RequestOptions request; + final c = b.client((o) { + request = o; + return b.response(b.commit()); + }); + addTearDown(c.close); + final result = await c.repository.fixedCommit( + project, + commitId: b.head, + ); + expect( + request.path, + '/projects/${project is int ? project : 'group%2Fproject%20%2B'}/repository/commits/${b.head}', + ); + expect(request.baseUrl, 'https://gitlab.example.com/subpath/api/v4'); + expect(request.method, 'GET'); + expect(request.queryParameters, {'stats': false}); + expect(request.followRedirects, isFalse); + expect(request.responseType, ResponseType.plain); + expect(result.id, b.head); + expect(result.title, ' Exact commit title '); + expect(result.message, 'Exact message\n\nBody.'); + expect(result.parentIds, [b.parent, b.second]); + expect(() => result.parentIds!.clear(), throwsUnsupportedError); + }, + ); + } + for (final parents in [ + null, + [], + [b.parent], + [b.second, b.parent], + ]) { + test('retains unknown root and ordered parent metadata $parents', () async { + final c = b.client( + (o) => b.response({...b.commit(), 'parent_ids': parents}), + ); + addTearDown(c.close); + expect( + (await c.repository.fixedCommit(8, commitId: b.head)).parentIds, + parents, + ); + }); + } + test('omitted parents remain unknown', () async { + final payload = b.commit()..remove('parent_ids'); + final c = b.client((o) => b.response(payload)); + addTearDown(c.close); + expect( + (await c.repository.fixedCommit(8, commitId: b.head)).parentIds, + isNull, + ); + }); + for (final project in [0, -1, '', ' ', 8.5, false]) { + test('invalid project never dispatches $project', () async { + var calls = 0; + final c = b.client((o) { + calls++; + return b.response(b.commit()); + }); + addTearDown(c.close); + await expectLater( + c.repository.fixedCommit(project, commitId: b.head), + throwsArgumentError, + ); + expect(calls, 0); + }); + } + for (final sha in [ + '', + 'main', + 'v1.0', + '12345678', + b.head.toUpperCase(), + '${b.head} ', + '${b.head}\n', + 'z${b.head.substring(1)}', + '${b.head}${b.head}', + ]) { + test('noncanonical commit reference never dispatches $sha', () async { + var calls = 0; + final c = b.client((o) { + calls++; + return b.response(b.commit()); + }); + addTearDown(c.close); + await expectLater( + c.repository.fixedCommit(8, commitId: sha), + throwsArgumentError, + ); + expect(calls, 0); + }); + } + for (final id in [ + null, + 8, + '', + 'main', + b.parent, + b.head.toUpperCase(), + '${b.head}\n', + ]) { + test('rejects mismatched or malformed response head $id', () async { + final c = b.client((o) => b.response({...b.commit(), 'id': id})); + addTearDown(c.close); + await expectLater( + c.repository.fixedCommit(8, commitId: b.head), + throwsA(isA()), + ); + }); + } + for (final parents in [ + false, + '', + {}, + [null], + [8], + ['main'], + [b.head], + [b.parent, b.parent], + [b.parent.toUpperCase()], + ['${b.parent}\n'], + ]) { + test('rejects malformed self or duplicate parents $parents', () async { + final c = b.client( + (o) => b.response({...b.commit(), 'parent_ids': parents}), + ); + addTearDown(c.close); + await expectLater( + c.repository.fixedCommit(8, commitId: b.head), + throwsA(isA()), + ); + }); + } + for (final payload in [ + null, + [], + 8, + 'private malformed JSON', + {'id': b.head}, + {...b.commit(), 'title': false}, + {...b.commit(), 'authored_date': 'private invalid date'}, + ]) { + test('malformed payload stays sanitized $payload', () async { + final c = b.client((o) => b.response(payload)); + addTearDown(c.close); + await expectLater( + c.repository.fixedCommit(8, commitId: b.head), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Invalid fixed commit response.', + ), + ), + ); + }); + } + for (final (status, type) in [ + (401, GitLabAuthException), + (403, GitLabForbiddenException), + (404, GitLabNotFoundException), + (429, GitLabRateLimitException), + (500, GitLabServerException), + (302, GitLabServerException), + ]) { + test('status $status precedes malformed JSON decoding', () async { + final c = b.client( + (o) => b.response('private invalid', status: status, raw: true), + ); + addTearDown(c.close); + await expectLater( + c.repository.fixedCommit(8, commitId: b.head), + throwsA( + predicate( + (e) => + e.runtimeType == type && + !e.toString().contains('private invalid'), + ), + ), + ); + }); + } + test('captured read-only OAuth retries only the same fixed route', () async { + var requests = 0, refreshes = 0; + final c = b.client( + (o) { + expect(o.path, '/projects/8/repository/commits/${b.head}'); + expect(o.queryParameters, {'stats': false}); + expect(o.followRedirects, isFalse); + return ++requests == 1 + ? b.response('invalid', status: 401, raw: true) + : b.response(b.commit()); + }, + onUnauthorized: () async { + refreshes++; + return 'dummy-refreshed-token'; + }, + ); + addTearDown(c.close); + expect((await c.repository.fixedCommit(8, commitId: b.head)).id, b.head); + expect(requests, 2); + expect(refreshes, 1); + }); + test('general detail still accepts a branch reference', () async { + final c = b.client((o) { + expect(o.path, '/projects/8/repository/commits/feature%2Fwork'); + return b.response(b.commit()); + }); + addTearDown(c.close); + expect((await c.repository.commit(8, sha: 'feature/work')).id, b.head); + }); +}