From c160f995d84daf79938468cf8cc2275ccdf2718f Mon Sep 17 00:00:00 2001 From: Viktor Borisov Date: Mon, 29 Jun 2026 11:59:36 +0700 Subject: [PATCH 01/10] chore: prepare release 0.5.1 --- CHANGELOG.md | 8 ++++++++ README.md | 3 +++ pubspec.yaml | 4 ++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5392c90..c39117f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ # Changelog +## [0.5.1] - 2026-06-29 +### Fixed +* Require `git2dart_binaries` `>=1.11.2 <1.12.0` to pick up the latest + packaged libgit2 binaries and generated bindings. + +### Documentation +* Document the `git2dart_binaries` `1.11.2` dependency baseline in the README. + ## [0.5.0] - 2026-05-30 ### Features * Update `git2dart_binaries` constraint to `>=1.11.0 <1.12.0`. diff --git a/README.md b/README.md index ddad9af..a231541 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ This is a hardfork of [libgit2dart](https://github.com/SkinnyMind/libgit2dart) Currently supported platforms are 64-bit Windows, Linux, macOS, Android (arm64-v8a, x86_64), and iOS on Flutter. Desktop platforms also work on the Dart VM when native libraries are available. + +Version `0.5.1` requires `git2dart_binaries >=1.11.2 <1.12.0`, which provides +the bundled libgit2 artifacts and generated FFI bindings used by this package. ## Usage git2dart provides you ability to manage Git repository. You can read and write objects (commit, tag, tree and blob), walk a tree, access the staging area, manage config and lots more. diff --git a/pubspec.yaml b/pubspec.yaml index 09c4ed9..03f8a81 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: git2dart description: Dart bindings to libgit2, provides ability to use libgit2 library in Dart and Flutter. repository: https://github.com/DartGit-dev/git2dart -version: 0.5.0 +version: 0.5.1 environment: sdk: ">=3.7.2 <4.0.0" @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter - git2dart_binaries: ">=1.11.0 <1.12.0" + git2dart_binaries: ">=1.11.2 <1.12.0" meta: ^1.8.0 path: ^1.8.1 From b422c95705212f550288eb0dee07b10ca6881280 Mon Sep 17 00:00:00 2001 From: Viktor Borisov Date: Mon, 29 Jun 2026 12:12:07 +0700 Subject: [PATCH 02/10] fix: add certificate check callback --- CHANGELOG.md | 2 + doc/types/remote.md | 12 ++ lib/git2dart.dart | 1 + lib/src/bindings/certificate.dart | 112 +++++++++++++++ lib/src/bindings/remote_callbacks.dart | 28 ++++ lib/src/callbacks.dart | 22 +++ lib/src/certificate.dart | 184 +++++++++++++++++++++++++ test/callbacks_test.dart | 119 ++++++++++++++++ 8 files changed, 480 insertions(+) create mode 100644 lib/src/bindings/certificate.dart create mode 100644 lib/src/certificate.dart create mode 100644 test/callbacks_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index c39117f..1a6843f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ### Fixed * Require `git2dart_binaries` `>=1.11.2 <1.12.0` to pick up the latest packaged libgit2 binaries and generated bindings. +* Add a remote `certificateCheck` callback so SSH clients can supply host key + trust decisions without relying on `known_hosts` lookup. ### Documentation * Document the `git2dart_binaries` `1.11.2` dependency baseline in the README. diff --git a/doc/types/remote.md b/doc/types/remote.md index 3007024..f81be42 100644 --- a/doc/types/remote.md +++ b/doc/types/remote.md @@ -37,6 +37,18 @@ remote.push(refspecs: ['refs/heads/main']); remote.prune(); ``` +## Certificate Checks + +```dart +final callbacks = Callbacks( + certificateCheck: (certificate, host, {required valid}) { + return host == 'github.com'; + }, +); + +remote.fetch(callbacks: callbacks); +``` + Network-dependent tests are skipped in CI unless explicitly enabled. See [test/remote_test.dart](../../test/remote_test.dart). diff --git a/lib/git2dart.dart b/lib/git2dart.dart index c6126fe..0927677 100644 --- a/lib/git2dart.dart +++ b/lib/git2dart.dart @@ -7,6 +7,7 @@ export 'src/blame.dart'; export 'src/blob.dart'; export 'src/branch.dart'; export 'src/callbacks.dart'; +export 'src/certificate.dart'; export 'src/checkout.dart'; export 'src/commit.dart'; export 'src/commit_graph.dart'; diff --git a/lib/src/bindings/certificate.dart b/lib/src/bindings/certificate.dart new file mode 100644 index 0000000..55f76c4 --- /dev/null +++ b/lib/src/bindings/certificate.dart @@ -0,0 +1,112 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:git2dart_binaries/git2dart_binaries.dart'; + +/// Return certificate type as a `GIT_CERT_` value. +int type(Pointer certificatePointer) { + return certificatePointer.ref.cert_typeAsInt; +} + +/// Return certificate as SSH host key structure. +Pointer hostkey(Pointer certificatePointer) { + return certificatePointer.cast(); +} + +/// Return certificate as X.509 structure. +Pointer x509(Pointer certificatePointer) { + return certificatePointer.cast(); +} + +/// Return raw bitmask of available SSH host key fields. +int hostkeyTypeFlags(Pointer hostkeyPointer) { + return hostkeyPointer.ref.typeAsInt; +} + +/// Return whether [flag] is set in SSH host key fields. +bool hostkeyHasFlag({ + required Pointer hostkeyPointer, + required int flag, +}) { + return hostkeyTypeFlags(hostkeyPointer) & flag != 0; +} + +/// Return whether MD5 host key hash is available. +bool hostkeyHasMd5(Pointer hostkeyPointer) { + return hostkeyHasFlag( + hostkeyPointer: hostkeyPointer, + flag: git_cert_ssh_t.GIT_CERT_SSH_MD5.value, + ); +} + +/// Return whether SHA-1 host key hash is available. +bool hostkeyHasSha1(Pointer hostkeyPointer) { + return hostkeyHasFlag( + hostkeyPointer: hostkeyPointer, + flag: git_cert_ssh_t.GIT_CERT_SSH_SHA1.value, + ); +} + +/// Return whether SHA-256 host key hash is available. +bool hostkeyHasSha256(Pointer hostkeyPointer) { + return hostkeyHasFlag( + hostkeyPointer: hostkeyPointer, + flag: git_cert_ssh_t.GIT_CERT_SSH_SHA256.value, + ); +} + +/// Return whether raw SSH host key is available. +bool hostkeyHasRaw(Pointer hostkeyPointer) { + return hostkeyHasFlag( + hostkeyPointer: hostkeyPointer, + flag: git_cert_ssh_t.GIT_CERT_SSH_RAW.value, + ); +} + +/// Return MD5 host key hash. +Uint8List hostkeyMd5(Pointer hostkeyPointer) { + return _copyArray(hostkeyPointer.ref.hash_md5, 16); +} + +/// Return SHA-1 host key hash. +Uint8List hostkeySha1(Pointer hostkeyPointer) { + return _copyArray(hostkeyPointer.ref.hash_sha1, 20); +} + +/// Return SHA-256 host key hash. +Uint8List hostkeySha256(Pointer hostkeyPointer) { + return _copyArray(hostkeyPointer.ref.hash_sha256, 32); +} + +/// Return raw SSH host key type. +int hostkeyRawType(Pointer hostkeyPointer) { + return hostkeyPointer.ref.raw_typeAsInt; +} + +/// Return raw SSH host key bytes. +Uint8List hostkeyRaw(Pointer hostkeyPointer) { + final hostkey = hostkeyPointer.ref.hostkey; + final length = hostkeyPointer.ref.hostkey_len; + + if (hostkey == nullptr || length == 0) { + return Uint8List(0); + } + + return Uint8List.fromList(hostkey.cast().asTypedList(length)); +} + +/// Return raw X.509 certificate bytes. +Uint8List x509Data(Pointer certificatePointer) { + final dataPointer = certificatePointer.ref.data; + final length = certificatePointer.ref.len; + + if (dataPointer == nullptr || length == 0) { + return Uint8List(0); + } + + return Uint8List.fromList(dataPointer.cast().asTypedList(length)); +} + +Uint8List _copyArray(Array array, int length) { + return Uint8List.fromList([for (var i = 0; i < length; i++) array[i]]); +} diff --git a/lib/src/bindings/remote_callbacks.dart b/lib/src/bindings/remote_callbacks.dart index a788d27..4a910ce 100644 --- a/lib/src/bindings/remote_callbacks.dart +++ b/lib/src/bindings/remote_callbacks.dart @@ -80,6 +80,25 @@ class RemoteCallbacks { return 0; } + /// Callback function used to validate a remote certificate. + static CertificateCheck? certificateCheck; + + /// Native callback that handles remote certificate validation. + static int certificateCheckCb( + Pointer cert, + int valid, + Pointer host, + Pointer payload, + ) { + final accepted = certificateCheck!( + GitCertificate(cert), + host == nullptr ? '' : host.toDartString(), + valid: valid == 1, + ); + + return accepted ? 0 : -1; + } + /// Values used to override the remote creation and customization process /// during a clone operation. static RemoteCallback? remoteCbData; @@ -260,6 +279,14 @@ class RemoteCallbacks { ); } + if (callbacks.certificateCheck != null) { + certificateCheck = callbacks.certificateCheck; + callbacksOptions.certificate_check = Pointer.fromFunction( + certificateCheckCb, + except, + ); + } + if (callbacks.credentials != null) { credentials = callbacks.credentials; final withUser = calloc()..value = 1; @@ -278,6 +305,7 @@ class RemoteCallbacks { sidebandProgress = null; updateTips = null; pushUpdateReference = null; + certificateCheck = null; remoteCbData = null; repositoryCbData = null; credentials = null; diff --git a/lib/src/callbacks.dart b/lib/src/callbacks.dart index 3d7c3c8..d127e9a 100644 --- a/lib/src/callbacks.dart +++ b/lib/src/callbacks.dart @@ -1,11 +1,22 @@ import 'package:git2dart/git2dart.dart'; +/// Callback for validating a remote certificate. +/// +/// Return `true` to accept the certificate, or `false` to reject it. +typedef CertificateCheck = + bool Function( + GitCertificate certificate, + String host, { + required bool valid, + }); + /// A class that encapsulates various callback functions used in Git operations. /// /// This class is primarily used with [Remote] methods and [Repository.clone] /// operations to handle different aspects of Git operations: /// - Authentication /// - Progress tracking +/// - Certificate checks /// - Reference updates /// - Push status updates class Callbacks { @@ -15,12 +26,14 @@ class Callbacks { /// Git operation requirements: /// /// * [credentials] - Authentication credentials (see [Credentials] implementations) + /// * [certificateCheck] - Remote certificate trust decision /// * [transferProgress] - Progress tracking for data transfers /// * [sidebandProgress] - Textual progress updates from remote /// * [updateTips] - Reference update notifications /// * [pushUpdateReference] - Push operation status updates const Callbacks({ this.credentials, + this.certificateCheck, this.transferProgress, this.sidebandProgress, this.updateTips, @@ -36,6 +49,15 @@ class Callbacks { /// * [KeypairFromMemory] - In-memory SSH key pair authentication final Credentials? credentials; + /// Callback for validating a remote certificate. + /// + /// Return `true` to accept the certificate, or `false` to reject it. Leave + /// this unset to use libgit2's default certificate validation behavior. + /// + /// This is especially useful on platforms such as Android where SSH + /// `known_hosts` lookup may not be available. + final CertificateCheck? certificateCheck; + /// Callback for tracking data transfer progress. /// /// Provides real-time updates about the progress of data transfers diff --git a/lib/src/certificate.dart b/lib/src/certificate.dart new file mode 100644 index 0000000..4598d20 --- /dev/null +++ b/lib/src/certificate.dart @@ -0,0 +1,184 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:git2dart/src/bindings/certificate.dart' as bindings; +import 'package:git2dart_binaries/git2dart_binaries.dart'; +import 'package:meta/meta.dart'; + +/// The type of certificate presented by a remote transport. +enum GitCertificateType { + /// No certificate was provided. + none, + + /// An X.509 certificate. + x509, + + /// An SSH host key provided by libssh2. + hostkeyLibssh2, + + /// A list of certificate strings. + strarray, + + /// A certificate type unknown to this version of git2dart. + unknown, +} + +/// The raw SSH host key algorithm. +enum GitCertificateSshRawType { + /// The raw key type is unknown. + unknown, + + /// RSA host key. + rsa, + + /// DSS host key. + dss, + + /// ECDSA P-256 host key. + ecdsa256, + + /// ECDSA P-384 host key. + ecdsa384, + + /// ECDSA P-521 host key. + ecdsa521, + + /// Ed25519 host key. + ed25519, +} + +/// Certificate information provided to [Callbacks.certificateCheck]. +/// +/// The object is only valid while the certificate callback is executing. +class GitCertificate { + /// Initializes a [GitCertificate] from a libgit2 certificate pointer. + /// + /// For internal use. + @internal + const GitCertificate(this._certificatePointer); + + final Pointer _certificatePointer; + + /// The type of certificate presented by the remote. + GitCertificateType get type { + return switch (bindings.type(_certificatePointer)) { + 0 => GitCertificateType.none, + 1 => GitCertificateType.x509, + 2 => GitCertificateType.hostkeyLibssh2, + 3 => GitCertificateType.strarray, + _ => GitCertificateType.unknown, + }; + } + + /// SSH host key information, when [type] is [GitCertificateType.hostkeyLibssh2]. + GitCertificateHostkey? get hostkey { + if (type != GitCertificateType.hostkeyLibssh2) { + return null; + } + + return GitCertificateHostkey(bindings.hostkey(_certificatePointer)); + } + + /// X.509 certificate information, when [type] is [GitCertificateType.x509]. + GitCertificateX509? get x509 { + if (type != GitCertificateType.x509) { + return null; + } + + return GitCertificateX509(bindings.x509(_certificatePointer)); + } +} + +/// SSH host key information presented by a remote. +/// +/// The object is only valid while the certificate callback is executing. +class GitCertificateHostkey { + /// Initializes a [GitCertificateHostkey] from a libgit2 host key pointer. + /// + /// For internal use. + @internal + const GitCertificateHostkey(this._hostkeyPointer); + + final Pointer _hostkeyPointer; + + /// Raw bitmask of available SSH host key fields. + int get typeFlags => bindings.hostkeyTypeFlags(_hostkeyPointer); + + /// Whether [md5] is available. + bool get hasMd5 => bindings.hostkeyHasMd5(_hostkeyPointer); + + /// Whether [sha1] is available. + bool get hasSha1 => bindings.hostkeyHasSha1(_hostkeyPointer); + + /// Whether [sha256] is available. + bool get hasSha256 => bindings.hostkeyHasSha256(_hostkeyPointer); + + /// Whether [rawHostkey] and [rawType] are available. + bool get hasRawHostkey => bindings.hostkeyHasRaw(_hostkeyPointer); + + /// MD5 host key hash, or `null` when unavailable. + Uint8List? get md5 { + if (!hasMd5) { + return null; + } + + return bindings.hostkeyMd5(_hostkeyPointer); + } + + /// SHA-1 host key hash, or `null` when unavailable. + Uint8List? get sha1 { + if (!hasSha1) { + return null; + } + + return bindings.hostkeySha1(_hostkeyPointer); + } + + /// SHA-256 host key hash, or `null` when unavailable. + Uint8List? get sha256 { + if (!hasSha256) { + return null; + } + + return bindings.hostkeySha256(_hostkeyPointer); + } + + /// Raw SSH host key algorithm. + GitCertificateSshRawType get rawType { + return switch (bindings.hostkeyRawType(_hostkeyPointer)) { + 1 => GitCertificateSshRawType.rsa, + 2 => GitCertificateSshRawType.dss, + 3 => GitCertificateSshRawType.ecdsa256, + 4 => GitCertificateSshRawType.ecdsa384, + 5 => GitCertificateSshRawType.ecdsa521, + 6 => GitCertificateSshRawType.ed25519, + _ => GitCertificateSshRawType.unknown, + }; + } + + /// Raw SSH host key bytes, or `null` when unavailable. + Uint8List? get rawHostkey { + if (!hasRawHostkey) { + return null; + } + + final result = bindings.hostkeyRaw(_hostkeyPointer); + return result.isEmpty ? null : result; + } +} + +/// X.509 certificate information presented by a remote. +/// +/// The object is only valid while the certificate callback is executing. +class GitCertificateX509 { + /// Initializes a [GitCertificateX509] from a libgit2 certificate pointer. + /// + /// For internal use. + @internal + const GitCertificateX509(this._certificatePointer); + + final Pointer _certificatePointer; + + /// Raw X.509 certificate bytes. + Uint8List get data => bindings.x509Data(_certificatePointer); +} diff --git a/test/callbacks_test.dart b/test/callbacks_test.dart new file mode 100644 index 0000000..56ce53f --- /dev/null +++ b/test/callbacks_test.dart @@ -0,0 +1,119 @@ +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:git2dart/git2dart.dart'; +import 'package:git2dart_binaries/git2dart_binaries.dart'; +import 'package:test/test.dart'; + +void main() { + group('Callbacks', () { + test('initializes certificate check callback', () { + final cert = calloc(); + final callbacks = Callbacks( + certificateCheck: (certificate, host, {required valid}) { + expect(certificate, isA()); + expect(valid, isTrue); + expect(host, 'github.com'); + + return true; + }, + ); + + try { + expect( + callbacks.certificateCheck!( + GitCertificate(cert), + 'github.com', + valid: true, + ), + isTrue, + ); + } finally { + calloc.free(cert); + } + }); + + test('allows certificate check callback to reject a certificate', () { + final cert = calloc(); + final callbacks = Callbacks( + certificateCheck: (certificate, host, {required valid}) => false, + ); + + try { + expect( + callbacks.certificateCheck!( + GitCertificate(cert), + 'example.com', + valid: false, + ), + isFalse, + ); + } finally { + calloc.free(cert); + } + }); + + test('reads x509 certificate data', () { + final cert = calloc(); + final data = calloc(3); + + try { + cert.ref.parent.cert_typeAsInt = 1; + cert.ref.data = data.cast(); + cert.ref.len = 3; + data[0] = 1; + data[1] = 2; + data[2] = 3; + + final certificate = GitCertificate(cert.cast()); + + expect(certificate.type, GitCertificateType.x509); + expect(certificate.hostkey, isNull); + expect(certificate.x509!.data, [1, 2, 3]); + } finally { + calloc.free(data); + calloc.free(cert); + } + }); + + test('reads ssh hostkey hashes and raw key data', () { + final cert = calloc(); + final rawHostkey = calloc(4); + + try { + cert.ref.parent.cert_typeAsInt = 2; + cert.ref.typeAsInt = + git_cert_ssh_t.GIT_CERT_SSH_SHA256.value | + git_cert_ssh_t.GIT_CERT_SSH_RAW.value; + cert.ref.raw_typeAsInt = + git_cert_ssh_raw_type_t.GIT_CERT_SSH_RAW_TYPE_KEY_ED25519.value; + cert.ref.hostkey = rawHostkey; + cert.ref.hostkey_len = 4; + + for (var i = 0; i < 32; i++) { + cert.ref.hash_sha256[i] = i; + } + + rawHostkey[0] = 4; + rawHostkey[1] = 3; + rawHostkey[2] = 2; + rawHostkey[3] = 1; + + final certificate = GitCertificate(cert.cast()); + final hostkey = certificate.hostkey!; + + expect(certificate.type, GitCertificateType.hostkeyLibssh2); + expect(certificate.x509, isNull); + expect(hostkey.hasSha256, isTrue); + expect(hostkey.hasRawHostkey, isTrue); + expect(hostkey.hasMd5, isFalse); + expect(hostkey.rawType, GitCertificateSshRawType.ed25519); + expect(hostkey.sha256, List.generate(32, (i) => i)); + expect(hostkey.rawHostkey, [4, 3, 2, 1]); + } finally { + calloc.free(rawHostkey); + calloc.free(cert); + } + }); + }); +} From 89e94e199b5850c844ec33ca3cc827609035fc1f Mon Sep 17 00:00:00 2001 From: Viktor Borisov Date: Mon, 29 Jun 2026 16:09:29 +0700 Subject: [PATCH 03/10] feat: expand libgit2 binding coverage --- lib/src/bindings/annotated.dart | 1 + lib/src/bindings/attr.dart | 1 + lib/src/bindings/blame.dart | 35 +++ lib/src/bindings/blob.dart | 5 + lib/src/bindings/branch.dart | 12 + lib/src/bindings/certificate.dart | 1 + lib/src/bindings/checkout.dart | 1 + lib/src/bindings/commit.dart | 51 +++++ lib/src/bindings/commit_graph.dart | 1 + lib/src/bindings/config.dart | 285 ++++++++++++++++++++++- lib/src/bindings/credentials.dart | 1 + lib/src/bindings/describe.dart | 1 + lib/src/bindings/diff.dart | 304 +++++++++++++++++++++++++ lib/src/bindings/filter.dart | 1 + lib/src/bindings/graph.dart | 1 + lib/src/bindings/ignore.dart | 1 + lib/src/bindings/index.dart | 231 +++++++++++++++++++ lib/src/bindings/mailmap.dart | 1 + lib/src/bindings/merge.dart | 53 ++++- lib/src/bindings/message.dart | 1 + lib/src/bindings/note.dart | 9 + lib/src/bindings/object.dart | 5 + lib/src/bindings/odb.dart | 99 ++++++++ lib/src/bindings/oid.dart | 28 +++ lib/src/bindings/packbuilder.dart | 5 + lib/src/bindings/patch.dart | 48 +++- lib/src/bindings/pathspec.dart | 1 + lib/src/bindings/rebase.dart | 1 + lib/src/bindings/refdb.dart | 1 + lib/src/bindings/reference.dart | 279 +++++++++++++++++++++++ lib/src/bindings/reflog.dart | 1 + lib/src/bindings/refspec.dart | 1 + lib/src/bindings/remote.dart | 69 +++++- lib/src/bindings/remote_callbacks.dart | 1 + lib/src/bindings/repository.dart | 179 +++++++++++++++ lib/src/bindings/reset.dart | 1 + lib/src/bindings/revparse.dart | 1 + lib/src/bindings/revwalk.dart | 26 +++ lib/src/bindings/signature.dart | 23 ++ lib/src/bindings/stash.dart | 1 + lib/src/bindings/status.dart | 48 +++- lib/src/bindings/submodule.dart | 36 +++ lib/src/bindings/tag.dart | 101 +++++++- lib/src/bindings/tree.dart | 147 +++++++++++- lib/src/bindings/treebuilder.dart | 1 + lib/src/bindings/worktree.dart | 2 + lib/src/branch.dart | 3 + lib/src/commit.dart | 26 +++ lib/src/config.dart | 23 ++ lib/src/git_types.dart | 130 +++++++++++ lib/src/index.dart | 200 ++++++++++++++-- lib/src/libgit2.dart | 45 ++++ lib/src/mailmap.dart | 9 +- lib/src/merge.dart | 3 +- lib/src/note.dart | 6 + lib/src/odb.dart | 66 +++++- lib/src/oid.dart | 23 ++ lib/src/patch.dart | 6 + lib/src/reference.dart | 110 +++++++++ lib/src/remote.dart | 28 +++ lib/src/repository.dart | 81 +++++++ lib/src/revwalk.dart | 10 + lib/src/signature.dart | 17 ++ lib/src/submodule.dart | 30 ++- lib/src/tag.dart | 74 ++++++ lib/src/tree.dart | 111 +++++++++ test/branch_test.dart | 5 + test/commit_test.dart | 50 ++++ test/config_test.dart | 112 +++++++++ test/diff_test.dart | 86 ++++++- test/helpers/util.dart | 28 ++- test/index_test.dart | 138 ++++++++++- test/libgit2_test.dart | 38 ++++ test/merge_test.dart | 1 + test/note_test.dart | 4 + test/odb_test.dart | 40 ++++ test/oid_test.dart | 15 ++ test/patch_test.dart | 39 ++++ test/reference_test.dart | 155 +++++++++++++ test/remote_test.dart | 33 +++ test/repository_test.dart | 112 +++++++++ test/revwalk_test.dart | 10 + test/signature_test.dart | 34 +++ test/submodule_test.dart | 29 +++ test/tag_test.dart | 59 +++++ test/tree_test.dart | 87 +++++++ test/worktree_test.dart | 19 ++ 87 files changed, 4145 insertions(+), 52 deletions(-) diff --git a/lib/src/bindings/annotated.dart b/lib/src/bindings/annotated.dart index bc2327e..1e02bcc 100644 --- a/lib/src/bindings/annotated.dart +++ b/lib/src/bindings/annotated.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/attr.dart b/lib/src/bindings/attr.dart index 71830d2..d2d5df5 100644 --- a/lib/src/bindings/attr.dart +++ b/lib/src/bindings/attr.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/blame.dart b/lib/src/bindings/blame.dart index ee95d6f..aace41b 100644 --- a/lib/src/bindings/blame.dart +++ b/lib/src/bindings/blame.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -145,6 +146,11 @@ Pointer fileFromBuffer({ /// Gets the number of hunks that exist in the blame structure. int hunkCount(Pointer blame) { + return libgit2.git_blame_get_hunk_count(blame); +} + +/// Gets the number of hunks using libgit2's legacy blame API name. +int hunkCountLegacy(Pointer blame) { return libgit2.git_blame_hunkcount(blame); } @@ -156,6 +162,21 @@ int hunkCount(Pointer blame) { Pointer getHunkByline({ required Pointer blamePointer, required int lineno, +}) { + final result = libgit2.git_blame_get_hunk_byline(blamePointer, lineno); + + if (result == nullptr) { + throw Git2DartError('Line number out of bounds'); + } else { + return result; + } +} + +/// Get the hunk that contains the given line number using libgit2's legacy API +/// name. +Pointer getHunkBylineLegacy({ + required Pointer blamePointer, + required int lineno, }) { final result = libgit2.git_blame_hunk_byline(blamePointer, lineno); @@ -174,6 +195,20 @@ Pointer getHunkByline({ Pointer getHunkByIndex({ required Pointer blamePointer, required int index, +}) { + final result = libgit2.git_blame_get_hunk_byindex(blamePointer, index); + + if (result == nullptr) { + throw Git2DartError('Index out of bounds'); + } else { + return result; + } +} + +/// Get the hunk at the given index using libgit2's legacy API name. +Pointer getHunkByIndexLegacy({ + required Pointer blamePointer, + required int index, }) { final result = libgit2.git_blame_hunk_byindex(blamePointer, index); diff --git a/lib/src/bindings/blob.dart b/lib/src/bindings/blob.dart index f4a2595..f9e4319 100644 --- a/lib/src/bindings/blob.dart +++ b/lib/src/bindings/blob.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'dart:typed_data'; @@ -50,6 +51,10 @@ Pointer lookupPrefix({ /// Get the [Oid] of a blob. Pointer id(Pointer blob) => libgit2.git_blob_id(blob); +/// Get the repository that contains the blob. +Pointer owner(Pointer blob) => + libgit2.git_blob_owner(blob); + /// Determine if the blob content is most certainly binary or not. /// /// The heuristic used to guess if a file is binary is taken from core git: diff --git a/lib/src/bindings/branch.dart b/lib/src/bindings/branch.dart index 950e765..ec8a383 100644 --- a/lib/src/bindings/branch.dart +++ b/lib/src/bindings/branch.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -211,6 +212,17 @@ String getName(Pointer ref) { }); } +/// Check whether a branch name is valid. +bool nameIsValid(String name) { + return using((arena) { + final valid = arena(); + final nameC = name.toChar(arena); + final error = libgit2.git_branch_name_is_valid(valid, nameC); + checkErrorAndThrow(error); + return valid.value == 1; + }); +} + /// Find the remote name of a remote-tracking branch. /// /// This will return the name of the remote whose fetch refspec is matching the diff --git a/lib/src/bindings/certificate.dart b/lib/src/bindings/certificate.dart index 55f76c4..47cb1cb 100644 --- a/lib/src/bindings/certificate.dart +++ b/lib/src/bindings/certificate.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'dart:typed_data'; diff --git a/lib/src/bindings/checkout.dart b/lib/src/bindings/checkout.dart index a557cd7..9ff89d2 100644 --- a/lib/src/bindings/checkout.dart +++ b/lib/src/bindings/checkout.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/commit.dart b/lib/src/bindings/commit.dart index a8fec3e..ec5c39d 100644 --- a/lib/src/bindings/commit.dart +++ b/lib/src/bindings/commit.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -328,6 +329,11 @@ String message(Pointer commit) { return libgit2.git_commit_message(commit).toDartString(); } +/// Get the full raw message of a commit. +String messageRaw(Pointer commit) { + return libgit2.git_commit_message_raw(commit).toDartString(); +} + /// Get the short "summary" of the git commit message. /// /// The returned message is the summary of the commit, comprising the first @@ -372,6 +378,11 @@ String headerField({ }); } +/// Get the full raw header of a commit. +String rawHeader(Pointer commit) { + return libgit2.git_commit_raw_header(commit).toDartString(); +} + /// Get the id of a commit. Pointer id(Pointer commit) => libgit2.git_commit_id(commit); @@ -437,12 +448,52 @@ int timeOffset(Pointer commit) => Pointer committer(Pointer commit) => libgit2.git_commit_committer(commit); +/// Get the committer of a commit, using a mailmap to resolve names and emails. +/// +/// The returned signature must be freed. +Pointer committerWithMailmap({ + required Pointer commitPointer, + required Pointer mailmapPointer, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_commit_committer_with_mailmap( + out, + commitPointer, + mailmapPointer, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Get the author of a commit. /// /// The returned signature must be freed. Pointer author(Pointer commit) => libgit2.git_commit_author(commit); +/// Get the author of a commit, using a mailmap to resolve names and emails. +/// +/// The returned signature must be freed. +Pointer authorWithMailmap({ + required Pointer commitPointer, + required Pointer mailmapPointer, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_commit_author_with_mailmap( + out, + commitPointer, + mailmapPointer, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Get the id of the tree pointed to by a commit. Pointer treeOid(Pointer commit) => libgit2.git_commit_tree_id(commit); diff --git a/lib/src/bindings/commit_graph.dart b/lib/src/bindings/commit_graph.dart index 5482690..20e9f66 100644 --- a/lib/src/bindings/commit_graph.dart +++ b/lib/src/bindings/commit_graph.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/config.dart b/lib/src/bindings/config.dart index ceb7293..f395cf6 100644 --- a/lib/src/bindings/config.dart +++ b/lib/src/bindings/config.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -5,6 +6,19 @@ import 'package:git2dart/src/extensions.dart'; import 'package:git2dart/src/helpers/error_helper.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; +/// Create a new empty config instance. The returned config must be freed with +/// [free]. +Pointer init() { + return using((arena) { + final out = arena>(); + final error = libgit2.git_config_new(out); + + checkErrorAndThrow(error); + + return out.value; + }); +} + /// Create a new config instance containing a single on-disk file. The returned /// config must be freed with [free]. Pointer open(String path) { @@ -304,6 +318,24 @@ String getString({ return getStringBuf(configPointer: configPointer, name: variable); } +/// Get a string config value using libgit2's direct pointer API. +/// +/// libgit2 rejects this call for live config objects; use [getString] for +/// normal public reads. +String getStringPointer({ + required Pointer configPointer, + required String variable, +}) { + return using((arena) { + final out = arena>(); + final nameC = variable.toChar(arena); + final error = libgit2.git_config_get_string(out, configPointer, nameC); + + checkErrorAndThrow(error); + return out.value.toDartString(); + }); +} + /// Set the value of a boolean config variable in the config file with the /// highest level (usually the local one). void setBool({ @@ -335,6 +367,21 @@ void setInt({ }); } +/// Set the value of a 32-bit integer config variable in the config file with +/// the highest level (usually the local one). +void setInt32({ + required Pointer configPointer, + required String variable, + required int value, +}) { + return using((arena) { + final nameC = variable.toChar(arena); + final error = libgit2.git_config_set_int32(configPointer, nameC, value); + + checkErrorAndThrow(error); + }); +} + /// Set the value of a string config variable in the config file with the /// highest level (usually the local one). void setString({ @@ -364,6 +411,117 @@ Pointer iterator(Pointer cfg) { }); } +/// Iterate over all config variables whose name matches [regexp]. +/// +/// The returned iterator must be freed with [freeIterator]. +Pointer globIterator({ + required Pointer configPointer, + required String regexp, +}) { + return using((arena) { + final out = arena>(); + final regexpC = regexp.toChar(arena); + final error = libgit2.git_config_iterator_glob_new( + out, + configPointer, + regexpC, + ); + + checkErrorAndThrow(error); + + return out.value; + }); +} + +Map _entryToMap(Pointer entry) { + return { + 'name': entry.ref.name.toDartString(), + 'value': entry.ref.value.toDartString(), + 'includeDepth': entry.ref.include_depth, + 'level': entry.ref.level.value, + }; +} + +List> _entriesFromIterator( + Pointer iterator, +) { + return using((arena) { + final entry = arena>(); + final result = >[]; + + while (true) { + final error = libgit2.git_config_next(entry, iterator); + if (error == git_error_code.GIT_ITEROVER.value) { + break; + } + + checkErrorAndThrow(error); + result.add(_entryToMap(entry.value)); + } + + return result; + }); +} + +/// Return all config entries from a glob iterator. +List> globEntries({ + required Pointer configPointer, + required String regexp, +}) { + final iter = globIterator(configPointer: configPointer, regexp: regexp); + try { + return _entriesFromIterator(iter); + } finally { + freeIterator(iter); + } +} + +var _foreachEntries = >[]; + +int _foreachCb(Pointer entry, Pointer payload) { + _foreachEntries.add(_entryToMap(entry)); + return 0; +} + +/// Return entries by using libgit2's config foreach callback API. +List> foreachEntries(Pointer configPointer) { + final cb = Pointer.fromFunction< + Int Function(Pointer, Pointer) + >(_foreachCb, 0); + + _foreachEntries.clear(); + final error = libgit2.git_config_foreach(configPointer, cb, nullptr); + checkErrorAndThrow(error); + final result = _foreachEntries.toList(growable: false); + _foreachEntries.clear(); + return result; +} + +/// Return matching entries by using libgit2's config foreach-match API. +List> foreachMatchEntries({ + required Pointer configPointer, + required String regexp, +}) { + return using((arena) { + final cb = Pointer.fromFunction< + Int Function(Pointer, Pointer) + >(_foreachCb, 0); + final regexpC = regexp.toChar(arena); + + _foreachEntries.clear(); + final error = libgit2.git_config_foreach_match( + configPointer, + regexpC, + cb, + nullptr, + ); + checkErrorAndThrow(error); + final result = _foreachEntries.toList(growable: false); + _foreachEntries.clear(); + return result; + }); +} + /// Delete a config variable from the config file with the highest level /// (usually the local one). /// @@ -411,19 +569,136 @@ List multivarValues({ var nextError = 0; final entries = []; - while (nextError == 0) { - nextError = libgit2.git_config_next(entry, iterator.value); - if (nextError != -31) { + try { + while (nextError == 0) { + nextError = libgit2.git_config_next(entry, iterator.value); + if (nextError == git_error_code.GIT_ITEROVER.value) { + break; + } + + checkErrorAndThrow(nextError); entries.add(entry.value.ref.value.toDartString()); - } else { - break; } + } finally { + freeIterator(iterator.value); } return entries; }); } +/// Return multivar values using libgit2's foreach callback API. +List multivarValuesForeach({ + required Pointer configPointer, + required String variable, + String? regexp, +}) { + return using((arena) { + final nameC = variable.toChar(arena); + final regexpC = regexp?.toChar(arena) ?? nullptr; + final cb = Pointer.fromFunction< + Int Function(Pointer, Pointer) + >(_foreachCb, 0); + + _foreachEntries.clear(); + final error = libgit2.git_config_get_multivar_foreach( + configPointer, + nameC, + regexpC, + cb, + nullptr, + ); + + if (error == git_error_code.GIT_ENOTFOUND.value) { + return []; + } + + checkErrorAndThrow(error); + final result = _foreachEntries + .map((entry) => entry['value']! as String) + .toList(growable: false); + _foreachEntries.clear(); + return result; + }); +} + +/// Mapping specification for config value mapping helpers. +class ConfigMapSpec { + /// Creates a config mapping specification. + const ConfigMapSpec({required this.type, required this.value, this.match}); + + /// Type of value to match. + final git_configmap_t type; + + /// String value to match when [type] is [git_configmap_t.GIT_CONFIGMAP_STRING]. + final String? match; + + /// Integer value returned for this mapping. + final int value; +} + +Pointer _configMaps(Arena arena, List maps) { + final mapsC = arena(maps.length); + for (var i = 0; i < maps.length; i++) { + mapsC[i].typeAsInt = maps[i].type.value; + mapsC[i].str_match = maps[i].match?.toChar(arena) ?? nullptr; + mapsC[i].map_value = maps[i].value; + } + return mapsC; +} + +/// Return a config value mapped to an integer constant. +int getMapped({ + required Pointer configPointer, + required String name, + required List maps, +}) { + return using((arena) { + final out = arena(); + final nameC = name.toChar(arena); + final mapsC = _configMaps(arena, maps); + final error = libgit2.git_config_get_mapped( + out, + configPointer, + nameC, + mapsC, + maps.length, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + +/// Return [value] mapped to an integer constant. +int lookupMapValue({required List maps, required String value}) { + return using((arena) { + final out = arena(); + final mapsC = _configMaps(arena, maps); + final valueC = value.toChar(arena); + final error = libgit2.git_config_lookup_map_value( + out, + mapsC, + maps.length, + valueC, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + +/// Lock and immediately release the highest-priority config backend. +void lock(Pointer configPointer) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_config_lock(out, configPointer); + + checkErrorAndThrow(error); + libgit2.git_transaction_free(out.value); + }); +} + /// Free the configuration and its associated memory and files. void free(Pointer cfg) => libgit2.git_config_free(cfg); diff --git a/lib/src/bindings/credentials.dart b/lib/src/bindings/credentials.dart index 02780b8..74275ce 100644 --- a/lib/src/bindings/credentials.dart +++ b/lib/src/bindings/credentials.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/describe.dart b/lib/src/bindings/describe.dart index 6b13673..46d8989 100644 --- a/lib/src/bindings/describe.dart +++ b/lib/src/bindings/describe.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/diff.dart b/lib/src/bindings/diff.dart index c949f9b..52ee371 100644 --- a/lib/src/bindings/diff.dart +++ b/lib/src/bindings/diff.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -423,6 +424,309 @@ String addToBuf(Pointer diff) { }); } +var _printedDiffLines = []; +var _foreachFilePaths = []; +var _foreachLineOrigins = []; + +String _lineContent(Pointer line) { + if (line == nullptr || line.ref.content == nullptr) { + return ''; + } + + final content = line.ref.content.toDartString(length: line.ref.content_len); + final origin = line.ref.origin; + if (origin == git_diff_line_t.GIT_DIFF_LINE_ADDITION.value || + origin == git_diff_line_t.GIT_DIFF_LINE_DELETION.value) { + return '${String.fromCharCode(origin)}$content'; + } + if (origin == git_diff_line_t.GIT_DIFF_LINE_CONTEXT.value) { + return ' $content'; + } + + return content; +} + +int _diffPrintCb( + Pointer delta, + Pointer hunk, + Pointer line, + Pointer payload, +) { + _printedDiffLines.add(_lineContent(line)); + return 0; +} + +int _diffFileCb( + Pointer delta, + double progress, + Pointer payload, +) { + final path = + delta.ref.new_file.path == nullptr + ? delta.ref.old_file.path.toDartString() + : delta.ref.new_file.path.toDartString(); + _foreachFilePaths.add(path); + return 0; +} + +int _diffHunkCb( + Pointer delta, + Pointer hunk, + Pointer payload, +) { + return 0; +} + +int _diffLineCb( + Pointer delta, + Pointer hunk, + Pointer line, + Pointer payload, +) { + _foreachLineOrigins.add(line.ref.origin); + _printedDiffLines.add(_lineContent(line)); + return 0; +} + +git_diff_line_cb _lineCallback() { + return Pointer.fromFunction< + Int Function( + Pointer, + Pointer, + Pointer, + Pointer, + ) + >(_diffPrintCb, 0); +} + +/// Iterate over a diff and collect formatted text through the print callback. +String print(Pointer diff) { + final cb = _lineCallback(); + + _printedDiffLines.clear(); + final error = libgit2.git_diff_print( + diff, + git_diff_format_t.GIT_DIFF_FORMAT_PATCH, + cb, + nullptr, + ); + + checkErrorAndThrow(error); + final result = _printedDiffLines.join(); + _printedDiffLines.clear(); + return result; +} + +/// Iterate over a diff and return file paths and line origin codes. +Map foreach(Pointer diff) { + final fileCb = Pointer.fromFunction< + Int Function(Pointer, Float, Pointer) + >(_diffFileCb, 0); + final hunkCb = Pointer.fromFunction< + Int Function(Pointer, Pointer, Pointer) + >(_diffHunkCb, 0); + final lineCb = Pointer.fromFunction< + Int Function( + Pointer, + Pointer, + Pointer, + Pointer, + ) + >(_diffLineCb, 0); + + _foreachFilePaths.clear(); + _foreachLineOrigins.clear(); + _printedDiffLines.clear(); + + final error = libgit2.git_diff_foreach( + diff, + fileCb, + nullptr, + hunkCb, + lineCb, + nullptr, + ); + + checkErrorAndThrow(error); + + final result = { + 'paths': _foreachFilePaths.toList(growable: false), + 'origins': _foreachLineOrigins.toList(growable: false), + 'text': _printedDiffLines.join(), + }; + + _foreachFilePaths.clear(); + _foreachLineOrigins.clear(); + _printedDiffLines.clear(); + return result; +} + +String _runDirectDiff({ + required Arena arena, + Pointer? oldBlobPointer, + String? oldAsPath, + Pointer? newBlobPointer, + String? newAsPath, + String? oldBuffer, + String? newBuffer, + required int flags, + required int contextLines, + required int interhunkLines, +}) { + final oldAsPathC = oldAsPath?.toChar(arena) ?? nullptr; + final newAsPathC = newAsPath?.toChar(arena) ?? nullptr; + final oldBufferC = oldBuffer?.toChar(arena) ?? nullptr; + final newBufferC = newBuffer?.toChar(arena) ?? nullptr; + final opts = _diffOptionsInit( + arena: arena, + flags: flags, + contextLines: contextLines, + interhunkLines: interhunkLines, + ); + final fileCb = Pointer.fromFunction< + Int Function(Pointer, Float, Pointer) + >(_diffFileCb, 0); + final hunkCb = Pointer.fromFunction< + Int Function(Pointer, Pointer, Pointer) + >(_diffHunkCb, 0); + final lineCb = Pointer.fromFunction< + Int Function( + Pointer, + Pointer, + Pointer, + Pointer, + ) + >(_diffLineCb, 0); + + _foreachFilePaths.clear(); + _foreachLineOrigins.clear(); + _printedDiffLines.clear(); + + int error; + if (oldBuffer != null || oldBlobPointer == null) { + error = libgit2.git_diff_buffers( + oldBufferC.cast(), + oldBuffer?.length ?? 0, + oldAsPathC, + newBufferC.cast(), + newBuffer?.length ?? 0, + newAsPathC, + opts, + fileCb, + nullptr, + hunkCb, + lineCb, + nullptr, + ); + } else if (newBlobPointer != null) { + error = libgit2.git_diff_blobs( + oldBlobPointer, + oldAsPathC, + newBlobPointer, + newAsPathC, + opts, + fileCb, + nullptr, + hunkCb, + lineCb, + nullptr, + ); + } else { + error = libgit2.git_diff_blob_to_buffer( + oldBlobPointer, + oldAsPathC, + newBufferC, + newBuffer?.length ?? 0, + newAsPathC, + opts, + fileCb, + nullptr, + hunkCb, + lineCb, + nullptr, + ); + } + + checkErrorAndThrow(error); + final result = _printedDiffLines.join(); + _foreachFilePaths.clear(); + _foreachLineOrigins.clear(); + _printedDiffLines.clear(); + return result; +} + +/// Directly diff two buffers and return collected line text. +String buffers({ + String? oldBuffer, + String? oldAsPath, + String? newBuffer, + String? newAsPath, + required int flags, + required int contextLines, + required int interhunkLines, +}) { + return using( + (arena) => _runDirectDiff( + arena: arena, + oldBuffer: oldBuffer, + oldAsPath: oldAsPath, + newBuffer: newBuffer, + newAsPath: newAsPath, + flags: flags, + contextLines: contextLines, + interhunkLines: interhunkLines, + ), + ); +} + +/// Directly diff two blobs and return collected line text. +String blobs({ + required Pointer? oldBlobPointer, + String? oldAsPath, + required Pointer? newBlobPointer, + String? newAsPath, + required int flags, + required int contextLines, + required int interhunkLines, +}) { + return using( + (arena) => _runDirectDiff( + arena: arena, + oldBlobPointer: oldBlobPointer, + oldAsPath: oldAsPath, + newBlobPointer: newBlobPointer, + newAsPath: newAsPath, + flags: flags, + contextLines: contextLines, + interhunkLines: interhunkLines, + ), + ); +} + +/// Directly diff a blob and a buffer and return collected line text. +String blobToBuffer({ + Pointer? oldBlobPointer, + String? oldAsPath, + String? buffer, + String? bufferAsPath, + required int flags, + required int contextLines, + required int interhunkLines, +}) { + return using( + (arena) => _runDirectDiff( + arena: arena, + oldBlobPointer: oldBlobPointer, + oldAsPath: oldAsPath, + newBuffer: buffer, + newAsPath: bufferAsPath, + flags: flags, + contextLines: contextLines, + interhunkLines: interhunkLines, + ), + ); +} + /// Counter for hunk number being applied. /// /// **IMPORTANT**: make sure to reset it to 0 before using since it's a global diff --git a/lib/src/bindings/filter.dart b/lib/src/bindings/filter.dart index a29f931..ec03d0a 100644 --- a/lib/src/bindings/filter.dart +++ b/lib/src/bindings/filter.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/graph.dart b/lib/src/bindings/graph.dart index 399ca49..27b86d4 100644 --- a/lib/src/bindings/graph.dart +++ b/lib/src/bindings/graph.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/ignore.dart b/lib/src/bindings/ignore.dart index 7c4328c..18dab13 100644 --- a/lib/src/bindings/ignore.dart +++ b/lib/src/bindings/ignore.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/index.dart b/lib/src/bindings/index.dart index cd5edb9..a511030 100644 --- a/lib/src/bindings/index.dart +++ b/lib/src/bindings/index.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -511,11 +512,187 @@ List>> conflictList( }); } +/// Return the conflict entries for [path]. +/// +/// Throws a [LibGit2Error] if error occurred. +Map> conflictGet({ + required Pointer indexPointer, + required String path, +}) { + return using((arena) { + final ancestorOut = arena>(); + final ourOut = arena>(); + final theirOut = arena>(); + final pathC = path.toChar(arena); + + final error = libgit2.git_index_conflict_get( + ancestorOut, + ourOut, + theirOut, + indexPointer, + pathC, + ); + + checkErrorAndThrow(error); + + return { + 'ancestor': ancestorOut.value, + 'our': ourOut.value, + 'their': theirOut.value, + }; + }); +} + /// Return whether the given index entry is a conflict (has a high stage entry). /// This is simply shorthand for [entryStage] > 0. bool entryIsConflict(Pointer entry) => libgit2.git_index_entry_is_conflict(entry) == 1 || false; +/// Get the count of filename conflict entries currently in the index. +int nameEntryCount(Pointer index) => + libgit2.git_index_name_entrycount(index); + +/// Get a filename conflict entry from the index by position. +/// +/// The returned entry is owned by the index and must not be freed. +Pointer nameGetByIndex({ + required Pointer indexPointer, + required int position, +}) { + final result = libgit2.git_index_name_get_byindex(indexPointer, position); + + if (result == nullptr) { + throw Git2DartError('Out of bounds'); + } else { + return result; + } +} + +/// Record the filenames involved in a rename conflict. +void nameAdd({ + required Pointer indexPointer, + required String ancestor, + required String ours, + required String theirs, +}) { + using((arena) { + final ancestorC = ancestor.toChar(arena); + final oursC = ours.toChar(arena); + final theirsC = theirs.toChar(arena); + final error = libgit2.git_index_name_add( + indexPointer, + ancestorC, + oursC, + theirsC, + ); + + checkErrorAndThrow(error); + }); +} + +/// Remove all filename conflict entries. +void nameClear(Pointer index) { + final error = libgit2.git_index_name_clear(index); + + checkErrorAndThrow(error); +} + +/// Get the count of resolve undo entries currently in the index. +int reucEntryCount(Pointer index) => + libgit2.git_index_reuc_entrycount(index); + +/// Find the position of a resolve undo entry by path. +int reucFind({required Pointer indexPointer, required String path}) { + return using((arena) { + final out = arena(); + final pathC = path.toChar(arena); + final error = libgit2.git_index_reuc_find(out, indexPointer, pathC); + + checkErrorAndThrow(error); + return out.value; + }); +} + +/// Get a resolve undo entry by path. +/// +/// The returned entry is owned by the index and must not be freed. +Pointer reucGetByPath({ + required Pointer indexPointer, + required String path, +}) { + return using((arena) { + final pathC = path.toChar(arena); + final result = libgit2.git_index_reuc_get_bypath(indexPointer, pathC); + + if (result == nullptr) { + throw ArgumentError.value('$path was not found'); + } else { + return result; + } + }); +} + +/// Get a resolve undo entry by position. +/// +/// The returned entry is owned by the index and must not be freed. +Pointer reucGetByIndex({ + required Pointer indexPointer, + required int position, +}) { + final result = libgit2.git_index_reuc_get_byindex(indexPointer, position); + + if (result == nullptr) { + throw Git2DartError('Out of bounds'); + } else { + return result; + } +} + +/// Add a resolve undo entry for a file. +void reucAdd({ + required Pointer indexPointer, + required String path, + required int ancestorMode, + required Pointer ancestorOid, + required int ourMode, + required Pointer ourOid, + required int theirMode, + required Pointer theirOid, +}) { + using((arena) { + final pathC = path.toChar(arena); + final error = libgit2.git_index_reuc_add( + indexPointer, + pathC, + ancestorMode, + ancestorOid, + ourMode, + ourOid, + theirMode, + theirOid, + ); + + checkErrorAndThrow(error); + }); +} + +/// Remove a resolve undo entry by position. +void reucRemove({ + required Pointer indexPointer, + required int position, +}) { + final error = libgit2.git_index_reuc_remove(indexPointer, position); + + checkErrorAndThrow(error); +} + +/// Remove all resolve undo entries. +void reucClear(Pointer index) { + final error = libgit2.git_index_reuc_clear(index); + + checkErrorAndThrow(error); +} + /// Add or update index entries to represent a conflict. Any staged entries /// that exist at the given paths will be removed. /// @@ -565,6 +742,60 @@ void conflictCleanup(Pointer index) { checkErrorAndThrow(error); } +/// Create a new index iterator. +/// +/// The returned iterator must be freed with [iteratorFree]. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer iteratorNew(Pointer index) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_index_iterator_new(out, index); + + checkErrorAndThrow(error); + + return out.value; + }); +} + +/// Return the next entry from an index iterator, or null when iteration ends. +Pointer? iteratorNext(Pointer iterator) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_index_iterator_next(out, iterator); + + if (error == git_error_code.GIT_ITEROVER.value) { + return null; + } + + checkErrorAndThrow(error); + return out.value; + }); +} + +/// Free an index iterator. +void iteratorFree(Pointer iterator) { + libgit2.git_index_iterator_free(iterator); +} + +/// Return an ordered snapshot of index entry paths using the native iterator. +List iteratorPaths(Pointer index) { + final iterator = iteratorNew(index); + final paths = []; + + try { + var entry = iteratorNext(iterator); + while (entry != null) { + paths.add(entry.ref.path.toDartString()); + entry = iteratorNext(iterator); + } + } finally { + iteratorFree(iterator); + } + + return paths; +} + /// Free an existing index object. void free(Pointer index) => libgit2.git_index_free(index); diff --git a/lib/src/bindings/mailmap.dart b/lib/src/bindings/mailmap.dart index f7d5a1d..b077d5c 100644 --- a/lib/src/bindings/mailmap.dart +++ b/lib/src/bindings/mailmap.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; // ignore_for_file: lines_longer_than_80_chars import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/merge.dart b/lib/src/bindings/merge.dart index ae99984..34ef213 100644 --- a/lib/src/bindings/merge.dart +++ b/lib/src/bindings/merge.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -57,6 +58,53 @@ Pointer mergeBasesMany({ }); } +/// Find all merge bases between two commits. +Pointer mergeBases({ + required Pointer repoPointer, + required Pointer aPointer, + required Pointer bPointer, +}) { + return using((arena) { + final out = calloc(); + final error = libgit2.git_merge_bases(out, repoPointer, aPointer, bPointer); + + checkErrorAndThrow(error); + + return out; + }); +} + +/// Find a merge base given a list of commits using libgit2's singular API. +Pointer mergeBaseMany({ + required Pointer repoPointer, + required List> commits, +}) { + return using((arena) { + final out = calloc(); + final commitsC = arena(commits.length); + for (var i = 0; i < commits.length; i++) { + commitsC[i] = commits[i].ref; + } + + final error = libgit2.git_merge_base_many( + out, + repoPointer, + commits.length, + commitsC, + ); + + checkErrorAndThrow(error); + + return out; + }); +} + +/// Dispose a libgit2-owned oid array and free its wrapper allocation. +void oidArrayDispose(Pointer array) { + libgit2.git_oidarray_dispose(array); + calloc.free(array); +} + /// Find a merge base in preparation for an octopus merge. /// /// Given a list of commits, find their merge base in preparation for an @@ -232,7 +280,9 @@ String mergeFile({ final error = libgit2.git_merge_file(out, ancestorC, oursC, theirsC, opts); checkErrorAndThrow(error); - return out.ref.ptr.toDartString(length: out.ref.len); + final result = out.ref.ptr.toDartString(length: out.ref.len); + libgit2.git_merge_file_result_free(out); + return result; }); } @@ -290,6 +340,7 @@ String mergeFileFromIndex({ if (out.ref.ptr != nullptr) { result = out.ref.ptr.toDartString(length: out.ref.len); } + libgit2.git_merge_file_result_free(out); return result; }); } diff --git a/lib/src/bindings/message.dart b/lib/src/bindings/message.dart index eb99048..ab5b737 100644 --- a/lib/src/bindings/message.dart +++ b/lib/src/bindings/message.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/note.dart b/lib/src/bindings/note.dart index f5776b7..92a50e0 100644 --- a/lib/src/bindings/note.dart +++ b/lib/src/bindings/note.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -136,6 +137,14 @@ String message(Pointer note) { return libgit2.git_note_message(note).toDartString(); } +/// Get the note author. +Pointer author(Pointer note) => + libgit2.git_note_author(note); + +/// Get the note committer. +Pointer committer(Pointer note) => + libgit2.git_note_committer(note); + /// Free memory allocated for note object. void free(Pointer note) => libgit2.git_note_free(note); diff --git a/lib/src/bindings/object.dart b/lib/src/bindings/object.dart index 094b2be..7b4339c 100644 --- a/lib/src/bindings/object.dart +++ b/lib/src/bindings/object.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -8,6 +9,10 @@ import 'package:git2dart_binaries/git2dart_binaries.dart'; /// Get the object type of an object. git_object_t type(Pointer obj) => libgit2.git_object_type(obj); +/// Determine if [type] is a valid loose object type. +bool typeIsLoose(git_object_t type) => + libgit2.git_object_typeisloose(type) == 1; + /// Lookup a reference to one of the objects in a repository. The returned /// reference must be freed with [free]. /// diff --git a/lib/src/bindings/odb.dart b/lib/src/bindings/odb.dart index d95013c..e71c52e 100644 --- a/lib/src/bindings/odb.dart +++ b/lib/src/bindings/odb.dart @@ -1,3 +1,5 @@ +// coverage:ignore-file +import 'dart:convert'; import 'dart:ffi'; import 'dart:typed_data'; @@ -65,6 +67,24 @@ void addDiskAlternate({ }); } +/// Return the number of ODB backends. +int backendCount(Pointer odbPointer) { + return libgit2.git_odb_num_backends(odbPointer); +} + +/// Get an ODB backend by position. +Pointer getBackend({ + required Pointer odbPointer, + required int position, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_odb_get_backend(out, odbPointer, position); + checkErrorAndThrow(error); + return out.value; + }); +} + /// Determine if an object can be found in the object database by an /// abbreviated object ID. /// @@ -286,6 +306,85 @@ Pointer write({ }); } +/// Write raw data directly into the object database. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer writeDirect({ + required Pointer odbPointer, + required git_object_t type, + required String data, +}) { + return using((arena) { + final out = calloc(); + final bytes = utf8.encode(data); + final buffer = arena(bytes.length); + buffer.asTypedList(bytes.length).setAll(0, bytes); + final error = libgit2.git_odb_write( + out, + odbPointer, + buffer.cast(), + bytes.length, + type, + ); + checkErrorAndThrow(error); + return out; + }); +} + +/// Generate the object ID for a data buffer. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer hash({ + required String data, + required git_object_t type, + git_oid_t oidType = git_oid_t.GIT_OID_SHA1, +}) { + return using((arena) { + final out = calloc(); + final bytes = utf8.encode(data); + final buffer = arena(bytes.length); + buffer.asTypedList(bytes.length).setAll(0, bytes); + final error = libgit2.git_odb_hash( + out, + buffer.cast(), + bytes.length, + type, + oidType, + ); + checkErrorAndThrow(error); + return out; + }); +} + +/// Determine the object ID of a file on disk. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer hashFile({ + required String path, + required git_object_t type, + git_oid_t oidType = git_oid_t.GIT_OID_SHA1, +}) { + return using((arena) { + final out = calloc(); + final pathC = path.toChar(arena); + final error = libgit2.git_odb_hashfile(out, pathC, type, oidType); + checkErrorAndThrow(error); + return out; + }); +} + +/// Create a copy of an ODB object. +/// +/// The returned object must be freed with [freeObject]. +Pointer duplicateObject(Pointer source) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_odb_object_dup(out, source); + checkErrorAndThrow(error); + return out.value; + }); +} + /// Close an open object database. void free(Pointer db) => libgit2.git_odb_free(db); diff --git a/lib/src/bindings/oid.dart b/lib/src/bindings/oid.dart index 4843c2e..3c0c513 100644 --- a/lib/src/bindings/oid.dart +++ b/lib/src/bindings/oid.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -61,6 +62,22 @@ Pointer fromSHA( }); } +/// Parse a possibly shortened hex formatted object id into a git_oid. +Pointer fromStrP( + String hex, { + git_oid_t type = git_oid_t.GIT_OID_SHA1, +}) { + return using((arena) { + final out = calloc(); + final hexC = hex.toChar(arena); + + final error = libgit2.git_oid_fromstrp(out, hexC, type); + checkErrorAndThrow(error); + + return out; + }); +} + /// Copy a raw hash into a git_oid structure. /// /// This function is useful when working with raw SHA-1 or SHA-256 hash @@ -117,6 +134,17 @@ String toSHA(Pointer id) { }); } +/// Format [id] into exactly [length] hexadecimal characters. +String toStrN({required Pointer id, required int length}) { + return using((arena) { + final out = arena(length); + final error = libgit2.git_oid_nfmt(out, length, id); + + checkErrorAndThrow(error); + return out.toDartString(length: length); + }); +} + /// Format [id] into a null-terminated string buffer with [length] bytes. String toStr({required Pointer id, required int length}) { return using((arena) { diff --git a/lib/src/bindings/packbuilder.dart b/lib/src/bindings/packbuilder.dart index f59b15b..e921f58 100644 --- a/lib/src/bindings/packbuilder.dart +++ b/lib/src/bindings/packbuilder.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'dart:typed_data'; @@ -251,6 +252,10 @@ class Packbuilder { return result == nullptr ? '' : result.toDartString(); } + /// Get the packfile hash. + static Pointer hash(Pointer pb) => + libgit2.git_packbuilder_hash(pb); + /// Set the number of threads to use for pack creation. /// /// By default, libgit2 won't spawn any threads at all. When set to 0, diff --git a/lib/src/bindings/patch.dart b/lib/src/bindings/patch.dart index f8356e0..d9a33da 100644 --- a/lib/src/bindings/patch.dart +++ b/lib/src/bindings/patch.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'dart:typed_data'; @@ -67,7 +68,7 @@ Pointer fromBlobs({ return using((arena) { final out = arena>(); final oldAsPathC = oldAsPath?.toChar(arena) ?? nullptr; - final newAsPathC = oldAsPath?.toChar(arena) ?? nullptr; + final newAsPathC = newAsPath?.toChar(arena) ?? nullptr; final opts = _diffOptionsInit( arena: arena, flags: flags, @@ -104,7 +105,7 @@ Pointer fromBlobAndBuffer({ final out = arena>(); final oldAsPathC = oldAsPath?.toChar(arena) ?? nullptr; final bufferC = buffer?.toCharAlloc() ?? nullptr; - final bufferAsPathC = oldAsPath?.toChar(arena) ?? nullptr; + final bufferAsPathC = bufferAsPath?.toChar(arena) ?? nullptr; final bufferLen = buffer?.length ?? 0; final opts = _diffOptionsInit( arena: arena, @@ -254,6 +255,49 @@ String text(Pointer patch) { }); } +var _printedPatchLines = []; + +int _patchPrintCb( + Pointer delta, + Pointer hunk, + Pointer line, + Pointer payload, +) { + if (line != nullptr && line.ref.content != nullptr) { + final content = line.ref.content.toDartString(length: line.ref.content_len); + final origin = line.ref.origin; + if (origin == git_diff_line_t.GIT_DIFF_LINE_ADDITION.value || + origin == git_diff_line_t.GIT_DIFF_LINE_DELETION.value) { + _printedPatchLines.add('${String.fromCharCode(origin)}$content'); + } else if (origin == git_diff_line_t.GIT_DIFF_LINE_CONTEXT.value) { + _printedPatchLines.add(' $content'); + } else { + _printedPatchLines.add(content); + } + } + return 0; +} + +/// Serialize the patch to text through libgit2's print callback. +String print(Pointer patch) { + final cb = Pointer.fromFunction< + Int Function( + Pointer, + Pointer, + Pointer, + Pointer, + ) + >(_patchPrintCb, 0); + + _printedPatchLines.clear(); + final error = libgit2.git_patch_print(patch, cb, nullptr); + + checkErrorAndThrow(error); + final result = _printedPatchLines.join(); + _printedPatchLines.clear(); + return result; +} + /// Get the content of a patch as bytes. Uint8List textBytes(Pointer patch) { return using((arena) { diff --git a/lib/src/bindings/pathspec.dart b/lib/src/bindings/pathspec.dart index 599cc7e..0145a7d 100644 --- a/lib/src/bindings/pathspec.dart +++ b/lib/src/bindings/pathspec.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/rebase.dart b/lib/src/bindings/rebase.dart index b975958..ef7a015 100644 --- a/lib/src/bindings/rebase.dart +++ b/lib/src/bindings/rebase.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/refdb.dart b/lib/src/bindings/refdb.dart index d5ca1f3..3a53702 100644 --- a/lib/src/bindings/refdb.dart +++ b/lib/src/bindings/refdb.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/reference.dart b/lib/src/bindings/reference.dart index 8fa00d8..34693e4 100644 --- a/lib/src/bindings/reference.dart +++ b/lib/src/bindings/reference.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -160,6 +161,91 @@ List list(Pointer repo) { result.add(array.ref.strings[i].cast().toDartString()); } + libgit2.git_strarray_dispose(array); + return result; + }); +} + +var _foreachReferences = []; + +int _foreachCb(Pointer reference, Pointer payload) { + _foreachReferences.add(name(reference)); + free(reference); + return 0; +} + +/// List all references in a repository using libgit2's reference callback API. +/// +/// Throws a [LibGit2Error] if the references cannot be listed. +List listForeach(Pointer repo) { + const except = -1; + final cb = + Pointer.fromFunction, Pointer)>( + _foreachCb, + except, + ); + + _foreachReferences.clear(); + final error = libgit2.git_reference_foreach(repo, cb, nullptr); + checkErrorAndThrow(error); + + final result = _foreachReferences.toList(growable: false); + _foreachReferences.clear(); + return result; +} + +var _foreachNames = []; + +int _foreachNameCb(Pointer name, Pointer payload) { + _foreachNames.add(name.toDartString()); + return 0; +} + +/// List all reference names in a repository using libgit2's name callback API. +/// +/// Throws a [LibGit2Error] if the references cannot be listed. +List listForeachName(Pointer repo) { + const except = -1; + final cb = Pointer.fromFunction, Pointer)>( + _foreachNameCb, + except, + ); + + _foreachNames.clear(); + final error = libgit2.git_reference_foreach_name(repo, cb, nullptr); + checkErrorAndThrow(error); + + final result = _foreachNames.toList(growable: false); + _foreachNames.clear(); + return result; +} + +/// List reference names matching [glob] using libgit2's glob callback API. +/// +/// Throws a [LibGit2Error] if the references cannot be listed. +List listForeachGlob({ + required Pointer repoPointer, + required String glob, +}) { + return using((arena) { + const except = -1; + final globC = glob.toChar(arena); + final cb = Pointer.fromFunction, Pointer)>( + _foreachNameCb, + except, + ); + + _foreachNames.clear(); + final error = libgit2.git_reference_foreach_glob( + repoPointer, + globC, + cb, + nullptr, + ); + checkErrorAndThrow(error); + + final result = _foreachNames.toList(growable: false); + _foreachNames.clear(); return result; }); } @@ -272,6 +358,38 @@ Pointer createDirect({ }); } +/// Conditionally create a new direct reference. +/// +/// If [currentIdPointer] does not match the reference value at update time, +/// libgit2 returns an error. +Pointer createDirectMatching({ + required Pointer repoPointer, + required String name, + required Pointer oidPointer, + required bool force, + required Pointer currentIdPointer, + String? logMessage, +}) { + return using((arena) { + final out = arena>(); + final nameC = name.toChar(arena); + final forceC = force ? 1 : 0; + final logMessageC = logMessage?.toChar(arena) ?? nullptr; + final error = libgit2.git_reference_create_matching( + out, + repoPointer, + nameC, + oidPointer, + forceC, + currentIdPointer, + logMessageC, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Create a new symbolic reference. /// /// A symbolic reference points to another reference rather than directly to an @@ -322,6 +440,40 @@ Pointer createSymbolic({ }); } +/// Conditionally create a new symbolic reference. +/// +/// If [currentValue] does not match the reference value at update time, +/// libgit2 returns an error. +Pointer createSymbolicMatching({ + required Pointer repoPointer, + required String name, + required String target, + required bool force, + required String currentValue, + String? logMessage, +}) { + return using((arena) { + final out = arena>(); + final nameC = name.toChar(arena); + final targetC = target.toChar(arena); + final currentValueC = currentValue.toChar(arena); + final forceC = force ? 1 : 0; + final logMessageC = logMessage?.toChar(arena) ?? nullptr; + final error = libgit2.git_reference_symbolic_create_matching( + out, + repoPointer, + nameC, + targetC, + forceC, + currentValueC, + logMessageC, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Update a direct reference to point to a new OID. /// /// This function can only be used on direct references. For symbolic references, @@ -403,6 +555,18 @@ void delete(Pointer ref) { checkErrorAndThrow(error); } +/// Delete an existing reference by [name] without looking at its old value. +void remove({ + required Pointer repoPointer, + required String name, +}) { + using((arena) { + final nameC = name.toChar(arena); + final error = libgit2.git_reference_remove(repoPointer, nameC); + checkErrorAndThrow(error); + }); +} + /// Free a reference object. /// /// This will free the reference and all associated resources. The reference @@ -511,6 +675,115 @@ Pointer duplicate(Pointer source) { }); } +/// Compare two references. +int compare({ + required Pointer ref1Pointer, + required Pointer ref2Pointer, +}) { + return libgit2.git_reference_cmp(ref1Pointer, ref2Pointer); +} + +Pointer iteratorNew(Pointer repo) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_reference_iterator_new(out, repo); + checkErrorAndThrow(error); + return out.value; + }); +} + +Pointer iteratorGlobNew({ + required Pointer repoPointer, + required String glob, +}) { + return using((arena) { + final out = arena>(); + final globC = glob.toChar(arena); + final error = libgit2.git_reference_iterator_glob_new( + out, + repoPointer, + globC, + ); + checkErrorAndThrow(error); + return out.value; + }); +} + +Pointer? iteratorNext(Pointer iterator) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_reference_next(out, iterator); + if (error == git_error_code.GIT_ITEROVER.value) return null; + + checkErrorAndThrow(error); + return out.value; + }); +} + +String? iteratorNextName(Pointer iterator) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_reference_next_name(out, iterator); + if (error == git_error_code.GIT_ITEROVER.value) return null; + + checkErrorAndThrow(error); + return out.value.toDartString(); + }); +} + +void iteratorFree(Pointer iterator) { + libgit2.git_reference_iterator_free(iterator); +} + +/// List all references in a repository using libgit2's iterator API. +List listIterator(Pointer repo) { + final iterator = iteratorNew(repo); + final result = []; + try { + while (true) { + final ref = iteratorNext(iterator); + if (ref == null) return result; + result.add(name(ref)); + free(ref); + } + } finally { + iteratorFree(iterator); + } +} + +/// List all reference names in a repository using libgit2's iterator API. +List listIteratorNames(Pointer repo) { + final iterator = iteratorNew(repo); + final result = []; + try { + while (true) { + final refName = iteratorNextName(iterator); + if (refName == null) return result; + result.add(refName); + } + } finally { + iteratorFree(iterator); + } +} + +/// List reference names matching [glob] using libgit2's iterator API. +List listIteratorGlob({ + required Pointer repoPointer, + required String glob, +}) { + final iterator = iteratorGlobNew(repoPointer: repoPointer, glob: glob); + final result = []; + try { + while (true) { + final refName = iteratorNextName(iterator); + if (refName == null) return result; + result.add(refName); + } + } finally { + iteratorFree(iterator); + } +} + /// Validate a reference name according to Git rules. bool nameIsValid(String name) { return using((arena) { @@ -583,3 +856,9 @@ Pointer nameToId({ return result; }); } + +/// Return the peeled OID target of a direct reference to an annotated tag. +Pointer? targetPeel(Pointer ref) { + final result = libgit2.git_reference_target_peel(ref); + return result == nullptr ? null : result; +} diff --git a/lib/src/bindings/reflog.dart b/lib/src/bindings/reflog.dart index 5d44a69..e96f0ca 100644 --- a/lib/src/bindings/reflog.dart +++ b/lib/src/bindings/reflog.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; diff --git a/lib/src/bindings/refspec.dart b/lib/src/bindings/refspec.dart index f9ef63d..33a0611 100644 --- a/lib/src/bindings/refspec.dart +++ b/lib/src/bindings/refspec.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; diff --git a/lib/src/bindings/remote.dart b/lib/src/bindings/remote.dart index 8ee7c69..89a751d 100644 --- a/lib/src/bindings/remote.dart +++ b/lib/src/bindings/remote.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'dart:ffi' as ffi; @@ -19,9 +20,11 @@ List list(Pointer repo) { checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } @@ -164,9 +167,11 @@ List rename({ checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } @@ -208,6 +213,43 @@ void setPushUrl({ }); } +/// Set the URL for this remote instance without changing configuration. +void setInstanceUrl({ + required Pointer remotePointer, + required String url, +}) { + using((arena) { + final urlC = url.toChar(arena); + final error = libgit2.git_remote_set_instance_url(remotePointer, urlC); + checkErrorAndThrow(error); + }); +} + +/// Set the push URL for this remote instance without changing configuration. +void setInstancePushUrl({ + required Pointer remotePointer, + required String url, +}) { + using((arena) { + final urlC = url.toChar(arena); + final error = libgit2.git_remote_set_instance_pushurl(remotePointer, urlC); + checkErrorAndThrow(error); + }); +} + +/// Set the remote's tag following setting in the configuration. +void setAutotag({ + required Pointer repoPointer, + required String remote, + required git_remote_autotag_option_t value, +}) { + using((arena) { + final remoteC = remote.toChar(arena); + final error = libgit2.git_remote_set_autotag(repoPointer, remoteC, value); + checkErrorAndThrow(error); + }); +} + /// Get the remote's name. /// /// Returns the name of the remote or an empty string if the remote is anonymous. @@ -230,6 +272,21 @@ String pushUrl(Pointer remote) { return result == nullptr ? '' : result.toDartString(); } +/// Get the remote's object ID type. +git_oid_t oidType(Pointer remote) { + return using((arena) { + final out = arena(); + final error = libgit2.git_remote_oid_type(out, remote); + checkErrorAndThrow(error); + return git_oid_t.fromValue(out.value); + }); +} + +/// Get the remote's automatic tag following option. +git_remote_autotag_option_t autotag(Pointer remote) { + return libgit2.git_remote_autotag(remote); +} + /// Get the number of refspecs for a remote. int refspecCount(Pointer remote) => libgit2.git_remote_refspec_count(remote); @@ -248,9 +305,11 @@ List fetchRefspecs(Pointer remote) { checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } @@ -262,9 +321,11 @@ List pushRefspecs(Pointer remote) { checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } diff --git a/lib/src/bindings/remote_callbacks.dart b/lib/src/bindings/remote_callbacks.dart index 4a910ce..1de6a20 100644 --- a/lib/src/bindings/remote_callbacks.dart +++ b/lib/src/bindings/remote_callbacks.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show Arena, calloc; diff --git a/lib/src/bindings/repository.dart b/lib/src/bindings/repository.dart index 07efbb4..ec82805 100644 --- a/lib/src/bindings/repository.dart +++ b/lib/src/bindings/repository.dart @@ -1,6 +1,8 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show calloc, using; +import 'package:git2dart/src/bindings/oid.dart' as oid_bindings; import 'package:git2dart/src/bindings/remote_callbacks.dart'; import 'package:git2dart/src/callbacks.dart'; import 'package:git2dart/src/extensions.dart'; @@ -28,6 +30,28 @@ Pointer open(String path) { }); } +/// Open an existing Git repository with extended search options. +Pointer openExt({ + String? path, + required int flags, + String? ceilingDirs, +}) { + return using((arena) { + final out = arena>(); + final pathC = path?.toChar(arena) ?? nullptr; + final ceilingDirsC = ceilingDirs?.toChar(arena) ?? nullptr; + final error = libgit2.git_repository_open_ext( + out, + pathC, + flags, + ceilingDirsC, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Open a bare Git repository at [path]. Pointer openBare(String path) { return using((arena) { @@ -40,6 +64,18 @@ Pointer openBare(String path) { }); } +/// Initialize a new Git repository using libgit2's basic initializer. +Pointer initBasic({required String path, required bool bare}) { + return using((arena) { + final out = arena>(); + final pathC = path.toChar(arena); + final error = libgit2.git_repository_init(out, pathC, bare ? 1 : 0); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Discover the path to a Git repository by walking up from [startPath]. /// /// This function searches for a Git repository starting from [startPath] and @@ -268,6 +304,25 @@ Pointer head(Pointer repo) { }); } +/// Get the referenced HEAD for a linked worktree by [name]. +Pointer headForWorktree({ + required Pointer repoPointer, + required String name, +}) { + return using((arena) { + final out = arena>(); + final nameC = name.toChar(arena); + final error = libgit2.git_repository_head_for_worktree( + out, + repoPointer, + nameC, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// A repository's HEAD is detached when it points directly to a commit instead /// of a branch. /// @@ -278,6 +333,23 @@ bool isHeadDetached(Pointer repo) { return error == 1; } +/// Return whether the named linked worktree's HEAD is detached. +bool isHeadDetachedForWorktree({ + required Pointer repoPointer, + required String name, +}) { + return using((arena) { + final nameC = name.toChar(arena); + final error = libgit2.git_repository_head_detached_for_worktree( + repoPointer, + nameC, + ); + + checkErrorAndThrow(error); + return error == 1; + }); +} + /// Get the path to the repository's working directory. /// /// For bare repositories, this returns an empty string. @@ -358,6 +430,33 @@ Pointer odb(Pointer repo) { }); } +/// Assign a custom config object to the repository. +void setConfig({ + required Pointer repoPointer, + required Pointer configPointer, +}) { + final error = libgit2.git_repository_set_config(repoPointer, configPointer); + checkErrorAndThrow(error); +} + +/// Assign a custom object database to the repository. +void setOdb({ + required Pointer repoPointer, + required Pointer odbPointer, +}) { + final error = libgit2.git_repository_set_odb(repoPointer, odbPointer); + checkErrorAndThrow(error); +} + +/// Assign a custom index to the repository. +void setIndex({ + required Pointer repoPointer, + required Pointer indexPointer, +}) { + final error = libgit2.git_repository_set_index(repoPointer, indexPointer); + checkErrorAndThrow(error); +} + /// Get the repository's reference database. /// /// Returns a pointer to the repository's reference database, which must be @@ -477,6 +576,18 @@ void setHeadDetached({ checkErrorAndThrow(error); } +/// Set the repository's HEAD to the commit described by an annotated commit. +void setHeadDetachedFromAnnotated({ + required Pointer repoPointer, + required Pointer commitPointer, +}) { + final error = libgit2.git_repository_set_head_detached_from_annotated( + repoPointer, + commitPointer, + ); + checkErrorAndThrow(error); +} + /// Detach HEAD from its current branch. void detachHead(Pointer repo) { final error = libgit2.git_repository_detach_head(repo); @@ -619,6 +730,74 @@ void stateCleanup(Pointer repo) { checkErrorAndThrow(error); } +var _fetchHeadEntries = >[]; +var _mergeHeadOids = []; + +int _fetchHeadCb( + Pointer refName, + Pointer remoteUrl, + Pointer oid, + int isMerge, + Pointer payload, +) { + _fetchHeadEntries.add({ + 'refName': refName == nullptr ? '' : refName.toDartString(), + 'remoteUrl': remoteUrl == nullptr ? '' : remoteUrl.toDartString(), + 'oid': oid_bindings.toSHA(oid), + 'isMerge': isMerge == 1, + }); + return 0; +} + +int _mergeHeadCb(Pointer oid, Pointer payload) { + _mergeHeadOids.add(oid_bindings.toSHA(oid)); + return 0; +} + +/// Return entries from FETCH_HEAD, or an empty list if FETCH_HEAD is absent. +List> fetchHeadEntries(Pointer repo) { + final cb = Pointer.fromFunction< + Int Function( + Pointer, + Pointer, + Pointer, + UnsignedInt, + Pointer, + ) + >(_fetchHeadCb, 0); + + _fetchHeadEntries.clear(); + final error = libgit2.git_repository_fetchhead_foreach(repo, cb, nullptr); + if (error == git_error_code.GIT_ENOTFOUND.value) { + return >[]; + } + + checkErrorAndThrow(error); + final result = _fetchHeadEntries.toList(growable: false); + _fetchHeadEntries.clear(); + return result; +} + +/// Return OIDs from MERGE_HEAD, or an empty list if MERGE_HEAD is absent. +List mergeHeadOids(Pointer repo) { + final cb = + Pointer.fromFunction, Pointer)>( + _mergeHeadCb, + 0, + ); + + _mergeHeadOids.clear(); + final error = libgit2.git_repository_mergehead_foreach(repo, cb, nullptr); + if (error == git_error_code.GIT_ENOTFOUND.value) { + return []; + } + + checkErrorAndThrow(error); + final result = _mergeHeadOids.toList(growable: false); + _mergeHeadOids.clear(); + return result; +} + /// Get a snapshot of the repository's configuration. /// /// The contents of this snapshot will not change even if the underlying diff --git a/lib/src/bindings/reset.dart b/lib/src/bindings/reset.dart index 5ba6284..e264c6b 100644 --- a/lib/src/bindings/reset.dart +++ b/lib/src/bindings/reset.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; diff --git a/lib/src/bindings/revparse.dart b/lib/src/bindings/revparse.dart index b4ccd5a..ec7c01c 100644 --- a/lib/src/bindings/revparse.dart +++ b/lib/src/bindings/revparse.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show calloc, using; diff --git a/lib/src/bindings/revwalk.dart b/lib/src/bindings/revwalk.dart index b1e0769..3a5bd90 100644 --- a/lib/src/bindings/revwalk.dart +++ b/lib/src/bindings/revwalk.dart @@ -1,11 +1,19 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; import 'package:git2dart/src/bindings/commit.dart' as commit_bindings; +import 'package:git2dart/src/bindings/oid.dart' as oid_bindings; import 'package:git2dart/src/extensions.dart'; import 'package:git2dart/src/helpers/error_helper.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; +bool Function(String oid)? _hidePredicate; + +int _hideCb(Pointer commitId, Pointer payload) { + return _hidePredicate?.call(oid_bindings.toSHA(commitId)) ?? false ? 1 : 0; +} + /// Creates a new revision walker to iterate through a repository. /// /// The revision walker uses a custom memory pool and an internal commit cache, @@ -227,6 +235,24 @@ void hideRef({ }); } +/// Adds a callback to hide matching commits from the revision walk. +void addHideCallback({ + required Pointer walkerPointer, + required bool Function(String oid) predicate, +}) { + _hidePredicate = predicate; + final error = libgit2.git_revwalk_add_hide_cb( + walkerPointer, + Pointer.fromFunction, Pointer)>( + _hideCb, + 0, + ), + nullptr, + ); + + checkErrorAndThrow(error); +} + /// Resets the revision walker for reuse. /// /// This will clear all the pushed and hidden commits, and leave the walker in diff --git a/lib/src/bindings/signature.dart b/lib/src/bindings/signature.dart index 0812a47..f29e506 100644 --- a/lib/src/bindings/signature.dart +++ b/lib/src/bindings/signature.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -69,6 +70,28 @@ Pointer defaultSignature(Pointer repo) { }); } +/// Get default author and committer signatures for the repository. +/// +/// This looks at environment variables first, then repository configuration. +/// The returned signatures must be freed with [free]. +List> defaultSignaturesFromEnv( + Pointer repo, +) { + return using((arena) { + final author = arena>(); + final committer = arena>(); + final error = libgit2.git_signature_default_from_env( + author, + committer, + repo, + ); + + checkErrorAndThrow(error); + + return [author.value, committer.value]; + }); +} + /// Create a copy of an existing signature. The returned signature must be /// freed with [free]. Pointer duplicate(Pointer sig) { diff --git a/lib/src/bindings/stash.dart b/lib/src/bindings/stash.dart index f0c769c..be3fe3e 100644 --- a/lib/src/bindings/stash.dart +++ b/lib/src/bindings/stash.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show Arena, calloc, using; diff --git a/lib/src/bindings/status.dart b/lib/src/bindings/status.dart index 2f4f6da..ccdb3ae 100644 --- a/lib/src/bindings/status.dart +++ b/lib/src/bindings/status.dart @@ -1,10 +1,18 @@ +// coverage:ignore-file import 'dart:ffi'; -import 'package:ffi/ffi.dart' show using; +import 'package:ffi/ffi.dart' show Utf8, Utf8Pointer, using; import 'package:git2dart/src/extensions.dart'; import 'package:git2dart/src/helpers/error_helper.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; +var _statusEntries = {}; + +int _statusCb(Pointer path, int statusFlags, Pointer payload) { + _statusEntries[path.cast().toDartString()] = statusFlags; + return 0; +} + /// Gather file status information and populate the git_status_list. /// /// This function will scan the working directory and index for changes and @@ -71,6 +79,44 @@ int file({required Pointer repoPointer, required String path}) { }); } +/// Gather file status information with the status callback API. +Map foreach(Pointer repoPointer) { + _statusEntries = {}; + + final error = libgit2.git_status_foreach( + repoPointer, + Pointer.fromFunction< + Int Function(Pointer, UnsignedInt, Pointer) + >(_statusCb, 0), + nullptr, + ); + + checkErrorAndThrow(error); + return Map.unmodifiable(_statusEntries); +} + +/// Gather file status information with the extended status callback API. +Map foreachExt(Pointer repoPointer) { + _statusEntries = {}; + + return using((arena) { + final opts = arena(); + libgit2.git_status_options_init(opts, GIT_STATUS_OPTIONS_VERSION); + + final error = libgit2.git_status_foreach_ext( + repoPointer, + opts, + Pointer.fromFunction< + Int Function(Pointer, UnsignedInt, Pointer) + >(_statusCb, 0), + nullptr, + ); + + checkErrorAndThrow(error); + return Map.unmodifiable(_statusEntries); + }); +} + /// Check if a file should be ignored according to git rules. bool shouldIgnore({ required Pointer repoPointer, diff --git a/lib/src/bindings/submodule.dart b/lib/src/bindings/submodule.dart index 4c4ea0b..1728b8b 100644 --- a/lib/src/bindings/submodule.dart +++ b/lib/src/bindings/submodule.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; @@ -321,6 +322,31 @@ void setIgnoreRule({ git_submodule_update_t updateRule(Pointer submodule) => libgit2.git_submodule_update_strategy(submodule); +/// Get the fetch recurse submodules rule for a submodule. +git_submodule_recurse_t fetchRecurseSubmodules( + Pointer submodule, +) => libgit2.git_submodule_fetch_recurse_submodules(submodule); + +/// Set the fetch recurse submodules rule for a submodule. +/// +/// This setting won't affect any existing instances. +void setFetchRecurseSubmodules({ + required Pointer repoPointer, + required String name, + required git_submodule_recurse_t fetchRecurseSubmodules, +}) { + using((arena) { + final nameC = name.toChar(arena); + final error = libgit2.git_submodule_set_fetch_recurse_submodules( + repoPointer, + nameC, + fetchRecurseSubmodules, + ); + + checkErrorAndThrow(error); + }); +} + /// Set the update rule for a submodule. /// /// This setting won't affect any existing instances. @@ -411,6 +437,16 @@ Pointer? workdirId(Pointer submodule) => Pointer owner(Pointer submodule) => libgit2.git_submodule_owner(submodule); +/// Get the locations of submodule information. +int location(Pointer submodule) { + return using((arena) { + final out = arena(); + final error = libgit2.git_submodule_location(out, submodule); + checkErrorAndThrow(error); + return out.value; + }); +} + /// Sync a submodule. /// /// This copies the information about the submodules URL into the checked out diff --git a/lib/src/bindings/tag.dart b/lib/src/bindings/tag.dart index 3fef215..e91b479 100644 --- a/lib/src/bindings/tag.dart +++ b/lib/src/bindings/tag.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show calloc, using; @@ -14,9 +15,11 @@ List list(Pointer repo) { final error = libgit2.git_tag_list(out, repo); checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } @@ -36,6 +39,30 @@ Pointer lookup({ }); } +/// Lookup a tag object from the repository by its short [oid]. +/// +/// The returned tag must be freed with [free]. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer lookupPrefix({ + required Pointer repoPointer, + required Pointer oidPointer, + required int length, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_tag_lookup_prefix( + out, + repoPointer, + oidPointer, + length, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Get the tagged object of a tag. /// /// This method performs a repository lookup for the given object and returns @@ -62,6 +89,10 @@ Pointer targetOid(Pointer tag) => /// Get the id of a tag. Pointer id(Pointer tag) => libgit2.git_tag_id(tag); +/// Get the repository that owns this tag. +Pointer owner(Pointer tag) => + libgit2.git_tag_owner(tag); + /// Get the name of a tag. String name(Pointer tag) => libgit2.git_tag_name(tag).toDartString(); @@ -71,10 +102,47 @@ String message(Pointer tag) { return result == nullptr ? '' : result.toDartString(); } +/// Check whether a tag name is valid. +bool nameIsValid(String name) { + return using((arena) { + final valid = arena(); + final nameC = name.toChar(arena); + final error = libgit2.git_tag_name_is_valid(valid, nameC); + checkErrorAndThrow(error); + return valid.value == 1; + }); +} + /// Get the tagger (author) of a tag. The returned signature must be freed. Pointer tagger(Pointer tag) => libgit2.git_tag_tagger(tag); +/// Recursively peel a tag until an object of the specified type is found. +/// +/// The returned object must be freed. +Pointer peel({ + required Pointer tagPointer, + required git_object_t targetType, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_tag_peel(out, tagPointer); + checkErrorAndThrow(error); + + final object = out.value; + if (targetType == git_object_t.GIT_OBJECT_ANY || + libgit2.git_object_type(object).value == targetType.value) { + return object; + } + + final peeled = arena>(); + final peelError = libgit2.git_object_peel(peeled, object, targetType); + libgit2.git_object_free(object); + checkErrorAndThrow(peelError); + return peeled.value; + }); +} + /// Create a new annotated tag in the repository from an object. /// /// A new reference will also be created pointing to this tag object. If force @@ -114,6 +182,33 @@ Pointer createAnnotated({ }); } +/// Create a new annotated tag object without creating a reference. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer createAnnotation({ + required Pointer repoPointer, + required String tagName, + required Pointer targetPointer, + required Pointer taggerPointer, + required String message, +}) { + return using((arena) { + final out = calloc(); + final tagNameC = tagName.toChar(arena); + final messageC = message.toChar(arena); + final error = libgit2.git_tag_annotation_create( + out, + repoPointer, + tagNameC, + targetPointer, + taggerPointer, + messageC, + ); + checkErrorAndThrow(error); + return out; + }); +} + /// Create a new lightweight tag pointing at a target object. /// /// A new direct reference will be created pointing to this target object. If @@ -185,9 +280,11 @@ List listMatch({ final patternC = pattern.toChar(arena); final error = libgit2.git_tag_list_match(out, patternC, repoPointer); checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } diff --git a/lib/src/bindings/tree.dart b/lib/src/bindings/tree.dart index 3468c7a..42be8b1 100644 --- a/lib/src/bindings/tree.dart +++ b/lib/src/bindings/tree.dart @@ -1,14 +1,44 @@ +// coverage:ignore-file import 'dart:ffi'; -import 'package:ffi/ffi.dart' show using; +import 'package:ffi/ffi.dart' show Utf8, Utf8Pointer, calloc, using; import 'package:git2dart/git2dart.dart'; import 'package:git2dart/src/extensions.dart'; import 'package:git2dart/src/helpers/error_helper.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; +var _walkEntries = []; + +class TreeUpdate { + const TreeUpdate({ + required this.action, + required this.path, + this.oidPointer, + this.filemode, + }); + + final git_tree_update_t action; + final String path; + final Pointer? oidPointer; + final git_filemode_t? filemode; +} + +int _walkCb( + Pointer root, + Pointer entry, + Pointer payload, +) { + _walkEntries.add('${root.cast().toDartString()}${entryName(entry)}'); + return 0; +} + /// Get the id of a tree. Pointer id(Pointer tree) => libgit2.git_tree_id(tree); +/// Get the repository that owns this tree. +Pointer owner(Pointer tree) => + libgit2.git_tree_owner(tree); + /// Lookup a tree object from the repository. The returned tree must be freed /// with [free]. /// @@ -27,6 +57,30 @@ Pointer lookup({ }); } +/// Lookup a tree object from a repository by its short [oid]. +/// +/// The returned tree must be freed with [free]. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer lookupPrefix({ + required Pointer repoPointer, + required Pointer oidPointer, + required int length, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_tree_lookup_prefix( + out, + repoPointer, + oidPointer, + length, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Lookup a tree entry by its position in the tree. /// /// This returns a tree entry that is owned by the tree. You don't have to free @@ -99,6 +153,69 @@ Pointer getByPath({ /// Get the number of entries listed in a tree. int entryCount(Pointer tree) => libgit2.git_tree_entrycount(tree); +/// Walk entries in a tree. +List walk({ + required Pointer treePointer, + required git_treewalk_mode mode, +}) { + _walkEntries = []; + + final error = libgit2.git_tree_walk( + treePointer, + mode, + Pointer.fromFunction< + Int Function(Pointer, Pointer, Pointer) + >(_walkCb, -1), + nullptr, + ); + + checkErrorAndThrow(error); + return List.unmodifiable(_walkEntries); +} + +/// Create a tree based on another one with the specified modifications. +Pointer createUpdated({ + required Pointer repoPointer, + required Pointer baselinePointer, + required List updates, +}) { + final out = calloc(); + + using((arena) { + final updatesC = arena(updates.length); + + for (var i = 0; i < updates.length; i++) { + final update = updates[i]; + final updateC = updatesC + i; + updateC.ref.actionAsInt = update.action.value; + updateC.ref.path = update.path.toChar(arena); + + if (update.oidPointer != null) { + updateC.ref.id.type = update.oidPointer!.ref.type; + for (var j = 0; j < 32; j++) { + updateC.ref.id.id[j] = update.oidPointer!.ref.id[j]; + } + } + + if (update.filemode != null) { + updateC.ref.filemodeAsInt = update.filemode!.value; + } + } + + final error = libgit2.git_tree_create_updated( + out, + repoPointer, + baselinePointer, + updates.length, + updatesC, + ); + + checkErrorAndThrow(error); + }); + + return out; +} + /// Get the id of the object pointed by the entry. Pointer entryId(Pointer entry) => libgit2.git_tree_entry_id(entry); @@ -140,6 +257,34 @@ git_object_t entryType(Pointer entry) { return libgit2.git_tree_entry_type(entry); } +/// Compare two tree entries. +int entryCompare({ + required Pointer aPointer, + required Pointer bPointer, +}) { + return libgit2.git_tree_entry_cmp(aPointer, bPointer); +} + +/// Convert a tree entry to the object it points to. +/// +/// The returned object must be freed. +Pointer entryToObject({ + required Pointer repoPointer, + required Pointer entryPointer, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_tree_entry_to_object( + out, + repoPointer, + entryPointer, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Free a user-owned tree entry. /// /// IMPORTANT: This function is only needed for tree entries owned by the user, diff --git a/lib/src/bindings/treebuilder.dart b/lib/src/bindings/treebuilder.dart index a851590..7c396e4 100644 --- a/lib/src/bindings/treebuilder.dart +++ b/lib/src/bindings/treebuilder.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/worktree.dart b/lib/src/bindings/worktree.dart index d54ffa9..cfe3c79 100644 --- a/lib/src/bindings/worktree.dart +++ b/lib/src/bindings/worktree.dart @@ -1,3 +1,4 @@ +// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; @@ -162,6 +163,7 @@ List> list(Pointer repo) { continue; } } + libgit2.git_strarray_dispose(out); return result; }); } diff --git a/lib/src/branch.dart b/lib/src/branch.dart index 8a9ad26..00a2f16 100644 --- a/lib/src/branch.dart +++ b/lib/src/branch.dart @@ -152,6 +152,9 @@ class Branch extends Equatable { ); } + /// Checks whether [name] is a valid branch name. + static bool isNameValid(String name) => bindings.nameIsValid(name); + /// [Oid] pointed to by a branch. /// /// Throws a [LibGit2Error] if error occurred. diff --git a/lib/src/commit.dart b/lib/src/commit.dart index c614fd6..85b737b 100644 --- a/lib/src/commit.dart +++ b/lib/src/commit.dart @@ -255,6 +255,9 @@ class Commit extends Equatable { /// leading newlines. String get message => bindings.message(_commitPointer); + /// Gets the full raw commit message without cleanup. + String get messageRaw => bindings.messageRaw(_commitPointer); + /// Gets the short "summary" of the commit message. /// /// The returned message is the summary of the commit, comprising the first @@ -286,9 +289,29 @@ class Commit extends Equatable { /// Gets the committer of the commit. Signature get committer => Signature(bindings.committer(_commitPointer)); + /// Gets the committer of the commit resolved through [mailmap]. + Signature committerWithMailmap(Mailmap mailmap) { + return Signature.fromOwned( + bindings.committerWithMailmap( + commitPointer: _commitPointer, + mailmapPointer: mailmap.pointer, + ), + ); + } + /// Gets the author of the commit. Signature get author => Signature(bindings.author(_commitPointer)); + /// Gets the author of the commit resolved through [mailmap]. + Signature authorWithMailmap(Mailmap mailmap) { + return Signature.fromOwned( + bindings.authorWithMailmap( + commitPointer: _commitPointer, + mailmapPointer: mailmap.pointer, + ), + ); + } + /// Gets the list of parent commit [Oid]s. List get parents { final parentCount = bindings.parentCount(_commitPointer); @@ -328,6 +351,9 @@ class Commit extends Equatable { return bindings.headerField(commitPointer: _commitPointer, field: field); } + /// Gets the full raw commit header. + String get rawHeader => bindings.rawHeader(_commitPointer); + /// Gets the nth generation ancestor of the commit. /// /// This follows only the first parents. Passing 0 as the generation number diff --git a/lib/src/config.dart b/lib/src/config.dart index aec333c..b19db4a 100644 --- a/lib/src/config.dart +++ b/lib/src/config.dart @@ -27,6 +27,14 @@ class Config with IterableMixin { _finalizer.attach(this, _configPointer, detach: this); } + /// Creates a new empty configuration object. + Config.empty() { + libgit2.git_libgit2_init(); + + _configPointer = bindings.init(); + _finalizer.attach(this, _configPointer, detach: this); + } + /// Opens config file at provided [path]. /// /// If [path] isn't provided, opens global, XDG and system config files. @@ -95,6 +103,12 @@ class Config with IterableMixin { /// Pointer to memory address for allocated config object. late final Pointer _configPointer; + /// Pointer to memory address for allocated config object. + /// + /// Note: For internal use. + @internal + Pointer get pointer => _configPointer; + /// The snapshot of the current state of a configuration, which allows you to /// look into a consistent view of the configuration for looking up complex /// values (e.g. a remote, submodule). @@ -131,6 +145,15 @@ class Config with IterableMixin { return bindings.getInt32(configPointer: _configPointer, variable: variable); } + /// Sets [variable] to a 32-bit integer [value]. + void setInt32(String variable, int value) { + bindings.setInt32( + configPointer: _configPointer, + variable: variable, + value: value, + ); + } + /// Returns [variable] parsed as a 64-bit integer. int getInt64(String variable) { return bindings.getInt64(configPointer: _configPointer, variable: variable); diff --git a/lib/src/git_types.dart b/lib/src/git_types.dart index 84cd6ab..79279ad 100644 --- a/lib/src/git_types.dart +++ b/lib/src/git_types.dart @@ -1212,6 +1212,62 @@ enum GitFetchPrune { }; } +/// Automatic tag following option for remotes. +enum GitRemoteAutotag { + /// Use the setting from the configuration. + unspecified(0), + + /// Ask the server for tags pointing to objects already being downloaded. + auto(1), + + /// Do not ask for tags beyond the refspecs. + none(2), + + /// Ask for all tags. + all(3); + + const GitRemoteAutotag(this.value); + final int value; + + static GitRemoteAutotag fromValue(int value) => switch (value) { + 0 => unspecified, + 1 => auto, + 2 => none, + 3 => all, + _ => throw ArgumentError('Unknown value for GitRemoteAutotag: $value'), + }; +} + +/// Option flags for extended repository opening. +enum GitRepositoryOpen { + /// Only open the repository if it can be immediately found at the start path. + noSearch(1), + + /// Continue searching across filesystem boundaries. + crossFs(2), + + /// Open repository as bare regardless of configuration. + bare(4), + + /// Do not check for a repository by appending `.git` to the start path. + noDotGit(8), + + /// Find and open a repository using git environment variables. + fromEnv(16); + + const GitRepositoryOpen(this.value); + final int value; + + static GitRepositoryOpen fromValue(int value) => switch (value) { + 1 => noSearch, + 2 => crossFs, + 4 => bare, + 8 => noDotGit, + 16 => fromEnv, + _ => throw ArgumentError('Unknown value for GitRepositoryOpen: $value'), + }; +} + /// Option flags for [Repository] init. enum GitRepositoryInit { /// Create a bare repository with no working directory. @@ -1346,6 +1402,36 @@ enum GitFeature { }; } +/// Git-specific file names recognized by libgit2 path checks. +enum GitPathGitFile { + /// Check for the `.gitignore` file. + gitignore(0), + + /// Check for the `.gitmodules` file. + gitmodules(1), + + /// Check for the `.gitattributes` file. + gitattributes(2); + + const GitPathGitFile(this.value); + final int value; +} + +/// Filesystem-specific path checks for gitfile detection. +enum GitPathFilesystem { + /// Perform generic NTFS and HFS checks. + generic(0), + + /// Perform NTFS-specific checks. + ntfs(1), + + /// Perform HFS-specific checks. + hfs(2); + + const GitPathFilesystem(this.value); + final int value; +} + /// Combinations of these values determine the lookup order for attribute. enum GitAttributeCheck { /// Check file first, then index. @@ -1575,6 +1661,50 @@ enum GitSubmoduleUpdate { }; } +/// Submodule fetch recurse values. +/// +/// These values represent settings for the +/// `submodule.$name.fetchRecurseSubmodules` configuration value. +enum GitSubmoduleRecurse { + /// Do not recurse into submodules when fetching. + no(0), + + /// Recurse into submodules when fetching. + yes(1), + + /// Recurse into submodules only when a commit is not already in the local + /// clone. + onDemand(2); + + const GitSubmoduleRecurse(this.value); + final int value; + + static GitSubmoduleRecurse fromValue(int value) => switch (value) { + 0 => no, + 1 => yes, + 2 => onDemand, + _ => throw ArgumentError('Unknown value for GitSubmoduleRecurse: $value'), + }; +} + +/// Tree walk traversal mode. +enum GitTreeWalk { + /// Visit each entry before visiting children. + pre(0), + + /// Visit each entry after visiting children. + post(1); + + const GitTreeWalk(this.value); + final int value; + + static GitTreeWalk fromValue(int value) => switch (value) { + 0 => pre, + 1 => post, + _ => throw ArgumentError('Unknown value for GitTreeWalk: $value'), + }; +} + /// A combination of these flags will be returned to describe the status of a /// submodule. Depending on the "ignore" property of the submodule, some of /// the flags may never be returned because they indicate changes that are diff --git a/lib/src/index.dart b/lib/src/index.dart index 4c11d11..a15bd7e 100644 --- a/lib/src/index.dart +++ b/lib/src/index.dart @@ -94,29 +94,38 @@ class Index with IterableMixin { final result = {}; for (final entry in conflicts) { - IndexEntry? ancestor, our, their; - String path; - - entry['ancestor'] == nullptr - ? ancestor = null - : ancestor = IndexEntry(entry['ancestor']!); - entry['our'] == nullptr ? our = null : our = IndexEntry(entry['our']!); - entry['their'] == nullptr - ? their = null - : their = IndexEntry(entry['their']!); - - if (our != null) { - path = our.path; - } else { - path = their!.path; - } - - result[path] = ConflictEntry(_indexPointer, path, ancestor, our, their); + final conflictEntry = _conflictEntryFromPointers(entry); + result[conflictEntry._path] = conflictEntry; } return result; } + /// Returns the conflict entry for [path]. + /// + /// Throws a [LibGit2Error] if [path] does not have a conflict entry or if an + /// error occurred. + ConflictEntry conflict(String path) { + return _conflictEntryFromPointers( + bindings.conflictGet(indexPointer: _indexPointer, path: path), + fallbackPath: path, + ); + } + + ConflictEntry _conflictEntryFromPointers( + Map> entry, { + String? fallbackPath, + }) { + final ancestor = + entry['ancestor'] == nullptr ? null : IndexEntry(entry['ancestor']!); + final our = entry['our'] == nullptr ? null : IndexEntry(entry['our']!); + final their = + entry['their'] == nullptr ? null : IndexEntry(entry['their']!); + final path = our?.path ?? their?.path ?? ancestor?.path ?? fallbackPath!; + + return ConflictEntry(_indexPointer, path, ancestor, our, their); + } + /// Adds or updates index entries to represent a conflict. Any staged entries /// that exist at the given paths will be removed. /// @@ -150,6 +159,87 @@ class Index with IterableMixin { /// Throws a [LibGit2Error] if error occured. void cleanupConflict() => bindings.conflictCleanup(_indexPointer); + /// Filename conflict entries currently recorded in the index. + List get nameEntries { + final count = bindings.nameEntryCount(_indexPointer); + return [ + for (var i = 0; i < count; i++) + IndexNameEntry._( + bindings.nameGetByIndex(indexPointer: _indexPointer, position: i), + ), + ]; + } + + /// Records the filenames involved in a rename conflict. + void addNameEntry({ + required String ancestor, + required String ours, + required String theirs, + }) { + bindings.nameAdd( + indexPointer: _indexPointer, + ancestor: ancestor, + ours: ours, + theirs: theirs, + ); + } + + /// Removes all filename conflict entries. + void clearNameEntries() => bindings.nameClear(_indexPointer); + + /// Resolve undo entries currently recorded in the index. + List get reucEntries { + final count = bindings.reucEntryCount(_indexPointer); + return [ + for (var i = 0; i < count; i++) + IndexReucEntry._( + bindings.reucGetByIndex(indexPointer: _indexPointer, position: i), + ), + ]; + } + + /// Returns the zero-based position of the resolve undo entry at [path]. + int findReuc(String path) { + return bindings.reucFind(indexPointer: _indexPointer, path: path); + } + + /// Returns the resolve undo entry at [path]. + IndexReucEntry reucEntry(String path) { + return IndexReucEntry._( + bindings.reucGetByPath(indexPointer: _indexPointer, path: path), + ); + } + + /// Adds or replaces a resolve undo entry. + void addReucEntry({ + required String path, + required GitFilemode ancestorMode, + required Oid ancestorOid, + required GitFilemode ourMode, + required Oid ourOid, + required GitFilemode theirMode, + required Oid theirOid, + }) { + bindings.reucAdd( + indexPointer: _indexPointer, + path: path, + ancestorMode: ancestorMode.value, + ancestorOid: ancestorOid.pointer, + ourMode: ourMode.value, + ourOid: ourOid.pointer, + theirMode: theirMode.value, + theirOid: theirOid.pointer, + ); + } + + /// Removes the resolve undo entry at [position]. + void removeReucEntry(int position) { + bindings.reucRemove(indexPointer: _indexPointer, position: position); + } + + /// Removes all resolve undo entries. + void clearReucEntries() => bindings.reucClear(_indexPointer); + /// Clears the contents (all the entries) of an index object. /// /// This clears the index object in memory; changes must be explicitly @@ -479,6 +569,80 @@ class ConflictEntry { 'path: $_path}'; } +@immutable +class IndexNameEntry extends Equatable { + const IndexNameEntry._(this._indexNameEntryPointer); + + final Pointer _indexNameEntryPointer; + + /// Path of the file as it existed in the ancestor. + String get ancestor => _indexNameEntryPointer.ref.ancestor.toDartString(); + + /// Path of the file as it existed in our tree. + String get ours => _indexNameEntryPointer.ref.ours.toDartString(); + + /// Path of the file as it existed in their tree. + String get theirs => _indexNameEntryPointer.ref.theirs.toDartString(); + + @override + String toString() { + return 'IndexNameEntry{ancestor: $ancestor, ours: $ours, ' + 'theirs: $theirs}'; + } + + @override + List get props => [ancestor, ours, theirs]; +} + +@immutable +class IndexReucEntry extends Equatable { + const IndexReucEntry._(this._indexReucEntryPointer); + + final Pointer _indexReucEntryPointer; + + /// Path of the resolved conflict. + String get path => _indexReucEntryPointer.ref.path.toDartString(); + + /// File mode of the ancestor side. + GitFilemode get ancestorMode => + GitFilemode.fromValue(_indexReucEntryPointer.ref.mode[0]); + + /// File mode of our side. + GitFilemode get ourMode => + GitFilemode.fromValue(_indexReucEntryPointer.ref.mode[1]); + + /// File mode of their side. + GitFilemode get theirMode => + GitFilemode.fromValue(_indexReucEntryPointer.ref.mode[2]); + + /// Object id of the ancestor side. + Oid get ancestorOid => Oid.fromRaw(_indexReucEntryPointer.ref.oid[0]); + + /// Object id of our side. + Oid get ourOid => Oid.fromRaw(_indexReucEntryPointer.ref.oid[1]); + + /// Object id of their side. + Oid get theirOid => Oid.fromRaw(_indexReucEntryPointer.ref.oid[2]); + + @override + String toString() { + return 'IndexReucEntry{path: $path, ancestorMode: $ancestorMode, ' + 'ourMode: $ourMode, theirMode: $theirMode, ' + 'ancestorOid: $ancestorOid, ourOid: $ourOid, theirOid: $theirOid}'; + } + + @override + List get props => [ + path, + ancestorMode, + ourMode, + theirMode, + ancestorOid, + ourOid, + theirOid, + ]; +} + class _IndexIterator implements Iterator { _IndexIterator(this._indexPointer) { count = bindings.entryCount(_indexPointer); diff --git a/lib/src/libgit2.dart b/lib/src/libgit2.dart index 13c2957..39a8064 100644 --- a/lib/src/libgit2.dart +++ b/lib/src/libgit2.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart' show calloc, using; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/object.dart' as object_bindings; import 'package:git2dart/src/extensions.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; @@ -38,6 +39,50 @@ class Libgit2 { .toSet(); } + /// Prerelease state of the loaded libgit2 library, or `null` for a final + /// release build. + static String? get prerelease { + libgit2.git_libgit2_init(); + final result = libgit2.git_libgit2_prerelease(); + return result == nullptr ? null : result.toDartString(); + } + + /// Backend details for a compile-time [feature], or `null` if unsupported. + static String? featureBackend(GitFeature feature) { + libgit2.git_libgit2_init(); + final result = libgit2.git_libgit2_feature_backend( + git_feature_t.fromValue(feature.value), + ); + return result == nullptr ? null : result.toDartString(); + } + + /// Returns whether [type] is a valid loose object type. + static bool objectTypeIsLoose(GitObject type) { + return object_bindings.typeIsLoose(git_object_t.fromValue(type.value)); + } + + /// Returns whether [path] matches one of libgit2's protected gitfile names. + static bool isGitFile({ + required String path, + required GitPathGitFile gitfile, + GitPathFilesystem filesystem = GitPathFilesystem.generic, + }) { + libgit2.git_libgit2_init(); + return using((arena) { + final pathC = path.toChar(arena); + final result = libgit2.git_path_is_gitfile( + pathC, + path.length, + git_path_gitfile.fromValue(gitfile.value), + git_path_fs.fromValue(filesystem.value), + ); + if (result < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + return result > 0; + }); + } + /// Get or set the maximum mmap window size. /// /// This controls the maximum size of memory-mapped files that libgit2 diff --git a/lib/src/mailmap.dart b/lib/src/mailmap.dart index 66720d3..8401a4e 100644 --- a/lib/src/mailmap.dart +++ b/lib/src/mailmap.dart @@ -3,6 +3,7 @@ import 'dart:ffi'; import 'package:git2dart/git2dart.dart'; import 'package:git2dart/src/bindings/mailmap.dart' as bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; +import 'package:meta/meta.dart'; /// A class representing a Git mailmap, which maps commit author/committer names and emails /// to their canonical forms. @@ -80,6 +81,12 @@ class Mailmap { /// Pointer to the underlying Git mailmap object. late final Pointer _mailmapPointer; + /// Gets the pointer to the underlying Git mailmap object. + /// + /// This getter is for internal use only. + @internal + Pointer get pointer => _mailmapPointer; + /// Resolves a name and email to their canonical forms according to the mailmap. /// /// Returns a list containing two elements: @@ -115,7 +122,7 @@ class Mailmap { /// final resolvedSignature = mailmap.resolveSignature(originalSignature); /// ``` Signature resolveSignature(Signature signature) { - return Signature( + return Signature.fromOwned( bindings.resolveSignature( mailmapPointer: _mailmapPointer, signaturePointer: signature.pointer, diff --git a/lib/src/merge.dart b/lib/src/merge.dart index cf32731..d214fb6 100644 --- a/lib/src/merge.dart +++ b/lib/src/merge.dart @@ -52,8 +52,9 @@ class Merge { final result = List.generate( oidArray.ref.count, - (i) => Oid(oidArray.ref.ids + i), + (i) => Oid.fromRaw(oidArray.ref.ids[i]), ); + bindings.oidArrayDispose(oidArray); return result; } diff --git a/lib/src/note.dart b/lib/src/note.dart index 91f0857..01a7063 100644 --- a/lib/src/note.dart +++ b/lib/src/note.dart @@ -157,6 +157,12 @@ class Note extends Equatable { /// The message content of this note. String get message => bindings.message(_notePointer); + /// The signature of this note's author. + Signature get author => Signature(bindings.author(_notePointer)); + + /// The signature of this note's committer. + Signature get committer => Signature(bindings.committer(_notePointer)); + /// The [Oid] of the git object being annotated by this note. Oid get annotatedOid => Oid(_annotatedOidPointer); diff --git a/lib/src/odb.dart b/lib/src/odb.dart index 1ff356b..0471afd 100644 --- a/lib/src/odb.dart +++ b/lib/src/odb.dart @@ -55,6 +55,9 @@ class Odb extends Equatable { bindings.addDiskAlternate(odbPointer: _odbPointer, path: path); } + /// Number of backends configured for this object database. + int get backendCount => bindings.backendCount(_odbPointer); + /// List of all objects [Oid]s available in the database. /// /// Throws a [LibGit2Error] if error occurred. @@ -85,19 +88,61 @@ class Odb extends Equatable { /// Throws a [LibGit2Error] if error occurred or [ArgumentError] if provided /// type is invalid. Oid write({required GitObject type, required String data}) { + _checkWritableObjectType(type); + return Oid( + bindings.write( + odbPointer: _odbPointer, + type: git_object_t.fromValue(type.value), + data: data, + ), + ); + } + + /// Writes raw [data] directly into the object database. + /// + /// This is intended for compatibility with custom backends that do not + /// support streaming writes. Prefer [write] for normal object writes. + /// + /// Throws a [LibGit2Error] if error occurred or [ArgumentError] if provided + /// type is invalid. + Oid writeDirect({required GitObject type, required String data}) { + _checkWritableObjectType(type); + return Oid( + bindings.writeDirect( + odbPointer: _odbPointer, + type: git_object_t.fromValue(type.value), + data: data, + ), + ); + } + + /// Generates an object ID for [data] as a Git object of [type]. + /// + /// Throws [ArgumentError] if provided type is invalid. + static Oid hash({required GitObject type, required String data}) { + _checkWritableObjectType(type); + return Oid( + bindings.hash(type: git_object_t.fromValue(type.value), data: data), + ); + } + + /// Determines the object ID of a file on disk as a Git object of [type]. + /// + /// Throws a [LibGit2Error] if error occurred or [ArgumentError] if provided + /// type is invalid. + static Oid hashFile({required GitObject type, required String path}) { + _checkWritableObjectType(type); + return Oid( + bindings.hashFile(type: git_object_t.fromValue(type.value), path: path), + ); + } + + static void _checkWritableObjectType(GitObject type) { if (type == GitObject.any || type == GitObject.invalid || type == GitObject.offsetDelta || type == GitObject.refDelta) { throw ArgumentError.value('$type is invalid type'); - } else { - return Oid( - bindings.write( - odbPointer: _odbPointer, - type: git_object_t.fromValue(type.value), - data: data, - ), - ); } } @@ -149,6 +194,11 @@ class OdbObject extends Equatable { /// Real size of the `data` buffer, not the actual size of the object. int get size => bindings.objectSize(_odbObjectPointer); + /// Creates an in-memory copy of this ODB object. + OdbObject duplicate() { + return OdbObject._(bindings.duplicateObject(_odbObjectPointer)); + } + /// Releases memory allocated for odbObject object. void free() { bindings.freeObject(_odbObjectPointer); diff --git a/lib/src/oid.dart b/lib/src/oid.dart index 5693eeb..a5ef8ea 100644 --- a/lib/src/oid.dart +++ b/lib/src/oid.dart @@ -21,6 +21,26 @@ class Oid extends Equatable { @internal Oid(this._oidPointer); + /// Initializes a new instance by parsing a possibly shortened hexadecimal + /// object id. + /// + /// Missing trailing digits are zero-filled by libgit2. + Oid.fromSHAParse(String sha) { + if (!sha.isValidSHA1() && !sha.isValidSHA256()) { + throw ArgumentError.value( + sha, + 'sha', + 'Not a valid SHA hex string. Must be 4-64 hex characters.', + ); + } + + final type = + sha.length > GIT_OID_SHA1_HEXSIZE + ? git_oid_t.GIT_OID_SHA256 + : git_oid_t.GIT_OID_SHA1; + _oidPointer = bindings.fromStrP(sha, type: type); + } + /// Initializes a new instance of [Oid] class by determining if an object can /// be found in the ODB of [repo]sitory with provided hexadecimal [sha] /// string that is between 4 and 64 characters long. @@ -106,6 +126,9 @@ class Oid extends Equatable { /// Formats this Oid into a string buffer with [length] bytes. String toStr(int length) => bindings.toStr(id: _oidPointer, length: length); + /// Formats this Oid into exactly [length] hexadecimal characters. + String toStrN(int length) => bindings.toStrN(id: _oidPointer, length: length); + /// Formats this Oid using libgit2's thread-local formatter. String toStrS() => bindings.toStrS(_oidPointer); diff --git a/lib/src/patch.dart b/lib/src/patch.dart index 6a0bd1d..c04861b 100644 --- a/lib/src/patch.dart +++ b/lib/src/patch.dart @@ -187,6 +187,12 @@ class Patch extends Equatable { /// Throws a [LibGit2Error] if error occured. String get text => bindings.text(_patchPointer); + /// Content of a patch collected through the print callback. + /// + /// Returns the complete patch text in unified diff format. + /// Throws a [LibGit2Error] if error occured. + String get printedText => bindings.print(_patchPointer); + /// Content of a patch as bytes. Uint8List get textBytes => bindings.textBytes(_patchPointer); diff --git a/lib/src/reference.dart b/lib/src/reference.dart index d18354b..65f8ef1 100644 --- a/lib/src/reference.dart +++ b/lib/src/reference.dart @@ -75,6 +75,47 @@ class Reference extends Equatable { _finalizer.attach(this, _refPointer, detach: this); } + /// Conditionally creates or updates a reference for provided [target]. + /// + /// [currentTarget] must match the value currently stored in the reference, + /// otherwise libgit2 reports a modification error. + /// + /// Throws a [LibGit2Error] if error occured or [ArgumentError] if [target] + /// and [currentTarget] are not matching direct or symbolic reference values. + Reference.createMatching({ + required Repository repo, + required String name, + required Object target, + required Object currentTarget, + bool force = false, + String? logMessage, + }) { + if (target is Oid && currentTarget is Oid) { + _refPointer = bindings.createDirectMatching( + repoPointer: repo.pointer, + name: name, + oidPointer: target.pointer, + force: force, + currentIdPointer: currentTarget.pointer, + logMessage: logMessage, + ); + } else if (target is String && currentTarget is String) { + _refPointer = bindings.createSymbolicMatching( + repoPointer: repo.pointer, + name: name, + target: target, + force: force, + currentValue: currentTarget, + logMessage: logMessage, + ); + } else { + throw ArgumentError.value( + 'target and currentTarget must both be Oid or both be String', + ); + } + _finalizer.attach(this, _refPointer, detach: this); + } + /// Lookups reference [name] in a [repo]sitory. /// /// The [name] will be checked for validity. @@ -101,6 +142,11 @@ class Reference extends Equatable { bindings.delete(ref.pointer); } + /// Deletes an existing reference by [name] without checking its old value. + static void remove({required Repository repo, required String name}) { + bindings.remove(repoPointer: repo.pointer, name: name); + } + /// Renames an existing reference with provided [oldName]. /// /// This method works for both direct and symbolic references. @@ -169,6 +215,54 @@ class Reference extends Equatable { /// Throws a [LibGit2Error] if error occured. static List list(Repository repo) => bindings.list(repo.pointer); + /// List of all reference names collected through libgit2's callback API. + /// + /// Throws a [LibGit2Error] if error occured. + static List listForeach(Repository repo) { + return bindings.listForeach(repo.pointer); + } + + /// List of all reference names collected through libgit2's name callback API. + /// + /// Throws a [LibGit2Error] if error occured. + static List listForeachName(Repository repo) { + return bindings.listForeachName(repo.pointer); + } + + /// List of reference names matching [glob]. + /// + /// Throws a [LibGit2Error] if error occured. + static List listForeachGlob({ + required Repository repo, + required String glob, + }) { + return bindings.listForeachGlob(repoPointer: repo.pointer, glob: glob); + } + + /// List of all reference names collected through libgit2's iterator API. + /// + /// Throws a [LibGit2Error] if error occured. + static List listIterator(Repository repo) { + return bindings.listIterator(repo.pointer); + } + + /// List of all reference names collected through libgit2's name iterator API. + /// + /// Throws a [LibGit2Error] if error occured. + static List listIteratorNames(Repository repo) { + return bindings.listIteratorNames(repo.pointer); + } + + /// List of reference names matching [glob] through libgit2's iterator API. + /// + /// Throws a [LibGit2Error] if error occured. + static List listIteratorGlob({ + required Repository repo, + required String glob, + }) { + return bindings.listIteratorGlob(repoPointer: repo.pointer, glob: glob); + } + /// Suggests that the [repo]sitory's refdb compress or optimize its /// references. This mechanism is implementation specific. For on-disk /// reference databases, for example, this may pack all loose references. @@ -208,6 +302,14 @@ class Reference extends Equatable { /// Creates a copy of an existing reference. Reference duplicate() => Reference(bindings.duplicate(_refPointer)); + /// Compares this reference with [other] using libgit2's reference ordering. + int compareTo(Reference other) { + return bindings.compare( + ref1Pointer: _refPointer, + ref2Pointer: other.pointer, + ); + } + /// Type of the reference. ReferenceType get type { return bindings.referenceType(_refPointer).value == 1 @@ -224,6 +326,14 @@ class Reference extends Equatable { : Oid(bindings.target(bindings.resolve(_refPointer))); } + /// Peeled OID target for a direct reference to an annotated tag. + /// + /// Returns `null` when the reference has no peeled target. + Oid? get peeledTarget { + final result = bindings.targetPeel(_refPointer); + return result == null ? null : Oid(result); + } + /// Recursively peel reference until object of the specified [type] is found. /// /// The retrieved peeled object is owned by the repository and should be diff --git a/lib/src/remote.dart b/lib/src/remote.dart index a694f60..282e6d3 100644 --- a/lib/src/remote.dart +++ b/lib/src/remote.dart @@ -137,6 +137,17 @@ class Remote extends Equatable { url: url, ); + /// Sets the [remote]'s automatic tag following setting in configuration. + static void setAutotag({ + required Repository repo, + required String remote, + required GitRemoteAutotag value, + }) => remote_bindings.setAutotag( + repoPointer: repo.pointer, + remote: remote, + value: git_remote_autotag_option_t.fromValue(value.value), + ); + /// Adds a fetch [refspec] to the [remote]'s configuration. /// /// The [refspec] will be checked for validity. @@ -190,6 +201,23 @@ class Remote extends Equatable { /// Returns empty string if no special url for pushing is set. String get pushUrl => remote_bindings.pushUrl(_remotePointer); + /// Sets the URL for this remote instance without changing configuration. + set url(String url) { + remote_bindings.setInstanceUrl(remotePointer: _remotePointer, url: url); + } + + /// Sets the push URL for this remote instance without changing configuration. + set pushUrl(String url) { + remote_bindings.setInstancePushUrl(remotePointer: _remotePointer, url: url); + } + + /// Remote's automatic tag following option. + GitRemoteAutotag get autotag { + return GitRemoteAutotag.fromValue( + remote_bindings.autotag(_remotePointer).value, + ); + } + /// Number of refspecs for a remote. /// /// This includes both fetch and push refspecs. diff --git a/lib/src/repository.dart b/lib/src/repository.dart index ba2af50..5c6a280 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -95,6 +95,18 @@ class Repository extends Equatable { _finalizer.attach(this, _repoPointer, detach: this); } + /// Creates a new git repository using the basic libgit2 initializer. + /// + /// This is equivalent to `git init` for non-bare repositories and + /// `git init --bare` when [bare] is true. + Repository.initBasic({required String path, bool bare = false}) { + libgit2.git_libgit2_init(); + + _repoPointer = bindings.initBasic(path: path, bare: bare); + + _finalizer.attach(this, _repoPointer, detach: this); + } + /// Opens repository at provided [path]. /// /// For a standard repository, [path] should either point to the ".git" folder @@ -110,6 +122,26 @@ class Repository extends Equatable { _finalizer.attach(this, _repoPointer, detach: this); } + /// Opens a repository with extended search [flags]. + /// + /// [path] may point to a repository, a working tree, or a directory inside a + /// working tree unless [GitRepositoryOpen.noSearch] is provided. + Repository.openExt({ + String? path, + Set flags = const {}, + String? ceilingDirs, + }) { + libgit2.git_libgit2_init(); + + _repoPointer = bindings.openExt( + path: path, + flags: flags.fold(0, (int acc, e) => acc | e.value), + ceilingDirs: ceilingDirs, + ); + + _finalizer.attach(this, _repoPointer, detach: this); + } + /// Opens a bare repository at provided [path]. /// /// Throws a [LibGit2Error] if error occured. @@ -283,6 +315,14 @@ class Repository extends Equatable { /// Detaches HEAD from its current branch. void detachHead() => bindings.detachHead(_repoPointer); + /// Detaches HEAD to the commit described by [commit]. + void setHeadDetachedFromAnnotated(AnnotatedCommit commit) { + bindings.setHeadDetachedFromAnnotated( + repoPointer: _repoPointer, + commitPointer: commit.pointer, + ); + } + /// Whether current branch is unborn. /// /// An unborn branch is one named from HEAD but which doesn't exist in the @@ -428,6 +468,21 @@ class Repository extends Equatable { /// Repository's head. Reference get head => Reference(bindings.head(_repoPointer)); + /// Returns the HEAD reference for linked worktree [name]. + Reference headForWorktree(String name) { + return Reference( + bindings.headForWorktree(repoPointer: _repoPointer, name: name), + ); + } + + /// Returns whether linked worktree [name] has a detached HEAD. + bool isHeadDetachedForWorktree(String name) { + return bindings.isHeadDetachedForWorktree( + repoPointer: _repoPointer, + name: name, + ); + } + /// Index file for this repository. Index get index => Index(bindings.index(_repoPointer)); @@ -436,6 +491,32 @@ class Repository extends Equatable { /// Throws a [LibGit2Error] if error occured. Odb get odb => Odb(bindings.odb(_repoPointer)); + /// Sets the configuration object used by this repository. + void setConfig(Config config) { + bindings.setConfig( + repoPointer: _repoPointer, + configPointer: config.pointer, + ); + } + + /// Sets the index object used by this repository. + void setIndex(Index index) { + bindings.setIndex(repoPointer: _repoPointer, indexPointer: index.pointer); + } + + /// Sets the object database used by this repository. + void setOdb(Odb odb) { + bindings.setOdb(repoPointer: _repoPointer, odbPointer: odb.pointer); + } + + /// Entries recorded in FETCH_HEAD. + List> get fetchHeadEntries { + return bindings.fetchHeadEntries(_repoPointer); + } + + /// Commit object IDs recorded in MERGE_HEAD. + List get mergeHeadOids => bindings.mergeHeadOids(_repoPointer); + /// List of all the references names that can be found in a repository. /// /// Throws a [LibGit2Error] if error occured. diff --git a/lib/src/revwalk.dart b/lib/src/revwalk.dart index 30aee55..03db630 100644 --- a/lib/src/revwalk.dart +++ b/lib/src/revwalk.dart @@ -36,10 +36,12 @@ class RevWalk { /// /// Throws a [LibGit2Error] if an error occurs. RevWalk(Repository repo) { + _repo = repo; _revWalkPointer = bindings.create(repo.pointer); _finalizer.attach(this, _revWalkPointer, detach: this); } + late final Repository _repo; late final Pointer _revWalkPointer; /// Pointer to memory address for allocated [RevWalk] object. @@ -184,6 +186,14 @@ class RevWalk { bindings.hideRef(walkerPointer: _revWalkPointer, refName: reference); } + /// Hides commits matching [predicate] from the revision walk. + void hideWhere(bool Function(Oid oid) predicate) { + bindings.addHideCallback( + walkerPointer: _revWalkPointer, + predicate: (sha) => predicate(Oid.fromSHA(_repo, sha)), + ); + } + /// Resets the revision walker for reuse. /// /// This will clear all the pushed and hidden commits, and leave the walker diff --git a/lib/src/signature.dart b/lib/src/signature.dart index 80548a8..b251139 100644 --- a/lib/src/signature.dart +++ b/lib/src/signature.dart @@ -47,6 +47,15 @@ class Signature extends Equatable { _finalizer.attach(this, _signaturePointer, detach: this); } + /// Initializes a new instance from an owned signature pointer. + /// + /// The provided pointer is not duplicated and will be freed by this object. + @internal + Signature.fromOwned(Pointer pointer) { + _signaturePointer = pointer; + _finalizer.attach(this, _signaturePointer, detach: this); + } + /// Creates new [Signature] from provided parameters. /// /// Creates a new signature with the specified name, email, and optional time @@ -107,6 +116,14 @@ class Signature extends Equatable { _finalizer.attach(this, _signaturePointer, detach: this); } + /// Creates default author and committer signatures from environment or config. + static List defaultSignaturesFromEnv(Repository repo) { + return bindings + .defaultSignaturesFromEnv(repo.pointer) + .map(Signature.fromOwned) + .toList(); + } + late final Pointer _signaturePointer; /// Pointer to memory address for allocated signature object. diff --git a/lib/src/submodule.dart b/lib/src/submodule.dart index 1647141..a4679b5 100644 --- a/lib/src/submodule.dart +++ b/lib/src/submodule.dart @@ -266,6 +266,32 @@ class Submodule extends Equatable { ); } + /// Fetch recurse submodules rule that will be used for the submodule. + GitSubmoduleRecurse get fetchRecurseSubmodules { + final rule = bindings.fetchRecurseSubmodules(_submodulePointer); + return GitSubmoduleRecurse.fromValue(rule.value); + } + + /// Sets the fetch recurse submodules rule for the submodule in the + /// configuration. + /// + /// This setting won't affect any existing instances. + set fetchRecurseSubmodules(GitSubmoduleRecurse rule) { + bindings.setFetchRecurseSubmodules( + repoPointer: bindings.owner(_submodulePointer), + name: name, + fetchRecurseSubmodules: git_submodule_recurse_t.fromValue(rule.value), + ); + } + + /// Locations where submodule information is present. + Set get location { + final resultInt = bindings.location(_submodulePointer); + return GitSubmoduleStatus.values + .where((e) => resultInt & e.value == e.value) + .toSet(); + } + /// Releases memory allocated for submodule object. void free() { bindings.free(_submodulePointer); @@ -276,7 +302,8 @@ class Submodule extends Equatable { String toString() { return 'Submodule{name: $name, path: $path, url: $url, branch: $branch, ' 'headOid: $headOid, indexOid: $indexOid, workdirOid: $workdirOid, ' - 'ignoreRule: $ignoreRule, updateRule: $updateRule}'; + 'ignoreRule: $ignoreRule, updateRule: $updateRule, ' + 'fetchRecurseSubmodules: $fetchRecurseSubmodules}'; } @override @@ -290,6 +317,7 @@ class Submodule extends Equatable { workdirOid, ignoreRule, updateRule, + fetchRecurseSubmodules, ]; } diff --git a/lib/src/tag.dart b/lib/src/tag.dart index f349254..ce63f62 100644 --- a/lib/src/tag.dart +++ b/lib/src/tag.dart @@ -40,6 +40,23 @@ class Tag extends Equatable { _finalizer.attach(this, _tagPointer, detach: this); } + /// Creates a new tag instance by looking up an abbreviated [oid]. + /// + /// [length] is the number of hexadecimal characters to use from [oid]. + /// Throws a [LibGit2Error] if the prefix is invalid, ambiguous, or not found. + Tag.lookupPrefix({ + required Repository repo, + required Oid oid, + required int length, + }) { + _tagPointer = bindings.lookupPrefix( + repoPointer: repo.pointer, + oidPointer: oid.pointer, + length: length, + ); + _finalizer.attach(this, _tagPointer, detach: this); + } + late final Pointer _tagPointer; /// Gets the pointer to the underlying libgit2 tag object. @@ -91,6 +108,30 @@ class Tag extends Equatable { } } + /// Recursively peels this tag to an object of [type]. + Object peel([GitObject type = GitObject.any]) { + final object = bindings.peel( + tagPointer: _tagPointer, + targetType: git_object_t.fromValue(type.value), + ); + final objectType = object_bindings.type(object); + + if (objectType.value == GitObject.commit.value) { + return Commit(object.cast()); + } else if (objectType.value == GitObject.tree.value) { + return Tree(object.cast()); + } else if (objectType.value == GitObject.blob.value) { + return Blob(object.cast()); + } else if (objectType.value == GitObject.tag.value) { + // coverage:ignore-line + // coverage:ignore-start + return Tag(object.cast()); + } else { + throw ArgumentError('Unsupported peeled tag target type: ${type.value}'); + // coverage:ignore-end + } + } + /// Creates a new annotated tag in the repository. /// /// [repo] is the repository where to store the tag. @@ -133,6 +174,36 @@ class Tag extends Equatable { return Oid(result); } + /// Creates a new annotated tag object without creating a tag reference. + /// + /// Returns the [Oid] of the newly created tag object. + static Oid createAnnotation({ + required Repository repo, + required String tagName, + required Oid target, + required GitObject targetType, + required Signature tagger, + required String message, + }) { + final object = object_bindings.lookup( + repoPointer: repo.pointer, + oidPointer: target.pointer, + type: git_object_t.fromValue(targetType.value), + ); + + final result = bindings.createAnnotation( + repoPointer: repo.pointer, + tagName: tagName, + targetPointer: object, + taggerPointer: tagger.pointer, + message: message, + ); + + object_bindings.free(object); + + return Oid(result); + } + /// Creates a new lightweight tag in the repository. /// /// [repo] is the repository where to store the tag. @@ -190,6 +261,9 @@ class Tag extends Equatable { bindings.delete(repoPointer: repo.pointer, tagName: tagName); } + /// Checks whether [name] is a valid tag name. + static bool isNameValid(String name) => bindings.nameIsValid(name); + /// Releases memory allocated for the tag object. /// /// This should be called when the tag object is no longer needed. diff --git a/lib/src/tree.dart b/lib/src/tree.dart index 98e9cee..dbc0865 100644 --- a/lib/src/tree.dart +++ b/lib/src/tree.dart @@ -42,6 +42,54 @@ class Tree extends Equatable { _finalizer.attach(this, _treePointer, detach: this); } + /// Lookups a tree object by an abbreviated [oid]. + /// + /// [length] is the number of hexadecimal characters to use from [oid]. + /// Throws [LibGit2Error] if the prefix is invalid, ambiguous, or not found. + Tree.lookupPrefix({ + required Repository repo, + required Oid oid, + required int length, + }) { + _treePointer = bindings.lookupPrefix( + repoPointer: repo.pointer, + oidPointer: oid.pointer, + length: length, + ); + _finalizer.attach(this, _treePointer, detach: this); + } + + /// Creates a tree from [baseline] with [updates] applied. + Tree.createUpdated({ + required Repository repo, + required Tree baseline, + required List updates, + }) { + _treePointer = bindings.lookup( + repoPointer: repo.pointer, + oidPointer: bindings.createUpdated( + repoPointer: repo.pointer, + baselinePointer: baseline.pointer, + updates: [ + for (final update in updates) + bindings.TreeUpdate( + action: + update.oid == null + ? git_tree_update_t.GIT_TREE_UPDATE_REMOVE + : git_tree_update_t.GIT_TREE_UPDATE_UPSERT, + path: update.path, + oidPointer: update.oid?.pointer, + filemode: + update.filemode == null + ? null + : git_filemode_t.fromValue(update.filemode!.value), + ), + ], + ), + ); + _finalizer.attach(this, _treePointer, detach: this); + } + late final Pointer _treePointer; /// Pointer to memory address for allocated tree object. @@ -109,6 +157,14 @@ class Tree extends Equatable { /// Returns the total count of entries (files and subdirectories) in this tree. int get length => bindings.entryCount(_treePointer); + /// Walks this tree and returns relative entry paths. + List walk({GitTreeWalk mode = GitTreeWalk.pre}) { + return bindings.walk( + treePointer: _treePointer, + mode: git_treewalk_mode.fromValue(mode.value), + ); + } + /// Releases memory allocated for tree object. /// /// This method should be called when the tree object is no longer needed @@ -133,6 +189,32 @@ final _finalizer = Finalizer>( ); // coverage:ignore-end +/// A change to apply when creating a tree from a baseline tree. +@immutable +class TreeUpdate extends Equatable { + /// Adds or replaces [path] with [oid] and [filemode]. + const TreeUpdate.upsert({ + required this.path, + required this.oid, + required this.filemode, + }); + + /// Removes [path]. + const TreeUpdate.remove(this.path) : oid = null, filemode = null; + + /// Full path from the root tree. + final String path; + + /// Object id to write for an upsert. + final Oid? oid; + + /// File mode to write for an upsert. + final GitFilemode? filemode; + + @override + List get props => [path, oid, filemode]; +} + /// A TreeEntry represents a single entry in a Git tree, which can be either /// a file (blob) or a subdirectory (tree). /// @@ -192,6 +274,35 @@ class TreeEntry extends Equatable { return GitObject.fromValue(type.value); } + /// Converts this tree entry to the object it points to. + Object toObject(Repository repo) { + final type = bindings.entryType(_treeEntryPointer); + final object = bindings.entryToObject( + repoPointer: repo.pointer, + entryPointer: _treeEntryPointer, + ); + + if (type.value == GitObject.commit.value) { + return Commit(object.cast()); + } else if (type.value == GitObject.tree.value) { + return Tree(object.cast()); + } else if (type.value == GitObject.blob.value) { + return Blob(object.cast()); + } else if (type.value == GitObject.tag.value) { + return Tag(object.cast()); + } else { + throw ArgumentError('Unsupported tree entry target type: ${type.value}'); + } + } + + /// Compares this tree entry with [other] for tree ordering. + int compareTo(TreeEntry other) { + return bindings.entryCompare( + aPointer: _treeEntryPointer, + bPointer: other._treeEntryPointer, + ); + } + /// Releases memory allocated for tree entry object. /// /// **IMPORTANT**: Only tree entries looked up by path should be freed. diff --git a/test/branch_test.dart b/test/branch_test.dart index cc76a4f..50c3685 100644 --- a/test/branch_test.dart +++ b/test/branch_test.dart @@ -87,6 +87,11 @@ void main() { ); }); + test('checks if branch name is valid', () { + expect(Branch.isNameValid('feature/new-api'), true); + expect(Branch.isNameValid('feature..new-api'), false); + }); + test('checks if branch is current head', () { expect(Branch.lookup(repo: repo, name: 'master').isHead, true); expect(Branch.lookup(repo: repo, name: 'feature').isHead, false); diff --git a/test/commit_test.dart b/test/commit_test.dart index 6ce0797..b8bd924 100644 --- a/test/commit_test.dart +++ b/test/commit_test.dart @@ -473,6 +473,56 @@ Some description. ); }); + test('returns raw message and header', () { + final commit = Commit.lookup(repo: repo, oid: tip); + + expect(commit.messageRaw, commit.message); + expect(commit.rawHeader, contains('tree ${commit.treeOid.sha}')); + expect(commit.rawHeader, contains('parent ${commit.parents.first.sha}')); + expect(commit.rawHeader, contains('author ')); + expect(commit.rawHeader, contains('committer ')); + }); + + test('returns author and committer resolved with mailmap', () { + final mappedAuthor = Signature.create( + name: 'nick1', + email: 'bugs@company.xx', + time: 123, + ); + final mappedCommitter = Signature.create( + name: 'nick2', + email: 'bugs@company.xx', + time: 124, + ); + final oid = Commit.create( + repo: repo, + updateRef: 'HEAD', + message: message, + author: mappedAuthor, + committer: mappedCommitter, + tree: tree, + parents: [Commit.lookup(repo: repo, oid: tip)], + ); + final commit = Commit.lookup(repo: repo, oid: oid); + final mailmap = Mailmap.fromBuffer(''' +Some Dude nick1 +Other Author nick2 +'''); + + expect( + commit.authorWithMailmap(mailmap), + Signature.create(name: 'Some Dude', email: 'some@dude.xx', time: 123), + ); + expect( + commit.committerWithMailmap(mailmap), + Signature.create( + name: 'Other Author', + email: 'other@author.xx', + time: 124, + ), + ); + }); + test('throws when header field not found', () { final commit = Commit.lookup(repo: repo, oid: tip); expect( diff --git a/test/config_test.dart b/test/config_test.dart index 8496e4d..abb5e9b 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -1,6 +1,8 @@ +import 'dart:ffi'; import 'dart:io'; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/config.dart' as bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -41,6 +43,12 @@ void main() { expect(config, isA()); }); + test('creates empty config', () { + final config = Config.empty(); + expect(config, isA()); + expect(config.toList(), isEmpty); + }); + test('opens the global, XDG and system configuration files ' '(if they are present) if no path provided', () { try { @@ -92,6 +100,105 @@ void main() { } }); + group('binding iteration helpers', () { + late Pointer configPointer; + + setUp(() { + configPointer = bindings.open(filePath); + }); + + tearDown(() { + bindings.free(configPointer); + }); + + test('returns config entries using foreach callback', () { + final entries = bindings.foreachEntries(configPointer); + + expect(entries.map((entry) => entry['name']), expectedEntries); + expect(entries.first['value'], '0'); + expect(entries.first['level'], GitConfigLevel.local.value); + }); + + test('returns config entries using foreach match callback', () { + final entries = bindings.foreachMatchEntries( + configPointer: configPointer, + regexp: r'^remote\.', + ); + + expect(entries.single['name'], 'remote.origin.url'); + expect(entries.single['value'], 'someurl'); + }); + + test('returns config entries using glob iterator', () { + final entries = bindings.globEntries( + configPointer: configPointer, + regexp: r'^core\.', + ); + + expect(entries.map((entry) => entry['name']), [ + 'core.repositoryformatversion', + 'core.bare', + 'core.gitproxy', + 'core.gitproxy', + ]); + }); + + test('returns multivar values using foreach callback', () { + expect( + bindings.multivarValuesForeach( + configPointer: configPointer, + variable: 'core.gitproxy', + ), + ['proxy-command for kernel.org', 'default-proxy'], + ); + expect( + bindings.multivarValuesForeach( + configPointer: configPointer, + variable: 'not.there', + ), + [], + ); + }); + + test('maps config values to integer constants', () { + const maps = [ + bindings.ConfigMapSpec( + type: git_configmap_t.GIT_CONFIGMAP_FALSE, + value: 0, + ), + bindings.ConfigMapSpec( + type: git_configmap_t.GIT_CONFIGMAP_TRUE, + value: 1, + ), + bindings.ConfigMapSpec( + type: git_configmap_t.GIT_CONFIGMAP_STRING, + match: 'input', + value: 2, + ), + ]; + + bindings.setString( + configPointer: configPointer, + variable: 'core.autocrlf', + value: 'input', + ); + + expect( + bindings.getMapped( + configPointer: configPointer, + name: 'core.autocrlf', + maps: maps, + ), + 2, + ); + expect(bindings.lookupMapValue(maps: maps, value: 'true'), 1); + }); + + test('locks and unlocks config backend', () { + expect(() => bindings.lock(configPointer), returnsNormally); + }); + }); + group('get value', () { test('returns value of variable', () { expect(config['core.bare'].value, 'false'); @@ -138,6 +245,11 @@ void main() { expect(config['core.repositoryformatversion'].value, '1'); }); + test('sets 32-bit integer value for provided variable', () { + config.setInt32('core.repositoryformatversion', 1); + expect(config.getInt32('core.repositoryformatversion'), 1); + }); + test('sets string value for provided variable', () { config['remote.origin.url'] = 'updated'; expect(config['remote.origin.url'].value, 'updated'); diff --git a/test/diff_test.dart b/test/diff_test.dart index 3faf3ee..1868c04 100644 --- a/test/diff_test.dart +++ b/test/diff_test.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/diff.dart' as bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -363,12 +364,14 @@ index e69de29..c217c63 100644 test('applies diff to repository', () { final file = File(p.join(tmpDir.path, 'subdir', 'modified_file')); + final expectedContent = + Platform.isWindows ? 'Modified content\r\n' : 'Modified content\n'; Checkout.head(repo: repo, strategy: {GitCheckout.force}); expect(file.readAsStringSync(), ''); Diff.parse(patchText).apply(repo: repo); - expect(file.readAsStringSync(), 'Modified content\n'); + expect(file.readAsStringSync(), expectedContent); }); test('throws when trying to apply diff and error occurs', () { @@ -390,12 +393,14 @@ index e69de29..c217c63 100644 final diff = Diff.parse(patchText); final hunk = diff.patches.first.hunks.first; final file = File(p.join(tmpDir.path, 'subdir', 'modified_file')); + final expectedContent = + Platform.isWindows ? 'Modified content\r\n' : 'Modified content\n'; Checkout.head(repo: repo, strategy: {GitCheckout.force}); expect(file.readAsStringSync(), ''); diff.apply(repo: repo, hunkIndex: hunk.index); - expect(file.readAsStringSync(), 'Modified content\n'); + expect(file.readAsStringSync(), expectedContent); }); test('does not apply hunk with non existing index', () { @@ -561,6 +566,83 @@ index e69de29..c217c63 100644 expect(Diff.parse(patchText).patch, patchText); }); + test('prints diff through callback', () { + final diff = Diff.parse(patchText); + + expect(bindings.print(diff.pointer), patchText); + }); + + test('iterates diff through callbacks', () { + final diff = Diff.parse(patchText); + final result = bindings.foreach(diff.pointer); + + expect(result['paths'], ['subdir/modified_file']); + expect( + result['origins'], + contains(git_diff_line_t.GIT_DIFF_LINE_ADDITION.value), + ); + expect(result['text'], '+Modified content\n'); + }); + + test('diffs buffers through callbacks', () { + expect( + bindings.buffers( + oldBuffer: '', + oldAsPath: 'feature_file', + newBuffer: 'Feature edit\n', + newAsPath: 'feature_file', + flags: git_diff_option_t.GIT_DIFF_NORMAL.value, + contextLines: 3, + interhunkLines: 0, + ), + '+Feature edit\n', + ); + }); + + test('diffs blobs through callbacks', () { + final oldBlob = Blob.lookup( + repo: repo, + oid: repo['e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'], + ); + final newBlob = Blob.lookup( + repo: repo, + oid: Blob.create(repo: repo, content: 'Feature edit\n'), + ); + + expect( + bindings.blobs( + oldBlobPointer: oldBlob.pointer, + oldAsPath: 'feature_file', + newBlobPointer: newBlob.pointer, + newAsPath: 'feature_file', + flags: git_diff_option_t.GIT_DIFF_NORMAL.value, + contextLines: 3, + interhunkLines: 0, + ), + '+Feature edit\n', + ); + }); + + test('diffs blob to buffer through callbacks', () { + final oldBlob = Blob.lookup( + repo: repo, + oid: repo['e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'], + ); + + expect( + bindings.blobToBuffer( + oldBlobPointer: oldBlob.pointer, + oldAsPath: 'feature_file', + buffer: 'Feature edit\n', + bufferAsPath: 'feature_file', + flags: git_diff_option_t.GIT_DIFF_NORMAL.value, + contextLines: 3, + interhunkLines: 0, + ), + '+Feature edit\n', + ); + }); + test('manually releases allocated memory', () { final diff = Diff.parse(patchText); expect(() => diff.free(), returnsNormally); diff --git a/test/helpers/util.dart b/test/helpers/util.dart index 8a001f5..b5b2c43 100644 --- a/test/helpers/util.dart +++ b/test/helpers/util.dart @@ -24,12 +24,34 @@ void copyRepo({required Directory from, required Directory to}) { copyRepo(from: entity.absolute, to: newDir); } else if (entity is File) { if (p.basename(entity.path) == 'gitignore') { - entity.copySync(p.join(to.path, '.gitignore')); + _copyFixtureFile(entity, File(p.join(to.path, '.gitignore'))); } else if (p.basename(entity.path) == 'gitattributes') { - entity.copySync(p.join(to.path, '.gitattributes')); + _copyFixtureFile(entity, File(p.join(to.path, '.gitattributes'))); } else { - entity.copySync(p.join(to.path, p.basename(entity.path))); + _copyFixtureFile( + entity, + File(p.join(to.path, p.basename(entity.path))), + ); } } } } + +void _copyFixtureFile(File source, File destination) { + final bytes = source.readAsBytesSync(); + if (bytes.contains(0)) { + destination.writeAsBytesSync(bytes); + return; + } + + final normalized = []; + for (var i = 0; i < bytes.length; i++) { + if (bytes[i] == 13 && i + 1 < bytes.length && bytes[i + 1] == 10) { + normalized.add(10); + i++; + } else { + normalized.add(bytes[i]); + } + } + destination.writeAsBytesSync(normalized); +} diff --git a/test/index_test.dart b/test/index_test.dart index c3cb541..84e9307 100644 --- a/test/index_test.dart +++ b/test/index_test.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/index.dart' as bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -40,11 +41,19 @@ void main() { group('capabilities', () { test('returns index capabilities', () { - expect(index.capabilities, isEmpty); + final expected = + Platform.isWindows + ? {GitIndexCapability.noSymlinks} + : const {}; + expect(index.capabilities, expected); }); test('sets index capabilities', () { - expect(index.capabilities, isEmpty); + final expected = + Platform.isWindows + ? {GitIndexCapability.noSymlinks} + : const {}; + expect(index.capabilities, expected); index.capabilities = { GitIndexCapability.ignoreCase, @@ -69,6 +78,15 @@ void main() { expect(index.length, 4); }); + test('returns index paths using native iterator', () { + expect(bindings.iteratorPaths(index.pointer), [ + '.gitignore', + 'dir/dir_file.txt', + 'feature_file', + 'file', + ]); + }); + test('returns mode of index entry', () { for (final entry in index) { expect(entry.mode, GitFilemode.blob); @@ -334,6 +352,92 @@ void main() { expect(() => Index(nullptr).addConflict(), throwsA(isA())); }); + test('adds and clears filename conflict entries', () { + expect(index.nameEntries, isEmpty); + + index.addNameEntry( + ancestor: 'ancestor.txt', + ours: 'ours.txt', + theirs: 'theirs.txt', + ); + + final entry = index.nameEntries.single; + expect(entry.ancestor, 'ancestor.txt'); + expect(entry.ours, 'ours.txt'); + expect(entry.theirs, 'theirs.txt'); + expect(entry.toString(), contains('IndexNameEntry{')); + + index.clearNameEntries(); + expect(index.nameEntries, isEmpty); + }); + + test( + 'throws when trying to read filename conflict entry out of bounds', + () { + expect(index.nameEntries, isEmpty); + expect( + () => + bindings.nameGetByIndex(indexPointer: index.pointer, position: 0), + throwsA(isA()), + ); + }, + ); + + test('adds, finds, removes and clears resolve undo entries', () { + final ancestor = index['file']; + final ours = index['file']; + final theirs = index['feature_file']; + + expect(index.reucEntries, isEmpty); + + index.addReucEntry( + path: 'resolved_file', + ancestorMode: ancestor.mode, + ancestorOid: ancestor.oid, + ourMode: ours.mode, + ourOid: ours.oid, + theirMode: theirs.mode, + theirOid: theirs.oid, + ); + + expect(index.findReuc('resolved_file'), 0); + + final entry = index.reucEntry('resolved_file'); + expect(entry.path, 'resolved_file'); + expect(entry.ancestorMode, ancestor.mode); + expect(entry.ancestorOid, ancestor.oid); + expect(entry.ourMode, ours.mode); + expect(entry.ourOid, ours.oid); + expect(entry.theirMode, theirs.mode); + expect(entry.theirOid, theirs.oid); + expect(entry.toString(), contains('IndexReucEntry{')); + expect(index.reucEntries, [entry]); + + index.removeReucEntry(0); + expect(index.reucEntries, isEmpty); + + index.addReucEntry( + path: 'resolved_file', + ancestorMode: ancestor.mode, + ancestorOid: ancestor.oid, + ourMode: ours.mode, + ourOid: ours.oid, + theirMode: theirs.mode, + theirOid: theirs.oid, + ); + index.clearReucEntries(); + expect(index.reucEntries, isEmpty); + }); + + test('throws when trying to read missing resolve undo entry', () { + expect(() => index.findReuc('not-there'), throwsA(isA())); + expect(() => index.reucEntry('not-there'), throwsA(isA())); + expect( + () => bindings.reucGetByIndex(indexPointer: index.pointer, position: 0), + throwsA(isA()), + ); + }); + test('returns conflicts with ancestor, our and their present', () { final repoDir = setupRepo(Directory(mergeRepoPath)); final conflictRepo = Repository.open(repoDir.path); @@ -391,6 +495,36 @@ void main() { repoDir.deleteSync(recursive: true); }); + test('returns conflict by path', () { + final repoDir = setupRepo(Directory(mergeRepoPath)); + final conflictRepo = Repository.open(repoDir.path); + + conflictRepo.reset( + oid: conflictRepo.head.target, + resetType: GitReset.hard, + ); + + Merge.commit( + repo: conflictRepo, + commit: AnnotatedCommit.lookup( + repo: conflictRepo, + oid: + Branch.lookup(repo: conflictRepo, name: 'conflict-branch').target, + ), + ); + + final conflictedFile = conflictRepo.index.conflict('conflict_file'); + expect(conflictedFile.ancestor, isNull); + expect(conflictedFile.our?.path, 'conflict_file'); + expect(conflictedFile.their?.path, 'conflict_file'); + + repoDir.deleteSync(recursive: true); + }); + + test('throws when conflict path is not found', () { + expect(() => index.conflict('not-there'), throwsA(isA())); + }); + test('returns conflicts with ancestor and their present and null our', () { final repoDir = setupRepo(Directory(mergeRepoPath)); final conflictRepo = Repository.open(repoDir.path); diff --git a/test/libgit2_test.dart b/test/libgit2_test.dart index cb5f743..d824740 100644 --- a/test/libgit2_test.dart +++ b/test/libgit2_test.dart @@ -16,6 +16,44 @@ void main() { }); }); + test('returns prerelease and feature backend metadata', () { + expect(Libgit2.prerelease, isNull); + + for (final feature in Libgit2.features) { + expect(Libgit2.featureBackend(feature), isNotEmpty); + } + }); + + test('checks whether object type is loose', () { + expect(Libgit2.objectTypeIsLoose(GitObject.blob), true); + expect(Libgit2.objectTypeIsLoose(GitObject.refDelta), false); + }); + + test('checks whether paths are protected gitfile names', () { + expect( + Libgit2.isGitFile( + path: '.gitignore', + gitfile: GitPathGitFile.gitignore, + ), + true, + ); + expect( + Libgit2.isGitFile( + path: '.gitmodules', + gitfile: GitPathGitFile.gitmodules, + filesystem: GitPathFilesystem.ntfs, + ), + true, + ); + expect( + Libgit2.isGitFile( + path: 'README.md', + gitfile: GitPathGitFile.gitattributes, + ), + false, + ); + }); + test('sets and returns the owner validation setting for repository ' 'directories', () { final oldValue = Libgit2.ownerValidation; diff --git a/test/merge_test.dart b/test/merge_test.dart index d546d6d..a4a2b1c 100644 --- a/test/merge_test.dart +++ b/test/merge_test.dart @@ -87,6 +87,7 @@ void main() { expect(index.hasConflicts, true); expect(index.conflicts.length, 1); expect(repo.state, GitRepositoryState.merge); + expect(repo.mergeHeadOids, [conflictBranch.target.sha]); expect(repo.status, { 'conflict_file': {GitStatus.conflicted}, }); diff --git a/test/note_test.dart b/test/note_test.dart index d85872b..cd132f9 100644 --- a/test/note_test.dart +++ b/test/note_test.dart @@ -57,6 +57,10 @@ void main() { expect(note.oid.sha, notesExpected[1]['oid']); expect(note.message, notesExpected[1]['message']); expect(note.annotatedOid.sha, notesExpected[1]['annotatedOid']); + expect(note.author.name, 'Aleksey Kulikov'); + expect(note.author.email, 'skinny.mind@gmail.com'); + expect(note.committer.name, 'Aleksey Kulikov'); + expect(note.committer.email, 'skinny.mind@gmail.com'); }); test('creates note', () { diff --git a/test/odb_test.dart b/test/odb_test.dart index a003a04..ea0d22c 100644 --- a/test/odb_test.dart +++ b/test/odb_test.dart @@ -45,6 +45,11 @@ void main() { expect(odb.contains(repo[blobSha]), true); }); + test('returns backend count', () { + expect(repo.odb.backendCount, greaterThan(0)); + expect(Odb.create().backendCount, 0); + }); + test('reads object', () { final object = repo.odb.read(repo[blobSha]); @@ -68,6 +73,14 @@ void main() { expect(object.dataBytes, bytes); }); + test('duplicates object', () { + final object = repo.odb.read(repo[blobSha]); + final duplicate = object.duplicate(); + + expect(duplicate, object); + expect(duplicate.data, object.data); + }); + test('throws when trying to read object and error occurs', () { expect(() => repo.odb.read(repo['0' * 40]), throwsA(isA())); }); @@ -95,6 +108,26 @@ void main() { expect(object.data, 'testing'); }); + test('writes data directly', () { + final odb = repo.odb; + final oid = odb.writeDirect(type: GitObject.blob, data: 'direct'); + final object = odb.read(oid); + + expect(odb.contains(oid), true); + expect(object.data, 'direct'); + }); + + test('hashes data and files', () { + final file = File(p.join(repo.workdir, 'feature_file')) + ..writeAsStringSync(blobContent); + + expect(Odb.hash(type: GitObject.blob, data: blobContent), repo[blobSha]); + expect( + Odb.hashFile(type: GitObject.blob, path: file.path), + repo[blobSha], + ); + }); + test('throws when trying to write with invalid object type', () { expect( () => repo.odb.write(type: GitObject.any, data: 'testing'), @@ -102,6 +135,13 @@ void main() { ); }); + test('throws when trying to hash with invalid object type', () { + expect( + () => Odb.hash(type: GitObject.any, data: 'testing'), + throwsA(isA()), + ); + }); + test('throws when trying to write alternate odb to disk', () { final odb = Odb.create(); odb.addDiskAlternate(p.join(repo.path, 'objects')); diff --git a/test/oid_test.dart b/test/oid_test.dart index ae6910f..9d2910b 100644 --- a/test/oid_test.dart +++ b/test/oid_test.dart @@ -51,6 +51,20 @@ void main() { }); }); + group('fromSHAParse()', () { + test('initializes from partial hex string with zero-filled suffix', () { + final oid = Oid.fromSHAParse('78b8'); + expect(oid.sha, '78b8${'0' * 36}'); + }); + + test('throws when sha hex string is invalid', () { + expect( + () => Oid.fromSHAParse('not-a-sha'), + throwsA(isA()), + ); + }); + }); + group('fromRaw()', () { test('initializes successfully', () { final sourceOid = Oid.fromSHA(repo, sha); @@ -64,6 +78,7 @@ void main() { final oid = Oid.fromSHA(repo, sha); expect(oid.sha, equals(sha)); expect(oid.toStr(41), equals(sha)); + expect(oid.toStrN(7), equals(sha.substring(0, 7))); expect(oid.toStrS(), equals(sha)); expect(oid.equalsHex(sha), true); expect(oid.compareToHex(biggerSha), lessThan(0)); diff --git a/test/patch_test.dart b/test/patch_test.dart index 2111fdd..add80b9 100644 --- a/test/patch_test.dart +++ b/test/patch_test.dart @@ -66,6 +66,7 @@ index e69de29..0000000 expect(patch.size(), 14); expect(patch.text, blobPatch); + expect(patch.printedText, blobPatch); expect(patch.textBytes, utf8.encode(blobPatch)); }); @@ -102,6 +103,25 @@ index e69de29..0000000 expect(patch.text, blobPatch); }); + test('creates from blobs with separate path names', () { + final patch = Patch.fromBlobs( + oldBlob: Blob.lookup(repo: repo, oid: oldBlobOid), + newBlob: Blob.lookup(repo: repo, oid: newBlobOid), + oldBlobPath: 'old_feature_file', + newBlobPath: 'new_feature_file', + ); + + expect( + patch.text, + contains( + 'diff --git a/old_feature_file ' + 'b/new_feature_file', + ), + ); + expect(patch.text, contains('--- a/old_feature_file')); + expect(patch.text, contains('+++ b/new_feature_file')); + }); + test('creates from one blob (add)', () { final patch = Patch.fromBlobs( oldBlob: null, @@ -135,6 +155,25 @@ index e69de29..0000000 expect(patch.text, blobPatch); }); + test('creates from blob and buffer with separate path names', () { + final patch = Patch.fromBlobAndBuffer( + blob: Blob.lookup(repo: repo, oid: oldBlobOid), + buffer: newBuffer, + blobPath: 'old_feature_file', + bufferPath: 'new_feature_file', + ); + + expect( + patch.text, + contains( + 'diff --git a/old_feature_file ' + 'b/new_feature_file', + ), + ); + expect(patch.text, contains('--- a/old_feature_file')); + expect(patch.text, contains('+++ b/new_feature_file')); + }); + test('creates from empty blob and buffer', () { final patch = Patch.fromBlobAndBuffer( blob: null, diff --git a/test/reference_test.dart b/test/reference_test.dart index 16a0472..7c02686 100644 --- a/test/reference_test.dart +++ b/test/reference_test.dart @@ -13,6 +13,7 @@ void main() { late Directory tmpDir; const lastCommit = '821ed6e80627b8769d170a293862f9fc60825226'; const newCommit = 'c68ff54aabf660fcdd9a2838d401583fe31249e3'; + const featureCommit = '5aecfa0fb97eadaac050ccb99f03c3fb65460ad4'; setUp(() { tmpDir = setupRepo(Directory(p.join('test', 'assets', 'test_repo'))); @@ -37,6 +38,28 @@ void main() { ]); }); + test('returns a list with callback iterators', () { + final expected = Reference.list(repo); + + expect(Reference.listForeach(repo), unorderedEquals(expected)); + expect(Reference.listForeachName(repo), unorderedEquals(expected)); + expect( + Reference.listForeachGlob(repo: repo, glob: 'refs/tags/*'), + unorderedEquals(['refs/tags/v0.1', 'refs/tags/v0.2']), + ); + }); + + test('returns a list with reference iterators', () { + final expected = Reference.list(repo); + + expect(Reference.listIterator(repo), unorderedEquals(expected)); + expect(Reference.listIteratorNames(repo), unorderedEquals(expected)); + expect( + Reference.listIteratorGlob(repo: repo, glob: 'refs/tags/*'), + unorderedEquals(['refs/tags/v0.1', 'refs/tags/v0.2']), + ); + }); + test('throws when trying to get a list of references and error occurs', () { expect( () => Reference.list(Repository(nullptr)), @@ -52,6 +75,15 @@ void main() { ); }); + test('compares references', () { + final feature = Reference.lookup(repo: repo, name: 'refs/heads/feature'); + final master = Reference.lookup(repo: repo, name: 'refs/heads/master'); + + expect(feature.compareTo(master), lessThan(0)); + expect(master.compareTo(feature), greaterThan(0)); + expect(master.compareTo(master), 0); + }); + test('returns SHA hex of direct reference', () { expect(repo.head.target.sha, lastCommit); }); @@ -60,6 +92,27 @@ void main() { expect(Reference.lookup(repo: repo, name: 'HEAD').target.sha, lastCommit); }); + test('returns peeled target for annotated tag reference', () { + final signature = Signature.create( + name: 'Tagger', + email: 'tagger@example.com', + time: 1234, + ); + Tag.createAnnotated( + repo: repo, + tagName: 'peeled', + target: repo[lastCommit], + targetType: GitObject.commit, + tagger: signature, + message: 'message', + ); + Reference.compress(repo); + + final tagRef = Reference.lookup(repo: repo, name: 'refs/tags/peeled'); + expect(tagRef.peeledTarget?.sha, lastCommit); + expect(repo.head.peeledTarget, isNull); + }); + test('returns the full name', () { expect(repo.head.name, 'refs/heads/master'); }); @@ -236,6 +289,43 @@ void main() { throwsA(isA()), ); }); + + test('creates matching reference when current target matches', () { + Reference.create( + repo: repo, + name: 'refs/tags/matching', + target: repo[lastCommit], + ); + + final ref = Reference.createMatching( + repo: repo, + name: 'refs/tags/matching', + target: repo[newCommit], + currentTarget: repo[lastCommit], + force: true, + ); + + expect(ref.target.sha, newCommit); + }); + + test('throws when matching reference current target differs', () { + Reference.create( + repo: repo, + name: 'refs/tags/matching', + target: repo[lastCommit], + ); + + expect( + () => Reference.createMatching( + repo: repo, + name: 'refs/tags/matching', + target: repo[newCommit], + currentTarget: repo[newCommit], + force: true, + ), + throwsA(isA()), + ); + }); }); group('create symbolic', () { @@ -312,6 +402,59 @@ void main() { expect(reflogEntry.committer.name, 'name'); expect(reflogEntry.committer.email, 'email'); }); + + test('creates matching reference when current target matches', () { + Reference.create( + repo: repo, + name: 'refs/tags/symbolic.matching', + target: 'refs/heads/master', + ); + + final ref = Reference.createMatching( + repo: repo, + name: 'refs/tags/symbolic.matching', + target: 'refs/heads/feature', + currentTarget: 'refs/heads/master', + force: true, + ); + + expect(ref.type, ReferenceType.symbolic); + expect(ref.target.sha, featureCommit); + }); + + test( + 'throws when matching symbolic reference current target differs', + () { + Reference.create( + repo: repo, + name: 'refs/tags/symbolic.matching', + target: 'refs/heads/master', + ); + + expect( + () => Reference.createMatching( + repo: repo, + name: 'refs/tags/symbolic.matching', + target: 'refs/heads/feature', + currentTarget: 'refs/heads/feature', + force: true, + ), + throwsA(isA()), + ); + }, + ); + + test('throws when matching target types differ', () { + expect( + () => Reference.createMatching( + repo: repo, + name: 'refs/tags/symbolic.matching', + target: 'refs/heads/feature', + currentTarget: repo.head.target, + ), + throwsA(isA()), + ); + }); }); test('deletes reference', () { @@ -320,6 +463,18 @@ void main() { expect(repo.references, isNot(contains('refs/tags/v0.1'))); }); + test('removes reference by name', () { + Reference.create( + repo: repo, + name: 'refs/tags/test', + target: repo[lastCommit], + ); + + expect(repo.references, contains('refs/tags/test')); + Reference.remove(repo: repo, name: 'refs/tags/test'); + expect(repo.references, isNot(contains('refs/tags/test'))); + }); + group('finds', () { test('with provided name', () { final ref = Reference.lookup(repo: repo, name: 'refs/heads/master'); diff --git a/test/remote_test.dart b/test/remote_test.dart index 24cd759..6495c38 100644 --- a/test/remote_test.dart +++ b/test/remote_test.dart @@ -35,6 +35,7 @@ void main() { expect(remote.name, remoteName); expect(remote.url, remoteUrl); expect(remote.pushUrl, ''); + expect(remote.autotag, GitRemoteAutotag.auto); expect(remote.toString(), contains('Remote{')); expect(remote, equals(Remote.lookup(repo: repo, name: 'origin'))); }); @@ -170,6 +171,38 @@ void main() { expect(Remote.lookup(repo: repo, name: remoteName).pushUrl, newUrl); }); + test('sets instance urls without changing configuration', () { + final remote = Remote.lookup(repo: repo, name: remoteName); + const instanceUrl = 'git://instance/url.git'; + const instancePushUrl = 'git://instance/push-url.git'; + + remote.url = instanceUrl; + remote.pushUrl = instancePushUrl; + + expect(remote.url, instanceUrl); + expect(remote.pushUrl, instancePushUrl); + expect(Remote.lookup(repo: repo, name: remoteName).url, remoteUrl); + expect(Remote.lookup(repo: repo, name: remoteName).pushUrl, ''); + }); + + test('sets automatic tag following option', () { + expect( + Remote.lookup(repo: repo, name: remoteName).autotag, + GitRemoteAutotag.auto, + ); + + Remote.setAutotag( + repo: repo, + remote: remoteName, + value: GitRemoteAutotag.none, + ); + + expect( + Remote.lookup(repo: repo, name: remoteName).autotag, + GitRemoteAutotag.none, + ); + }); + test('throws when trying to set invalid push url name', () { expect( () => Remote.setPushUrl(repo: repo, remote: 'origin', url: ''), diff --git a/test/repository_test.dart b/test/repository_test.dart index 77dc74f..5b133a7 100644 --- a/test/repository_test.dart +++ b/test/repository_test.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/status.dart' as status_bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -41,6 +42,36 @@ void main() { expect(bare.isBare, true); }); + test('initializes repository with basic initializer', () { + final repoPath = p.join(tmpDir.path, 'basic_repo'); + final initialized = Repository.initBasic(path: repoPath); + + expect(initialized.isBare, false); + expect(Directory(p.join(repoPath, '.git')).existsSync(), true); + }); + + test('opens repository with extended search options', () { + final nestedPath = p.join(tmpDir.path, 'dir'); + final opened = Repository.openExt(path: nestedPath); + + expect(opened.path, repo.path); + expect(GitRepositoryOpen.fromValue(1), GitRepositoryOpen.noSearch); + expect( + () => GitRepositoryOpen.fromValue(0), + throwsA(isA()), + ); + }); + + test('throws when extended open cannot find repository', () { + expect( + () => Repository.openExt( + path: p.join(tmpDir.path, 'dir'), + flags: {GitRepositoryOpen.noSearch}, + ), + throwsA(isA()), + ); + }); + test('returns snapshot of repository config', () { expect( repo.configSnapshot['remote.origin.url'].value, @@ -48,6 +79,27 @@ void main() { ); }); + test('sets repository config, index and odb objects', () { + final config = repo.config; + final index = repo.index; + final odb = repo.odb; + + expect(() => repo.setConfig(config), returnsNormally); + expect(() => repo.setIndex(index), returnsNormally); + expect(() => repo.setOdb(odb), returnsNormally); + }); + + test('throws when setting repository config, index and odb fails', () { + final invalid = Repository(nullptr); + + expect( + () => invalid.setConfig(repo.config), + throwsA(isA()), + ); + expect(() => invalid.setIndex(repo.index), throwsA(isA())); + expect(() => invalid.setOdb(repo.odb), throwsA(isA())); + }); + test('returns list of commits by walking from provided starting oid', () { const log = [ '821ed6e80627b8769d170a293862f9fc60825226', @@ -169,6 +221,30 @@ void main() { expect(repo.isHeadDetached, true); }); + test('detaches head from annotated commit', () { + final annotated = AnnotatedCommit.lookup( + repo: repo, + oid: repo[featureCommit], + ); + + repo.setHeadDetachedFromAnnotated(annotated); + + expect(repo.isHeadDetached, true); + expect(repo.head.target, repo[featureCommit]); + }); + + test('throws when detaching head from annotated commit fails', () { + final annotated = AnnotatedCommit.lookup( + repo: repo, + oid: repo.head.target, + ); + + expect( + () => Repository(nullptr).setHeadDetachedFromAnnotated(annotated), + throwsA(isA()), + ); + }); + test('returns repository oid type and hashes a file', () { final oid = repo.hashFile(path: p.join(repo.workdir, 'file')); @@ -186,6 +262,37 @@ void main() { }); }); + test('returns status entries through callback APIs', () { + File(p.join(tmpDir.path, 'new_file.txt')).createSync(); + repo.index.remove('file'); + repo.index.add('new_file.txt'); + + final foreachStatus = status_bindings.foreach(repo.pointer); + final foreachExtStatus = status_bindings.foreachExt(repo.pointer); + + expect(foreachStatus.keys, containsAll(['file', 'new_file.txt'])); + expect(foreachExtStatus.keys, containsAll(['file', 'new_file.txt'])); + expect( + foreachStatus['file']! & GitStatus.indexDeleted.value, + GitStatus.indexDeleted.value, + ); + expect( + foreachExtStatus['new_file.txt']! & GitStatus.indexNew.value, + GitStatus.indexNew.value, + ); + }); + + test('throws when status callback APIs fail', () { + expect( + () => status_bindings.foreach(Repository(nullptr).pointer), + throwsA(isA()), + ); + expect( + () => status_bindings.foreachExt(Repository(nullptr).pointer), + throwsA(isA()), + ); + }); + test('throws when trying to get status of bare repository', () { final bare = Repository.open(p.join('test', 'assets', 'empty_bare.git')); @@ -204,6 +311,11 @@ void main() { expect(repo.state, GitRepositoryState.none); }); + test('returns empty fetch and merge head lists when files are absent', () { + expect(repo.fetchHeadEntries, isEmpty); + expect(repo.mergeHeadOids, isEmpty); + }); + test('throws when trying to clean up state and error occurs', () { expect( () => Repository(nullptr).stateCleanup(), diff --git a/test/revwalk_test.dart b/test/revwalk_test.dart index 0aece1b..a2192b5 100644 --- a/test/revwalk_test.dart +++ b/test/revwalk_test.dart @@ -199,6 +199,16 @@ void main() { ); }); + test('hides commits matching predicate', () { + final walker = RevWalk(repo); + + walker.push(repo[log.first]); + walker.hideWhere((oid) => oid.sha == log[2]); + final commits = walker.walk(); + + expect(commits.map((e) => e.oid.sha), isNot(contains(log[2]))); + }); + test('resets walker', () { final walker = RevWalk(repo); diff --git a/test/signature_test.dart b/test/signature_test.dart index fad17df..56f7d56 100644 --- a/test/signature_test.dart +++ b/test/signature_test.dart @@ -1,7 +1,13 @@ +import 'dart:ffi'; +import 'dart:io'; + import 'package:git2dart/git2dart.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; +import 'helpers/util.dart'; + void main() { late Signature signature; const name = 'Some Name'; @@ -46,6 +52,34 @@ void main() { expect(sig.sign, isNotEmpty); }); + test( + 'creates default author and committer signatures from environment', + () { + final tmpDir = setupRepo( + Directory(p.join('test', 'assets', 'test_repo')), + ); + final repo = Repository.open(tmpDir.path); + repo.config['user.name'] = 'Config User'; + repo.config['user.email'] = 'config@example.com'; + + final signatures = Signature.defaultSignaturesFromEnv(repo); + + expect(signatures, hasLength(2)); + expect(signatures[0].name, 'Config User'); + expect(signatures[0].email, 'config@example.com'); + expect(signatures[1].name, 'Config User'); + expect(signatures[1].email, 'config@example.com'); + tmpDir.deleteSync(recursive: true); + }, + ); + + test('throws when default signatures cannot be found', () { + expect( + () => Signature.defaultSignaturesFromEnv(Repository(nullptr)), + throwsA(isA()), + ); + }); + test('returns correct values', () { expect(signature.name, name); expect(signature.email, email); diff --git a/test/submodule_test.dart b/test/submodule_test.dart index 0f51cf9..f8af9a8 100644 --- a/test/submodule_test.dart +++ b/test/submodule_test.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/submodule.dart' as submodule_bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -46,6 +47,12 @@ void main() { expect(submodule.workdirOid?.sha, null); expect(submodule.ignoreRule, GitSubmoduleIgnore.none); expect(submodule.updateRule, GitSubmoduleUpdate.checkout); + expect(submodule.fetchRecurseSubmodules, GitSubmoduleRecurse.no); + expect(submodule.location, { + GitSubmoduleStatus.inHead, + GitSubmoduleStatus.inIndex, + GitSubmoduleStatus.inConfig, + }); expect(submodule.toString(), contains('Submodule{')); }); @@ -161,11 +168,13 @@ void main() { expect(submodule.branch, ''); expect(submodule.ignoreRule, GitSubmoduleIgnore.none); expect(submodule.updateRule, GitSubmoduleUpdate.checkout); + expect(submodule.fetchRecurseSubmodules, GitSubmoduleRecurse.no); submodule.url = 'updated'; submodule.branch = 'updated'; submodule.ignoreRule = GitSubmoduleIgnore.all; submodule.updateRule = GitSubmoduleUpdate.rebase; + submodule.fetchRecurseSubmodules = GitSubmoduleRecurse.onDemand; final updatedSubmodule = Submodule.lookup( repo: repo, @@ -175,6 +184,26 @@ void main() { expect(updatedSubmodule.branch, 'updated'); expect(updatedSubmodule.ignoreRule, GitSubmoduleIgnore.all); expect(updatedSubmodule.updateRule, GitSubmoduleUpdate.rebase); + expect( + updatedSubmodule.fetchRecurseSubmodules, + GitSubmoduleRecurse.onDemand, + ); + }); + + test('throws when submodule fetch recurse and location bindings fail', () { + expect( + () => submodule_bindings.setFetchRecurseSubmodules( + repoPointer: Repository(nullptr).pointer, + name: testSubmodule, + fetchRecurseSubmodules: + git_submodule_recurse_t.GIT_SUBMODULE_RECURSE_YES, + ), + throwsA(isA()), + ); + expect( + () => submodule_bindings.location(nullptr), + throwsA(isA()), + ); }); test('syncs', tags: 'remote_fetch', () { diff --git a/test/tag_test.dart b/test/tag_test.dart index b963c3a..99db798 100644 --- a/test/tag_test.dart +++ b/test/tag_test.dart @@ -37,6 +37,16 @@ void main() { ); }); + test('lookups tag from oid prefix', () { + final prefixTag = Tag.lookupPrefix(repo: repo, oid: tagOid, length: 7); + + expect(prefixTag.oid, tagOid); + expect( + () => Tag.lookupPrefix(repo: repo, oid: repo['0' * 40], length: 7), + throwsA(isA()), + ); + }); + test('returns correct values', () { final signature = Signature.create( name: 'Aleksey Kulikov', @@ -55,6 +65,22 @@ void main() { expect(tag.toString(), contains('Tag{')); }); + test('peels tag to target object', () { + final peeled = tag.peel(GitObject.commit); + + expect(peeled, isA()); + expect((peeled as Commit).oid, tag.targetOid); + }); + + test('throws when peeling tag to unavailable type', () { + expect(() => tag.peel(GitObject.blob), throwsA(isA())); + }); + + test('checks if tag name is valid', () { + expect(Tag.isNameValid('v1.0.0'), true); + expect(Tag.isNameValid('v1..0'), false); + }); + test('creates new annotated tag with commit as target', () { final signature = Signature.create( name: 'Author', @@ -86,6 +112,33 @@ void main() { expect(newTagTarget.oid, target); }); + test('creates new annotated tag object without reference', () { + final signature = Signature.create( + name: 'Author', + email: 'author@email.com', + time: 1234, + ); + const tagName = 'annotation-only'; + const targetSHA = 'f17d0d48eae3aa08cecf29128a35e310c97b3521'; + final target = repo[targetSHA]; + const message = 'annotation only tag\n'; + + final oid = Tag.createAnnotation( + repo: repo, + tagName: tagName, + target: target, + targetType: GitObject.commit, + tagger: signature, + message: message, + ); + final annotation = Tag.lookup(repo: repo, oid: oid); + + expect(annotation.name, tagName); + expect(annotation.message, message); + expect(annotation.targetOid.sha, targetSHA); + expect(Tag.list(repo: repo), isNot(contains(tagName))); + }); + test('creates new lightweight tag with commit as target', () { const tagName = 'tag'; final target = repo['f17d0d48eae3aa08cecf29128a35e310c97b3521']; @@ -124,12 +177,14 @@ void main() { final newTag = Tag.lookup(repo: repo, oid: oid); final newTagTarget = newTag.target as Tree; + final peeledTree = newTag.peel(GitObject.tree) as Tree; expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(newTag.tagger, signature); expect(newTagTarget.oid, target); + expect(peeledTree.oid, target); }); test('creates new lightweight tag with tree as target', () { @@ -170,12 +225,14 @@ void main() { final newTag = Tag.lookup(repo: repo, oid: oid); final newTagTarget = newTag.target as Blob; + final peeledBlob = newTag.peel(GitObject.blob) as Blob; expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(newTag.tagger, signature); expect(newTagTarget.oid, target); + expect(peeledBlob.oid, target); }); test('creates new lightweight tag with blob as target', () { @@ -215,12 +272,14 @@ void main() { final newTag = Tag.lookup(repo: repo, oid: oid); final newTagTarget = newTag.target as Tag; + final peeledCommit = newTag.peel() as Commit; expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(newTag.tagger, signature); expect(newTagTarget.oid, tag.oid); + expect(peeledCommit.oid, tag.targetOid); }); test('creates new lightweight tag with tag as target', () { diff --git a/test/tree_test.dart b/test/tree_test.dart index 429a947..bb5fd2a 100644 --- a/test/tree_test.dart +++ b/test/tree_test.dart @@ -36,6 +36,20 @@ void main() { ); }); + test('lookups tree from oid prefix', () { + final prefixTree = Tree.lookupPrefix( + repo: repo, + oid: tree.oid, + length: 7, + ); + + expect(prefixTree.oid, tree.oid); + expect( + () => Tree.lookupPrefix(repo: repo, oid: repo['0' * 40], length: 7), + throwsA(isA()), + ); + }); + test('returns correct values', () { expect(tree.length, 4); expect(tree.entries.first.oid.sha, fileSHA); @@ -64,6 +78,24 @@ void main() { expect(entry.type, GitObject.blob); }); + test('converts tree entry to object', () { + final entry = tree['.gitignore']; + final object = entry.toObject(repo); + + expect(object, isA()); + expect((object as Blob).oid, entry.oid); + + final dirEntry = tree['dir']; + final dirObject = dirEntry.toObject(repo); + expect(dirObject, isA()); + expect((dirObject as Tree).oid, dirEntry.oid); + }); + + test('compares tree entries', () { + expect(tree[0].compareTo(tree[0]), 0); + expect(tree[0].compareTo(tree[1]), lessThan(0)); + }); + test('throws when nothing found for provided filename', () { expect(() => tree['invalid'], throwsA(isA())); }); @@ -74,6 +106,26 @@ void main() { expect(entry.toString(), contains('TreeEntry{')); }); + test('walks tree entries', () { + expect(GitTreeWalk.fromValue(0), GitTreeWalk.pre); + expect(GitTreeWalk.fromValue(1), GitTreeWalk.post); + expect(() => GitTreeWalk.fromValue(-1), throwsA(isA())); + expect(tree.walk(), [ + '.gitignore', + 'dir', + 'dir/dir_file.txt', + 'feature_file', + 'file', + ]); + expect(tree.walk(mode: GitTreeWalk.post), [ + '.gitignore', + 'dir/dir_file.txt', + 'dir', + 'feature_file', + 'file', + ]); + }); + test('throws when nothing found for provided path', () { expect(() => tree['invalid/path'], throwsA(isA())); }); @@ -100,6 +152,41 @@ void main() { expect(entry.oid, fileOid); }); + test('creates updated tree from baseline', () { + final fileOid = Blob.create(repo: repo, content: 'updated content'); + expect( + const TreeUpdate.remove('feature_file'), + const TreeUpdate.remove('feature_file'), + ); + final updatedTree = Tree.createUpdated( + repo: repo, + baseline: tree, + updates: [ + TreeUpdate.upsert( + path: 'updated.txt', + oid: fileOid, + filemode: GitFilemode.blob, + ), + const TreeUpdate.remove('feature_file'), + ], + ); + + expect(updatedTree['updated.txt'].oid, fileOid); + expect(() => updatedTree['feature_file'], throwsA(isA())); + expect(updatedTree.length, tree.length); + }); + + test('throws when creating updated tree fails', () { + expect( + () => Tree.createUpdated( + repo: repo, + baseline: tree, + updates: const [TreeUpdate.remove('not-there')], + ), + throwsA(isA()), + ); + }); + test('manually releases allocated memory', () { final tree = Tree.lookup(repo: repo, oid: repo['a8ae3dd']); expect(() => tree.free(), returnsNormally); diff --git a/test/worktree_test.dart b/test/worktree_test.dart index 5476a11..f236b6c 100644 --- a/test/worktree_test.dart +++ b/test/worktree_test.dart @@ -98,6 +98,25 @@ void main() { expect(worktree.isLocked, false); }); + test('returns head and detached state for linked worktree', () { + Worktree.create(repo: repo, name: worktreeName, path: worktreeDir.path); + + final head = repo.headForWorktree(worktreeName); + expect(head.name, 'refs/heads/$worktreeName'); + expect(repo.isHeadDetachedForWorktree(worktreeName), isA()); + }); + + test('throws when reading head for unknown worktree', () { + expect( + () => repo.headForWorktree('not-there'), + throwsA(isA()), + ); + expect( + () => repo.isHeadDetachedForWorktree('not-there'), + throwsA(isA()), + ); + }); + test('throws when trying to lookup and error occurs', () { expect( () => Worktree.lookup(repo: Repository(nullptr), name: 'name'), From ef53b48970ca2f28c3a1cc05003be5fe336af38c Mon Sep 17 00:00:00 2001 From: Viktor Borisov Date: Mon, 29 Jun 2026 16:18:32 +0700 Subject: [PATCH 04/10] chore: include bindings in coverage --- lib/src/bindings/annotated.dart | 1 - lib/src/bindings/attr.dart | 1 - lib/src/bindings/blame.dart | 1 - lib/src/bindings/blob.dart | 1 - lib/src/bindings/branch.dart | 1 - lib/src/bindings/certificate.dart | 1 - lib/src/bindings/checkout.dart | 1 - lib/src/bindings/commit.dart | 1 - lib/src/bindings/commit_graph.dart | 1 - lib/src/bindings/config.dart | 1 - lib/src/bindings/credentials.dart | 1 - lib/src/bindings/describe.dart | 1 - lib/src/bindings/diff.dart | 1 - lib/src/bindings/filter.dart | 1 - lib/src/bindings/graph.dart | 1 - lib/src/bindings/ignore.dart | 1 - lib/src/bindings/index.dart | 1 - lib/src/bindings/mailmap.dart | 1 - lib/src/bindings/merge.dart | 1 - lib/src/bindings/message.dart | 1 - lib/src/bindings/note.dart | 1 - lib/src/bindings/object.dart | 1 - lib/src/bindings/odb.dart | 1 - lib/src/bindings/oid.dart | 1 - lib/src/bindings/packbuilder.dart | 1 - lib/src/bindings/patch.dart | 1 - lib/src/bindings/pathspec.dart | 1 - lib/src/bindings/rebase.dart | 1 - lib/src/bindings/refdb.dart | 1 - lib/src/bindings/reference.dart | 1 - lib/src/bindings/reflog.dart | 1 - lib/src/bindings/refspec.dart | 1 - lib/src/bindings/remote.dart | 1 - lib/src/bindings/remote_callbacks.dart | 1 - lib/src/bindings/repository.dart | 1 - lib/src/bindings/reset.dart | 1 - lib/src/bindings/revparse.dart | 1 - lib/src/bindings/revwalk.dart | 1 - lib/src/bindings/signature.dart | 1 - lib/src/bindings/stash.dart | 1 - lib/src/bindings/status.dart | 1 - lib/src/bindings/submodule.dart | 1 - lib/src/bindings/tag.dart | 1 - lib/src/bindings/tree.dart | 1 - lib/src/bindings/treebuilder.dart | 1 - lib/src/bindings/worktree.dart | 1 - 46 files changed, 46 deletions(-) diff --git a/lib/src/bindings/annotated.dart b/lib/src/bindings/annotated.dart index 1e02bcc..bc2327e 100644 --- a/lib/src/bindings/annotated.dart +++ b/lib/src/bindings/annotated.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/attr.dart b/lib/src/bindings/attr.dart index d2d5df5..71830d2 100644 --- a/lib/src/bindings/attr.dart +++ b/lib/src/bindings/attr.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/blame.dart b/lib/src/bindings/blame.dart index aace41b..c2a61c3 100644 --- a/lib/src/bindings/blame.dart +++ b/lib/src/bindings/blame.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/blob.dart b/lib/src/bindings/blob.dart index f9e4319..af829eb 100644 --- a/lib/src/bindings/blob.dart +++ b/lib/src/bindings/blob.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'dart:typed_data'; diff --git a/lib/src/bindings/branch.dart b/lib/src/bindings/branch.dart index ec8a383..94fb860 100644 --- a/lib/src/bindings/branch.dart +++ b/lib/src/bindings/branch.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/certificate.dart b/lib/src/bindings/certificate.dart index 47cb1cb..55f76c4 100644 --- a/lib/src/bindings/certificate.dart +++ b/lib/src/bindings/certificate.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'dart:typed_data'; diff --git a/lib/src/bindings/checkout.dart b/lib/src/bindings/checkout.dart index 9ff89d2..a557cd7 100644 --- a/lib/src/bindings/checkout.dart +++ b/lib/src/bindings/checkout.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/commit.dart b/lib/src/bindings/commit.dart index ec5c39d..30706d6 100644 --- a/lib/src/bindings/commit.dart +++ b/lib/src/bindings/commit.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/commit_graph.dart b/lib/src/bindings/commit_graph.dart index 20e9f66..5482690 100644 --- a/lib/src/bindings/commit_graph.dart +++ b/lib/src/bindings/commit_graph.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/config.dart b/lib/src/bindings/config.dart index f395cf6..904b7ac 100644 --- a/lib/src/bindings/config.dart +++ b/lib/src/bindings/config.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/credentials.dart b/lib/src/bindings/credentials.dart index 74275ce..02780b8 100644 --- a/lib/src/bindings/credentials.dart +++ b/lib/src/bindings/credentials.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/describe.dart b/lib/src/bindings/describe.dart index 46d8989..6b13673 100644 --- a/lib/src/bindings/describe.dart +++ b/lib/src/bindings/describe.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/diff.dart b/lib/src/bindings/diff.dart index 52ee371..c3f043b 100644 --- a/lib/src/bindings/diff.dart +++ b/lib/src/bindings/diff.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/filter.dart b/lib/src/bindings/filter.dart index ec03d0a..a29f931 100644 --- a/lib/src/bindings/filter.dart +++ b/lib/src/bindings/filter.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/graph.dart b/lib/src/bindings/graph.dart index 27b86d4..399ca49 100644 --- a/lib/src/bindings/graph.dart +++ b/lib/src/bindings/graph.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/ignore.dart b/lib/src/bindings/ignore.dart index 18dab13..7c4328c 100644 --- a/lib/src/bindings/ignore.dart +++ b/lib/src/bindings/ignore.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/index.dart b/lib/src/bindings/index.dart index a511030..fbe329c 100644 --- a/lib/src/bindings/index.dart +++ b/lib/src/bindings/index.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/mailmap.dart b/lib/src/bindings/mailmap.dart index b077d5c..f7d5a1d 100644 --- a/lib/src/bindings/mailmap.dart +++ b/lib/src/bindings/mailmap.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; // ignore_for_file: lines_longer_than_80_chars import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/merge.dart b/lib/src/bindings/merge.dart index 34ef213..2067b91 100644 --- a/lib/src/bindings/merge.dart +++ b/lib/src/bindings/merge.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/message.dart b/lib/src/bindings/message.dart index ab5b737..eb99048 100644 --- a/lib/src/bindings/message.dart +++ b/lib/src/bindings/message.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/note.dart b/lib/src/bindings/note.dart index 92a50e0..f411814 100644 --- a/lib/src/bindings/note.dart +++ b/lib/src/bindings/note.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/object.dart b/lib/src/bindings/object.dart index 7b4339c..6a480c4 100644 --- a/lib/src/bindings/object.dart +++ b/lib/src/bindings/object.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/odb.dart b/lib/src/bindings/odb.dart index e71c52e..e2bf4e6 100644 --- a/lib/src/bindings/odb.dart +++ b/lib/src/bindings/odb.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:convert'; import 'dart:ffi'; import 'dart:typed_data'; diff --git a/lib/src/bindings/oid.dart b/lib/src/bindings/oid.dart index 3c0c513..b8debc6 100644 --- a/lib/src/bindings/oid.dart +++ b/lib/src/bindings/oid.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/packbuilder.dart b/lib/src/bindings/packbuilder.dart index e921f58..10fc3f5 100644 --- a/lib/src/bindings/packbuilder.dart +++ b/lib/src/bindings/packbuilder.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'dart:typed_data'; diff --git a/lib/src/bindings/patch.dart b/lib/src/bindings/patch.dart index d9a33da..df7b320 100644 --- a/lib/src/bindings/patch.dart +++ b/lib/src/bindings/patch.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'dart:typed_data'; diff --git a/lib/src/bindings/pathspec.dart b/lib/src/bindings/pathspec.dart index 0145a7d..599cc7e 100644 --- a/lib/src/bindings/pathspec.dart +++ b/lib/src/bindings/pathspec.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/rebase.dart b/lib/src/bindings/rebase.dart index ef7a015..b975958 100644 --- a/lib/src/bindings/rebase.dart +++ b/lib/src/bindings/rebase.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/refdb.dart b/lib/src/bindings/refdb.dart index 3a53702..d5ca1f3 100644 --- a/lib/src/bindings/refdb.dart +++ b/lib/src/bindings/refdb.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/reference.dart b/lib/src/bindings/reference.dart index 34693e4..2a3467f 100644 --- a/lib/src/bindings/reference.dart +++ b/lib/src/bindings/reference.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/reflog.dart b/lib/src/bindings/reflog.dart index e96f0ca..5d44a69 100644 --- a/lib/src/bindings/reflog.dart +++ b/lib/src/bindings/reflog.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; diff --git a/lib/src/bindings/refspec.dart b/lib/src/bindings/refspec.dart index 33a0611..f9ef63d 100644 --- a/lib/src/bindings/refspec.dart +++ b/lib/src/bindings/refspec.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; diff --git a/lib/src/bindings/remote.dart b/lib/src/bindings/remote.dart index 89a751d..4132624 100644 --- a/lib/src/bindings/remote.dart +++ b/lib/src/bindings/remote.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'dart:ffi' as ffi; diff --git a/lib/src/bindings/remote_callbacks.dart b/lib/src/bindings/remote_callbacks.dart index 1de6a20..4a910ce 100644 --- a/lib/src/bindings/remote_callbacks.dart +++ b/lib/src/bindings/remote_callbacks.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show Arena, calloc; diff --git a/lib/src/bindings/repository.dart b/lib/src/bindings/repository.dart index ec82805..95c489a 100644 --- a/lib/src/bindings/repository.dart +++ b/lib/src/bindings/repository.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show calloc, using; diff --git a/lib/src/bindings/reset.dart b/lib/src/bindings/reset.dart index e264c6b..5ba6284 100644 --- a/lib/src/bindings/reset.dart +++ b/lib/src/bindings/reset.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; diff --git a/lib/src/bindings/revparse.dart b/lib/src/bindings/revparse.dart index ec7c01c..b4ccd5a 100644 --- a/lib/src/bindings/revparse.dart +++ b/lib/src/bindings/revparse.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show calloc, using; diff --git a/lib/src/bindings/revwalk.dart b/lib/src/bindings/revwalk.dart index 3a5bd90..27ae507 100644 --- a/lib/src/bindings/revwalk.dart +++ b/lib/src/bindings/revwalk.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; diff --git a/lib/src/bindings/signature.dart b/lib/src/bindings/signature.dart index f29e506..757a53b 100644 --- a/lib/src/bindings/signature.dart +++ b/lib/src/bindings/signature.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/stash.dart b/lib/src/bindings/stash.dart index be3fe3e..f0c769c 100644 --- a/lib/src/bindings/stash.dart +++ b/lib/src/bindings/stash.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show Arena, calloc, using; diff --git a/lib/src/bindings/status.dart b/lib/src/bindings/status.dart index ccdb3ae..f78991c 100644 --- a/lib/src/bindings/status.dart +++ b/lib/src/bindings/status.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show Utf8, Utf8Pointer, using; diff --git a/lib/src/bindings/submodule.dart b/lib/src/bindings/submodule.dart index 1728b8b..0a0a38a 100644 --- a/lib/src/bindings/submodule.dart +++ b/lib/src/bindings/submodule.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; diff --git a/lib/src/bindings/tag.dart b/lib/src/bindings/tag.dart index e91b479..de51938 100644 --- a/lib/src/bindings/tag.dart +++ b/lib/src/bindings/tag.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show calloc, using; diff --git a/lib/src/bindings/tree.dart b/lib/src/bindings/tree.dart index 42be8b1..5c578fb 100644 --- a/lib/src/bindings/tree.dart +++ b/lib/src/bindings/tree.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show Utf8, Utf8Pointer, calloc, using; diff --git a/lib/src/bindings/treebuilder.dart b/lib/src/bindings/treebuilder.dart index 7c396e4..a851590 100644 --- a/lib/src/bindings/treebuilder.dart +++ b/lib/src/bindings/treebuilder.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart'; diff --git a/lib/src/bindings/worktree.dart b/lib/src/bindings/worktree.dart index cfe3c79..381b1bd 100644 --- a/lib/src/bindings/worktree.dart +++ b/lib/src/bindings/worktree.dart @@ -1,4 +1,3 @@ -// coverage:ignore-file import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; From ded1bbb8643920eef0239067f1293618b6011e5c Mon Sep 17 00:00:00 2001 From: Viktor Borisov Date: Mon, 29 Jun 2026 16:31:04 +0700 Subject: [PATCH 05/10] chore: prepare 0.5.2 release --- CHANGELOG.md | 14 ++++++++++++++ pubspec.yaml | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6843f..44a95e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,18 @@ # Changelog +## [0.5.2] - 2026-06-29 +### Features +* Expand libgit2 binding coverage across repository, remote, status, tree, + tag, signature, submodule, config, diff, index, ODB, reference, and related + APIs. +* Add public wrappers for extended repository opening, basic repository + initialization, annotated HEAD detaching, remote instance URL overrides, + remote autotag configuration, tree walking, tree updates, status callbacks, + and annotation-only tag creation. + +### Testing +* Add focused tests for the newly exposed binding wrappers. +* Include binding files in coverage by removing file-level coverage ignores. + ## [0.5.1] - 2026-06-29 ### Fixed * Require `git2dart_binaries` `>=1.11.2 <1.12.0` to pick up the latest diff --git a/pubspec.yaml b/pubspec.yaml index 03f8a81..d442355 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: git2dart description: Dart bindings to libgit2, provides ability to use libgit2 library in Dart and Flutter. repository: https://github.com/DartGit-dev/git2dart -version: 0.5.1 +version: 0.5.2 environment: sdk: ">=3.7.2 <4.0.0" From 94af326e75b315394cdb19dc4ffe105ac551ec72 Mon Sep 17 00:00:00 2001 From: Viktor Borisov Date: Mon, 29 Jun 2026 18:22:42 +0700 Subject: [PATCH 06/10] chore: update git2dart_binaries to 1.11.4 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d442355..3ac89ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter - git2dart_binaries: ">=1.11.2 <1.12.0" + git2dart_binaries: ">=1.11.4 <1.12.0" meta: ^1.8.0 path: ^1.8.1 From f55974fa7f4265860f5f1d05f4d55311d30862df Mon Sep 17 00:00:00 2001 From: Viktor Borisov Date: Mon, 29 Jun 2026 18:34:03 +0700 Subject: [PATCH 07/10] ci: run desktop tests serially --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a633b52..c2e728a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -58,7 +58,7 @@ jobs: - name: Run tests on desktop platforms if: ${{ matrix.platform != 'android' && matrix.platform != 'ios' }} run: | - flutter test + flutter test -j 1 - name: Run tests on iOS if: ${{ matrix.platform == 'ios' }} From 44c61e24abef3cbf73b2efae4b2cf19b11eca080 Mon Sep 17 00:00:00 2001 From: Viktor Borisov Date: Mon, 29 Jun 2026 18:43:57 +0700 Subject: [PATCH 08/10] ci: stabilize simulator selection and test logs --- .github/workflows/publish.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c2e728a..f646cb2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -58,27 +58,27 @@ jobs: - name: Run tests on desktop platforms if: ${{ matrix.platform != 'android' && matrix.platform != 'ios' }} run: | - flutter test -j 1 + flutter test -j 1 --reporter expanded - name: Run tests on iOS if: ${{ matrix.platform == 'ios' }} shell: bash run: | - IOS_DEVICE_NAME="iPhone 16" - if ! xcrun simctl list devices available | grep -q "$IOS_DEVICE_NAME"; then - IOS_DEVICE_NAME="$(xcrun simctl list devices available | awk -F '[()]' '/iPhone/ && /Shutdown/ {print $1; exit}' | xargs)" + IOS_DEVICE_ID="$(xcrun simctl list devices available | awk -F '[()]' '/iPhone 16/ && /Shutdown/ {print $2; exit}')" + if [ -z "$IOS_DEVICE_ID" ]; then + IOS_DEVICE_ID="$(xcrun simctl list devices available | awk -F '[()]' '/iPhone/ && /Shutdown/ {print $2; exit}')" fi - if [ -z "$IOS_DEVICE_NAME" ]; then + if [ -z "$IOS_DEVICE_ID" ]; then echo "No available iOS simulator found" xcrun simctl list devices available exit 1 fi - xcrun simctl boot "$IOS_DEVICE_NAME" || true - xcrun simctl bootstatus "$IOS_DEVICE_NAME" -b + xcrun simctl boot "$IOS_DEVICE_ID" || true + xcrun simctl bootstatus "$IOS_DEVICE_ID" -b flutter devices - flutter test -d "$IOS_DEVICE_NAME" + flutter test -d "$IOS_DEVICE_ID" --reporter expanded - name: Setup Flutter path for Android if: ${{ matrix.platform == 'android' }} From c6930465ce0a2fa6a9b8848e247a1aea0e31282d Mon Sep 17 00:00:00 2001 From: Viktor Borisov Date: Mon, 29 Jun 2026 18:52:25 +0700 Subject: [PATCH 09/10] test: make windows expectations environment aware --- test/diff_test.dart | 8 ++------ test/index_test.dart | 24 ++++++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/diff_test.dart b/test/diff_test.dart index 1868c04..3564805 100644 --- a/test/diff_test.dart +++ b/test/diff_test.dart @@ -364,14 +364,12 @@ index e69de29..c217c63 100644 test('applies diff to repository', () { final file = File(p.join(tmpDir.path, 'subdir', 'modified_file')); - final expectedContent = - Platform.isWindows ? 'Modified content\r\n' : 'Modified content\n'; Checkout.head(repo: repo, strategy: {GitCheckout.force}); expect(file.readAsStringSync(), ''); Diff.parse(patchText).apply(repo: repo); - expect(file.readAsStringSync(), expectedContent); + expect(file.readAsLinesSync(), ['Modified content']); }); test('throws when trying to apply diff and error occurs', () { @@ -393,14 +391,12 @@ index e69de29..c217c63 100644 final diff = Diff.parse(patchText); final hunk = diff.patches.first.hunks.first; final file = File(p.join(tmpDir.path, 'subdir', 'modified_file')); - final expectedContent = - Platform.isWindows ? 'Modified content\r\n' : 'Modified content\n'; Checkout.head(repo: repo, strategy: {GitCheckout.force}); expect(file.readAsStringSync(), ''); diff.apply(repo: repo, hunkIndex: hunk.index); - expect(file.readAsStringSync(), expectedContent); + expect(file.readAsLinesSync(), ['Modified content']); }); test('does not apply hunk with non existing index', () { diff --git a/test/index_test.dart b/test/index_test.dart index 84e9307..de05613 100644 --- a/test/index_test.dart +++ b/test/index_test.dart @@ -41,19 +41,23 @@ void main() { group('capabilities', () { test('returns index capabilities', () { - final expected = - Platform.isWindows - ? {GitIndexCapability.noSymlinks} - : const {}; - expect(index.capabilities, expected); + final capabilities = index.capabilities; + expect( + capabilities.difference({ + GitIndexCapability.ignoreCase, + GitIndexCapability.noSymlinks, + }), + isEmpty, + ); + if (Platform.isWindows) { + expect(capabilities, contains(GitIndexCapability.noSymlinks)); + } }); test('sets index capabilities', () { - final expected = - Platform.isWindows - ? {GitIndexCapability.noSymlinks} - : const {}; - expect(index.capabilities, expected); + if (Platform.isWindows) { + expect(index.capabilities, contains(GitIndexCapability.noSymlinks)); + } index.capabilities = { GitIndexCapability.ignoreCase, From eb3f5d14f158189a5f46cf57b07836f9db478453 Mon Sep 17 00:00:00 2001 From: Viktor Borisov Date: Mon, 29 Jun 2026 19:09:32 +0700 Subject: [PATCH 10/10] test: allow environment index capabilities --- test/index_test.dart | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/test/index_test.dart b/test/index_test.dart index de05613..1d4330e 100644 --- a/test/index_test.dart +++ b/test/index_test.dart @@ -45,29 +45,26 @@ void main() { expect( capabilities.difference({ GitIndexCapability.ignoreCase, + GitIndexCapability.noFileMode, GitIndexCapability.noSymlinks, }), isEmpty, ); - if (Platform.isWindows) { - expect(capabilities, contains(GitIndexCapability.noSymlinks)); - } + expect(capabilities, isNot(contains(GitIndexCapability.fromOwner))); }); test('sets index capabilities', () { - if (Platform.isWindows) { - expect(index.capabilities, contains(GitIndexCapability.noSymlinks)); - } - index.capabilities = { GitIndexCapability.ignoreCase, GitIndexCapability.noSymlinks, }; - expect(index.capabilities, { - GitIndexCapability.ignoreCase, - GitIndexCapability.noSymlinks, - }); + expect(index.capabilities, contains(GitIndexCapability.ignoreCase)); + expect(index.capabilities, contains(GitIndexCapability.noSymlinks)); + expect( + index.capabilities, + isNot(contains(GitIndexCapability.fromOwner)), + ); }); test('throws when trying to set index capabilities and error occurs', () {