diff --git a/lib/src/bindings/odb.dart b/lib/src/bindings/odb.dart index f62ba9d..15cf904 100644 --- a/lib/src/bindings/odb.dart +++ b/lib/src/bindings/odb.dart @@ -1,9 +1,53 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; +import 'oid.dart' as oid_bindings; import '../error.dart'; +import '../oid.dart'; import 'libgit2_bindings.dart'; import '../util.dart'; +/// Create a new object database with no backends. +/// +/// Before the ODB can be used for read/writing, a custom database backend must be +/// manually added. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer create() { + final out = calloc>(); + final error = libgit2.git_odb_new(out); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Add an on-disk alternate to an existing Object DB. +/// +/// Note that the added path must point to an `objects`, not to a full repository, +/// to use it as an alternate store. +/// +/// Alternate backends are always checked for objects after all the main backends +/// have been exhausted. +/// +/// Writing is disabled on alternate backends. +/// +/// Throws a [LibGit2Error] if error occured. +void addDiskAlternate({ + required Pointer odbPointer, + required String path, +}) { + final pathC = path.toNativeUtf8().cast(); + final error = libgit2.git_odb_add_disk_alternate(odbPointer, pathC); + + calloc.free(pathC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + /// Determine if an object can be found in the object database by an abbreviated object ID. /// /// Throws a [LibGit2Error] if error occured. @@ -27,5 +71,175 @@ Pointer existsPrefix({ } } +/// Determine if the given object can be found in the object database. +bool exists({ + required Pointer odbPointer, + required Pointer oidPointer, +}) { + final result = libgit2.git_odb_exists(odbPointer, oidPointer); + return result == 1 ? true : false; +} + +/// List of objects in the database. +var _objects = []; + +/// List all objects available in the database. +/// +/// Throws a [LibGit2Error] if error occured. +List objects(Pointer odb) { + const except = -1; + final payload = calloc>(); + final cb = + Pointer.fromFunction, Pointer)>( + _forEachCb, except); + final error = libgit2.git_odb_foreach(odb, cb, payload.cast()); + + if (error < 0) { + _objects.clear(); + throw LibGit2Error(libgit2.git_error_last()); + } + + final result = _objects.toList(growable: false); + _objects.clear(); + + return result; +} + +/// The callback to call for each object. +int _forEachCb( + Pointer oid, + Pointer payload, +) { + final _oid = oid_bindings.copy(oid); + _objects.add(Oid(_oid)); + return 0; +} + +/// Read an object from the database. +/// +/// This method queries all available ODB backends trying to read the given OID. +/// +/// The returned object is reference counted and internally cached, so it should be +/// closed by the user once it's no longer in use. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer read({ + required Pointer odbPointer, + required Pointer oidPointer, +}) { + final out = calloc>(); + final error = libgit2.git_odb_read(out, odbPointer, oidPointer); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Return the OID of an ODB object. +/// +/// This is the OID from which the object was read from. +Pointer objectId(Pointer object) { + return libgit2.git_odb_object_id(object); +} + +/// Return the type of an ODB object. +int objectType(Pointer object) { + return libgit2.git_odb_object_type(object); +} + +/// Return the data of an ODB object. +/// +/// This is the uncompressed, raw data as read from the ODB, without the leading header. +String objectData(Pointer object) { + final out = libgit2.git_odb_object_data(object); + + return out.cast().toDartString(); +} + +/// Return the size of an ODB object. +/// +/// This is the real size of the `data` buffer, not the actual size of the object. +int objectSize(Pointer object) { + return libgit2.git_odb_object_size(object); +} + +/// Close an ODB object. +/// +/// This method must always be called once a odb object is no longer needed, +/// otherwise memory will leak. +void objectFree(Pointer object) { + libgit2.git_odb_object_free(object); +} + +/// Write raw data to into the object database. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer write({ + required Pointer odbPointer, + required int type, + required String data, +}) { + final stream = calloc>(); + final streamError = libgit2.git_odb_open_wstream( + stream, + odbPointer, + data.length, + type, + ); + + if (streamError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + final buffer = data.toNativeUtf8().cast(); + final writeError = libgit2.git_odb_stream_write( + stream.value, + buffer, + data.length, + ); + + if (writeError < 0) { + libgit2.git_odb_stream_free(stream.value); + throw LibGit2Error(libgit2.git_error_last()); + } + + final out = calloc(); + final finalizeError = libgit2.git_odb_stream_finalize_write( + out, + stream.value, + ); + + calloc.free(buffer); + libgit2.git_odb_stream_free(stream.value); + + if (finalizeError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} + +/// Get the number of ODB backend objects. +int backendsCount(Pointer odb) => libgit2.git_odb_num_backends(odb); + +/// Lookup an ODB backend object by index. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer getBackend({ + required Pointer odbPointer, + required int position, +}) { + final out = calloc>(); + final error = libgit2.git_odb_get_backend(out, odbPointer, position); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + 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 c1e244a..9b25100 100644 --- a/lib/src/bindings/oid.dart +++ b/lib/src/bindings/oid.dart @@ -84,3 +84,17 @@ int compare({ }) { return libgit2.git_oid_cmp(aPointer, bPointer); } + +/// Copy an oid from one structure to another. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer copy(Pointer src) { + final out = calloc(); + final error = libgit2.git_oid_cpy(out, src); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} diff --git a/lib/src/odb.dart b/lib/src/odb.dart index 91bc4e6..1cba960 100644 --- a/lib/src/odb.dart +++ b/lib/src/odb.dart @@ -1,31 +1,138 @@ import 'dart:ffi'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'package:libgit2dart/src/git_types.dart'; + import 'bindings/libgit2_bindings.dart'; import 'bindings/odb.dart' as bindings; +import 'oid.dart'; +import 'util.dart'; class Odb { /// Initializes a new instance of [Odb] class from provided /// pointer to Odb object in memory. - const Odb(this._odbPointer); + Odb(this._odbPointer); - final Pointer _odbPointer; + /// Initializes a new instance of [Odb] class by creating a new object database with + /// no backends. + /// + /// Before the ODB can be used for read/writing, a custom database backend must be + /// manually added. + /// + /// Throws a [LibGit2Error] if error occured. + Odb.create() { + libgit2.git_libgit2_init(); + + _odbPointer = bindings.create(); + } + + late final Pointer _odbPointer; /// Pointer to memory address for allocated oid object. Pointer get pointer => _odbPointer; - /// Determine if an object can be found in the object database by an abbreviated object ID. + /// Adds an on-disk alternate to an existing Object DB. + /// + /// Note that the added [path] must point to an `objects`, not to a full repository, + /// to use it as an alternate store. + /// + /// Alternate backends are always checked for objects after all the main backends + /// have been exhausted. + /// + /// Writing is disabled on alternate backends. /// /// Throws a [LibGit2Error] if error occured. - Pointer existsPrefix({ - required Pointer shortOidPointer, - required int length, - }) { - return bindings.existsPrefix( + void addDiskAlternate(String path) { + bindings.addDiskAlternate( odbPointer: _odbPointer, - shortOidPointer: shortOidPointer, - length: length, + path: path, ); } + /// Returns list of all objects available in the database. + /// + /// Throws a [LibGit2Error] if error occured. + List get objects => bindings.objects(_odbPointer); + + /// Checks if the given object can be found in the object database. + bool contains(Oid oid) { + return bindings.exists(odbPointer: _odbPointer, oidPointer: oid.pointer); + } + + /// Reads an object from the database. + /// + /// This method queries all available ODB backends trying to read the given [oid]. + /// + /// The returned object should be freed by the user once it's no longer in use. + /// + /// Throws a [LibGit2Error] if error occured. + OdbObject read(Oid oid) { + return OdbObject(bindings.read( + odbPointer: _odbPointer, + oidPointer: oid.pointer, + )); + } + + /// Writes raw [data] to into the object database. + /// + /// [type] should be one of [GitObject.blob], [GitObject.commit], [GitObject.tag], + /// [GitObject.tree]. + /// + /// Throws a [LibGit2Error] if error occured or [ArgumentError] if provided type is invalid. + Oid write({required GitObject type, required String data}) { + 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: type.value, + data: data, + )); + } + } + /// Releases memory allocated for odb object. void free() => bindings.free(_odbPointer); } + +class OdbObject { + /// Initializes a new instance of the [OdbObject] class from + /// provided pointer to odbObject object in memory. + const OdbObject(this._odbObjectPointer); + + /// Pointer to memory address for allocated odbObject object. + final Pointer _odbObjectPointer; + + /// Returns the OID of an ODB object. + /// + /// This is the OID from which the object was read from. + Oid get id => Oid(bindings.objectId(_odbObjectPointer)); + + /// Returns the type of an ODB object. + GitObject get type { + late GitObject result; + final typeInt = bindings.objectType(_odbObjectPointer); + for (var type in GitObject.values) { + if (typeInt == type.value) { + result = type; + break; + } + } + return result; + } + + /// Returns the data of an ODB object. + /// + /// This is the uncompressed, raw data as read from the ODB, without the leading header. + String get data => bindings.objectData(_odbObjectPointer); + + /// Returns the size of an ODB object. + /// + /// This is the real size of the `data` buffer, not the actual size of the object. + int get size => bindings.objectSize(_odbObjectPointer); + + /// Releases memory allocated for odbObject object. + void free() => bindings.objectFree(_odbObjectPointer); +} diff --git a/lib/src/oid.dart b/lib/src/oid.dart index 6db8d82..c954622 100644 --- a/lib/src/oid.dart +++ b/lib/src/oid.dart @@ -1,6 +1,7 @@ import 'dart:ffi'; import 'bindings/libgit2_bindings.dart'; import 'bindings/oid.dart' as bindings; +import 'bindings/odb.dart' as odb_bindings; import 'repository.dart'; import 'util.dart'; @@ -22,7 +23,8 @@ class Oid { _oidPointer = bindings.fromSHA(sha); } else { final odb = repo.odb; - _oidPointer = odb.existsPrefix( + _oidPointer = odb_bindings.existsPrefix( + odbPointer: odb.pointer, shortOidPointer: bindings.fromStrN(sha), length: sha.length, ); diff --git a/test/odb_test.dart b/test/odb_test.dart index 0f0b03f..5abb18f 100644 --- a/test/odb_test.dart +++ b/test/odb_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:libgit2dart/src/git_types.dart'; import 'package:test/test.dart'; import 'package:libgit2dart/libgit2dart.dart'; import 'helpers/util.dart'; @@ -7,7 +8,9 @@ import 'helpers/util.dart'; void main() { late Repository repo; late Directory tmpDir; - const lastCommit = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'; + const commitSha = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'; + const blobSha = '9c78c21d6680a7ffebc76f7ac68cacc11d8f48bc'; + const blobContent = 'Feature edit\n'; setUp(() async { tmpDir = await setupRepo(Directory('test/assets/testrepo/')); @@ -26,12 +29,74 @@ void main() { odb.free(); }); + test('successfully creates new odb with no backends', () { + final odb = Odb.create(); + expect(odb, isA()); + odb.free(); + }); + + test('successfully adds disk alternate', () { + final oid = Oid.fromSHA(repo: repo, sha: blobSha); + final odb = Odb.create(); + odb.addDiskAlternate('${repo.workdir}.git/objects/'); + + expect(odb.contains(oid), true); + + odb.free(); + }); + + test('successfully reads object', () { + final oid = Oid.fromSHA(repo: repo, sha: blobSha); + final odb = repo.odb; + final object = odb.read(oid); + + expect(object.id, oid); + expect(object.type, GitObject.blob); + expect(object.data, blobContent); + expect(object.size, 13); + + object.free(); + odb.free(); + }); + + test('returns list of all objects oid\'s in database', () { + final oid = Oid.fromSHA(repo: repo, sha: commitSha); + final odb = repo.odb; + + expect(odb.objects, isNot(isEmpty)); + expect(odb.objects.contains(oid), true); + + odb.free(); + }); + test('finds object by short oid', () { final oid = Oid.fromSHA( repo: repo, - sha: lastCommit.substring(0, 5), + sha: commitSha.substring(0, 5), ); - expect(oid.sha, lastCommit); + expect(oid.sha, commitSha); + }); + + test('successfully writes data', () { + final odb = repo.odb; + final oid = odb.write(type: GitObject.blob, data: 'testing'); + final object = odb.read(oid); + + expect(odb.contains(oid), true); + expect(object.data, 'testing'); + + object.free(); + odb.free(); + }); + + test('throws when trying to write with invalid object type', () { + final odb = repo.odb; + expect( + () => odb.write(type: GitObject.any, data: 'testing'), + throwsA(isA()), + ); + + odb.free(); }); }); }