diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a633b52..f646cb2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -58,27 +58,27 @@ jobs: - name: Run tests on desktop platforms if: ${{ matrix.platform != 'android' && matrix.platform != 'ios' }} run: | - flutter test + flutter test -j 1 --reporter expanded - name: Run tests on iOS if: ${{ matrix.platform == 'ios' }} shell: bash run: | - IOS_DEVICE_NAME="iPhone 16" - if ! xcrun simctl list devices available | grep -q "$IOS_DEVICE_NAME"; then - IOS_DEVICE_NAME="$(xcrun simctl list devices available | awk -F '[()]' '/iPhone/ && /Shutdown/ {print $1; exit}' | xargs)" + IOS_DEVICE_ID="$(xcrun simctl list devices available | awk -F '[()]' '/iPhone 16/ && /Shutdown/ {print $2; exit}')" + if [ -z "$IOS_DEVICE_ID" ]; then + IOS_DEVICE_ID="$(xcrun simctl list devices available | awk -F '[()]' '/iPhone/ && /Shutdown/ {print $2; exit}')" fi - if [ -z "$IOS_DEVICE_NAME" ]; then + if [ -z "$IOS_DEVICE_ID" ]; then echo "No available iOS simulator found" xcrun simctl list devices available exit 1 fi - xcrun simctl boot "$IOS_DEVICE_NAME" || true - xcrun simctl bootstatus "$IOS_DEVICE_NAME" -b + xcrun simctl boot "$IOS_DEVICE_ID" || true + xcrun simctl bootstatus "$IOS_DEVICE_ID" -b flutter devices - flutter test -d "$IOS_DEVICE_NAME" + flutter test -d "$IOS_DEVICE_ID" --reporter expanded - name: Setup Flutter path for Android if: ${{ matrix.platform == 'android' }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6843f..44a95e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,18 @@ # Changelog +## [0.5.2] - 2026-06-29 +### Features +* Expand libgit2 binding coverage across repository, remote, status, tree, + tag, signature, submodule, config, diff, index, ODB, reference, and related + APIs. +* Add public wrappers for extended repository opening, basic repository + initialization, annotated HEAD detaching, remote instance URL overrides, + remote autotag configuration, tree walking, tree updates, status callbacks, + and annotation-only tag creation. + +### Testing +* Add focused tests for the newly exposed binding wrappers. +* Include binding files in coverage by removing file-level coverage ignores. + ## [0.5.1] - 2026-06-29 ### Fixed * Require `git2dart_binaries` `>=1.11.2 <1.12.0` to pick up the latest diff --git a/lib/src/bindings/blame.dart b/lib/src/bindings/blame.dart index ee95d6f..c2a61c3 100644 --- a/lib/src/bindings/blame.dart +++ b/lib/src/bindings/blame.dart @@ -145,6 +145,11 @@ Pointer fileFromBuffer({ /// Gets the number of hunks that exist in the blame structure. int hunkCount(Pointer blame) { + return libgit2.git_blame_get_hunk_count(blame); +} + +/// Gets the number of hunks using libgit2's legacy blame API name. +int hunkCountLegacy(Pointer blame) { return libgit2.git_blame_hunkcount(blame); } @@ -156,6 +161,21 @@ int hunkCount(Pointer blame) { Pointer getHunkByline({ required Pointer blamePointer, required int lineno, +}) { + final result = libgit2.git_blame_get_hunk_byline(blamePointer, lineno); + + if (result == nullptr) { + throw Git2DartError('Line number out of bounds'); + } else { + return result; + } +} + +/// Get the hunk that contains the given line number using libgit2's legacy API +/// name. +Pointer getHunkBylineLegacy({ + required Pointer blamePointer, + required int lineno, }) { final result = libgit2.git_blame_hunk_byline(blamePointer, lineno); @@ -174,6 +194,20 @@ Pointer getHunkByline({ Pointer getHunkByIndex({ required Pointer blamePointer, required int index, +}) { + final result = libgit2.git_blame_get_hunk_byindex(blamePointer, index); + + if (result == nullptr) { + throw Git2DartError('Index out of bounds'); + } else { + return result; + } +} + +/// Get the hunk at the given index using libgit2's legacy API name. +Pointer getHunkByIndexLegacy({ + required Pointer blamePointer, + required int index, }) { final result = libgit2.git_blame_hunk_byindex(blamePointer, index); diff --git a/lib/src/bindings/blob.dart b/lib/src/bindings/blob.dart index f4a2595..af829eb 100644 --- a/lib/src/bindings/blob.dart +++ b/lib/src/bindings/blob.dart @@ -50,6 +50,10 @@ Pointer lookupPrefix({ /// Get the [Oid] of a blob. Pointer id(Pointer blob) => libgit2.git_blob_id(blob); +/// Get the repository that contains the blob. +Pointer owner(Pointer blob) => + libgit2.git_blob_owner(blob); + /// Determine if the blob content is most certainly binary or not. /// /// The heuristic used to guess if a file is binary is taken from core git: diff --git a/lib/src/bindings/branch.dart b/lib/src/bindings/branch.dart index 950e765..94fb860 100644 --- a/lib/src/bindings/branch.dart +++ b/lib/src/bindings/branch.dart @@ -211,6 +211,17 @@ String getName(Pointer ref) { }); } +/// Check whether a branch name is valid. +bool nameIsValid(String name) { + return using((arena) { + final valid = arena(); + final nameC = name.toChar(arena); + final error = libgit2.git_branch_name_is_valid(valid, nameC); + checkErrorAndThrow(error); + return valid.value == 1; + }); +} + /// Find the remote name of a remote-tracking branch. /// /// This will return the name of the remote whose fetch refspec is matching the diff --git a/lib/src/bindings/commit.dart b/lib/src/bindings/commit.dart index a8fec3e..30706d6 100644 --- a/lib/src/bindings/commit.dart +++ b/lib/src/bindings/commit.dart @@ -328,6 +328,11 @@ String message(Pointer commit) { return libgit2.git_commit_message(commit).toDartString(); } +/// Get the full raw message of a commit. +String messageRaw(Pointer commit) { + return libgit2.git_commit_message_raw(commit).toDartString(); +} + /// Get the short "summary" of the git commit message. /// /// The returned message is the summary of the commit, comprising the first @@ -372,6 +377,11 @@ String headerField({ }); } +/// Get the full raw header of a commit. +String rawHeader(Pointer commit) { + return libgit2.git_commit_raw_header(commit).toDartString(); +} + /// Get the id of a commit. Pointer id(Pointer commit) => libgit2.git_commit_id(commit); @@ -437,12 +447,52 @@ int timeOffset(Pointer commit) => Pointer committer(Pointer commit) => libgit2.git_commit_committer(commit); +/// Get the committer of a commit, using a mailmap to resolve names and emails. +/// +/// The returned signature must be freed. +Pointer committerWithMailmap({ + required Pointer commitPointer, + required Pointer mailmapPointer, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_commit_committer_with_mailmap( + out, + commitPointer, + mailmapPointer, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Get the author of a commit. /// /// The returned signature must be freed. Pointer author(Pointer commit) => libgit2.git_commit_author(commit); +/// Get the author of a commit, using a mailmap to resolve names and emails. +/// +/// The returned signature must be freed. +Pointer authorWithMailmap({ + required Pointer commitPointer, + required Pointer mailmapPointer, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_commit_author_with_mailmap( + out, + commitPointer, + mailmapPointer, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Get the id of the tree pointed to by a commit. Pointer treeOid(Pointer commit) => libgit2.git_commit_tree_id(commit); diff --git a/lib/src/bindings/config.dart b/lib/src/bindings/config.dart index ceb7293..904b7ac 100644 --- a/lib/src/bindings/config.dart +++ b/lib/src/bindings/config.dart @@ -5,6 +5,19 @@ import 'package:git2dart/src/extensions.dart'; import 'package:git2dart/src/helpers/error_helper.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; +/// Create a new empty config instance. The returned config must be freed with +/// [free]. +Pointer init() { + return using((arena) { + final out = arena>(); + final error = libgit2.git_config_new(out); + + checkErrorAndThrow(error); + + return out.value; + }); +} + /// Create a new config instance containing a single on-disk file. The returned /// config must be freed with [free]. Pointer open(String path) { @@ -304,6 +317,24 @@ String getString({ return getStringBuf(configPointer: configPointer, name: variable); } +/// Get a string config value using libgit2's direct pointer API. +/// +/// libgit2 rejects this call for live config objects; use [getString] for +/// normal public reads. +String getStringPointer({ + required Pointer configPointer, + required String variable, +}) { + return using((arena) { + final out = arena>(); + final nameC = variable.toChar(arena); + final error = libgit2.git_config_get_string(out, configPointer, nameC); + + checkErrorAndThrow(error); + return out.value.toDartString(); + }); +} + /// Set the value of a boolean config variable in the config file with the /// highest level (usually the local one). void setBool({ @@ -335,6 +366,21 @@ void setInt({ }); } +/// Set the value of a 32-bit integer config variable in the config file with +/// the highest level (usually the local one). +void setInt32({ + required Pointer configPointer, + required String variable, + required int value, +}) { + return using((arena) { + final nameC = variable.toChar(arena); + final error = libgit2.git_config_set_int32(configPointer, nameC, value); + + checkErrorAndThrow(error); + }); +} + /// Set the value of a string config variable in the config file with the /// highest level (usually the local one). void setString({ @@ -364,6 +410,117 @@ Pointer iterator(Pointer cfg) { }); } +/// Iterate over all config variables whose name matches [regexp]. +/// +/// The returned iterator must be freed with [freeIterator]. +Pointer globIterator({ + required Pointer configPointer, + required String regexp, +}) { + return using((arena) { + final out = arena>(); + final regexpC = regexp.toChar(arena); + final error = libgit2.git_config_iterator_glob_new( + out, + configPointer, + regexpC, + ); + + checkErrorAndThrow(error); + + return out.value; + }); +} + +Map _entryToMap(Pointer entry) { + return { + 'name': entry.ref.name.toDartString(), + 'value': entry.ref.value.toDartString(), + 'includeDepth': entry.ref.include_depth, + 'level': entry.ref.level.value, + }; +} + +List> _entriesFromIterator( + Pointer iterator, +) { + return using((arena) { + final entry = arena>(); + final result = >[]; + + while (true) { + final error = libgit2.git_config_next(entry, iterator); + if (error == git_error_code.GIT_ITEROVER.value) { + break; + } + + checkErrorAndThrow(error); + result.add(_entryToMap(entry.value)); + } + + return result; + }); +} + +/// Return all config entries from a glob iterator. +List> globEntries({ + required Pointer configPointer, + required String regexp, +}) { + final iter = globIterator(configPointer: configPointer, regexp: regexp); + try { + return _entriesFromIterator(iter); + } finally { + freeIterator(iter); + } +} + +var _foreachEntries = >[]; + +int _foreachCb(Pointer entry, Pointer payload) { + _foreachEntries.add(_entryToMap(entry)); + return 0; +} + +/// Return entries by using libgit2's config foreach callback API. +List> foreachEntries(Pointer configPointer) { + final cb = Pointer.fromFunction< + Int Function(Pointer, Pointer) + >(_foreachCb, 0); + + _foreachEntries.clear(); + final error = libgit2.git_config_foreach(configPointer, cb, nullptr); + checkErrorAndThrow(error); + final result = _foreachEntries.toList(growable: false); + _foreachEntries.clear(); + return result; +} + +/// Return matching entries by using libgit2's config foreach-match API. +List> foreachMatchEntries({ + required Pointer configPointer, + required String regexp, +}) { + return using((arena) { + final cb = Pointer.fromFunction< + Int Function(Pointer, Pointer) + >(_foreachCb, 0); + final regexpC = regexp.toChar(arena); + + _foreachEntries.clear(); + final error = libgit2.git_config_foreach_match( + configPointer, + regexpC, + cb, + nullptr, + ); + checkErrorAndThrow(error); + final result = _foreachEntries.toList(growable: false); + _foreachEntries.clear(); + return result; + }); +} + /// Delete a config variable from the config file with the highest level /// (usually the local one). /// @@ -411,19 +568,136 @@ List multivarValues({ var nextError = 0; final entries = []; - while (nextError == 0) { - nextError = libgit2.git_config_next(entry, iterator.value); - if (nextError != -31) { + try { + while (nextError == 0) { + nextError = libgit2.git_config_next(entry, iterator.value); + if (nextError == git_error_code.GIT_ITEROVER.value) { + break; + } + + checkErrorAndThrow(nextError); entries.add(entry.value.ref.value.toDartString()); - } else { - break; } + } finally { + freeIterator(iterator.value); } return entries; }); } +/// Return multivar values using libgit2's foreach callback API. +List multivarValuesForeach({ + required Pointer configPointer, + required String variable, + String? regexp, +}) { + return using((arena) { + final nameC = variable.toChar(arena); + final regexpC = regexp?.toChar(arena) ?? nullptr; + final cb = Pointer.fromFunction< + Int Function(Pointer, Pointer) + >(_foreachCb, 0); + + _foreachEntries.clear(); + final error = libgit2.git_config_get_multivar_foreach( + configPointer, + nameC, + regexpC, + cb, + nullptr, + ); + + if (error == git_error_code.GIT_ENOTFOUND.value) { + return []; + } + + checkErrorAndThrow(error); + final result = _foreachEntries + .map((entry) => entry['value']! as String) + .toList(growable: false); + _foreachEntries.clear(); + return result; + }); +} + +/// Mapping specification for config value mapping helpers. +class ConfigMapSpec { + /// Creates a config mapping specification. + const ConfigMapSpec({required this.type, required this.value, this.match}); + + /// Type of value to match. + final git_configmap_t type; + + /// String value to match when [type] is [git_configmap_t.GIT_CONFIGMAP_STRING]. + final String? match; + + /// Integer value returned for this mapping. + final int value; +} + +Pointer _configMaps(Arena arena, List maps) { + final mapsC = arena(maps.length); + for (var i = 0; i < maps.length; i++) { + mapsC[i].typeAsInt = maps[i].type.value; + mapsC[i].str_match = maps[i].match?.toChar(arena) ?? nullptr; + mapsC[i].map_value = maps[i].value; + } + return mapsC; +} + +/// Return a config value mapped to an integer constant. +int getMapped({ + required Pointer configPointer, + required String name, + required List maps, +}) { + return using((arena) { + final out = arena(); + final nameC = name.toChar(arena); + final mapsC = _configMaps(arena, maps); + final error = libgit2.git_config_get_mapped( + out, + configPointer, + nameC, + mapsC, + maps.length, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + +/// Return [value] mapped to an integer constant. +int lookupMapValue({required List maps, required String value}) { + return using((arena) { + final out = arena(); + final mapsC = _configMaps(arena, maps); + final valueC = value.toChar(arena); + final error = libgit2.git_config_lookup_map_value( + out, + mapsC, + maps.length, + valueC, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + +/// Lock and immediately release the highest-priority config backend. +void lock(Pointer configPointer) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_config_lock(out, configPointer); + + checkErrorAndThrow(error); + libgit2.git_transaction_free(out.value); + }); +} + /// Free the configuration and its associated memory and files. void free(Pointer cfg) => libgit2.git_config_free(cfg); diff --git a/lib/src/bindings/diff.dart b/lib/src/bindings/diff.dart index c949f9b..c3f043b 100644 --- a/lib/src/bindings/diff.dart +++ b/lib/src/bindings/diff.dart @@ -423,6 +423,309 @@ String addToBuf(Pointer diff) { }); } +var _printedDiffLines = []; +var _foreachFilePaths = []; +var _foreachLineOrigins = []; + +String _lineContent(Pointer line) { + if (line == nullptr || line.ref.content == nullptr) { + return ''; + } + + final content = line.ref.content.toDartString(length: line.ref.content_len); + final origin = line.ref.origin; + if (origin == git_diff_line_t.GIT_DIFF_LINE_ADDITION.value || + origin == git_diff_line_t.GIT_DIFF_LINE_DELETION.value) { + return '${String.fromCharCode(origin)}$content'; + } + if (origin == git_diff_line_t.GIT_DIFF_LINE_CONTEXT.value) { + return ' $content'; + } + + return content; +} + +int _diffPrintCb( + Pointer delta, + Pointer hunk, + Pointer line, + Pointer payload, +) { + _printedDiffLines.add(_lineContent(line)); + return 0; +} + +int _diffFileCb( + Pointer delta, + double progress, + Pointer payload, +) { + final path = + delta.ref.new_file.path == nullptr + ? delta.ref.old_file.path.toDartString() + : delta.ref.new_file.path.toDartString(); + _foreachFilePaths.add(path); + return 0; +} + +int _diffHunkCb( + Pointer delta, + Pointer hunk, + Pointer payload, +) { + return 0; +} + +int _diffLineCb( + Pointer delta, + Pointer hunk, + Pointer line, + Pointer payload, +) { + _foreachLineOrigins.add(line.ref.origin); + _printedDiffLines.add(_lineContent(line)); + return 0; +} + +git_diff_line_cb _lineCallback() { + return Pointer.fromFunction< + Int Function( + Pointer, + Pointer, + Pointer, + Pointer, + ) + >(_diffPrintCb, 0); +} + +/// Iterate over a diff and collect formatted text through the print callback. +String print(Pointer diff) { + final cb = _lineCallback(); + + _printedDiffLines.clear(); + final error = libgit2.git_diff_print( + diff, + git_diff_format_t.GIT_DIFF_FORMAT_PATCH, + cb, + nullptr, + ); + + checkErrorAndThrow(error); + final result = _printedDiffLines.join(); + _printedDiffLines.clear(); + return result; +} + +/// Iterate over a diff and return file paths and line origin codes. +Map foreach(Pointer diff) { + final fileCb = Pointer.fromFunction< + Int Function(Pointer, Float, Pointer) + >(_diffFileCb, 0); + final hunkCb = Pointer.fromFunction< + Int Function(Pointer, Pointer, Pointer) + >(_diffHunkCb, 0); + final lineCb = Pointer.fromFunction< + Int Function( + Pointer, + Pointer, + Pointer, + Pointer, + ) + >(_diffLineCb, 0); + + _foreachFilePaths.clear(); + _foreachLineOrigins.clear(); + _printedDiffLines.clear(); + + final error = libgit2.git_diff_foreach( + diff, + fileCb, + nullptr, + hunkCb, + lineCb, + nullptr, + ); + + checkErrorAndThrow(error); + + final result = { + 'paths': _foreachFilePaths.toList(growable: false), + 'origins': _foreachLineOrigins.toList(growable: false), + 'text': _printedDiffLines.join(), + }; + + _foreachFilePaths.clear(); + _foreachLineOrigins.clear(); + _printedDiffLines.clear(); + return result; +} + +String _runDirectDiff({ + required Arena arena, + Pointer? oldBlobPointer, + String? oldAsPath, + Pointer? newBlobPointer, + String? newAsPath, + String? oldBuffer, + String? newBuffer, + required int flags, + required int contextLines, + required int interhunkLines, +}) { + final oldAsPathC = oldAsPath?.toChar(arena) ?? nullptr; + final newAsPathC = newAsPath?.toChar(arena) ?? nullptr; + final oldBufferC = oldBuffer?.toChar(arena) ?? nullptr; + final newBufferC = newBuffer?.toChar(arena) ?? nullptr; + final opts = _diffOptionsInit( + arena: arena, + flags: flags, + contextLines: contextLines, + interhunkLines: interhunkLines, + ); + final fileCb = Pointer.fromFunction< + Int Function(Pointer, Float, Pointer) + >(_diffFileCb, 0); + final hunkCb = Pointer.fromFunction< + Int Function(Pointer, Pointer, Pointer) + >(_diffHunkCb, 0); + final lineCb = Pointer.fromFunction< + Int Function( + Pointer, + Pointer, + Pointer, + Pointer, + ) + >(_diffLineCb, 0); + + _foreachFilePaths.clear(); + _foreachLineOrigins.clear(); + _printedDiffLines.clear(); + + int error; + if (oldBuffer != null || oldBlobPointer == null) { + error = libgit2.git_diff_buffers( + oldBufferC.cast(), + oldBuffer?.length ?? 0, + oldAsPathC, + newBufferC.cast(), + newBuffer?.length ?? 0, + newAsPathC, + opts, + fileCb, + nullptr, + hunkCb, + lineCb, + nullptr, + ); + } else if (newBlobPointer != null) { + error = libgit2.git_diff_blobs( + oldBlobPointer, + oldAsPathC, + newBlobPointer, + newAsPathC, + opts, + fileCb, + nullptr, + hunkCb, + lineCb, + nullptr, + ); + } else { + error = libgit2.git_diff_blob_to_buffer( + oldBlobPointer, + oldAsPathC, + newBufferC, + newBuffer?.length ?? 0, + newAsPathC, + opts, + fileCb, + nullptr, + hunkCb, + lineCb, + nullptr, + ); + } + + checkErrorAndThrow(error); + final result = _printedDiffLines.join(); + _foreachFilePaths.clear(); + _foreachLineOrigins.clear(); + _printedDiffLines.clear(); + return result; +} + +/// Directly diff two buffers and return collected line text. +String buffers({ + String? oldBuffer, + String? oldAsPath, + String? newBuffer, + String? newAsPath, + required int flags, + required int contextLines, + required int interhunkLines, +}) { + return using( + (arena) => _runDirectDiff( + arena: arena, + oldBuffer: oldBuffer, + oldAsPath: oldAsPath, + newBuffer: newBuffer, + newAsPath: newAsPath, + flags: flags, + contextLines: contextLines, + interhunkLines: interhunkLines, + ), + ); +} + +/// Directly diff two blobs and return collected line text. +String blobs({ + required Pointer? oldBlobPointer, + String? oldAsPath, + required Pointer? newBlobPointer, + String? newAsPath, + required int flags, + required int contextLines, + required int interhunkLines, +}) { + return using( + (arena) => _runDirectDiff( + arena: arena, + oldBlobPointer: oldBlobPointer, + oldAsPath: oldAsPath, + newBlobPointer: newBlobPointer, + newAsPath: newAsPath, + flags: flags, + contextLines: contextLines, + interhunkLines: interhunkLines, + ), + ); +} + +/// Directly diff a blob and a buffer and return collected line text. +String blobToBuffer({ + Pointer? oldBlobPointer, + String? oldAsPath, + String? buffer, + String? bufferAsPath, + required int flags, + required int contextLines, + required int interhunkLines, +}) { + return using( + (arena) => _runDirectDiff( + arena: arena, + oldBlobPointer: oldBlobPointer, + oldAsPath: oldAsPath, + newBuffer: buffer, + newAsPath: bufferAsPath, + flags: flags, + contextLines: contextLines, + interhunkLines: interhunkLines, + ), + ); +} + /// Counter for hunk number being applied. /// /// **IMPORTANT**: make sure to reset it to 0 before using since it's a global diff --git a/lib/src/bindings/index.dart b/lib/src/bindings/index.dart index cd5edb9..fbe329c 100644 --- a/lib/src/bindings/index.dart +++ b/lib/src/bindings/index.dart @@ -511,11 +511,187 @@ List>> conflictList( }); } +/// Return the conflict entries for [path]. +/// +/// Throws a [LibGit2Error] if error occurred. +Map> conflictGet({ + required Pointer indexPointer, + required String path, +}) { + return using((arena) { + final ancestorOut = arena>(); + final ourOut = arena>(); + final theirOut = arena>(); + final pathC = path.toChar(arena); + + final error = libgit2.git_index_conflict_get( + ancestorOut, + ourOut, + theirOut, + indexPointer, + pathC, + ); + + checkErrorAndThrow(error); + + return { + 'ancestor': ancestorOut.value, + 'our': ourOut.value, + 'their': theirOut.value, + }; + }); +} + /// Return whether the given index entry is a conflict (has a high stage entry). /// This is simply shorthand for [entryStage] > 0. bool entryIsConflict(Pointer entry) => libgit2.git_index_entry_is_conflict(entry) == 1 || false; +/// Get the count of filename conflict entries currently in the index. +int nameEntryCount(Pointer index) => + libgit2.git_index_name_entrycount(index); + +/// Get a filename conflict entry from the index by position. +/// +/// The returned entry is owned by the index and must not be freed. +Pointer nameGetByIndex({ + required Pointer indexPointer, + required int position, +}) { + final result = libgit2.git_index_name_get_byindex(indexPointer, position); + + if (result == nullptr) { + throw Git2DartError('Out of bounds'); + } else { + return result; + } +} + +/// Record the filenames involved in a rename conflict. +void nameAdd({ + required Pointer indexPointer, + required String ancestor, + required String ours, + required String theirs, +}) { + using((arena) { + final ancestorC = ancestor.toChar(arena); + final oursC = ours.toChar(arena); + final theirsC = theirs.toChar(arena); + final error = libgit2.git_index_name_add( + indexPointer, + ancestorC, + oursC, + theirsC, + ); + + checkErrorAndThrow(error); + }); +} + +/// Remove all filename conflict entries. +void nameClear(Pointer index) { + final error = libgit2.git_index_name_clear(index); + + checkErrorAndThrow(error); +} + +/// Get the count of resolve undo entries currently in the index. +int reucEntryCount(Pointer index) => + libgit2.git_index_reuc_entrycount(index); + +/// Find the position of a resolve undo entry by path. +int reucFind({required Pointer indexPointer, required String path}) { + return using((arena) { + final out = arena(); + final pathC = path.toChar(arena); + final error = libgit2.git_index_reuc_find(out, indexPointer, pathC); + + checkErrorAndThrow(error); + return out.value; + }); +} + +/// Get a resolve undo entry by path. +/// +/// The returned entry is owned by the index and must not be freed. +Pointer reucGetByPath({ + required Pointer indexPointer, + required String path, +}) { + return using((arena) { + final pathC = path.toChar(arena); + final result = libgit2.git_index_reuc_get_bypath(indexPointer, pathC); + + if (result == nullptr) { + throw ArgumentError.value('$path was not found'); + } else { + return result; + } + }); +} + +/// Get a resolve undo entry by position. +/// +/// The returned entry is owned by the index and must not be freed. +Pointer reucGetByIndex({ + required Pointer indexPointer, + required int position, +}) { + final result = libgit2.git_index_reuc_get_byindex(indexPointer, position); + + if (result == nullptr) { + throw Git2DartError('Out of bounds'); + } else { + return result; + } +} + +/// Add a resolve undo entry for a file. +void reucAdd({ + required Pointer indexPointer, + required String path, + required int ancestorMode, + required Pointer ancestorOid, + required int ourMode, + required Pointer ourOid, + required int theirMode, + required Pointer theirOid, +}) { + using((arena) { + final pathC = path.toChar(arena); + final error = libgit2.git_index_reuc_add( + indexPointer, + pathC, + ancestorMode, + ancestorOid, + ourMode, + ourOid, + theirMode, + theirOid, + ); + + checkErrorAndThrow(error); + }); +} + +/// Remove a resolve undo entry by position. +void reucRemove({ + required Pointer indexPointer, + required int position, +}) { + final error = libgit2.git_index_reuc_remove(indexPointer, position); + + checkErrorAndThrow(error); +} + +/// Remove all resolve undo entries. +void reucClear(Pointer index) { + final error = libgit2.git_index_reuc_clear(index); + + checkErrorAndThrow(error); +} + /// Add or update index entries to represent a conflict. Any staged entries /// that exist at the given paths will be removed. /// @@ -565,6 +741,60 @@ void conflictCleanup(Pointer index) { checkErrorAndThrow(error); } +/// Create a new index iterator. +/// +/// The returned iterator must be freed with [iteratorFree]. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer iteratorNew(Pointer index) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_index_iterator_new(out, index); + + checkErrorAndThrow(error); + + return out.value; + }); +} + +/// Return the next entry from an index iterator, or null when iteration ends. +Pointer? iteratorNext(Pointer iterator) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_index_iterator_next(out, iterator); + + if (error == git_error_code.GIT_ITEROVER.value) { + return null; + } + + checkErrorAndThrow(error); + return out.value; + }); +} + +/// Free an index iterator. +void iteratorFree(Pointer iterator) { + libgit2.git_index_iterator_free(iterator); +} + +/// Return an ordered snapshot of index entry paths using the native iterator. +List iteratorPaths(Pointer index) { + final iterator = iteratorNew(index); + final paths = []; + + try { + var entry = iteratorNext(iterator); + while (entry != null) { + paths.add(entry.ref.path.toDartString()); + entry = iteratorNext(iterator); + } + } finally { + iteratorFree(iterator); + } + + return paths; +} + /// Free an existing index object. void free(Pointer index) => libgit2.git_index_free(index); diff --git a/lib/src/bindings/merge.dart b/lib/src/bindings/merge.dart index ae99984..2067b91 100644 --- a/lib/src/bindings/merge.dart +++ b/lib/src/bindings/merge.dart @@ -57,6 +57,53 @@ Pointer mergeBasesMany({ }); } +/// Find all merge bases between two commits. +Pointer mergeBases({ + required Pointer repoPointer, + required Pointer aPointer, + required Pointer bPointer, +}) { + return using((arena) { + final out = calloc(); + final error = libgit2.git_merge_bases(out, repoPointer, aPointer, bPointer); + + checkErrorAndThrow(error); + + return out; + }); +} + +/// Find a merge base given a list of commits using libgit2's singular API. +Pointer mergeBaseMany({ + required Pointer repoPointer, + required List> commits, +}) { + return using((arena) { + final out = calloc(); + final commitsC = arena(commits.length); + for (var i = 0; i < commits.length; i++) { + commitsC[i] = commits[i].ref; + } + + final error = libgit2.git_merge_base_many( + out, + repoPointer, + commits.length, + commitsC, + ); + + checkErrorAndThrow(error); + + return out; + }); +} + +/// Dispose a libgit2-owned oid array and free its wrapper allocation. +void oidArrayDispose(Pointer array) { + libgit2.git_oidarray_dispose(array); + calloc.free(array); +} + /// Find a merge base in preparation for an octopus merge. /// /// Given a list of commits, find their merge base in preparation for an @@ -232,7 +279,9 @@ String mergeFile({ final error = libgit2.git_merge_file(out, ancestorC, oursC, theirsC, opts); checkErrorAndThrow(error); - return out.ref.ptr.toDartString(length: out.ref.len); + final result = out.ref.ptr.toDartString(length: out.ref.len); + libgit2.git_merge_file_result_free(out); + return result; }); } @@ -290,6 +339,7 @@ String mergeFileFromIndex({ if (out.ref.ptr != nullptr) { result = out.ref.ptr.toDartString(length: out.ref.len); } + libgit2.git_merge_file_result_free(out); return result; }); } diff --git a/lib/src/bindings/note.dart b/lib/src/bindings/note.dart index f5776b7..f411814 100644 --- a/lib/src/bindings/note.dart +++ b/lib/src/bindings/note.dart @@ -136,6 +136,14 @@ String message(Pointer note) { return libgit2.git_note_message(note).toDartString(); } +/// Get the note author. +Pointer author(Pointer note) => + libgit2.git_note_author(note); + +/// Get the note committer. +Pointer committer(Pointer note) => + libgit2.git_note_committer(note); + /// Free memory allocated for note object. void free(Pointer note) => libgit2.git_note_free(note); diff --git a/lib/src/bindings/object.dart b/lib/src/bindings/object.dart index 094b2be..6a480c4 100644 --- a/lib/src/bindings/object.dart +++ b/lib/src/bindings/object.dart @@ -8,6 +8,10 @@ import 'package:git2dart_binaries/git2dart_binaries.dart'; /// Get the object type of an object. git_object_t type(Pointer obj) => libgit2.git_object_type(obj); +/// Determine if [type] is a valid loose object type. +bool typeIsLoose(git_object_t type) => + libgit2.git_object_typeisloose(type) == 1; + /// Lookup a reference to one of the objects in a repository. The returned /// reference must be freed with [free]. /// diff --git a/lib/src/bindings/odb.dart b/lib/src/bindings/odb.dart index d95013c..e2bf4e6 100644 --- a/lib/src/bindings/odb.dart +++ b/lib/src/bindings/odb.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:ffi'; import 'dart:typed_data'; @@ -65,6 +66,24 @@ void addDiskAlternate({ }); } +/// Return the number of ODB backends. +int backendCount(Pointer odbPointer) { + return libgit2.git_odb_num_backends(odbPointer); +} + +/// Get an ODB backend by position. +Pointer getBackend({ + required Pointer odbPointer, + required int position, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_odb_get_backend(out, odbPointer, position); + checkErrorAndThrow(error); + return out.value; + }); +} + /// Determine if an object can be found in the object database by an /// abbreviated object ID. /// @@ -286,6 +305,85 @@ Pointer write({ }); } +/// Write raw data directly into the object database. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer writeDirect({ + required Pointer odbPointer, + required git_object_t type, + required String data, +}) { + return using((arena) { + final out = calloc(); + final bytes = utf8.encode(data); + final buffer = arena(bytes.length); + buffer.asTypedList(bytes.length).setAll(0, bytes); + final error = libgit2.git_odb_write( + out, + odbPointer, + buffer.cast(), + bytes.length, + type, + ); + checkErrorAndThrow(error); + return out; + }); +} + +/// Generate the object ID for a data buffer. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer hash({ + required String data, + required git_object_t type, + git_oid_t oidType = git_oid_t.GIT_OID_SHA1, +}) { + return using((arena) { + final out = calloc(); + final bytes = utf8.encode(data); + final buffer = arena(bytes.length); + buffer.asTypedList(bytes.length).setAll(0, bytes); + final error = libgit2.git_odb_hash( + out, + buffer.cast(), + bytes.length, + type, + oidType, + ); + checkErrorAndThrow(error); + return out; + }); +} + +/// Determine the object ID of a file on disk. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer hashFile({ + required String path, + required git_object_t type, + git_oid_t oidType = git_oid_t.GIT_OID_SHA1, +}) { + return using((arena) { + final out = calloc(); + final pathC = path.toChar(arena); + final error = libgit2.git_odb_hashfile(out, pathC, type, oidType); + checkErrorAndThrow(error); + return out; + }); +} + +/// Create a copy of an ODB object. +/// +/// The returned object must be freed with [freeObject]. +Pointer duplicateObject(Pointer source) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_odb_object_dup(out, source); + checkErrorAndThrow(error); + return out.value; + }); +} + /// Close an open object database. void free(Pointer db) => libgit2.git_odb_free(db); diff --git a/lib/src/bindings/oid.dart b/lib/src/bindings/oid.dart index 4843c2e..b8debc6 100644 --- a/lib/src/bindings/oid.dart +++ b/lib/src/bindings/oid.dart @@ -61,6 +61,22 @@ Pointer fromSHA( }); } +/// Parse a possibly shortened hex formatted object id into a git_oid. +Pointer fromStrP( + String hex, { + git_oid_t type = git_oid_t.GIT_OID_SHA1, +}) { + return using((arena) { + final out = calloc(); + final hexC = hex.toChar(arena); + + final error = libgit2.git_oid_fromstrp(out, hexC, type); + checkErrorAndThrow(error); + + return out; + }); +} + /// Copy a raw hash into a git_oid structure. /// /// This function is useful when working with raw SHA-1 or SHA-256 hash @@ -117,6 +133,17 @@ String toSHA(Pointer id) { }); } +/// Format [id] into exactly [length] hexadecimal characters. +String toStrN({required Pointer id, required int length}) { + return using((arena) { + final out = arena(length); + final error = libgit2.git_oid_nfmt(out, length, id); + + checkErrorAndThrow(error); + return out.toDartString(length: length); + }); +} + /// Format [id] into a null-terminated string buffer with [length] bytes. String toStr({required Pointer id, required int length}) { return using((arena) { diff --git a/lib/src/bindings/packbuilder.dart b/lib/src/bindings/packbuilder.dart index f59b15b..10fc3f5 100644 --- a/lib/src/bindings/packbuilder.dart +++ b/lib/src/bindings/packbuilder.dart @@ -251,6 +251,10 @@ class Packbuilder { return result == nullptr ? '' : result.toDartString(); } + /// Get the packfile hash. + static Pointer hash(Pointer pb) => + libgit2.git_packbuilder_hash(pb); + /// Set the number of threads to use for pack creation. /// /// By default, libgit2 won't spawn any threads at all. When set to 0, diff --git a/lib/src/bindings/patch.dart b/lib/src/bindings/patch.dart index f8356e0..df7b320 100644 --- a/lib/src/bindings/patch.dart +++ b/lib/src/bindings/patch.dart @@ -67,7 +67,7 @@ Pointer fromBlobs({ return using((arena) { final out = arena>(); final oldAsPathC = oldAsPath?.toChar(arena) ?? nullptr; - final newAsPathC = oldAsPath?.toChar(arena) ?? nullptr; + final newAsPathC = newAsPath?.toChar(arena) ?? nullptr; final opts = _diffOptionsInit( arena: arena, flags: flags, @@ -104,7 +104,7 @@ Pointer fromBlobAndBuffer({ final out = arena>(); final oldAsPathC = oldAsPath?.toChar(arena) ?? nullptr; final bufferC = buffer?.toCharAlloc() ?? nullptr; - final bufferAsPathC = oldAsPath?.toChar(arena) ?? nullptr; + final bufferAsPathC = bufferAsPath?.toChar(arena) ?? nullptr; final bufferLen = buffer?.length ?? 0; final opts = _diffOptionsInit( arena: arena, @@ -254,6 +254,49 @@ String text(Pointer patch) { }); } +var _printedPatchLines = []; + +int _patchPrintCb( + Pointer delta, + Pointer hunk, + Pointer line, + Pointer payload, +) { + if (line != nullptr && line.ref.content != nullptr) { + final content = line.ref.content.toDartString(length: line.ref.content_len); + final origin = line.ref.origin; + if (origin == git_diff_line_t.GIT_DIFF_LINE_ADDITION.value || + origin == git_diff_line_t.GIT_DIFF_LINE_DELETION.value) { + _printedPatchLines.add('${String.fromCharCode(origin)}$content'); + } else if (origin == git_diff_line_t.GIT_DIFF_LINE_CONTEXT.value) { + _printedPatchLines.add(' $content'); + } else { + _printedPatchLines.add(content); + } + } + return 0; +} + +/// Serialize the patch to text through libgit2's print callback. +String print(Pointer patch) { + final cb = Pointer.fromFunction< + Int Function( + Pointer, + Pointer, + Pointer, + Pointer, + ) + >(_patchPrintCb, 0); + + _printedPatchLines.clear(); + final error = libgit2.git_patch_print(patch, cb, nullptr); + + checkErrorAndThrow(error); + final result = _printedPatchLines.join(); + _printedPatchLines.clear(); + return result; +} + /// Get the content of a patch as bytes. Uint8List textBytes(Pointer patch) { return using((arena) { diff --git a/lib/src/bindings/reference.dart b/lib/src/bindings/reference.dart index 8fa00d8..2a3467f 100644 --- a/lib/src/bindings/reference.dart +++ b/lib/src/bindings/reference.dart @@ -160,6 +160,91 @@ List list(Pointer repo) { result.add(array.ref.strings[i].cast().toDartString()); } + libgit2.git_strarray_dispose(array); + return result; + }); +} + +var _foreachReferences = []; + +int _foreachCb(Pointer reference, Pointer payload) { + _foreachReferences.add(name(reference)); + free(reference); + return 0; +} + +/// List all references in a repository using libgit2's reference callback API. +/// +/// Throws a [LibGit2Error] if the references cannot be listed. +List listForeach(Pointer repo) { + const except = -1; + final cb = + Pointer.fromFunction, Pointer)>( + _foreachCb, + except, + ); + + _foreachReferences.clear(); + final error = libgit2.git_reference_foreach(repo, cb, nullptr); + checkErrorAndThrow(error); + + final result = _foreachReferences.toList(growable: false); + _foreachReferences.clear(); + return result; +} + +var _foreachNames = []; + +int _foreachNameCb(Pointer name, Pointer payload) { + _foreachNames.add(name.toDartString()); + return 0; +} + +/// List all reference names in a repository using libgit2's name callback API. +/// +/// Throws a [LibGit2Error] if the references cannot be listed. +List listForeachName(Pointer repo) { + const except = -1; + final cb = Pointer.fromFunction, Pointer)>( + _foreachNameCb, + except, + ); + + _foreachNames.clear(); + final error = libgit2.git_reference_foreach_name(repo, cb, nullptr); + checkErrorAndThrow(error); + + final result = _foreachNames.toList(growable: false); + _foreachNames.clear(); + return result; +} + +/// List reference names matching [glob] using libgit2's glob callback API. +/// +/// Throws a [LibGit2Error] if the references cannot be listed. +List listForeachGlob({ + required Pointer repoPointer, + required String glob, +}) { + return using((arena) { + const except = -1; + final globC = glob.toChar(arena); + final cb = Pointer.fromFunction, Pointer)>( + _foreachNameCb, + except, + ); + + _foreachNames.clear(); + final error = libgit2.git_reference_foreach_glob( + repoPointer, + globC, + cb, + nullptr, + ); + checkErrorAndThrow(error); + + final result = _foreachNames.toList(growable: false); + _foreachNames.clear(); return result; }); } @@ -272,6 +357,38 @@ Pointer createDirect({ }); } +/// Conditionally create a new direct reference. +/// +/// If [currentIdPointer] does not match the reference value at update time, +/// libgit2 returns an error. +Pointer createDirectMatching({ + required Pointer repoPointer, + required String name, + required Pointer oidPointer, + required bool force, + required Pointer currentIdPointer, + String? logMessage, +}) { + return using((arena) { + final out = arena>(); + final nameC = name.toChar(arena); + final forceC = force ? 1 : 0; + final logMessageC = logMessage?.toChar(arena) ?? nullptr; + final error = libgit2.git_reference_create_matching( + out, + repoPointer, + nameC, + oidPointer, + forceC, + currentIdPointer, + logMessageC, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Create a new symbolic reference. /// /// A symbolic reference points to another reference rather than directly to an @@ -322,6 +439,40 @@ Pointer createSymbolic({ }); } +/// Conditionally create a new symbolic reference. +/// +/// If [currentValue] does not match the reference value at update time, +/// libgit2 returns an error. +Pointer createSymbolicMatching({ + required Pointer repoPointer, + required String name, + required String target, + required bool force, + required String currentValue, + String? logMessage, +}) { + return using((arena) { + final out = arena>(); + final nameC = name.toChar(arena); + final targetC = target.toChar(arena); + final currentValueC = currentValue.toChar(arena); + final forceC = force ? 1 : 0; + final logMessageC = logMessage?.toChar(arena) ?? nullptr; + final error = libgit2.git_reference_symbolic_create_matching( + out, + repoPointer, + nameC, + targetC, + forceC, + currentValueC, + logMessageC, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Update a direct reference to point to a new OID. /// /// This function can only be used on direct references. For symbolic references, @@ -403,6 +554,18 @@ void delete(Pointer ref) { checkErrorAndThrow(error); } +/// Delete an existing reference by [name] without looking at its old value. +void remove({ + required Pointer repoPointer, + required String name, +}) { + using((arena) { + final nameC = name.toChar(arena); + final error = libgit2.git_reference_remove(repoPointer, nameC); + checkErrorAndThrow(error); + }); +} + /// Free a reference object. /// /// This will free the reference and all associated resources. The reference @@ -511,6 +674,115 @@ Pointer duplicate(Pointer source) { }); } +/// Compare two references. +int compare({ + required Pointer ref1Pointer, + required Pointer ref2Pointer, +}) { + return libgit2.git_reference_cmp(ref1Pointer, ref2Pointer); +} + +Pointer iteratorNew(Pointer repo) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_reference_iterator_new(out, repo); + checkErrorAndThrow(error); + return out.value; + }); +} + +Pointer iteratorGlobNew({ + required Pointer repoPointer, + required String glob, +}) { + return using((arena) { + final out = arena>(); + final globC = glob.toChar(arena); + final error = libgit2.git_reference_iterator_glob_new( + out, + repoPointer, + globC, + ); + checkErrorAndThrow(error); + return out.value; + }); +} + +Pointer? iteratorNext(Pointer iterator) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_reference_next(out, iterator); + if (error == git_error_code.GIT_ITEROVER.value) return null; + + checkErrorAndThrow(error); + return out.value; + }); +} + +String? iteratorNextName(Pointer iterator) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_reference_next_name(out, iterator); + if (error == git_error_code.GIT_ITEROVER.value) return null; + + checkErrorAndThrow(error); + return out.value.toDartString(); + }); +} + +void iteratorFree(Pointer iterator) { + libgit2.git_reference_iterator_free(iterator); +} + +/// List all references in a repository using libgit2's iterator API. +List listIterator(Pointer repo) { + final iterator = iteratorNew(repo); + final result = []; + try { + while (true) { + final ref = iteratorNext(iterator); + if (ref == null) return result; + result.add(name(ref)); + free(ref); + } + } finally { + iteratorFree(iterator); + } +} + +/// List all reference names in a repository using libgit2's iterator API. +List listIteratorNames(Pointer repo) { + final iterator = iteratorNew(repo); + final result = []; + try { + while (true) { + final refName = iteratorNextName(iterator); + if (refName == null) return result; + result.add(refName); + } + } finally { + iteratorFree(iterator); + } +} + +/// List reference names matching [glob] using libgit2's iterator API. +List listIteratorGlob({ + required Pointer repoPointer, + required String glob, +}) { + final iterator = iteratorGlobNew(repoPointer: repoPointer, glob: glob); + final result = []; + try { + while (true) { + final refName = iteratorNextName(iterator); + if (refName == null) return result; + result.add(refName); + } + } finally { + iteratorFree(iterator); + } +} + /// Validate a reference name according to Git rules. bool nameIsValid(String name) { return using((arena) { @@ -583,3 +855,9 @@ Pointer nameToId({ return result; }); } + +/// Return the peeled OID target of a direct reference to an annotated tag. +Pointer? targetPeel(Pointer ref) { + final result = libgit2.git_reference_target_peel(ref); + return result == nullptr ? null : result; +} diff --git a/lib/src/bindings/remote.dart b/lib/src/bindings/remote.dart index 8ee7c69..4132624 100644 --- a/lib/src/bindings/remote.dart +++ b/lib/src/bindings/remote.dart @@ -19,9 +19,11 @@ List list(Pointer repo) { checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } @@ -164,9 +166,11 @@ List rename({ checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } @@ -208,6 +212,43 @@ void setPushUrl({ }); } +/// Set the URL for this remote instance without changing configuration. +void setInstanceUrl({ + required Pointer remotePointer, + required String url, +}) { + using((arena) { + final urlC = url.toChar(arena); + final error = libgit2.git_remote_set_instance_url(remotePointer, urlC); + checkErrorAndThrow(error); + }); +} + +/// Set the push URL for this remote instance without changing configuration. +void setInstancePushUrl({ + required Pointer remotePointer, + required String url, +}) { + using((arena) { + final urlC = url.toChar(arena); + final error = libgit2.git_remote_set_instance_pushurl(remotePointer, urlC); + checkErrorAndThrow(error); + }); +} + +/// Set the remote's tag following setting in the configuration. +void setAutotag({ + required Pointer repoPointer, + required String remote, + required git_remote_autotag_option_t value, +}) { + using((arena) { + final remoteC = remote.toChar(arena); + final error = libgit2.git_remote_set_autotag(repoPointer, remoteC, value); + checkErrorAndThrow(error); + }); +} + /// Get the remote's name. /// /// Returns the name of the remote or an empty string if the remote is anonymous. @@ -230,6 +271,21 @@ String pushUrl(Pointer remote) { return result == nullptr ? '' : result.toDartString(); } +/// Get the remote's object ID type. +git_oid_t oidType(Pointer remote) { + return using((arena) { + final out = arena(); + final error = libgit2.git_remote_oid_type(out, remote); + checkErrorAndThrow(error); + return git_oid_t.fromValue(out.value); + }); +} + +/// Get the remote's automatic tag following option. +git_remote_autotag_option_t autotag(Pointer remote) { + return libgit2.git_remote_autotag(remote); +} + /// Get the number of refspecs for a remote. int refspecCount(Pointer remote) => libgit2.git_remote_refspec_count(remote); @@ -248,9 +304,11 @@ List fetchRefspecs(Pointer remote) { checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } @@ -262,9 +320,11 @@ List pushRefspecs(Pointer remote) { checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } diff --git a/lib/src/bindings/repository.dart b/lib/src/bindings/repository.dart index 07efbb4..95c489a 100644 --- a/lib/src/bindings/repository.dart +++ b/lib/src/bindings/repository.dart @@ -1,6 +1,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart' show calloc, using; +import 'package:git2dart/src/bindings/oid.dart' as oid_bindings; import 'package:git2dart/src/bindings/remote_callbacks.dart'; import 'package:git2dart/src/callbacks.dart'; import 'package:git2dart/src/extensions.dart'; @@ -28,6 +29,28 @@ Pointer open(String path) { }); } +/// Open an existing Git repository with extended search options. +Pointer openExt({ + String? path, + required int flags, + String? ceilingDirs, +}) { + return using((arena) { + final out = arena>(); + final pathC = path?.toChar(arena) ?? nullptr; + final ceilingDirsC = ceilingDirs?.toChar(arena) ?? nullptr; + final error = libgit2.git_repository_open_ext( + out, + pathC, + flags, + ceilingDirsC, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Open a bare Git repository at [path]. Pointer openBare(String path) { return using((arena) { @@ -40,6 +63,18 @@ Pointer openBare(String path) { }); } +/// Initialize a new Git repository using libgit2's basic initializer. +Pointer initBasic({required String path, required bool bare}) { + return using((arena) { + final out = arena>(); + final pathC = path.toChar(arena); + final error = libgit2.git_repository_init(out, pathC, bare ? 1 : 0); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Discover the path to a Git repository by walking up from [startPath]. /// /// This function searches for a Git repository starting from [startPath] and @@ -268,6 +303,25 @@ Pointer head(Pointer repo) { }); } +/// Get the referenced HEAD for a linked worktree by [name]. +Pointer headForWorktree({ + required Pointer repoPointer, + required String name, +}) { + return using((arena) { + final out = arena>(); + final nameC = name.toChar(arena); + final error = libgit2.git_repository_head_for_worktree( + out, + repoPointer, + nameC, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// A repository's HEAD is detached when it points directly to a commit instead /// of a branch. /// @@ -278,6 +332,23 @@ bool isHeadDetached(Pointer repo) { return error == 1; } +/// Return whether the named linked worktree's HEAD is detached. +bool isHeadDetachedForWorktree({ + required Pointer repoPointer, + required String name, +}) { + return using((arena) { + final nameC = name.toChar(arena); + final error = libgit2.git_repository_head_detached_for_worktree( + repoPointer, + nameC, + ); + + checkErrorAndThrow(error); + return error == 1; + }); +} + /// Get the path to the repository's working directory. /// /// For bare repositories, this returns an empty string. @@ -358,6 +429,33 @@ Pointer odb(Pointer repo) { }); } +/// Assign a custom config object to the repository. +void setConfig({ + required Pointer repoPointer, + required Pointer configPointer, +}) { + final error = libgit2.git_repository_set_config(repoPointer, configPointer); + checkErrorAndThrow(error); +} + +/// Assign a custom object database to the repository. +void setOdb({ + required Pointer repoPointer, + required Pointer odbPointer, +}) { + final error = libgit2.git_repository_set_odb(repoPointer, odbPointer); + checkErrorAndThrow(error); +} + +/// Assign a custom index to the repository. +void setIndex({ + required Pointer repoPointer, + required Pointer indexPointer, +}) { + final error = libgit2.git_repository_set_index(repoPointer, indexPointer); + checkErrorAndThrow(error); +} + /// Get the repository's reference database. /// /// Returns a pointer to the repository's reference database, which must be @@ -477,6 +575,18 @@ void setHeadDetached({ checkErrorAndThrow(error); } +/// Set the repository's HEAD to the commit described by an annotated commit. +void setHeadDetachedFromAnnotated({ + required Pointer repoPointer, + required Pointer commitPointer, +}) { + final error = libgit2.git_repository_set_head_detached_from_annotated( + repoPointer, + commitPointer, + ); + checkErrorAndThrow(error); +} + /// Detach HEAD from its current branch. void detachHead(Pointer repo) { final error = libgit2.git_repository_detach_head(repo); @@ -619,6 +729,74 @@ void stateCleanup(Pointer repo) { checkErrorAndThrow(error); } +var _fetchHeadEntries = >[]; +var _mergeHeadOids = []; + +int _fetchHeadCb( + Pointer refName, + Pointer remoteUrl, + Pointer oid, + int isMerge, + Pointer payload, +) { + _fetchHeadEntries.add({ + 'refName': refName == nullptr ? '' : refName.toDartString(), + 'remoteUrl': remoteUrl == nullptr ? '' : remoteUrl.toDartString(), + 'oid': oid_bindings.toSHA(oid), + 'isMerge': isMerge == 1, + }); + return 0; +} + +int _mergeHeadCb(Pointer oid, Pointer payload) { + _mergeHeadOids.add(oid_bindings.toSHA(oid)); + return 0; +} + +/// Return entries from FETCH_HEAD, or an empty list if FETCH_HEAD is absent. +List> fetchHeadEntries(Pointer repo) { + final cb = Pointer.fromFunction< + Int Function( + Pointer, + Pointer, + Pointer, + UnsignedInt, + Pointer, + ) + >(_fetchHeadCb, 0); + + _fetchHeadEntries.clear(); + final error = libgit2.git_repository_fetchhead_foreach(repo, cb, nullptr); + if (error == git_error_code.GIT_ENOTFOUND.value) { + return >[]; + } + + checkErrorAndThrow(error); + final result = _fetchHeadEntries.toList(growable: false); + _fetchHeadEntries.clear(); + return result; +} + +/// Return OIDs from MERGE_HEAD, or an empty list if MERGE_HEAD is absent. +List mergeHeadOids(Pointer repo) { + final cb = + Pointer.fromFunction, Pointer)>( + _mergeHeadCb, + 0, + ); + + _mergeHeadOids.clear(); + final error = libgit2.git_repository_mergehead_foreach(repo, cb, nullptr); + if (error == git_error_code.GIT_ENOTFOUND.value) { + return []; + } + + checkErrorAndThrow(error); + final result = _mergeHeadOids.toList(growable: false); + _mergeHeadOids.clear(); + return result; +} + /// Get a snapshot of the repository's configuration. /// /// The contents of this snapshot will not change even if the underlying diff --git a/lib/src/bindings/revwalk.dart b/lib/src/bindings/revwalk.dart index b1e0769..27ae507 100644 --- a/lib/src/bindings/revwalk.dart +++ b/lib/src/bindings/revwalk.dart @@ -2,10 +2,17 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart' show using; import 'package:git2dart/src/bindings/commit.dart' as commit_bindings; +import 'package:git2dart/src/bindings/oid.dart' as oid_bindings; import 'package:git2dart/src/extensions.dart'; import 'package:git2dart/src/helpers/error_helper.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; +bool Function(String oid)? _hidePredicate; + +int _hideCb(Pointer commitId, Pointer payload) { + return _hidePredicate?.call(oid_bindings.toSHA(commitId)) ?? false ? 1 : 0; +} + /// Creates a new revision walker to iterate through a repository. /// /// The revision walker uses a custom memory pool and an internal commit cache, @@ -227,6 +234,24 @@ void hideRef({ }); } +/// Adds a callback to hide matching commits from the revision walk. +void addHideCallback({ + required Pointer walkerPointer, + required bool Function(String oid) predicate, +}) { + _hidePredicate = predicate; + final error = libgit2.git_revwalk_add_hide_cb( + walkerPointer, + Pointer.fromFunction, Pointer)>( + _hideCb, + 0, + ), + nullptr, + ); + + checkErrorAndThrow(error); +} + /// Resets the revision walker for reuse. /// /// This will clear all the pushed and hidden commits, and leave the walker in diff --git a/lib/src/bindings/signature.dart b/lib/src/bindings/signature.dart index 0812a47..757a53b 100644 --- a/lib/src/bindings/signature.dart +++ b/lib/src/bindings/signature.dart @@ -69,6 +69,28 @@ Pointer defaultSignature(Pointer repo) { }); } +/// Get default author and committer signatures for the repository. +/// +/// This looks at environment variables first, then repository configuration. +/// The returned signatures must be freed with [free]. +List> defaultSignaturesFromEnv( + Pointer repo, +) { + return using((arena) { + final author = arena>(); + final committer = arena>(); + final error = libgit2.git_signature_default_from_env( + author, + committer, + repo, + ); + + checkErrorAndThrow(error); + + return [author.value, committer.value]; + }); +} + /// Create a copy of an existing signature. The returned signature must be /// freed with [free]. Pointer duplicate(Pointer sig) { diff --git a/lib/src/bindings/status.dart b/lib/src/bindings/status.dart index 2f4f6da..f78991c 100644 --- a/lib/src/bindings/status.dart +++ b/lib/src/bindings/status.dart @@ -1,10 +1,17 @@ import 'dart:ffi'; -import 'package:ffi/ffi.dart' show using; +import 'package:ffi/ffi.dart' show Utf8, Utf8Pointer, using; import 'package:git2dart/src/extensions.dart'; import 'package:git2dart/src/helpers/error_helper.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; +var _statusEntries = {}; + +int _statusCb(Pointer path, int statusFlags, Pointer payload) { + _statusEntries[path.cast().toDartString()] = statusFlags; + return 0; +} + /// Gather file status information and populate the git_status_list. /// /// This function will scan the working directory and index for changes and @@ -71,6 +78,44 @@ int file({required Pointer repoPointer, required String path}) { }); } +/// Gather file status information with the status callback API. +Map foreach(Pointer repoPointer) { + _statusEntries = {}; + + final error = libgit2.git_status_foreach( + repoPointer, + Pointer.fromFunction< + Int Function(Pointer, UnsignedInt, Pointer) + >(_statusCb, 0), + nullptr, + ); + + checkErrorAndThrow(error); + return Map.unmodifiable(_statusEntries); +} + +/// Gather file status information with the extended status callback API. +Map foreachExt(Pointer repoPointer) { + _statusEntries = {}; + + return using((arena) { + final opts = arena(); + libgit2.git_status_options_init(opts, GIT_STATUS_OPTIONS_VERSION); + + final error = libgit2.git_status_foreach_ext( + repoPointer, + opts, + Pointer.fromFunction< + Int Function(Pointer, UnsignedInt, Pointer) + >(_statusCb, 0), + nullptr, + ); + + checkErrorAndThrow(error); + return Map.unmodifiable(_statusEntries); + }); +} + /// Check if a file should be ignored according to git rules. bool shouldIgnore({ required Pointer repoPointer, diff --git a/lib/src/bindings/submodule.dart b/lib/src/bindings/submodule.dart index 4c4ea0b..0a0a38a 100644 --- a/lib/src/bindings/submodule.dart +++ b/lib/src/bindings/submodule.dart @@ -321,6 +321,31 @@ void setIgnoreRule({ git_submodule_update_t updateRule(Pointer submodule) => libgit2.git_submodule_update_strategy(submodule); +/// Get the fetch recurse submodules rule for a submodule. +git_submodule_recurse_t fetchRecurseSubmodules( + Pointer submodule, +) => libgit2.git_submodule_fetch_recurse_submodules(submodule); + +/// Set the fetch recurse submodules rule for a submodule. +/// +/// This setting won't affect any existing instances. +void setFetchRecurseSubmodules({ + required Pointer repoPointer, + required String name, + required git_submodule_recurse_t fetchRecurseSubmodules, +}) { + using((arena) { + final nameC = name.toChar(arena); + final error = libgit2.git_submodule_set_fetch_recurse_submodules( + repoPointer, + nameC, + fetchRecurseSubmodules, + ); + + checkErrorAndThrow(error); + }); +} + /// Set the update rule for a submodule. /// /// This setting won't affect any existing instances. @@ -411,6 +436,16 @@ Pointer? workdirId(Pointer submodule) => Pointer owner(Pointer submodule) => libgit2.git_submodule_owner(submodule); +/// Get the locations of submodule information. +int location(Pointer submodule) { + return using((arena) { + final out = arena(); + final error = libgit2.git_submodule_location(out, submodule); + checkErrorAndThrow(error); + return out.value; + }); +} + /// Sync a submodule. /// /// This copies the information about the submodules URL into the checked out diff --git a/lib/src/bindings/tag.dart b/lib/src/bindings/tag.dart index 3fef215..de51938 100644 --- a/lib/src/bindings/tag.dart +++ b/lib/src/bindings/tag.dart @@ -14,9 +14,11 @@ List list(Pointer repo) { final error = libgit2.git_tag_list(out, repo); checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } @@ -36,6 +38,30 @@ Pointer lookup({ }); } +/// Lookup a tag object from the repository by its short [oid]. +/// +/// The returned tag must be freed with [free]. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer lookupPrefix({ + required Pointer repoPointer, + required Pointer oidPointer, + required int length, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_tag_lookup_prefix( + out, + repoPointer, + oidPointer, + length, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Get the tagged object of a tag. /// /// This method performs a repository lookup for the given object and returns @@ -62,6 +88,10 @@ Pointer targetOid(Pointer tag) => /// Get the id of a tag. Pointer id(Pointer tag) => libgit2.git_tag_id(tag); +/// Get the repository that owns this tag. +Pointer owner(Pointer tag) => + libgit2.git_tag_owner(tag); + /// Get the name of a tag. String name(Pointer tag) => libgit2.git_tag_name(tag).toDartString(); @@ -71,10 +101,47 @@ String message(Pointer tag) { return result == nullptr ? '' : result.toDartString(); } +/// Check whether a tag name is valid. +bool nameIsValid(String name) { + return using((arena) { + final valid = arena(); + final nameC = name.toChar(arena); + final error = libgit2.git_tag_name_is_valid(valid, nameC); + checkErrorAndThrow(error); + return valid.value == 1; + }); +} + /// Get the tagger (author) of a tag. The returned signature must be freed. Pointer tagger(Pointer tag) => libgit2.git_tag_tagger(tag); +/// Recursively peel a tag until an object of the specified type is found. +/// +/// The returned object must be freed. +Pointer peel({ + required Pointer tagPointer, + required git_object_t targetType, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_tag_peel(out, tagPointer); + checkErrorAndThrow(error); + + final object = out.value; + if (targetType == git_object_t.GIT_OBJECT_ANY || + libgit2.git_object_type(object).value == targetType.value) { + return object; + } + + final peeled = arena>(); + final peelError = libgit2.git_object_peel(peeled, object, targetType); + libgit2.git_object_free(object); + checkErrorAndThrow(peelError); + return peeled.value; + }); +} + /// 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 @@ -114,6 +181,33 @@ Pointer createAnnotated({ }); } +/// Create a new annotated tag object without creating a reference. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer createAnnotation({ + required Pointer repoPointer, + required String tagName, + required Pointer targetPointer, + required Pointer taggerPointer, + required String message, +}) { + return using((arena) { + final out = calloc(); + final tagNameC = tagName.toChar(arena); + final messageC = message.toChar(arena); + final error = libgit2.git_tag_annotation_create( + out, + repoPointer, + tagNameC, + targetPointer, + taggerPointer, + messageC, + ); + checkErrorAndThrow(error); + return out; + }); +} + /// Create a new lightweight tag pointing at a target object. /// /// A new direct reference will be created pointing to this target object. If @@ -185,9 +279,11 @@ List listMatch({ final patternC = pattern.toChar(arena); final error = libgit2.git_tag_list_match(out, patternC, repoPointer); checkErrorAndThrow(error); - return [ + final result = [ for (var i = 0; i < out.ref.count; i++) out.ref.strings[i].toDartString(), ]; + libgit2.git_strarray_dispose(out); + return result; }); } diff --git a/lib/src/bindings/tree.dart b/lib/src/bindings/tree.dart index 3468c7a..5c578fb 100644 --- a/lib/src/bindings/tree.dart +++ b/lib/src/bindings/tree.dart @@ -1,14 +1,43 @@ import 'dart:ffi'; -import 'package:ffi/ffi.dart' show using; +import 'package:ffi/ffi.dart' show Utf8, Utf8Pointer, calloc, using; import 'package:git2dart/git2dart.dart'; import 'package:git2dart/src/extensions.dart'; import 'package:git2dart/src/helpers/error_helper.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; +var _walkEntries = []; + +class TreeUpdate { + const TreeUpdate({ + required this.action, + required this.path, + this.oidPointer, + this.filemode, + }); + + final git_tree_update_t action; + final String path; + final Pointer? oidPointer; + final git_filemode_t? filemode; +} + +int _walkCb( + Pointer root, + Pointer entry, + Pointer payload, +) { + _walkEntries.add('${root.cast().toDartString()}${entryName(entry)}'); + return 0; +} + /// Get the id of a tree. Pointer id(Pointer tree) => libgit2.git_tree_id(tree); +/// Get the repository that owns this tree. +Pointer owner(Pointer tree) => + libgit2.git_tree_owner(tree); + /// Lookup a tree object from the repository. The returned tree must be freed /// with [free]. /// @@ -27,6 +56,30 @@ Pointer lookup({ }); } +/// Lookup a tree object from a repository by its short [oid]. +/// +/// The returned tree must be freed with [free]. +/// +/// Throws a [LibGit2Error] if error occurred. +Pointer lookupPrefix({ + required Pointer repoPointer, + required Pointer oidPointer, + required int length, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_tree_lookup_prefix( + out, + repoPointer, + oidPointer, + length, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// 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 @@ -99,6 +152,69 @@ Pointer getByPath({ /// Get the number of entries listed in a tree. int entryCount(Pointer tree) => libgit2.git_tree_entrycount(tree); +/// Walk entries in a tree. +List walk({ + required Pointer treePointer, + required git_treewalk_mode mode, +}) { + _walkEntries = []; + + final error = libgit2.git_tree_walk( + treePointer, + mode, + Pointer.fromFunction< + Int Function(Pointer, Pointer, Pointer) + >(_walkCb, -1), + nullptr, + ); + + checkErrorAndThrow(error); + return List.unmodifiable(_walkEntries); +} + +/// Create a tree based on another one with the specified modifications. +Pointer createUpdated({ + required Pointer repoPointer, + required Pointer baselinePointer, + required List updates, +}) { + final out = calloc(); + + using((arena) { + final updatesC = arena(updates.length); + + for (var i = 0; i < updates.length; i++) { + final update = updates[i]; + final updateC = updatesC + i; + updateC.ref.actionAsInt = update.action.value; + updateC.ref.path = update.path.toChar(arena); + + if (update.oidPointer != null) { + updateC.ref.id.type = update.oidPointer!.ref.type; + for (var j = 0; j < 32; j++) { + updateC.ref.id.id[j] = update.oidPointer!.ref.id[j]; + } + } + + if (update.filemode != null) { + updateC.ref.filemodeAsInt = update.filemode!.value; + } + } + + final error = libgit2.git_tree_create_updated( + out, + repoPointer, + baselinePointer, + updates.length, + updatesC, + ); + + checkErrorAndThrow(error); + }); + + return out; +} + /// Get the id of the object pointed by the entry. Pointer entryId(Pointer entry) => libgit2.git_tree_entry_id(entry); @@ -140,6 +256,34 @@ git_object_t entryType(Pointer entry) { return libgit2.git_tree_entry_type(entry); } +/// Compare two tree entries. +int entryCompare({ + required Pointer aPointer, + required Pointer bPointer, +}) { + return libgit2.git_tree_entry_cmp(aPointer, bPointer); +} + +/// Convert a tree entry to the object it points to. +/// +/// The returned object must be freed. +Pointer entryToObject({ + required Pointer repoPointer, + required Pointer entryPointer, +}) { + return using((arena) { + final out = arena>(); + final error = libgit2.git_tree_entry_to_object( + out, + repoPointer, + entryPointer, + ); + + checkErrorAndThrow(error); + return out.value; + }); +} + /// Free a user-owned tree entry. /// /// IMPORTANT: This function is only needed for tree entries owned by the user, diff --git a/lib/src/bindings/worktree.dart b/lib/src/bindings/worktree.dart index d54ffa9..381b1bd 100644 --- a/lib/src/bindings/worktree.dart +++ b/lib/src/bindings/worktree.dart @@ -162,6 +162,7 @@ List> list(Pointer repo) { continue; } } + libgit2.git_strarray_dispose(out); return result; }); } diff --git a/lib/src/branch.dart b/lib/src/branch.dart index 8a9ad26..00a2f16 100644 --- a/lib/src/branch.dart +++ b/lib/src/branch.dart @@ -152,6 +152,9 @@ class Branch extends Equatable { ); } + /// Checks whether [name] is a valid branch name. + static bool isNameValid(String name) => bindings.nameIsValid(name); + /// [Oid] pointed to by a branch. /// /// Throws a [LibGit2Error] if error occurred. diff --git a/lib/src/commit.dart b/lib/src/commit.dart index c614fd6..85b737b 100644 --- a/lib/src/commit.dart +++ b/lib/src/commit.dart @@ -255,6 +255,9 @@ class Commit extends Equatable { /// leading newlines. String get message => bindings.message(_commitPointer); + /// Gets the full raw commit message without cleanup. + String get messageRaw => bindings.messageRaw(_commitPointer); + /// Gets the short "summary" of the commit message. /// /// The returned message is the summary of the commit, comprising the first @@ -286,9 +289,29 @@ class Commit extends Equatable { /// Gets the committer of the commit. Signature get committer => Signature(bindings.committer(_commitPointer)); + /// Gets the committer of the commit resolved through [mailmap]. + Signature committerWithMailmap(Mailmap mailmap) { + return Signature.fromOwned( + bindings.committerWithMailmap( + commitPointer: _commitPointer, + mailmapPointer: mailmap.pointer, + ), + ); + } + /// Gets the author of the commit. Signature get author => Signature(bindings.author(_commitPointer)); + /// Gets the author of the commit resolved through [mailmap]. + Signature authorWithMailmap(Mailmap mailmap) { + return Signature.fromOwned( + bindings.authorWithMailmap( + commitPointer: _commitPointer, + mailmapPointer: mailmap.pointer, + ), + ); + } + /// Gets the list of parent commit [Oid]s. List get parents { final parentCount = bindings.parentCount(_commitPointer); @@ -328,6 +351,9 @@ class Commit extends Equatable { return bindings.headerField(commitPointer: _commitPointer, field: field); } + /// Gets the full raw commit header. + String get rawHeader => bindings.rawHeader(_commitPointer); + /// Gets the nth generation ancestor of the commit. /// /// This follows only the first parents. Passing 0 as the generation number diff --git a/lib/src/config.dart b/lib/src/config.dart index aec333c..b19db4a 100644 --- a/lib/src/config.dart +++ b/lib/src/config.dart @@ -27,6 +27,14 @@ class Config with IterableMixin { _finalizer.attach(this, _configPointer, detach: this); } + /// Creates a new empty configuration object. + Config.empty() { + libgit2.git_libgit2_init(); + + _configPointer = bindings.init(); + _finalizer.attach(this, _configPointer, detach: this); + } + /// Opens config file at provided [path]. /// /// If [path] isn't provided, opens global, XDG and system config files. @@ -95,6 +103,12 @@ class Config with IterableMixin { /// Pointer to memory address for allocated config object. late final Pointer _configPointer; + /// Pointer to memory address for allocated config object. + /// + /// Note: For internal use. + @internal + Pointer get pointer => _configPointer; + /// The snapshot of the current state of a configuration, which allows you to /// look into a consistent view of the configuration for looking up complex /// values (e.g. a remote, submodule). @@ -131,6 +145,15 @@ class Config with IterableMixin { return bindings.getInt32(configPointer: _configPointer, variable: variable); } + /// Sets [variable] to a 32-bit integer [value]. + void setInt32(String variable, int value) { + bindings.setInt32( + configPointer: _configPointer, + variable: variable, + value: value, + ); + } + /// Returns [variable] parsed as a 64-bit integer. int getInt64(String variable) { return bindings.getInt64(configPointer: _configPointer, variable: variable); diff --git a/lib/src/git_types.dart b/lib/src/git_types.dart index 84cd6ab..79279ad 100644 --- a/lib/src/git_types.dart +++ b/lib/src/git_types.dart @@ -1212,6 +1212,62 @@ enum GitFetchPrune { }; } +/// Automatic tag following option for remotes. +enum GitRemoteAutotag { + /// Use the setting from the configuration. + unspecified(0), + + /// Ask the server for tags pointing to objects already being downloaded. + auto(1), + + /// Do not ask for tags beyond the refspecs. + none(2), + + /// Ask for all tags. + all(3); + + const GitRemoteAutotag(this.value); + final int value; + + static GitRemoteAutotag fromValue(int value) => switch (value) { + 0 => unspecified, + 1 => auto, + 2 => none, + 3 => all, + _ => throw ArgumentError('Unknown value for GitRemoteAutotag: $value'), + }; +} + +/// Option flags for extended repository opening. +enum GitRepositoryOpen { + /// Only open the repository if it can be immediately found at the start path. + noSearch(1), + + /// Continue searching across filesystem boundaries. + crossFs(2), + + /// Open repository as bare regardless of configuration. + bare(4), + + /// Do not check for a repository by appending `.git` to the start path. + noDotGit(8), + + /// Find and open a repository using git environment variables. + fromEnv(16); + + const GitRepositoryOpen(this.value); + final int value; + + static GitRepositoryOpen fromValue(int value) => switch (value) { + 1 => noSearch, + 2 => crossFs, + 4 => bare, + 8 => noDotGit, + 16 => fromEnv, + _ => throw ArgumentError('Unknown value for GitRepositoryOpen: $value'), + }; +} + /// Option flags for [Repository] init. enum GitRepositoryInit { /// Create a bare repository with no working directory. @@ -1346,6 +1402,36 @@ enum GitFeature { }; } +/// Git-specific file names recognized by libgit2 path checks. +enum GitPathGitFile { + /// Check for the `.gitignore` file. + gitignore(0), + + /// Check for the `.gitmodules` file. + gitmodules(1), + + /// Check for the `.gitattributes` file. + gitattributes(2); + + const GitPathGitFile(this.value); + final int value; +} + +/// Filesystem-specific path checks for gitfile detection. +enum GitPathFilesystem { + /// Perform generic NTFS and HFS checks. + generic(0), + + /// Perform NTFS-specific checks. + ntfs(1), + + /// Perform HFS-specific checks. + hfs(2); + + const GitPathFilesystem(this.value); + final int value; +} + /// Combinations of these values determine the lookup order for attribute. enum GitAttributeCheck { /// Check file first, then index. @@ -1575,6 +1661,50 @@ enum GitSubmoduleUpdate { }; } +/// Submodule fetch recurse values. +/// +/// These values represent settings for the +/// `submodule.$name.fetchRecurseSubmodules` configuration value. +enum GitSubmoduleRecurse { + /// Do not recurse into submodules when fetching. + no(0), + + /// Recurse into submodules when fetching. + yes(1), + + /// Recurse into submodules only when a commit is not already in the local + /// clone. + onDemand(2); + + const GitSubmoduleRecurse(this.value); + final int value; + + static GitSubmoduleRecurse fromValue(int value) => switch (value) { + 0 => no, + 1 => yes, + 2 => onDemand, + _ => throw ArgumentError('Unknown value for GitSubmoduleRecurse: $value'), + }; +} + +/// Tree walk traversal mode. +enum GitTreeWalk { + /// Visit each entry before visiting children. + pre(0), + + /// Visit each entry after visiting children. + post(1); + + const GitTreeWalk(this.value); + final int value; + + static GitTreeWalk fromValue(int value) => switch (value) { + 0 => pre, + 1 => post, + _ => throw ArgumentError('Unknown value for GitTreeWalk: $value'), + }; +} + /// A combination of these flags will be returned to describe the status of a /// submodule. Depending on the "ignore" property of the submodule, some of /// the flags may never be returned because they indicate changes that are diff --git a/lib/src/index.dart b/lib/src/index.dart index 4c11d11..a15bd7e 100644 --- a/lib/src/index.dart +++ b/lib/src/index.dart @@ -94,29 +94,38 @@ class Index with IterableMixin { final result = {}; for (final entry in conflicts) { - IndexEntry? ancestor, our, their; - String path; - - entry['ancestor'] == nullptr - ? ancestor = null - : ancestor = IndexEntry(entry['ancestor']!); - entry['our'] == nullptr ? our = null : our = IndexEntry(entry['our']!); - entry['their'] == nullptr - ? their = null - : their = IndexEntry(entry['their']!); - - if (our != null) { - path = our.path; - } else { - path = their!.path; - } - - result[path] = ConflictEntry(_indexPointer, path, ancestor, our, their); + final conflictEntry = _conflictEntryFromPointers(entry); + result[conflictEntry._path] = conflictEntry; } return result; } + /// Returns the conflict entry for [path]. + /// + /// Throws a [LibGit2Error] if [path] does not have a conflict entry or if an + /// error occurred. + ConflictEntry conflict(String path) { + return _conflictEntryFromPointers( + bindings.conflictGet(indexPointer: _indexPointer, path: path), + fallbackPath: path, + ); + } + + ConflictEntry _conflictEntryFromPointers( + Map> entry, { + String? fallbackPath, + }) { + final ancestor = + entry['ancestor'] == nullptr ? null : IndexEntry(entry['ancestor']!); + final our = entry['our'] == nullptr ? null : IndexEntry(entry['our']!); + final their = + entry['their'] == nullptr ? null : IndexEntry(entry['their']!); + final path = our?.path ?? their?.path ?? ancestor?.path ?? fallbackPath!; + + return ConflictEntry(_indexPointer, path, ancestor, our, their); + } + /// Adds or updates index entries to represent a conflict. Any staged entries /// that exist at the given paths will be removed. /// @@ -150,6 +159,87 @@ class Index with IterableMixin { /// Throws a [LibGit2Error] if error occured. void cleanupConflict() => bindings.conflictCleanup(_indexPointer); + /// Filename conflict entries currently recorded in the index. + List get nameEntries { + final count = bindings.nameEntryCount(_indexPointer); + return [ + for (var i = 0; i < count; i++) + IndexNameEntry._( + bindings.nameGetByIndex(indexPointer: _indexPointer, position: i), + ), + ]; + } + + /// Records the filenames involved in a rename conflict. + void addNameEntry({ + required String ancestor, + required String ours, + required String theirs, + }) { + bindings.nameAdd( + indexPointer: _indexPointer, + ancestor: ancestor, + ours: ours, + theirs: theirs, + ); + } + + /// Removes all filename conflict entries. + void clearNameEntries() => bindings.nameClear(_indexPointer); + + /// Resolve undo entries currently recorded in the index. + List get reucEntries { + final count = bindings.reucEntryCount(_indexPointer); + return [ + for (var i = 0; i < count; i++) + IndexReucEntry._( + bindings.reucGetByIndex(indexPointer: _indexPointer, position: i), + ), + ]; + } + + /// Returns the zero-based position of the resolve undo entry at [path]. + int findReuc(String path) { + return bindings.reucFind(indexPointer: _indexPointer, path: path); + } + + /// Returns the resolve undo entry at [path]. + IndexReucEntry reucEntry(String path) { + return IndexReucEntry._( + bindings.reucGetByPath(indexPointer: _indexPointer, path: path), + ); + } + + /// Adds or replaces a resolve undo entry. + void addReucEntry({ + required String path, + required GitFilemode ancestorMode, + required Oid ancestorOid, + required GitFilemode ourMode, + required Oid ourOid, + required GitFilemode theirMode, + required Oid theirOid, + }) { + bindings.reucAdd( + indexPointer: _indexPointer, + path: path, + ancestorMode: ancestorMode.value, + ancestorOid: ancestorOid.pointer, + ourMode: ourMode.value, + ourOid: ourOid.pointer, + theirMode: theirMode.value, + theirOid: theirOid.pointer, + ); + } + + /// Removes the resolve undo entry at [position]. + void removeReucEntry(int position) { + bindings.reucRemove(indexPointer: _indexPointer, position: position); + } + + /// Removes all resolve undo entries. + void clearReucEntries() => bindings.reucClear(_indexPointer); + /// Clears the contents (all the entries) of an index object. /// /// This clears the index object in memory; changes must be explicitly @@ -479,6 +569,80 @@ class ConflictEntry { 'path: $_path}'; } +@immutable +class IndexNameEntry extends Equatable { + const IndexNameEntry._(this._indexNameEntryPointer); + + final Pointer _indexNameEntryPointer; + + /// Path of the file as it existed in the ancestor. + String get ancestor => _indexNameEntryPointer.ref.ancestor.toDartString(); + + /// Path of the file as it existed in our tree. + String get ours => _indexNameEntryPointer.ref.ours.toDartString(); + + /// Path of the file as it existed in their tree. + String get theirs => _indexNameEntryPointer.ref.theirs.toDartString(); + + @override + String toString() { + return 'IndexNameEntry{ancestor: $ancestor, ours: $ours, ' + 'theirs: $theirs}'; + } + + @override + List get props => [ancestor, ours, theirs]; +} + +@immutable +class IndexReucEntry extends Equatable { + const IndexReucEntry._(this._indexReucEntryPointer); + + final Pointer _indexReucEntryPointer; + + /// Path of the resolved conflict. + String get path => _indexReucEntryPointer.ref.path.toDartString(); + + /// File mode of the ancestor side. + GitFilemode get ancestorMode => + GitFilemode.fromValue(_indexReucEntryPointer.ref.mode[0]); + + /// File mode of our side. + GitFilemode get ourMode => + GitFilemode.fromValue(_indexReucEntryPointer.ref.mode[1]); + + /// File mode of their side. + GitFilemode get theirMode => + GitFilemode.fromValue(_indexReucEntryPointer.ref.mode[2]); + + /// Object id of the ancestor side. + Oid get ancestorOid => Oid.fromRaw(_indexReucEntryPointer.ref.oid[0]); + + /// Object id of our side. + Oid get ourOid => Oid.fromRaw(_indexReucEntryPointer.ref.oid[1]); + + /// Object id of their side. + Oid get theirOid => Oid.fromRaw(_indexReucEntryPointer.ref.oid[2]); + + @override + String toString() { + return 'IndexReucEntry{path: $path, ancestorMode: $ancestorMode, ' + 'ourMode: $ourMode, theirMode: $theirMode, ' + 'ancestorOid: $ancestorOid, ourOid: $ourOid, theirOid: $theirOid}'; + } + + @override + List get props => [ + path, + ancestorMode, + ourMode, + theirMode, + ancestorOid, + ourOid, + theirOid, + ]; +} + class _IndexIterator implements Iterator { _IndexIterator(this._indexPointer) { count = bindings.entryCount(_indexPointer); diff --git a/lib/src/libgit2.dart b/lib/src/libgit2.dart index 13c2957..39a8064 100644 --- a/lib/src/libgit2.dart +++ b/lib/src/libgit2.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart' show calloc, using; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/object.dart' as object_bindings; import 'package:git2dart/src/extensions.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; @@ -38,6 +39,50 @@ class Libgit2 { .toSet(); } + /// Prerelease state of the loaded libgit2 library, or `null` for a final + /// release build. + static String? get prerelease { + libgit2.git_libgit2_init(); + final result = libgit2.git_libgit2_prerelease(); + return result == nullptr ? null : result.toDartString(); + } + + /// Backend details for a compile-time [feature], or `null` if unsupported. + static String? featureBackend(GitFeature feature) { + libgit2.git_libgit2_init(); + final result = libgit2.git_libgit2_feature_backend( + git_feature_t.fromValue(feature.value), + ); + return result == nullptr ? null : result.toDartString(); + } + + /// Returns whether [type] is a valid loose object type. + static bool objectTypeIsLoose(GitObject type) { + return object_bindings.typeIsLoose(git_object_t.fromValue(type.value)); + } + + /// Returns whether [path] matches one of libgit2's protected gitfile names. + static bool isGitFile({ + required String path, + required GitPathGitFile gitfile, + GitPathFilesystem filesystem = GitPathFilesystem.generic, + }) { + libgit2.git_libgit2_init(); + return using((arena) { + final pathC = path.toChar(arena); + final result = libgit2.git_path_is_gitfile( + pathC, + path.length, + git_path_gitfile.fromValue(gitfile.value), + git_path_fs.fromValue(filesystem.value), + ); + if (result < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + return result > 0; + }); + } + /// Get or set the maximum mmap window size. /// /// This controls the maximum size of memory-mapped files that libgit2 diff --git a/lib/src/mailmap.dart b/lib/src/mailmap.dart index 66720d3..8401a4e 100644 --- a/lib/src/mailmap.dart +++ b/lib/src/mailmap.dart @@ -3,6 +3,7 @@ import 'dart:ffi'; import 'package:git2dart/git2dart.dart'; import 'package:git2dart/src/bindings/mailmap.dart' as bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; +import 'package:meta/meta.dart'; /// A class representing a Git mailmap, which maps commit author/committer names and emails /// to their canonical forms. @@ -80,6 +81,12 @@ class Mailmap { /// Pointer to the underlying Git mailmap object. late final Pointer _mailmapPointer; + /// Gets the pointer to the underlying Git mailmap object. + /// + /// This getter is for internal use only. + @internal + Pointer get pointer => _mailmapPointer; + /// Resolves a name and email to their canonical forms according to the mailmap. /// /// Returns a list containing two elements: @@ -115,7 +122,7 @@ class Mailmap { /// final resolvedSignature = mailmap.resolveSignature(originalSignature); /// ``` Signature resolveSignature(Signature signature) { - return Signature( + return Signature.fromOwned( bindings.resolveSignature( mailmapPointer: _mailmapPointer, signaturePointer: signature.pointer, diff --git a/lib/src/merge.dart b/lib/src/merge.dart index cf32731..d214fb6 100644 --- a/lib/src/merge.dart +++ b/lib/src/merge.dart @@ -52,8 +52,9 @@ class Merge { final result = List.generate( oidArray.ref.count, - (i) => Oid(oidArray.ref.ids + i), + (i) => Oid.fromRaw(oidArray.ref.ids[i]), ); + bindings.oidArrayDispose(oidArray); return result; } diff --git a/lib/src/note.dart b/lib/src/note.dart index 91f0857..01a7063 100644 --- a/lib/src/note.dart +++ b/lib/src/note.dart @@ -157,6 +157,12 @@ class Note extends Equatable { /// The message content of this note. String get message => bindings.message(_notePointer); + /// The signature of this note's author. + Signature get author => Signature(bindings.author(_notePointer)); + + /// The signature of this note's committer. + Signature get committer => Signature(bindings.committer(_notePointer)); + /// The [Oid] of the git object being annotated by this note. Oid get annotatedOid => Oid(_annotatedOidPointer); diff --git a/lib/src/odb.dart b/lib/src/odb.dart index 1ff356b..0471afd 100644 --- a/lib/src/odb.dart +++ b/lib/src/odb.dart @@ -55,6 +55,9 @@ class Odb extends Equatable { bindings.addDiskAlternate(odbPointer: _odbPointer, path: path); } + /// Number of backends configured for this object database. + int get backendCount => bindings.backendCount(_odbPointer); + /// List of all objects [Oid]s available in the database. /// /// Throws a [LibGit2Error] if error occurred. @@ -85,19 +88,61 @@ class Odb extends Equatable { /// Throws a [LibGit2Error] if error occurred or [ArgumentError] if provided /// type is invalid. Oid write({required GitObject type, required String data}) { + _checkWritableObjectType(type); + return Oid( + bindings.write( + odbPointer: _odbPointer, + type: git_object_t.fromValue(type.value), + data: data, + ), + ); + } + + /// Writes raw [data] directly into the object database. + /// + /// This is intended for compatibility with custom backends that do not + /// support streaming writes. Prefer [write] for normal object writes. + /// + /// Throws a [LibGit2Error] if error occurred or [ArgumentError] if provided + /// type is invalid. + Oid writeDirect({required GitObject type, required String data}) { + _checkWritableObjectType(type); + return Oid( + bindings.writeDirect( + odbPointer: _odbPointer, + type: git_object_t.fromValue(type.value), + data: data, + ), + ); + } + + /// Generates an object ID for [data] as a Git object of [type]. + /// + /// Throws [ArgumentError] if provided type is invalid. + static Oid hash({required GitObject type, required String data}) { + _checkWritableObjectType(type); + return Oid( + bindings.hash(type: git_object_t.fromValue(type.value), data: data), + ); + } + + /// Determines the object ID of a file on disk as a Git object of [type]. + /// + /// Throws a [LibGit2Error] if error occurred or [ArgumentError] if provided + /// type is invalid. + static Oid hashFile({required GitObject type, required String path}) { + _checkWritableObjectType(type); + return Oid( + bindings.hashFile(type: git_object_t.fromValue(type.value), path: path), + ); + } + + static void _checkWritableObjectType(GitObject type) { if (type == GitObject.any || type == GitObject.invalid || type == GitObject.offsetDelta || type == GitObject.refDelta) { throw ArgumentError.value('$type is invalid type'); - } else { - return Oid( - bindings.write( - odbPointer: _odbPointer, - type: git_object_t.fromValue(type.value), - data: data, - ), - ); } } @@ -149,6 +194,11 @@ class OdbObject extends Equatable { /// Real size of the `data` buffer, not the actual size of the object. int get size => bindings.objectSize(_odbObjectPointer); + /// Creates an in-memory copy of this ODB object. + OdbObject duplicate() { + return OdbObject._(bindings.duplicateObject(_odbObjectPointer)); + } + /// Releases memory allocated for odbObject object. void free() { bindings.freeObject(_odbObjectPointer); diff --git a/lib/src/oid.dart b/lib/src/oid.dart index 5693eeb..a5ef8ea 100644 --- a/lib/src/oid.dart +++ b/lib/src/oid.dart @@ -21,6 +21,26 @@ class Oid extends Equatable { @internal Oid(this._oidPointer); + /// Initializes a new instance by parsing a possibly shortened hexadecimal + /// object id. + /// + /// Missing trailing digits are zero-filled by libgit2. + Oid.fromSHAParse(String sha) { + if (!sha.isValidSHA1() && !sha.isValidSHA256()) { + throw ArgumentError.value( + sha, + 'sha', + 'Not a valid SHA hex string. Must be 4-64 hex characters.', + ); + } + + final type = + sha.length > GIT_OID_SHA1_HEXSIZE + ? git_oid_t.GIT_OID_SHA256 + : git_oid_t.GIT_OID_SHA1; + _oidPointer = bindings.fromStrP(sha, type: type); + } + /// Initializes a new instance of [Oid] class by determining if an object can /// be found in the ODB of [repo]sitory with provided hexadecimal [sha] /// string that is between 4 and 64 characters long. @@ -106,6 +126,9 @@ class Oid extends Equatable { /// Formats this Oid into a string buffer with [length] bytes. String toStr(int length) => bindings.toStr(id: _oidPointer, length: length); + /// Formats this Oid into exactly [length] hexadecimal characters. + String toStrN(int length) => bindings.toStrN(id: _oidPointer, length: length); + /// Formats this Oid using libgit2's thread-local formatter. String toStrS() => bindings.toStrS(_oidPointer); diff --git a/lib/src/patch.dart b/lib/src/patch.dart index 6a0bd1d..c04861b 100644 --- a/lib/src/patch.dart +++ b/lib/src/patch.dart @@ -187,6 +187,12 @@ class Patch extends Equatable { /// Throws a [LibGit2Error] if error occured. String get text => bindings.text(_patchPointer); + /// Content of a patch collected through the print callback. + /// + /// Returns the complete patch text in unified diff format. + /// Throws a [LibGit2Error] if error occured. + String get printedText => bindings.print(_patchPointer); + /// Content of a patch as bytes. Uint8List get textBytes => bindings.textBytes(_patchPointer); diff --git a/lib/src/reference.dart b/lib/src/reference.dart index d18354b..65f8ef1 100644 --- a/lib/src/reference.dart +++ b/lib/src/reference.dart @@ -75,6 +75,47 @@ class Reference extends Equatable { _finalizer.attach(this, _refPointer, detach: this); } + /// Conditionally creates or updates a reference for provided [target]. + /// + /// [currentTarget] must match the value currently stored in the reference, + /// otherwise libgit2 reports a modification error. + /// + /// Throws a [LibGit2Error] if error occured or [ArgumentError] if [target] + /// and [currentTarget] are not matching direct or symbolic reference values. + Reference.createMatching({ + required Repository repo, + required String name, + required Object target, + required Object currentTarget, + bool force = false, + String? logMessage, + }) { + if (target is Oid && currentTarget is Oid) { + _refPointer = bindings.createDirectMatching( + repoPointer: repo.pointer, + name: name, + oidPointer: target.pointer, + force: force, + currentIdPointer: currentTarget.pointer, + logMessage: logMessage, + ); + } else if (target is String && currentTarget is String) { + _refPointer = bindings.createSymbolicMatching( + repoPointer: repo.pointer, + name: name, + target: target, + force: force, + currentValue: currentTarget, + logMessage: logMessage, + ); + } else { + throw ArgumentError.value( + 'target and currentTarget must both be Oid or both be String', + ); + } + _finalizer.attach(this, _refPointer, detach: this); + } + /// Lookups reference [name] in a [repo]sitory. /// /// The [name] will be checked for validity. @@ -101,6 +142,11 @@ class Reference extends Equatable { bindings.delete(ref.pointer); } + /// Deletes an existing reference by [name] without checking its old value. + static void remove({required Repository repo, required String name}) { + bindings.remove(repoPointer: repo.pointer, name: name); + } + /// Renames an existing reference with provided [oldName]. /// /// This method works for both direct and symbolic references. @@ -169,6 +215,54 @@ class Reference extends Equatable { /// Throws a [LibGit2Error] if error occured. static List list(Repository repo) => bindings.list(repo.pointer); + /// List of all reference names collected through libgit2's callback API. + /// + /// Throws a [LibGit2Error] if error occured. + static List listForeach(Repository repo) { + return bindings.listForeach(repo.pointer); + } + + /// List of all reference names collected through libgit2's name callback API. + /// + /// Throws a [LibGit2Error] if error occured. + static List listForeachName(Repository repo) { + return bindings.listForeachName(repo.pointer); + } + + /// List of reference names matching [glob]. + /// + /// Throws a [LibGit2Error] if error occured. + static List listForeachGlob({ + required Repository repo, + required String glob, + }) { + return bindings.listForeachGlob(repoPointer: repo.pointer, glob: glob); + } + + /// List of all reference names collected through libgit2's iterator API. + /// + /// Throws a [LibGit2Error] if error occured. + static List listIterator(Repository repo) { + return bindings.listIterator(repo.pointer); + } + + /// List of all reference names collected through libgit2's name iterator API. + /// + /// Throws a [LibGit2Error] if error occured. + static List listIteratorNames(Repository repo) { + return bindings.listIteratorNames(repo.pointer); + } + + /// List of reference names matching [glob] through libgit2's iterator API. + /// + /// Throws a [LibGit2Error] if error occured. + static List listIteratorGlob({ + required Repository repo, + required String glob, + }) { + return bindings.listIteratorGlob(repoPointer: repo.pointer, glob: glob); + } + /// Suggests that the [repo]sitory's refdb compress or optimize its /// references. This mechanism is implementation specific. For on-disk /// reference databases, for example, this may pack all loose references. @@ -208,6 +302,14 @@ class Reference extends Equatable { /// Creates a copy of an existing reference. Reference duplicate() => Reference(bindings.duplicate(_refPointer)); + /// Compares this reference with [other] using libgit2's reference ordering. + int compareTo(Reference other) { + return bindings.compare( + ref1Pointer: _refPointer, + ref2Pointer: other.pointer, + ); + } + /// Type of the reference. ReferenceType get type { return bindings.referenceType(_refPointer).value == 1 @@ -224,6 +326,14 @@ class Reference extends Equatable { : Oid(bindings.target(bindings.resolve(_refPointer))); } + /// Peeled OID target for a direct reference to an annotated tag. + /// + /// Returns `null` when the reference has no peeled target. + Oid? get peeledTarget { + final result = bindings.targetPeel(_refPointer); + return result == null ? null : Oid(result); + } + /// Recursively peel reference until object of the specified [type] is found. /// /// The retrieved peeled object is owned by the repository and should be diff --git a/lib/src/remote.dart b/lib/src/remote.dart index a694f60..282e6d3 100644 --- a/lib/src/remote.dart +++ b/lib/src/remote.dart @@ -137,6 +137,17 @@ class Remote extends Equatable { url: url, ); + /// Sets the [remote]'s automatic tag following setting in configuration. + static void setAutotag({ + required Repository repo, + required String remote, + required GitRemoteAutotag value, + }) => remote_bindings.setAutotag( + repoPointer: repo.pointer, + remote: remote, + value: git_remote_autotag_option_t.fromValue(value.value), + ); + /// Adds a fetch [refspec] to the [remote]'s configuration. /// /// The [refspec] will be checked for validity. @@ -190,6 +201,23 @@ class Remote extends Equatable { /// Returns empty string if no special url for pushing is set. String get pushUrl => remote_bindings.pushUrl(_remotePointer); + /// Sets the URL for this remote instance without changing configuration. + set url(String url) { + remote_bindings.setInstanceUrl(remotePointer: _remotePointer, url: url); + } + + /// Sets the push URL for this remote instance without changing configuration. + set pushUrl(String url) { + remote_bindings.setInstancePushUrl(remotePointer: _remotePointer, url: url); + } + + /// Remote's automatic tag following option. + GitRemoteAutotag get autotag { + return GitRemoteAutotag.fromValue( + remote_bindings.autotag(_remotePointer).value, + ); + } + /// Number of refspecs for a remote. /// /// This includes both fetch and push refspecs. diff --git a/lib/src/repository.dart b/lib/src/repository.dart index ba2af50..5c6a280 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -95,6 +95,18 @@ class Repository extends Equatable { _finalizer.attach(this, _repoPointer, detach: this); } + /// Creates a new git repository using the basic libgit2 initializer. + /// + /// This is equivalent to `git init` for non-bare repositories and + /// `git init --bare` when [bare] is true. + Repository.initBasic({required String path, bool bare = false}) { + libgit2.git_libgit2_init(); + + _repoPointer = bindings.initBasic(path: path, bare: bare); + + _finalizer.attach(this, _repoPointer, detach: this); + } + /// Opens repository at provided [path]. /// /// For a standard repository, [path] should either point to the ".git" folder @@ -110,6 +122,26 @@ class Repository extends Equatable { _finalizer.attach(this, _repoPointer, detach: this); } + /// Opens a repository with extended search [flags]. + /// + /// [path] may point to a repository, a working tree, or a directory inside a + /// working tree unless [GitRepositoryOpen.noSearch] is provided. + Repository.openExt({ + String? path, + Set flags = const {}, + String? ceilingDirs, + }) { + libgit2.git_libgit2_init(); + + _repoPointer = bindings.openExt( + path: path, + flags: flags.fold(0, (int acc, e) => acc | e.value), + ceilingDirs: ceilingDirs, + ); + + _finalizer.attach(this, _repoPointer, detach: this); + } + /// Opens a bare repository at provided [path]. /// /// Throws a [LibGit2Error] if error occured. @@ -283,6 +315,14 @@ class Repository extends Equatable { /// Detaches HEAD from its current branch. void detachHead() => bindings.detachHead(_repoPointer); + /// Detaches HEAD to the commit described by [commit]. + void setHeadDetachedFromAnnotated(AnnotatedCommit commit) { + bindings.setHeadDetachedFromAnnotated( + repoPointer: _repoPointer, + commitPointer: commit.pointer, + ); + } + /// Whether current branch is unborn. /// /// An unborn branch is one named from HEAD but which doesn't exist in the @@ -428,6 +468,21 @@ class Repository extends Equatable { /// Repository's head. Reference get head => Reference(bindings.head(_repoPointer)); + /// Returns the HEAD reference for linked worktree [name]. + Reference headForWorktree(String name) { + return Reference( + bindings.headForWorktree(repoPointer: _repoPointer, name: name), + ); + } + + /// Returns whether linked worktree [name] has a detached HEAD. + bool isHeadDetachedForWorktree(String name) { + return bindings.isHeadDetachedForWorktree( + repoPointer: _repoPointer, + name: name, + ); + } + /// Index file for this repository. Index get index => Index(bindings.index(_repoPointer)); @@ -436,6 +491,32 @@ class Repository extends Equatable { /// Throws a [LibGit2Error] if error occured. Odb get odb => Odb(bindings.odb(_repoPointer)); + /// Sets the configuration object used by this repository. + void setConfig(Config config) { + bindings.setConfig( + repoPointer: _repoPointer, + configPointer: config.pointer, + ); + } + + /// Sets the index object used by this repository. + void setIndex(Index index) { + bindings.setIndex(repoPointer: _repoPointer, indexPointer: index.pointer); + } + + /// Sets the object database used by this repository. + void setOdb(Odb odb) { + bindings.setOdb(repoPointer: _repoPointer, odbPointer: odb.pointer); + } + + /// Entries recorded in FETCH_HEAD. + List> get fetchHeadEntries { + return bindings.fetchHeadEntries(_repoPointer); + } + + /// Commit object IDs recorded in MERGE_HEAD. + List get mergeHeadOids => bindings.mergeHeadOids(_repoPointer); + /// List of all the references names that can be found in a repository. /// /// Throws a [LibGit2Error] if error occured. diff --git a/lib/src/revwalk.dart b/lib/src/revwalk.dart index 30aee55..03db630 100644 --- a/lib/src/revwalk.dart +++ b/lib/src/revwalk.dart @@ -36,10 +36,12 @@ class RevWalk { /// /// Throws a [LibGit2Error] if an error occurs. RevWalk(Repository repo) { + _repo = repo; _revWalkPointer = bindings.create(repo.pointer); _finalizer.attach(this, _revWalkPointer, detach: this); } + late final Repository _repo; late final Pointer _revWalkPointer; /// Pointer to memory address for allocated [RevWalk] object. @@ -184,6 +186,14 @@ class RevWalk { bindings.hideRef(walkerPointer: _revWalkPointer, refName: reference); } + /// Hides commits matching [predicate] from the revision walk. + void hideWhere(bool Function(Oid oid) predicate) { + bindings.addHideCallback( + walkerPointer: _revWalkPointer, + predicate: (sha) => predicate(Oid.fromSHA(_repo, sha)), + ); + } + /// Resets the revision walker for reuse. /// /// This will clear all the pushed and hidden commits, and leave the walker diff --git a/lib/src/signature.dart b/lib/src/signature.dart index 80548a8..b251139 100644 --- a/lib/src/signature.dart +++ b/lib/src/signature.dart @@ -47,6 +47,15 @@ class Signature extends Equatable { _finalizer.attach(this, _signaturePointer, detach: this); } + /// Initializes a new instance from an owned signature pointer. + /// + /// The provided pointer is not duplicated and will be freed by this object. + @internal + Signature.fromOwned(Pointer pointer) { + _signaturePointer = pointer; + _finalizer.attach(this, _signaturePointer, detach: this); + } + /// Creates new [Signature] from provided parameters. /// /// Creates a new signature with the specified name, email, and optional time @@ -107,6 +116,14 @@ class Signature extends Equatable { _finalizer.attach(this, _signaturePointer, detach: this); } + /// Creates default author and committer signatures from environment or config. + static List defaultSignaturesFromEnv(Repository repo) { + return bindings + .defaultSignaturesFromEnv(repo.pointer) + .map(Signature.fromOwned) + .toList(); + } + late final Pointer _signaturePointer; /// Pointer to memory address for allocated signature object. diff --git a/lib/src/submodule.dart b/lib/src/submodule.dart index 1647141..a4679b5 100644 --- a/lib/src/submodule.dart +++ b/lib/src/submodule.dart @@ -266,6 +266,32 @@ class Submodule extends Equatable { ); } + /// Fetch recurse submodules rule that will be used for the submodule. + GitSubmoduleRecurse get fetchRecurseSubmodules { + final rule = bindings.fetchRecurseSubmodules(_submodulePointer); + return GitSubmoduleRecurse.fromValue(rule.value); + } + + /// Sets the fetch recurse submodules rule for the submodule in the + /// configuration. + /// + /// This setting won't affect any existing instances. + set fetchRecurseSubmodules(GitSubmoduleRecurse rule) { + bindings.setFetchRecurseSubmodules( + repoPointer: bindings.owner(_submodulePointer), + name: name, + fetchRecurseSubmodules: git_submodule_recurse_t.fromValue(rule.value), + ); + } + + /// Locations where submodule information is present. + Set get location { + final resultInt = bindings.location(_submodulePointer); + return GitSubmoduleStatus.values + .where((e) => resultInt & e.value == e.value) + .toSet(); + } + /// Releases memory allocated for submodule object. void free() { bindings.free(_submodulePointer); @@ -276,7 +302,8 @@ class Submodule extends Equatable { String toString() { return 'Submodule{name: $name, path: $path, url: $url, branch: $branch, ' 'headOid: $headOid, indexOid: $indexOid, workdirOid: $workdirOid, ' - 'ignoreRule: $ignoreRule, updateRule: $updateRule}'; + 'ignoreRule: $ignoreRule, updateRule: $updateRule, ' + 'fetchRecurseSubmodules: $fetchRecurseSubmodules}'; } @override @@ -290,6 +317,7 @@ class Submodule extends Equatable { workdirOid, ignoreRule, updateRule, + fetchRecurseSubmodules, ]; } diff --git a/lib/src/tag.dart b/lib/src/tag.dart index f349254..ce63f62 100644 --- a/lib/src/tag.dart +++ b/lib/src/tag.dart @@ -40,6 +40,23 @@ class Tag extends Equatable { _finalizer.attach(this, _tagPointer, detach: this); } + /// Creates a new tag instance by looking up an abbreviated [oid]. + /// + /// [length] is the number of hexadecimal characters to use from [oid]. + /// Throws a [LibGit2Error] if the prefix is invalid, ambiguous, or not found. + Tag.lookupPrefix({ + required Repository repo, + required Oid oid, + required int length, + }) { + _tagPointer = bindings.lookupPrefix( + repoPointer: repo.pointer, + oidPointer: oid.pointer, + length: length, + ); + _finalizer.attach(this, _tagPointer, detach: this); + } + late final Pointer _tagPointer; /// Gets the pointer to the underlying libgit2 tag object. @@ -91,6 +108,30 @@ class Tag extends Equatable { } } + /// Recursively peels this tag to an object of [type]. + Object peel([GitObject type = GitObject.any]) { + final object = bindings.peel( + tagPointer: _tagPointer, + targetType: git_object_t.fromValue(type.value), + ); + final objectType = object_bindings.type(object); + + if (objectType.value == GitObject.commit.value) { + return Commit(object.cast()); + } else if (objectType.value == GitObject.tree.value) { + return Tree(object.cast()); + } else if (objectType.value == GitObject.blob.value) { + return Blob(object.cast()); + } else if (objectType.value == GitObject.tag.value) { + // coverage:ignore-line + // coverage:ignore-start + return Tag(object.cast()); + } else { + throw ArgumentError('Unsupported peeled tag target type: ${type.value}'); + // coverage:ignore-end + } + } + /// Creates a new annotated tag in the repository. /// /// [repo] is the repository where to store the tag. @@ -133,6 +174,36 @@ class Tag extends Equatable { return Oid(result); } + /// Creates a new annotated tag object without creating a tag reference. + /// + /// Returns the [Oid] of the newly created tag object. + static Oid createAnnotation({ + required Repository repo, + required String tagName, + required Oid target, + required GitObject targetType, + required Signature tagger, + required String message, + }) { + final object = object_bindings.lookup( + repoPointer: repo.pointer, + oidPointer: target.pointer, + type: git_object_t.fromValue(targetType.value), + ); + + final result = bindings.createAnnotation( + repoPointer: repo.pointer, + tagName: tagName, + targetPointer: object, + taggerPointer: tagger.pointer, + message: message, + ); + + object_bindings.free(object); + + return Oid(result); + } + /// Creates a new lightweight tag in the repository. /// /// [repo] is the repository where to store the tag. @@ -190,6 +261,9 @@ class Tag extends Equatable { bindings.delete(repoPointer: repo.pointer, tagName: tagName); } + /// Checks whether [name] is a valid tag name. + static bool isNameValid(String name) => bindings.nameIsValid(name); + /// Releases memory allocated for the tag object. /// /// This should be called when the tag object is no longer needed. diff --git a/lib/src/tree.dart b/lib/src/tree.dart index 98e9cee..dbc0865 100644 --- a/lib/src/tree.dart +++ b/lib/src/tree.dart @@ -42,6 +42,54 @@ class Tree extends Equatable { _finalizer.attach(this, _treePointer, detach: this); } + /// Lookups a tree object by an abbreviated [oid]. + /// + /// [length] is the number of hexadecimal characters to use from [oid]. + /// Throws [LibGit2Error] if the prefix is invalid, ambiguous, or not found. + Tree.lookupPrefix({ + required Repository repo, + required Oid oid, + required int length, + }) { + _treePointer = bindings.lookupPrefix( + repoPointer: repo.pointer, + oidPointer: oid.pointer, + length: length, + ); + _finalizer.attach(this, _treePointer, detach: this); + } + + /// Creates a tree from [baseline] with [updates] applied. + Tree.createUpdated({ + required Repository repo, + required Tree baseline, + required List updates, + }) { + _treePointer = bindings.lookup( + repoPointer: repo.pointer, + oidPointer: bindings.createUpdated( + repoPointer: repo.pointer, + baselinePointer: baseline.pointer, + updates: [ + for (final update in updates) + bindings.TreeUpdate( + action: + update.oid == null + ? git_tree_update_t.GIT_TREE_UPDATE_REMOVE + : git_tree_update_t.GIT_TREE_UPDATE_UPSERT, + path: update.path, + oidPointer: update.oid?.pointer, + filemode: + update.filemode == null + ? null + : git_filemode_t.fromValue(update.filemode!.value), + ), + ], + ), + ); + _finalizer.attach(this, _treePointer, detach: this); + } + late final Pointer _treePointer; /// Pointer to memory address for allocated tree object. @@ -109,6 +157,14 @@ class Tree extends Equatable { /// Returns the total count of entries (files and subdirectories) in this tree. int get length => bindings.entryCount(_treePointer); + /// Walks this tree and returns relative entry paths. + List walk({GitTreeWalk mode = GitTreeWalk.pre}) { + return bindings.walk( + treePointer: _treePointer, + mode: git_treewalk_mode.fromValue(mode.value), + ); + } + /// Releases memory allocated for tree object. /// /// This method should be called when the tree object is no longer needed @@ -133,6 +189,32 @@ final _finalizer = Finalizer>( ); // coverage:ignore-end +/// A change to apply when creating a tree from a baseline tree. +@immutable +class TreeUpdate extends Equatable { + /// Adds or replaces [path] with [oid] and [filemode]. + const TreeUpdate.upsert({ + required this.path, + required this.oid, + required this.filemode, + }); + + /// Removes [path]. + const TreeUpdate.remove(this.path) : oid = null, filemode = null; + + /// Full path from the root tree. + final String path; + + /// Object id to write for an upsert. + final Oid? oid; + + /// File mode to write for an upsert. + final GitFilemode? filemode; + + @override + List get props => [path, oid, filemode]; +} + /// A TreeEntry represents a single entry in a Git tree, which can be either /// a file (blob) or a subdirectory (tree). /// @@ -192,6 +274,35 @@ class TreeEntry extends Equatable { return GitObject.fromValue(type.value); } + /// Converts this tree entry to the object it points to. + Object toObject(Repository repo) { + final type = bindings.entryType(_treeEntryPointer); + final object = bindings.entryToObject( + repoPointer: repo.pointer, + entryPointer: _treeEntryPointer, + ); + + if (type.value == GitObject.commit.value) { + return Commit(object.cast()); + } else if (type.value == GitObject.tree.value) { + return Tree(object.cast()); + } else if (type.value == GitObject.blob.value) { + return Blob(object.cast()); + } else if (type.value == GitObject.tag.value) { + return Tag(object.cast()); + } else { + throw ArgumentError('Unsupported tree entry target type: ${type.value}'); + } + } + + /// Compares this tree entry with [other] for tree ordering. + int compareTo(TreeEntry other) { + return bindings.entryCompare( + aPointer: _treeEntryPointer, + bPointer: other._treeEntryPointer, + ); + } + /// Releases memory allocated for tree entry object. /// /// **IMPORTANT**: Only tree entries looked up by path should be freed. diff --git a/pubspec.yaml b/pubspec.yaml index 03f8a81..3ac89ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: git2dart description: Dart bindings to libgit2, provides ability to use libgit2 library in Dart and Flutter. repository: https://github.com/DartGit-dev/git2dart -version: 0.5.1 +version: 0.5.2 environment: sdk: ">=3.7.2 <4.0.0" @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter - git2dart_binaries: ">=1.11.2 <1.12.0" + git2dart_binaries: ">=1.11.4 <1.12.0" meta: ^1.8.0 path: ^1.8.1 diff --git a/test/branch_test.dart b/test/branch_test.dart index cc76a4f..50c3685 100644 --- a/test/branch_test.dart +++ b/test/branch_test.dart @@ -87,6 +87,11 @@ void main() { ); }); + test('checks if branch name is valid', () { + expect(Branch.isNameValid('feature/new-api'), true); + expect(Branch.isNameValid('feature..new-api'), false); + }); + test('checks if branch is current head', () { expect(Branch.lookup(repo: repo, name: 'master').isHead, true); expect(Branch.lookup(repo: repo, name: 'feature').isHead, false); diff --git a/test/commit_test.dart b/test/commit_test.dart index 6ce0797..b8bd924 100644 --- a/test/commit_test.dart +++ b/test/commit_test.dart @@ -473,6 +473,56 @@ Some description. ); }); + test('returns raw message and header', () { + final commit = Commit.lookup(repo: repo, oid: tip); + + expect(commit.messageRaw, commit.message); + expect(commit.rawHeader, contains('tree ${commit.treeOid.sha}')); + expect(commit.rawHeader, contains('parent ${commit.parents.first.sha}')); + expect(commit.rawHeader, contains('author ')); + expect(commit.rawHeader, contains('committer ')); + }); + + test('returns author and committer resolved with mailmap', () { + final mappedAuthor = Signature.create( + name: 'nick1', + email: 'bugs@company.xx', + time: 123, + ); + final mappedCommitter = Signature.create( + name: 'nick2', + email: 'bugs@company.xx', + time: 124, + ); + final oid = Commit.create( + repo: repo, + updateRef: 'HEAD', + message: message, + author: mappedAuthor, + committer: mappedCommitter, + tree: tree, + parents: [Commit.lookup(repo: repo, oid: tip)], + ); + final commit = Commit.lookup(repo: repo, oid: oid); + final mailmap = Mailmap.fromBuffer(''' +Some Dude nick1 +Other Author nick2 +'''); + + expect( + commit.authorWithMailmap(mailmap), + Signature.create(name: 'Some Dude', email: 'some@dude.xx', time: 123), + ); + expect( + commit.committerWithMailmap(mailmap), + Signature.create( + name: 'Other Author', + email: 'other@author.xx', + time: 124, + ), + ); + }); + test('throws when header field not found', () { final commit = Commit.lookup(repo: repo, oid: tip); expect( diff --git a/test/config_test.dart b/test/config_test.dart index 8496e4d..abb5e9b 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -1,6 +1,8 @@ +import 'dart:ffi'; import 'dart:io'; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/config.dart' as bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -41,6 +43,12 @@ void main() { expect(config, isA()); }); + test('creates empty config', () { + final config = Config.empty(); + expect(config, isA()); + expect(config.toList(), isEmpty); + }); + test('opens the global, XDG and system configuration files ' '(if they are present) if no path provided', () { try { @@ -92,6 +100,105 @@ void main() { } }); + group('binding iteration helpers', () { + late Pointer configPointer; + + setUp(() { + configPointer = bindings.open(filePath); + }); + + tearDown(() { + bindings.free(configPointer); + }); + + test('returns config entries using foreach callback', () { + final entries = bindings.foreachEntries(configPointer); + + expect(entries.map((entry) => entry['name']), expectedEntries); + expect(entries.first['value'], '0'); + expect(entries.first['level'], GitConfigLevel.local.value); + }); + + test('returns config entries using foreach match callback', () { + final entries = bindings.foreachMatchEntries( + configPointer: configPointer, + regexp: r'^remote\.', + ); + + expect(entries.single['name'], 'remote.origin.url'); + expect(entries.single['value'], 'someurl'); + }); + + test('returns config entries using glob iterator', () { + final entries = bindings.globEntries( + configPointer: configPointer, + regexp: r'^core\.', + ); + + expect(entries.map((entry) => entry['name']), [ + 'core.repositoryformatversion', + 'core.bare', + 'core.gitproxy', + 'core.gitproxy', + ]); + }); + + test('returns multivar values using foreach callback', () { + expect( + bindings.multivarValuesForeach( + configPointer: configPointer, + variable: 'core.gitproxy', + ), + ['proxy-command for kernel.org', 'default-proxy'], + ); + expect( + bindings.multivarValuesForeach( + configPointer: configPointer, + variable: 'not.there', + ), + [], + ); + }); + + test('maps config values to integer constants', () { + const maps = [ + bindings.ConfigMapSpec( + type: git_configmap_t.GIT_CONFIGMAP_FALSE, + value: 0, + ), + bindings.ConfigMapSpec( + type: git_configmap_t.GIT_CONFIGMAP_TRUE, + value: 1, + ), + bindings.ConfigMapSpec( + type: git_configmap_t.GIT_CONFIGMAP_STRING, + match: 'input', + value: 2, + ), + ]; + + bindings.setString( + configPointer: configPointer, + variable: 'core.autocrlf', + value: 'input', + ); + + expect( + bindings.getMapped( + configPointer: configPointer, + name: 'core.autocrlf', + maps: maps, + ), + 2, + ); + expect(bindings.lookupMapValue(maps: maps, value: 'true'), 1); + }); + + test('locks and unlocks config backend', () { + expect(() => bindings.lock(configPointer), returnsNormally); + }); + }); + group('get value', () { test('returns value of variable', () { expect(config['core.bare'].value, 'false'); @@ -138,6 +245,11 @@ void main() { expect(config['core.repositoryformatversion'].value, '1'); }); + test('sets 32-bit integer value for provided variable', () { + config.setInt32('core.repositoryformatversion', 1); + expect(config.getInt32('core.repositoryformatversion'), 1); + }); + test('sets string value for provided variable', () { config['remote.origin.url'] = 'updated'; expect(config['remote.origin.url'].value, 'updated'); diff --git a/test/diff_test.dart b/test/diff_test.dart index 3faf3ee..3564805 100644 --- a/test/diff_test.dart +++ b/test/diff_test.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/diff.dart' as bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -368,7 +369,7 @@ index e69de29..c217c63 100644 expect(file.readAsStringSync(), ''); Diff.parse(patchText).apply(repo: repo); - expect(file.readAsStringSync(), 'Modified content\n'); + expect(file.readAsLinesSync(), ['Modified content']); }); test('throws when trying to apply diff and error occurs', () { @@ -395,7 +396,7 @@ index e69de29..c217c63 100644 expect(file.readAsStringSync(), ''); diff.apply(repo: repo, hunkIndex: hunk.index); - expect(file.readAsStringSync(), 'Modified content\n'); + expect(file.readAsLinesSync(), ['Modified content']); }); test('does not apply hunk with non existing index', () { @@ -561,6 +562,83 @@ index e69de29..c217c63 100644 expect(Diff.parse(patchText).patch, patchText); }); + test('prints diff through callback', () { + final diff = Diff.parse(patchText); + + expect(bindings.print(diff.pointer), patchText); + }); + + test('iterates diff through callbacks', () { + final diff = Diff.parse(patchText); + final result = bindings.foreach(diff.pointer); + + expect(result['paths'], ['subdir/modified_file']); + expect( + result['origins'], + contains(git_diff_line_t.GIT_DIFF_LINE_ADDITION.value), + ); + expect(result['text'], '+Modified content\n'); + }); + + test('diffs buffers through callbacks', () { + expect( + bindings.buffers( + oldBuffer: '', + oldAsPath: 'feature_file', + newBuffer: 'Feature edit\n', + newAsPath: 'feature_file', + flags: git_diff_option_t.GIT_DIFF_NORMAL.value, + contextLines: 3, + interhunkLines: 0, + ), + '+Feature edit\n', + ); + }); + + test('diffs blobs through callbacks', () { + final oldBlob = Blob.lookup( + repo: repo, + oid: repo['e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'], + ); + final newBlob = Blob.lookup( + repo: repo, + oid: Blob.create(repo: repo, content: 'Feature edit\n'), + ); + + expect( + bindings.blobs( + oldBlobPointer: oldBlob.pointer, + oldAsPath: 'feature_file', + newBlobPointer: newBlob.pointer, + newAsPath: 'feature_file', + flags: git_diff_option_t.GIT_DIFF_NORMAL.value, + contextLines: 3, + interhunkLines: 0, + ), + '+Feature edit\n', + ); + }); + + test('diffs blob to buffer through callbacks', () { + final oldBlob = Blob.lookup( + repo: repo, + oid: repo['e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'], + ); + + expect( + bindings.blobToBuffer( + oldBlobPointer: oldBlob.pointer, + oldAsPath: 'feature_file', + buffer: 'Feature edit\n', + bufferAsPath: 'feature_file', + flags: git_diff_option_t.GIT_DIFF_NORMAL.value, + contextLines: 3, + interhunkLines: 0, + ), + '+Feature edit\n', + ); + }); + test('manually releases allocated memory', () { final diff = Diff.parse(patchText); expect(() => diff.free(), returnsNormally); diff --git a/test/helpers/util.dart b/test/helpers/util.dart index 8a001f5..b5b2c43 100644 --- a/test/helpers/util.dart +++ b/test/helpers/util.dart @@ -24,12 +24,34 @@ void copyRepo({required Directory from, required Directory to}) { copyRepo(from: entity.absolute, to: newDir); } else if (entity is File) { if (p.basename(entity.path) == 'gitignore') { - entity.copySync(p.join(to.path, '.gitignore')); + _copyFixtureFile(entity, File(p.join(to.path, '.gitignore'))); } else if (p.basename(entity.path) == 'gitattributes') { - entity.copySync(p.join(to.path, '.gitattributes')); + _copyFixtureFile(entity, File(p.join(to.path, '.gitattributes'))); } else { - entity.copySync(p.join(to.path, p.basename(entity.path))); + _copyFixtureFile( + entity, + File(p.join(to.path, p.basename(entity.path))), + ); } } } } + +void _copyFixtureFile(File source, File destination) { + final bytes = source.readAsBytesSync(); + if (bytes.contains(0)) { + destination.writeAsBytesSync(bytes); + return; + } + + final normalized = []; + for (var i = 0; i < bytes.length; i++) { + if (bytes[i] == 13 && i + 1 < bytes.length && bytes[i + 1] == 10) { + normalized.add(10); + i++; + } else { + normalized.add(bytes[i]); + } + } + destination.writeAsBytesSync(normalized); +} diff --git a/test/index_test.dart b/test/index_test.dart index c3cb541..1d4330e 100644 --- a/test/index_test.dart +++ b/test/index_test.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/index.dart' as bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -40,21 +41,30 @@ void main() { group('capabilities', () { test('returns index capabilities', () { - expect(index.capabilities, isEmpty); + final capabilities = index.capabilities; + expect( + capabilities.difference({ + GitIndexCapability.ignoreCase, + GitIndexCapability.noFileMode, + GitIndexCapability.noSymlinks, + }), + isEmpty, + ); + expect(capabilities, isNot(contains(GitIndexCapability.fromOwner))); }); test('sets index capabilities', () { - expect(index.capabilities, isEmpty); - index.capabilities = { GitIndexCapability.ignoreCase, GitIndexCapability.noSymlinks, }; - expect(index.capabilities, { - GitIndexCapability.ignoreCase, - GitIndexCapability.noSymlinks, - }); + expect(index.capabilities, contains(GitIndexCapability.ignoreCase)); + expect(index.capabilities, contains(GitIndexCapability.noSymlinks)); + expect( + index.capabilities, + isNot(contains(GitIndexCapability.fromOwner)), + ); }); test('throws when trying to set index capabilities and error occurs', () { @@ -69,6 +79,15 @@ void main() { expect(index.length, 4); }); + test('returns index paths using native iterator', () { + expect(bindings.iteratorPaths(index.pointer), [ + '.gitignore', + 'dir/dir_file.txt', + 'feature_file', + 'file', + ]); + }); + test('returns mode of index entry', () { for (final entry in index) { expect(entry.mode, GitFilemode.blob); @@ -334,6 +353,92 @@ void main() { expect(() => Index(nullptr).addConflict(), throwsA(isA())); }); + test('adds and clears filename conflict entries', () { + expect(index.nameEntries, isEmpty); + + index.addNameEntry( + ancestor: 'ancestor.txt', + ours: 'ours.txt', + theirs: 'theirs.txt', + ); + + final entry = index.nameEntries.single; + expect(entry.ancestor, 'ancestor.txt'); + expect(entry.ours, 'ours.txt'); + expect(entry.theirs, 'theirs.txt'); + expect(entry.toString(), contains('IndexNameEntry{')); + + index.clearNameEntries(); + expect(index.nameEntries, isEmpty); + }); + + test( + 'throws when trying to read filename conflict entry out of bounds', + () { + expect(index.nameEntries, isEmpty); + expect( + () => + bindings.nameGetByIndex(indexPointer: index.pointer, position: 0), + throwsA(isA()), + ); + }, + ); + + test('adds, finds, removes and clears resolve undo entries', () { + final ancestor = index['file']; + final ours = index['file']; + final theirs = index['feature_file']; + + expect(index.reucEntries, isEmpty); + + index.addReucEntry( + path: 'resolved_file', + ancestorMode: ancestor.mode, + ancestorOid: ancestor.oid, + ourMode: ours.mode, + ourOid: ours.oid, + theirMode: theirs.mode, + theirOid: theirs.oid, + ); + + expect(index.findReuc('resolved_file'), 0); + + final entry = index.reucEntry('resolved_file'); + expect(entry.path, 'resolved_file'); + expect(entry.ancestorMode, ancestor.mode); + expect(entry.ancestorOid, ancestor.oid); + expect(entry.ourMode, ours.mode); + expect(entry.ourOid, ours.oid); + expect(entry.theirMode, theirs.mode); + expect(entry.theirOid, theirs.oid); + expect(entry.toString(), contains('IndexReucEntry{')); + expect(index.reucEntries, [entry]); + + index.removeReucEntry(0); + expect(index.reucEntries, isEmpty); + + index.addReucEntry( + path: 'resolved_file', + ancestorMode: ancestor.mode, + ancestorOid: ancestor.oid, + ourMode: ours.mode, + ourOid: ours.oid, + theirMode: theirs.mode, + theirOid: theirs.oid, + ); + index.clearReucEntries(); + expect(index.reucEntries, isEmpty); + }); + + test('throws when trying to read missing resolve undo entry', () { + expect(() => index.findReuc('not-there'), throwsA(isA())); + expect(() => index.reucEntry('not-there'), throwsA(isA())); + expect( + () => bindings.reucGetByIndex(indexPointer: index.pointer, position: 0), + throwsA(isA()), + ); + }); + test('returns conflicts with ancestor, our and their present', () { final repoDir = setupRepo(Directory(mergeRepoPath)); final conflictRepo = Repository.open(repoDir.path); @@ -391,6 +496,36 @@ void main() { repoDir.deleteSync(recursive: true); }); + test('returns conflict by path', () { + final repoDir = setupRepo(Directory(mergeRepoPath)); + final conflictRepo = Repository.open(repoDir.path); + + conflictRepo.reset( + oid: conflictRepo.head.target, + resetType: GitReset.hard, + ); + + Merge.commit( + repo: conflictRepo, + commit: AnnotatedCommit.lookup( + repo: conflictRepo, + oid: + Branch.lookup(repo: conflictRepo, name: 'conflict-branch').target, + ), + ); + + final conflictedFile = conflictRepo.index.conflict('conflict_file'); + expect(conflictedFile.ancestor, isNull); + expect(conflictedFile.our?.path, 'conflict_file'); + expect(conflictedFile.their?.path, 'conflict_file'); + + repoDir.deleteSync(recursive: true); + }); + + test('throws when conflict path is not found', () { + expect(() => index.conflict('not-there'), throwsA(isA())); + }); + test('returns conflicts with ancestor and their present and null our', () { final repoDir = setupRepo(Directory(mergeRepoPath)); final conflictRepo = Repository.open(repoDir.path); diff --git a/test/libgit2_test.dart b/test/libgit2_test.dart index cb5f743..d824740 100644 --- a/test/libgit2_test.dart +++ b/test/libgit2_test.dart @@ -16,6 +16,44 @@ void main() { }); }); + test('returns prerelease and feature backend metadata', () { + expect(Libgit2.prerelease, isNull); + + for (final feature in Libgit2.features) { + expect(Libgit2.featureBackend(feature), isNotEmpty); + } + }); + + test('checks whether object type is loose', () { + expect(Libgit2.objectTypeIsLoose(GitObject.blob), true); + expect(Libgit2.objectTypeIsLoose(GitObject.refDelta), false); + }); + + test('checks whether paths are protected gitfile names', () { + expect( + Libgit2.isGitFile( + path: '.gitignore', + gitfile: GitPathGitFile.gitignore, + ), + true, + ); + expect( + Libgit2.isGitFile( + path: '.gitmodules', + gitfile: GitPathGitFile.gitmodules, + filesystem: GitPathFilesystem.ntfs, + ), + true, + ); + expect( + Libgit2.isGitFile( + path: 'README.md', + gitfile: GitPathGitFile.gitattributes, + ), + false, + ); + }); + test('sets and returns the owner validation setting for repository ' 'directories', () { final oldValue = Libgit2.ownerValidation; diff --git a/test/merge_test.dart b/test/merge_test.dart index d546d6d..a4a2b1c 100644 --- a/test/merge_test.dart +++ b/test/merge_test.dart @@ -87,6 +87,7 @@ void main() { expect(index.hasConflicts, true); expect(index.conflicts.length, 1); expect(repo.state, GitRepositoryState.merge); + expect(repo.mergeHeadOids, [conflictBranch.target.sha]); expect(repo.status, { 'conflict_file': {GitStatus.conflicted}, }); diff --git a/test/note_test.dart b/test/note_test.dart index d85872b..cd132f9 100644 --- a/test/note_test.dart +++ b/test/note_test.dart @@ -57,6 +57,10 @@ void main() { expect(note.oid.sha, notesExpected[1]['oid']); expect(note.message, notesExpected[1]['message']); expect(note.annotatedOid.sha, notesExpected[1]['annotatedOid']); + expect(note.author.name, 'Aleksey Kulikov'); + expect(note.author.email, 'skinny.mind@gmail.com'); + expect(note.committer.name, 'Aleksey Kulikov'); + expect(note.committer.email, 'skinny.mind@gmail.com'); }); test('creates note', () { diff --git a/test/odb_test.dart b/test/odb_test.dart index a003a04..ea0d22c 100644 --- a/test/odb_test.dart +++ b/test/odb_test.dart @@ -45,6 +45,11 @@ void main() { expect(odb.contains(repo[blobSha]), true); }); + test('returns backend count', () { + expect(repo.odb.backendCount, greaterThan(0)); + expect(Odb.create().backendCount, 0); + }); + test('reads object', () { final object = repo.odb.read(repo[blobSha]); @@ -68,6 +73,14 @@ void main() { expect(object.dataBytes, bytes); }); + test('duplicates object', () { + final object = repo.odb.read(repo[blobSha]); + final duplicate = object.duplicate(); + + expect(duplicate, object); + expect(duplicate.data, object.data); + }); + test('throws when trying to read object and error occurs', () { expect(() => repo.odb.read(repo['0' * 40]), throwsA(isA())); }); @@ -95,6 +108,26 @@ void main() { expect(object.data, 'testing'); }); + test('writes data directly', () { + final odb = repo.odb; + final oid = odb.writeDirect(type: GitObject.blob, data: 'direct'); + final object = odb.read(oid); + + expect(odb.contains(oid), true); + expect(object.data, 'direct'); + }); + + test('hashes data and files', () { + final file = File(p.join(repo.workdir, 'feature_file')) + ..writeAsStringSync(blobContent); + + expect(Odb.hash(type: GitObject.blob, data: blobContent), repo[blobSha]); + expect( + Odb.hashFile(type: GitObject.blob, path: file.path), + repo[blobSha], + ); + }); + test('throws when trying to write with invalid object type', () { expect( () => repo.odb.write(type: GitObject.any, data: 'testing'), @@ -102,6 +135,13 @@ void main() { ); }); + test('throws when trying to hash with invalid object type', () { + expect( + () => Odb.hash(type: GitObject.any, data: 'testing'), + throwsA(isA()), + ); + }); + test('throws when trying to write alternate odb to disk', () { final odb = Odb.create(); odb.addDiskAlternate(p.join(repo.path, 'objects')); diff --git a/test/oid_test.dart b/test/oid_test.dart index ae6910f..9d2910b 100644 --- a/test/oid_test.dart +++ b/test/oid_test.dart @@ -51,6 +51,20 @@ void main() { }); }); + group('fromSHAParse()', () { + test('initializes from partial hex string with zero-filled suffix', () { + final oid = Oid.fromSHAParse('78b8'); + expect(oid.sha, '78b8${'0' * 36}'); + }); + + test('throws when sha hex string is invalid', () { + expect( + () => Oid.fromSHAParse('not-a-sha'), + throwsA(isA()), + ); + }); + }); + group('fromRaw()', () { test('initializes successfully', () { final sourceOid = Oid.fromSHA(repo, sha); @@ -64,6 +78,7 @@ void main() { final oid = Oid.fromSHA(repo, sha); expect(oid.sha, equals(sha)); expect(oid.toStr(41), equals(sha)); + expect(oid.toStrN(7), equals(sha.substring(0, 7))); expect(oid.toStrS(), equals(sha)); expect(oid.equalsHex(sha), true); expect(oid.compareToHex(biggerSha), lessThan(0)); diff --git a/test/patch_test.dart b/test/patch_test.dart index 2111fdd..add80b9 100644 --- a/test/patch_test.dart +++ b/test/patch_test.dart @@ -66,6 +66,7 @@ index e69de29..0000000 expect(patch.size(), 14); expect(patch.text, blobPatch); + expect(patch.printedText, blobPatch); expect(patch.textBytes, utf8.encode(blobPatch)); }); @@ -102,6 +103,25 @@ index e69de29..0000000 expect(patch.text, blobPatch); }); + test('creates from blobs with separate path names', () { + final patch = Patch.fromBlobs( + oldBlob: Blob.lookup(repo: repo, oid: oldBlobOid), + newBlob: Blob.lookup(repo: repo, oid: newBlobOid), + oldBlobPath: 'old_feature_file', + newBlobPath: 'new_feature_file', + ); + + expect( + patch.text, + contains( + 'diff --git a/old_feature_file ' + 'b/new_feature_file', + ), + ); + expect(patch.text, contains('--- a/old_feature_file')); + expect(patch.text, contains('+++ b/new_feature_file')); + }); + test('creates from one blob (add)', () { final patch = Patch.fromBlobs( oldBlob: null, @@ -135,6 +155,25 @@ index e69de29..0000000 expect(patch.text, blobPatch); }); + test('creates from blob and buffer with separate path names', () { + final patch = Patch.fromBlobAndBuffer( + blob: Blob.lookup(repo: repo, oid: oldBlobOid), + buffer: newBuffer, + blobPath: 'old_feature_file', + bufferPath: 'new_feature_file', + ); + + expect( + patch.text, + contains( + 'diff --git a/old_feature_file ' + 'b/new_feature_file', + ), + ); + expect(patch.text, contains('--- a/old_feature_file')); + expect(patch.text, contains('+++ b/new_feature_file')); + }); + test('creates from empty blob and buffer', () { final patch = Patch.fromBlobAndBuffer( blob: null, diff --git a/test/reference_test.dart b/test/reference_test.dart index 16a0472..7c02686 100644 --- a/test/reference_test.dart +++ b/test/reference_test.dart @@ -13,6 +13,7 @@ void main() { late Directory tmpDir; const lastCommit = '821ed6e80627b8769d170a293862f9fc60825226'; const newCommit = 'c68ff54aabf660fcdd9a2838d401583fe31249e3'; + const featureCommit = '5aecfa0fb97eadaac050ccb99f03c3fb65460ad4'; setUp(() { tmpDir = setupRepo(Directory(p.join('test', 'assets', 'test_repo'))); @@ -37,6 +38,28 @@ void main() { ]); }); + test('returns a list with callback iterators', () { + final expected = Reference.list(repo); + + expect(Reference.listForeach(repo), unorderedEquals(expected)); + expect(Reference.listForeachName(repo), unorderedEquals(expected)); + expect( + Reference.listForeachGlob(repo: repo, glob: 'refs/tags/*'), + unorderedEquals(['refs/tags/v0.1', 'refs/tags/v0.2']), + ); + }); + + test('returns a list with reference iterators', () { + final expected = Reference.list(repo); + + expect(Reference.listIterator(repo), unorderedEquals(expected)); + expect(Reference.listIteratorNames(repo), unorderedEquals(expected)); + expect( + Reference.listIteratorGlob(repo: repo, glob: 'refs/tags/*'), + unorderedEquals(['refs/tags/v0.1', 'refs/tags/v0.2']), + ); + }); + test('throws when trying to get a list of references and error occurs', () { expect( () => Reference.list(Repository(nullptr)), @@ -52,6 +75,15 @@ void main() { ); }); + test('compares references', () { + final feature = Reference.lookup(repo: repo, name: 'refs/heads/feature'); + final master = Reference.lookup(repo: repo, name: 'refs/heads/master'); + + expect(feature.compareTo(master), lessThan(0)); + expect(master.compareTo(feature), greaterThan(0)); + expect(master.compareTo(master), 0); + }); + test('returns SHA hex of direct reference', () { expect(repo.head.target.sha, lastCommit); }); @@ -60,6 +92,27 @@ void main() { expect(Reference.lookup(repo: repo, name: 'HEAD').target.sha, lastCommit); }); + test('returns peeled target for annotated tag reference', () { + final signature = Signature.create( + name: 'Tagger', + email: 'tagger@example.com', + time: 1234, + ); + Tag.createAnnotated( + repo: repo, + tagName: 'peeled', + target: repo[lastCommit], + targetType: GitObject.commit, + tagger: signature, + message: 'message', + ); + Reference.compress(repo); + + final tagRef = Reference.lookup(repo: repo, name: 'refs/tags/peeled'); + expect(tagRef.peeledTarget?.sha, lastCommit); + expect(repo.head.peeledTarget, isNull); + }); + test('returns the full name', () { expect(repo.head.name, 'refs/heads/master'); }); @@ -236,6 +289,43 @@ void main() { throwsA(isA()), ); }); + + test('creates matching reference when current target matches', () { + Reference.create( + repo: repo, + name: 'refs/tags/matching', + target: repo[lastCommit], + ); + + final ref = Reference.createMatching( + repo: repo, + name: 'refs/tags/matching', + target: repo[newCommit], + currentTarget: repo[lastCommit], + force: true, + ); + + expect(ref.target.sha, newCommit); + }); + + test('throws when matching reference current target differs', () { + Reference.create( + repo: repo, + name: 'refs/tags/matching', + target: repo[lastCommit], + ); + + expect( + () => Reference.createMatching( + repo: repo, + name: 'refs/tags/matching', + target: repo[newCommit], + currentTarget: repo[newCommit], + force: true, + ), + throwsA(isA()), + ); + }); }); group('create symbolic', () { @@ -312,6 +402,59 @@ void main() { expect(reflogEntry.committer.name, 'name'); expect(reflogEntry.committer.email, 'email'); }); + + test('creates matching reference when current target matches', () { + Reference.create( + repo: repo, + name: 'refs/tags/symbolic.matching', + target: 'refs/heads/master', + ); + + final ref = Reference.createMatching( + repo: repo, + name: 'refs/tags/symbolic.matching', + target: 'refs/heads/feature', + currentTarget: 'refs/heads/master', + force: true, + ); + + expect(ref.type, ReferenceType.symbolic); + expect(ref.target.sha, featureCommit); + }); + + test( + 'throws when matching symbolic reference current target differs', + () { + Reference.create( + repo: repo, + name: 'refs/tags/symbolic.matching', + target: 'refs/heads/master', + ); + + expect( + () => Reference.createMatching( + repo: repo, + name: 'refs/tags/symbolic.matching', + target: 'refs/heads/feature', + currentTarget: 'refs/heads/feature', + force: true, + ), + throwsA(isA()), + ); + }, + ); + + test('throws when matching target types differ', () { + expect( + () => Reference.createMatching( + repo: repo, + name: 'refs/tags/symbolic.matching', + target: 'refs/heads/feature', + currentTarget: repo.head.target, + ), + throwsA(isA()), + ); + }); }); test('deletes reference', () { @@ -320,6 +463,18 @@ void main() { expect(repo.references, isNot(contains('refs/tags/v0.1'))); }); + test('removes reference by name', () { + Reference.create( + repo: repo, + name: 'refs/tags/test', + target: repo[lastCommit], + ); + + expect(repo.references, contains('refs/tags/test')); + Reference.remove(repo: repo, name: 'refs/tags/test'); + expect(repo.references, isNot(contains('refs/tags/test'))); + }); + group('finds', () { test('with provided name', () { final ref = Reference.lookup(repo: repo, name: 'refs/heads/master'); diff --git a/test/remote_test.dart b/test/remote_test.dart index 24cd759..6495c38 100644 --- a/test/remote_test.dart +++ b/test/remote_test.dart @@ -35,6 +35,7 @@ void main() { expect(remote.name, remoteName); expect(remote.url, remoteUrl); expect(remote.pushUrl, ''); + expect(remote.autotag, GitRemoteAutotag.auto); expect(remote.toString(), contains('Remote{')); expect(remote, equals(Remote.lookup(repo: repo, name: 'origin'))); }); @@ -170,6 +171,38 @@ void main() { expect(Remote.lookup(repo: repo, name: remoteName).pushUrl, newUrl); }); + test('sets instance urls without changing configuration', () { + final remote = Remote.lookup(repo: repo, name: remoteName); + const instanceUrl = 'git://instance/url.git'; + const instancePushUrl = 'git://instance/push-url.git'; + + remote.url = instanceUrl; + remote.pushUrl = instancePushUrl; + + expect(remote.url, instanceUrl); + expect(remote.pushUrl, instancePushUrl); + expect(Remote.lookup(repo: repo, name: remoteName).url, remoteUrl); + expect(Remote.lookup(repo: repo, name: remoteName).pushUrl, ''); + }); + + test('sets automatic tag following option', () { + expect( + Remote.lookup(repo: repo, name: remoteName).autotag, + GitRemoteAutotag.auto, + ); + + Remote.setAutotag( + repo: repo, + remote: remoteName, + value: GitRemoteAutotag.none, + ); + + expect( + Remote.lookup(repo: repo, name: remoteName).autotag, + GitRemoteAutotag.none, + ); + }); + test('throws when trying to set invalid push url name', () { expect( () => Remote.setPushUrl(repo: repo, remote: 'origin', url: ''), diff --git a/test/repository_test.dart b/test/repository_test.dart index 77dc74f..5b133a7 100644 --- a/test/repository_test.dart +++ b/test/repository_test.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/status.dart' as status_bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -41,6 +42,36 @@ void main() { expect(bare.isBare, true); }); + test('initializes repository with basic initializer', () { + final repoPath = p.join(tmpDir.path, 'basic_repo'); + final initialized = Repository.initBasic(path: repoPath); + + expect(initialized.isBare, false); + expect(Directory(p.join(repoPath, '.git')).existsSync(), true); + }); + + test('opens repository with extended search options', () { + final nestedPath = p.join(tmpDir.path, 'dir'); + final opened = Repository.openExt(path: nestedPath); + + expect(opened.path, repo.path); + expect(GitRepositoryOpen.fromValue(1), GitRepositoryOpen.noSearch); + expect( + () => GitRepositoryOpen.fromValue(0), + throwsA(isA()), + ); + }); + + test('throws when extended open cannot find repository', () { + expect( + () => Repository.openExt( + path: p.join(tmpDir.path, 'dir'), + flags: {GitRepositoryOpen.noSearch}, + ), + throwsA(isA()), + ); + }); + test('returns snapshot of repository config', () { expect( repo.configSnapshot['remote.origin.url'].value, @@ -48,6 +79,27 @@ void main() { ); }); + test('sets repository config, index and odb objects', () { + final config = repo.config; + final index = repo.index; + final odb = repo.odb; + + expect(() => repo.setConfig(config), returnsNormally); + expect(() => repo.setIndex(index), returnsNormally); + expect(() => repo.setOdb(odb), returnsNormally); + }); + + test('throws when setting repository config, index and odb fails', () { + final invalid = Repository(nullptr); + + expect( + () => invalid.setConfig(repo.config), + throwsA(isA()), + ); + expect(() => invalid.setIndex(repo.index), throwsA(isA())); + expect(() => invalid.setOdb(repo.odb), throwsA(isA())); + }); + test('returns list of commits by walking from provided starting oid', () { const log = [ '821ed6e80627b8769d170a293862f9fc60825226', @@ -169,6 +221,30 @@ void main() { expect(repo.isHeadDetached, true); }); + test('detaches head from annotated commit', () { + final annotated = AnnotatedCommit.lookup( + repo: repo, + oid: repo[featureCommit], + ); + + repo.setHeadDetachedFromAnnotated(annotated); + + expect(repo.isHeadDetached, true); + expect(repo.head.target, repo[featureCommit]); + }); + + test('throws when detaching head from annotated commit fails', () { + final annotated = AnnotatedCommit.lookup( + repo: repo, + oid: repo.head.target, + ); + + expect( + () => Repository(nullptr).setHeadDetachedFromAnnotated(annotated), + throwsA(isA()), + ); + }); + test('returns repository oid type and hashes a file', () { final oid = repo.hashFile(path: p.join(repo.workdir, 'file')); @@ -186,6 +262,37 @@ void main() { }); }); + test('returns status entries through callback APIs', () { + File(p.join(tmpDir.path, 'new_file.txt')).createSync(); + repo.index.remove('file'); + repo.index.add('new_file.txt'); + + final foreachStatus = status_bindings.foreach(repo.pointer); + final foreachExtStatus = status_bindings.foreachExt(repo.pointer); + + expect(foreachStatus.keys, containsAll(['file', 'new_file.txt'])); + expect(foreachExtStatus.keys, containsAll(['file', 'new_file.txt'])); + expect( + foreachStatus['file']! & GitStatus.indexDeleted.value, + GitStatus.indexDeleted.value, + ); + expect( + foreachExtStatus['new_file.txt']! & GitStatus.indexNew.value, + GitStatus.indexNew.value, + ); + }); + + test('throws when status callback APIs fail', () { + expect( + () => status_bindings.foreach(Repository(nullptr).pointer), + throwsA(isA()), + ); + expect( + () => status_bindings.foreachExt(Repository(nullptr).pointer), + throwsA(isA()), + ); + }); + test('throws when trying to get status of bare repository', () { final bare = Repository.open(p.join('test', 'assets', 'empty_bare.git')); @@ -204,6 +311,11 @@ void main() { expect(repo.state, GitRepositoryState.none); }); + test('returns empty fetch and merge head lists when files are absent', () { + expect(repo.fetchHeadEntries, isEmpty); + expect(repo.mergeHeadOids, isEmpty); + }); + test('throws when trying to clean up state and error occurs', () { expect( () => Repository(nullptr).stateCleanup(), diff --git a/test/revwalk_test.dart b/test/revwalk_test.dart index 0aece1b..a2192b5 100644 --- a/test/revwalk_test.dart +++ b/test/revwalk_test.dart @@ -199,6 +199,16 @@ void main() { ); }); + test('hides commits matching predicate', () { + final walker = RevWalk(repo); + + walker.push(repo[log.first]); + walker.hideWhere((oid) => oid.sha == log[2]); + final commits = walker.walk(); + + expect(commits.map((e) => e.oid.sha), isNot(contains(log[2]))); + }); + test('resets walker', () { final walker = RevWalk(repo); diff --git a/test/signature_test.dart b/test/signature_test.dart index fad17df..56f7d56 100644 --- a/test/signature_test.dart +++ b/test/signature_test.dart @@ -1,7 +1,13 @@ +import 'dart:ffi'; +import 'dart:io'; + import 'package:git2dart/git2dart.dart'; import 'package:git2dart_binaries/git2dart_binaries.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; +import 'helpers/util.dart'; + void main() { late Signature signature; const name = 'Some Name'; @@ -46,6 +52,34 @@ void main() { expect(sig.sign, isNotEmpty); }); + test( + 'creates default author and committer signatures from environment', + () { + final tmpDir = setupRepo( + Directory(p.join('test', 'assets', 'test_repo')), + ); + final repo = Repository.open(tmpDir.path); + repo.config['user.name'] = 'Config User'; + repo.config['user.email'] = 'config@example.com'; + + final signatures = Signature.defaultSignaturesFromEnv(repo); + + expect(signatures, hasLength(2)); + expect(signatures[0].name, 'Config User'); + expect(signatures[0].email, 'config@example.com'); + expect(signatures[1].name, 'Config User'); + expect(signatures[1].email, 'config@example.com'); + tmpDir.deleteSync(recursive: true); + }, + ); + + test('throws when default signatures cannot be found', () { + expect( + () => Signature.defaultSignaturesFromEnv(Repository(nullptr)), + throwsA(isA()), + ); + }); + test('returns correct values', () { expect(signature.name, name); expect(signature.email, email); diff --git a/test/submodule_test.dart b/test/submodule_test.dart index 0f51cf9..f8af9a8 100644 --- a/test/submodule_test.dart +++ b/test/submodule_test.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:git2dart/git2dart.dart'; +import 'package:git2dart/src/bindings/submodule.dart' as submodule_bindings; import 'package:git2dart_binaries/git2dart_binaries.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -46,6 +47,12 @@ void main() { expect(submodule.workdirOid?.sha, null); expect(submodule.ignoreRule, GitSubmoduleIgnore.none); expect(submodule.updateRule, GitSubmoduleUpdate.checkout); + expect(submodule.fetchRecurseSubmodules, GitSubmoduleRecurse.no); + expect(submodule.location, { + GitSubmoduleStatus.inHead, + GitSubmoduleStatus.inIndex, + GitSubmoduleStatus.inConfig, + }); expect(submodule.toString(), contains('Submodule{')); }); @@ -161,11 +168,13 @@ void main() { expect(submodule.branch, ''); expect(submodule.ignoreRule, GitSubmoduleIgnore.none); expect(submodule.updateRule, GitSubmoduleUpdate.checkout); + expect(submodule.fetchRecurseSubmodules, GitSubmoduleRecurse.no); submodule.url = 'updated'; submodule.branch = 'updated'; submodule.ignoreRule = GitSubmoduleIgnore.all; submodule.updateRule = GitSubmoduleUpdate.rebase; + submodule.fetchRecurseSubmodules = GitSubmoduleRecurse.onDemand; final updatedSubmodule = Submodule.lookup( repo: repo, @@ -175,6 +184,26 @@ void main() { expect(updatedSubmodule.branch, 'updated'); expect(updatedSubmodule.ignoreRule, GitSubmoduleIgnore.all); expect(updatedSubmodule.updateRule, GitSubmoduleUpdate.rebase); + expect( + updatedSubmodule.fetchRecurseSubmodules, + GitSubmoduleRecurse.onDemand, + ); + }); + + test('throws when submodule fetch recurse and location bindings fail', () { + expect( + () => submodule_bindings.setFetchRecurseSubmodules( + repoPointer: Repository(nullptr).pointer, + name: testSubmodule, + fetchRecurseSubmodules: + git_submodule_recurse_t.GIT_SUBMODULE_RECURSE_YES, + ), + throwsA(isA()), + ); + expect( + () => submodule_bindings.location(nullptr), + throwsA(isA()), + ); }); test('syncs', tags: 'remote_fetch', () { diff --git a/test/tag_test.dart b/test/tag_test.dart index b963c3a..99db798 100644 --- a/test/tag_test.dart +++ b/test/tag_test.dart @@ -37,6 +37,16 @@ void main() { ); }); + test('lookups tag from oid prefix', () { + final prefixTag = Tag.lookupPrefix(repo: repo, oid: tagOid, length: 7); + + expect(prefixTag.oid, tagOid); + expect( + () => Tag.lookupPrefix(repo: repo, oid: repo['0' * 40], length: 7), + throwsA(isA()), + ); + }); + test('returns correct values', () { final signature = Signature.create( name: 'Aleksey Kulikov', @@ -55,6 +65,22 @@ void main() { expect(tag.toString(), contains('Tag{')); }); + test('peels tag to target object', () { + final peeled = tag.peel(GitObject.commit); + + expect(peeled, isA()); + expect((peeled as Commit).oid, tag.targetOid); + }); + + test('throws when peeling tag to unavailable type', () { + expect(() => tag.peel(GitObject.blob), throwsA(isA())); + }); + + test('checks if tag name is valid', () { + expect(Tag.isNameValid('v1.0.0'), true); + expect(Tag.isNameValid('v1..0'), false); + }); + test('creates new annotated tag with commit as target', () { final signature = Signature.create( name: 'Author', @@ -86,6 +112,33 @@ void main() { expect(newTagTarget.oid, target); }); + test('creates new annotated tag object without reference', () { + final signature = Signature.create( + name: 'Author', + email: 'author@email.com', + time: 1234, + ); + const tagName = 'annotation-only'; + const targetSHA = 'f17d0d48eae3aa08cecf29128a35e310c97b3521'; + final target = repo[targetSHA]; + const message = 'annotation only tag\n'; + + final oid = Tag.createAnnotation( + repo: repo, + tagName: tagName, + target: target, + targetType: GitObject.commit, + tagger: signature, + message: message, + ); + final annotation = Tag.lookup(repo: repo, oid: oid); + + expect(annotation.name, tagName); + expect(annotation.message, message); + expect(annotation.targetOid.sha, targetSHA); + expect(Tag.list(repo: repo), isNot(contains(tagName))); + }); + test('creates new lightweight tag with commit as target', () { const tagName = 'tag'; final target = repo['f17d0d48eae3aa08cecf29128a35e310c97b3521']; @@ -124,12 +177,14 @@ void main() { final newTag = Tag.lookup(repo: repo, oid: oid); final newTagTarget = newTag.target as Tree; + final peeledTree = newTag.peel(GitObject.tree) as Tree; expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(newTag.tagger, signature); expect(newTagTarget.oid, target); + expect(peeledTree.oid, target); }); test('creates new lightweight tag with tree as target', () { @@ -170,12 +225,14 @@ void main() { final newTag = Tag.lookup(repo: repo, oid: oid); final newTagTarget = newTag.target as Blob; + final peeledBlob = newTag.peel(GitObject.blob) as Blob; expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(newTag.tagger, signature); expect(newTagTarget.oid, target); + expect(peeledBlob.oid, target); }); test('creates new lightweight tag with blob as target', () { @@ -215,12 +272,14 @@ void main() { final newTag = Tag.lookup(repo: repo, oid: oid); final newTagTarget = newTag.target as Tag; + final peeledCommit = newTag.peel() as Commit; expect(newTag.oid, oid); expect(newTag.name, tagName); expect(newTag.message, message); expect(newTag.tagger, signature); expect(newTagTarget.oid, tag.oid); + expect(peeledCommit.oid, tag.targetOid); }); test('creates new lightweight tag with tag as target', () { diff --git a/test/tree_test.dart b/test/tree_test.dart index 429a947..bb5fd2a 100644 --- a/test/tree_test.dart +++ b/test/tree_test.dart @@ -36,6 +36,20 @@ void main() { ); }); + test('lookups tree from oid prefix', () { + final prefixTree = Tree.lookupPrefix( + repo: repo, + oid: tree.oid, + length: 7, + ); + + expect(prefixTree.oid, tree.oid); + expect( + () => Tree.lookupPrefix(repo: repo, oid: repo['0' * 40], length: 7), + throwsA(isA()), + ); + }); + test('returns correct values', () { expect(tree.length, 4); expect(tree.entries.first.oid.sha, fileSHA); @@ -64,6 +78,24 @@ void main() { expect(entry.type, GitObject.blob); }); + test('converts tree entry to object', () { + final entry = tree['.gitignore']; + final object = entry.toObject(repo); + + expect(object, isA()); + expect((object as Blob).oid, entry.oid); + + final dirEntry = tree['dir']; + final dirObject = dirEntry.toObject(repo); + expect(dirObject, isA()); + expect((dirObject as Tree).oid, dirEntry.oid); + }); + + test('compares tree entries', () { + expect(tree[0].compareTo(tree[0]), 0); + expect(tree[0].compareTo(tree[1]), lessThan(0)); + }); + test('throws when nothing found for provided filename', () { expect(() => tree['invalid'], throwsA(isA())); }); @@ -74,6 +106,26 @@ void main() { expect(entry.toString(), contains('TreeEntry{')); }); + test('walks tree entries', () { + expect(GitTreeWalk.fromValue(0), GitTreeWalk.pre); + expect(GitTreeWalk.fromValue(1), GitTreeWalk.post); + expect(() => GitTreeWalk.fromValue(-1), throwsA(isA())); + expect(tree.walk(), [ + '.gitignore', + 'dir', + 'dir/dir_file.txt', + 'feature_file', + 'file', + ]); + expect(tree.walk(mode: GitTreeWalk.post), [ + '.gitignore', + 'dir/dir_file.txt', + 'dir', + 'feature_file', + 'file', + ]); + }); + test('throws when nothing found for provided path', () { expect(() => tree['invalid/path'], throwsA(isA())); }); @@ -100,6 +152,41 @@ void main() { expect(entry.oid, fileOid); }); + test('creates updated tree from baseline', () { + final fileOid = Blob.create(repo: repo, content: 'updated content'); + expect( + const TreeUpdate.remove('feature_file'), + const TreeUpdate.remove('feature_file'), + ); + final updatedTree = Tree.createUpdated( + repo: repo, + baseline: tree, + updates: [ + TreeUpdate.upsert( + path: 'updated.txt', + oid: fileOid, + filemode: GitFilemode.blob, + ), + const TreeUpdate.remove('feature_file'), + ], + ); + + expect(updatedTree['updated.txt'].oid, fileOid); + expect(() => updatedTree['feature_file'], throwsA(isA())); + expect(updatedTree.length, tree.length); + }); + + test('throws when creating updated tree fails', () { + expect( + () => Tree.createUpdated( + repo: repo, + baseline: tree, + updates: const [TreeUpdate.remove('not-there')], + ), + throwsA(isA()), + ); + }); + test('manually releases allocated memory', () { final tree = Tree.lookup(repo: repo, oid: repo['a8ae3dd']); expect(() => tree.free(), returnsNormally); diff --git a/test/worktree_test.dart b/test/worktree_test.dart index 5476a11..f236b6c 100644 --- a/test/worktree_test.dart +++ b/test/worktree_test.dart @@ -98,6 +98,25 @@ void main() { expect(worktree.isLocked, false); }); + test('returns head and detached state for linked worktree', () { + Worktree.create(repo: repo, name: worktreeName, path: worktreeDir.path); + + final head = repo.headForWorktree(worktreeName); + expect(head.name, 'refs/heads/$worktreeName'); + expect(repo.isHeadDetachedForWorktree(worktreeName), isA()); + }); + + test('throws when reading head for unknown worktree', () { + expect( + () => repo.headForWorktree('not-there'), + throwsA(isA()), + ); + expect( + () => repo.isHeadDetachedForWorktree('not-there'), + throwsA(isA()), + ); + }); + test('throws when trying to lookup and error occurs', () { expect( () => Worktree.lookup(repo: Repository(nullptr), name: 'name'),