From 5f60a693d3532d77d909ad9fd48a9f83b37a4f39 Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Tue, 3 Aug 2021 21:19:27 +0300 Subject: [PATCH] feat(reference): add base bindings and api --- example/reference_example.dart | 14 ++++ example/repository_example.dart | 6 +- lib/libgit2dart.dart | 1 + lib/src/bindings/reference.dart | 122 ++++++++++++++++++++++++++++++ lib/src/oid.dart | 6 ++ lib/src/reference.dart | 119 +++++++++++++++++++++++++++++ lib/src/repository.dart | 22 +++++- test/reference_test.dart | 129 ++++++++++++++++++++++++++++++++ 8 files changed, 413 insertions(+), 6 deletions(-) create mode 100644 example/reference_example.dart create mode 100644 lib/src/bindings/reference.dart create mode 100644 lib/src/reference.dart create mode 100644 test/reference_test.dart diff --git a/example/reference_example.dart b/example/reference_example.dart new file mode 100644 index 0000000..8f87e8e --- /dev/null +++ b/example/reference_example.dart @@ -0,0 +1,14 @@ +import 'dart:io'; + +import 'package:libgit2dart/libgit2dart.dart'; + +void main() { + final repo = Repository.open(Directory.current.path); + final ref = Reference.lookup(repo, 'refs/heads/master'); + + print('Reference SHA hex: ${ref.target}'); + print('Is reference a local branch: ${ref.isBranch}'); + print('Reference full name: ${ref.name}'); + + ref.free(); +} diff --git a/example/repository_example.dart b/example/repository_example.dart index eff3980..75e7955 100644 --- a/example/repository_example.dart +++ b/example/repository_example.dart @@ -11,11 +11,7 @@ void main() { print('Is repository bare: ${repo.isBare}'); print('Is repository empty: ${repo.isEmpty}'); print('Is head detached: ${repo.isHeadDetached}'); - try { - print('Prepared message: ${repo.message}'); - } catch (e) { - print('Prepared message: $e'); - } + print('Repository refs: ${repo.references}'); // close() should be called on object to free memory when done. repo.close(); diff --git a/lib/libgit2dart.dart b/lib/libgit2dart.dart index ab62a5a..a87cafa 100644 --- a/lib/libgit2dart.dart +++ b/lib/libgit2dart.dart @@ -1,3 +1,4 @@ export 'src/repository.dart'; export 'src/config.dart'; export 'src/error.dart'; +export 'src/reference.dart'; diff --git a/lib/src/bindings/reference.dart b/lib/src/bindings/reference.dart new file mode 100644 index 0000000..3f96aec --- /dev/null +++ b/lib/src/bindings/reference.dart @@ -0,0 +1,122 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import 'libgit2_bindings.dart'; +import '../util.dart'; + +/// Get the type of a reference. +int referenceType(Pointer ref) => + libgit2.git_reference_type(ref); + +/// Get the OID pointed to by a direct reference. +/// +/// Only available if the reference is direct (i.e. an object id reference, not a symbolic one). +Pointer? target(Pointer ref) => + libgit2.git_reference_target(ref); + +/// Lookup a reference by name in a repository. +/// +/// The returned reference must be freed by the user. +/// +/// The name will be checked for validity. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer lookup(Pointer repo, String name) { + final out = calloc>(); + final nameC = name.toNativeUtf8().cast(); + final error = libgit2.git_reference_lookup(out, repo, nameC); + calloc.free(nameC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Get the full name of a reference. +String name(Pointer ref) { + var result = calloc(); + result = libgit2.git_reference_name(ref); + + return result.cast().toDartString(); +} + +/// Fill a list with all the references that can be found in a repository. +/// +/// The string array will be filled with the names of all references; +/// these values are owned by the user and should be free'd manually when no longer needed. +/// +/// Throws a [LibGit2Error] if error occured. +List list(Pointer repo) { + var array = calloc(); + final error = libgit2.git_reference_list(array, repo); + var result = []; + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + for (var i = 0; i < array.ref.count; i++) { + result.add( + array.ref.strings.elementAt(i).value.cast().toDartString()); + } + } + + calloc.free(array); + return result; +} + +/// Check if a reflog exists for the specified reference. +/// +/// Throws a [LibGit2Error] if error occured. +bool hasLog(Pointer repo, String name) { + final refname = name.toNativeUtf8().cast(); + final error = libgit2.git_reference_has_log(repo, refname); + calloc.free(refname); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return error == 1 ? true : false; + } +} + +/// Check if a reference is a local branch. +bool isBranch(Pointer ref) { + final result = libgit2.git_reference_is_branch(ref); + return result == 1 ? true : false; +} + +/// Check if a reference is a note. +bool isNote(Pointer ref) { + final result = libgit2.git_reference_is_note(ref); + return result == 1 ? true : false; +} + +/// Check if a reference is a remote tracking branch. +bool isRemote(Pointer ref) { + final result = libgit2.git_reference_is_remote(ref); + return result == 1 ? true : false; +} + +/// Check if a reference is a tag. +bool isTag(Pointer ref) { + final result = libgit2.git_reference_is_tag(ref); + return result == 1 ? true : false; +} + +/// Ensure the reference name is well-formed. +/// +/// Valid reference names must follow one of two patterns: +/// +/// Top-level names must contain only capital letters and underscores, +/// and must begin and end with a letter. (e.g. "HEAD", "ORIG_HEAD"). +/// Names prefixed with "refs/" can be almost anything. You must avoid +/// the characters '~', '^', ':', '\', '?', '[', and '*', and the sequences ".." +/// and "@{" which have special meaning to revparse. +bool isValidName(String name) { + final refname = name.toNativeUtf8().cast(); + final result = libgit2.git_reference_is_valid_name(refname); + calloc.free(refname); + return result == 1 ? true : false; +} diff --git a/lib/src/oid.dart b/lib/src/oid.dart index 3203f98..00c7be2 100644 --- a/lib/src/oid.dart +++ b/lib/src/oid.dart @@ -4,6 +4,12 @@ import 'bindings/oid.dart' as bindings; import 'util.dart'; class Oid { + /// Initializes a new instance of [Oid] class from provided + /// pointer to Oid object in memory. + Oid(this._oidPointer) { + libgit2.git_libgit2_init(); + } + /// Initializes a new instance of [Oid] class from provided /// hexadecimal [sha] string. /// diff --git a/lib/src/reference.dart b/lib/src/reference.dart new file mode 100644 index 0000000..be58273 --- /dev/null +++ b/lib/src/reference.dart @@ -0,0 +1,119 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'bindings/libgit2_bindings.dart'; +import 'bindings/reference.dart' as bindings; +import 'repository.dart'; +import 'oid.dart'; +import 'util.dart'; + +enum ReferenceType { direct, symbolic } + +class Reference { + /// Initializes a new instance of the [Reference] class. + /// Should be freed with `free()` to release allocated memory. + Reference(this._refPointer) { + libgit2.git_libgit2_init(); + } + + /// Initializes a new instance of the [Reference] class by + /// lookingup a reference by [name] in a [repository]. + /// + /// Should be freed with `free()` to release allocated memory. + /// + /// The name will be checked for validity. + /// + /// Throws a [LibGit2Error] if error occured. + Reference.lookup(Repository repository, String name) { + libgit2.git_libgit2_init(); + + try { + _refPointer = bindings.lookup(repository.pointer, name); + } catch (e) { + rethrow; + } + } + + /// Pointer to memory address for allocated reference object. + late final Pointer _refPointer; + + /// Checks if the reference [name] is well-formed. + /// + /// Valid reference names must follow one of two patterns: + /// + /// Top-level names must contain only capital letters and underscores, + /// and must begin and end with a letter. (e.g. "HEAD", "ORIG_HEAD"). + /// Names prefixed with "refs/" can be almost anything. You must avoid + /// the characters '~', '^', ':', '\', '?', '[', and '*', and the sequences ".." + /// and "@{" which have special meaning to revparse. + static bool isValidName(String name) { + libgit2.git_libgit2_init(); + final result = bindings.isValidName(name); + libgit2.git_libgit2_shutdown(); + + return result; + } + + /// Returns the type of the reference + ReferenceType get type { + return bindings.referenceType(_refPointer) == 1 + ? ReferenceType.direct + : ReferenceType.symbolic; + } + + /// Returns the SHA hex of the OID pointed to by a direct reference. + /// + /// Only available if the reference is direct (i.e. an object id reference, not a symbolic one). + String get target { + final oidPointer = bindings.target(_refPointer); + final sha = ''; + if (oidPointer == nullptr) { + return sha; + } else { + final oid = Oid(oidPointer!); + return oid.sha; + } + } + + /// Returns the full name of a reference. + String get name => bindings.name(_refPointer); + + /// Returns a list with all the references that can be found in a repository. + /// + /// Throws a [LibGit2Error] if error occured. + static List list(Pointer repo) { + try { + return bindings.list(repo); + } catch (e) { + rethrow; + } + } + + /// Checks if a reflog exists for the specified reference [name]. + /// + /// Throws a [LibGit2Error] if error occured. + static bool hasLog(Repository repo, String name) { + try { + return bindings.hasLog(repo.pointer, name); + } catch (e) { + rethrow; + } + } + + /// Checks if a reference is a local branch. + bool get isBranch => bindings.isBranch(_refPointer); + + /// Checks if a reference is a note. + bool get isNote => bindings.isNote(_refPointer); + + /// Check if a reference is a remote tracking branch. + bool get isRemote => bindings.isRemote(_refPointer); + + /// Check if a reference is a tag. + bool get isTag => bindings.isTag(_refPointer); + + /// Releases memory allocated for reference object. + void free() { + calloc.free(_refPointer); + libgit2.git_libgit2_shutdown(); + } +} diff --git a/lib/src/repository.dart b/lib/src/repository.dart index beceb8d..f23474e 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -1,5 +1,6 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; +import 'reference.dart'; import 'bindings/libgit2_bindings.dart'; import 'bindings/repository.dart' as bindings; import 'util.dart'; @@ -24,9 +25,11 @@ class Repository { } } - /// Pointer to memory address for allocated repository object. late final Pointer _repoPointer; + /// Pointer to memory address for allocated repository object. + Pointer get pointer => _repoPointer; + /// Returns path to the `.git` folder for normal repositories /// or path to the repository itself for bare repositories. String get path => bindings.path(_repoPointer); @@ -86,6 +89,23 @@ class Repository { } } + /// Returns reference object pointing to repository head. + Reference get head => Reference(bindings.head(_repoPointer)); + + /// Returns a map with all the references names and corresponding SHA hexes + /// that can be found in a repository. + Map get references { + var refMap = {}; + final refList = Reference.list(_repoPointer); + for (var ref in refList) { + final r = Reference.lookup(this, ref); + refMap[ref] = r.target; + r.free(); + } + + return refMap; + } + /// Makes the repository HEAD point to the specified reference. /// /// If the provided [reference] points to a Tree or a Blob, the HEAD is unaltered. diff --git a/test/reference_test.dart b/test/reference_test.dart new file mode 100644 index 0000000..955fca7 --- /dev/null +++ b/test/reference_test.dart @@ -0,0 +1,129 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:libgit2dart/src/repository.dart'; +import 'package:libgit2dart/src/reference.dart'; +import 'package:libgit2dart/src/error.dart'; + +import 'helpers/util.dart'; + +void main() { + const lastCommit = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'; + + group('Reference', () { + late Repository repo; + final tmpDir = '${Directory.systemTemp.path}/testrepo/'; + + setUpAll(() 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); + }); + + tearDownAll(() async { + repo.close(); + await Directory(tmpDir).delete(recursive: true); + }); + + test('returns correct type of reference', () { + expect(repo.head.type, ReferenceType.direct); + repo.head.free(); + }); + + test('returns SHA hex of direct reference', () { + expect(repo.head.target, lastCommit); + repo.head.free(); + }); + + test('returns the full name of a reference', () { + expect(repo.head.name, 'refs/heads/master'); + repo.head.free(); + }); + + test('returns a map with all the references of repository', () { + expect( + repo.references, + { + 'refs/heads/feature': '5aecfa0fb97eadaac050ccb99f03c3fb65460ad4', + 'refs/heads/master': '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8', + }, + ); + }); + + test('checks if reflog exists for the reference', () { + expect(Reference.hasLog(repo, 'refs/heads/master'), true); + expect(Reference.hasLog(repo, 'refs/heads/not/there'), false); + }); + + test('checks if reference is a local branch', () { + final ref = Reference.lookup(repo, 'refs/heads/feature'); + expect(ref.isBranch, true); + ref.free(); + }); + + test('checks if reference is a note', () { + final ref = Reference.lookup(repo, 'refs/heads/master'); + expect(ref.isNote, false); + ref.free(); + }); + + test('checks if reference is a remote branch', () { + final ref = Reference.lookup(repo, 'refs/heads/master'); + expect(ref.isRemote, false); + ref.free(); + }); + + test('checks if reference is a tag', () { + final ref = Reference.lookup(repo, 'refs/heads/master'); + expect(ref.isTag, false); + ref.free(); + }); + + group('.lookup()', () { + test('finds a reference with provided name', () { + final ref = Reference.lookup(repo, 'refs/heads/master'); + expect(ref.target, lastCommit); + ref.free(); + }); + + test('throws when error occured', () { + expect( + () => Reference.lookup(repo, 'refs/heads/not/there'), + throwsA(isA()), + ); + }); + }); + + group('isValidName()', () { + test('returns true for valid names', () { + expect(Reference.isValidName('HEAD'), true); + expect(Reference.isValidName('refs/heads/master'), true); + expect(Reference.isValidName('refs/heads/perfectly/valid'), true); + expect(Reference.isValidName('refs/tags/v1'), true); + expect(Reference.isValidName('refs/special/ref'), true); + expect(Reference.isValidName('refs/heads/ünicöde'), true); + expect(Reference.isValidName('refs/tags/😀'), true); + }); + + test('returns false for invalid names', () { + expect(Reference.isValidName(''), false); + expect(Reference.isValidName(' refs/heads/master'), false); + expect(Reference.isValidName('refs/heads/in..valid'), false); + expect(Reference.isValidName('refs/heads/invalid~'), false); + expect(Reference.isValidName('refs/heads/invalid^'), false); + expect(Reference.isValidName('refs/heads/invalid:'), false); + expect(Reference.isValidName('refs/heads/invalid\\'), false); + expect(Reference.isValidName('refs/heads/invalid?'), false); + expect(Reference.isValidName('refs/heads/invalid['), false); + expect(Reference.isValidName('refs/heads/invalid*'), false); + expect(Reference.isValidName('refs/heads/@{no}'), false); + expect(Reference.isValidName('refs/heads/foo//bar'), false); + }); + }); + }); +}