diff --git a/lib/src/bindings/index.dart b/lib/src/bindings/index.dart index c2f5265..e1221c8 100644 --- a/lib/src/bindings/index.dart +++ b/lib/src/bindings/index.dart @@ -5,6 +5,32 @@ import 'package:libgit2dart/src/bindings/libgit2_bindings.dart'; import 'package:libgit2dart/src/error.dart'; import 'package:libgit2dart/src/util.dart'; +/// Read index capabilities flags. +int capabilities(Pointer index) => libgit2.git_index_caps(index); + +/// Set index capabilities flags. +/// +/// If you pass [GitIndexCapability.fromOwner] for the caps, then capabilities +/// will be read from the config of the owner object, looking at +/// core.ignorecase, core.filemode, core.symlinks. +/// +/// Throws a [LibGit2Error] if error occured. +void setCapabilities({ + required Pointer indexPointer, + required int caps, +}) { + final error = libgit2.git_index_set_caps(indexPointer, caps); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Get the full path to the index file on disk. +String path(Pointer index) { + return libgit2.git_index_path(index).cast().toDartString(); +} + /// Update the contents of an existing index object in memory by reading from /// the hard disk. /// @@ -133,6 +159,10 @@ Pointer getByPath({ } } +/// Return the stage number from a git index entry. +int entryStage(Pointer entry) => + libgit2.git_index_entry_stage(entry); + /// Clear the contents (all the entries) of an index object. /// /// This clears the index object in memory; changes must be explicitly written @@ -194,6 +224,41 @@ void addByPath({ } } +/// Add or update an index entry from a buffer in memory. +/// +/// This method will create a blob in the repository that owns the index and +/// then add the index entry to the index. The path of the entry represents the +/// position of the blob relative to the repository's root folder. +/// +/// If a previous index entry exists that has the same path as the given +/// 'entry', it will be replaced. Otherwise, the 'entry' will be added. +/// +/// This forces the file to be added to the index, not looking at gitignore +/// rules. +/// +/// If this file currently is the result of a merge conflict, this file will no +/// longer be marked as conflicting. The data about the conflict will be moved +/// to the "resolve undo" (REUC) section. +/// +/// Throws a [LibGit2Error] if error occured. +void addFromBuffer({ + required Pointer indexPointer, + required Pointer entryPointer, + required String buffer, +}) { + final bufferC = buffer.toNativeUtf8().cast(); + final error = libgit2.git_index_add_from_buffer( + indexPointer, + entryPointer, + bufferC.cast(), + buffer.length, + ); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + /// Add or update index entries matching files in the working directory. /// /// This method will fail in bare index instances. @@ -239,6 +304,50 @@ void addAll({ } } +/// Update all index entries to match the working directory. +/// +/// This method will fail in bare index instances. +/// +/// This scans the existing index entries and synchronizes them with the +/// working directory, deleting them if the corresponding working directory +/// file no longer exists otherwise updating the information (including adding +/// the latest version of file to the ODB if needed). +/// +/// Throws a [LibGit2Error] if error occured. +void updateAll({ + required Pointer indexPointer, + required List pathspec, +}) { + final pathspecC = calloc(); + final pathPointers = + pathspec.map((e) => e.toNativeUtf8().cast()).toList(); + final strArray = calloc>(pathspec.length); + + for (var i = 0; i < pathspec.length; i++) { + strArray[i] = pathPointers[i]; + } + + pathspecC.ref.strings = strArray; + pathspecC.ref.count = pathspec.length; + + final error = libgit2.git_index_update_all( + indexPointer, + pathspecC, + nullptr, + nullptr, + ); + + calloc.free(pathspecC); + for (final p in pathPointers) { + calloc.free(p); + } + calloc.free(strArray); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + /// Write an existing index object from memory back to disk using an atomic /// file lock. void write(Pointer index) => libgit2.git_index_write(index); @@ -261,6 +370,17 @@ void remove({ } } +/// Remove all entries from the index under a given directory. +void removeDirectory({ + required Pointer indexPointer, + required String dir, + required int stage, +}) { + final dirC = dir.toNativeUtf8().cast(); + libgit2.git_index_remove_directory(indexPointer, dirC, stage); + calloc.free(dirC); +} + /// Remove all matching index entries. void removeAll({ required Pointer indexPointer, @@ -332,6 +452,39 @@ List>> conflictList( return result; } +/// 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) { + return libgit2.git_index_entry_is_conflict(entry) == 1 || false; +} + +/// Add or update index entries to represent a conflict. Any staged entries +/// that exist at the given paths will be removed. +/// +/// The entries are the entries from the tree included in the merge. Any entry +/// may be null to indicate that that file was not present in the trees during +/// the merge. For example, [ancestorEntryPointer] may be null to indicate that +/// a file was added in both branches and must be resolved. +/// +/// Throws a [LibGit2Error] if error occured. +void conflictAdd({ + required Pointer indexPointer, + Pointer? ancestorEntryPointer, + Pointer? ourEntryPointer, + Pointer? theirEntryPointer, +}) { + final error = libgit2.git_index_conflict_add( + indexPointer, + ancestorEntryPointer ?? nullptr, + ourEntryPointer ?? nullptr, + theirEntryPointer ?? nullptr, + ); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + /// Removes the index entries that represent a conflict of a single file. /// /// Throws a [LibGit2Error] if error occured. @@ -349,6 +502,17 @@ void conflictRemove({ } } +/// Remove all conflicts in the index (entries with a stage greater than 0). +/// +/// Throws a [LibGit2Error] if error occured. +void conflictCleanup(Pointer index) { + final error = libgit2.git_index_conflict_cleanup(index); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + /// Get the repository this index relates to. Pointer owner(Pointer index) => libgit2.git_index_owner(index); diff --git a/lib/src/git_types.dart b/lib/src/git_types.dart index b2041ca..8c6d185 100644 --- a/lib/src/git_types.dart +++ b/lib/src/git_types.dart @@ -1704,3 +1704,27 @@ class GitSubmoduleStatus { @override String toString() => 'GitSubmoduleStatus.$_name'; } + +/// Capabilities of system that affect index actions. +class GitIndexCapability { + const GitIndexCapability._(this._value, this._name); + final int _value; + final String _name; + + static const ignoreCase = GitIndexCapability._(1, 'ignoreCase'); + static const noFileMode = GitIndexCapability._(2, 'noFileMode'); + static const noSymlinks = GitIndexCapability._(4, 'noSymlinks'); + static const fromOwner = GitIndexCapability._(-1, 'fromOwner'); + + static const List values = [ + ignoreCase, + noFileMode, + noSymlinks, + fromOwner, + ]; + + int get value => _value; + + @override + String toString() => 'GitIndexCapability.$_name'; +} diff --git a/lib/src/index.dart b/lib/src/index.dart index 10da823..3aaeeab 100644 --- a/lib/src/index.dart +++ b/lib/src/index.dart @@ -19,6 +19,24 @@ class Index with IterableMixin { /// Pointer to memory address for allocated index object. Pointer get pointer => _indexPointer; + /// Full path to the index file on disk. + String get path => bindings.path(_indexPointer); + + /// Index capabilities flags. + Set get capabilities { + final capInt = bindings.capabilities(_indexPointer); + return GitIndexCapability.values + .where((e) => capInt & e.value == e.value) + .toSet(); + } + + set capabilities(Set flags) { + bindings.setCapabilities( + indexPointer: _indexPointer, + caps: flags.fold(0, (acc, e) => acc | e.value), + ); + } + /// Returns index entry located at provided 0-based position or string path. /// /// Throws [RangeError] when provided [value] is outside of valid range or @@ -85,6 +103,39 @@ class Index with IterableMixin { return result; } + /// Adds or updates index entries to represent a conflict. Any staged entries + /// that exist at the given paths will be removed. + /// + /// The entries are the entries from the tree included in the merge. Any entry + /// may be null to indicate that that file was not present in the trees during + /// the merge. For example, [ancestorEntry] may be null to indicate + /// that a file was added in both branches and must be resolved. + /// + /// [ancestorEntry] is the entry data for the ancestor of the conflict. + /// + /// [ourEntry] is the entry data for our side of the merge conflict. + /// + /// [theirEntry] is the entry data for their side of the merge conflict. + /// + /// Throws a [LibGit2Error] if error occured. + void addConflict({ + IndexEntry? ancestorEntry, + IndexEntry? ourEntry, + IndexEntry? theirEntry, + }) { + bindings.conflictAdd( + indexPointer: _indexPointer, + ancestorEntryPointer: ancestorEntry?.pointer, + ourEntryPointer: ourEntry?.pointer, + theirEntryPointer: theirEntry?.pointer, + ); + } + + /// Removes all conflicts in the index (entries with a stage greater than 0). + /// + /// Throws a [LibGit2Error] if error occured. + void cleanupConflict() => bindings.conflictCleanup(_indexPointer); + /// Clears the contents (all the entries) of an index object. /// /// This clears the index object in memory; changes must be explicitly @@ -116,6 +167,31 @@ class Index with IterableMixin { } } + /// Adds or updates an index [entry] from a [buffer] in memory. + /// + /// This method will create a blob in the repository that owns the index and + /// then add the index entry to the index. The path of the entry represents + /// the position of the blob relative to the repository's root folder. + /// + /// If a previous index entry exists that has the same path as the given + /// 'entry', it will be replaced. Otherwise, the 'entry' will be added. + /// + /// This forces the file to be added to the index, not looking at gitignore + /// rules. + /// + /// If this file currently is the result of a merge conflict, this file will + /// no longer be marked as conflicting. The data about the conflict will be + /// moved to the "resolve undo" (REUC) section. + /// + /// Throws a [LibGit2Error] if error occured. + void addFromBuffer({required IndexEntry entry, required String buffer}) { + bindings.addFromBuffer( + indexPointer: _indexPointer, + entryPointer: entry.pointer, + buffer: buffer, + ); + } + /// Adds or updates index entries matching files in the working directory. /// /// This method will fail in bare index instances. @@ -130,6 +206,20 @@ class Index with IterableMixin { bindings.addAll(indexPointer: _indexPointer, pathspec: pathspec); } + /// Updates all index entries to match the working directory. + /// + /// This method will fail in bare index instances. + /// + /// This scans the existing index entries and synchronizes them with the + /// working directory, deleting them if the corresponding working directory + /// file no longer exists otherwise updating the information (including adding + /// the latest version of file to the ODB if needed). + /// + /// Throws a [LibGit2Error] if error occured. + void updateAll(List pathspec) { + bindings.updateAll(indexPointer: _indexPointer, pathspec: pathspec); + } + /// Updates the contents of an existing index object in memory by reading /// from the hard disk. /// @@ -185,6 +275,16 @@ class Index with IterableMixin { void remove(String path, [int stage = 0]) => bindings.remove(indexPointer: _indexPointer, path: path, stage: stage); + /// Removes all entries from the index under a given [directory] with + /// optional [stage]. + void removeDirectory(String directory, [int stage = 0]) { + bindings.removeDirectory( + indexPointer: _indexPointer, + dir: directory, + stage: stage, + ); + } + /// Removes all matching index entries at provided list of [path]s relative /// to repository working directory. /// @@ -286,9 +386,15 @@ class IndexEntry { /// Sets the UNIX file attributes of a index entry. set mode(GitFilemode mode) => _indexEntryPointer.ref.mode = mode.value; + /// Stage number. + int get stage => bindings.entryStage(_indexEntryPointer); + + /// Whether the given index entry is a conflict (has a high stage entry). + bool get isConflict => bindings.entryIsConflict(_indexEntryPointer); + @override String toString() { - return 'IndexEntry{oid: $oid, path: $path, mode: $mode}'; + return 'IndexEntry{oid: $oid, path: $path, mode: $mode, stage: $stage}'; } } diff --git a/test/git_types_test.dart b/test/git_types_test.dart index f818a88..7fc3961 100644 --- a/test/git_types_test.dart +++ b/test/git_types_test.dart @@ -561,5 +561,20 @@ void main() { ); }); }); + + group('GitIndexCapability', () { + test('returns correct values', () { + const expected = [1, 2, 4, -1]; + final actual = GitIndexCapability.values.map((e) => e.value).toList(); + expect(actual, expected); + }); + + test('returns string representation of object', () { + expect( + GitIndexCapability.ignoreCase.toString(), + 'GitIndexCapability.ignoreCase', + ); + }); + }); }); } diff --git a/test/index_test.dart b/test/index_test.dart index 9c7777d..2d5ea50 100644 --- a/test/index_test.dart +++ b/test/index_test.dart @@ -27,6 +27,37 @@ void main() { const fileSha = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'; const featureFileSha = '9c78c21d6680a7ffebc76f7ac68cacc11d8f48bc'; + test('returns full path to the index file on disk', () { + expect(index.path, '${repo.path}index'); + }); + + group('capabilities', () { + test('returns index capabilities', () { + expect(index.capabilities, isEmpty); + }); + + test('successfully sets index capabilities', () { + expect(index.capabilities, isEmpty); + + index.capabilities = { + GitIndexCapability.ignoreCase, + GitIndexCapability.noSymlinks, + }; + + expect(index.capabilities, { + GitIndexCapability.ignoreCase, + GitIndexCapability.noSymlinks, + }); + }); + + test('throws when trying to set index capabilities and error occurs', () { + expect( + () => Index(nullptr).capabilities = {}, + throwsA(isA()), + ); + }); + }); + test('returns number of entries', () { expect(index.length, 4); }); @@ -37,6 +68,10 @@ void main() { } }); + test('returns stage of entry', () { + expect(index['file'].stage, 0); + }); + test('returns index entry at provided position', () { expect(index[3].path, 'file'); expect(index[3].oid.sha, fileSha); @@ -118,6 +153,26 @@ void main() { }); }); + group('addFromBuffer()', () { + test('successfully updates index entry from a buffer', () { + final entry = index['file']; + expect(repo.status, isEmpty); + + index.addFromBuffer(entry: entry, buffer: 'updated'); + expect(repo.status, { + 'file': {GitStatus.indexModified, GitStatus.wtModified} + }); + }); + + test('throws when trying to update entry and error occurs', () { + final nullEntry = IndexEntry(nullptr); + expect( + () => index.addFromBuffer(entry: nullEntry, buffer: ''), + throwsA(isA()), + ); + }); + }); + group('addAll()', () { test('successfully adds with provided pathspec', () { index.clear(); @@ -152,6 +207,33 @@ void main() { }); }); + group('updateAll()', () { + test('successfully updates all entries to match working directory', () { + expect(repo.status, isEmpty); + File('${repo.workdir}file').deleteSync(); + File('${repo.workdir}feature_file').deleteSync(); + + index.updateAll(['file', 'feature_file']); + expect(repo.status, { + 'file': {GitStatus.indexDeleted}, + 'feature_file': {GitStatus.indexDeleted}, + }); + }); + + test('throws when trying to update all entries in bare repository', () { + final bare = Repository.open('test/assets/empty_bare.git'); + final bareIndex = bare.index; + + expect( + () => bareIndex.updateAll(['not_there']), + throwsA(isA()), + ); + + bareIndex.free(); + bare.free(); + }); + }); + test('writes to disk', () { expect(index.length, 4); @@ -186,6 +268,17 @@ void main() { expect(index.find('feature_file'), false); }); + test('removes all entries from a directory', () { + Directory('${repo.workdir}subdir/').createSync(); + File('${repo.workdir}subdir/subfile').createSync(); + + index.add('subdir/subfile'); + expect(index.length, 5); + + index.removeDirectory('subdir'); + expect(index.length, 4); + }); + test('successfully reads tree with provided SHA hex', () { final tree = repo.lookupTree( repo['df2b8fc99e1c1d4dbc0a854d9f72157f1d6ea078'], @@ -228,6 +321,19 @@ void main() { tmpDir.deleteSync(recursive: true); }); + test('successfully adds conflict entry', () { + expect(index.conflicts, isEmpty); + index.addConflict( + ourEntry: index['file'], + theirEntry: index['feature_file'], + ); + expect(index.conflicts.length, 2); + }); + + test('throws when trying to add conflict entry and error occurs', () { + expect(() => Index(nullptr).addConflict(), throwsA(isA())); + }); + test('returns conflicts with ancestor, our and their present', () { final repoDir = setupRepo(Directory('test/assets/mergerepo/')); final conflictRepo = Repository.open(repoDir.path); @@ -322,7 +428,7 @@ void main() { repoDir.deleteSync(recursive: true); }); - test('successfully removes conflicts', () { + test('successfully removes conflict', () { final repoDir = setupRepo(Directory('test/assets/mergerepo/')); final conflictRepo = Repository.open(repoDir.path); @@ -331,6 +437,8 @@ void main() { conflictRepo.merge(conflictBranch.target); expect(index.hasConflicts, true); + expect(index['.gitignore'].isConflict, false); + expect(index.conflicts['conflict_file']!.our!.isConflict, true); expect(index.conflicts.length, 1); final conflictedFile = index.conflicts['conflict_file']!; @@ -353,6 +461,34 @@ void main() { ); }); + test('successfully removes all conflicts', () { + final repoDir = setupRepo(Directory('test/assets/mergerepo/')); + final conflictRepo = Repository.open(repoDir.path); + + final conflictBranch = conflictRepo.lookupBranch(name: 'conflict-branch'); + final index = conflictRepo.index; + + conflictRepo.merge(conflictBranch.target); + expect(index.hasConflicts, true); + expect(index.conflicts.length, 1); + + index.cleanupConflict(); + expect(index.hasConflicts, false); + expect(index.conflicts, isEmpty); + + index.free(); + conflictBranch.free(); + conflictRepo.free(); + repoDir.deleteSync(recursive: true); + }); + + test('throws when trying to remove all conflicts and error occurs', () { + expect( + () => Index(nullptr).cleanupConflict(), + throwsA(isA()), + ); + }); + test('returns string representation of Index and IndexEntry objects', () { final index = repo.index;