From 5c8d6647eb527c916a736e843fec05efe8708306 Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Fri, 1 Oct 2021 17:34:01 +0300 Subject: [PATCH] feat(note): add bindings and api --- lib/libgit2dart.dart | 1 + lib/src/bindings/note.dart | 146 ++++++++++++++++++ lib/src/note.dart | 121 +++++++++++++++ lib/src/repository.dart | 45 ++++++ .../testrepo/.gitdir/logs/refs/notes/commits | 2 + .../00/eb1490ff4e39fa29c0ef352a9f93989b7018f8 | Bin 0 -> 182 bytes .../af/1bbb249ea750bbfa66c1e7fd879622c2dbfe3a | Bin 0 -> 150 bytes .../b2/36335c81848293b04ac099b82a2843d5ce3423 | Bin 0 -> 149 bytes .../d2/ffe6b06b11dd90c2ee3f15d2c6b62f018554ed | Bin 0 -> 30 bytes .../d8/54ba919e1bb303f4d6bb4ca9a15c5cab2a2a50 | Bin 0 -> 27 bytes .../fb/befa2a73da026d459067d84cbf8b747be07c7c | Bin 0 -> 85 bytes .../testrepo/.gitdir/refs/notes/commits | 1 + test/note_test.dart | 97 ++++++++++++ test/reference_test.dart | 1 + 14 files changed, 414 insertions(+) create mode 100644 lib/src/bindings/note.dart create mode 100644 lib/src/note.dart create mode 100644 test/assets/testrepo/.gitdir/logs/refs/notes/commits create mode 100644 test/assets/testrepo/.gitdir/objects/00/eb1490ff4e39fa29c0ef352a9f93989b7018f8 create mode 100644 test/assets/testrepo/.gitdir/objects/af/1bbb249ea750bbfa66c1e7fd879622c2dbfe3a create mode 100644 test/assets/testrepo/.gitdir/objects/b2/36335c81848293b04ac099b82a2843d5ce3423 create mode 100644 test/assets/testrepo/.gitdir/objects/d2/ffe6b06b11dd90c2ee3f15d2c6b62f018554ed create mode 100644 test/assets/testrepo/.gitdir/objects/d8/54ba919e1bb303f4d6bb4ca9a15c5cab2a2a50 create mode 100644 test/assets/testrepo/.gitdir/objects/fb/befa2a73da026d459067d84cbf8b747be07c7c create mode 100644 test/assets/testrepo/.gitdir/refs/notes/commits create mode 100644 test/note_test.dart diff --git a/lib/libgit2dart.dart b/lib/libgit2dart.dart index 8fcad97..2735eb6 100644 --- a/lib/libgit2dart.dart +++ b/lib/libgit2dart.dart @@ -22,6 +22,7 @@ export 'src/refspec.dart'; export 'src/callbacks.dart'; export 'src/credentials.dart'; export 'src/blame.dart'; +export 'src/note.dart'; export 'src/features.dart'; export 'src/error.dart'; export 'src/git_types.dart'; diff --git a/lib/src/bindings/note.dart b/lib/src/bindings/note.dart new file mode 100644 index 0000000..4dd45ce --- /dev/null +++ b/lib/src/bindings/note.dart @@ -0,0 +1,146 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import '../util.dart'; +import 'libgit2_bindings.dart'; + +/// Returns list of notes for repository. +/// +/// Notes must be freed manually by the user. +/// +/// Throws a [LibGit2Error] if error occured. +List> list(Pointer repo) { + final notesRef = 'refs/notes/commits'.toNativeUtf8().cast(); + final iterator = calloc>(); + final iteratorError = libgit2.git_note_iterator_new(iterator, repo, notesRef); + + if (iteratorError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + var result = >[]; + var nextError = 0; + + while (nextError >= 0) { + final noteId = calloc(); + var annotatedId = calloc(); + nextError = libgit2.git_note_next(noteId, annotatedId, iterator.value); + if (nextError >= 0) { + final out = calloc>(); + final error = libgit2.git_note_read(out, repo, notesRef, annotatedId); + + calloc.free(noteId); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + result.add({'note': out.value, 'annotatedId': annotatedId}); + } + } else { + break; + } + } + + calloc.free(notesRef); + libgit2.git_note_iterator_free(iterator.value); + + return result; +} + +/// Read the note for an object. +/// +/// The note must be freed manually by the user. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer lookup({ + required Pointer repoPointer, + required Pointer oidPointer, + String notesRef = 'refs/notes/commits', +}) { + final out = calloc>(); + final notesRefC = notesRef.toNativeUtf8().cast(); + final error = libgit2.git_note_read(out, repoPointer, notesRefC, oidPointer); + + calloc.free(notesRefC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Add a note for an object. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer create({ + required Pointer repoPointer, + String notesRef = 'refs/notes/commits', + required Pointer authorPointer, + required Pointer committerPointer, + required Pointer oidPointer, + required String note, + bool force = false, +}) { + final out = calloc(); + final notesRefC = notesRef.toNativeUtf8().cast(); + final noteC = note.toNativeUtf8().cast(); + final forceC = force ? 1 : 0; + final error = libgit2.git_note_create( + out, + repoPointer, + notesRefC, + authorPointer, + committerPointer, + oidPointer, + noteC, + forceC, + ); + + calloc.free(notesRefC); + calloc.free(noteC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} + +/// Remove the note for an object. +/// +/// Throws a [LibGit2Error] if error occured. +void remove({ + required Pointer repoPointer, + String notesRef = 'refs/notes/commits', + required Pointer authorPointer, + required Pointer committerPointer, + required Pointer oidPointer, +}) { + final notesRefC = notesRef.toNativeUtf8().cast(); + + final error = libgit2.git_note_remove( + repoPointer, + notesRefC, + authorPointer, + committerPointer, + oidPointer, + ); + + calloc.free(notesRefC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Get the note object's id. +Pointer id(Pointer note) => libgit2.git_note_id(note); + +/// Get the note message. +String message(Pointer note) { + return libgit2.git_note_message(note).cast().toDartString(); +} + +/// Free memory allocated for note object. +void free(Pointer note) => libgit2.git_note_free(note); diff --git a/lib/src/note.dart b/lib/src/note.dart new file mode 100644 index 0000000..603c2f3 --- /dev/null +++ b/lib/src/note.dart @@ -0,0 +1,121 @@ +import 'dart:ffi'; +import 'bindings/libgit2_bindings.dart'; +import 'bindings/note.dart' as bindings; +import 'oid.dart'; +import 'repository.dart'; +import 'signature.dart'; + +class Notes { + /// Initializes a new instance of the [Notes] class from + /// provided [Repository] object. + Notes(Repository repo) { + _repoPointer = repo.pointer; + } + + /// Pointer to memory address for allocated repository object. + late final Pointer _repoPointer; + + /// Reads the note for an object. + /// + /// The note must be freed manually by the user. + /// + /// Throws a [LibGit2Error] if error occured. + static Note lookup({ + required Repository repo, + required Oid annotatedId, + String notesRef = 'refs/notes/commits', + }) { + final note = bindings.lookup( + repoPointer: repo.pointer, + oidPointer: annotatedId.pointer, + notesRef: notesRef, + ); + + return Note(note, annotatedId.pointer, repo.pointer); + } + + /// Adds a note for an [object]. + /// + /// Throws a [LibGit2Error] if error occured. + static Oid create({ + required Repository repo, + required Signature author, + required Signature committer, + required Oid object, + required String note, + String notesRef = 'refs/notes/commits', + bool force = false, + }) { + return Oid(bindings.create( + repoPointer: repo.pointer, + authorPointer: author.pointer, + committerPointer: committer.pointer, + oidPointer: object.pointer, + note: note, + notesRef: notesRef, + force: force, + )); + } + + /// Returns list of notes for repository. + /// + /// Notes must be freed manually by the user. + /// + /// Throws a [LibGit2Error] if error occured. + List get list { + final notesPointers = bindings.list(_repoPointer); + var result = []; + for (var note in notesPointers) { + result.add(Note( + note['note'] as Pointer, + note['annotatedId'] as Pointer, + _repoPointer, + )); + } + + return result; + } +} + +class Note { + /// Initializes a new instance of the [Note] class from provided + /// pointer to note object in memory. + Note(this._notePointer, this._annotatedIdPointer, this._repoPointer); + + /// Pointer to memory address for allocated note object. + final Pointer _notePointer; + + /// Pointer to memory address for allocated annotetedId object. + final Pointer _annotatedIdPointer; + + /// Pointer to memory address for allocated repository object. + final Pointer _repoPointer; + + /// Removes the note for an [object]. + /// + /// Throws a [LibGit2Error] if error occured. + void remove({ + required Signature author, + required Signature committer, + String notesRef = 'refs/notes/commits', + }) { + bindings.remove( + repoPointer: _repoPointer, + authorPointer: author.pointer, + committerPointer: committer.pointer, + oidPointer: annotatedId.pointer, + ); + } + + /// Returns the note object's [Oid]. + Oid get id => Oid(bindings.id(_notePointer)); + + /// Returns the note message. + String get message => bindings.message(_notePointer); + + /// Returns the [Oid] of the git object being annotated. + Oid get annotatedId => Oid(_annotatedIdPointer); + + /// Releases memory allocated for note object. + void free() => bindings.free(_notePointer); +} diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 8e311eb..ad1651c 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -1118,4 +1118,49 @@ class Repository { maxLine: maxLine, ); } + + /// Returns list of notes for repository. + /// + /// Notes must be freed manually. + /// + /// Throws a [LibGit2Error] if error occured. + List get notes => Notes(this).list; + + /// Reads the note for an object. + /// + /// The note must be freed manually. + /// + /// Throws a [LibGit2Error] if error occured. + Note lookupNote({ + required Oid annotatedId, + String notesRef = 'refs/notes/commits', + }) { + return Notes.lookup( + repo: this, + annotatedId: annotatedId, + notesRef: notesRef, + ); + } + + /// Adds a note for an [object]. + /// + /// Throws a [LibGit2Error] if error occured. + Oid createNote({ + required Signature author, + required Signature committer, + required Oid object, + required String note, + String notesRef = 'refs/notes/commits', + bool force = false, + }) { + return Notes.create( + repo: this, + author: author, + committer: committer, + object: object, + note: note, + notesRef: notesRef, + force: force, + ); + } } diff --git a/test/assets/testrepo/.gitdir/logs/refs/notes/commits b/test/assets/testrepo/.gitdir/logs/refs/notes/commits new file mode 100644 index 0000000..b0fdfb1 --- /dev/null +++ b/test/assets/testrepo/.gitdir/logs/refs/notes/commits @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 af1bbb249ea750bbfa66c1e7fd879622c2dbfe3a Aleksey Kulikov 1633093379 +0300 notes: Notes added by 'git notes add' +af1bbb249ea750bbfa66c1e7fd879622c2dbfe3a 00eb1490ff4e39fa29c0ef352a9f93989b7018f8 Aleksey Kulikov 1633093431 +0300 notes: Notes added by 'git notes add' diff --git a/test/assets/testrepo/.gitdir/objects/00/eb1490ff4e39fa29c0ef352a9f93989b7018f8 b/test/assets/testrepo/.gitdir/objects/00/eb1490ff4e39fa29c0ef352a9f93989b7018f8 new file mode 100644 index 0000000000000000000000000000000000000000..884de42d612ea836db9032c583874ab339e2b021 GIT binary patch literal 182 zcmV;n07?IN0i};IZp0uE08{gd?XHM)f?aHb6h*l@QvR^O`hsjAvbmG|`?$#q(oQj& ziLI2}0f*_OZz2$oMxr4)n+XHX#JGUU(XG2s4jk zG&0S}O@2g-n8GZ?iXZ*EZE#y<+2w%G$C{V+gtvXkwI0`!Yx=ks&g(U{@($*#(4H(A kc-5q}I_VkOU-g$<{B;z6*Jl83V`2*Kw z3@L4O42kbUv|*}!jyE`UTBFgL3>;A-s-5oPB7Z27He)a}FfcPQQ82eivPeoZG%`*#wlpCMbvuI=_&xiBVXwU$-@)C>zF!_*X@8Ur&U^CSy% zGs_f1pfN_4#ujEqX_jfpW(F2Urbb3)443{t+mJ1Icfz4}_M(@LZPRCL4S5RyU0W}& D3PCtS literal 0 HcmV?d00001 diff --git a/test/assets/testrepo/.gitdir/objects/d2/ffe6b06b11dd90c2ee3f15d2c6b62f018554ed b/test/assets/testrepo/.gitdir/objects/d2/ffe6b06b11dd90c2ee3f15d2c6b62f018554ed new file mode 100644 index 0000000000000000000000000000000000000000..c4e4cdaddff4f9d5fbcc71aca95f75c34a76489f GIT binary patch literal 30 mcmbDaQ{gWQLK@34FV;H>R*{%Wrmsbj8 literal 0 HcmV?d00001 diff --git a/test/assets/testrepo/.gitdir/objects/fb/befa2a73da026d459067d84cbf8b747be07c7c b/test/assets/testrepo/.gitdir/objects/fb/befa2a73da026d459067d84cbf8b747be07c7c new file mode 100644 index 0000000000000000000000000000000000000000..94781f8917f9e903768e02ec99171ea1acd7c6a4 GIT binary patch literal 85 zcmb)DU|?ckU~Cw;!NADJ*U)<_kYnV($-sQGkD-aVhsh?BZ3f0(n|-{^ ojDadljf|3aeEEN~E$85FVZOba#rKxv%(hF;J`v3zzgUtF0IfV8iU0rr literal 0 HcmV?d00001 diff --git a/test/assets/testrepo/.gitdir/refs/notes/commits b/test/assets/testrepo/.gitdir/refs/notes/commits new file mode 100644 index 0000000..d275d7d --- /dev/null +++ b/test/assets/testrepo/.gitdir/refs/notes/commits @@ -0,0 +1 @@ +00eb1490ff4e39fa29c0ef352a9f93989b7018f8 diff --git a/test/note_test.dart b/test/note_test.dart new file mode 100644 index 0000000..b0c5249 --- /dev/null +++ b/test/note_test.dart @@ -0,0 +1,97 @@ +import 'dart:io'; +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'helpers/util.dart'; + +void main() { + late Repository repo; + late Directory tmpDir; + const notesExpected = [ + { + 'id': 'd854ba919e1bb303f4d6bb4ca9a15c5cab2a2a50', + 'message': 'Another note\n', + 'annotatedId': '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8', + }, + { + 'id': 'd2ffe6b06b11dd90c2ee3f15d2c6b62f018554ed', + 'message': 'Note for HEAD\n', + 'annotatedId': '821ed6e80627b8769d170a293862f9fc60825226', + }, + ]; + + setUp(() async { + tmpDir = await setupRepo(Directory('test/assets/testrepo/')); + repo = Repository.open(tmpDir.path); + }); + + tearDown(() async { + repo.free(); + await tmpDir.delete(recursive: true); + }); + + group('Note', () { + test('returns list of notes', () { + final notes = repo.notes; + + expect(notes.length, 2); + + for (var i = 0; i < notes.length; i++) { + expect(notes[i].id.sha, notesExpected[i]['id']); + expect(notes[i].message, notesExpected[i]['message']); + expect(notes[i].annotatedId.sha, notesExpected[i]['annotatedId']); + } + + for (var note in notes) { + note.free(); + } + }); + + test('successfully lookups note', () { + final head = repo.head; + final note = repo.lookupNote(annotatedId: head.target); + + expect(note.id.sha, notesExpected[1]['id']); + expect(note.message, notesExpected[1]['message']); + expect(note.annotatedId.sha, notesExpected[1]['annotatedId']); + + note.free(); + head.free(); + }); + + test('successfully creates note', () { + final signature = repo.defaultSignature; + final head = repo.head; + final noteOid = repo.createNote( + author: signature, + committer: signature, + object: head.target, + note: 'New note for HEAD', + force: true, + ); + final noteBlob = repo[noteOid.sha] as Blob; + + expect(noteOid.sha, 'ffd6e2ceaf91c00ea6d29e2e897f906da720529f'); + expect(noteBlob.content, 'New note for HEAD'); + + noteBlob.free(); + head.free(); + signature.free(); + }); + + test('successfully removes note', () { + final signature = repo.defaultSignature; + final head = repo.head; + final note = repo.lookupNote(annotatedId: head.target); + + note.remove(author: signature, committer: signature); + expect( + () => repo.lookupNote(annotatedId: head.target), + throwsA(isA()), + ); + + note.free(); + head.free(); + signature.free(); + }); + }); +} diff --git a/test/reference_test.dart b/test/reference_test.dart index a69d7c8..5a21e25 100644 --- a/test/reference_test.dart +++ b/test/reference_test.dart @@ -28,6 +28,7 @@ void main() { [ 'refs/heads/feature', 'refs/heads/master', + 'refs/notes/commits', 'refs/tags/v0.1', 'refs/tags/v0.2', ],