clone: Split into multiple files and make it more pluggable

This way I can more easily choose if I want to use libgit2 or the git
executable or whatever. It's being done so that one can wrtie automated
tests for this cloneRemote function.

There are still some users for whom this doesn't work, and after so so
many iterations, it's time to write tests. This is just way too complex
for me without tests.
This commit is contained in:
Vishesh Handa
2021-07-23 12:46:55 +02:00
parent 12577aa3bc
commit 9597f35a44
5 changed files with 251 additions and 105 deletions

View File

@ -1,13 +1,11 @@
import 'dart:async';
import 'dart:io' show Platform, Directory, File;
import 'dart:io' show Directory, File;
import 'package:dart_git/dart_git.dart';
import 'package:dart_git/exceptions.dart';
import 'package:function_types/function_types.dart';
import 'package:git_bindings/git_bindings.dart' as git_bindings;
import 'package:path/path.dart' as p;
import 'package:gitjournal/utils/git_desktop.dart';
import 'package:gitjournal/utils/logger.dart';
class GitTransferProgress {
@ -25,8 +23,6 @@ class GitTransferProgress {
}
var str = await File(statusFile).readAsString();
var parts = str.split(' ');
print('Str #$str#');
print('Parts $parts');
var tp = GitTransferProgress();
tp.totalObjects = int.parse(parts[0]);
@ -40,7 +36,32 @@ class GitTransferProgress {
}
}
Future<Result<void>> cloneRemote({
typedef GitFetchFunction = Future<Result<void>> Function(
String repoPath,
String remoteName,
String sshPublicKey,
String sshPrivateKey,
String sshPassword,
String statusFile,
);
typedef GitDefaultBranchFunction = Future<Result<String>> Function(
String repoPath,
String remoteName,
String sshPublicKey,
String sshPrivateKey,
String sshPassword,
);
typedef GitMergeFn = Future<Result<void>> Function(
String repoPath,
String remoteName,
String remoteBranchName,
String authorName,
String authorEmail,
);
Future<Result<void>> cloneRemotePluggable({
required String repoPath,
required String cloneUrl,
required String remoteName,
@ -50,61 +71,38 @@ Future<Result<void>> cloneRemote({
required String authorName,
required String authorEmail,
required Func1<GitTransferProgress, void> progressUpdate,
required GitFetchFunction gitFetchFn,
required GitDefaultBranchFunction defaultBranchFn,
required GitMergeFn gitMergeFn,
}) async {
// FIXME: Do not throw exceptions
var repo = await GitRepository.load(repoPath).getOrThrow();
var remote = await repo.addOrUpdateRemote(remoteName, cloneUrl).getOrThrow();
var remoteBranchName = "master";
var _gitRepo = git_bindings.GitRepo(folderPath: repoPath);
if (Platform.isAndroid || Platform.isIOS) {
var statusFile = p.join(Directory.systemTemp.path, 'gj');
var duration = const Duration(milliseconds: 10);
var timer = Timer.periodic(duration, (_) async {
var progress = await GitTransferProgress.load(statusFile);
if (progress != null) {
progressUpdate(progress);
}
});
await _gitRepo.fetch(
remote: remoteName,
publicKey: sshPublicKey,
privateKey: sshPrivateKey,
password: sshPassword,
statusFile: statusFile,
);
timer.cancel();
remoteBranchName = await _remoteDefaultBranch(
repo: repo,
libGit2Repo: _gitRepo,
remoteName: remoteName,
sshPublicKey: sshPublicKey,
sshPrivateKey: sshPrivateKey,
sshPassword: sshPassword,
);
} else if (Platform.isMacOS) {
var r = await gitFetchViaExecutable(
repoPath: repoPath,
privateKey: sshPrivateKey,
privateKeyPassword: sshPassword,
remoteName: remoteName,
);
if (r.isFailure) {
return fail(r);
var statusFile = p.join(Directory.systemTemp.path, 'gj');
var duration = const Duration(milliseconds: 50);
var timer = Timer.periodic(duration, (_) async {
var progress = await GitTransferProgress.load(statusFile);
if (progress != null) {
progressUpdate(progress);
}
});
var branchR = await gitDefaultBranchViaExecutable(
repoPath: repoPath,
privateKey: sshPrivateKey,
privateKeyPassword: sshPassword,
remoteName: remoteName,
);
if (r.isFailure) {
return fail(r);
}
remoteBranchName = branchR.getOrThrow();
var fetchR = await gitFetchFn(repoPath, remoteName, sshPublicKey,
sshPrivateKey, sshPassword, statusFile);
timer.cancel();
if (fetchR.isFailure) {
// FIXME: Give a better error?
return fail(fetchR);
}
var branchNameR = await defaultBranchFn(
repoPath, remoteName, sshPublicKey, sshPrivateKey, sshPassword);
if (branchNameR.isFailure) {
return fail(branchNameR);
}
var remoteBranchName = branchNameR.getOrThrow();
Log.i("Using remote branch: $remoteBranchName");
var branches = await repo.branches().getOrThrow();
@ -145,16 +143,10 @@ Future<Result<void>> cloneRemote({
var remoteBranchR = await repo.remoteBranch(remoteName, remoteBranchName);
if (remoteBranchR.isSuccess) {
Log.i("Merging '$remoteName/$remoteBranchName'");
if (Platform.isAndroid || Platform.isIOS) {
await _gitRepo.merge(
branch: '$remoteName/$remoteBranchName',
authorName: authorName,
authorEmail: authorEmail,
);
} else {
var repo = await GitRepository.load(repoPath).getOrThrow();
var author = GitAuthor(name: authorName, email: authorEmail);
repo.mergeCurrentTrackingBranch(author: author).throwOnError();
var r = await gitMergeFn(
repoPath, remoteName, remoteBranchName, authorName, authorEmail);
if (r.isFailure) {
return fail(r);
}
}
} else {
@ -166,16 +158,10 @@ Future<Result<void>> cloneRemote({
await repo.setUpstreamTo(remote, remoteBranchName).getOrThrow();
Log.i("Merging '$remoteName/$remoteBranchName'");
if (Platform.isAndroid || Platform.isIOS) {
await _gitRepo.merge(
branch: '$remoteName/$remoteBranchName',
authorName: authorName,
authorEmail: authorEmail,
);
} else {
var repo = await GitRepository.load(repoPath).getOrThrow();
var author = GitAuthor(name: authorName, email: authorEmail);
repo.mergeCurrentTrackingBranch(author: author).throwOnError();
var r = await gitMergeFn(
repoPath, remoteName, remoteBranchName, authorName, authorEmail);
if (r.isFailure) {
return fail(r);
}
}
}
@ -191,36 +177,6 @@ Future<Result<void>> cloneRemote({
return Result(null);
}
Future<String> _remoteDefaultBranch({
required GitRepository repo,
required git_bindings.GitRepo libGit2Repo,
required String remoteName,
required String sshPublicKey,
required String sshPrivateKey,
required String sshPassword,
}) async {
try {
var branch = await libGit2Repo.defaultBranch(
remote: remoteName,
publicKey: sshPublicKey,
privateKey: sshPrivateKey,
password: sshPassword,
);
Log.i("Got default branch: $branch");
if (branch != null && branch.isNotEmpty) {
return branch;
}
} catch (ex) {
Log.w("Could not fetch git main branch", ex: ex);
}
var remoteBranch = await repo.guessRemoteHead(remoteName);
if (remoteBranch == null) {
return 'master';
}
return remoteBranch.target!.branchName()!;
}
String folderNameFromCloneUrl(String cloneUrl) {
var name = p.basename(cloneUrl);
if (name.endsWith('.git')) {

View File

@ -0,0 +1,8 @@
import 'dart:io' show Platform;
import 'clone_gitExec.dart' as git_exec;
import 'clone_libgit2.dart' as libgit2;
final isMobileApp = Platform.isIOS || Platform.isAndroid;
var cloneRemote = isMobileApp ? libgit2.cloneRemote : git_exec.cloneRemote;

View File

@ -0,0 +1,79 @@
import 'package:dart_git/dart_git.dart';
import 'package:dart_git/utils/result.dart';
import 'package:function_types/function_types.dart';
import 'package:gitjournal/utils/git_desktop.dart';
import 'clone.dart';
Future<Result<void>> cloneRemote({
required String repoPath,
required String cloneUrl,
required String remoteName,
required String sshPublicKey,
required String sshPrivateKey,
required String sshPassword,
required String authorName,
required String authorEmail,
required Func1<GitTransferProgress, void> progressUpdate,
}) =>
cloneRemotePluggable(
repoPath: repoPath,
cloneUrl: cloneUrl,
remoteName: remoteName,
sshPublicKey: sshPublicKey,
sshPrivateKey: sshPrivateKey,
sshPassword: sshPassword,
authorName: authorName,
authorEmail: authorEmail,
progressUpdate: progressUpdate,
gitFetchFn: _fetch,
defaultBranchFn: _defaultBranch,
gitMergeFn: _merge,
);
Future<Result<void>> _fetch(
String repoPath,
String remoteName,
String sshPublicKey,
String sshPrivateKey,
String sshPassword,
String statusFile,
) {
// FIXME: Stop ignoring the statusFile
return gitFetchViaExecutable(
repoPath: repoPath,
privateKey: sshPrivateKey,
privateKeyPassword: sshPassword,
remoteName: remoteName,
);
}
Future<Result<String>> _defaultBranch(
String repoPath,
String remoteName,
String sshPublicKey,
String sshPrivateKey,
String sshPassword,
) {
return gitDefaultBranchViaExecutable(
repoPath: repoPath,
privateKey: sshPrivateKey,
privateKeyPassword: sshPassword,
remoteName: remoteName,
);
}
Future<Result<void>> _merge(
String repoPath,
String remoteName,
String remoteBranchName,
String authorName,
String authorEmail,
) {
return catchAll(() async {
var repo = await GitRepository.load(repoPath).getOrThrow();
var author = GitAuthor(name: authorName, email: authorEmail);
await repo.mergeCurrentTrackingBranch(author: author).throwOnError();
return Result(null);
});
}

View File

@ -0,0 +1,102 @@
import 'package:dart_git/utils/result.dart';
import 'package:function_types/function_types.dart';
import 'package:git_bindings/git_bindings.dart' as git_bindings;
import 'clone.dart';
Future<Result<void>> cloneRemote({
required String repoPath,
required String cloneUrl,
required String remoteName,
required String sshPublicKey,
required String sshPrivateKey,
required String sshPassword,
required String authorName,
required String authorEmail,
required Func1<GitTransferProgress, void> progressUpdate,
}) =>
cloneRemotePluggable(
repoPath: repoPath,
cloneUrl: cloneUrl,
remoteName: remoteName,
sshPublicKey: sshPublicKey,
sshPrivateKey: sshPrivateKey,
sshPassword: sshPassword,
authorName: authorName,
authorEmail: authorEmail,
progressUpdate: progressUpdate,
gitFetchFn: _fetch,
defaultBranchFn: _defaultBranch,
gitMergeFn: _merge,
);
Future<Result<void>> _fetch(
String repoPath,
String remoteName,
String sshPublicKey,
String sshPrivateKey,
String sshPassword,
String statusFile,
) async {
try {
var gitRepo = git_bindings.GitRepo(folderPath: repoPath);
await gitRepo.fetch(
remote: remoteName,
publicKey: sshPublicKey,
privateKey: sshPrivateKey,
password: sshPassword,
statusFile: statusFile,
);
} on Exception catch (e, st) {
return Result.fail(e, st);
}
return Result(null);
}
Future<Result<String>> _defaultBranch(
String repoPath,
String remoteName,
String sshPublicKey,
String sshPrivateKey,
String sshPassword,
) async {
try {
var gitRepo = git_bindings.GitRepo(folderPath: repoPath);
var branch = await gitRepo.defaultBranch(
remote: remoteName,
publicKey: sshPublicKey,
privateKey: sshPrivateKey,
password: sshPassword,
);
if (branch != null && branch.isNotEmpty) {
return Result(branch);
}
} on Exception catch (e, st) {
return Result.fail(e, st);
}
var ex = Exception("No Remote Branch found");
return Result.fail(ex);
}
Future<Result<void>> _merge(
String repoPath,
String remoteName,
String remoteBranchName,
String authorName,
String authorEmail,
) async {
try {
var gitRepo = git_bindings.GitRepo(folderPath: repoPath);
await gitRepo.merge(
branch: '$remoteName/$remoteBranchName',
authorName: authorName,
authorEmail: authorEmail,
);
} on Exception catch (e, st) {
return Result.fail(e, st);
}
return Result(null);
}

View File

@ -20,6 +20,7 @@ import 'package:gitjournal/settings/settings.dart';
import 'package:gitjournal/setup/autoconfigure.dart';
import 'package:gitjournal/setup/button.dart';
import 'package:gitjournal/setup/clone.dart';
import 'package:gitjournal/setup/clone_auto_select.dart';
import 'package:gitjournal/setup/clone_url.dart';
import 'package:gitjournal/setup/loading_error.dart';
import 'package:gitjournal/setup/repo_selector.dart';
@ -566,7 +567,7 @@ class GitHostSetupScreenState extends State<GitHostSetupScreen> {
_cloneProgress = p;
});
},
);
).throwOnError();
} on Exception catch (e, stacktrace) {
Log.e("Failed to clone", ex: e, stacktrace: stacktrace);
error = e.toString();