Skip to content

Commit

Permalink
Automatic Revert (Part 1): Base services needed for the automated rev…
Browse files Browse the repository at this point in the history
…ert. (flutter#3083)

This is the first part of the changes for Automated revert. These are the base classes that actually perform the revert. 

There are two methods that can be used. Unfortunately the github graphql method does not currently work. I have an open issue with github but included the code here anyway.

The git command line tools currently perform the revert. It checks out the repo, reverts the commit on the command line and then pushes the commit to github. It then creates a branch with the change which will be auto approved by the fluttergithubbot. Then ultimately merged.

*List which issues are fixed by this PR. You must list at least one issue.*
Part of flutter/flutter#113867

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
  • Loading branch information
ricardoamador authored Sep 15, 2023
1 parent 5d3c2be commit 31f6f44
Show file tree
Hide file tree
Showing 13 changed files with 991 additions and 0 deletions.
89 changes: 89 additions & 0 deletions auto_submit/lib/action/git_cli_revert_method.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2023 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:io';

import 'package:auto_submit/action/revert_method.dart';
import 'package:auto_submit/configuration/repository_configuration.dart';
import 'package:auto_submit/git/cli_command.dart';
import 'package:auto_submit/git/utilities.dart';
import 'package:auto_submit/git/git_cli.dart';
import 'package:auto_submit/git/git_repository_manager.dart';
import 'package:auto_submit/requests/exceptions.dart';
import 'package:auto_submit/service/config.dart';
import 'package:auto_submit/service/github_service.dart';
import 'package:auto_submit/service/log.dart';
import 'package:auto_submit/service/revert_issue_body_formatter.dart';
import 'package:github/github.dart' as github;
import 'package:github/github.dart';
import 'package:retry/retry.dart';

class GitCliRevertMethod implements RevertMethod {
@override
Future<github.PullRequest?> createRevert(Config config, github.PullRequest pullRequest) async {
final github.RepositorySlug slug = pullRequest.base!.repo!.slug();
final String commitSha = pullRequest.mergeCommitSha!;
// we will need to collect the pr number after the revert request is generated.

final RepositoryConfiguration repositoryConfiguration = await config.getRepositoryConfiguration(slug);
final String baseBranch = repositoryConfiguration.defaultBranch;

final String cloneToDirectory = '${slug.name}_$commitSha';
final GitRepositoryManager gitRepositoryManager = GitRepositoryManager(
slug: slug,
workingDirectory: Directory.current.path,
cloneToDirectory: cloneToDirectory,
gitCli: GitCli(GitAccessMethod.HTTP, CliCommand()),
);

// The exception is caught by the thrower.
try {
await gitRepositoryManager.cloneRepository();
await gitRepositoryManager.setupConfig();
await gitRepositoryManager.revertCommit(baseBranch, commitSha, slug, await config.generateGithubToken(slug));
} finally {
await gitRepositoryManager.deleteRepository();
}

final GitRevertBranchName gitRevertBranchName = GitRevertBranchName(commitSha);
final GithubService githubService = await config.createGithubService(slug);

const RetryOptions retryOptions =
RetryOptions(delayFactor: Duration(seconds: 1), maxDelay: Duration(seconds: 1), maxAttempts: 4);

Branch? branch;
// Attempt a few times to get the branch name. This may not be needed.
// Let the exception bubble up from here.
await retryOptions.retry(
() async {
branch = await githubService.getBranch(slug, gitRevertBranchName.branch);
},
retryIf: (Exception e) => e is NotFoundException,
);

log.info('found branch ${slug.fullName}/${branch!.name}, safe to create revert request.');

final RevertIssueBodyFormatter formatter = RevertIssueBodyFormatter(
slug: slug,
originalPrNumber: pullRequest.number!,
initiatingAuthor: 'ricardoamador',
originalPrTitle: pullRequest.title!,
originalPrBody: pullRequest.body!,
).format;

log.info('Attempting to create pull request with ${slug.fullName}/${gitRevertBranchName.branch}.');
final github.PullRequest revertPullRequest = await githubService.createPullRequest(
slug: slug,
title: formatter.revertPrTitle,
head: gitRevertBranchName.branch,
base: 'main',
draft: false,
body: formatter.revertPrBody,
);

log.info('pull request number is: ${slug.fullName}/${revertPullRequest.number}');

return revertPullRequest;
}
}
11 changes: 11 additions & 0 deletions auto_submit/lib/action/revert_method.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2023 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 'package:auto_submit/service/config.dart';
import 'package:github/github.dart' as github;

abstract class RevertMethod {
// Allows substitution of the method of creating the revert request.
Future<Object?> createRevert(Config config, github.PullRequest pullRequest);
}
58 changes: 58 additions & 0 deletions auto_submit/lib/git/cli_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2023 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:io';

class CliCommand {
CliCommand();

/// Method runs a single command in a shell.
Future<ProcessResult> runCliCommand({
required String executable,
required List<String> arguments,
bool throwOnError = true,
String? workingDirectory,
}) async {
final process = await Process.start(
executable,
arguments,
workingDirectory: workingDirectory,
runInShell: true,
mode: ProcessStartMode.normal,
);

final result = await Future.wait([
process.exitCode,
process.stdout.transform(const SystemEncoding().decoder).join(),
process.stderr.transform(const SystemEncoding().decoder).join(),
]);

final ProcessResult processResult = ProcessResult(
process.pid,
result[0] as int,
result[1] as String,
result[2] as String,
);

if (throwOnError) {
if (processResult.exitCode != 0) {
final Map<String, String> outputs = {
if (processResult.stdout != null) 'Standard out': processResult.stdout.toString().trim(),
if (processResult.stderr != null) 'Standard error': processResult.stderr.toString().trim(),
}..removeWhere((k, v) => v.isEmpty);

String errorMessage;
if (outputs.isEmpty) {
errorMessage = 'Unknown error.';
} else {
errorMessage = outputs.entries.map((entry) => '${entry.key}\n${entry.value}').join('\n');
}

throw ProcessException(executable, arguments, errorMessage, processResult.exitCode);
}
}

return processResult;
}
}
Loading

0 comments on commit 31f6f44

Please sign in to comment.