diff --git a/lib/src/bindings/reflog.dart b/lib/src/bindings/reflog.dart index 4cb355b..5a5977c 100644 --- a/lib/src/bindings/reflog.dart +++ b/lib/src/bindings/reflog.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; import 'package:libgit2dart/src/bindings/libgit2_bindings.dart'; +import 'package:libgit2dart/src/error.dart'; import 'package:libgit2dart/src/util.dart'; /// Read the reflog for the given reference. @@ -23,6 +24,90 @@ Pointer read({ return out.value; } +/// Write an existing in-memory reflog object back to disk using an atomic file +/// lock. +void write(Pointer reflog) { + final error = libgit2.git_reflog_write(reflog); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Delete the reflog for the given reference. +void delete({ + required Pointer repoPointer, + required String name, +}) { + final nameC = name.toNativeUtf8().cast(); + libgit2.git_reflog_delete(repoPointer, nameC); + calloc.free(nameC); +} + +/// Rename a reflog. +/// +/// The reflog to be renamed is expected to already exist. +/// +/// The new name will be checked for validity. +/// +/// Throws a [LibGit2Error] if error occured. +void rename({ + required Pointer repoPointer, + required String oldName, + required String newName, +}) { + final oldNameC = oldName.toNativeUtf8().cast(); + final newNameC = newName.toNativeUtf8().cast(); + final error = libgit2.git_reflog_rename(repoPointer, oldNameC, newNameC); + + calloc.free(oldNameC); + calloc.free(newNameC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Add a new entry to the in-memory reflog. +/// +/// Throws a [LibGit2Error] if error occured. +void add({ + required Pointer reflogPointer, + required Pointer oidPointer, + required Pointer committerPointer, + required String message, +}) { + final messageC = + message.isEmpty ? nullptr : message.toNativeUtf8().cast(); + + final error = libgit2.git_reflog_append( + reflogPointer, + oidPointer, + committerPointer, + messageC, + ); + + calloc.free(messageC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Remove an entry from the reflog by its index. +/// +/// Throws a [LibGit2Error] if error occured. +void remove({ + required Pointer reflogPointer, + required int index, +}) { + final error = libgit2.git_reflog_drop(reflogPointer, index, 1); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + /// Get the number of log entries in a reflog. int entryCount(Pointer reflog) => libgit2.git_reflog_entrycount(reflog); @@ -40,7 +125,8 @@ Pointer getByIndex({ /// Get the log message. String entryMessage(Pointer entry) { - return libgit2.git_reflog_entry_message(entry).cast().toDartString(); + final result = libgit2.git_reflog_entry_message(entry); + return result == nullptr ? '' : result.cast().toDartString(); } /// Get the committer of this entry. @@ -48,5 +134,15 @@ Pointer entryCommiter(Pointer entry) { return libgit2.git_reflog_entry_committer(entry); } +/// Get the new oid. +Pointer entryOidNew(Pointer entry) { + return libgit2.git_reflog_entry_id_new(entry); +} + +/// Get the old oid. +Pointer entryOidOld(Pointer entry) { + return libgit2.git_reflog_entry_id_old(entry); +} + /// Free the reflog. void free(Pointer reflog) => libgit2.git_reflog_free(reflog); diff --git a/lib/src/reference.dart b/lib/src/reference.dart index adad335..2d969fd 100644 --- a/lib/src/reference.dart +++ b/lib/src/reference.dart @@ -139,6 +139,19 @@ class Reference { refdb_bindings.free(refdb); } + /// Ensures there is a reflog for a particular reference. + /// + /// Makes sure that successive updates to the reference will append to its + /// log. + /// + /// Throws a [LibGit2Error] if error occured. + static void ensureLog({ + required Repository repo, + required String refName, + }) { + bindings.ensureLog(repoPointer: repo.pointer, refName: refName); + } + /// Creates a copy of an existing reference. /// /// **IMPORTANT**: Should be freed to release allocated memory. @@ -247,17 +260,6 @@ class Reference { ); } - /// Ensures there is a reflog for a particular reference. - /// - /// Makes sure that successive updates to the reference will append to its - /// log. - /// - /// Throws a [LibGit2Error] if error occured. - void ensureLog() { - final owner = bindings.owner(_refPointer); - bindings.ensureLog(repoPointer: owner, refName: name); - } - /// [RefLog] object. /// /// **IMPORTANT**: Should be freed to release allocated memory. diff --git a/lib/src/reflog.dart b/lib/src/reflog.dart index 1e0aa35..81ce9b0 100644 --- a/lib/src/reflog.dart +++ b/lib/src/reflog.dart @@ -18,6 +18,30 @@ class RefLog with IterableMixin { /// Pointer to memory address for allocated reflog object. late final Pointer _reflogPointer; + /// Deletes the reflog for the given reference. + static void delete(Reference ref) { + bindings.delete(repoPointer: ref.owner.pointer, name: ref.name); + } + + /// Renames a reflog. + /// + /// The reflog to be renamed is expected to already exist. + /// + /// The new name will be checked for validity. + /// + /// Throws a [LibGit2Error] if error occured. + static void rename({ + required Repository repo, + required String oldName, + required String newName, + }) { + bindings.rename( + repoPointer: repo.pointer, + oldName: oldName, + newName: newName, + ); + } + /// Lookups an entry by its index. /// /// Requesting the reflog entry with an index of 0 will return the most @@ -31,6 +55,39 @@ class RefLog with IterableMixin { ); } + /// Adds a new entry to the in-memory reflog. + /// + /// [oid] is the OID the reference is now pointing to. + /// + /// [committer] is the signature of the committer. + /// + /// [message] is optional reflog message. + /// + /// Throws a [LibGit2Error] if error occured. + void add({ + required Oid oid, + required Signature committer, + String message = '', + }) { + bindings.add( + reflogPointer: _reflogPointer, + oidPointer: oid.pointer, + committerPointer: committer.pointer, + message: message, + ); + } + + /// Removes an entry from the reflog by its [index]. + /// + /// Throws a [LibGit2Error] if error occured. + void remove(int index) { + bindings.remove(reflogPointer: _reflogPointer, index: index); + } + + /// Writes an existing in-memory reflog object back to disk using an atomic + /// file lock. + void write() => bindings.write(_reflogPointer); + /// Releases memory allocated for reflog object. void free() => bindings.free(_reflogPointer); @@ -47,11 +104,19 @@ class RefLogEntry { final Pointer _entryPointer; /// Log message. + /// + /// Returns empty string if there is no message. String get message => bindings.entryMessage(_entryPointer); /// Committer of this entry. Signature get committer => Signature(bindings.entryCommiter(_entryPointer)); + /// New oid of entry at this time. + Oid get newOid => Oid(bindings.entryOidNew(_entryPointer)); + + /// Old oid of entry. + Oid get oldOid => Oid(bindings.entryOidOld(_entryPointer)); + @override String toString() => 'RefLogEntry{message: $message, committer: $committer}'; } diff --git a/test/reference_test.dart b/test/reference_test.dart index 9661b8c..c401aa7 100644 --- a/test/reference_test.dart +++ b/test/reference_test.dart @@ -128,6 +128,31 @@ void main() { ref.free(); }); + test('ensures updates to the reference will append to its log', () { + Reference.ensureLog(repo: repo, refName: 'refs/tags/tag'); + + final ref = repo.createReference( + name: 'refs/tags/tag', + target: repo[lastCommit], + ); + final reflog = ref.log; + + expect(reflog.length, 1); + + reflog.free(); + ref.free(); + }); + + test('throws when trying to ensure there is a reflog and error occurs', () { + expect( + () => Reference.ensureLog( + repo: Repository(nullptr), + refName: 'refs/tags/tag', + ), + throwsA(isA()), + ); + }); + test('duplicates existing reference', () { expect(repo.references.length, 6); diff --git a/test/reflog_test.dart b/test/reflog_test.dart index b9fb9f4..35a0807 100644 --- a/test/reflog_test.dart +++ b/test/reflog_test.dart @@ -1,3 +1,4 @@ +import 'dart:ffi'; import 'dart:io'; import 'package:libgit2dart/libgit2dart.dart'; @@ -44,6 +45,110 @@ void main() { expect(reflog[0].committer.time, 1630568461); }); + test('returns new and old oids of entry', () { + expect(reflog[0].newOid.sha, '821ed6e80627b8769d170a293862f9fc60825226'); + expect(reflog.last.oldOid.sha, '0' * 40); + }); + + test('deletes the reflog of provided reference', () { + expect(head.hasLog, true); + RefLog.delete(head); + expect(head.hasLog, false); + }); + + test('renames existing reflog', () { + expect( + File('${repo.workdir}.git/logs/refs/heads/master').existsSync(), + true, + ); + expect( + File('${repo.workdir}.git/logs/refs/heads/renamed').existsSync(), + false, + ); + + RefLog.rename( + repo: repo, + oldName: 'refs/heads/master', + newName: 'refs/heads/renamed', + ); + + expect( + File('${repo.workdir}.git/logs/refs/heads/master').existsSync(), + false, + ); + expect( + File('${repo.workdir}.git/logs/refs/heads/renamed').existsSync(), + true, + ); + }); + + test('throws when trying to rename reflog and provided new name is invalid', + () { + expect( + () => RefLog.rename( + repo: repo, + oldName: 'refs/heads/master', + newName: '', + ), + throwsA(isA()), + ); + }); + + test('adds a new entry to the in-memory reflog', () { + final committer = Signature.create( + name: 'Commiter', + email: 'commiter@email.com', + time: 124, + ); + + expect(reflog.length, 4); + reflog.add(oid: head.target, committer: committer); + expect(reflog.length, 5); + + reflog.add(oid: head.target, committer: committer, message: 'new entry'); + expect(reflog.length, 6); + expect(reflog[0].message, 'new entry'); + + committer.free(); + }); + + test('throws when trying to add new entry', () { + expect( + () => reflog.add(oid: head.target, committer: Signature(nullptr)), + throwsA(isA()), + ); + }); + + test('removes entry from reflog with provided index', () { + expect(reflog.length, 4); + expect(reflog[0].message, 'commit: add subdirectory file'); + + reflog.remove(0); + expect(reflog.length, 3); + expect( + reflog[0].message, + "merge feature: Merge made by the 'recursive' strategy.", + ); + }); + + test('throws when trying to remove entry from reflog at invalid index', () { + expect(() => reflog.remove(-1), throwsA(isA())); + }); + + test('writes in-memory reflog to disk', () { + expect(reflog.length, 4); + reflog.remove(0); + + // making sure change is only in memory + final oldReflog = RefLog(head); + expect(oldReflog.length, 4); + + reflog.write(); + + final newReflog = RefLog(head); + expect(newReflog.length, 3); + }); + test('returns string representation of RefLogEntry object', () { expect(reflog[0].toString(), contains('RefLogEntry{')); });