feat(remote): add ability to pass callbacks as arguments

This commit is contained in:
Aleksey Kulikov 2021-09-28 17:17:03 +03:00
parent b15b56f0ae
commit 299d1a17e7
8 changed files with 374 additions and 68 deletions

View file

@ -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';

View file

@ -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<git_remote> remote,
int direction,
String? proxyOption,
Callbacks callbacks,
) {
final callbacks = calloc<git_remote_callbacks>();
final callbacksOptions = calloc<git_remote_callbacks>();
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<git_strarray>();
final refspecsPointers =
@ -395,6 +405,7 @@ void fetch(
refspecsC.ref.count = refspecs.length;
refspecsC.ref.strings = strArray;
final reflogMessageC = reflogMessage?.toNativeUtf8().cast<Int8>() ?? 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<Int8>() ?? 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<git_remote> remote,
List<String> refspecs,
String? proxyOption,
Callbacks callbacks,
) {
var refspecsC = calloc<git_strarray>();
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<git_remote> remote) {
/// Prune tracking refs that are no longer present on remote.
///
/// Throws a [LibGit2Error] if error occured.
void prune(Pointer<git_remote> remote) {
final callbacks = calloc<git_remote_callbacks>();
void prune(
Pointer<git_remote> remote,
Callbacks callbacks,
) {
final callbacksOptions = calloc<git_remote_callbacks>();
final callbacksError = libgit2.git_remote_init_callbacks(
callbacks,
callbacksOptions,
GIT_REMOTE_CALLBACKS_VERSION,
);
@ -510,9 +533,15 @@ void prune(Pointer<git_remote> 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());

View file

@ -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<git_indexer_progress> stats,
Pointer<Void> 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<Int8> progressOutput,
int length,
Pointer<Void> payload,
) {
sidebandProgress!(progressOutput.cast<Utf8>().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<Int8> refname,
Pointer<git_oid> oldOid,
Pointer<git_oid> newOid,
Pointer<Void> payload,
) {
updateTips!(refname.cast<Utf8>().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<Int8> refname,
Pointer<Int8> message,
Pointer<Void> payload,
) {
final messageResult =
message == nullptr ? '' : message.cast<Utf8>().toDartString();
pushUpdateReference!(refname.cast<Utf8>().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<Pointer<git_remote>> remote,
Pointer<git_repository> repo,
Pointer<Int8> name,
Pointer<Int8> url,
Pointer<Void> payload,
) {
remote[0] = remoteFunction!(
Repository(repo),
name.cast<Utf8>().toDartString(),
url.cast<Utf8>().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<Pointer<git_repository>> repo,
Pointer<Int8> path,
int bare,
Pointer<Void> payload,
) {
repo[0] = repositoryFunction!(
path.cast<Utf8>().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;
}
}

View file

@ -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<git_repository> clone(
Remote Function(Repository, String, String)? remote,
Repository Function(String, bool)? repository,
String? checkoutBranch,
Callbacks callbacks,
) {
final out = calloc<Pointer<git_repository>>();
final urlC = url.toNativeUtf8().cast<Int8>();
@ -163,18 +166,23 @@ Pointer<git_repository> 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<git_repository> 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<git_repository> 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<Pointer<git_remote>> remote,
Pointer<git_repository> repo,
Pointer<Int8> name,
Pointer<Int8> url,
Pointer<Void> payload,
) {
remote[0] = _remoteFunction!(
Repository(repo),
name.cast<Utf8>().toDartString(),
url.cast<Utf8>().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<Pointer<git_repository>> repo,
Pointer<Int8> path,
int bare,
Pointer<Void> payload,
) {
repo[0] = _repositoryFunction!(
path.cast<Utf8>().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<git_repository> repo) {

25
lib/src/callbacks.dart Normal file
View file

@ -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;
}

View file

@ -154,8 +154,16 @@ class Remote {
/// specified url. By default connection isn't done through proxy.
///
/// Throws a [LibGit2Error] if error occured.
List<Map<String, dynamic>> ls([String? proxy]) {
bindings.connect(_remotePointer, GitDirection.fetch.value, proxy);
List<Map<String, dynamic>> 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<String> refspecs, [String? proxy]) {
bindings.push(_remotePointer, refspecs, proxy);
void push({
required List<String> 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);

View file

@ -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,
);
}

View file

@ -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 = <Map<String, String>>[];
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 = <String, String>{};
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();