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: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/dart_git.dart';
import 'package:dart_git/exceptions.dart'; import 'package:dart_git/exceptions.dart';
import 'package:function_types/function_types.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:path/path.dart' as p;
import 'package:gitjournal/utils/git_desktop.dart';
import 'package:gitjournal/utils/logger.dart'; import 'package:gitjournal/utils/logger.dart';
class GitTransferProgress { class GitTransferProgress {
@ -25,8 +23,6 @@ class GitTransferProgress {
} }
var str = await File(statusFile).readAsString(); var str = await File(statusFile).readAsString();
var parts = str.split(' '); var parts = str.split(' ');
print('Str #$str#');
print('Parts $parts');
var tp = GitTransferProgress(); var tp = GitTransferProgress();
tp.totalObjects = int.parse(parts[0]); 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 repoPath,
required String cloneUrl, required String cloneUrl,
required String remoteName, required String remoteName,
@ -50,61 +71,38 @@ Future<Result<void>> cloneRemote({
required String authorName, required String authorName,
required String authorEmail, required String authorEmail,
required Func1<GitTransferProgress, void> progressUpdate, required Func1<GitTransferProgress, void> progressUpdate,
required GitFetchFunction gitFetchFn,
required GitDefaultBranchFunction defaultBranchFn,
required GitMergeFn gitMergeFn,
}) async { }) async {
// FIXME: Do not throw exceptions
var repo = await GitRepository.load(repoPath).getOrThrow(); var repo = await GitRepository.load(repoPath).getOrThrow();
var remote = await repo.addOrUpdateRemote(remoteName, cloneUrl).getOrThrow(); var remote = await repo.addOrUpdateRemote(remoteName, cloneUrl).getOrThrow();
var remoteBranchName = "master"; var statusFile = p.join(Directory.systemTemp.path, 'gj');
var _gitRepo = git_bindings.GitRepo(folderPath: repoPath); var duration = const Duration(milliseconds: 50);
var timer = Timer.periodic(duration, (_) async {
if (Platform.isAndroid || Platform.isIOS) { var progress = await GitTransferProgress.load(statusFile);
var statusFile = p.join(Directory.systemTemp.path, 'gj'); if (progress != null) {
var duration = const Duration(milliseconds: 10); progressUpdate(progress);
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 branchR = await gitDefaultBranchViaExecutable( var fetchR = await gitFetchFn(repoPath, remoteName, sshPublicKey,
repoPath: repoPath, sshPrivateKey, sshPassword, statusFile);
privateKey: sshPrivateKey, timer.cancel();
privateKeyPassword: sshPassword, if (fetchR.isFailure) {
remoteName: remoteName, // FIXME: Give a better error?
); return fail(fetchR);
if (r.isFailure) {
return fail(r);
}
remoteBranchName = branchR.getOrThrow();
} }
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"); Log.i("Using remote branch: $remoteBranchName");
var branches = await repo.branches().getOrThrow(); var branches = await repo.branches().getOrThrow();
@ -145,16 +143,10 @@ Future<Result<void>> cloneRemote({
var remoteBranchR = await repo.remoteBranch(remoteName, remoteBranchName); var remoteBranchR = await repo.remoteBranch(remoteName, remoteBranchName);
if (remoteBranchR.isSuccess) { if (remoteBranchR.isSuccess) {
Log.i("Merging '$remoteName/$remoteBranchName'"); Log.i("Merging '$remoteName/$remoteBranchName'");
if (Platform.isAndroid || Platform.isIOS) { var r = await gitMergeFn(
await _gitRepo.merge( repoPath, remoteName, remoteBranchName, authorName, authorEmail);
branch: '$remoteName/$remoteBranchName', if (r.isFailure) {
authorName: authorName, return fail(r);
authorEmail: authorEmail,
);
} else {
var repo = await GitRepository.load(repoPath).getOrThrow();
var author = GitAuthor(name: authorName, email: authorEmail);
repo.mergeCurrentTrackingBranch(author: author).throwOnError();
} }
} }
} else { } else {
@ -166,16 +158,10 @@ Future<Result<void>> cloneRemote({
await repo.setUpstreamTo(remote, remoteBranchName).getOrThrow(); await repo.setUpstreamTo(remote, remoteBranchName).getOrThrow();
Log.i("Merging '$remoteName/$remoteBranchName'"); Log.i("Merging '$remoteName/$remoteBranchName'");
if (Platform.isAndroid || Platform.isIOS) { var r = await gitMergeFn(
await _gitRepo.merge( repoPath, remoteName, remoteBranchName, authorName, authorEmail);
branch: '$remoteName/$remoteBranchName', if (r.isFailure) {
authorName: authorName, return fail(r);
authorEmail: authorEmail,
);
} else {
var repo = await GitRepository.load(repoPath).getOrThrow();
var author = GitAuthor(name: authorName, email: authorEmail);
repo.mergeCurrentTrackingBranch(author: author).throwOnError();
} }
} }
} }
@ -191,36 +177,6 @@ Future<Result<void>> cloneRemote({
return Result(null); 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) { String folderNameFromCloneUrl(String cloneUrl) {
var name = p.basename(cloneUrl); var name = p.basename(cloneUrl);
if (name.endsWith('.git')) { 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/autoconfigure.dart';
import 'package:gitjournal/setup/button.dart'; import 'package:gitjournal/setup/button.dart';
import 'package:gitjournal/setup/clone.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/clone_url.dart';
import 'package:gitjournal/setup/loading_error.dart'; import 'package:gitjournal/setup/loading_error.dart';
import 'package:gitjournal/setup/repo_selector.dart'; import 'package:gitjournal/setup/repo_selector.dart';
@ -566,7 +567,7 @@ class GitHostSetupScreenState extends State<GitHostSetupScreen> {
_cloneProgress = p; _cloneProgress = p;
}); });
}, },
); ).throwOnError();
} on Exception catch (e, stacktrace) { } on Exception catch (e, stacktrace) {
Log.e("Failed to clone", ex: e, stacktrace: stacktrace); Log.e("Failed to clone", ex: e, stacktrace: stacktrace);
error = e.toString(); error = e.toString();