feat(commit): add more bindings and API methods

This commit is contained in:
Aleksey Kulikov 2021-12-20 20:32:14 +03:00
parent e6bfdc5a85
commit d5c4057de7
4 changed files with 438 additions and 28 deletions

View file

@ -115,6 +115,65 @@ Pointer<git_oid> 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<git_repository> repoPointer,
required String updateRef,
required Pointer<git_signature> authorPointer,
required Pointer<git_signature> committerPointer,
String? messageEncoding,
required String message,
required Pointer<git_tree> treePointer,
required int parentCount,
required List<Pointer<git_commit>> parents,
}) {
final out = calloc<git_buf>();
final updateRefC = updateRef.toNativeUtf8().cast<Int8>();
final messageEncodingC =
messageEncoding?.toNativeUtf8().cast<Int8>() ?? nullptr;
final messageC = message.toNativeUtf8().cast<Int8>();
final parentsC = calloc<Pointer<git_commit>>(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<Utf8>().toDartString(length: out.ref.size);
calloc.free(out);
return result;
}
}
/// Amend an existing commit by replacing only non-null values. /// Amend an existing commit by replacing only non-null values.
/// ///
/// This creates a new commit that is exactly the same as the old commit, /// This creates a new commit that is exactly the same as the old commit,
@ -167,6 +226,14 @@ Pointer<git_oid> amend({
} }
} }
/// Create an in-memory copy of a commit. The copy must be explicitly free'd or
/// it will leak.
Pointer<git_commit> duplicate(Pointer<git_commit> source) {
final out = calloc<Pointer<git_commit>>();
libgit2.git_commit_dup(out, source);
return out.value;
}
/// Get the encoding for the message of a commit, as a string representing a /// Get the encoding for the message of a commit, as a string representing a
/// standard encoding name. /// standard encoding name.
/// ///
@ -185,6 +252,55 @@ String message(Pointer<git_commit> commit) {
return libgit2.git_commit_message(commit).cast<Utf8>().toDartString(); return libgit2.git_commit_message(commit).cast<Utf8>().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<git_commit> commit) {
final result = libgit2.git_commit_summary(commit);
if (result == nullptr) {
throw LibGit2Error(libgit2.git_error_last());
} else {
return result.cast<Utf8>().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<git_commit> commit) {
final result = libgit2.git_commit_body(commit);
return result == nullptr ? '' : result.cast<Utf8>().toDartString();
}
/// Get an arbitrary header field.
///
/// Throws a [LibGit2Error] if error occured.
String headerField({
required Pointer<git_commit> commitPointer,
required String field,
}) {
final out = calloc<git_buf>();
final fieldC = field.toNativeUtf8().cast<Int8>();
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<Utf8>().toDartString(length: out.ref.size);
calloc.free(out);
return result;
}
}
/// Get the id of a commit. /// Get the id of a commit.
Pointer<git_oid> id(Pointer<git_commit> commit) => Pointer<git_oid> id(Pointer<git_commit> commit) =>
libgit2.git_commit_id(commit); libgit2.git_commit_id(commit);
@ -194,8 +310,6 @@ int parentCount(Pointer<git_commit> commit) =>
libgit2.git_commit_parentcount(commit); libgit2.git_commit_parentcount(commit);
/// Get the oid of a specified parent for a commit. /// Get the oid of a specified parent for a commit.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_oid> parentId({ Pointer<git_oid> parentId({
required Pointer<git_commit> commitPointer, required Pointer<git_commit> commitPointer,
required int position, required int position,
@ -203,9 +317,55 @@ Pointer<git_oid> parentId({
return libgit2.git_commit_parent_id(commitPointer, position); return libgit2.git_commit_parent_id(commitPointer, position);
} }
/// Get the specified parent of the commit (0-based).
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_commit> parent({
required Pointer<git_commit> commitPointer,
required int position,
}) {
final out = calloc<Pointer<git_commit>>();
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<git_commit> nthGenAncestor({
required Pointer<git_commit> commitPointer,
required int n,
}) {
final out = calloc<Pointer<git_commit>>();
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. /// Get the commit time (i.e. committer time) of a commit.
int time(Pointer<git_commit> commit) => libgit2.git_commit_time(commit); int time(Pointer<git_commit> 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<git_commit> commit) =>
libgit2.git_commit_time_offset(commit);
/// Get the committer of a commit. /// Get the committer of a commit.
Pointer<git_signature> committer(Pointer<git_commit> commit) { Pointer<git_signature> committer(Pointer<git_commit> commit) {
return libgit2.git_commit_committer(commit); return libgit2.git_commit_committer(commit);
@ -217,10 +377,25 @@ Pointer<git_signature> author(Pointer<git_commit> commit) {
} }
/// Get the id of the tree pointed to by a commit. /// Get the id of the tree pointed to by a commit.
Pointer<git_oid> tree(Pointer<git_commit> commit) { Pointer<git_oid> treeOid(Pointer<git_commit> commit) {
return libgit2.git_commit_tree_id(commit); return libgit2.git_commit_tree_id(commit);
} }
/// Get the tree pointed to by a commit.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_tree> tree(Pointer<git_commit> commit) {
final out = calloc<Pointer<git_tree>>();
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 /// Reverts the given commit against the given "our" commit, producing an index
/// that reflects the result of the revert. /// that reflects the result of the revert.
/// ///

View file

@ -3,7 +3,6 @@ import 'dart:ffi';
import 'package:libgit2dart/libgit2dart.dart'; import 'package:libgit2dart/libgit2dart.dart';
import 'package:libgit2dart/src/bindings/commit.dart' as bindings; import 'package:libgit2dart/src/bindings/commit.dart' as bindings;
import 'package:libgit2dart/src/bindings/libgit2_bindings.dart'; import 'package:libgit2dart/src/bindings/libgit2_bindings.dart';
import 'package:libgit2dart/src/bindings/tree.dart' as tree_bindings;
class Commit { class Commit {
/// Initializes a new instance of [Commit] class from provided pointer to /// 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<Commit> 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. /// Amends an existing commit by replacing only non-null values.
/// ///
/// This creates a new commit that is exactly the same as the old commit, /// This creates a new commit that is exactly the same as the old commit,
@ -134,12 +188,33 @@ class Commit {
/// leading newlines. /// leading newlines.
String get message => bindings.message(_commitPointer); 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] of a commit.
Oid get oid => Oid(bindings.id(_commitPointer)); Oid get oid => Oid(bindings.id(_commitPointer));
/// Commit time (i.e. committer time) of a commit. /// Commit time (i.e. committer time) of a commit.
int get time => bindings.time(_commitPointer); 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. /// Committer of a commit.
Signature get committer => Signature(bindings.committer(_commitPointer)); Signature get committer => Signature(bindings.committer(_commitPointer));
@ -162,16 +237,51 @@ class Commit {
return parents; return parents;
} }
/// Tree pointed to by a commit. /// Returns the specified parent of the commit at provided 0-based [position].
Tree get tree { ///
return Tree( /// **IMPORTANT**: Should be freed to release allocated memory.
tree_bindings.lookup( ///
repoPointer: bindings.owner(_commitPointer), /// Throws a [LibGit2Error] if error occured.
oidPointer: bindings.tree(_commitPointer), 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. /// Releases memory allocated for commit object.
void free() => bindings.free(_commitPointer); void free() => bindings.free(_commitPointer);

View file

@ -585,7 +585,7 @@ class Repository {
required String updateRef, required String updateRef,
required String message, required String message,
required Signature author, required Signature author,
required Signature commiter, required Signature committer,
required Tree tree, required Tree tree,
required List<Commit> parents, required List<Commit> parents,
String? messageEncoding, String? messageEncoding,
@ -595,7 +595,7 @@ class Repository {
updateRef: updateRef, updateRef: updateRef,
message: message, message: message,
author: author, author: author,
committer: commiter, committer: committer,
tree: tree, tree: tree,
parents: parents, parents: parents,
messageEncoding: messageEncoding, messageEncoding: messageEncoding,

View file

@ -10,7 +10,7 @@ void main() {
late Repository repo; late Repository repo;
late Directory tmpDir; late Directory tmpDir;
late Signature author; late Signature author;
late Signature commiter; late Signature committer;
late Tree tree; late Tree tree;
late Oid tip; late Oid tip;
const message = "Commit message.\n\nSome description.\n"; const message = "Commit message.\n\nSome description.\n";
@ -23,7 +23,7 @@ void main() {
email: 'author@email.com', email: 'author@email.com',
time: 123, time: 123,
); );
commiter = Signature.create( committer = Signature.create(
name: 'Commiter', name: 'Commiter',
email: 'commiter@email.com', email: 'commiter@email.com',
time: 124, time: 124,
@ -37,7 +37,7 @@ void main() {
tearDown(() { tearDown(() {
author.free(); author.free();
commiter.free(); committer.free();
tree.free(); tree.free();
repo.free(); repo.free();
tmpDir.deleteSync(recursive: true); 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<LibGit2Error>()));
});
test('successfully reverts commit', () { test('successfully reverts commit', () {
final to = repo.lookupCommit( final to = repo.lookupCommit(
repo['78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'], repo['78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'],
@ -106,7 +112,7 @@ void main() {
updateRef: 'HEAD', updateRef: 'HEAD',
message: message, message: message,
author: author, author: author,
commiter: commiter, committer: committer,
tree: tree, tree: tree,
parents: [parent], parents: [parent],
); );
@ -116,10 +122,14 @@ void main() {
expect(commit.oid, oid); expect(commit.oid, oid);
expect(commit.message, message); expect(commit.message, message);
expect(commit.messageEncoding, 'utf-8'); expect(commit.messageEncoding, 'utf-8');
expect(commit.summary, 'Commit message.');
expect(commit.body, 'Some description.');
expect(commit.author, author); expect(commit.author, author);
expect(commit.committer, commiter); expect(commit.committer, committer);
expect(commit.time, 124); expect(commit.time, 124);
expect(commit.timeOffset, 0);
expect(commit.tree.oid, tree.oid); expect(commit.tree.oid, tree.oid);
expect(commit.treeOid, tree.oid);
expect(commit.parents.length, 1); expect(commit.parents.length, 1);
expect(commit.parents[0], tip); expect(commit.parents[0], tip);
@ -127,12 +137,40 @@ void main() {
parent.free(); 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 <author@email.com> 123 +0000
committer Commiter <commiter@email.com> 124 +0000
Commit message.
Some description.
""";
expect(commit, expected);
parent.free();
});
test('successfully creates commit without parents', () { test('successfully creates commit without parents', () {
final oid = repo.createCommit( final oid = repo.createCommit(
updateRef: 'refs/heads/new', updateRef: 'refs/heads/new',
message: message, message: message,
author: author, author: author,
commiter: commiter, committer: committer,
tree: tree, tree: tree,
parents: [], parents: [],
); );
@ -143,9 +181,9 @@ void main() {
expect(commit.message, message); expect(commit.message, message);
expect(commit.messageEncoding, 'utf-8'); expect(commit.messageEncoding, 'utf-8');
expect(commit.author, author); expect(commit.author, author);
expect(commit.committer, commiter); expect(commit.committer, committer);
expect(commit.time, 124); expect(commit.time, 124);
expect(commit.tree.oid, tree.oid); expect(commit.treeOid, tree.oid);
expect(commit.parents.length, 0); expect(commit.parents.length, 0);
commit.free(); commit.free();
@ -162,7 +200,7 @@ void main() {
repo: repo, repo: repo,
message: message, message: message,
author: author, author: author,
committer: commiter, committer: committer,
tree: tree, tree: tree,
parents: [parent1, parent2], parents: [parent1, parent2],
); );
@ -173,9 +211,9 @@ void main() {
expect(commit.message, message); expect(commit.message, message);
expect(commit.messageEncoding, 'utf-8'); expect(commit.messageEncoding, 'utf-8');
expect(commit.author, author); expect(commit.author, author);
expect(commit.committer, commiter); expect(commit.committer, committer);
expect(commit.time, 124); expect(commit.time, 124);
expect(commit.tree.oid, tree.oid); expect(commit.treeOid, tree.oid);
expect(commit.parents.length, 2); expect(commit.parents.length, 2);
expect(commit.parents[0], tip); expect(commit.parents[0], tip);
expect(commit.parents[1], parent2.oid); expect(commit.parents[1], parent2.oid);
@ -194,7 +232,28 @@ void main() {
updateRef: 'HEAD', updateRef: 'HEAD',
message: message, message: message,
author: author, author: author,
commiter: commiter, committer: committer,
tree: tree,
parents: [parent],
),
throwsA(isA<LibGit2Error>()),
);
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, tree: tree,
parents: [parent], parents: [parent],
), ),
@ -221,7 +280,7 @@ void main() {
expect(amendedCommit.message, 'amended commit\n'); expect(amendedCommit.message, 'amended commit\n');
expect(amendedCommit.author, commit.author); expect(amendedCommit.author, commit.author);
expect(amendedCommit.committer, commit.committer); expect(amendedCommit.committer, commit.committer);
expect(amendedCommit.tree.oid, commit.tree.oid); expect(amendedCommit.treeOid, commit.treeOid);
expect(amendedCommit.parents, commit.parents); expect(amendedCommit.parents, commit.parents);
amendedCommit.free(); amendedCommit.free();
@ -240,7 +299,7 @@ void main() {
message: 'amended commit\n', message: 'amended commit\n',
updateRef: 'HEAD', updateRef: 'HEAD',
author: author, author: author,
committer: commiter, committer: committer,
tree: tree, tree: tree,
); );
final amendedCommit = repo.lookupCommit(amendedOid); final amendedCommit = repo.lookupCommit(amendedOid);
@ -249,8 +308,8 @@ void main() {
expect(amendedCommit.oid, newHead.target); expect(amendedCommit.oid, newHead.target);
expect(amendedCommit.message, 'amended commit\n'); expect(amendedCommit.message, 'amended commit\n');
expect(amendedCommit.author, author); expect(amendedCommit.author, author);
expect(amendedCommit.committer, commiter); expect(amendedCommit.committer, committer);
expect(amendedCommit.tree.oid, tree.oid); expect(amendedCommit.treeOid, tree.oid);
expect(amendedCommit.parents, commit.parents); expect(amendedCommit.parents, commit.parents);
amendedCommit.free(); amendedCommit.free();
@ -297,6 +356,72 @@ void main() {
head.free(); 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<LibGit2Error>()),
);
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<LibGit2Error>()));
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<LibGit2Error>()));
commit.free();
});
test('returns string representation of Commit object', () { test('returns string representation of Commit object', () {
final commit = repo.lookupCommit(tip); final commit = repo.lookupCommit(tip);
expect(commit.toString(), contains('Commit{')); expect(commit.toString(), contains('Commit{'));