feat(commit): add base bindings and api

This commit is contained in:
Aleksey Kulikov 2021-08-24 19:08:12 +03:00
parent 696d55bb3a
commit dc5f510aa5
12 changed files with 485 additions and 17 deletions

View file

@ -1,4 +1,5 @@
export 'src/repository.dart'; export 'src/repository.dart';
export 'src/config.dart'; export 'src/config.dart';
export 'src/signature.dart';
export 'src/error.dart'; export 'src/error.dart';
export 'src/types.dart'; export 'src/types.dart';

View file

@ -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<git_commit> lookup(Pointer<git_repository> repo, Pointer<git_oid> id) {
final out = calloc<Pointer<git_commit>>();
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<git_commit> commit) {
final result = libgit2.git_commit_message_encoding(commit);
if (result == nullptr) {
return 'utf-8';
} else {
return result.cast<Utf8>().toDartString();
}
}
/// Get the full message of a commit.
///
/// The returned message will be slightly prettified by removing any potential leading newlines.
String message(Pointer<git_commit> commit) {
final out = libgit2.git_commit_message(commit);
return out.cast<Utf8>().toDartString();
}
/// Get the id of a commit.
Pointer<git_oid> id(Pointer<git_commit> commit) =>
libgit2.git_commit_id(commit);
/// Get the number of parents of this commit.
int parentCount(Pointer<git_commit> commit) =>
libgit2.git_commit_parentcount(commit);
/// Get the oid of a specified parent for a commit.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_oid> parentId(Pointer<git_commit> 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<git_commit> commit) => libgit2.git_commit_time(commit);
/// Get the committer of a commit.
Pointer<git_signature> committer(Pointer<git_commit> commit) {
return libgit2.git_commit_committer(commit);
}
/// Get the author of a commit.
Pointer<git_signature> author(Pointer<git_commit> commit) {
return libgit2.git_commit_author(commit);
}
/// Get the id of the tree pointed to by a commit.
Pointer<git_oid> tree(Pointer<git_commit> commit) {
return libgit2.git_commit_tree_id(commit);
}
/// Get the repository that contains the commit.
Pointer<git_repository> owner(Pointer<git_commit> 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<git_commit> commit) => libgit2.git_commit_free(commit);

View file

@ -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<git_signature> create(
String name,
String email,
int time,
int offset,
) {
final out = calloc<Pointer<git_signature>>();
final nameC = name.toNativeUtf8().cast<Int8>();
final emailC = email.toNativeUtf8().cast<Int8>();
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<git_signature> now(String name, String email) {
final out = calloc<Pointer<git_signature>>();
final nameC = name.toNativeUtf8().cast<Int8>();
final emailC = email.toNativeUtf8().cast<Int8>();
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<git_signature> sig) => libgit2.git_signature_free(sig);

78
lib/src/commit.dart Normal file
View file

@ -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<git_repository> repo, Pointer<git_oid> oid) {
libgit2.git_libgit2_init();
_commitPointer = bindings.lookup(repo, oid);
}
/// Pointer to memory address for allocated commit object.
late final Pointer<git_commit> _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<Commit> get parents {
var parents = <Commit>[];
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();
}
}

View file

@ -4,11 +4,11 @@ import 'bindings/libgit2_bindings.dart';
import 'bindings/config.dart' as bindings; import 'bindings/config.dart' as bindings;
import 'util.dart'; import 'util.dart';
/// [Config] provides management of global configuration options
/// (system, global, XDG, excluding repository config)
class Config { class Config {
/// Initializes a new instance of [Config] class from provided /// Initializes a new instance of [Config] class from provided
/// pointer to config object in memory. /// pointer to config object in memory.
///
/// Should be freed with `free()` to release allocated memory.
Config(this._configPointer) { Config(this._configPointer) {
libgit2.git_libgit2_init(); 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 /// [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). /// 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]) { Config.open([String? path]) {
libgit2.git_libgit2_init(); libgit2.git_libgit2_init();
@ -39,6 +39,8 @@ class Config {
/// ///
/// Opens the system configuration file. /// Opens the system configuration file.
/// ///
/// Should be freed with `free()` to release allocated memory.
///
/// Throws a [LibGit2Error] if error occured. /// Throws a [LibGit2Error] if error occured.
Config.system() { Config.system() {
libgit2.git_libgit2_init(); libgit2.git_libgit2_init();
@ -51,6 +53,8 @@ class Config {
/// ///
/// Opens the global configuration file. /// Opens the global configuration file.
/// ///
/// Should be freed with `free()` to release allocated memory.
///
/// Throws an error if file has not been found. /// Throws an error if file has not been found.
Config.global() { Config.global() {
libgit2.git_libgit2_init(); libgit2.git_libgit2_init();
@ -63,6 +67,8 @@ class Config {
/// ///
/// Opens the global XDG configuration file. /// Opens the global XDG configuration file.
/// ///
/// Should be freed with `free()` to release allocated memory.
///
/// Throws a [LibGit2Error] if error occured. /// Throws a [LibGit2Error] if error occured.
Config.xdg() { Config.xdg() {
libgit2.git_libgit2_init(); libgit2.git_libgit2_init();

View file

@ -97,19 +97,19 @@ class Index {
late final Oid oid; late final Oid oid;
late final Tree tree; late final Tree tree;
if (target is Oid) { if (target is Oid) {
tree = Tree(bindings.owner(_indexPointer), target.pointer); tree = Tree.lookup(bindings.owner(_indexPointer), target.pointer);
} else if (target is Tree) { } else if (target is Tree) {
tree = target; tree = target;
} else if (isValidShaHex(target as String)) { } else if (isValidShaHex(target as String)) {
if (target.length == 40) { if (target.length == 40) {
oid = Oid.fromSHA(target); oid = Oid.fromSHA(target);
tree = Tree(bindings.owner(_indexPointer), oid.pointer); tree = Tree.lookup(bindings.owner(_indexPointer), oid.pointer);
} else { } else {
final shortOid = Oid.fromSHAn(target); final shortOid = Oid.fromSHAn(target);
final odb = Odb(repo_bindings.odb(bindings.owner(_indexPointer))); final odb = Odb(repo_bindings.odb(bindings.owner(_indexPointer)));
oid = Oid(odb.existsPrefix(shortOid.pointer, target.length)); oid = Oid(odb.existsPrefix(shortOid.pointer, target.length));
odb.free(); odb.free();
tree = Tree(bindings.owner(_indexPointer), oid.pointer); tree = Tree.lookup(bindings.owner(_indexPointer), oid.pointer);
} }
} else { } else {
throw ArgumentError.value( throw ArgumentError.value(

View file

@ -1,4 +1,5 @@
import 'dart:ffi'; import 'dart:ffi';
import 'commit.dart';
import 'config.dart'; import 'config.dart';
import 'index.dart'; import 'index.dart';
import 'odb.dart'; import 'odb.dart';
@ -8,7 +9,6 @@ import 'bindings/libgit2_bindings.dart';
import 'bindings/repository.dart' as bindings; import 'bindings/repository.dart' as bindings;
import 'util.dart'; import 'util.dart';
/// A Repository is the primary interface into a git repository
class Repository { class Repository {
/// Initializes a new instance of the [Repository] class by creating a new /// Initializes a new instance of the [Repository] class by creating a new
/// Git repository in the given folder. /// Git repository in the given folder.
@ -270,14 +270,7 @@ class Repository {
oid = target as Oid; oid = target as Oid;
isDirect = true; isDirect = true;
} else if (isValidShaHex(target as String)) { } else if (isValidShaHex(target as String)) {
if (target.length == 40) { oid = _getOid(target);
oid = Oid.fromSHA(target);
} else {
final shortOid = Oid.fromSHAn(target);
final odb = this.odb;
oid = Oid(odb.existsPrefix(shortOid.pointer, target.length));
odb.free();
}
isDirect = true; isDirect = true;
} else { } else {
isDirect = false; isDirect = false;
@ -313,4 +306,33 @@ class Repository {
/// ///
/// Throws a [LibGit2Error] if error occured. /// Throws a [LibGit2Error] if error occured.
Odb get odb => Odb(bindings.odb(_repoPointer)); 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;
}
} }

71
lib/src/signature.dart Normal file
View file

@ -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<git_signature> _signaturePointer;
/// Returns full name of the author.
String get name => _signaturePointer.ref.name.cast<Utf8>().toDartString();
/// Returns email of the author.
String get email => _signaturePointer.ref.email.cast<Utf8>().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();
}
}

View file

@ -4,11 +4,17 @@ import 'bindings/tree.dart' as bindings;
import 'util.dart'; import 'util.dart';
class Tree { 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 /// Initializes a new instance of [Tree] class from provided
/// pointers to repository object and oid object in memory. /// pointers to repository object and oid object in memory.
/// ///
/// Should be freed with `free()` to release allocated memory. /// Should be freed with `free()` to release allocated memory.
Tree(Pointer<git_repository> repo, Pointer<git_oid> id) { Tree.lookup(Pointer<git_repository> repo, Pointer<git_oid> id) {
libgit2.git_libgit2_init(); libgit2.git_libgit2_init();
_treePointer = bindings.lookup(repo, id); _treePointer = bindings.lookup(repo, id);
} }

View file

@ -1,4 +1,3 @@
enum ReferenceType { direct, symbolic } enum ReferenceType { direct, symbolic }
enum GitFilemode { undreadable, tree, blob, blobExecutable, link, commit } enum GitFilemode { undreadable, tree, blob, blobExecutable, link, commit }

79
test/commit_test.dart Normal file
View file

@ -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>());
commit.free();
});
test('successful when sha hex is short', () {
final commit = repo[mergeCommit.substring(0, 5)];
expect(commit, isA<Commit>());
commit.free();
});
test('throws when provided sha hex is invalid', () {
expect(() => repo['invalid'], throwsA(isA<ArgumentError>()));
});
test('throws when nothing found', () {
expect(() => repo['970ae5c'], throwsA(isA<LibGit2Error>()));
});
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();
});
});
});
}

63
test/signature_test.dart Normal file
View file

@ -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<Signature>());
});
test('successfully creates without provided time and offset', () {
final defaultSignature =
Signature.create(name: 'Name', email: 'email@example.com');
expect(defaultSignature, isA<Signature>());
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();
});
});
}