From 84ee4be945a8ef3b66903259507a67c9f55ba918 Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Thu, 2 Sep 2021 11:58:14 +0300 Subject: [PATCH] feat(tree): add bindings and api --- lib/src/bindings/index.dart | 4 +- lib/src/bindings/tree.dart | 86 ++++++++++++++-- lib/src/commit.dart | 7 +- lib/src/index.dart | 42 +------- lib/src/tree.dart | 95 ++++++++++++++++-- lib/src/util.dart | 39 ++++++- test/assets/testrepo/.gitdir/COMMIT_EDITMSG | 2 +- test/assets/testrepo/.gitdir/index | Bin 297 -> 405 bytes test/assets/testrepo/.gitdir/logs/HEAD | 1 + .../testrepo/.gitdir/logs/refs/heads/master | 1 + .../66/bd6147704f78165d764ce8348ccaec81415db4 | Bin 0 -> 57 bytes .../82/1ed6e80627b8769d170a293862f9fc60825226 | 1 + .../a8/ae3dd59e6e1802c6f78e05e301bfd57c9f334f | Bin 0 -> 146 bytes .../assets/testrepo/.gitdir/refs/heads/master | 2 +- .../testrepo/.gitdir/refs/tags/empty_marker | 0 test/assets/testrepo/dir/dir_file.txt | 0 test/index_test.dart | 22 ++-- test/reference_test.dart | 2 +- test/reflog_test.dart | 9 +- test/repository_test.dart | 3 +- test/revparse_test.dart | 4 +- test/tree_test.dart | 80 +++++++++++++++ 22 files changed, 316 insertions(+), 84 deletions(-) create mode 100644 test/assets/testrepo/.gitdir/objects/66/bd6147704f78165d764ce8348ccaec81415db4 create mode 100644 test/assets/testrepo/.gitdir/objects/82/1ed6e80627b8769d170a293862f9fc60825226 create mode 100644 test/assets/testrepo/.gitdir/objects/a8/ae3dd59e6e1802c6f78e05e301bfd57c9f334f create mode 100644 test/assets/testrepo/.gitdir/refs/tags/empty_marker create mode 100644 test/assets/testrepo/dir/dir_file.txt create mode 100644 test/tree_test.dart diff --git a/lib/src/bindings/index.dart b/lib/src/bindings/index.dart index 4741233..bc1e1ea 100644 --- a/lib/src/bindings/index.dart +++ b/lib/src/bindings/index.dart @@ -77,7 +77,7 @@ int entryCount(Pointer index) => libgit2.git_index_entrycount(index); /// /// The entry is not modifiable and should not be freed. /// -/// Throws error if position is out of bounds. +/// Throws [RangeError] when provided index is outside of valid range. Pointer getByIndex(Pointer index, int n) { final result = libgit2.git_index_get_byindex(index, n); @@ -92,7 +92,7 @@ Pointer getByIndex(Pointer index, int n) { /// ///The entry is not modifiable and should not be freed. /// -/// Throws error if entry isn't found. +/// Throws [ArgumentError] if nothing found for provided path. Pointer getByPath( Pointer index, String path, diff --git a/lib/src/bindings/tree.dart b/lib/src/bindings/tree.dart index 5eb8672..c0c07fb 100644 --- a/lib/src/bindings/tree.dart +++ b/lib/src/bindings/tree.dart @@ -21,16 +21,57 @@ Pointer lookup(Pointer repo, Pointer id) { } } -/// Lookup a tree object from the repository, given a prefix of its identifier (short id). +/// Get the repository that contains the tree. +Pointer owner(Pointer tree) => + libgit2.git_tree_owner(tree); + +/// Lookup a tree entry by its position in the tree. +/// +/// This returns a tree entry that is owned by the tree. You don't have to free it, +/// but you must not use it after the tree is released. +/// +/// Throws [RangeError] when provided index is outside of valid range. +Pointer getByIndex(Pointer tree, int index) { + final result = libgit2.git_tree_entry_byindex(tree, index); + + if (result == nullptr) { + throw RangeError('Out of bounds'); + } else { + return result; + } +} + +/// Lookup a tree entry by its filename. +/// +/// This returns a tree entry that is owned by the tree. You don't have to free it, +/// but you must not use it after the tree is released. +/// +/// Throws [ArgumentError] if nothing found for provided filename. +Pointer getByName(Pointer tree, String filename) { + final filenameC = filename.toNativeUtf8().cast(); + final result = libgit2.git_tree_entry_byname(tree, filenameC); + + calloc.free(filenameC); + + if (result == nullptr) { + throw ArgumentError.value('$filename was not found'); + } else { + return result; + } +} + +/// Retrieve a tree entry contained in a tree or in any of its subtrees, given its relative path. +/// +/// Unlike the other lookup functions, the returned tree entry is owned by the user and must be +/// freed explicitly with `entryFree()`. /// /// Throws a [LibGit2Error] if error occured. -Pointer lookupPrefix( - Pointer repo, - Pointer id, - int len, -) { - final out = calloc>(); - final error = libgit2.git_tree_lookup_prefix(out, repo, id, len); +Pointer getByPath(Pointer root, String path) { + final out = calloc>(); + final pathC = path.toNativeUtf8().cast(); + final error = libgit2.git_tree_entry_bypath(out, root, pathC); + + calloc.free(pathC); if (error < 0) { throw LibGit2Error(libgit2.git_error_last()); @@ -39,5 +80,34 @@ Pointer lookupPrefix( } } +/// Get the number of entries listed in a tree. +int entryCount(Pointer tree) => libgit2.git_tree_entrycount(tree); + +/// Get the id of the object pointed by the entry. +Pointer entryId(Pointer entry) => + libgit2.git_tree_entry_id(entry); + +/// Get the filename of a tree entry. +String entryName(Pointer entry) => + libgit2.git_tree_entry_name(entry).cast().toDartString(); + +/// Get the UNIX file attributes of a tree entry. +int entryFilemode(Pointer entry) => + libgit2.git_tree_entry_filemode(entry); + +/// Compare two tree entries. +/// +/// Returns <0 if e1 is before e2, 0 if e1 == e2, >0 if e1 is after e2. +int compare(Pointer e1, Pointer e2) { + return libgit2.git_tree_entry_cmp(e1, e2); +} + +/// Free a user-owned tree entry. +/// +/// IMPORTANT: This function is only needed for tree entries owned by the user, +/// such as `getByPath()`. +void entryFree(Pointer entry) => + libgit2.git_tree_entry_free(entry); + /// Close an open tree to release memory. void free(Pointer tree) => libgit2.git_tree_free(tree); diff --git a/lib/src/commit.dart b/lib/src/commit.dart index 52ac032..cbeb8f2 100644 --- a/lib/src/commit.dart +++ b/lib/src/commit.dart @@ -1,8 +1,6 @@ import 'dart:ffi'; import 'bindings/libgit2_bindings.dart'; import 'bindings/commit.dart' as bindings; -import 'bindings/oid.dart' as oid_bindings; -import 'bindings/tree.dart' as tree_bindings; import 'repository.dart'; import 'oid.dart'; import 'signature.dart'; @@ -42,9 +40,8 @@ class Commit { String? updateRef, String? messageEncoding, }) { - final treeOid = oid_bindings.fromStrN(treeSHA); - final tree = - Tree(tree_bindings.lookupPrefix(repo.pointer, treeOid, treeSHA.length)); + final treeOid = Oid.fromSHA(repo, treeSHA); + final tree = Tree.lookup(repo, treeOid); final result = Oid(bindings.create( repo.pointer, diff --git a/lib/src/index.dart b/lib/src/index.dart index bd96dbb..0adea12 100644 --- a/lib/src/index.dart +++ b/lib/src/index.dart @@ -164,46 +164,12 @@ class IndexEntry { /// Returns id of the index entry as sha-1 hex. String get sha => _oidToHex(_indexEntryPointer.ref.id); - GitFilemode get mode { - switch (_indexEntryPointer.ref.mode) { - case 0: - return GitFilemode.undreadable; - case 16384: - return GitFilemode.tree; - case 33188: - return GitFilemode.blob; - case 33261: - return GitFilemode.blobExecutable; - case 40960: - return GitFilemode.link; - case 57344: - return GitFilemode.commit; - default: - return GitFilemode.undreadable; - } - } + /// Returns the UNIX file attributes of a index entry. + GitFilemode get mode => intToGitFilemode(_indexEntryPointer.ref.mode); + /// Sets the UNIX file attributes of a index entry. set mode(GitFilemode mode) { - switch (mode) { - case GitFilemode.undreadable: - _indexEntryPointer.ref.mode = 0; - break; - case GitFilemode.tree: - _indexEntryPointer.ref.mode = 16384; - break; - case GitFilemode.blob: - _indexEntryPointer.ref.mode = 33188; - break; - case GitFilemode.blobExecutable: - _indexEntryPointer.ref.mode = 33261; - break; - case GitFilemode.link: - _indexEntryPointer.ref.mode = 40960; - break; - case GitFilemode.commit: - _indexEntryPointer.ref.mode = 57344; - break; - } + _indexEntryPointer.ref.mode = gitFilemodeToInt(mode); } String _oidToHex(git_oid oid) { diff --git a/lib/src/tree.dart b/lib/src/tree.dart index 211f3ae..a71df7f 100644 --- a/lib/src/tree.dart +++ b/lib/src/tree.dart @@ -1,9 +1,9 @@ import 'dart:ffi'; -import 'package:libgit2dart/src/repository.dart'; - import 'bindings/libgit2_bindings.dart'; import 'bindings/tree.dart' as bindings; +import 'repository.dart'; import 'oid.dart'; +import 'enums.dart'; import 'util.dart'; class Tree { @@ -17,8 +17,8 @@ class Tree { /// [Repository] and [Oid] objects. /// /// Should be freed with `free()` to release allocated memory. - Tree.lookup(Repository repo, Oid id) { - _treePointer = bindings.lookup(repo.pointer, id.pointer); + Tree.lookup(Repository repo, Oid oid) { + _treePointer = bindings.lookup(repo.pointer, oid.pointer); } late final Pointer _treePointer; @@ -26,8 +26,89 @@ class Tree { /// Pointer to memory address for allocated tree object. Pointer get pointer => _treePointer; - /// Releases memory allocated for tree object. - void free() { - bindings.free(_treePointer); + /// Returns a list with tree entries of a tree. + List get entries { + final entryCount = bindings.entryCount(_treePointer); + var result = []; + for (var i = 0; i < entryCount; i++) { + result.add(TreeEntry(bindings.getByIndex(_treePointer, i))); + } + + return result; } + + /// Looksup a tree entry in the tree. + /// + /// If integer [value] is provided, lookup is done by entry position in the tree. + /// + /// If string [value] is provided, lookup is done by entry filename. + /// + /// If provided string [value] is a path to file, lookup is done by path. In that case + /// returned object should be freed explicitly. + TreeEntry operator [](Object value) { + if (value is int) { + return TreeEntry(bindings.getByIndex(_treePointer, value)); + } else if (value is String && value.contains('/')) { + return TreeEntry(bindings.getByPath(_treePointer, value)); + } else if (value is String) { + return TreeEntry(bindings.getByName(_treePointer, value)); + } else { + throw ArgumentError.value( + '$value should be either index position, filename or path'); + } + } + + /// Releases memory allocated for tree object. + void free() => bindings.free(_treePointer); +} + +class TreeEntry { + /// Initializes a new instance of [TreeEntry] class. + TreeEntry(this._treeEntryPointer); + + /// Pointer to memory address for allocated tree entry object. + final Pointer _treeEntryPointer; + + /// Returns the Oid of the object pointed by the entry. + Oid get id => Oid(bindings.entryId(_treeEntryPointer)); + + /// Returns the filename of a tree entry. + String get name => bindings.entryName(_treeEntryPointer); + + /// Returns the UNIX file attributes of a tree entry. + GitFilemode get filemode { + return intToGitFilemode(bindings.entryFilemode(_treeEntryPointer)); + } + + @override + bool operator ==(other) { + return (other is TreeEntry) && + (bindings.compare(_treeEntryPointer, other._treeEntryPointer) == 0); + } + + bool operator <(other) { + return (other is TreeEntry) && + (bindings.compare(_treeEntryPointer, other._treeEntryPointer) == -1); + } + + bool operator <=(other) { + return (other is TreeEntry) && + (bindings.compare(_treeEntryPointer, other._treeEntryPointer) == -1); + } + + bool operator >(other) { + return (other is TreeEntry) && + (bindings.compare(_treeEntryPointer, other._treeEntryPointer) == 1); + } + + bool operator >=(other) { + return (other is TreeEntry) && + (bindings.compare(_treeEntryPointer, other._treeEntryPointer) == 1); + } + + @override + int get hashCode => _treeEntryPointer.address.hashCode; + + /// Releases memory allocated for tree entry object. + void free() => bindings.entryFree(_treeEntryPointer); } diff --git a/lib/src/util.dart b/lib/src/util.dart index 8183020..520af43 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -1,6 +1,7 @@ -import 'dart:ffi'; import 'dart:io'; +import 'dart:ffi'; import 'bindings/libgit2_bindings.dart'; +import 'enums.dart'; DynamicLibrary loadLibrary() { if (Platform.isLinux || Platform.isAndroid || Platform.isFuchsia) { @@ -25,3 +26,39 @@ bool isValidShaHex(String str) { return hexRegExp.hasMatch(str) && (GIT_OID_MINPREFIXLEN <= str.length && GIT_OID_HEXSZ >= str.length); } + +GitFilemode intToGitFilemode(int i) { + switch (i) { + case 0: + return GitFilemode.undreadable; + case 16384: + return GitFilemode.tree; + case 33188: + return GitFilemode.blob; + case 33261: + return GitFilemode.blobExecutable; + case 40960: + return GitFilemode.link; + case 57344: + return GitFilemode.commit; + default: + return GitFilemode.undreadable; + } +} + +int gitFilemodeToInt(GitFilemode filemode) { + switch (filemode) { + case GitFilemode.undreadable: + return 0; + case GitFilemode.tree: + return 16384; + case GitFilemode.blob: + return 33188; + case GitFilemode.blobExecutable: + return 33261; + case GitFilemode.link: + return 40960; + case GitFilemode.commit: + return 57344; + } +} diff --git a/test/assets/testrepo/.gitdir/COMMIT_EDITMSG b/test/assets/testrepo/.gitdir/COMMIT_EDITMSG index 2cdbe7d..5fc5f18 100644 --- a/test/assets/testrepo/.gitdir/COMMIT_EDITMSG +++ b/test/assets/testrepo/.gitdir/COMMIT_EDITMSG @@ -1 +1 @@ -add another feature file +add subdirectory file diff --git a/test/assets/testrepo/.gitdir/index b/test/assets/testrepo/.gitdir/index index 2c38a7560a05d7857013b5c2700a237ae7d798b4..42726c1a80309e389b6b6266c403903492f685c2 100644 GIT binary patch delta 222 zcmZ3RZ`#*0aNCpqM(tT;b}8Di#tzv8%G{cNoHGs9fK~ zBdXd0DVat3KpdZznUktlQc=Rd05bwcLzP!eECCyZ&6>)IN6fuU8B7!mxmK*Ry*e*X zg6Y`zKGw&K`>)o_H}(gb&tRxvz?HT)(Y?UGLM*n-=Y>hnsW**|v0I}4$!y4z;^zNy PvaMq4u8#NjoNS^1TB=Gf delta 148 zcmbQrypl=9#WTp6fq{Vuh?x`K=!=?Wu^)xeKrwZO`VRkzDi#t6Z}c|ERy@4{qoHzw z6OVx8boGr5r~1HXh+KQYWD!PlJxK;*1p}_~X{NKLy)M$UJmkIpw$#7*?4Q1A98o=Z WJLkRh-7=kZDoYF!YWDB`&;bB};x&-~ diff --git a/test/assets/testrepo/.gitdir/logs/HEAD b/test/assets/testrepo/.gitdir/logs/HEAD index 2087c0c..fee0ae9 100644 --- a/test/assets/testrepo/.gitdir/logs/HEAD +++ b/test/assets/testrepo/.gitdir/logs/HEAD @@ -8,3 +8,4 @@ c68ff54aabf660fcdd9a2838d401583fe31249e3 78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e 78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8 fc38877b2552ab554752d9a77e1f48f738cca79b Aleksey Kulikov 1626091245 +0300 checkout: moving from master to feature fc38877b2552ab554752d9a77e1f48f738cca79b 5aecfa0fb97eadaac050ccb99f03c3fb65460ad4 Aleksey Kulikov 1626091274 +0300 commit: add another feature file 5aecfa0fb97eadaac050ccb99f03c3fb65460ad4 78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8 Aleksey Kulikov 1626091285 +0300 checkout: moving from feature to master +78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8 821ed6e80627b8769d170a293862f9fc60825226 Aleksey Kulikov 1630568461 +0300 commit: add subdirectory file diff --git a/test/assets/testrepo/.gitdir/logs/refs/heads/master b/test/assets/testrepo/.gitdir/logs/refs/heads/master index d2dba8f..2debc2c 100644 --- a/test/assets/testrepo/.gitdir/logs/refs/heads/master +++ b/test/assets/testrepo/.gitdir/logs/refs/heads/master @@ -1,3 +1,4 @@ 0000000000000000000000000000000000000000 f17d0d48eae3aa08cecf29128a35e310c97b3521 Aleksey Kulikov 1626090830 +0300 commit (initial): init f17d0d48eae3aa08cecf29128a35e310c97b3521 c68ff54aabf660fcdd9a2838d401583fe31249e3 Aleksey Kulikov 1626091171 +0300 commit: add .gitignore c68ff54aabf660fcdd9a2838d401583fe31249e3 78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8 Aleksey Kulikov 1626091184 +0300 merge feature: Merge made by the 'recursive' strategy. +78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8 821ed6e80627b8769d170a293862f9fc60825226 Aleksey Kulikov 1630568461 +0300 commit: add subdirectory file diff --git a/test/assets/testrepo/.gitdir/objects/66/bd6147704f78165d764ce8348ccaec81415db4 b/test/assets/testrepo/.gitdir/objects/66/bd6147704f78165d764ce8348ccaec81415db4 new file mode 100644 index 0000000000000000000000000000000000000000..2da87bdba5a6c81c0e3786e51e9e824ae2ac8c24 GIT binary patch literal 57 zcmb7F<>w>FfcPQQP4}zEXhpI%P&f05H1h(+qdjzmrhAz^PZ?_ zty&+o4w)DLfkH}V5kuPEME3&!3bEKSpBE-Qr`|L=#%_U{k(QcRQd*Q6pO%@E$}p$m zkZfAR^8c@o=T{x;S#waf-(wF<9Z3G!+()xFUA*S(-L1K()); + }); + + test('returns number of entries', () { + expect(tree.entries.length, 4); + }); + + test('returns sha of tree entry', () { + expect(tree.entries.first.id.sha, fileSHA); + }); + + test('returns name of tree entry', () { + expect(tree.entries[0].name, '.gitignore'); + }); + + test('returns filemode of tree entry', () { + expect(tree.entries[0].filemode, GitFilemode.blob); + }); + + test('returns tree entry with provided index position', () { + expect(tree[0].id.sha, fileSHA); + }); + + test('throws when provided index position is outside of valid range', () { + expect(() => tree[10], throwsA(isA())); + expect(() => tree[-10], throwsA(isA())); + }); + + test('returns tree entry with provided filename', () { + expect(tree['.gitignore'].id.sha, fileSHA); + }); + + test('throws when nothing found for provided filename', () { + expect(() => tree['invalid'], throwsA(isA())); + }); + + test('returns tree entry with provided path to file', () { + final entry = tree['dir/dir_file.txt']; + expect(entry.id.sha, 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'); + entry.free(); + }); + + test('throws when nothing found for provided path', () { + expect(() => tree['invalid/path'], throwsA(isA())); + }); + }); +}