blob: db28e6df70ab65fea99b46bd7db03ddbc77cfd7e [file] [log] [blame]
// Copyright 2013 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' as io show Directory, File, stdout;
import 'package:path/path.dart' as path;
import 'package:process/process.dart';
import 'package:process_runner/process_runner.dart';
/// Utility methods for working with a git repository.
final class GitRepo {
/// The git repository rooted at `root`.
GitRepo.fromRoot(this.root, {
this.verbose = false,
StringSink? logSink,
ProcessManager processManager = const LocalProcessManager(),
}) :
_processManager = processManager,
logSink = logSink ?? io.stdout;
/// Whether to produce verbose log output.
///
/// If true, output of git commands will be printed to [logSink].
final bool verbose;
/// Where to send verbose log output.
///
/// Defaults to [io.stdout].
final StringSink logSink;
/// The root of the git repo.
final io.Directory root;
/// The delegate to use for running processes.
final ProcessManager _processManager;
/// Returns a list of all non-deleted files which differ from the nearest
/// merge-base with `main`. If it can't find a fork point, uses the default
/// merge-base.
///
/// This is only computed once and cached. Subsequent invocations of the
/// getter will return the same result.
late final Future<List<io.File>> changedFiles = _changedFiles();
/// Returns the SHA of the current HEAD commit.
Future<String> headSha({bool short = false}) async {
final ProcessRunnerResult result = await ProcessRunner(
defaultWorkingDirectory: root,
processManager: _processManager,
).runProcess(
<String>['git', 'rev-parse', if (short) '--short' ,'HEAD'],
);
return result.stdout.trim();
}
Future<List<io.File>> _changedFiles() async {
final ProcessRunner processRunner = ProcessRunner(
defaultWorkingDirectory: root,
processManager: _processManager,
);
await _fetch(processRunner);
// Find the merge base between the current branch and the branch that was
// checked out at the time of the last fetch. The merge base is the common
// ancestor of the two branches, and the output is the hash of the merge
// base.
ProcessRunnerResult mergeBaseResult = await processRunner.runProcess(
<String>['git', 'merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'],
failOk: true,
);
if (mergeBaseResult.exitCode != 0) {
if (verbose) {
logSink.writeln('git merge-base --fork-point failed, using default merge-base');
logSink.writeln('Output:\n${mergeBaseResult.stdout}');
}
mergeBaseResult = await processRunner.runProcess(<String>[
'git',
'merge-base',
'FETCH_HEAD',
'HEAD',
]);
}
final String mergeBase = mergeBaseResult.stdout.trim();
final ProcessRunnerResult masterResult = await processRunner.runProcess(<String>[
'git',
'diff',
'--name-only',
'--diff-filter=ACMRT',
mergeBase,
]);
return _gitOutputToList(masterResult);
}
/// Returns a list of non-deleted files which differ between the HEAD
/// commit and its parent.
///
/// This is only computed once and cached. Subsequent invocations of the
/// getter will return the same result.
late final Future<List<io.File>> changedFilesAtHead = _changedFilesAtHead();
Future<List<io.File>> _changedFilesAtHead() async {
final ProcessRunner processRunner = ProcessRunner(
defaultWorkingDirectory: root,
processManager: _processManager,
);
await _fetch(processRunner);
final ProcessRunnerResult diffTreeResult = await processRunner.runProcess(
<String>[
'git',
'diff-tree',
'--no-commit-id',
'--name-only',
'--diff-filter=ACMRT', // Added, copied, modified, renamed, or type-changed.
'-r',
'HEAD',
],
);
return _gitOutputToList(diffTreeResult);
}
Future<void> _fetch(ProcessRunner processRunner) async {
final ProcessRunnerResult fetchResult = await processRunner.runProcess(
<String>['git', 'fetch', 'upstream', 'main'],
failOk: true,
);
if (fetchResult.exitCode != 0) {
if (verbose) {
logSink.writeln('git fetch upstream main failed, using origin main');
logSink.writeln('Output:\n${fetchResult.stdout}');
}
await processRunner.runProcess(<String>[
'git',
'fetch',
'origin',
'main',
]);
}
}
List<io.File> _gitOutputToList(ProcessRunnerResult result) {
final String diffOutput = result.stdout.trim();
if (verbose) {
logSink.writeln('git diff output:\n$diffOutput');
}
final Set<String> resultMap = <String>{};
resultMap.addAll(diffOutput.split('\n').where(
(String str) => str.isNotEmpty,
));
return resultMap.map<io.File>(
(String filePath) => io.File(path.join(root.path, filePath)),
).toList();
}
}