From 9597f35a440ab7267c7519e8ac44f26feade91c6 Mon Sep 17 00:00:00 2001 From: Vishesh Handa Date: Fri, 23 Jul 2021 12:46:55 +0200 Subject: [PATCH] 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. --- lib/setup/clone.dart | 164 +++++++++++-------------------- lib/setup/clone_auto_select.dart | 8 ++ lib/setup/clone_gitExec.dart | 79 +++++++++++++++ lib/setup/clone_libgit2.dart | 102 +++++++++++++++++++ lib/setup/screens.dart | 3 +- 5 files changed, 251 insertions(+), 105 deletions(-) create mode 100644 lib/setup/clone_auto_select.dart create mode 100644 lib/setup/clone_gitExec.dart create mode 100644 lib/setup/clone_libgit2.dart diff --git a/lib/setup/clone.dart b/lib/setup/clone.dart index aead8a6a..1530e40c 100644 --- a/lib/setup/clone.dart +++ b/lib/setup/clone.dart @@ -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> cloneRemote({ +typedef GitFetchFunction = Future> Function( + String repoPath, + String remoteName, + String sshPublicKey, + String sshPrivateKey, + String sshPassword, + String statusFile, +); + +typedef GitDefaultBranchFunction = Future> Function( + String repoPath, + String remoteName, + String sshPublicKey, + String sshPrivateKey, + String sshPassword, +); + +typedef GitMergeFn = Future> Function( + String repoPath, + String remoteName, + String remoteBranchName, + String authorName, + String authorEmail, +); + +Future> cloneRemotePluggable({ required String repoPath, required String cloneUrl, required String remoteName, @@ -50,61 +71,38 @@ Future> cloneRemote({ required String authorName, required String authorEmail, required Func1 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> 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> 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> cloneRemote({ return Result(null); } -Future _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')) { diff --git a/lib/setup/clone_auto_select.dart b/lib/setup/clone_auto_select.dart new file mode 100644 index 00000000..70677f54 --- /dev/null +++ b/lib/setup/clone_auto_select.dart @@ -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; diff --git a/lib/setup/clone_gitExec.dart b/lib/setup/clone_gitExec.dart new file mode 100644 index 00000000..cafde597 --- /dev/null +++ b/lib/setup/clone_gitExec.dart @@ -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> 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 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> _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> _defaultBranch( + String repoPath, + String remoteName, + String sshPublicKey, + String sshPrivateKey, + String sshPassword, +) { + return gitDefaultBranchViaExecutable( + repoPath: repoPath, + privateKey: sshPrivateKey, + privateKeyPassword: sshPassword, + remoteName: remoteName, + ); +} + +Future> _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); + }); +} diff --git a/lib/setup/clone_libgit2.dart b/lib/setup/clone_libgit2.dart new file mode 100644 index 00000000..102b4e51 --- /dev/null +++ b/lib/setup/clone_libgit2.dart @@ -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> 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 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> _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> _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> _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); +} diff --git a/lib/setup/screens.dart b/lib/setup/screens.dart index 763403e6..61a42ec0 100644 --- a/lib/setup/screens.dart +++ b/lib/setup/screens.dart @@ -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 { _cloneProgress = p; }); }, - ); + ).throwOnError(); } on Exception catch (e, stacktrace) { Log.e("Failed to clone", ex: e, stacktrace: stacktrace); error = e.toString();