From 5680fd867458b4d099f7c3697e0cb1eefe599332 Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Fri, 24 Sep 2021 12:07:11 +0300 Subject: [PATCH] feat(repository): add ability to clone repository --- lib/src/bindings/repository.dart | 47 ++++++++++++++++++++++ lib/src/repository.dart | 22 ++++++++++- test/repository_clone_test.dart | 68 ++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 test/repository_clone_test.dart diff --git a/lib/src/bindings/repository.dart b/lib/src/bindings/repository.dart index a654185..2cdbe28 100644 --- a/lib/src/bindings/repository.dart +++ b/lib/src/bindings/repository.dart @@ -86,6 +86,53 @@ Pointer init(String path, bool isBare) { } } +/// Clone a remote repository. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer clone( + String url, + String localPath, + bool bare, + String checkoutBranch, +) { + final out = calloc>(); + final urlC = url.toNativeUtf8().cast(); + final localPathC = localPath.toNativeUtf8().cast(); + final cloneOptions = calloc(); + final cloneOptionsError = + libgit2.git_clone_options_init(cloneOptions, GIT_CLONE_OPTIONS_VERSION); + + if (cloneOptionsError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + final fetchOptions = calloc(); + final fetchOptionsError = + libgit2.git_fetch_options_init(fetchOptions, GIT_FETCH_OPTIONS_VERSION); + + if (fetchOptionsError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + cloneOptions.ref.bare = bare ? 1 : 0; + cloneOptions.ref.checkout_branch = checkoutBranch.isEmpty + ? nullptr + : checkoutBranch.toNativeUtf8().cast(); + cloneOptions.ref.fetch_opts = fetchOptions.ref; + + final error = libgit2.git_clone(out, urlC, localPathC, cloneOptions); + + calloc.free(urlC); + calloc.free(localPathC); + calloc.free(cloneOptions); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + /// Returns the path to the `.git` folder for normal repositories or the /// repository itself for bare repositories. String path(Pointer repo) { diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 87f476b..bad2cae 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -46,7 +46,9 @@ class Repository { _repoPointer = bindings.init(path, isBare); } - /// Initializes a new instance of the [Repository] class. + /// Initializes a new instance of the [Repository] class by opening repository + /// at provided [path]. + /// /// For a standard repository, [path] should either point to the `.git` folder /// or to the working directory. For a bare repository, [path] should directly /// point to the repository folder. @@ -60,6 +62,24 @@ class Repository { _repoPointer = bindings.open(path); } + /// Initializes a new instance of the [Repository] class by cloning a remote repository + /// at provided [url] into [localPath]. + /// + /// [checkoutBranch] is the name of the branch to checkout after the clone. Defaults + /// to using the remote's default branch. + /// + /// Throws a [LibGit2Error] if error occured. + Repository.clone({ + required String url, + required String localPath, + bool bare = false, + String checkoutBranch = '', + }) { + libgit2.git_libgit2_init(); + + _repoPointer = bindings.clone(url, localPath, bare, checkoutBranch); + } + late final Pointer _repoPointer; /// Pointer to memory address for allocated repository object. diff --git a/test/repository_clone_test.dart b/test/repository_clone_test.dart new file mode 100644 index 0000000..e2129bf --- /dev/null +++ b/test/repository_clone_test.dart @@ -0,0 +1,68 @@ +import 'dart:io'; +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'helpers/util.dart'; + +void main() { + late Repository repo; + late Directory tmpDir; + final cloneDir = Directory('${Directory.systemTemp.path}/cloned'); + + setUp(() async { + tmpDir = await setupRepo(Directory('test/assets/testrepo/')); + repo = Repository.open(tmpDir.path); + + if (await cloneDir.exists()) { + cloneDir.delete(recursive: true); + } + }); + + tearDown(() async { + repo.free(); + await tmpDir.delete(recursive: true); + cloneDir.delete(recursive: true); + }); + + group('Repository.clone', () { + test('successfully clones repository', () async { + final clonedRepo = Repository.clone( + url: tmpDir.path, + localPath: cloneDir.path, + ); + + expect(clonedRepo.isEmpty, false); + expect(clonedRepo.isBare, false); + + clonedRepo.free(); + }); + + test('successfully clones repository as bare', () async { + final clonedRepo = Repository.clone( + url: tmpDir.path, + localPath: cloneDir.path, + bare: true, + ); + + expect(clonedRepo.isEmpty, false); + expect(clonedRepo.isBare, true); + + clonedRepo.free(); + }); + + test('successfully clones repository with provided checkout branch name', + () async { + final clonedRepo = Repository.clone( + url: tmpDir.path, + localPath: cloneDir.path, + bare: true, + checkoutBranch: 'feature', + ); + + expect(clonedRepo.isEmpty, false); + expect(clonedRepo.isBare, true); + expect(clonedRepo.head.name, 'refs/heads/feature'); + + clonedRepo.free(); + }); + }); +}