From e0e16aea30a6fbc55593db48964cb371e069b4d8 Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Wed, 22 Sep 2021 18:11:20 +0300 Subject: [PATCH] feat(remote): add bindings and api --- lib/libgit2dart.dart | 2 + lib/src/bindings/refspec.dart | 87 ++++++ lib/src/bindings/remote.dart | 549 ++++++++++++++++++++++++++++++++++ lib/src/git_types.dart | 41 +++ lib/src/refspec.dart | 52 ++++ lib/src/remote.dart | 226 ++++++++++++++ lib/src/repository.dart | 3 + test/remote_test.dart | 257 ++++++++++++++++ test/stash_test.dart | 2 +- 9 files changed, 1218 insertions(+), 1 deletion(-) create mode 100644 lib/src/bindings/refspec.dart create mode 100644 lib/src/bindings/remote.dart create mode 100644 lib/src/refspec.dart create mode 100644 lib/src/remote.dart create mode 100644 test/remote_test.dart diff --git a/lib/libgit2dart.dart b/lib/libgit2dart.dart index 2df0fa4..c7db22b 100644 --- a/lib/libgit2dart.dart +++ b/lib/libgit2dart.dart @@ -17,5 +17,7 @@ export 'src/worktree.dart'; export 'src/diff.dart'; export 'src/patch.dart'; export 'src/stash.dart'; +export 'src/remote.dart'; +export 'src/refspec.dart'; export 'src/error.dart'; export 'src/git_types.dart'; diff --git a/lib/src/bindings/refspec.dart b/lib/src/bindings/refspec.dart new file mode 100644 index 0000000..41a4443 --- /dev/null +++ b/lib/src/bindings/refspec.dart @@ -0,0 +1,87 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import 'libgit2_bindings.dart'; +import '../util.dart'; + +/// Get the source specifier. +String source(Pointer refspec) { + return libgit2.git_refspec_src(refspec).cast().toDartString(); +} + +/// Get the destination specifier. +String destination(Pointer refspec) { + return libgit2.git_refspec_dst(refspec).cast().toDartString(); +} + +/// Get the force update setting. +bool force(Pointer refspec) { + return libgit2.git_refspec_force(refspec) == 1 ? true : false; +} + +/// Get the refspec's string. +String string(Pointer refspec) { + return libgit2.git_refspec_string(refspec).cast().toDartString(); +} + +/// Get the refspec's direction. +int direction(Pointer refspec) => + libgit2.git_refspec_direction(refspec); + +/// Check if a refspec's source descriptor matches a reference. +bool matchesSource(Pointer refspec, String refname) { + final refnameC = refname.toNativeUtf8().cast(); + final result = libgit2.git_refspec_src_matches(refspec, refnameC); + + calloc.free(refnameC); + + return result == 1 ? true : false; +} + +/// Check if a refspec's destination descriptor matches a reference. +bool matchesDestination(Pointer refspec, String refname) { + final refnameC = refname.toNativeUtf8().cast(); + final result = libgit2.git_refspec_dst_matches(refspec, refnameC); + + calloc.free(refnameC); + + return result == 1 ? true : false; +} + +/// Transform a reference to its target following the refspec's rules. +/// +/// Throws a [LibGit2Error] if error occured. +String transform(Pointer spec, String name) { + final out = calloc(sizeOf()); + final nameC = name.toNativeUtf8().cast(); + final error = libgit2.git_refspec_transform(out, spec, nameC); + + calloc.free(nameC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + final result = out.ref.ptr.cast().toDartString(); + calloc.free(out); + return result; + } +} + +/// Transform a target reference to its source reference following the refspec's rules. +/// +/// Throws a [LibGit2Error] if error occured. +String rTransform(Pointer spec, String name) { + final out = calloc(sizeOf()); + final nameC = name.toNativeUtf8().cast(); + final error = libgit2.git_refspec_rtransform(out, spec, nameC); + + calloc.free(nameC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + final result = out.ref.ptr.cast().toDartString(); + calloc.free(out); + return result; + } +} diff --git a/lib/src/bindings/remote.dart b/lib/src/bindings/remote.dart new file mode 100644 index 0000000..49ce350 --- /dev/null +++ b/lib/src/bindings/remote.dart @@ -0,0 +1,549 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import '../oid.dart'; +import 'libgit2_bindings.dart'; +import '../util.dart'; + +/// Get a list of the configured remotes for a repo. +/// +/// Throws a [LibGit2Error] if error occured. +List list(Pointer repo) { + final out = calloc(); + final error = libgit2.git_remote_list(out, repo); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + final count = out.ref.count; + var result = []; + for (var i = 0; i < count; i++) { + result.add(out.ref.strings[i].cast().toDartString()); + } + calloc.free(out); + return result; + } +} + +/// Get the information for a particular remote. +/// +/// The name will be checked for validity. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer lookup(Pointer repo, String name) { + final out = calloc>(); + final nameC = name.toNativeUtf8().cast(); + final error = libgit2.git_remote_lookup(out, repo, nameC); + + calloc.free(nameC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Add a remote with the default fetch refspec to the repository's configuration. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer create( + Pointer repo, + String name, + String url, +) { + final out = calloc>(); + final nameC = name.toNativeUtf8().cast(); + final urlC = url.toNativeUtf8().cast(); + final error = libgit2.git_remote_create(out, repo, nameC, urlC); + + calloc.free(nameC); + calloc.free(urlC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Add a remote with the provided fetch refspec to the repository's configuration. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer createWithFetchSpec( + Pointer repo, + String name, + String url, + String fetch, +) { + final out = calloc>(); + final nameC = name.toNativeUtf8().cast(); + final urlC = url.toNativeUtf8().cast(); + final fetchC = fetch.toNativeUtf8().cast(); + final error = libgit2.git_remote_create_with_fetchspec( + out, + repo, + nameC, + urlC, + fetchC, + ); + + calloc.free(nameC); + calloc.free(urlC); + calloc.free(fetchC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Delete an existing persisted remote. +/// +/// All remote-tracking branches and configuration settings for the remote will be removed. +/// +/// Throws a [LibGit2Error] if error occured. +void delete(Pointer repo, String name) { + final nameC = name.toNativeUtf8().cast(); + final error = libgit2.git_remote_delete(repo, nameC); + + calloc.free(nameC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Give the remote a new name. +/// +/// Returns list of non-default refspecs that cannot be renamed. +/// +/// All remote-tracking branches and configuration settings for the remote are updated. +/// +/// The new name will be checked for validity. +/// +/// No loaded instances of a the remote with the old name will change their name or +/// their list of refspecs. +/// +/// Throws a [LibGit2Error] if error occured. +List rename(Pointer repo, String name, String newName) { + final out = calloc(); + final nameC = name.toNativeUtf8().cast(); + final newNameC = newName.toNativeUtf8().cast(); + final error = libgit2.git_remote_rename(out, repo, nameC, newNameC); + + calloc.free(nameC); + calloc.free(newNameC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + final count = out.ref.count; + var result = []; + for (var i = 0; i < count; i++) { + result.add(out.ref.strings[i].cast().toDartString()); + } + calloc.free(out); + return result; + } +} + +/// Set the remote's url in the configuration. +/// +/// Remote objects already in memory will not be affected. This assumes the common +/// case of a single-url remote and will otherwise return an error. +/// +/// Throws a [LibGit2Error] if error occured. +void setUrl(Pointer repo, String remote, String url) { + final remoteC = remote.toNativeUtf8().cast(); + final urlC = url.toNativeUtf8().cast(); + final error = libgit2.git_remote_set_url(repo, remoteC, urlC); + + calloc.free(remoteC); + calloc.free(urlC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Set the remote's url for pushing in the configuration. +/// +/// Remote objects already in memory will not be affected. This assumes the common +/// case of a single-url remote and will otherwise return an error. +/// +/// Throws a [LibGit2Error] if error occured. +void setPushUrl(Pointer repo, String remote, String url) { + final remoteC = remote.toNativeUtf8().cast(); + final urlC = url.toNativeUtf8().cast(); + final error = libgit2.git_remote_set_pushurl(repo, remoteC, urlC); + + calloc.free(remoteC); + calloc.free(urlC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Get the remote's repository. +Pointer owner(Pointer remote) { + return libgit2.git_remote_owner(remote); +} + +/// Get the remote's name. +String name(Pointer remote) { + final result = libgit2.git_remote_name(remote); + return result == nullptr ? '' : result.cast().toDartString(); +} + +/// Get the remote's url. +String url(Pointer remote) { + return libgit2.git_remote_url(remote).cast().toDartString(); +} + +/// Get the remote's url for pushing. +/// +/// Returns empty string if no special url for pushing is set. +String pushUrl(Pointer remote) { + final result = libgit2.git_remote_pushurl(remote); + return result == nullptr ? '' : result.cast().toDartString(); +} + +/// Get the number of refspecs for a remote. +int refspecCount(Pointer remote) => + libgit2.git_remote_refspec_count(remote); + +/// Get a refspec from the remote at provided position. +Pointer getRefspec(Pointer remote, int n) { + return libgit2.git_remote_get_refspec(remote, n); +} + +/// Get the remote's list of fetch refspecs. +List fetchRefspecs(Pointer remote) { + final out = calloc(); + libgit2.git_remote_get_fetch_refspecs(out, remote); + + var result = []; + final count = out.ref.count; + for (var i = 0; i < count; i++) { + result.add(out.ref.strings[i].cast().toDartString()); + } + calloc.free(out); + return result; +} + +/// Get the remote's list of push refspecs. +List pushRefspecs(Pointer remote) { + final out = calloc(); + libgit2.git_remote_get_push_refspecs(out, remote); + + var result = []; + final count = out.ref.count; + for (var i = 0; i < count; i++) { + result.add(out.ref.strings[i].cast().toDartString()); + } + calloc.free(out); + return result; +} + +/// Add a fetch refspec to the remote's configuration. +/// +/// Add the given refspec to the fetch list in the configuration. No loaded remote +/// instances will be affected. +/// +/// Throws a [LibGit2Error] if error occured. +void addFetch(Pointer repo, String remote, String refspec) { + final remoteC = remote.toNativeUtf8().cast(); + final refspecC = refspec.toNativeUtf8().cast(); + final error = libgit2.git_remote_add_fetch(repo, remoteC, refspecC); + + calloc.free(remoteC); + calloc.free(refspecC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Add a push refspec to the remote's configuration. +/// +/// Add the given refspec to the push list in the configuration. No loaded remote +/// instances will be affected. +/// +/// Throws a [LibGit2Error] if error occured. +void addPush(Pointer repo, String remote, String refspec) { + final remoteC = remote.toNativeUtf8().cast(); + final refspecC = refspec.toNativeUtf8().cast(); + final error = libgit2.git_remote_add_push(repo, remoteC, refspecC); + + calloc.free(remoteC); + calloc.free(refspecC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Open a connection to a remote. +/// +/// The transport is selected based on the URL. The direction argument is due to a +/// limitation of the git protocol (over TCP or SSH) which starts up a specific binary +/// which can only do the one or the other. +/// +/// Throws a [LibGit2Error] if error occured. +void connect( + Pointer remote, + int direction, + String proxyOption, +) { + final callbacks = calloc(); + final callbacksError = libgit2.git_remote_init_callbacks( + callbacks, + GIT_REMOTE_CALLBACKS_VERSION, + ); + + if (callbacksError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + final proxyOptions = _proxyOptionsInit(proxyOption); + + final error = libgit2.git_remote_connect( + remote, + direction, + callbacks, + proxyOptions, + nullptr, + ); + + calloc.free(callbacks); + calloc.free(proxyOptions); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Get the remote repository's reference advertisement list. +/// +/// Get the list of references with which the server responds to a new connection. +/// +/// The remote (or more exactly its transport) must have connected to the remote repository. +/// This list is available as soon as the connection to the remote is initiated and it +/// remains available after disconnecting. +/// +/// Throws a [LibGit2Error] if error occured. +List> lsRemotes(Pointer remote) { + final out = calloc>>(); + final size = calloc(); + final error = libgit2.git_remote_ls(out, size, remote); + + var result = >[]; + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + for (var i = 0; i < size.value; i++) { + var remote = {}; + Oid? loid; + + final bool local = out[0][i].ref.local == 1 ? true : false; + if (local) { + loid = Oid.fromRaw(out[0][i].ref.loid); + } + + remote['local'] = local; + remote['loid'] = loid; + remote['name'] = out[0][i].ref.name == nullptr + ? '' + : out[0][i].ref.name.cast().toDartString(); + remote['symref'] = out[0][i].ref.symref_target == nullptr + ? '' + : out[0][i].ref.symref_target.cast().toDartString(); + remote['oid'] = Oid.fromRaw(out[0][i].ref.oid); + + result.add(remote); + } + + return result; + } +} + +/// Download new data and update tips. +/// +/// Convenience function to connect to a remote, download the data, disconnect and +/// update the remote-tracking branches. +/// +/// Throws a [LibGit2Error] if error occured. +void fetch( + Pointer remote, + List refspecs, + String reflogMessage, + int prune, + String proxyOption, +) { + var refspecsC = calloc(); + final refspecsPointers = + refspecs.map((e) => e.toNativeUtf8().cast()).toList(); + final strArray = calloc>(refspecs.length); + + for (var i = 0; i < refspecs.length; i++) { + strArray[i] = refspecsPointers[i]; + } + + refspecsC.ref.count = refspecs.length; + refspecsC.ref.strings = strArray; + + final proxyOptions = _proxyOptionsInit(proxyOption); + + final opts = calloc(); + final optsError = libgit2.git_fetch_options_init( + opts, + GIT_FETCH_OPTIONS_VERSION, + ); + + if (optsError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + opts.ref.prune = prune; + opts.ref.proxy_opts = proxyOptions.ref; + + final reflogMessageC = reflogMessage.isEmpty + ? nullptr + : reflogMessage.toNativeUtf8().cast(); + + final error = libgit2.git_remote_fetch( + remote, + refspecsC, + opts, + reflogMessageC, + ); + + for (var p in refspecsPointers) { + calloc.free(p); + } + calloc.free(strArray); + calloc.free(refspecsC); + calloc.free(proxyOptions); + calloc.free(opts); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Perform a push. +/// +/// Throws a [LibGit2Error] if error occured. +void push( + Pointer remote, + List refspecs, + String proxyOption, +) { + var refspecsC = calloc(); + final refspecsPointers = + refspecs.map((e) => e.toNativeUtf8().cast()).toList(); + final strArray = calloc>(refspecs.length); + + for (var i = 0; i < refspecs.length; i++) { + strArray[i] = refspecsPointers[i]; + } + + refspecsC.ref.count = refspecs.length; + refspecsC.ref.strings = strArray; + + final proxyOptions = _proxyOptionsInit(proxyOption); + + final opts = calloc(); + final optsError = + libgit2.git_push_options_init(opts, GIT_PUSH_OPTIONS_VERSION); + + if (optsError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + opts.ref.proxy_opts = proxyOptions.ref; + + final error = libgit2.git_remote_push(remote, refspecsC, opts); + + for (var p in refspecsPointers) { + calloc.free(p); + } + calloc.free(strArray); + calloc.free(refspecsC); + calloc.free(proxyOptions); + calloc.free(opts); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Get the statistics structure that is filled in by the fetch operation. +Pointer stats(Pointer remote) => + libgit2.git_remote_stats(remote); + +/// Close the connection to the remote. +/// +/// Throws a [LibGit2Error] if error occured. +void disconnect(Pointer remote) { + final error = libgit2.git_remote_disconnect(remote); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Prune tracking refs that are no longer present on remote. +/// +/// Throws a [LibGit2Error] if error occured. +void prune(Pointer remote) { + final callbacks = calloc(); + final callbacksError = libgit2.git_remote_init_callbacks( + callbacks, + GIT_REMOTE_CALLBACKS_VERSION, + ); + + if (callbacksError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + final error = libgit2.git_remote_prune(remote, callbacks); + + calloc.free(callbacks); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Free the memory associated with a remote. +/// +/// This also disconnects from the remote, if the connection has not been closed +/// yet (using `disconnect()`). +void free(Pointer remote) => libgit2.git_remote_free(remote); + +/// Initializes git_proxy_options structure. +Pointer _proxyOptionsInit(String proxyOption) { + final proxyOptions = calloc(); + final proxyOptionsError = + libgit2.git_proxy_options_init(proxyOptions, GIT_PROXY_OPTIONS_VERSION); + + if (proxyOptionsError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + if (proxyOption.isEmpty) { + proxyOptions.ref.type = git_proxy_t.GIT_PROXY_NONE; + } else if (proxyOption == 'auto') { + proxyOptions.ref.type = git_proxy_t.GIT_PROXY_AUTO; + } else { + proxyOptions.ref.type = git_proxy_t.GIT_PROXY_SPECIFIED; + proxyOptions.ref.url = proxyOption.toNativeUtf8().cast(); + } + + return proxyOptions; +} diff --git a/lib/src/git_types.dart b/lib/src/git_types.dart index 52d3cbf..bc71af2 100644 --- a/lib/src/git_types.dart +++ b/lib/src/git_types.dart @@ -1174,3 +1174,44 @@ class GitStashApply { @override String toString() => 'GitStashApply.$_name'; } + +/// Direction of the connection. +class GitDirection { + const GitDirection._(this._value, this._name); + final int _value; + final String _name; + + static const fetch = GitDirection._(0, 'fetch'); + static const push = GitDirection._(1, 'push'); + + static const List values = [fetch, push]; + + int get value => _value; + + @override + String toString() => 'GitDirection.$_name'; +} + +/// Acceptable prune settings when fetching. +class GitFetchPrune { + const GitFetchPrune._(this._value, this._name); + final int _value; + final String _name; + + /// Use the setting from the configuration. + static const unspecified = GitFetchPrune._(0, 'unspecified'); + + /// Force pruning on. Removes any remote branch in the local repository + /// that does not exist in the remote + static const prune = GitFetchPrune._(1, 'prune'); + + /// Force pruning off. Keeps the remote branches. + static const noPrune = GitFetchPrune._(2, 'noPrune'); + + static const List values = [unspecified, prune, noPrune]; + + int get value => _value; + + @override + String toString() => 'GitFetchPrune.$_name'; +} diff --git a/lib/src/refspec.dart b/lib/src/refspec.dart new file mode 100644 index 0000000..54bf501 --- /dev/null +++ b/lib/src/refspec.dart @@ -0,0 +1,52 @@ +import 'dart:ffi'; +import 'package:libgit2dart/libgit2dart.dart'; + +import 'bindings/libgit2_bindings.dart'; +import 'bindings/refspec.dart' as bindings; +import 'git_types.dart'; + +class Refspec { + /// Initializes a new instance of the [Refspec] class + /// from provided pointer to refspec object in memory. + Refspec(this._refspecPointer); + + /// Pointer to memory address for allocated refspec object. + final Pointer _refspecPointer; + + /// Returns the source specifier. + String get source => bindings.source(_refspecPointer); + + /// Returns the destination specifier. + String get destination => bindings.destination(_refspecPointer); + + /// Returns the force update setting. + bool get force => bindings.force(_refspecPointer); + + /// Returns the refspec's string. + String get string => bindings.string(_refspecPointer); + + /// Returns the refspec's direction (fetch or push). + GitDirection get direction { + return bindings.direction(_refspecPointer) == 0 + ? GitDirection.fetch + : GitDirection.push; + } + + /// Checks if a refspec's source descriptor matches a reference. + bool matchesSource(String refname) => + bindings.matchesSource(_refspecPointer, refname); + + /// Checks if a refspec's destination descriptor matches a reference. + bool matchesDestination(String refname) => + bindings.matchesDestination(_refspecPointer, refname); + + /// Transforms a reference to its target following the refspec's rules. + /// + /// Throws a [LibGit2Error] if error occured. + String transform(String name) => bindings.transform(_refspecPointer, name); + + /// Transforms a target reference to its source reference following the refspec's rules. + /// + /// Throws a [LibGit2Error] if error occured. + String rTransform(String name) => bindings.rTransform(_refspecPointer, name); +} diff --git a/lib/src/remote.dart b/lib/src/remote.dart new file mode 100644 index 0000000..b0b03c6 --- /dev/null +++ b/lib/src/remote.dart @@ -0,0 +1,226 @@ +import 'dart:ffi'; +import 'package:libgit2dart/libgit2dart.dart'; + +import 'bindings/libgit2_bindings.dart'; +import 'bindings/remote.dart' as bindings; +import 'git_types.dart'; +import 'refspec.dart'; +import 'repository.dart'; + +class Remotes { + /// Initializes a new instance of the [References] class + /// from provided [Repository] object. + Remotes(Repository repo) { + _repoPointer = repo.pointer; + } + + /// Pointer to memory address for allocated repository object. + late final Pointer _repoPointer; + + /// Returns a list of the configured remotes for a repo. + /// + /// Throws a [LibGit2Error] if error occured. + List get list { + return bindings.list(_repoPointer); + } + + /// Returns number of the configured remotes for a repo. + int get length => list.length; + + /// Returns [Remote] by looking up [name] in a repository. + /// + /// The name will be checked for validity. + /// + /// Throws a [LibGit2Error] if error occured. + Remote operator [](String name) { + return Remote(bindings.lookup(_repoPointer, name)); + } + + /// Adds a remote to the repository's configuration with the default [fetch] + /// refspec if none provided . + /// + /// Throws a [LibGit2Error] if error occured. + Remote create({ + required String name, + required String url, + String fetch = '', + }) { + if (fetch.isEmpty) { + return Remote(bindings.create(_repoPointer, name, url)); + } else { + return Remote(bindings.createWithFetchSpec( + _repoPointer, + name, + url, + fetch, + )); + } + } + + /// Deletes an existing persisted remote. + /// + /// All remote-tracking branches and configuration settings for the remote will be removed. + /// + /// Throws a [LibGit2Error] if error occured. + void delete(String name) => bindings.delete(_repoPointer, name); + + /// Give the remote a new name. + /// + /// Returns list of non-default refspecs that cannot be renamed. + /// + /// All remote-tracking branches and configuration settings for the remote are updated. + /// + /// The new name will be checked for validity. + /// + /// No loaded instances of a the remote with the old name will change their name or + /// their list of refspecs. + /// + /// Throws a [LibGit2Error] if error occured. + List rename(String name, String newName) => + bindings.rename(_repoPointer, name, newName); + + /// Sets the remote's url in the configuration. + /// + /// Remote objects already in memory will not be affected. This assumes the common + /// case of a single-url remote and will otherwise return an error. + /// + /// Throws a [LibGit2Error] if error occured. + void setUrl(String remote, String url) => + bindings.setUrl(_repoPointer, remote, url); + + /// Sets the remote's url for pushing in the configuration. + /// + /// Remote objects already in memory will not be affected. This assumes the common + /// case of a single-url remote and will otherwise return an error. + /// + /// Throws a [LibGit2Error] if error occured. + void setPushUrl(String remote, String url) => + bindings.setPushUrl(_repoPointer, remote, url); + + /// Adds a fetch refspec to the remote's configuration. + /// + /// No loaded remote instances will be affected. + /// + /// Throws a [LibGit2Error] if error occured. + void addFetch(String remote, String refspec) => + bindings.addFetch(_repoPointer, remote, refspec); + + /// Adds a push refspec to the remote's configuration. + /// + /// No loaded remote instances will be affected. + /// + /// Throws a [LibGit2Error] if error occured. + void addPush(String remote, String refspec) => + bindings.addPush(_repoPointer, remote, refspec); +} + +class Remote { + /// Initializes a new instance of [Remote] class from provided pointer + /// to remote object in memory. + Remote(this._remotePointer); + + /// Pointer to memory address for allocated remote object. + late final Pointer _remotePointer; + + /// Returns the remote's name. + String get name => bindings.name(_remotePointer); + + /// Returns the remote's url. + String get url => bindings.url(_remotePointer); + + /// Returns the remote's url for pushing. + /// + /// Returns empty string if no special url for pushing is set. + String get pushUrl => bindings.pushUrl(_remotePointer); + + /// Returns the number of refspecs for a remote. + int get refspecCount => bindings.refspecCount(_remotePointer); + + /// Returns a [Refspec] object from the remote at provided position. + Refspec getRefspec(int index) => + Refspec(bindings.getRefspec(_remotePointer, index)); + + /// Returns the remote's list of fetch refspecs. + List get fetchRefspecs => bindings.fetchRefspecs(_remotePointer); + + /// Get the remote's list of push refspecs. + List get pushRefspecs => bindings.pushRefspecs(_remotePointer); + + /// Get the remote repository's reference advertisement list. + /// + /// [proxy] can be 'auto' to try to auto-detect the proxy from the git configuration or some + /// 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); + final result = bindings.lsRemotes(_remotePointer); + bindings.disconnect(_remotePointer); + return result; + } + + /// Downloads new data and updates tips. + /// + /// [proxy] can be 'auto' to try to auto-detect the proxy from the git configuration or some + /// specified url. By default connection isn't done through proxy. + /// + /// [reflogMessage] is the message to insert into the reflogs. Default is "fetch". + /// + /// Throws a [LibGit2Error] if error occured. + TransferProgress fetch({ + List refspecs = const [], + String reflogMessage = '', + GitFetchPrune prune = GitFetchPrune.unspecified, + String proxy = '', + }) { + bindings.fetch(_remotePointer, refspecs, reflogMessage, prune.value, proxy); + 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); + } + + /// Prunes tracking refs that are no longer present on remote. + /// + /// Throws a [LibGit2Error] if error occured. + void prune() => bindings.prune(_remotePointer); + + /// Releases memory allocated for remote object. + void free() => bindings.free(_remotePointer); +} + +/// Provides callers information about the progress of indexing a packfile, either +/// directly or part of a fetch or clone that downloads a packfile. +class TransferProgress { + /// Initializes a new instance of [TransferProgress] class from provided pointer + /// to transfer progress object in memory. + TransferProgress(this._transferProgressPointer); + + /// Pointer to memory address for allocated transfer progress object. + final Pointer _transferProgressPointer; + + /// Returns total number of objects to download. + int get totalObjects => _transferProgressPointer.ref.total_objects; + + /// Returns number of objects that have been indexed. + int get indexedObjects => _transferProgressPointer.ref.indexed_objects; + + /// Returns number of objects that have been downloaded. + int get receivedObjects => _transferProgressPointer.ref.received_objects; + + /// Returns number of local objects that have been used to fix the thin pack. + int get localObjects => _transferProgressPointer.ref.local_objects; + + /// Returns total number of deltas in the pack. + int get totalDeltas => _transferProgressPointer.ref.total_deltas; + + /// Returns number of deltas that have been indexed. + int get indexedDeltas => _transferProgressPointer.ref.indexed_deltas; + + /// Returns number of bytes received up to now. + int get receivedBytes => _transferProgressPointer.ref.received_bytes; +} diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 6ae61ed..6b1a9aa 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -919,4 +919,7 @@ class Repository { List get stashList { return stash_bindings.list(_repoPointer); } + + /// Returns [Remotes] object. + Remotes get remotes => Remotes(this); } diff --git a/test/remote_test.dart b/test/remote_test.dart new file mode 100644 index 0000000..7f72193 --- /dev/null +++ b/test/remote_test.dart @@ -0,0 +1,257 @@ +import 'dart:io'; +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'helpers/util.dart'; + +void main() { + late Repository repo; + final tmpDir = '${Directory.systemTemp.path}/remote_testrepo/'; + const remoteName = 'origin'; + const remoteUrl = 'git://github.com/SkinnyMind/libgit2dart.git'; + + setUp(() async { + if (await Directory(tmpDir).exists()) { + await Directory(tmpDir).delete(recursive: true); + } + await copyRepo( + from: Directory('test/assets/testrepo/'), + to: await Directory(tmpDir).create(), + ); + repo = Repository.open(tmpDir); + }); + + tearDown(() async { + repo.free(); + await Directory(tmpDir).delete(recursive: true); + }); + + group('Remote', () { + test('returns list of remotes', () { + expect(repo.remotes.list, ['origin']); + }); + + test('successfully looks up remote for provided name', () { + final remote = repo.remotes['origin']; + + expect(remote.name, remoteName); + expect(remote.url, remoteUrl); + expect(remote.pushUrl, ''); + + remote.free(); + }); + + test('throws when provided name for lookup is not found', () { + expect(() => repo.remotes['upstream'], throwsA(isA())); + }); + + test('successfully creates without fetchspec', () { + final remote = repo.remotes.create(name: 'upstream', url: remoteUrl); + + expect(repo.remotes.length, 2); + expect(remote.name, 'upstream'); + expect(remote.url, remoteUrl); + expect(remote.pushUrl, ''); + + remote.free(); + }); + + test('successfully creates with provided fetchspec', () { + const spec = '+refs/*:refs/*'; + final remote = repo.remotes.create( + name: 'upstream', + url: remoteUrl, + fetch: spec, + ); + + expect(repo.remotes.length, 2); + expect(remote.name, 'upstream'); + expect(remote.url, remoteUrl); + expect(remote.pushUrl, ''); + expect(remote.fetchRefspecs, [spec]); + + remote.free(); + }); + + test('successfully deletes', () { + final remote = repo.remotes.create(name: 'upstream', url: remoteUrl); + expect(repo.remotes.length, 2); + + repo.remotes.delete(remote.name); + expect(repo.remotes.length, 1); + + remote.free(); + }); + + test('successfully renames', () { + final remote = repo.remotes[remoteName]; + + final problems = repo.remotes.rename(remoteName, 'new'); + expect(problems, isEmpty); + expect(remote.name, isNot('new')); + + final newRemote = repo.remotes['new']; + expect(newRemote.name, 'new'); + + newRemote.free(); + remote.free(); + }); + + test('throws when renaming with invalid names', () { + expect(() => repo.remotes.rename('', ''), throwsA(isA())); + }); + + test('successfully sets url', () { + final remote = repo.remotes[remoteName]; + expect(remote.url, remoteUrl); + + const newUrl = 'git://new/url.git'; + repo.remotes.setUrl(remoteName, newUrl); + + final newRemote = repo.remotes[remoteName]; + expect(newRemote.url, newUrl); + + newRemote.free(); + remote.free(); + }); + + test('throws when trying to set invalid url name', () { + expect( + () => repo.remotes.setUrl('origin', ''), + throwsA(isA()), + ); + }); + + test('successfully sets url for pushing', () { + const newUrl = 'git://new/url.git'; + repo.remotes.setPushUrl(remoteName, newUrl); + + final remote = repo.remotes[remoteName]; + expect(remote.pushUrl, newUrl); + + remote.free(); + }); + + test('throws when trying to set invalid push url name', () { + expect( + () => repo.remotes.setPushUrl('origin', ''), + throwsA(isA()), + ); + }); + + test('returns refspec', () { + final remote = repo.remotes['origin']; + expect(remote.refspecCount, 1); + + final refspec = remote.getRefspec(0); + expect(refspec.source, 'refs/heads/*'); + expect(refspec.destination, 'refs/remotes/origin/*'); + expect(refspec.force, true); + expect(refspec.string, '+refs/heads/*:refs/remotes/origin/*'); + expect(remote.fetchRefspecs, ['+refs/heads/*:refs/remotes/origin/*']); + + expect(refspec.matchesSource('refs/heads/master'), true); + expect(refspec.matchesDestination('refs/remotes/origin/master'), true); + + expect( + refspec.transform('refs/heads/master'), + 'refs/remotes/origin/master', + ); + expect( + refspec.rTransform('refs/remotes/origin/master'), + 'refs/heads/master', + ); + + remote.free(); + }); + + test('successfully adds fetch refspec', () { + repo.remotes.addFetch('origin', '+refs/test/*:refs/test/remotes/*'); + final remote = repo.remotes['origin']; + expect(remote.fetchRefspecs.length, 2); + expect( + remote.fetchRefspecs, + [ + '+refs/heads/*:refs/remotes/origin/*', + '+refs/test/*:refs/test/remotes/*', + ], + ); + + remote.free(); + }); + + test('successfully adds push refspec', () { + repo.remotes.addPush('origin', '+refs/test/*:refs/test/remotes/*'); + final remote = repo.remotes['origin']; + expect(remote.pushRefspecs.length, 1); + expect(remote.pushRefspecs, ['+refs/test/*:refs/test/remotes/*']); + + remote.free(); + }); + + test('successfully returns remote repo\'s reference list', () { + repo.remotes.setUrl( + 'libgit2', + 'https://github.com/libgit2/TestGitRepository', + ); + final remote = repo.remotes['libgit2']; + + final refs = remote.ls(); + expect(refs.first['local'], false); + expect(refs.first['loid'], null); + expect(refs.first['name'], 'HEAD'); + expect(refs.first['symref'], 'refs/heads/master'); + expect( + (refs.first['oid'] as Oid).sha, + '49322bb17d3acc9146f98c97d078513228bbf3c0', + ); + + remote.free(); + }); + + test('successfully fetches data', () async { + repo.remotes.setUrl( + 'libgit2', + 'https://github.com/libgit2/TestGitRepository', + ); + final remote = repo.remotes['libgit2']; + + final stats = remote.fetch(); + + expect(stats.totalObjects, 69); + expect(stats.indexedObjects, 69); + expect(stats.receivedObjects, 69); + expect(stats.localObjects, 0); + expect(stats.totalDeltas, 3); + expect(stats.indexedDeltas, 3); + expect(stats.receivedBytes, 0); + + remote.free(); + }); + + test('successfully pushes', () async { + final originDir = '${Directory.systemTemp.path}/origin_testrepo/'; + + if (await Directory(originDir).exists()) { + await Directory(originDir).delete(recursive: true); + } + await copyRepo( + from: Directory('test/assets/empty_bare.git/'), + to: await Directory(originDir).create(), + ); + final originRepo = Repository.open(originDir); + + repo.remotes.create(name: 'local', url: originDir); + final remote = repo.remotes['local']; + + remote.push(['refs/heads/master']); + expect( + (originRepo[originRepo.head.target.sha] as Commit).id.sha, + '821ed6e80627b8769d170a293862f9fc60825226', + ); + + remote.free(); + originRepo.free(); + await Directory(originDir).delete(recursive: true); + }); + }); +} diff --git a/test/stash_test.dart b/test/stash_test.dart index 48656e4..79f8d2c 100644 --- a/test/stash_test.dart +++ b/test/stash_test.dart @@ -6,7 +6,7 @@ import 'helpers/util.dart'; void main() { late Repository repo; late Signature stasher; - final tmpDir = '${Directory.systemTemp.path}/patch_testrepo/'; + final tmpDir = '${Directory.systemTemp.path}/stash_testrepo/'; setUp(() async { if (await Directory(tmpDir).exists()) {