| // Copyright 2013 The Flutter Authors |
| // 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; |
| |
| import 'package:git/git.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| /// Finding diffs based on `baseGitDir` and `baseSha`. |
| class GitVersionFinder { |
| /// Constructor |
| GitVersionFinder(this.baseGitDir, {String? baseSha, String? baseBranch}) |
| : assert( |
| baseSha == null || baseBranch == null, |
| 'At most one of baseSha and baseBranch can be provided', |
| ), |
| _baseSha = baseSha, |
| _baseBranch = baseBranch ?? 'main'; |
| |
| /// The top level directory of the git repo. |
| /// |
| /// That is where the .git/ folder exists. |
| final GitDir baseGitDir; |
| |
| /// The base sha used to get diff. |
| String? _baseSha; |
| |
| /// The base branche used to find a merge point if baseSha is not provided. |
| final String _baseBranch; |
| |
| /// Get a list of all the changed files. |
| Future<List<String>> getChangedFiles({bool includeUncommitted = false}) async { |
| final String baseSha = await getBaseSha(); |
| final io.ProcessResult changedFilesCommand = await baseGitDir.runCommand(<String>[ |
| 'diff', |
| '-z', |
| '--name-only', |
| baseSha, |
| if (!includeUncommitted) 'HEAD', |
| ]); |
| return _splitDiffOutputs(changedFilesCommand.stdout.toString()); |
| } |
| |
| /// Get a list of all the staged files. |
| Future<List<String>> getStagedFiles() async { |
| final io.ProcessResult changedFilesCommand = await baseGitDir.runCommand(const <String>[ |
| 'diff', |
| '--cached', |
| '-z', |
| '--name-only', |
| '--diff-filter=ACM', |
| ]); |
| return _splitDiffOutputs(changedFilesCommand.stdout.toString()); |
| } |
| |
| /// Splits the stdout of a `git diff` command into a list of file paths. |
| /// |
| /// When `git diff` is run with the `-z` flag, it outputs file paths separated |
| /// by a null byte (`\u0000`, ASCII 0). Otherwise, it outputs file paths |
| /// separated by newlines. This method accounts for that by checking for |
| /// null bytes and falling back to splitting by newlines if none are found. |
| List<String> _splitDiffOutputs(String stdout) { |
| if (stdout.isEmpty) { |
| return <String>[]; |
| } |
| final List<String> files; |
| if (stdout.contains('\u0000')) { |
| files = stdout.split('\u0000'); |
| } else { |
| files = stdout.split('\n'); |
| } |
| return files.where((String file) => file.isNotEmpty).toList(); |
| } |
| |
| /// Get a list of all the changed files. |
| Future<List<String>> getDiffContents({ |
| String? targetPath, |
| bool includeUncommitted = false, |
| }) async { |
| final String baseSha = await getBaseSha(); |
| final io.ProcessResult diffCommand = await baseGitDir.runCommand(<String>[ |
| 'diff', |
| baseSha, |
| if (!includeUncommitted) 'HEAD', |
| if (targetPath != null) ...<String>['--', targetPath], |
| ]); |
| final diffStdout = diffCommand.stdout.toString(); |
| if (diffStdout.isEmpty) { |
| return <String>[]; |
| } |
| final List<String> changedFiles = diffStdout.split('\n') |
| ..removeWhere((String element) => element.isEmpty); |
| return changedFiles.toList(); |
| } |
| |
| /// Get the package version specified in the pubspec file in `pubspecPath` and |
| /// at the revision of `gitRef` (defaulting to the base if not provided). |
| Future<Version?> getPackageVersion(String pubspecPath, {String? gitRef}) async { |
| final String ref = gitRef ?? (await getBaseSha()); |
| |
| io.ProcessResult gitShow; |
| try { |
| gitShow = await baseGitDir.runCommand(<String>['show', '$ref:$pubspecPath']); |
| } on io.ProcessException { |
| return null; |
| } |
| final fileContent = gitShow.stdout as String; |
| if (fileContent.trim().isEmpty) { |
| return null; |
| } |
| final fileYaml = loadYaml(fileContent) as YamlMap; |
| final versionString = fileYaml['version'] as String?; |
| return versionString == null ? null : Version.parse(versionString); |
| } |
| |
| /// Returns the base used to diff against. |
| Future<String> getBaseSha() async { |
| String? baseSha = _baseSha; |
| if (baseSha != null && baseSha.isNotEmpty) { |
| return baseSha; |
| } |
| |
| io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand(<String>[ |
| 'merge-base', |
| '--fork-point', |
| _baseBranch, |
| 'HEAD', |
| ], throwOnError: false); |
| final String stdout = (baseShaFromMergeBase.stdout as String? ?? '').trim(); |
| final String stderr = (baseShaFromMergeBase.stderr as String? ?? '').trim(); |
| if (stderr.isNotEmpty || stdout.isEmpty) { |
| baseShaFromMergeBase = await baseGitDir.runCommand(<String>[ |
| 'merge-base', |
| _baseBranch, |
| 'HEAD', |
| ]); |
| } |
| baseSha = (baseShaFromMergeBase.stdout as String).trim(); |
| _baseSha = baseSha; |
| return baseSha; |
| } |
| } |