| // 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:github/github.dart'; | |
| import 'package:logging/logging.dart'; | |
| import 'cli_command.dart'; | |
| import 'utilities.dart'; | |
| /// Class to wrap the command line calls to git. | |
| class GitCli { | |
| Logger logger = Logger('RepositoryManager'); | |
| static const String git = 'git'; | |
| static const String repositoryHttpPrefix = 'https://github.com/'; | |
| static const String repositorySshPrefix = 'git@github.com:'; | |
| late String repositoryPrefix; | |
| late CliCommand _cliCommand; | |
| GitCli(GitAccessMethod gitCloneMethod, CliCommand cliCommand) { | |
| switch (gitCloneMethod) { | |
| case GitAccessMethod.SSH: | |
| repositoryPrefix = repositorySshPrefix; | |
| break; | |
| case GitAccessMethod.HTTP: | |
| repositoryPrefix = repositoryHttpPrefix; | |
| break; | |
| } | |
| _cliCommand = cliCommand; | |
| } | |
| /// Check to see if the current directory is a git repository. | |
| Future<bool> isGitRepository(String directory) async { | |
| final processResult = await _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>['rev-parse'], | |
| throwOnError: false, | |
| workingDirectory: directory, | |
| ); | |
| return processResult.exitCode == 0; | |
| } | |
| /// Checkout repository if it does not currently exist on disk. | |
| /// We will need to protect against multiple checkouts just in case multiple | |
| /// calls occur at the same time. | |
| Future<ProcessResult> cloneRepository({ | |
| required RepositorySlug slug, | |
| required String workingDirectory, | |
| required String targetDirectory, | |
| List<String>? options, | |
| bool throwOnError = true, | |
| }) async { | |
| final clone = <String>[ | |
| 'clone', | |
| '$repositoryPrefix${slug.fullName}', | |
| targetDirectory, | |
| ]; | |
| if (options != null) { | |
| clone.addAll(options); | |
| } | |
| final processResult = await _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: clone, | |
| workingDirectory: workingDirectory, | |
| throwOnError: throwOnError, | |
| ); | |
| return processResult; | |
| } | |
| Future<ProcessResult> setupUserConfig({ | |
| required RepositorySlug slug, | |
| required String workingDirectory, | |
| bool throwOnError = true, | |
| }) async { | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>[ | |
| 'config', | |
| '--global', | |
| 'user.name', | |
| '"auto-submit[bot]"', | |
| ], | |
| workingDirectory: workingDirectory, | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| Future<ProcessResult> setupUserEmailConfig({ | |
| required RepositorySlug slug, | |
| required String workingDirectory, | |
| bool throwOnError = true, | |
| }) async { | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>[ | |
| 'config', | |
| '--global', | |
| 'user.email', | |
| // TODO not sure if the bot will end up needing a valid email. | |
| // This might also be flutter-auto-submit-bot@google.com | |
| '"flutter-engprod-team@google.com"', | |
| ], | |
| workingDirectory: workingDirectory, | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| /// This is necessary with forked repos but may not be necessary with the bot | |
| /// as the bot has direct access to the repository. | |
| Future<ProcessResult> setUpstream({ | |
| required RepositorySlug slug, | |
| required String workingDirectory, | |
| required String branchName, | |
| required String token, | |
| bool throwOnError = true, | |
| }) async { | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>[ | |
| 'remote', | |
| 'set-url', | |
| 'origin', | |
| 'https://autosubmit[bot]:$token@github.com/${slug.fullName}.git', | |
| ], | |
| workingDirectory: workingDirectory, | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| /// Fetch all new refs for the repository. | |
| Future<ProcessResult> fetchAll({ | |
| required String workingDirectory, | |
| bool throwOnError = true, | |
| }) async { | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>['fetch', '--all'], | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| Future<ProcessResult> pullRebase({ | |
| required String? workingDirectory, | |
| bool throwOnError = true, | |
| }) async { | |
| return _updateRepository( | |
| workingDirectory: workingDirectory, | |
| pullMethod: '--rebase', | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| Future<ProcessResult> pullMerge({ | |
| required String? workingDirectory, | |
| bool throwOnError = true, | |
| }) async { | |
| return _updateRepository( | |
| workingDirectory: workingDirectory, | |
| pullMethod: '--merge', | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| /// Run the git pull rebase command to keep the repository up to date. | |
| Future<ProcessResult> _updateRepository({ | |
| required String? workingDirectory, | |
| required String pullMethod, | |
| bool throwOnError = true, | |
| }) async { | |
| final processResult = await _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>['pull', pullMethod], | |
| workingDirectory: workingDirectory, | |
| ); | |
| return processResult; | |
| } | |
| /// Checkout and create a branch for the current edit. | |
| /// | |
| /// TODO The strategy may be unneccessary here as the bot will not have to | |
| /// create its own fork of the repo. | |
| Future<ProcessResult> createBranch({ | |
| required String newBranchName, | |
| required String workingDirectory, | |
| bool useCheckout = false, | |
| bool throwOnError = true, | |
| }) async { | |
| // Then create the new branch. | |
| List<String> args; | |
| if (useCheckout) { | |
| args = <String>['checkout', '-b', newBranchName]; | |
| } else { | |
| args = <String>['branch', newBranchName]; | |
| } | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: args, | |
| workingDirectory: workingDirectory, | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| /// Revert a pull request commit. | |
| Future<ProcessResult> revertChange({ | |
| required String commitSha, | |
| required String workingDirectory, | |
| bool throwOnError = true, | |
| }) async { | |
| // Issue a revert of the pull request. | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>['revert', '--no-edit', '-m', '1', commitSha], | |
| workingDirectory: workingDirectory, | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| /// Push changes made to the local branch to github. | |
| Future<ProcessResult> pushBranch({ | |
| required String branchName, | |
| required String workingDirectory, | |
| bool throwOnError = true, | |
| }) async { | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>['push', '--verbose', 'origin', branchName], | |
| workingDirectory: workingDirectory, | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| /// Delete a local branch from the repo. | |
| Future<ProcessResult> deleteLocalBranch({ | |
| required String branchName, | |
| required String workingDirectory, | |
| bool throwOnError = true, | |
| }) async { | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>['branch', '-D', branchName], | |
| workingDirectory: workingDirectory, | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| /// Delete a remote branch from the repo. | |
| /// | |
| /// When merging a pull request the pr branch is not automatically deleted. | |
| Future<ProcessResult> deleteRemoteBranch({ | |
| required String branchName, | |
| required String workingDirectory, | |
| bool throwOnError = true, | |
| }) async { | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>['push', 'origin', '--delete', branchName], | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| /// Get the remote origin of the current repository. | |
| Future<ProcessResult> showOriginUrl({ | |
| required String workingDirectory, | |
| bool throwOnError = true, | |
| }) async { | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>['config', '--get', 'remote.origin.url'], | |
| workingDirectory: workingDirectory, | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| Future<ProcessResult> switchBranch({ | |
| required String workingDirectory, | |
| required String branchName, | |
| bool throwOnError = true, | |
| }) async { | |
| return _cliCommand.runCliCommand( | |
| executable: git, | |
| arguments: <String>['switch', branchName], | |
| workingDirectory: workingDirectory, | |
| throwOnError: throwOnError, | |
| ); | |
| } | |
| } |