diff --git a/lib/libgit2dart.dart b/lib/libgit2dart.dart index c7db22b..b73111d 100644 --- a/lib/libgit2dart.dart +++ b/lib/libgit2dart.dart @@ -19,5 +19,6 @@ export 'src/patch.dart'; export 'src/stash.dart'; export 'src/remote.dart'; export 'src/refspec.dart'; +export 'src/callbacks.dart'; export 'src/error.dart'; export 'src/git_types.dart'; diff --git a/lib/src/bindings/remote.dart b/lib/src/bindings/remote.dart index aeb1623..0f861cc 100644 --- a/lib/src/bindings/remote.dart +++ b/lib/src/bindings/remote.dart @@ -1,9 +1,11 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; +import '../callbacks.dart'; import '../error.dart'; import '../oid.dart'; -import 'libgit2_bindings.dart'; import '../util.dart'; +import 'libgit2_bindings.dart'; +import 'remote_callbacks.dart'; /// Get a list of the configured remotes for a repo. /// @@ -297,13 +299,19 @@ void connect( Pointer remote, int direction, String? proxyOption, + Callbacks callbacks, ) { - final callbacks = calloc(); + final callbacksOptions = calloc(); final callbacksError = libgit2.git_remote_init_callbacks( - callbacks, + callbacksOptions, GIT_REMOTE_CALLBACKS_VERSION, ); + RemoteCallbacks.plug( + callbacksOptions: callbacksOptions.ref, + callbacks: callbacks, + ); + if (callbacksError < 0) { throw LibGit2Error(libgit2.git_error_last()); } @@ -313,13 +321,14 @@ void connect( final error = libgit2.git_remote_connect( remote, direction, - callbacks, + callbacksOptions, proxyOptions, nullptr, ); - calloc.free(callbacks); + calloc.free(callbacksOptions); calloc.free(proxyOptions); + RemoteCallbacks.reset(); if (error < 0) { throw LibGit2Error(libgit2.git_error_last()); @@ -383,6 +392,7 @@ void fetch( String? reflogMessage, int prune, String? proxyOption, + Callbacks callbacks, ) { var refspecsC = calloc(); final refspecsPointers = @@ -395,6 +405,7 @@ void fetch( refspecsC.ref.count = refspecs.length; refspecsC.ref.strings = strArray; + final reflogMessageC = reflogMessage?.toNativeUtf8().cast() ?? nullptr; final proxyOptions = _proxyOptionsInit(proxyOption); @@ -408,11 +419,13 @@ void fetch( throw LibGit2Error(libgit2.git_error_last()); } + RemoteCallbacks.plug( + callbacksOptions: opts.ref.callbacks, + callbacks: callbacks, + ); opts.ref.prune = prune; opts.ref.proxy_opts = proxyOptions.ref; - final reflogMessageC = reflogMessage?.toNativeUtf8().cast() ?? nullptr; - final error = libgit2.git_remote_fetch( remote, refspecsC, @@ -428,6 +441,7 @@ void fetch( calloc.free(proxyOptions); calloc.free(reflogMessageC); calloc.free(opts); + RemoteCallbacks.reset(); if (error < 0) { throw LibGit2Error(libgit2.git_error_last()); @@ -441,6 +455,7 @@ void push( Pointer remote, List refspecs, String? proxyOption, + Callbacks callbacks, ) { var refspecsC = calloc(); final refspecsPointers = @@ -464,6 +479,10 @@ void push( throw LibGit2Error(libgit2.git_error_last()); } + RemoteCallbacks.plug( + callbacksOptions: opts.ref.callbacks, + callbacks: callbacks, + ); opts.ref.proxy_opts = proxyOptions.ref; final error = libgit2.git_remote_push(remote, refspecsC, opts); @@ -475,6 +494,7 @@ void push( calloc.free(refspecsC); calloc.free(proxyOptions); calloc.free(opts); + RemoteCallbacks.reset(); if (error < 0) { throw LibGit2Error(libgit2.git_error_last()); @@ -499,10 +519,13 @@ void disconnect(Pointer remote) { /// Prune tracking refs that are no longer present on remote. /// /// Throws a [LibGit2Error] if error occured. -void prune(Pointer remote) { - final callbacks = calloc(); +void prune( + Pointer remote, + Callbacks callbacks, +) { + final callbacksOptions = calloc(); final callbacksError = libgit2.git_remote_init_callbacks( - callbacks, + callbacksOptions, GIT_REMOTE_CALLBACKS_VERSION, ); @@ -510,9 +533,15 @@ void prune(Pointer remote) { throw LibGit2Error(libgit2.git_error_last()); } - final error = libgit2.git_remote_prune(remote, callbacks); + RemoteCallbacks.plug( + callbacksOptions: callbacksOptions.ref, + callbacks: callbacks, + ); - calloc.free(callbacks); + final error = libgit2.git_remote_prune(remote, callbacksOptions); + + calloc.free(callbacksOptions); + RemoteCallbacks.reset(); if (error < 0) { throw LibGit2Error(libgit2.git_error_last()); diff --git a/lib/src/bindings/remote_callbacks.dart b/lib/src/bindings/remote_callbacks.dart new file mode 100644 index 0000000..369138b --- /dev/null +++ b/lib/src/bindings/remote_callbacks.dart @@ -0,0 +1,157 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../callbacks.dart'; +import '../repository.dart'; +import 'libgit2_bindings.dart'; +import '../oid.dart'; +import '../remote.dart'; + +class RemoteCallbacks { + /// Callback function that reports transfer progress. + static void Function(TransferProgress)? transferProgress; + + /// A callback that will be regularly called with the current count of progress + /// done by the indexer during the download of new data. + static int transferProgressCb( + Pointer stats, + Pointer payload, + ) { + transferProgress!(TransferProgress(stats)); + return 0; + } + + /// Callback function that reports textual progress from the remote. + static void Function(String)? sidebandProgress; + + /// Callback for messages received by the transport. + static int sidebandProgressCb( + Pointer progressOutput, + int length, + Pointer payload, + ) { + sidebandProgress!(progressOutput.cast().toDartString(length: length)); + return 0; + } + + /// Callback function that report reference updates. + static void Function(String, Oid, Oid)? updateTips; + + /// A callback that will be called for every reference. + static int updateTipsCb( + Pointer refname, + Pointer oldOid, + Pointer newOid, + Pointer payload, + ) { + updateTips!(refname.cast().toDartString(), Oid(oldOid), Oid(newOid)); + return 0; + } + + /// Callback function used to inform of the update status from the remote. + static void Function(String, String)? pushUpdateReference; + + /// Callback called for each updated reference on push. If [message] is + /// not empty, the update was rejected by the remote server + /// and [message] contains the reason given. + static int pushUpdateReferenceCb( + Pointer refname, + Pointer message, + Pointer payload, + ) { + final messageResult = + message == nullptr ? '' : message.cast().toDartString(); + pushUpdateReference!(refname.cast().toDartString(), messageResult); + return 0; + } + + /// A function matching the `Remote Function(Repository repo, String name, String url)` signature + /// to override the remote creation and customization process during a clone operation. + static Remote Function(Repository, String, String)? remoteFunction; + + /// A callback used to create the git remote, prior to its being used to perform + /// the clone operation. + static int remoteCb( + Pointer> remote, + Pointer repo, + Pointer name, + Pointer url, + Pointer payload, + ) { + remote[0] = remoteFunction!( + Repository(repo), + name.cast().toDartString(), + url.cast().toDartString(), + ).pointer; + + return 0; + } + + /// A function matching the `Repository Function(String path, bool bare)` signature to override + /// the repository creation and customization process during a clone operation. + static Repository Function(String, bool)? repositoryFunction; + + /// A callback used to create the new repository into which to clone. + static int repositoryCb( + Pointer> repo, + Pointer path, + int bare, + Pointer payload, + ) { + repo[0] = repositoryFunction!( + path.cast().toDartString(), + bare == 1 ? true : false, + ).pointer; + + return 0; + } + + /// Plugs provided callbacks into libgit2 callbacks. + static void plug({ + required git_remote_callbacks callbacksOptions, + required Callbacks callbacks, + }) { + const except = -1; + + if (callbacks.transferProgress != null) { + transferProgress = callbacks.transferProgress; + callbacksOptions.transfer_progress = Pointer.fromFunction( + transferProgressCb, + except, + ); + } + + if (callbacks.sidebandProgress != null) { + sidebandProgress = callbacks.sidebandProgress; + callbacksOptions.sideband_progress = Pointer.fromFunction( + sidebandProgressCb, + except, + ); + } + + if (callbacks.updateTips != null) { + updateTips = callbacks.updateTips; + callbacksOptions.update_tips = Pointer.fromFunction( + updateTipsCb, + except, + ); + } + + if (callbacks.pushUpdateReference != null) { + pushUpdateReference = callbacks.pushUpdateReference; + callbacksOptions.push_update_reference = Pointer.fromFunction( + pushUpdateReferenceCb, + except, + ); + } + } + + /// Resets callback functions to their original null values. + static void reset() { + transferProgress = null; + sidebandProgress = null; + updateTips = null; + pushUpdateReference = null; + remoteFunction = null; + repositoryFunction = null; + } +} diff --git a/lib/src/bindings/repository.dart b/lib/src/bindings/repository.dart index 72caf75..36a0cb4 100644 --- a/lib/src/bindings/repository.dart +++ b/lib/src/bindings/repository.dart @@ -1,10 +1,12 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; +import '../callbacks.dart'; import '../error.dart'; import '../remote.dart'; import '../repository.dart'; import 'libgit2_bindings.dart'; import '../util.dart'; +import 'remote_callbacks.dart'; /// Attempt to open an already-existing repository at [path]. /// @@ -140,6 +142,7 @@ Pointer clone( Remote Function(Repository, String, String)? remote, Repository Function(String, bool)? repository, String? checkoutBranch, + Callbacks callbacks, ) { final out = calloc>(); final urlC = url.toNativeUtf8().cast(); @@ -163,18 +166,23 @@ Pointer clone( throw LibGit2Error(libgit2.git_error_last()); } + RemoteCallbacks.plug( + callbacksOptions: fetchOptions.ref.callbacks, + callbacks: callbacks, + ); + const except = -1; git_remote_create_cb remoteCb = nullptr; if (remote != null) { - _remoteFunction = remote; - remoteCb = Pointer.fromFunction(_remoteCb, except); + RemoteCallbacks.remoteFunction = remote; + remoteCb = Pointer.fromFunction(RemoteCallbacks.remoteCb, except); } git_repository_create_cb repositoryCb = nullptr; if (repository != null) { - _repositoryFunction = repository; - repositoryCb = Pointer.fromFunction(_repositoryCb, except); + RemoteCallbacks.repositoryFunction = repository; + repositoryCb = Pointer.fromFunction(RemoteCallbacks.repositoryCb, except); } cloneOptions.ref.bare = bare ? 1 : 0; @@ -190,8 +198,7 @@ Pointer clone( calloc.free(checkoutBranchC); calloc.free(cloneOptions); calloc.free(fetchOptions); - _remoteFunction = null; - _repositoryFunction = null; + RemoteCallbacks.reset(); if (error < 0) { throw LibGit2Error(libgit2.git_error_last()); @@ -200,47 +207,6 @@ Pointer clone( } } -/// A function matching the `Remote Function(Repository repo, String name, String url)` signature -/// to override the remote creation and customization process during a clone operation. -Remote Function(Repository, String, String)? _remoteFunction; - -/// A callback used to create the git remote, prior to its being used to perform -/// the clone operation. -int _remoteCb( - Pointer> remote, - Pointer repo, - Pointer name, - Pointer url, - Pointer payload, -) { - remote[0] = _remoteFunction!( - Repository(repo), - name.cast().toDartString(), - url.cast().toDartString(), - ).pointer; - - return 0; -} - -/// A function matching the `Repository Function(String path, bool bare)` signature to override -/// the repository creation and customization process during a clone operation. -Repository Function(String, bool)? _repositoryFunction; - -/// A callback used to create the new repository into which to clone. -int _repositoryCb( - Pointer> repo, - Pointer path, - int bare, - Pointer payload, -) { - repo[0] = _repositoryFunction!( - path.cast().toDartString(), - bare == 1 ? true : false, - ).pointer; - - return 0; -} - /// 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/callbacks.dart b/lib/src/callbacks.dart new file mode 100644 index 0000000..311f4f6 --- /dev/null +++ b/lib/src/callbacks.dart @@ -0,0 +1,25 @@ +import 'oid.dart'; +import 'remote.dart'; + +class Callbacks { + const Callbacks({ + this.transferProgress, + this.sidebandProgress, + this.updateTips, + this.pushUpdateReference, + }); + + /// Callback function that reports transfer progress. + final void Function(TransferProgress)? transferProgress; + + /// Callback function that reports textual progress from the remote. + final void Function(String)? sidebandProgress; + + /// Callback function matching the `void Function(String refname, Oid old, Oid new)` + /// that report reference updates. + final void Function(String, Oid, Oid)? updateTips; + + /// Callback function matching the `void Function(String refname, String message)` + /// used to inform of the update status from the remote. + final void Function(String, String)? pushUpdateReference; +} diff --git a/lib/src/remote.dart b/lib/src/remote.dart index ae3196a..419846a 100644 --- a/lib/src/remote.dart +++ b/lib/src/remote.dart @@ -154,8 +154,16 @@ class Remote { /// specified url. By default connection isn't done through proxy. /// /// Throws a [LibGit2Error] if error occured. - List> ls([String? proxy]) { - bindings.connect(_remotePointer, GitDirection.fetch.value, proxy); + List> ls({ + String? proxy, + Callbacks callbacks = const Callbacks(), + }) { + bindings.connect( + _remotePointer, + GitDirection.fetch.value, + proxy, + callbacks, + ); final result = bindings.lsRemotes(_remotePointer); bindings.disconnect(_remotePointer); return result; @@ -174,22 +182,40 @@ class Remote { String? reflogMessage, GitFetchPrune prune = GitFetchPrune.unspecified, String? proxy, + Callbacks callbacks = const Callbacks(), }) { - bindings.fetch(_remotePointer, refspecs, reflogMessage, prune.value, proxy); + bindings.fetch( + _remotePointer, + refspecs, + reflogMessage, + prune.value, + proxy, + callbacks, + ); return TransferProgress(bindings.stats(_remotePointer)); } /// Performs a push. /// /// Throws a [LibGit2Error] if error occured. - void push(List refspecs, [String? proxy]) { - bindings.push(_remotePointer, refspecs, proxy); + void push({ + required List refspecs, + String? proxy, + Callbacks callbacks = const Callbacks(), + }) { + bindings.push( + _remotePointer, + refspecs, + proxy, + callbacks, + ); } /// Prunes tracking refs that are no longer present on remote. /// /// Throws a [LibGit2Error] if error occured. - void prune() => bindings.prune(_remotePointer); + void prune([Callbacks callbacks = const Callbacks()]) => + bindings.prune(_remotePointer, callbacks); /// Releases memory allocated for remote object. void free() => bindings.free(_remotePointer); diff --git a/lib/src/repository.dart b/lib/src/repository.dart index fa61fcb..d820167 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -107,6 +107,7 @@ class Repository { Remote Function(Repository, String, String)? remote, Repository Function(String, bool)? repository, String? checkoutBranch, + Callbacks callbacks = const Callbacks(), }) { libgit2.git_libgit2_init(); @@ -117,6 +118,7 @@ class Repository { remote, repository, checkoutBranch, + callbacks, ); } diff --git a/test/remote_test.dart b/test/remote_test.dart index ab6164a..6f817f0 100644 --- a/test/remote_test.dart +++ b/test/remote_test.dart @@ -202,7 +202,7 @@ void main() { remote.free(); }); - test('successfully fetches data', () async { + test('successfully fetches data', () { repo.remotes.setUrl( 'libgit2', 'https://github.com/libgit2/TestGitRepository', @@ -222,7 +222,99 @@ void main() { remote.free(); }); - test('successfully pushes', () async { + test('successfully fetches data with provided transfer progress callback', + () { + repo.remotes.setUrl( + 'libgit2', + 'https://github.com/libgit2/TestGitRepository', + ); + final remote = repo.remotes['libgit2']; + + TransferProgress? callbackStats; + void tp(TransferProgress stats) => callbackStats = stats; + final callbacks = Callbacks(transferProgress: tp); + + final stats = remote.fetch(callbacks: callbacks); + + expect(stats.totalObjects == callbackStats?.totalObjects, true); + expect(stats.indexedObjects == callbackStats?.indexedObjects, true); + expect(stats.receivedObjects == callbackStats?.receivedObjects, true); + expect(stats.localObjects == callbackStats?.localObjects, true); + expect(stats.totalDeltas == callbackStats?.totalDeltas, true); + expect(stats.indexedDeltas == callbackStats?.indexedDeltas, true); + expect(stats.receivedBytes == callbackStats?.receivedBytes, true); + + remote.free(); + }); + + test('successfully fetches data with provided sideband progress callback', + () { + const sidebandMessage = """ +Enumerating objects: 69, done. +Counting objects: 100% (1/1)\rCounting objects: 100% (1/1), done. +Total 69 (delta 0), reused 1 (delta 0), pack-reused 68 +"""; + repo.remotes.setUrl( + 'libgit2', + 'https://github.com/libgit2/TestGitRepository', + ); + final remote = repo.remotes['libgit2']; + + var sidebandOutput = StringBuffer(); + void sideband(String message) { + sidebandOutput.write(message); + } + + final callbacks = Callbacks(sidebandProgress: sideband); + + remote.fetch(callbacks: callbacks); + expect(sidebandOutput.toString(), sidebandMessage); + + remote.free(); + }); + + test('successfully fetches data with provided update tips callback', () { + repo.remotes.setUrl( + 'libgit2', + 'https://github.com/libgit2/TestGitRepository', + ); + final remote = repo.remotes['libgit2']; + const tipsExpected = [ + { + 'refname': 'refs/tags/annotated_tag', + 'oldSha': '0000000000000000000000000000000000000000', + 'newSha': 'd96c4e80345534eccee5ac7b07fc7603b56124cb', + }, + { + 'refname': 'refs/tags/blob', + 'oldSha': '0000000000000000000000000000000000000000', + 'newSha': '55a1a760df4b86a02094a904dfa511deb5655905' + }, + { + 'refname': 'refs/tags/commit_tree', + 'oldSha': '0000000000000000000000000000000000000000', + 'newSha': '8f50ba15d49353813cc6e20298002c0d17b0a9ee', + }, + ]; + + var updateTipsOutput = >[]; + void updateTips(String refname, Oid oldOid, Oid newOid) { + updateTipsOutput.add({ + 'refname': refname, + 'oldSha': oldOid.sha, + 'newSha': newOid.sha, + }); + } + + final callbacks = Callbacks(updateTips: updateTips); + + remote.fetch(callbacks: callbacks); + expect(updateTipsOutput, tipsExpected); + + remote.free(); + }); + + test('successfully pushes with update reference callback', () async { final originDir = Directory('${Directory.systemTemp.path}/origin_testrepo'); @@ -238,11 +330,19 @@ void main() { repo.remotes.create(name: 'local', url: originDir.path); final remote = repo.remotes['local']; - remote.push(['refs/heads/master']); + var updateRefOutput = {}; + void updateRef(String refname, String message) { + updateRefOutput[refname] = message; + } + + final callbacks = Callbacks(pushUpdateReference: updateRef); + + remote.push(refspecs: ['refs/heads/master'], callbacks: callbacks); expect( (originRepo[originRepo.head.target.sha] as Commit).id.sha, '821ed6e80627b8769d170a293862f9fc60825226', ); + expect(updateRefOutput, {'refs/heads/master': ''}); remote.free(); originRepo.free();