Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/mobile-web-parity.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/mr-suggestion-application-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ Only HTTP 200 with a strictly validated matching suggestion ID and `applied: tru

The shared suggestion validator retains the existing MR read/reply/resolution/positioned-creation boundaries: positive integral IDs and supplied coordinates, ordered complete ranges, string content, boolean status, consistent legacy/documented applicability aliases, and response-wide identity uniqueness. It runs before generated numeric parsing so fractional values cannot be truncated. The apply response uses the same validator and generated DTO; malformed or unconfirmed results become `Invalid suggestion application response.` without source text, IDs or raw parser details.

An error or unconfirmed response can follow an accepted repository write. A caller must reload authoritative discussions and inspect the current suggestion state before offering an explicitly confirmed manual retry. The API does not implement that UI, shared discussion reservation, account/resource completion handling, custom-message composer or batch application. Those are separate slices; the existing preview remains read-only. No live repository change or physical-device validation is claimed.
An error or unconfirmed response can follow an accepted repository write. A caller must reload authoritative discussions and inspect the current suggestion state before offering an explicitly confirmed manual retry. The API itself does not own UI confirmation or completion effects. The app now implements single-suggestion confirmation, shared discussion reservations, account/resource isolation and read-only recovery as documented in [the application UI](mr-suggestion-application-ui.md). The [batch API](mr-suggestion-batch-api.md) is a separate foundation; batch selection and confirmation UI remain follow-up work. No live repository change or physical-device validation is claimed.

Tests use documented synthetic payloads and dummy credentials. Reference: [Suggest Changes API](https://docs.gitlab.com/api/suggestions/).
2 changes: 1 addition & 1 deletion docs/mr-suggestion-application-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ These are synthetic Flutter widget-test captures with dummy metadata and SDK fon
| Mobile, 390 × 844 | [Capture](images/mr-suggestion-apply/apply-390-light.png) | [Capture](images/mr-suggestion-apply/apply-390-dark.png) |
| Desktop, 1200 × 900 | [Capture](images/mr-suggestion-apply/apply-1200-light.png) | [Capture](images/mr-suggestion-apply/apply-1200-dark.png) |

Batch suggestion application and multiline discussion context/creation remain separate parity slices.
The [batch application API](mr-suggestion-batch-api.md) is available as a separate foundation. Batch selection/confirmation UI and multiline discussion context/creation remain separate parity slices.
17 changes: 17 additions & 0 deletions docs/mr-suggestion-batch-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Confirmed batch suggestion application API

`GitLabClient.suggestions.applyBatch(suggestionIds, commitMessage: ...)` uses authenticated `PUT /suggestions/batch_apply` with an `ids` array of global suggestion identities. It sends one batch request, with no fallback to separate suggestion writes. The account-bound instance URL and existing authentication retain self-hosted subpaths and PAT/OAuth support.

The input must be nonempty, positive, and unique. The client rejects invalid inputs before dispatch rather than silently removing duplicates or invalid identities. A snapshot taken before the first asynchronous boundary protects both the outgoing request and confirmation against caller list mutation. The requested order is preserved in the body. A null message is omitted; present strings, including empty or whitespace-only messages, remain exact. GitLab decides their validity and the caller's permissions.

Only HTTP 200 with a valid array confirms the batch. Every requested identity must appear exactly once with `applied=true`; missing, extra, duplicate, fractional, unknown or unapplied identities reject the entire returned confirmation. No partial result is exposed. The returned list is immutable and preserves server order, which need not match request order. Original/replacement code, inclusive line ranges and both applicability spellings use the existing strict validator before generated parsing. Optional metadata remains nullable, empty strings remain empty, and conflicting flags or reversed ranges are rejected.

The write disables redirects and automatic OAuth refresh/replay. Status-first mapping preserves authentication, permission, not-found, rate-limit and other HTTP status information; transport failures are sanitized domain errors. Malformed or incomplete confirmation becomes `Invalid suggestion batch application response.` without source code, identities or raw decoding details.

This confirms returned suggestion states, not a commit count, branch SHA, permission claim or client-side atomicity guarantee. An error or incomplete response can follow an accepted repository write. Callers must freshly inspect every selected discussion and require renewed confirmation before an explicit manual retry. There is no automatic application retry, single-write fallback, rollback or inferred success for part of a batch.

This slice provides the API only. The existing app still offers single-suggestion confirmation/application. Batch selection, MR grouping, multi-patch confirmation, shared UI reservations and complete fresh-state recovery belong to a separate UI slice. No model, dependency or generated file changes, live GitLab writes or physical-device validation are included.

Test-first coverage includes missing-method failure and 130 failing behavior contracts against a minimal unsafe implementation, followed by all 149 batch contracts and 254 existing single-application/metadata cases passing together. Tests cover exact routing/authentication/messages, invalid inputs, reordered and sparse confirmed payloads, every member's strict metadata boundary, incomplete identities, caller mutation, immutable results, typed failures and no OAuth replay.

Reference: [GitLab batch suggestion application](https://docs.gitlab.com/api/suggestions/#apply-multiple-suggestions).
72 changes: 72 additions & 0 deletions packages/gitlab_api/lib/src/suggestions/suggestions_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,78 @@ class SuggestionsApi {
}
}

/// Applies one batch without falling back to separate suggestion writes.
///
/// Every requested identity must appear once with applied=true. Response
/// order is preserved; no commit count or atomicity guarantee is inferred.
/// An uncertain result requires fresh discussion inspection before retrying.
Future<List<Suggestion>> applyBatch(
List<int> suggestionIds, {
String? commitMessage,
}) async {
// Copy before the first await so caller changes cannot alter the write or
// the identities whose returned state must be confirmed.
final ids = List<int>.unmodifiable(suggestionIds);
if (ids.isEmpty ||
ids.any((id) => id < 1) ||
ids.toSet().length != ids.length) {
throw ArgumentError(
'Nonempty unique positive suggestion IDs are required.',
'suggestionIds',
);
}
try {
final response = await _dio.put<dynamic>(
'/suggestions/batch_apply',
data: {'ids': ids, 'commit_message': ?commitMessage},
options: Options(
followRedirects: false,
extra: {'labfox_no_auth_retry': true},
),
);
if (response.statusCode != 200) {
throw mapStatus(
response.statusCode,
response.headers.map,
context: 'applying code suggestions',
);
}
return _confirmedBatch(response.data, ids.toSet());
} on DioException catch (error) {
throw mapError(error, context: 'applying code suggestions');
}
}

static List<Suggestion> _confirmedBatch(
Object? payload,
Set<int> expectedIds,
) {
try {
if (payload is! List || payload.length != expectedIds.length) {
throw const FormatException();
}
validateSuggestionPayloads(payload);
final suggestions = <Suggestion>[];
for (final entry in payload) {
final suggestion = Suggestion.fromJson(entry as Map<String, dynamic>);
if (!expectedIds.contains(suggestion.id) ||
suggestion.applied != true) {
throw const FormatException();
}
suggestions.add(suggestion);
}
return List<Suggestion>.unmodifiable(suggestions);
} on FormatException {
throw const GitLabServerException(
'Invalid suggestion batch application response.',
);
} on TypeError {
throw const GitLabServerException(
'Invalid suggestion batch application response.',
);
}
}

static Suggestion _confirmed(Object? payload, int suggestionId) {
try {
if (payload is! Map<String, dynamic>) throw const FormatException();
Expand Down
Loading
Loading