diff --git a/lib/libgit2dart.dart b/lib/libgit2dart.dart index dae8dc2..2ff75fa 100644 --- a/lib/libgit2dart.dart +++ b/lib/libgit2dart.dart @@ -9,5 +9,6 @@ export 'src/reflog.dart'; export 'src/tree.dart'; export 'src/signature.dart'; export 'src/revwalk.dart'; +export 'src/blob.dart'; export 'src/error.dart'; export 'src/enums.dart'; diff --git a/lib/src/bindings/blob.dart b/lib/src/bindings/blob.dart new file mode 100644 index 0000000..67e55df --- /dev/null +++ b/lib/src/bindings/blob.dart @@ -0,0 +1,108 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import 'libgit2_bindings.dart'; +import '../util.dart'; + +/// Lookup a blob object from a repository. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer lookup(Pointer repo, Pointer id) { + final out = calloc>(); + final error = libgit2.git_blob_lookup(out, repo, id); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Get the id of a blob. +Pointer id(Pointer blob) => libgit2.git_blob_id(blob); + +/// Determine if the blob content is most certainly binary or not. +/// +/// The heuristic used to guess if a file is binary is taken from core git: +/// Searching for NUL bytes and looking for a reasonable ratio of printable to +/// non-printable characters among the first 8000 bytes. +bool isBinary(Pointer blob) { + final result = libgit2.git_blob_is_binary(blob); + return result == 1 ? true : false; +} + +/// Get a read-only buffer with the raw content of a blob. +/// +/// A pointer to the raw content of a blob is returned; this pointer is owned +/// internally by the object and shall not be free'd. The pointer may be invalidated +/// at a later time. +String content(Pointer blob) { + final result = libgit2.git_blob_rawcontent(blob); + return result.cast().toDartString(); +} + +/// Get the size in bytes of the contents of a blob. +int size(Pointer blob) => libgit2.git_blob_rawsize(blob); + +/// Write content of a string buffer to the ODB as a blob. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer create( + Pointer repo, + String buffer, + int len, +) { + final out = calloc(); + final bufferC = buffer.toNativeUtf8().cast(); + final error = libgit2.git_blob_create_from_buffer(out, repo, bufferC, len); + + calloc.free(bufferC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} + +/// Read a file from the working folder of a repository and write it to the +/// Object Database as a loose blob. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer createFromWorkdir( + Pointer repo, + String relativePath, +) { + final out = calloc(); + final relativePathC = relativePath.toNativeUtf8().cast(); + final error = libgit2.git_blob_create_from_workdir(out, repo, relativePathC); + + calloc.free(relativePathC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} + +/// Read a file from the filesystem and write its content to the Object Database as a loose blob. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer createFromDisk( + Pointer repo, + String path, +) { + final out = calloc(); + final pathC = path.toNativeUtf8().cast(); + final error = libgit2.git_blob_create_from_disk(out, repo, pathC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} + +/// Close an open blob to release memory. +void free(Pointer blob) => libgit2.git_blob_free(blob); diff --git a/lib/src/blob.dart b/lib/src/blob.dart new file mode 100644 index 0000000..5a7c595 --- /dev/null +++ b/lib/src/blob.dart @@ -0,0 +1,61 @@ +import 'dart:ffi'; +import 'bindings/libgit2_bindings.dart'; +import 'bindings/blob.dart' as bindings; +import 'oid.dart'; +import 'repository.dart'; + +class Blob { + /// Initializes a new instance of [Blob] class from provided + /// [Repository] and [Oid] objects. + /// + /// Should be freed with `free()` to release allocated memory. + Blob.lookup(Repository repo, Oid oid) { + _blobPointer = bindings.lookup(repo.pointer, oid.pointer); + } + + late final Pointer _blobPointer; + + /// Pointer to memory address for allocated blob object. + Pointer get pointer => _blobPointer; + + /// Creates a new blob from a [content] string and writes it to ODB. + /// + /// Throws a [LibGit2Error] if error occured. + static Oid create(Repository repo, String content) { + return Oid(bindings.create(repo.pointer, content, content.length)); + } + + /// Creates a new blob from the file in working directory of a repository and writes + /// it to the ODB. Provided [relativePath] should be relative to the working directory. + /// + /// Throws a [LibGit2Error] if error occured. + static Oid createFromWorkdir(Repository repo, String relativePath) { + return Oid(bindings.createFromWorkdir(repo.pointer, relativePath)); + } + + /// Creates a new blob from the file in filesystem and writes it to the ODB. + /// + /// Throws a [LibGit2Error] if error occured. + static Oid createFromDisk(Repository repo, String path) { + return Oid(bindings.createFromDisk(repo.pointer, path)); + } + + /// Returns the Oid of the blob. + Oid get id => Oid(bindings.id(_blobPointer)); + + /// Determines if the blob content is most certainly binary or not. + /// + /// The heuristic used to guess if a file is binary is taken from core git: + /// Searching for NUL bytes and looking for a reasonable ratio of printable to + /// non-printable characters among the first 8000 bytes. + bool get isBinary => bindings.isBinary(_blobPointer); + + /// Returns a read-only buffer with the raw content of a blob. + String get content => bindings.content(_blobPointer); + + /// Returns the size in bytes of the contents of a blob. + int get size => bindings.size(_blobPointer); + + /// Releases memory allocated for blob object. + void free() => bindings.free(_blobPointer); +} diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 4f3aa17..dc21fb6 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -10,6 +10,7 @@ import 'oid.dart'; import 'reference.dart'; import 'revwalk.dart'; import 'revparse.dart'; +import 'blob.dart'; import 'enums.dart'; import 'util.dart'; @@ -379,4 +380,21 @@ class Repository { two.pointer, )); } + + /// Creates a new blob from a [content] string and writes it to ODB. + /// + /// Throws a [LibGit2Error] if error occured. + Oid createBlob(String content) => Blob.create(this, content); + + /// Creates a new blob from the file in working directory of a repository and writes + /// it to the ODB. Provided [path] should be relative to the working directory. + /// + /// Throws a [LibGit2Error] if error occured. + Oid createBlobFromWorkdir(String relativePath) => + Blob.createFromWorkdir(this, relativePath); + + /// Creates a new blob from the file in filesystem and writes it to the ODB. + /// + /// Throws a [LibGit2Error] if error occured. + Oid createBlobFromDisk(String path) => Blob.createFromDisk(this, path); } diff --git a/lib/src/tree.dart b/lib/src/tree.dart index a71df7f..7e5c3c7 100644 --- a/lib/src/tree.dart +++ b/lib/src/tree.dart @@ -7,12 +7,6 @@ import 'enums.dart'; import 'util.dart'; 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 /// [Repository] and [Oid] objects. /// diff --git a/test/blob_test.dart b/test/blob_test.dart new file mode 100644 index 0000000..29025e3 --- /dev/null +++ b/test/blob_test.dart @@ -0,0 +1,100 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'helpers/util.dart'; + +void main() { + late Repository repo; + late Blob blob; + final tmpDir = '${Directory.systemTemp.path}/blob_testrepo/'; + const blobSHA = '9c78c21d6680a7ffebc76f7ac68cacc11d8f48bc'; + const blobContent = 'Feature edit\n'; + const newBlobContent = 'New blob\n'; + + 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); + blob = Blob.lookup(repo, Oid.fromSHA(repo, blobSHA)); + }); + + tearDown(() async { + blob.free(); + repo.free(); + await Directory(tmpDir).delete(recursive: true); + }); + + group('Blob', () { + test('successfully initializes blob from provided Oid', () { + expect(blob, isA()); + }); + + test('returns correct values', () { + expect(blob.id.sha, blobSHA); + expect(blob.isBinary, false); + expect(blob.size, 13); + expect(blob.content, blobContent); + }); + + test('successfully creates new blob', () { + final oid = Blob.create(repo, newBlobContent); + final newBlob = Blob.lookup(repo, oid); + + expect(newBlob.id.sha, '18fdaeef018e57a92bcad2d4a35b577f34089af6'); + expect(newBlob.isBinary, false); + expect(newBlob.size, 9); + expect(newBlob.content, newBlobContent); + + newBlob.free(); + }); + + test('successfully creates new blob from file at provided relative path', + () { + final oid = Blob.createFromWorkdir(repo, 'feature_file'); + final newBlob = Blob.lookup(repo, oid); + + expect(newBlob.id.sha, blobSHA); + expect(newBlob.isBinary, false); + expect(newBlob.size, 13); + expect(newBlob.content, blobContent); + + newBlob.free(); + }); + + test('throws when creating new blob from invalid path', () { + expect( + () => Blob.createFromWorkdir(repo, 'invalid/path.txt'), + throwsA(isA()), + ); + }); + + test( + 'throws when creating new blob from path that is outside of working directory', + () { + final outsideFile = + File('${Directory.current.absolute.path}/test/blob_test.dart'); + expect( + () => Blob.createFromWorkdir(repo, outsideFile.path), + throwsA(isA()), + ); + }); + + test('successfully creates new blob from file at provided path', () { + final outsideFile = + File('${Directory.current.absolute.path}/test/blob_test.dart'); + final oid = Blob.createFromDisk(repo, outsideFile.path); + final newBlob = Blob.lookup(repo, oid); + + expect(newBlob, isA()); + expect(newBlob.isBinary, false); + + newBlob.free(); + }); + }); +} diff --git a/test/commit_test.dart b/test/commit_test.dart index 14ec5f6..28f2bbf 100644 --- a/test/commit_test.dart +++ b/test/commit_test.dart @@ -7,43 +7,43 @@ import 'helpers/util.dart'; void main() { const mergeCommit = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'; - group('Commit', () { - late Repository repo; - final tmpDir = '${Directory.systemTemp.path}/commit_testrepo/'; + late Repository repo; + final tmpDir = '${Directory.systemTemp.path}/commit_testrepo/'; - const message = "Commit message.\n\nSome description.\n"; - const tree = '7796359a96eb722939c24bafdb1afe9f07f2f628'; - late Signature author; - late Signature commiter; + const message = "Commit message.\n\nSome description.\n"; + const tree = '7796359a96eb722939c24bafdb1afe9f07f2f628'; + late Signature author; + late Signature commiter; - 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); - author = Signature.create( - name: 'Author Name', - email: 'author@email.com', - time: 123, - ); - commiter = Signature.create( - name: 'Commiter', - email: 'commiter@email.com', - time: 124, - ); - }); - - tearDown(() async { - author.free(); - commiter.free(); - repo.free(); + 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); + author = Signature.create( + name: 'Author Name', + email: 'author@email.com', + time: 123, + ); + commiter = Signature.create( + name: 'Commiter', + email: 'commiter@email.com', + time: 124, + ); + }); + tearDown(() async { + author.free(); + commiter.free(); + repo.free(); + await Directory(tmpDir).delete(recursive: true); + }); + + group('Commit', () { test('successfully returns when 40 char sha hex is provided', () { final commit = repo[mergeCommit]; expect(commit, isA()); diff --git a/test/config_test.dart b/test/config_test.dart index 4588674..d51531f 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -18,17 +18,17 @@ void main() { late Config config; + setUp(() { + File('$tmpDir/$configFileName').writeAsStringSync(contents); + config = Config.open('$tmpDir/$configFileName'); + }); + + tearDown(() { + config.free(); + File('$tmpDir/$configFileName').deleteSync(); + }); + group('Config', () { - setUp(() { - File('$tmpDir/$configFileName').writeAsStringSync(contents); - config = Config.open('$tmpDir/$configFileName'); - }); - - tearDown(() { - config.free(); - File('$tmpDir/$configFileName').deleteSync(); - }); - test('opens file successfully with provided path', () { expect(config, isA()); }); diff --git a/test/odb_test.dart b/test/odb_test.dart index 9b92268..0c5d778 100644 --- a/test/odb_test.dart +++ b/test/odb_test.dart @@ -6,27 +6,25 @@ import 'helpers/util.dart'; void main() { const lastCommit = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'; + late Repository repo; + final tmpDir = '${Directory.systemTemp.path}/odb_testrepo/'; - group('Odb', () { - late Repository repo; - final tmpDir = '${Directory.systemTemp.path}/odb_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(); + 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('Odb', () { test('successfully initializes', () { expect(repo.odb, isA()); repo.odb.free(); diff --git a/test/oid_test.dart b/test/oid_test.dart index d9ffa59..3f52278 100644 --- a/test/oid_test.dart +++ b/test/oid_test.dart @@ -8,26 +8,26 @@ void main() { const sha = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'; const biggerSha = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e9'; const lesserSha = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e7'; + late Repository repo; + final tmpDir = '${Directory.systemTemp.path}/oid_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('Oid', () { - late Repository repo; - final tmpDir = '${Directory.systemTemp.path}/oid_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('fromSHA()', () { test('initializes successfully', () { final oid = Oid.fromSHA(repo, sha); diff --git a/test/reference_test.dart b/test/reference_test.dart index 5a18a90..e260c6b 100644 --- a/test/reference_test.dart +++ b/test/reference_test.dart @@ -7,27 +7,26 @@ import 'helpers/util.dart'; void main() { const lastCommit = '821ed6e80627b8769d170a293862f9fc60825226'; const newCommit = 'c68ff54aabf660fcdd9a2838d401583fe31249e3'; + 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('Reference', () { - 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); - }); - test('returns a list', () { expect( repo.references.list(), diff --git a/test/reflog_test.dart b/test/reflog_test.dart index f5d6ee4..0e29f4c 100644 --- a/test/reflog_test.dart +++ b/test/reflog_test.dart @@ -5,30 +5,29 @@ import 'package:libgit2dart/libgit2dart.dart'; import 'helpers/util.dart'; void main() { - group('RefLog', () { - late Repository repo; - late RefLog reflog; - final tmpDir = '${Directory.systemTemp.path}/reflog_testrepo/'; + late Repository repo; + late RefLog reflog; + final tmpDir = '${Directory.systemTemp.path}/reflog_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); - reflog = RefLog(repo.head); - }); - - tearDown(() async { - repo.head.free(); - reflog.free(); - repo.free(); + 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); + reflog = RefLog(repo.head); + }); + tearDown(() async { + repo.head.free(); + reflog.free(); + repo.free(); + await Directory(tmpDir).delete(recursive: true); + }); + group('RefLog', () { test('initializes successfully', () { expect(reflog, isA()); }); diff --git a/test/repository_test.dart b/test/repository_test.dart index 31f7bec..45fefee 100644 --- a/test/repository_test.dart +++ b/test/repository_test.dart @@ -269,6 +269,41 @@ void main() { expect(repo.isBranchUnborn, true); }); }); + + group('createBlob', () { + const newBlobContent = 'New blob\n'; + + test('successfully creates new blob', () { + final oid = repo.createBlob(newBlobContent); + final newBlob = Blob.lookup(repo, oid); + + expect(newBlob, isA()); + + newBlob.free(); + }); + + test( + 'successfully creates new blob from file at provided relative path', + () { + final oid = repo.createBlobFromWorkdir('feature_file'); + final newBlob = Blob.lookup(repo, oid); + + expect(newBlob, isA()); + + newBlob.free(); + }); + + test('successfully creates new blob from file at provided path', () { + final outsideFile = + File('${Directory.current.absolute.path}/test/blob_test.dart'); + final oid = repo.createBlobFromDisk(outsideFile.path); + final newBlob = Blob.lookup(repo, oid); + + expect(newBlob, isA()); + + newBlob.free(); + }); + }); }); }); } diff --git a/test/signature_test.dart b/test/signature_test.dart index 36179e6..bee634c 100644 --- a/test/signature_test.dart +++ b/test/signature_test.dart @@ -3,25 +3,24 @@ import 'package:libgit2dart/libgit2dart.dart'; void main() { late Signature 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(); + }); 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()); });