blob: f2e56199d492b25fd09b11d337c9da8b6bf70d85 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. 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:path/path.dart' as p;
import 'process.dart';
/// The upstream ref that rolls are computed against and uploaded on top of.
const _upstreamRef = 'origin/main';
/// Git operations on an SDK checkout.
final class GitCheckout {
final String root;
GitCheckout(this.root);
/// Updates [_upstreamRef] from the remote.
Future<void> fetchUpstream() => _git(['fetch', '--quiet', 'origin', 'main']);
/// Returns the contents of `DEPS` at [_upstreamRef].
Future<String> readUpstreamDeps() => _git(['show', '$_upstreamRef:DEPS']);
/// Returns how long ago the DEPS variable [name] was last changed on
/// [_upstreamRef].
Future<Duration> depsVarAge(String name) async {
final pattern = '"$name":';
final date = await _git([
'log',
'-1',
'--format=%cI',
'-G',
pattern,
_upstreamRef,
'--',
'DEPS',
]);
if (date.trim().isEmpty) {
throw StateError('No commit on $_upstreamRef changes $pattern in DEPS.');
}
return DateTime.now().difference(DateTime.parse(date.trim()));
}
/// Commits [depsContents] to `DEPS` with [commitMessage] on a temporary
/// branch off [_upstreamRef], and runs [action] in a temporary worktree
/// with that branch checked out.
///
/// The worktree is created under the system temp directory and removed
/// along with the branch afterwards, so this checkout's working tree and
/// current branch are never touched.
Future<T> withTemporaryDepsCommit<T>({
required String depsContents,
required String commitMessage,
required Future<T> Function(GitCheckout worktree) action,
}) async {
final tempDir = Directory.systemTemp.createTempSync('roll_browsers.');
final worktree = GitCheckout(p.join(tempDir.path, 'sdk'));
final branch = 'roll-browsers-${DateTime.now().millisecondsSinceEpoch}';
try {
await _git([
'worktree',
'add',
'--quiet',
'--track',
'-b',
branch,
worktree.root,
_upstreamRef,
]);
File(p.join(worktree.root, 'DEPS')).writeAsStringSync(depsContents);
await worktree._git([
'commit',
'--quiet',
'--all',
'--message',
commitMessage,
]);
return await action(worktree);
} finally {
await _removeWorktree(worktree.root, branch);
if (tempDir.existsSync()) tempDir.deleteSync(recursive: true);
}
}
/// Removes the worktree at [path] and deletes [branch], ignoring failures
/// so cleanup never hides the original error.
Future<void> _removeWorktree(String path, String branch) async {
for (final arguments in [
['worktree', 'remove', '--force', path],
['worktree', 'prune'],
['branch', '--quiet', '-D', branch],
]) {
await runProcess('git', arguments, workingDirectory: root);
}
}
/// Runs `git cl` with [arguments] and returns its stdout.
Future<String> cl(List<String> arguments) =>
runChecked('git', ['cl', ...arguments], workingDirectory: root);
Future<String> _git(List<String> arguments) =>
runChecked('git', arguments, workingDirectory: root);
}