diff --git a/lib/libgit2dart.dart b/lib/libgit2dart.dart index 60e97ec..d22077f 100644 --- a/lib/libgit2dart.dart +++ b/lib/libgit2dart.dart @@ -1,4 +1,5 @@ export 'src/repository.dart'; export 'src/config.dart'; +export 'src/signature.dart'; export 'src/error.dart'; export 'src/types.dart'; diff --git a/lib/src/bindings/commit.dart b/lib/src/bindings/commit.dart new file mode 100644 index 0000000..336a19f --- /dev/null +++ b/lib/src/bindings/commit.dart @@ -0,0 +1,91 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import 'libgit2_bindings.dart'; +import '../util.dart'; + +/// Lookup a commit object from a repository. +/// +/// The returned object should be released with `free()` when no longer needed. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer lookup(Pointer repo, Pointer id) { + final out = calloc>(); + final error = libgit2.git_commit_lookup(out, repo, id); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Get the encoding for the message of a commit, as a string representing a standard encoding name. +/// +/// The encoding may be NULL if the encoding header in the commit is missing; +/// in that case UTF-8 is assumed. +String messageEncoding(Pointer commit) { + final result = libgit2.git_commit_message_encoding(commit); + if (result == nullptr) { + return 'utf-8'; + } else { + return result.cast().toDartString(); + } +} + +/// Get the full message of a commit. +/// +/// The returned message will be slightly prettified by removing any potential leading newlines. +String message(Pointer commit) { + final out = libgit2.git_commit_message(commit); + return out.cast().toDartString(); +} + +/// Get the id of a commit. +Pointer id(Pointer commit) => + libgit2.git_commit_id(commit); + +/// Get the number of parents of this commit. +int parentCount(Pointer commit) => + libgit2.git_commit_parentcount(commit); + +/// Get the oid of a specified parent for a commit. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer parentId(Pointer commit, int n) { + final parentOid = libgit2.git_commit_parent_id(commit, n); + + if (parentOid is int && parentOid as int < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return parentOid; + } +} + +/// Get the commit time (i.e. committer time) of a commit. +int time(Pointer commit) => libgit2.git_commit_time(commit); + +/// Get the committer of a commit. +Pointer committer(Pointer commit) { + return libgit2.git_commit_committer(commit); +} + +/// Get the author of a commit. +Pointer author(Pointer commit) { + return libgit2.git_commit_author(commit); +} + +/// Get the id of the tree pointed to by a commit. +Pointer tree(Pointer commit) { + return libgit2.git_commit_tree_id(commit); +} + +/// Get the repository that contains the commit. +Pointer owner(Pointer commit) => + libgit2.git_commit_owner(commit); + +/// Close an open commit. +/// +/// IMPORTANT: It is necessary to call this method when you stop using a commit. +/// Failure to do so will cause a memory leak. +void free(Pointer commit) => libgit2.git_commit_free(commit); diff --git a/lib/src/bindings/signature.dart b/lib/src/bindings/signature.dart new file mode 100644 index 0000000..a3a0364 --- /dev/null +++ b/lib/src/bindings/signature.dart @@ -0,0 +1,52 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import 'libgit2_bindings.dart'; +import '../util.dart'; + +/// Create a new action signature. +/// +/// Note: angle brackets ('<' and '>') characters are not allowed to be used in +/// either the name or the email parameter. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer create( + String name, + String email, + int time, + int offset, +) { + final out = calloc>(); + final nameC = name.toNativeUtf8().cast(); + final emailC = email.toNativeUtf8().cast(); + final error = libgit2.git_signature_new(out, nameC, emailC, time, offset); + calloc.free(nameC); + calloc.free(emailC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Create a new action signature with a timestamp of 'now'. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer now(String name, String email) { + final out = calloc>(); + final nameC = name.toNativeUtf8().cast(); + final emailC = email.toNativeUtf8().cast(); + final error = libgit2.git_signature_now(out, nameC, emailC); + calloc.free(nameC); + calloc.free(emailC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Free an existing signature. +void free(Pointer sig) => libgit2.git_signature_free(sig); diff --git a/lib/src/commit.dart b/lib/src/commit.dart new file mode 100644 index 0000000..22ed274 --- /dev/null +++ b/lib/src/commit.dart @@ -0,0 +1,78 @@ +import 'dart:ffi'; +import 'bindings/libgit2_bindings.dart'; +import 'bindings/commit.dart' as bindings; +import 'oid.dart'; +import 'signature.dart'; +import 'util.dart'; + +class Commit { + /// Initializes a new instance of [Commit] class from provided pointer to + /// commit object in memory. + /// + /// Should be freed with `free()` to release allocated memory. + Commit(this._commitPointer) { + libgit2.git_libgit2_init(); + } + + /// Initializes a new instance of [Commit] class from provided pointer to [Repository] + /// object in memory and pointer to [Oid] object in memory. + /// + /// Should be freed with `free()` to release allocated memory. + Commit.lookup(Pointer repo, Pointer oid) { + libgit2.git_libgit2_init(); + _commitPointer = bindings.lookup(repo, oid); + } + + /// Pointer to memory address for allocated commit object. + late final Pointer _commitPointer; + + /// Returns the encoding for the message of a commit, as a string + /// representing a standard encoding name. + String get messageEncoding => bindings.messageEncoding(_commitPointer); + + /// Returns the full message of a commit. + /// + /// The returned message will be slightly prettified by removing any potential leading newlines. + String get message => bindings.message(_commitPointer); + + /// Returns the id of a commit. + Oid get id => Oid(bindings.id(_commitPointer)); + + /// Returns the commit time (i.e. committer time) of a commit. + int get time => bindings.time(_commitPointer); + + /// Returns the committer of a commit. + Signature get committer => Signature(bindings.committer(_commitPointer)); + + /// Returns the author of a commit. + Signature get author => Signature(bindings.author(_commitPointer)); + + /// Returns list of parent commits. + /// + /// Throws a [LibGit2Error] if error occured. + List get parents { + var parents = []; + final parentCount = bindings.parentCount(_commitPointer); + + for (var i = 0; i < parentCount; i++) { + final parentOid = bindings.parentId(_commitPointer, i); + + if (parentOid != nullptr) { + final owner = bindings.owner(_commitPointer); + final commit = bindings.lookup(owner, parentOid); + parents.add(Commit(commit)); + } + } + + return parents; + } + + /// Get the id of the tree pointed to by a commit. + Oid get tree => Oid(bindings.tree(_commitPointer)); + + /// Releases memory allocated for commit object. + void free() { + bindings.free(_commitPointer); + libgit2.git_libgit2_shutdown(); + } +} diff --git a/lib/src/config.dart b/lib/src/config.dart index e2e2814..3f6dcb3 100644 --- a/lib/src/config.dart +++ b/lib/src/config.dart @@ -4,11 +4,11 @@ import 'bindings/libgit2_bindings.dart'; import 'bindings/config.dart' as bindings; import 'util.dart'; -/// [Config] provides management of global configuration options -/// (system, global, XDG, excluding repository config) class Config { /// Initializes a new instance of [Config] class from provided /// pointer to config object in memory. + /// + /// Should be freed with `free()` to release allocated memory. Config(this._configPointer) { libgit2.git_libgit2_init(); } @@ -20,7 +20,7 @@ class Config { /// [path] should point to single on-disk file; it's expected to be a native /// Git config file following the default Git config syntax (see man git-config). /// - /// [Config] object should be closed with [free] function to release allocated memory. + /// Should be freed with `free()` to release allocated memory. Config.open([String? path]) { libgit2.git_libgit2_init(); @@ -39,6 +39,8 @@ class Config { /// /// Opens the system configuration file. /// + /// Should be freed with `free()` to release allocated memory. + /// /// Throws a [LibGit2Error] if error occured. Config.system() { libgit2.git_libgit2_init(); @@ -51,6 +53,8 @@ class Config { /// /// Opens the global configuration file. /// + /// Should be freed with `free()` to release allocated memory. + /// /// Throws an error if file has not been found. Config.global() { libgit2.git_libgit2_init(); @@ -63,6 +67,8 @@ class Config { /// /// Opens the global XDG configuration file. /// + /// Should be freed with `free()` to release allocated memory. + /// /// Throws a [LibGit2Error] if error occured. Config.xdg() { libgit2.git_libgit2_init(); diff --git a/lib/src/index.dart b/lib/src/index.dart index 44d52b2..7c6083d 100644 --- a/lib/src/index.dart +++ b/lib/src/index.dart @@ -97,19 +97,19 @@ class Index { late final Oid oid; late final Tree tree; if (target is Oid) { - tree = Tree(bindings.owner(_indexPointer), target.pointer); + tree = Tree.lookup(bindings.owner(_indexPointer), target.pointer); } else if (target is Tree) { tree = target; } else if (isValidShaHex(target as String)) { if (target.length == 40) { oid = Oid.fromSHA(target); - tree = Tree(bindings.owner(_indexPointer), oid.pointer); + tree = Tree.lookup(bindings.owner(_indexPointer), oid.pointer); } else { final shortOid = Oid.fromSHAn(target); final odb = Odb(repo_bindings.odb(bindings.owner(_indexPointer))); oid = Oid(odb.existsPrefix(shortOid.pointer, target.length)); odb.free(); - tree = Tree(bindings.owner(_indexPointer), oid.pointer); + tree = Tree.lookup(bindings.owner(_indexPointer), oid.pointer); } } else { throw ArgumentError.value( diff --git a/lib/src/repository.dart b/lib/src/repository.dart index c24bdc8..243843c 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -1,4 +1,5 @@ import 'dart:ffi'; +import 'commit.dart'; import 'config.dart'; import 'index.dart'; import 'odb.dart'; @@ -8,7 +9,6 @@ import 'bindings/libgit2_bindings.dart'; import 'bindings/repository.dart' as bindings; import 'util.dart'; -/// A Repository is the primary interface into a git repository class Repository { /// Initializes a new instance of the [Repository] class by creating a new /// Git repository in the given folder. @@ -270,14 +270,7 @@ class Repository { oid = target as Oid; isDirect = true; } else if (isValidShaHex(target as String)) { - if (target.length == 40) { - oid = Oid.fromSHA(target); - } else { - final shortOid = Oid.fromSHAn(target); - final odb = this.odb; - oid = Oid(odb.existsPrefix(shortOid.pointer, target.length)); - odb.free(); - } + oid = _getOid(target); isDirect = true; } else { isDirect = false; @@ -313,4 +306,33 @@ class Repository { /// /// Throws a [LibGit2Error] if error occured. Odb get odb => Odb(bindings.odb(_repoPointer)); + + /// Looksup [Commit] for provided [sha] hex string. + /// + /// Throws [ArgumentError] if provided [sha] is not valid sha hex string. + Commit operator [](String sha) { + late final Oid oid; + + if (isValidShaHex(sha)) { + oid = _getOid(sha); + } else { + throw ArgumentError.value('$sha is not a valid sha hex string'); + } + return Commit.lookup(_repoPointer, oid.pointer); + } + + /// Returns [Oid] for provided sha hex that is 40 characters long or less. + Oid _getOid(String sha) { + late final Oid oid; + + if (sha.length == 40) { + oid = Oid.fromSHA(sha); + } else { + final shortOid = Oid.fromSHAn(sha); + final odb = this.odb; + oid = Oid(odb.existsPrefix(shortOid.pointer, sha.length)); + odb.free(); + } + return oid; + } } diff --git a/lib/src/signature.dart b/lib/src/signature.dart new file mode 100644 index 0000000..8b9ed44 --- /dev/null +++ b/lib/src/signature.dart @@ -0,0 +1,71 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'bindings/libgit2_bindings.dart'; +import 'bindings/signature.dart' as bindings; +import 'util.dart'; + +class Signature { + /// Initializes a new instance of [Signature] class from provided pointer to + /// signature object in memory. + /// + /// Should be freed with `free()` to release allocated memory. + Signature(this._signaturePointer) { + libgit2.git_libgit2_init(); + } + + /// Initializes a new instance of [Signature] class from provided [name], [email], + /// and optional [time] in seconds from epoch and [offset] in minutes. + /// + /// If [time] isn't provided [Signature] will be created with a timestamp of 'now'. + /// + /// Should be freed with `free()` to release allocated memory. + Signature.create({ + required String name, + required String email, + int? time, + int offset = 0, + }) { + libgit2.git_libgit2_init(); + + if (time == null) { + _signaturePointer = bindings.now(name, email); + } else { + _signaturePointer = bindings.create(name, email, time, offset); + } + } + + /// Pointer to memory address for allocated signature object. + late final Pointer _signaturePointer; + + /// Returns full name of the author. + String get name => _signaturePointer.ref.name.cast().toDartString(); + + /// Returns email of the author. + String get email => _signaturePointer.ref.email.cast().toDartString(); + + /// Returns time in seconds from epoch. + int get time => _signaturePointer.ref.when.time; + + /// Returns timezone offset in minutes. + int get offset => _signaturePointer.ref.when.offset; + + @override + bool operator ==(other) { + return (other is Signature) && + (name == other.name) && + (email == other.email) && + (time == other.time) && + (offset == other.offset) && + (_signaturePointer.ref.when.sign == + other._signaturePointer.ref.when.sign); + } + + @override + int get hashCode => _signaturePointer.address.hashCode; + + /// Releases memory allocated for signature object. + void free() { + bindings.free(_signaturePointer); + libgit2.git_libgit2_shutdown(); + } +} diff --git a/lib/src/tree.dart b/lib/src/tree.dart index b21c13e..12753ea 100644 --- a/lib/src/tree.dart +++ b/lib/src/tree.dart @@ -4,11 +4,17 @@ import 'bindings/tree.dart' as bindings; import 'util.dart'; class Tree { + /// Initializes a new instance of [Tree] class from provided + /// pointer to tree object in memory. + Tree(this._treePointer) { + libgit2.git_libgit2_init(); + } + /// Initializes a new instance of [Tree] class from provided /// pointers to repository object and oid object in memory. /// /// Should be freed with `free()` to release allocated memory. - Tree(Pointer repo, Pointer id) { + Tree.lookup(Pointer repo, Pointer id) { libgit2.git_libgit2_init(); _treePointer = bindings.lookup(repo, id); } diff --git a/lib/src/types.dart b/lib/src/types.dart index 83b9634..13954ae 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -1,4 +1,3 @@ enum ReferenceType { direct, symbolic } enum GitFilemode { undreadable, tree, blob, blobExecutable, link, commit } - diff --git a/test/commit_test.dart b/test/commit_test.dart new file mode 100644 index 0000000..6f48a1c --- /dev/null +++ b/test/commit_test.dart @@ -0,0 +1,79 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'package:libgit2dart/src/commit.dart'; +import 'helpers/util.dart'; + +void main() { + const mergeCommit = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'; + + group('Commit', () { + late Repository repo; + final tmpDir = '${Directory.systemTemp.path}/ref_testrepo/'; + + setUp(() async { + if (await Directory(tmpDir).exists()) { + await Directory(tmpDir).delete(recursive: true); + } + await copyRepo( + from: Directory('test/assets/testrepo/'), + to: await Directory(tmpDir).create(), + ); + repo = Repository.open(tmpDir); + }); + + tearDown(() async { + repo.free(); + await Directory(tmpDir).delete(recursive: true); + }); + + group('lookup', () { + test('successful when 40 char sha hex is provided', () { + final commit = repo[mergeCommit]; + expect(commit, isA()); + commit.free(); + }); + + test('successful when sha hex is short', () { + final commit = repo[mergeCommit.substring(0, 5)]; + expect(commit, isA()); + commit.free(); + }); + + test('throws when provided sha hex is invalid', () { + expect(() => repo['invalid'], throwsA(isA())); + }); + + test('throws when nothing found', () { + expect(() => repo['970ae5c'], throwsA(isA())); + }); + + test('returns with correct fields', () { + final signature = Signature.create( + name: 'Aleksey Kulikov', + email: 'skinny.mind@gmail.com', + time: 1626091184, + offset: 180, + ); + final commit = repo[mergeCommit]; + + expect(commit.messageEncoding, 'utf-8'); + expect(commit.message, 'Merge branch \'feature\'\n'); + expect(commit.id.sha, mergeCommit); + expect(commit.parents.length, 2); + expect( + commit.parents[0].id.sha, + 'c68ff54aabf660fcdd9a2838d401583fe31249e3', + ); + expect(commit.time, 1626091184); + expect(commit.committer, signature); + expect(commit.author, signature); + expect(commit.tree.sha, '7796359a96eb722939c24bafdb1afe9f07f2f628'); + + signature.free(); + commit.free(); + }); + }); + }); +} diff --git a/test/signature_test.dart b/test/signature_test.dart new file mode 100644 index 0000000..36179e6 --- /dev/null +++ b/test/signature_test.dart @@ -0,0 +1,63 @@ +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; + +void main() { + late Signature signature; + group('Signature', () { + const name = 'Some Name'; + const email = 'some@email.com'; + const time = 1234567890; + const offset = 0; + + setUp(() { + signature = Signature.create( + name: name, + email: email, + time: time, + offset: offset, + ); + }); + + tearDown(() { + signature.free(); + }); + + test('successfully creates with provided time and offset', () { + expect(signature, isA()); + }); + + test('successfully creates without provided time and offset', () { + final defaultSignature = + Signature.create(name: 'Name', email: 'email@example.com'); + expect(defaultSignature, isA()); + expect(defaultSignature.name, 'Name'); + expect(defaultSignature.email, 'email@example.com'); + expect( + defaultSignature.time - + (DateTime.now().millisecondsSinceEpoch / 1000).truncate(), + lessThan(5), + ); + expect(defaultSignature.offset, 180); + defaultSignature.free(); + }); + + test('returns correct values', () { + expect(signature.name, name); + expect(signature.email, email); + expect(signature.time, time); + expect(signature.offset, offset); + }); + + test('compares two objects', () { + final otherSignature = Signature.create( + name: name, + email: email, + time: time, + offset: offset, + ); + expect(signature == otherSignature, true); + + otherSignature.free(); + }); + }); +}