From d5c4057de7623d1736a06e0e5516966339df415b Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Mon, 20 Dec 2021 20:32:14 +0300 Subject: [PATCH] feat(commit): add more bindings and API methods --- lib/src/bindings/commit.dart | 181 ++++++++++++++++++++++++++++++++++- lib/src/commit.dart | 124 ++++++++++++++++++++++-- lib/src/repository.dart | 4 +- test/commit_test.dart | 157 ++++++++++++++++++++++++++---- 4 files changed, 438 insertions(+), 28 deletions(-) diff --git a/lib/src/bindings/commit.dart b/lib/src/bindings/commit.dart index a139e95..10d7c0e 100644 --- a/lib/src/bindings/commit.dart +++ b/lib/src/bindings/commit.dart @@ -115,6 +115,65 @@ Pointer create({ } } +/// Create a commit and write it into a buffer. +/// +/// Create a commit as with [create] but instead of writing it to the objectdb, +/// write the contents of the object into a buffer. +/// +/// Throws a [LibGit2Error] if error occured. +String createBuffer({ + required Pointer repoPointer, + required String updateRef, + required Pointer authorPointer, + required Pointer committerPointer, + String? messageEncoding, + required String message, + required Pointer treePointer, + required int parentCount, + required List> parents, +}) { + final out = calloc(); + final updateRefC = updateRef.toNativeUtf8().cast(); + final messageEncodingC = + messageEncoding?.toNativeUtf8().cast() ?? nullptr; + final messageC = message.toNativeUtf8().cast(); + final parentsC = calloc>(parentCount); + + if (parents.isNotEmpty) { + for (var i = 0; i < parentCount; i++) { + parentsC[i] = parents[i]; + } + } else { + parentsC[0] = nullptr; + } + + final error = libgit2.git_commit_create_buffer( + out, + repoPointer, + authorPointer, + committerPointer, + messageEncodingC, + messageC, + treePointer, + parentCount, + parentsC, + ); + + calloc.free(updateRefC); + calloc.free(messageEncodingC); + calloc.free(messageC); + calloc.free(parentsC); + + if (error < 0) { + calloc.free(out); + throw LibGit2Error(libgit2.git_error_last()); + } else { + final result = out.ref.ptr.cast().toDartString(length: out.ref.size); + calloc.free(out); + return result; + } +} + /// Amend an existing commit by replacing only non-null values. /// /// This creates a new commit that is exactly the same as the old commit, @@ -167,6 +226,14 @@ Pointer amend({ } } +/// Create an in-memory copy of a commit. The copy must be explicitly free'd or +/// it will leak. +Pointer duplicate(Pointer source) { + final out = calloc>(); + libgit2.git_commit_dup(out, source); + return out.value; +} + /// Get the encoding for the message of a commit, as a string representing a /// standard encoding name. /// @@ -185,6 +252,55 @@ String message(Pointer commit) { return libgit2.git_commit_message(commit).cast().toDartString(); } +/// Get the short "summary" of the git commit message. +/// +/// The returned message is the summary of the commit, comprising the first +/// paragraph of the message with whitespace trimmed and squashed. +/// +/// Throws a [LibGit2Error] if error occured. +String summary(Pointer commit) { + final result = libgit2.git_commit_summary(commit); + + if (result == nullptr) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return result.cast().toDartString(); + } +} + +/// Get the long "body" of the git commit message. +/// +/// The returned message is the body of the commit, comprising everything but +/// the first paragraph of the message. Leading and trailing whitespaces are +/// trimmed. +String body(Pointer commit) { + final result = libgit2.git_commit_body(commit); + return result == nullptr ? '' : result.cast().toDartString(); +} + +/// Get an arbitrary header field. +/// +/// Throws a [LibGit2Error] if error occured. +String headerField({ + required Pointer commitPointer, + required String field, +}) { + final out = calloc(); + final fieldC = field.toNativeUtf8().cast(); + final error = libgit2.git_commit_header_field(out, commitPointer, fieldC); + + calloc.free(fieldC); + + if (error < 0) { + calloc.free(out); + throw LibGit2Error(libgit2.git_error_last()); + } else { + final result = out.ref.ptr.cast().toDartString(length: out.ref.size); + calloc.free(out); + return result; + } +} + /// Get the id of a commit. Pointer id(Pointer commit) => libgit2.git_commit_id(commit); @@ -194,8 +310,6 @@ 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({ required Pointer commitPointer, required int position, @@ -203,9 +317,55 @@ Pointer parentId({ return libgit2.git_commit_parent_id(commitPointer, position); } +/// Get the specified parent of the commit (0-based). +/// +/// Throws a [LibGit2Error] if error occured. +Pointer parent({ + required Pointer commitPointer, + required int position, +}) { + final out = calloc>(); + final error = libgit2.git_commit_parent(out, commitPointer, position); + + if (error < 0) { + calloc.free(out); + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Get the commit object that is the th generation ancestor of the named +/// commit object, following only the first parents. The returned commit has to +/// be freed by the caller. +/// +/// Passing 0 as the generation number returns another instance of the base +/// commit itself. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer nthGenAncestor({ + required Pointer commitPointer, + required int n, +}) { + final out = calloc>(); + final error = libgit2.git_commit_nth_gen_ancestor(out, commitPointer, n); + + if (error < 0) { + calloc.free(out); + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + /// Get the commit time (i.e. committer time) of a commit. int time(Pointer commit) => libgit2.git_commit_time(commit); +/// Get the commit timezone offset in minutes (i.e. committer's preferred +/// timezone) of a commit. +int timeOffset(Pointer commit) => + libgit2.git_commit_time_offset(commit); + /// Get the committer of a commit. Pointer committer(Pointer commit) { return libgit2.git_commit_committer(commit); @@ -217,10 +377,25 @@ Pointer author(Pointer commit) { } /// Get the id of the tree pointed to by a commit. -Pointer tree(Pointer commit) { +Pointer treeOid(Pointer commit) { return libgit2.git_commit_tree_id(commit); } +/// Get the tree pointed to by a commit. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer tree(Pointer commit) { + final out = calloc>(); + final error = libgit2.git_commit_tree(out, commit); + + if (error < 0) { + calloc.free(out); + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + /// Reverts the given commit against the given "our" commit, producing an index /// that reflects the result of the revert. /// diff --git a/lib/src/commit.dart b/lib/src/commit.dart index c69944a..add02fb 100644 --- a/lib/src/commit.dart +++ b/lib/src/commit.dart @@ -3,7 +3,6 @@ import 'dart:ffi'; import 'package:libgit2dart/libgit2dart.dart'; import 'package:libgit2dart/src/bindings/commit.dart' as bindings; import 'package:libgit2dart/src/bindings/libgit2_bindings.dart'; -import 'package:libgit2dart/src/bindings/tree.dart' as tree_bindings; class Commit { /// Initializes a new instance of [Commit] class from provided pointer to @@ -81,6 +80,61 @@ class Commit { ); } + /// Creates a commit and writes it into a buffer. + /// + /// Creates a commit as with [create] but instead of writing it to the + /// objectdb, writes the contents of the object into a buffer. + /// + /// [repo] is the repository where to store the commit. + /// + /// [updateRef] is the name of the reference that will be updated to point to + /// this commit. If the reference is not direct, it will be resolved to a + /// direct reference. Use "HEAD" to update the HEAD of the current branch and + /// make it point to this commit. If the reference doesn't exist yet, it will + /// be created. If it does exist, the first parent must be the tip of this + /// branch. + /// + /// [author] is the signature with author and author time of commit. + /// + /// [committer] is the signature with committer and commit time of commit. + /// + /// [messageEncoding] is the encoding for the message in the commit, + /// represented with a standard encoding name. E.g. "UTF-8". If null, no + /// encoding header is written and UTF-8 is assumed. + /// + /// [message] is the full message for this commit. + /// + /// [tree] is an instance of a [Tree] object that will be used as the tree + /// for the commit. This tree object must also be owned by the given [repo]. + /// + /// [parents] is a list of [Commit] objects that will be used as the parents + /// for this commit. This array may be empty if parent count is 0 + /// (root commit). All the given commits must be owned by the [repo]. + /// + /// Throws a [LibGit2Error] if error occured. + static String createBuffer({ + required Repository repo, + required String updateRef, + required Signature author, + required Signature committer, + String? messageEncoding, + required String message, + required Tree tree, + required List parents, + }) { + return bindings.createBuffer( + repoPointer: repo.pointer, + updateRef: updateRef, + authorPointer: author.pointer, + committerPointer: committer.pointer, + messageEncoding: messageEncoding, + message: message, + treePointer: tree.pointer, + parentCount: parents.length, + parents: parents.map((e) => e.pointer).toList(), + ); + } + /// Amends an existing commit by replacing only non-null values. /// /// This creates a new commit that is exactly the same as the old commit, @@ -134,12 +188,33 @@ class Commit { /// leading newlines. String get message => bindings.message(_commitPointer); + /// Returns the short "summary" of the git commit message. + /// + /// The returned message is the summary of the commit, comprising the first + /// paragraph of the message with whitespace trimmed and squashed. + /// + /// Throws a [LibGit2Error] if error occured. + String get summary => bindings.summary(_commitPointer); + + /// Returns the long "body" of the commit message. + /// + /// The returned message is the body of the commit, comprising everything but + /// the first paragraph of the message. Leading and trailing whitespaces are + /// trimmed. + /// + /// Returns empty string if message only consists of a summary. + String get body => bindings.body(_commitPointer); + /// [Oid] of a commit. Oid get oid => Oid(bindings.id(_commitPointer)); /// Commit time (i.e. committer time) of a commit. int get time => bindings.time(_commitPointer); + /// Commit timezone offset in minutes (i.e. committer's preferred timezone) + /// of a commit. + int get timeOffset => bindings.timeOffset(_commitPointer); + /// Committer of a commit. Signature get committer => Signature(bindings.committer(_commitPointer)); @@ -162,16 +237,51 @@ class Commit { return parents; } - /// Tree pointed to by a commit. - Tree get tree { - return Tree( - tree_bindings.lookup( - repoPointer: bindings.owner(_commitPointer), - oidPointer: bindings.tree(_commitPointer), + /// Returns the specified parent of the commit at provided 0-based [position]. + /// + /// **IMPORTANT**: Should be freed to release allocated memory. + /// + /// Throws a [LibGit2Error] if error occured. + Commit parent(int position) { + return Commit( + bindings.parent( + commitPointer: _commitPointer, + position: position, ), ); } + /// Tree pointed to by a commit. + Tree get tree => Tree(bindings.tree(_commitPointer)); + + /// Oid of the tree pointed to by a commit. + Oid get treeOid => Oid(bindings.treeOid(_commitPointer)); + + /// Returns an arbitrary header field. + /// + /// Throws a [LibGit2Error] if error occured. + String headerField(String field) { + return bindings.headerField(commitPointer: _commitPointer, field: field); + } + + /// Returns commit object that is the [n]th generation ancestor of the + /// commit, following only the first parents. + /// + /// Passing 0 as the generation number returns another instance of the base + /// commit itself. + /// + /// **IMPORTANT**: Should be freed to release allocated memory. + /// + /// Throws a [LibGit2Error] if error occured. + Commit nthGenAncestor(int n) { + return Commit(bindings.nthGenAncestor(commitPointer: _commitPointer, n: n)); + } + + /// Creates an in-memory copy of a commit. + /// + /// **IMPORTANT**: Should be freed to release allocated memory. + Commit duplicate() => Commit(bindings.duplicate(_commitPointer)); + /// Releases memory allocated for commit object. void free() => bindings.free(_commitPointer); diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 01c6a07..6ff6910 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -585,7 +585,7 @@ class Repository { required String updateRef, required String message, required Signature author, - required Signature commiter, + required Signature committer, required Tree tree, required List parents, String? messageEncoding, @@ -595,7 +595,7 @@ class Repository { updateRef: updateRef, message: message, author: author, - committer: commiter, + committer: committer, tree: tree, parents: parents, messageEncoding: messageEncoding, diff --git a/test/commit_test.dart b/test/commit_test.dart index 6865a04..c277894 100644 --- a/test/commit_test.dart +++ b/test/commit_test.dart @@ -10,7 +10,7 @@ void main() { late Repository repo; late Directory tmpDir; late Signature author; - late Signature commiter; + late Signature committer; late Tree tree; late Oid tip; const message = "Commit message.\n\nSome description.\n"; @@ -23,7 +23,7 @@ void main() { email: 'author@email.com', time: 123, ); - commiter = Signature.create( + committer = Signature.create( name: 'Commiter', email: 'commiter@email.com', time: 124, @@ -37,7 +37,7 @@ void main() { tearDown(() { author.free(); - commiter.free(); + committer.free(); tree.free(); repo.free(); tmpDir.deleteSync(recursive: true); @@ -70,6 +70,12 @@ void main() { ); }); + test( + 'throws when trying to get the summary of the commit message and error ' + 'occurs', () { + expect(() => Commit(nullptr).summary, throwsA(isA())); + }); + test('successfully reverts commit', () { final to = repo.lookupCommit( repo['78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'], @@ -106,7 +112,7 @@ void main() { updateRef: 'HEAD', message: message, author: author, - commiter: commiter, + committer: committer, tree: tree, parents: [parent], ); @@ -116,10 +122,14 @@ void main() { expect(commit.oid, oid); expect(commit.message, message); expect(commit.messageEncoding, 'utf-8'); + expect(commit.summary, 'Commit message.'); + expect(commit.body, 'Some description.'); expect(commit.author, author); - expect(commit.committer, commiter); + expect(commit.committer, committer); expect(commit.time, 124); + expect(commit.timeOffset, 0); expect(commit.tree.oid, tree.oid); + expect(commit.treeOid, tree.oid); expect(commit.parents.length, 1); expect(commit.parents[0], tip); @@ -127,12 +137,40 @@ void main() { parent.free(); }); + test('writes commit into the buffer', () { + final parent = repo.lookupCommit(tip); + final commit = Commit.createBuffer( + repo: repo, + updateRef: 'HEAD', + message: message, + author: author, + committer: committer, + tree: tree, + parents: [parent], + ); + + const expected = """ +tree a8ae3dd59e6e1802c6f78e05e301bfd57c9f334f +parent 821ed6e80627b8769d170a293862f9fc60825226 +author Author Name 123 +0000 +committer Commiter 124 +0000 + +Commit message. + +Some description. +"""; + + expect(commit, expected); + + parent.free(); + }); + test('successfully creates commit without parents', () { final oid = repo.createCommit( updateRef: 'refs/heads/new', message: message, author: author, - commiter: commiter, + committer: committer, tree: tree, parents: [], ); @@ -143,9 +181,9 @@ void main() { expect(commit.message, message); expect(commit.messageEncoding, 'utf-8'); expect(commit.author, author); - expect(commit.committer, commiter); + expect(commit.committer, committer); expect(commit.time, 124); - expect(commit.tree.oid, tree.oid); + expect(commit.treeOid, tree.oid); expect(commit.parents.length, 0); commit.free(); @@ -162,7 +200,7 @@ void main() { repo: repo, message: message, author: author, - committer: commiter, + committer: committer, tree: tree, parents: [parent1, parent2], ); @@ -173,9 +211,9 @@ void main() { expect(commit.message, message); expect(commit.messageEncoding, 'utf-8'); expect(commit.author, author); - expect(commit.committer, commiter); + expect(commit.committer, committer); expect(commit.time, 124); - expect(commit.tree.oid, tree.oid); + expect(commit.treeOid, tree.oid); expect(commit.parents.length, 2); expect(commit.parents[0], tip); expect(commit.parents[1], parent2.oid); @@ -194,7 +232,28 @@ void main() { updateRef: 'HEAD', message: message, author: author, - commiter: commiter, + committer: committer, + tree: tree, + parents: [parent], + ), + throwsA(isA()), + ); + + parent.free(); + }); + + test('throws when trying to write commit into a buffer and error occurs', + () { + final parent = repo.lookupCommit(tip); + final nullRepo = Repository(nullptr); + + expect( + () => Commit.createBuffer( + repo: nullRepo, + updateRef: 'HEAD', + message: message, + author: author, + committer: committer, tree: tree, parents: [parent], ), @@ -221,7 +280,7 @@ void main() { expect(amendedCommit.message, 'amended commit\n'); expect(amendedCommit.author, commit.author); expect(amendedCommit.committer, commit.committer); - expect(amendedCommit.tree.oid, commit.tree.oid); + expect(amendedCommit.treeOid, commit.treeOid); expect(amendedCommit.parents, commit.parents); amendedCommit.free(); @@ -240,7 +299,7 @@ void main() { message: 'amended commit\n', updateRef: 'HEAD', author: author, - committer: commiter, + committer: committer, tree: tree, ); final amendedCommit = repo.lookupCommit(amendedOid); @@ -249,8 +308,8 @@ void main() { expect(amendedCommit.oid, newHead.target); expect(amendedCommit.message, 'amended commit\n'); expect(amendedCommit.author, author); - expect(amendedCommit.committer, commiter); - expect(amendedCommit.tree.oid, tree.oid); + expect(amendedCommit.committer, committer); + expect(amendedCommit.treeOid, tree.oid); expect(amendedCommit.parents, commit.parents); amendedCommit.free(); @@ -297,6 +356,72 @@ void main() { head.free(); }); + test('creates an in-memory copy of a commit', () { + final commit = repo.lookupCommit(tip); + final dupCommit = commit.duplicate(); + + expect(dupCommit.oid, commit.oid); + + dupCommit.free(); + commit.free(); + }); + + test('returns header field', () { + final commit = repo.lookupCommit(tip); + expect( + commit.headerField('parent'), + '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8', + ); + commit.free(); + }); + + test('throws when header field not found', () { + final commit = repo.lookupCommit(tip); + expect( + () => commit.headerField('not-there'), + throwsA(isA()), + ); + commit.free(); + }); + + test('returns nth generation ancestor commit', () { + final commit = repo.lookupCommit(tip); + final ancestor = commit.nthGenAncestor(3); + + expect(ancestor.oid.sha, 'f17d0d48eae3aa08cecf29128a35e310c97b3521'); + + ancestor.free(); + commit.free(); + }); + + test('throws when trying to get nth generation ancestor and none exists', + () { + final commit = repo.lookupCommit(tip); + expect(() => commit.nthGenAncestor(10), throwsA(isA())); + commit.free(); + }); + + test('returns parent at specified position', () { + final commit = repo.lookupCommit( + repo['78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'], + ); + final firstParent = commit.parent(0); + final secondParent = commit.parent(1); + + expect(firstParent.oid.sha, 'c68ff54aabf660fcdd9a2838d401583fe31249e3'); + expect(secondParent.oid.sha, 'fc38877b2552ab554752d9a77e1f48f738cca79b'); + + secondParent.free(); + firstParent.free(); + commit.free(); + }); + + test('throws when trying to get the parent at invalid position', () { + final commit = repo.lookupCommit(tip); + expect(() => commit.parent(10), throwsA(isA())); + commit.free(); + }); + test('returns string representation of Commit object', () { final commit = repo.lookupCommit(tip); expect(commit.toString(), contains('Commit{'));