diff --git a/lib/src/bindings/describe.dart b/lib/src/bindings/describe.dart new file mode 100644 index 0000000..8c06cae --- /dev/null +++ b/lib/src/bindings/describe.dart @@ -0,0 +1,158 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'libgit2_bindings.dart'; +import '../error.dart'; +import '../util.dart'; + +/// Describe a commit. +/// +/// Perform the describe operation on the given committish object. +/// +/// Returned object should be freed with `describeResultFree()` once no longer needed. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer commit({ + required Pointer commitPointer, + int? maxCandidatesTags, + int? describeStrategy, + String? pattern, + bool? onlyFollowFirstParent, + bool? showCommitOidAsFallback, +}) { + final out = calloc>(); + final opts = _initOpts( + maxCandidatesTags: maxCandidatesTags, + describeStrategy: describeStrategy, + pattern: pattern, + onlyFollowFirstParent: onlyFollowFirstParent, + showCommitOidAsFallback: showCommitOidAsFallback, + ); + + final error = libgit2.git_describe_commit(out, commitPointer.cast(), opts); + + calloc.free(opts); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Describe a commit. +/// +/// Perform the describe operation on the current commit and the worktree. +/// After peforming describe on HEAD, a status is run and the description is +/// considered to be dirty if there are. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer workdir({ + required Pointer repo, + int? maxCandidatesTags, + int? describeStrategy, + String? pattern, + bool? onlyFollowFirstParent, + bool? showCommitOidAsFallback, +}) { + final out = calloc>(); + final opts = _initOpts( + maxCandidatesTags: maxCandidatesTags, + describeStrategy: describeStrategy, + pattern: pattern, + onlyFollowFirstParent: onlyFollowFirstParent, + showCommitOidAsFallback: showCommitOidAsFallback, + ); + + final error = libgit2.git_describe_workdir(out, repo, opts); + + calloc.free(opts); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Print the describe result to a buffer. +/// +/// Throws a [LibGit2Error] if error occured. +String format({ + required Pointer describeResultPointer, + int? abbreviatedSize, + bool? alwaysUseLongFormat, + String? dirtySuffix, +}) { + final out = calloc(sizeOf()); + final opts = calloc(); + final optsError = libgit2.git_describe_format_options_init( + opts, + GIT_DESCRIBE_FORMAT_OPTIONS_VERSION, + ); + + if (optsError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + if (abbreviatedSize != null) { + opts.ref.abbreviated_size = abbreviatedSize; + } + if (alwaysUseLongFormat != null) { + opts.ref.always_use_long_format = alwaysUseLongFormat ? 1 : 0; + } + if (dirtySuffix != null) { + opts.ref.dirty_suffix = dirtySuffix.toNativeUtf8().cast(); + } + + final error = libgit2.git_describe_format(out, describeResultPointer, opts); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + final result = out.ref.ptr.cast().toDartString(); + calloc.free(out); + return result; + } +} + +/// Free the describe result. +void describeResultFree(Pointer result) { + libgit2.git_describe_result_free(result); +} + +/// Initialize git_describe_options structure. +Pointer _initOpts({ + int? maxCandidatesTags, + int? describeStrategy, + String? pattern, + bool? onlyFollowFirstParent, + bool? showCommitOidAsFallback, +}) { + final opts = calloc(); + final error = libgit2.git_describe_options_init( + opts, + GIT_DESCRIBE_OPTIONS_VERSION, + ); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + if (maxCandidatesTags != null) { + opts.ref.max_candidates_tags = maxCandidatesTags; + } + if (describeStrategy != null) { + opts.ref.describe_strategy = describeStrategy; + } + if (pattern != null) { + opts.ref.pattern = pattern.toNativeUtf8().cast(); + } + if (onlyFollowFirstParent != null) { + opts.ref.only_follow_first_parent = onlyFollowFirstParent ? 1 : 0; + } + if (showCommitOidAsFallback != null) { + opts.ref.show_commit_oid_as_fallback = showCommitOidAsFallback ? 1 : 0; + } + + return opts; +} diff --git a/lib/src/git_types.dart b/lib/src/git_types.dart index 0d3f4cd..d4f6459 100644 --- a/lib/src/git_types.dart +++ b/lib/src/git_types.dart @@ -1498,3 +1498,30 @@ class GitRebaseOperation { @override String toString() => 'GitRebaseOperation.$_name'; } + +/// Reference lookup strategy. +/// +/// These behave like the --tags and --all options to git-describe, +/// namely they say to look for any reference in either refs/tags/ or +/// refs/ respectively. +class GitDescribeStrategy { + const GitDescribeStrategy._(this._value, this._name); + final int _value; + final String _name; + + /// Only match annotated tags. + static const defaultStrategy = GitDescribeStrategy._(0, 'defaultStrategy'); + + /// Match everything under `refs/tags/` (includes lightweight tags). + static const tags = GitDescribeStrategy._(1, 'tags'); + + /// Match everything under `refs/` (includes branches). + static const all = GitDescribeStrategy._(2, 'all'); + + static const List values = [defaultStrategy, tags, all]; + + int get value => _value; + + @override + String toString() => 'GitDescribeStrategy.$_name'; +} diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 35eb2f8..402b821 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -13,6 +13,7 @@ import 'bindings/diff.dart' as diff_bindings; import 'bindings/stash.dart' as stash_bindings; import 'bindings/attr.dart' as attr_bindings; import 'bindings/graph.dart' as graph_bindings; +import 'bindings/describe.dart' as describe_bindings; import 'branch.dart'; import 'commit.dart'; import 'config.dart'; @@ -530,6 +531,11 @@ class Repository { force: force); } + /// Returns a list with all the tags in the repository. + /// + /// Throws a [LibGit2Error] if error occured. + List get tags => Tag.list(this); + /// Returns a [Branches] object. Branches get branches => Branches(this); @@ -1218,4 +1224,77 @@ class Repository { upstreamPointer: upstream.pointer, ); } + + /// Describes a commit or the current worktree. + /// + /// [maxCandidatesTags] is the number of candidate tags to consider. Increasing above 10 will + /// take slightly longer but may produce a more accurate result. A value of 0 will cause + /// only exact matches to be output. Default is 10. + /// + /// [describeStrategy] is reference lookup strategy that is one of [GitDescribeStrategy]. + /// Default matches only annotated tags. + /// + /// [pattern] is pattern to use for tags matching, excluding the "refs/tags/" prefix. + /// + /// [onlyFollowFirstParent] checks whether or not to follow only the first parent + /// commit upon seeing a merge commit. + /// + /// [showCommitOidAsFallback] determines if full id of the commit should be shown + /// if no matching tag or reference is found. + /// + /// [abbreviatedSize] is the minimum number of hexadecimal digits to show for abbreviated + /// object names. A value of 0 will suppress long format, only showing the closest tag. + /// Default is 7. + /// + /// [alwaysUseLongFormat] determines if he long format (the nearest tag, the number of + /// commits, and the abbrevated commit name) should be used even when the commit matches + /// the tag. + /// + /// [dirtySuffix] is a string to append if the working tree is dirty. + /// + /// Throws a [LibGit2Error] if error occured. + String describe({ + Commit? commit, + int? maxCandidatesTags, + GitDescribeStrategy? describeStrategy, + String? pattern, + bool? onlyFollowFirstParent, + bool? showCommitOidAsFallback, + int? abbreviatedSize, + bool? alwaysUseLongFormat, + String? dirtySuffix, + }) { + late final Pointer describeResult; + + if (commit != null) { + describeResult = describe_bindings.commit( + commitPointer: commit.pointer, + maxCandidatesTags: maxCandidatesTags, + describeStrategy: describeStrategy?.value, + pattern: pattern, + onlyFollowFirstParent: onlyFollowFirstParent, + showCommitOidAsFallback: showCommitOidAsFallback, + ); + } else { + describeResult = describe_bindings.workdir( + repo: _repoPointer, + maxCandidatesTags: maxCandidatesTags, + describeStrategy: describeStrategy?.value, + pattern: pattern, + onlyFollowFirstParent: onlyFollowFirstParent, + showCommitOidAsFallback: showCommitOidAsFallback, + ); + } + + final result = describe_bindings.format( + describeResultPointer: describeResult, + abbreviatedSize: abbreviatedSize, + alwaysUseLongFormat: alwaysUseLongFormat, + dirtySuffix: dirtySuffix, + ); + + describe_bindings.describeResultFree(describeResult); + + return result; + } } diff --git a/test/describe_test.dart b/test/describe_test.dart new file mode 100644 index 0000000..586ba81 --- /dev/null +++ b/test/describe_test.dart @@ -0,0 +1,143 @@ +import 'dart:io'; +import 'package:libgit2dart/src/git_types.dart'; +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'helpers/util.dart'; + +void main() { + late Repository repo; + late Directory tmpDir; + + setUp(() async { + tmpDir = await setupRepo(Directory('test/assets/testrepo/')); + repo = Repository.open(tmpDir.path); + }); + + tearDown(() async { + repo.free(); + await tmpDir.delete(recursive: true); + }); + + group('Describe', () { + test('successfully describes with default arguments', () { + expect(repo.describe(), 'v0.2'); + }); + + test('successfully describes commit', () { + final tag = Tag.lookup(repo: repo, sha: 'f0fdbf5'); + tag.delete(); + + expect( + repo.describe(describeStrategy: GitDescribeStrategy.tags), + 'v0.1-1-g821ed6e', + ); + + tag.free(); + }); + + test('throws when trying to describe and no reference found', () { + final commit = repo['f17d0d48'] as Commit; + expect(() => repo.describe(commit: commit), throwsA(isA())); + commit.free(); + }); + + test('returns oid when fallback argument is provided', () { + final commit = repo['f17d0d48'] as Commit; + expect( + repo.describe(commit: commit, showCommitOidAsFallback: true), + 'f17d0d4', + ); + commit.free(); + }); + + test('successfully describes with provided strategy', () { + final commit = repo['5aecfa0'] as Commit; + expect( + repo.describe( + commit: commit, + describeStrategy: GitDescribeStrategy.all, + ), + 'heads/feature', + ); + commit.free(); + }); + + test('successfully describes with provided pattern', () { + final signature = repo.defaultSignature; + final commit = repo['fc38877'] as Commit; + repo.createTag( + tagName: 'test/tag1', + target: 'f17d0d48', + targetType: GitObject.commit, + tagger: signature, + message: '', + ); + + expect( + repo.describe(commit: commit, pattern: 'test/*'), + 'test/tag1-2-gfc38877', + ); + + commit.free(); + signature.free(); + }); + + test('successfully describes and follows first parent only', () { + final tag = Tag.lookup(repo: repo, sha: 'f0fdbf5'); + tag.delete(); + + final commit = repo['821ed6e'] as Commit; + expect( + repo.describe( + commit: commit, + onlyFollowFirstParent: true, + describeStrategy: GitDescribeStrategy.tags, + ), + 'v0.1-1-g821ed6e', + ); + + tag.free(); + commit.free(); + }); + + test('successfully describes with abbreviated size provided', () { + final tag = Tag.lookup(repo: repo, sha: 'f0fdbf5'); + tag.delete(); + + final commit = repo['821ed6e'] as Commit; + expect( + repo.describe( + commit: commit, + describeStrategy: GitDescribeStrategy.tags, + abbreviatedSize: 20, + ), + 'v0.1-1-g821ed6e80627b8769d17', + ); + + expect( + repo.describe( + commit: commit, + describeStrategy: GitDescribeStrategy.tags, + abbreviatedSize: 0, + ), + 'v0.1', + ); + + tag.free(); + commit.free(); + }); + + test('successfully describes with long format', () { + expect(repo.describe(alwaysUseLongFormat: true), 'v0.2-0-g821ed6e'); + }); + + test('successfully describes and appends dirty suffix', () { + final index = repo.index; + index.clear(); + + expect(repo.describe(dirtySuffix: '-dirty'), 'v0.2-dirty'); + + index.free(); + }); + }); +} diff --git a/test/odb_test.dart b/test/odb_test.dart index 5abb18f..f0ab320 100644 --- a/test/odb_test.dart +++ b/test/odb_test.dart @@ -1,6 +1,4 @@ import 'dart:io'; - -import 'package:libgit2dart/src/git_types.dart'; import 'package:test/test.dart'; import 'package:libgit2dart/libgit2dart.dart'; import 'helpers/util.dart'; diff --git a/test/tag_test.dart b/test/tag_test.dart index afb9696..c79196e 100644 --- a/test/tag_test.dart +++ b/test/tag_test.dart @@ -86,10 +86,10 @@ void main() { }); test('successfully deletes tag', () { - expect(Tag.list(repo), ['v0.1', 'v0.2']); + expect(repo.tags, ['v0.1', 'v0.2']); tag.delete(); - expect(Tag.list(repo), ['v0.1']); + expect(repo.tags, ['v0.1']); }); }); }