| // Copyright 2020 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:cocoon_service/src/request_handling/exceptions.dart'; |
| import 'package:github/github.dart'; |
| import 'package:googleapis_auth/auth_io.dart'; |
| import 'package:http/http.dart' as http; |
| import 'package:http/retry.dart'; |
| import 'package:meta/meta.dart'; |
| |
| import '../model/gerrit/commit.dart'; |
| import 'logging.dart'; |
| |
| /// Communicates with gerrit APIs https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html |
| /// to get information about projects hosted in Git on Borg. |
| class GerritService { |
| GerritService({ |
| http.Client? httpClient, |
| @visibleForTesting this.authClientProvider = clientViaApplicationDefaultCredentials, |
| @visibleForTesting this.retryDelay, |
| }) : httpClient = httpClient ?? http.Client(); |
| |
| final http.Client httpClient; |
| |
| final Duration? retryDelay; |
| |
| /// Provider for generating a [http.Client] that is authenticated to make calls to GCP services. |
| final Future<AutoRefreshingAuthClient> Function({ |
| http.Client? baseClient, |
| required List<String> scopes, |
| }) authClientProvider; |
| |
| /// Gets the branches from a remote git repository using the gerrit APIs. |
| /// |
| /// [subString] allows to filter branches with this text (not case sensitive). |
| /// |
| /// See more: |
| /// * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#list-branches |
| Future<List<String>> branches(String repo, String project, {String? subString}) async { |
| final Map<String, String> queryParameters = <String, String>{ |
| if (subString != null && subString.isNotEmpty) 'm': subString, |
| }; |
| final Uri url = Uri.https(repo, 'projects/$project/branches', queryParameters); |
| final List<dynamic> response = await _get(url) as List<dynamic>; |
| |
| final List<String> branches = <String>[]; |
| |
| final Iterable<Map<String, dynamic>> json = response.map((dynamic e) => e as Map<String, dynamic>); |
| for (Map<String, dynamic> element in json) { |
| branches.add(element['ref'] as String); |
| } |
| |
| return branches; |
| } |
| |
| /// Gets the commit log for a project-branch pair. |
| Future<Iterable<GerritCommit>> commits(RepositorySlug slug, String branch) async { |
| final Uri url = |
| Uri.https('${slug.owner}.googlesource.com', '${slug.name}/+log/refs/heads/$branch', <String, String>{ |
| 'format': 'json', |
| }); |
| final Map<String, dynamic> response = await _get(url) as Map<String, dynamic>; |
| final List<dynamic> commitsJson = response['log'] as List<dynamic>; |
| |
| return commitsJson.map((dynamic part) => GerritCommit.fromJson(part as Map<String, dynamic>)); |
| } |
| |
| /// Creates a new branch. |
| /// |
| /// See more: |
| /// * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#create-branch |
| Future<void> createBranch(RepositorySlug slug, String branchName, String revision) async { |
| log.info('Creating branch $branchName at $revision'); |
| final Uri url = Uri.https('${slug.owner}-review.googlesource.com', 'projects/${slug.name}/branches/$branchName'); |
| String body = jsonEncode(<String, String>{ |
| 'revision': revision, |
| }); |
| final Map<String, dynamic> response = await _put( |
| url, |
| body: body, |
| branchName: branchName, |
| ) as Map<String, dynamic>; |
| log.info(response); |
| if (response['revision'] != revision) { |
| throw const InternalServerError('Failed to create branch'); |
| } |
| log.info('Created branch $branchName'); |
| } |
| |
| Future<dynamic> _get(Uri url) async { |
| final RetryClient client = RetryClient(httpClient); |
| final http.Response response = await client.get(url); |
| |
| /// To prevent against Cross Site Script Inclusion (XSSI) attacks, the JSON response body starts with a magic prefix line that |
| /// must be stripped before feeding the rest of the response body to a JSON parser. The magic prefix is ")]}'". |
| final String jsonBody = response.body.replaceRange(0, 4, ''); |
| return jsonDecode(jsonBody) as dynamic; |
| } |
| |
| Future<dynamic> _put( |
| Uri url, { |
| Object? body, |
| // TODO(chillers): Remove once b/239021831 has been fixed by the GoB side. |
| String? branchName, |
| }) async { |
| final http.Client authClient = await authClientProvider(baseClient: httpClient, scopes: <String>[]); |
| // GoB replicas may not have all the Flutter state, and can require several retries |
| final http.Client client = RetryClient( |
| authClient, |
| when: (http.BaseResponse response) => _responseIsAcceptable(response) == false, |
| delay: (int attempt) => retryDelay ?? const Duration(seconds: 3) * attempt, |
| ); |
| final http.Response response = await client.put( |
| url, |
| body: body, |
| headers: <String, String>{ |
| // TODO(chillers): Remove once b/239021831 has been fixed by the GoB side. |
| 'X-Gerrit-Trace': 'bug-239021831-$branchName' |
| }, |
| ); |
| if (_responseIsAcceptable(response) == false) { |
| throw InternalServerError('Gerrit returned ${response.statusCode} which is not 200 or 202'); |
| } |
| log.info('Sent PUT to $url'); |
| log.info(response.body); |
| // Remove XSS token |
| final String jsonBody = response.body.replaceRange(0, 4, ''); |
| log.info(jsonBody); |
| return jsonDecode(jsonBody); |
| } |
| |
| bool _responseIsAcceptable(http.BaseResponse response) => |
| response.statusCode == HttpStatus.ok || response.statusCode == HttpStatus.accepted; |
| } |