From ce9384cac96f9ede83f642e4382c00e2b2b48dfb Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Wed, 1 Sep 2021 16:53:40 +0300 Subject: [PATCH] feat(revparse): add bindings and api --- lib/src/bindings/merge.dart | 23 ++++++ lib/src/bindings/repository.dart | 28 -------- lib/src/bindings/revparse.dart | 85 ++++++++++++++++++++++ lib/src/enums.dart | 9 +++ lib/src/repository.dart | 55 +++++++++++--- lib/src/revparse.dart | 91 +++++++++++++++++++++++ test/repository_test.dart | 18 ----- test/revparse_test.dart | 120 +++++++++++++++++++++++++++++++ 8 files changed, 372 insertions(+), 57 deletions(-) create mode 100644 lib/src/bindings/merge.dart create mode 100644 lib/src/bindings/revparse.dart create mode 100644 lib/src/revparse.dart create mode 100644 test/revparse_test.dart diff --git a/lib/src/bindings/merge.dart b/lib/src/bindings/merge.dart new file mode 100644 index 0000000..4c2b08e --- /dev/null +++ b/lib/src/bindings/merge.dart @@ -0,0 +1,23 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import 'libgit2_bindings.dart'; +import '../util.dart'; + +/// Find a merge base between two commits. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer mergeBase( + Pointer repo, + Pointer one, + Pointer two, +) { + final out = calloc(); + final error = libgit2.git_merge_base(out, repo, one, two); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} diff --git a/lib/src/bindings/repository.dart b/lib/src/bindings/repository.dart index 704d92a..a654185 100644 --- a/lib/src/bindings/repository.dart +++ b/lib/src/bindings/repository.dart @@ -510,33 +510,5 @@ Pointer wrapODB(Pointer odb) { } } -/// Find a single object, as specified by a [spec] string. -/// See `man gitrevisions`, or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions -/// for information on the syntax accepted. -/// -/// The returned object should be released when no longer needed. -/// -/// Throws a [LibGit2Error] if error occured. -Pointer revParseSingle( - Pointer repo, - String spec, -) { - final out = calloc>(); - final specC = spec.toNativeUtf8().cast(); - final error = libgit2.git_revparse_single( - out, - repo, - specC, - ); - - calloc.free(specC); - - if (error < 0) { - throw LibGit2Error(libgit2.git_error_last()); - } else { - return out.value; - } -} - /// Free a previously allocated repository. void free(Pointer repo) => libgit2.git_repository_free(repo); diff --git a/lib/src/bindings/revparse.dart b/lib/src/bindings/revparse.dart new file mode 100644 index 0000000..cbd43c7 --- /dev/null +++ b/lib/src/bindings/revparse.dart @@ -0,0 +1,85 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'libgit2_bindings.dart'; +import '../error.dart'; +import '../util.dart'; + +/// Parse a revision string for from, to, and intent. +/// +/// See `man gitrevisions` or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions +/// for information on the syntax accepted. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer revParse( + Pointer repo, + String spec, +) { + final out = calloc(); + final specC = spec.toNativeUtf8().cast(); + final error = libgit2.git_revparse(out, repo, specC); + + calloc.free(specC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} + +/// Find a single object, as specified by a [spec] revision string. +/// See `man gitrevisions`, or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions +/// for information on the syntax accepted. +/// +/// The returned object should be released when no longer needed. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer revParseSingle(Pointer repo, String spec) { + final out = calloc>(); + final specC = spec.toNativeUtf8().cast(); + final error = libgit2.git_revparse_single( + out, + repo, + specC, + ); + + calloc.free(specC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Find a single object and intermediate reference by a [spec] revision string. +/// +/// See `man gitrevisions`, or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions +/// for information on the syntax accepted. +/// +/// In some cases (@{<-n>} or @{upstream}), the expression may point to an +/// intermediate reference. When such expressions are being passed in, reference_out will be +/// valued as well. +/// +/// The returned object and reference should be released when no longer needed. +/// +/// Throws a [LibGit2Error] if error occured. +List revParseExt(Pointer repo, String spec) { + final objectOut = calloc>(); + final referenceOut = calloc>(); + final specC = spec.toNativeUtf8().cast(); + var result = []; + final error = libgit2.git_revparse_ext(objectOut, referenceOut, repo, specC); + + calloc.free(specC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + result.add(objectOut.value); + if (referenceOut.value != nullptr) { + result.add(referenceOut.value); + } + return result; + } +} diff --git a/lib/src/enums.dart b/lib/src/enums.dart index 6781efe..9949946 100644 --- a/lib/src/enums.dart +++ b/lib/src/enums.dart @@ -19,3 +19,12 @@ enum GitFilemode { undreadable, tree, blob, blobExecutable, link, commit } enum GitSort { none, topological, time, reverse } enum GitObject { commit, tree, blob, tag } + +/// Revparse flags, indicate the intended behavior of the spec. +/// +/// [single]: the spec targeted a single object. +/// +/// [range]: the spec targeted a range of commits. +/// +/// [mergeBase]: the spec used the '...' operator, which invokes special semantics. +enum GitRevParse { single, range, mergeBase } diff --git a/lib/src/repository.dart b/lib/src/repository.dart index b108284..e86a857 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -1,6 +1,7 @@ import 'dart:ffi'; import 'bindings/libgit2_bindings.dart'; import 'bindings/repository.dart' as bindings; +import 'bindings/merge.dart' as merge_bindings; import 'commit.dart'; import 'config.dart'; import 'index.dart'; @@ -8,6 +9,7 @@ import 'odb.dart'; import 'oid.dart'; import 'reference.dart'; import 'revwalk.dart'; +import 'revparse.dart'; import 'enums.dart'; import 'util.dart'; @@ -323,17 +325,6 @@ class Repository { return Commit.lookup(this, oid); } - /// Find a single object, as specified by a [spec] string. - /// See `man gitrevisions`, or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions - /// for information on the syntax accepted. - /// - /// The returned object should be released when no longer needed. - /// - /// Throws a [LibGit2Error] if error occured. - Commit revParseSingle(String spec) { - return Commit(bindings.revParseSingle(_repoPointer, spec).cast()); - } - /// Returns the list of commits starting from provided [oid]. /// /// If [sorting] isn't provided default will be used (reverse chronological order, like in git). @@ -348,4 +339,46 @@ class Repository { return result; } + + /// Finds a single object, as specified by a [spec] revision string. + /// See `man gitrevisions`, or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions + /// for information on the syntax accepted. + /// + /// The returned object should be released when no longer needed. + /// + /// Throws a [LibGit2Error] if error occured. + Commit revParseSingle(String spec) => RevParse.single(this, spec); + + /// Finds a single object and intermediate reference (if there is one) by a [spec] revision string. + /// + /// See `man gitrevisions`, or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions + /// for information on the syntax accepted. + /// + /// In some cases (@{<-n>} or @{upstream}), the expression may point to an + /// intermediate reference. When such expressions are being passed in, reference_out will be + /// valued as well. + /// + /// The returned object and reference should be released when no longer needed. + /// + /// Throws a [LibGit2Error] if error occured. + RevParse revParseExt(String spec) => RevParse.ext(this, spec); + + /// Parses a revision string for from, to, and intent. + /// + /// See `man gitrevisions` or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions + /// for information on the syntax accepted. + /// + /// Throws a [LibGit2Error] if error occured. + RevSpec revParse(String spec) => RevParse.range(this, spec); + + /// Finds a merge base between two commits. + /// + /// Throws a [LibGit2Error] if error occured. + Oid mergeBase(Oid one, Oid two) { + return Oid(merge_bindings.mergeBase( + _repoPointer, + one.pointer, + two.pointer, + )); + } } diff --git a/lib/src/revparse.dart b/lib/src/revparse.dart new file mode 100644 index 0000000..d6831f0 --- /dev/null +++ b/lib/src/revparse.dart @@ -0,0 +1,91 @@ +import 'dart:ffi'; +import 'bindings/libgit2_bindings.dart'; +import 'bindings/revparse.dart' as bindings; +import 'commit.dart'; +import 'reference.dart'; +import 'repository.dart'; +import 'enums.dart'; + +class RevParse { + /// Finds a single object and intermediate reference (if there is one) by a [spec] revision string. + /// + /// See `man gitrevisions`, or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions + /// for information on the syntax accepted. + /// + /// In some cases (@{<-n>} or @{upstream}), the expression may point to an + /// intermediate reference. When such expressions are being passed in, reference_out will be + /// valued as well. + /// + /// The returned object and reference should be released when no longer needed. + /// + /// Throws a [LibGit2Error] if error occured. + RevParse.ext(Repository repo, String spec) { + final pointers = bindings.revParseExt(repo.pointer, spec); + object = Commit(pointers[0].cast()); + if (pointers.length == 2) { + reference = Reference(repo.pointer, pointers[1].cast()); + } else { + reference = null; + } + } + + /// Object found by a revision string. + late final Commit object; + + /// Intermediate reference found by a revision string. + late final Reference? reference; + + /// Finds a single object, as specified by a [spec] revision string. + /// See `man gitrevisions`, or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions + /// for information on the syntax accepted. + /// + /// The returned object should be released when no longer needed. + /// + /// Throws a [LibGit2Error] if error occured. + static Commit single(Repository repo, String spec) { + return Commit(bindings.revParseSingle(repo.pointer, spec).cast()); + } + + /// Parses a revision string for from, to, and intent. + /// + /// See `man gitrevisions` or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions + /// for information on the syntax accepted. + /// + /// Throws a [LibGit2Error] if error occured. + static RevSpec range(Repository repo, String spec) { + return RevSpec(bindings.revParse(repo.pointer, spec)); + } +} + +class RevSpec { + /// Initializes a new instance of [RevSpec] class from provided + /// pointer to revspec object in memory. + RevSpec(this._revSpecPointer); + + /// Pointer to memory address for allocated revspec object. + late final Pointer _revSpecPointer; + + /// The left element of the revspec; must be freed by the user. + Commit get from => Commit(_revSpecPointer.ref.from.cast()); + + /// The right element of the revspec; must be freed by the user. + Commit? get to { + if (_revSpecPointer.ref.to == nullptr) { + return null; + } else { + return Commit(_revSpecPointer.ref.to.cast()); + } + } + + /// The intent of the revspec. + GitRevParse get flags { + final flag = _revSpecPointer.ref.flags; + if (flag == 1) { + return GitRevParse.single; + } else if (flag == 2) { + return GitRevParse.range; + } else { + return GitRevParse.mergeBase; + } + } +} diff --git a/test/repository_test.dart b/test/repository_test.dart index 7c31768..c4ba14a 100644 --- a/test/repository_test.dart +++ b/test/repository_test.dart @@ -199,24 +199,6 @@ void main() { } }); - test('returns commit with different spec strings', () { - const headSHA = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'; - const parentSHA = 'c68ff54aabf660fcdd9a2838d401583fe31249e3'; - - final headCommit = repo.revParseSingle('HEAD'); - expect(headCommit.id.sha, headSHA); - - final parentCommit = repo.revParseSingle('HEAD^'); - expect(parentCommit.id.sha, parentSHA); - - final initCommit = repo.revParseSingle('@{-1}'); - expect(initCommit.id.sha, '5aecfa0fb97eadaac050ccb99f03c3fb65460ad4'); - - headCommit.free(); - parentCommit.free(); - initCommit.free(); - }); - group('.discover()', () { test('discovers repository', () async { final subDir = '${tmpDir}subdir1/subdir2/'; diff --git a/test/revparse_test.dart b/test/revparse_test.dart new file mode 100644 index 0000000..c07e1b4 --- /dev/null +++ b/test/revparse_test.dart @@ -0,0 +1,120 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'helpers/util.dart'; + +void main() { + late Repository repo; + final tmpDir = '${Directory.systemTemp.path}/revparse_testrepo/'; + const headSHA = '78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8'; + const parentSHA = 'c68ff54aabf660fcdd9a2838d401583fe31249e3'; + + 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('revParse', () { + test('.single() returns commit with different spec strings', () { + final headCommit = repo.revParseSingle('HEAD'); + expect(headCommit.id.sha, headSHA); + + final parentCommit = repo.revParseSingle('HEAD^'); + expect(parentCommit.id.sha, parentSHA); + + final initCommit = repo.revParseSingle('@{-1}'); + expect(initCommit.id.sha, '5aecfa0fb97eadaac050ccb99f03c3fb65460ad4'); + + headCommit.free(); + parentCommit.free(); + initCommit.free(); + }); + + test('.ext() returns commit and reference', () { + final masterRef = repo.references['refs/heads/master']; + var headParse = repo.revParseExt('master'); + + expect(headParse.object.id.sha, headSHA); + expect(headParse.reference, masterRef); + + masterRef.free(); + headParse.object.free(); + headParse.reference?.free(); + + final featureRef = repo.references['refs/heads/feature']; + headParse = repo.revParseExt('feature'); + + expect( + headParse.object.id.sha, '5aecfa0fb97eadaac050ccb99f03c3fb65460ad4'); + expect(headParse.reference, featureRef); + + featureRef.free(); + headParse.object.free(); + headParse.reference?.free(); + }); + + test('.ext() returns only commit when no intermidiate reference found', () { + final headParse = repo.revParseExt('HEAD^'); + + expect(headParse.object.id.sha, parentSHA); + expect(headParse.reference, isNull); + + headParse.object.free(); + }); + + test( + '.range returns revspec with correct fields values based on provided spec', + () { + var revspec = repo.revParse('master'); + + expect(revspec.from.id.sha, headSHA); + expect(revspec.to, isNull); + expect(revspec.flags, GitRevParse.single); + + revspec.from.free(); + + revspec = repo.revParse('HEAD^1..5aecfa'); + + expect(revspec.from.id.sha, parentSHA); + expect(revspec.to?.id.sha, '5aecfa0fb97eadaac050ccb99f03c3fb65460ad4'); + expect(revspec.flags, GitRevParse.range); + + revspec.from.free(); + revspec.to?.free(); + + revspec = repo.revParse('HEAD...feature'); + + expect(revspec.from.id.sha, headSHA); + expect(revspec.to?.id.sha, '5aecfa0fb97eadaac050ccb99f03c3fb65460ad4'); + expect(revspec.flags, GitRevParse.mergeBase); + expect(repo.mergeBase(revspec.from.id, revspec.to!.id), isA()); + + revspec.from.free(); + revspec.to?.free(); + }); + + test('throws on invalid range spec', () { + expect( + () => repo.revParse('invalid..5aecfa'), + throwsA(isA()), + ); + + expect( + () => repo.revParse('master........5aecfa'), + throwsA(isA()), + ); + }); + }); +}