From 3e1ece4e6f37447d4146c2d2edf3988f3e534c6f Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Mon, 24 Jan 2022 16:42:08 +0300 Subject: [PATCH] feat(tag)!: add bindings and API methods (#36) - add binding and API method for git_tag_create_lightweight - add API method that returns tag target type BREAKING CHANGE: add specific methods `createAnnotated` and `createLightweight`. --- lib/src/bindings/tag.dart | 43 +++++++- lib/src/repository.dart | 55 ++++++++-- lib/src/tag.dart | 71 +++++++++++-- test/describe_test.dart | 2 +- test/repository_test.dart | 24 ++++- test/tag_test.dart | 213 ++++++++++++++++++++++++++++++++++---- 6 files changed, 366 insertions(+), 42 deletions(-) diff --git a/lib/src/bindings/tag.dart b/lib/src/bindings/tag.dart index 4210428..d177a64 100644 --- a/lib/src/bindings/tag.dart +++ b/lib/src/bindings/tag.dart @@ -84,7 +84,7 @@ String message(Pointer tag) => Pointer tagger(Pointer tag) => libgit2.git_tag_tagger(tag); -/// Create a new tag in the repository from an object. +/// Create a new annotated tag in the repository from an object. /// /// A new reference will also be created pointing to this tag object. If force /// is true and a reference already exists with the given name, it'll be @@ -97,7 +97,7 @@ Pointer tagger(Pointer tag) => /// special meaning to revparse. /// /// Throws a [LibGit2Error] if error occured. -Pointer create({ +Pointer createAnnotated({ required Pointer repoPointer, required String tagName, required Pointer targetPointer, @@ -108,7 +108,6 @@ Pointer create({ final out = calloc(); final tagNameC = tagName.toNativeUtf8().cast(); final messageC = message.toNativeUtf8().cast(); - final forceC = force ? 1 : 0; final error = libgit2.git_tag_create( out, repoPointer, @@ -116,7 +115,7 @@ Pointer create({ targetPointer, taggerPointer, messageC, - forceC, + force ? 1 : 0, ); calloc.free(tagNameC); @@ -130,6 +129,42 @@ Pointer create({ } } +/// Create a new lightweight tag pointing at a target object. +/// +/// A new direct reference will be created pointing to this target object. If +/// force is true and a reference already exists with the given name, it'll be +/// replaced. +/// +/// The tag name will be checked for validity. See [createAnnotated] for rules +/// about valid names. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer createLightweight({ + required Pointer repoPointer, + required String tagName, + required Pointer targetPointer, + required bool force, +}) { + final out = calloc(); + final tagNameC = tagName.toNativeUtf8().cast(); + final error = libgit2.git_tag_create_lightweight( + out, + repoPointer, + tagNameC, + targetPointer, + force ? 1 : 0, + ); + + calloc.free(tagNameC); + + if (error < 0) { + calloc.free(out); + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} + /// Delete an existing tag reference. /// /// The tag name will be checked for validity. diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 0cd9b62..9898ca1 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -746,13 +746,12 @@ class Repository { /// **IMPORTANT**: Should be freed to release allocated memory. Tag lookupTag(Oid oid) => Tag.lookup(repo: this, oid: oid); - /// Creates a new tag in the repository for provided [target] object. + /// Creates a new annotated tag in the repository for provided [target] + /// object. /// - /// A new reference will also be created pointing to this tag object. If - /// [force] is true and a reference already exists with the given name, it'll - /// be replaced. - /// - /// The [message] will not be cleaned up. + /// A new reference will also be created in the `/refs/tags` folder pointing + /// to this tag object. If [force] is true and a reference already exists + /// with the given name, it'll be replaced. /// /// The [tagName] will be checked for validity. You must avoid the characters /// '~', '^', ':', '\', '?', '[', and '*', and the sequences ".." and "@{" which have @@ -777,7 +776,7 @@ class Repository { /// should be replaced. /// /// Throws a [LibGit2Error] if error occured. - Oid createTag({ + Oid createAnnotatedTag({ required String tagName, required Oid target, required GitObject targetType, @@ -785,7 +784,7 @@ class Repository { required String message, bool force = false, }) { - return Tag.create( + return Tag.createAnnotated( repo: this, tagName: tagName, target: target, @@ -796,6 +795,46 @@ class Repository { ); } + /// Creates a new lightweight tag in the repository for provided [target] + /// object. + /// + /// A new reference will also be created in the `/refs/tags` folder pointing + /// to this tag object. If [force] is true and a reference already exists + /// with the given name, it'll be replaced. + /// + /// The [tagName] will be checked for validity. You must avoid the characters + /// '~', '^', ':', '\', '?', '[', and '*', and the sequences ".." and "@{" which have + /// special meaning to revparse. + /// + /// [tagName] is the name for the tag. This name is validated for + /// consistency. It should also not conflict with an already existing tag + /// name. + /// + /// [target] is the object to which this tag points. This object must belong + /// to the given [repo]. + /// + /// [targetType] is one of the [GitObject] basic types: commit, tree, blob or + /// tag. + /// + /// [force] determines whether existing reference with the same [tagName] + /// should be replaced. + /// + /// Throws a [LibGit2Error] if error occured. + void createLightweightTag({ + required String tagName, + required Oid target, + required GitObject targetType, + bool force = false, + }) { + Tag.createLightweight( + repo: this, + tagName: tagName, + target: target, + targetType: targetType, + force: force, + ); + } + /// Deletes an existing tag reference with provided [name]. /// /// The tag [name] will be checked for validity. diff --git a/lib/src/tag.dart b/lib/src/tag.dart index e7c9d10..fb1fec3 100644 --- a/lib/src/tag.dart +++ b/lib/src/tag.dart @@ -25,13 +25,12 @@ class Tag { /// Pointer to memory address for allocated tag object. late final Pointer _tagPointer; - /// Creates a new tag in the repository for provided [target] object. + /// Creates a new annotated tag in the repository for provided [target] + /// object. /// - /// A new reference will also be created pointing to this tag object. If - /// [force] is true and a reference already exists with the given name, it'll - /// be replaced. - /// - /// The [message] will not be cleaned up. + /// A new reference will also be created in the `/refs/tags` folder pointing + /// to this tag object. If [force] is true and a reference already exists + /// with the given name, it'll be replaced. /// /// The [tagName] will be checked for validity. You must avoid the characters /// '~', '^', ':', '\', '?', '[', and '*', and the sequences ".." and "@{" which have @@ -58,7 +57,7 @@ class Tag { /// should be replaced. /// /// Throws a [LibGit2Error] if error occured. - static Oid create({ + static Oid createAnnotated({ required Repository repo, required String tagName, required Oid target, @@ -73,7 +72,7 @@ class Tag { type: targetType.value, ); - final result = bindings.create( + final result = bindings.createAnnotated( repoPointer: repo.pointer, tagName: tagName, targetPointer: object, @@ -87,6 +86,56 @@ class Tag { return Oid(result); } + /// Creates a new lightweight tag in the repository for provided [target] + /// object. + /// + /// A new reference will also be created in the `/refs/tags` folder pointing + /// to this tag object. If [force] is true and a reference already exists + /// with the given name, it'll be replaced. + /// + /// The [tagName] will be checked for validity. You must avoid the characters + /// '~', '^', ':', '\', '?', '[', and '*', and the sequences ".." and "@{" which have + /// special meaning to revparse. + /// + /// [repo] is the repository where to store the tag. + /// + /// [tagName] is the name for the tag. This name is validated for + /// consistency. It should also not conflict with an already existing tag + /// name. + /// + /// [target] is the object to which this tag points. This object must belong + /// to the given [repo]. + /// + /// [targetType] is one of the [GitObject] basic types: commit, tree, blob or + /// tag. + /// + /// [force] determines whether existing reference with the same [tagName] + /// should be replaced. + /// + /// Throws a [LibGit2Error] if error occured. + static void createLightweight({ + required Repository repo, + required String tagName, + required Oid target, + required GitObject targetType, + bool force = false, + }) { + final object = object_bindings.lookup( + repoPointer: repo.pointer, + oidPointer: target.pointer, + type: targetType.value, + ); + + bindings.createLightweight( + repoPointer: repo.pointer, + tagName: tagName, + targetPointer: object, + force: force, + ); + + object_bindings.free(object); + } + /// Deletes an existing tag reference with provided [name] in a [repo]sitory. /// /// The tag [name] will be checked for validity. @@ -137,6 +186,12 @@ class Tag { } } + /// The type of a tag's tagged object. + GitObject get targetType { + final type = bindings.targetType(_tagPointer); + return GitObject.values.firstWhere((e) => type & e.value == e.value); + } + /// [Oid] of the tagged object of a tag. Oid get targetOid => Oid(bindings.targetOid(_tagPointer)); diff --git a/test/describe_test.dart b/test/describe_test.dart index 24db481..fdc0fbe 100644 --- a/test/describe_test.dart +++ b/test/describe_test.dart @@ -74,7 +74,7 @@ void main() { time: 1234, ); final commit = repo.lookupCommit(repo['fc38877']); - repo.createTag( + repo.createAnnotatedTag( tagName: 'test/tag1', target: repo['f17d0d48'], targetType: GitObject.commit, diff --git a/test/repository_test.dart b/test/repository_test.dart index 68d20fb..6f58c80 100644 --- a/test/repository_test.dart +++ b/test/repository_test.dart @@ -196,7 +196,7 @@ void main() { }); }); - test('creates tag with provided sha', () { + test('creates annotated tag with provided sha', () { final signature = Signature.create( name: 'Author', email: 'author@email.com', @@ -206,7 +206,7 @@ void main() { final target = repo['f17d0d48eae3aa08cecf29128a35e310c97b3521']; const message = 'init tag\n'; - final oid = repo.createTag( + final oid = repo.createAnnotatedTag( tagName: tagName, target: target, targetType: GitObject.commit, @@ -218,7 +218,7 @@ void main() { final tagger = newTag.tagger; final newTagTarget = newTag.target as Commit; - expect(newTag.oid.sha, '131a5eb6b7a880b5096c550ee7351aeae7b95a42'); + expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(tagger, signature); @@ -229,6 +229,24 @@ void main() { signature.free(); }); + test('creates lightweight tag with provided sha', () { + const tagName = 'tag'; + final target = repo['f17d0d48eae3aa08cecf29128a35e310c97b3521']; + + repo.createLightweightTag( + tagName: tagName, + target: target, + targetType: GitObject.commit, + ); + + final newTag = repo.lookupReference('refs/tags/$tagName'); + + expect(newTag.shorthand, tagName); + expect(newTag.target, target); + + newTag.free(); + }); + test('returns status of a repository', () { File(p.join(tmpDir.path, 'new_file.txt')).createSync(); final index = repo.index; diff --git a/test/tag_test.dart b/test/tag_test.dart index 6654ef0..b123327 100644 --- a/test/tag_test.dart +++ b/test/tag_test.dart @@ -55,6 +55,7 @@ void main() { expect(tag.oid, tagOid); expect(tag.name, 'v0.2'); expect(tag.message, 'annotated tag\n'); + expect(tag.targetType, GitObject.commit); expect(target.message, 'add subdirectory file\n'); expect(tagger, signature); expect(tag.toString(), contains('Tag{')); @@ -63,7 +64,7 @@ void main() { target.free(); }); - test('creates new tag with commit as target', () { + test('creates new annotated tag with commit as target', () { final signature = Signature.create( name: 'Author', email: 'author@email.com', @@ -74,7 +75,8 @@ void main() { final target = repo[targetSHA]; const message = 'init tag\n'; - final oid = repo.createTag( + final oid = Tag.createAnnotated( + repo: repo, tagName: tagName, target: target, targetType: GitObject.commit, @@ -86,7 +88,7 @@ void main() { final tagger = newTag.tagger; final newTagTarget = newTag.target as Commit; - expect(newTag.oid.sha, '131a5eb6b7a880b5096c550ee7351aeae7b95a42'); + expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(newTag.targetOid.sha, targetSHA); @@ -98,7 +100,26 @@ void main() { signature.free(); }); - test('creates new tag with tree as target', () { + test('creates new lightweight tag with commit as target', () { + const tagName = 'tag'; + final target = repo['f17d0d48eae3aa08cecf29128a35e310c97b3521']; + + Tag.createLightweight( + repo: repo, + tagName: tagName, + target: target, + targetType: GitObject.commit, + ); + + final newTag = repo.lookupReference('refs/tags/$tagName'); + + expect(newTag.shorthand, tagName); + expect(newTag.target, target); + + newTag.free(); + }); + + test('creates new annotated tag with tree as target', () { final signature = Signature.create( name: 'Author', email: 'author@email.com', @@ -108,7 +129,8 @@ void main() { final target = repo['a8ae3dd59e6e1802c6f78e05e301bfd57c9f334f']; const message = 'init tag\n'; - final oid = repo.createTag( + final oid = Tag.createAnnotated( + repo: repo, tagName: tagName, target: target, targetType: GitObject.tree, @@ -120,7 +142,7 @@ void main() { final tagger = newTag.tagger; final newTagTarget = newTag.target as Tree; - expect(newTag.oid.sha, 'ca715c0bafad5d39d568675aad69f71a82178416'); + expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(tagger, signature); @@ -131,7 +153,26 @@ void main() { signature.free(); }); - test('creates new tag with blob as target', () { + test('creates new lightweight tag with tree as target', () { + const tagName = 'tag'; + final target = repo['a8ae3dd59e6e1802c6f78e05e301bfd57c9f334f']; + + Tag.createLightweight( + repo: repo, + tagName: tagName, + target: target, + targetType: GitObject.tree, + ); + + final newTag = repo.lookupReference('refs/tags/$tagName'); + + expect(newTag.shorthand, tagName); + expect(newTag.target, target); + + newTag.free(); + }); + + test('creates new annotated tag with blob as target', () { final signature = Signature.create( name: 'Author', email: 'author@email.com', @@ -141,7 +182,8 @@ void main() { final target = repo['9c78c21d6680a7ffebc76f7ac68cacc11d8f48bc']; const message = 'init tag\n'; - final oid = repo.createTag( + final oid = Tag.createAnnotated( + repo: repo, tagName: tagName, target: target, targetType: GitObject.blob, @@ -153,7 +195,7 @@ void main() { final tagger = newTag.tagger; final newTagTarget = newTag.target as Blob; - expect(newTag.oid.sha, '8b1edabda95e934d2252e563219315b08e38dce5'); + expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(tagger, signature); @@ -164,7 +206,26 @@ void main() { signature.free(); }); - test('creates new tag with tag as target', () { + test('creates new lightweight tag with blob as target', () { + const tagName = 'tag'; + final target = repo['9c78c21d6680a7ffebc76f7ac68cacc11d8f48bc']; + + Tag.createLightweight( + repo: repo, + tagName: tagName, + target: target, + targetType: GitObject.blob, + ); + + final newTag = repo.lookupReference('refs/tags/$tagName'); + + expect(newTag.shorthand, tagName); + expect(newTag.target, target); + + newTag.free(); + }); + + test('creates new annotated tag with tag as target', () { final signature = Signature.create( name: 'Author', email: 'author@email.com', @@ -173,7 +234,8 @@ void main() { const tagName = 'tag'; const message = 'init tag\n'; - final oid = repo.createTag( + final oid = Tag.createAnnotated( + repo: repo, tagName: tagName, target: tag.oid, targetType: GitObject.tag, @@ -185,7 +247,7 @@ void main() { final tagger = newTag.tagger; final newTagTarget = newTag.target as Tag; - expect(newTag.oid.sha, '20286cf6c3b150b58b6c419814b0931d9b17c2ba'); + expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(tagger, signature); @@ -196,12 +258,101 @@ void main() { signature.free(); }); - test('throws when trying to create tag with invalid name', () { + test('creates new lightweight tag with tag as target', () { + const tagName = 'tag'; + + Tag.createLightweight( + repo: repo, + tagName: tagName, + target: tag.oid, + targetType: GitObject.tag, + ); + + final newTag = repo.lookupReference('refs/tags/$tagName'); + + expect(newTag.shorthand, tagName); + expect(newTag.target, tag.oid); + + newTag.free(); + }); + + test( + 'creates new annotated tag with already existing name ' + 'when force is set to true', () { + final signature = Signature.create( + name: 'Author', + email: 'author@email.com', + time: 1234, + ); + final tagName = tag.name; + const targetSHA = 'f17d0d48eae3aa08cecf29128a35e310c97b3521'; + final target = repo[targetSHA]; + const message = 'init tag\n'; + + expect(tag.targetOid.sha, isNot(targetSHA)); + expect(repo.tags.length, equals(2)); + + final oid = Tag.createAnnotated( + repo: repo, + tagName: tagName, + target: target, + targetType: GitObject.commit, + tagger: signature, + message: message, + force: true, + ); + + final newTag = repo.lookupTag(oid); + final tagger = newTag.tagger; + final newTagTarget = newTag.target as Commit; + + expect(newTag.oid, oid); + expect(newTag.name, tagName); + expect(newTag.message, message); + expect(newTag.targetOid.sha, targetSHA); + expect(tagger, signature); + expect(newTagTarget.oid, target); + expect(repo.tags.length, equals(2)); + + newTag.free(); + newTagTarget.free(); + signature.free(); + }); + + test( + 'creates new lightweight tag with already existing name ' + 'when force is set to true', () { + final tagName = tag.name; + const targetSHA = 'f17d0d48eae3aa08cecf29128a35e310c97b3521'; + final target = repo[targetSHA]; + + expect(tag.targetOid.sha, isNot(targetSHA)); + expect(repo.tags.length, equals(2)); + + Tag.createLightweight( + repo: repo, + tagName: tagName, + target: target, + targetType: GitObject.commit, + force: true, + ); + + final newTag = repo.lookupReference('refs/tags/$tagName'); + + expect(newTag.shorthand, tagName); + expect(newTag.target, target); + expect(repo.tags.length, equals(2)); + + newTag.free(); + }); + + test('throws when trying to create annotated tag with invalid name', () { expect( - () => repo.createTag( + () => Tag.createAnnotated( + repo: repo, tagName: '', target: repo['9c78c21'], - targetType: GitObject.any, + targetType: GitObject.commit, tagger: Signature(nullptr), message: '', ), @@ -209,12 +360,25 @@ void main() { ); }); - test('throws when trying to create tag with invalid target', () { + test('throws when trying to create lightweight tag with invalid name', () { expect( - () => repo.createTag( + () => Tag.createLightweight( + repo: repo, + tagName: '', + target: repo['9c78c21'], + targetType: GitObject.commit, + ), + throwsA(isA()), + ); + }); + + test('throws when trying to create annotated tag with invalid target', () { + expect( + () => Tag.createAnnotated( + repo: repo, tagName: '', target: repo['0' * 40], - targetType: GitObject.any, + targetType: GitObject.commit, tagger: Signature(nullptr), message: '', ), @@ -222,6 +386,19 @@ void main() { ); }); + test('throws when trying to create lightweight tag with invalid target', + () { + expect( + () => Tag.createLightweight( + repo: repo, + tagName: '', + target: repo['0' * 40], + targetType: GitObject.commit, + ), + throwsA(isA()), + ); + }); + test('returns list of tags in repository', () { expect(Tag.list(repo), ['v0.1', 'v0.2']); });