From 90e99faf811946a5066d8c0b334e7c1abc081340 Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Thu, 7 Oct 2021 12:14:20 +0300 Subject: [PATCH] feat(packbuilder): add bindings and api --- lib/src/bindings/packbuilder.dart | 107 ++++++++++++++++++++++++++++++ lib/src/packbuilder.dart | 73 ++++++++++++++++++++ lib/src/repository.dart | 36 ++++++++++ test/packbuilder_test.dart | 104 +++++++++++++++++++++++++++++ 4 files changed, 320 insertions(+) create mode 100644 lib/src/bindings/packbuilder.dart create mode 100644 lib/src/packbuilder.dart create mode 100644 test/packbuilder_test.dart diff --git a/lib/src/bindings/packbuilder.dart b/lib/src/bindings/packbuilder.dart new file mode 100644 index 0000000..3d7a219 --- /dev/null +++ b/lib/src/bindings/packbuilder.dart @@ -0,0 +1,107 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'libgit2_bindings.dart'; +import '../error.dart'; +import '../util.dart'; + +/// Initialize a new packbuilder. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer init(Pointer repo) { + final out = calloc>(); + final error = libgit2.git_packbuilder_new(out, repo); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Insert a single object. +/// +/// For an optimal pack it's mandatory to insert objects in recency order, +/// commits followed by trees and blobs. +/// +/// Throws a [LibGit2Error] if error occured. +void add({ + required Pointer packbuilderPointer, + required Pointer oidPointer, +}) { + final error = libgit2.git_packbuilder_insert( + packbuilderPointer, + oidPointer, + nullptr, + ); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Recursively insert an object and its referenced objects. +/// +/// Insert the object as well as any object it references. +/// +/// Throws a [LibGit2Error] if error occured. +void addRecursively({ + required Pointer packbuilderPointer, + required Pointer oidPointer, +}) { + final error = libgit2.git_packbuilder_insert_recur( + packbuilderPointer, + oidPointer, + nullptr, + ); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Write the new pack and corresponding index file to path. +/// +/// Throws a [LibGit2Error] if error occured. +void write({ + required Pointer packbuilderPointer, + String? path, +}) { + final pathC = path?.toNativeUtf8().cast() ?? nullptr; + final error = libgit2.git_packbuilder_write( + packbuilderPointer, + pathC, + 0, + nullptr, + nullptr, + ); + + calloc.free(pathC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Get the total number of objects the packbuilder will write out. +int length(Pointer pb) { + return libgit2.git_packbuilder_object_count(pb); +} + +/// Get the number of objects the packbuilder has already written out. +int writtenCount(Pointer pb) { + return libgit2.git_packbuilder_written(pb); +} + +/// Set number of threads to spawn. +/// +/// By default, libgit2 won't spawn any threads at all; when set to 0, +/// libgit2 will autodetect the number of CPUs. +int setThreads({ + required Pointer packbuilderPointer, + required int number, +}) { + return libgit2.git_packbuilder_set_threads(packbuilderPointer, number); +} + +/// Free the packbuilder and all associated data. +void free(Pointer pb) => libgit2.git_packbuilder_free(pb); diff --git a/lib/src/packbuilder.dart b/lib/src/packbuilder.dart new file mode 100644 index 0000000..2f7c5ec --- /dev/null +++ b/lib/src/packbuilder.dart @@ -0,0 +1,73 @@ +import 'dart:ffi'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'bindings/libgit2_bindings.dart'; +import 'bindings/packbuilder.dart' as bindings; + +class PackBuilder { + /// Initializes a new instance of [PackBuilder] class. + /// + /// Should be freed with `free()`. + /// + /// Throws a [LibGit2Error] if error occured. + PackBuilder(Repository repo) { + _packbuilderPointer = bindings.init(repo.pointer); + } + + late final Pointer _packbuilderPointer; + + /// Pointer to memory address for allocated packbuilder object. + Pointer get pointer => _packbuilderPointer; + + /// Adds a single object. + /// + /// For an optimal pack it's mandatory to add objects in recency order, + /// commits followed by trees and blobs. + /// + /// Throws a [LibGit2Error] if error occured. + void add(Oid oid) { + bindings.add( + packbuilderPointer: _packbuilderPointer, + oidPointer: oid.pointer, + ); + } + + /// Recursively adds an object and its referenced objects. + /// + /// Adds the object as well as any object it references. + /// + /// Throws a [LibGit2Error] if error occured. + void addRecursively(Oid oid) { + bindings.addRecursively( + packbuilderPointer: _packbuilderPointer, + oidPointer: oid.pointer, + ); + } + + /// Writes the new pack and corresponding index file to [path] if provided + /// or default location. + /// + /// Throws a [LibGit2Error] if error occured. + void write(String? path) { + bindings.write(packbuilderPointer: _packbuilderPointer, path: path); + } + + /// Returns the total number of objects the packbuilder will write out. + int get length => bindings.length(_packbuilderPointer); + + /// Returns the number of objects the packbuilder has already written out. + int get writtenLength => bindings.writtenCount(_packbuilderPointer); + + /// Sets and returns the number of threads to spawn. + /// + /// By default, libgit2 won't spawn any threads at all; when set to 0, + /// libgit2 will autodetect the number of CPUs. + int setThreads(int number) { + return bindings.setThreads( + packbuilderPointer: _packbuilderPointer, + number: number, + ); + } + + /// Releases memory allocated for packbuilder object. + void free() => bindings.free(_packbuilderPointer); +} diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 402b821..12ee36c 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -1,6 +1,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; import 'package:libgit2dart/libgit2dart.dart'; +import 'package:libgit2dart/src/packbuilder.dart'; import 'bindings/libgit2_bindings.dart'; import 'bindings/repository.dart' as bindings; import 'bindings/merge.dart' as merge_bindings; @@ -1297,4 +1298,39 @@ class Repository { return result; } + + /// Packs the objects in the odb chosen by the [packDelegate] function and + /// writes .pack and .idx files for them into provided [path] or default location. + /// + /// Returns the number of objects written to the pack. + /// + /// [packDelegate] is a function that will provide what objects should be added to the + /// pack builder. Default is add all objects. + /// + /// [threads] is number of threads the PackBuilder will spawn. Default is none. 0 will + /// let libgit2 to autodetect number of CPUs. + /// + /// Throws a [LibGit2Error] if error occured. + int pack( + {String? path, void Function(PackBuilder)? packDelegate, int? threads}) { + void packAll(PackBuilder packbuilder) { + for (var object in odb.objects) { + packbuilder.add(object); + } + } + + final _packDelegate = packDelegate ?? packAll; + + final packbuilder = PackBuilder(this); + if (threads != null) { + packbuilder.setThreads(threads); + } + _packDelegate(packbuilder); + packbuilder.write(path); + final result = packbuilder.writtenLength; + + packbuilder.free(); + + return result; + } } diff --git a/test/packbuilder_test.dart b/test/packbuilder_test.dart new file mode 100644 index 0000000..6fcc598 --- /dev/null +++ b/test/packbuilder_test.dart @@ -0,0 +1,104 @@ +import 'dart:io'; +import 'package:libgit2dart/src/packbuilder.dart'; +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'helpers/util.dart'; + +void main() { + late Repository repo; + late Directory tmpDir; + + setUp(() async { + tmpDir = await setupRepo(Directory('test/assets/testrepo/')); + repo = Repository.open(tmpDir.path); + }); + + tearDown(() async { + repo.free(); + await tmpDir.delete(recursive: true); + }); + + group('PackBuilder', () { + test('successfully initializes', () { + final packbuilder = PackBuilder(repo); + + expect(packbuilder, isA()); + expect(packbuilder.length, 0); + + packbuilder.free(); + }); + + test('successfully adds objects', () { + final packbuilder = PackBuilder(repo); + final odb = repo.odb; + + packbuilder.add(odb.objects[0]); + expect(packbuilder.length, 1); + + packbuilder.add(odb.objects[1]); + expect(packbuilder.length, 2); + + odb.free(); + packbuilder.free(); + }); + + test('successfully adds objects recursively', () { + final packbuilder = PackBuilder(repo); + final oid = Oid.fromSHA(repo: repo, sha: 'f17d0d48'); + + packbuilder.addRecursively(oid); + expect(packbuilder.length, 3); + + packbuilder.free(); + }); + + test('successfully sets number of threads', () { + final packbuilder = PackBuilder(repo); + + expect(packbuilder.setThreads(1), 1); + + packbuilder.free(); + }); + + test('successfully packs with default arguments', () { + final odb = repo.odb; + final objectsCount = odb.objects.length; + final writtenCount = repo.pack(); + + expect(writtenCount, objectsCount); + + odb.free(); + }); + + test('successfully packs into provided path', () { + final odb = repo.odb; + final objectsCount = odb.objects.length; + Directory('${repo.workdir}test-pack').createSync(); + + final writtenCount = repo.pack(path: '${repo.workdir}test-pack'); + expect(writtenCount, objectsCount); + expect( + Directory('${repo.workdir}test-pack').listSync().isNotEmpty, + true, + ); + + odb.free(); + }); + + test('successfully packs with provided packDelegate', () { + void packDelegate(PackBuilder packBuilder) { + for (var branchName in repo.branches.list()) { + final branch = repo.references['refs/heads/$branchName']; + for (var commit in repo.log(sha: branch.target.sha)) { + packBuilder.addRecursively(commit.id); + commit.free(); + } + branch.free(); + } + } + + final writtenCount = repo.pack(packDelegate: packDelegate); + expect(writtenCount, 18); + }); + }); +}