diff --git a/lib/src/bindings/index.dart b/lib/src/bindings/index.dart new file mode 100644 index 0000000..ef4a4ff --- /dev/null +++ b/lib/src/bindings/index.dart @@ -0,0 +1,227 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import 'libgit2_bindings.dart'; +import '../util.dart'; + +/// Update the contents of an existing index object in memory by reading from the hard disk. +/// +/// If force is true, this performs a "hard" read that discards in-memory changes and +/// always reloads the on-disk index data. If there is no on-disk version, +/// the index will be cleared. +/// +/// If force is false, this does a "soft" read that reloads the index data from disk only +/// if it has changed since the last time it was loaded. Purely in-memory index data +/// will be untouched. Be aware: if there are changes on disk, unwritten in-memory changes +/// are discarded. +/// +/// Throws a [LibGit2Error] if error occured. +void read(Pointer index, bool force) { + final forceC = force == true ? 1 : 0; + final error = libgit2.git_index_read(index, forceC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Find the first position of any entries which point to given path in the Git index. +bool find(Pointer index, String path) { + final pathC = path.toNativeUtf8().cast(); + final result = libgit2.git_index_find(nullptr, index, pathC); + calloc.free(pathC); + + return result == git_error_code.GIT_ENOTFOUND ? false : true; +} + +/// Get the count of entries currently in the index. +int entryCount(Pointer index) => libgit2.git_index_entrycount(index); + +/// Get a pointer to one of the entries in the index based on position. +/// +/// The entry is not modifiable and should not be freed. +/// +/// Throws error if position is out of bounds. +Pointer getByIndex(Pointer index, int n) { + final result = libgit2.git_index_get_byindex(index, n); + + if (result == nullptr) { + throw RangeError('Out of bounds'); + } else { + return result; + } +} + +/// Get a pointer to one of the entries in the index based on path. +/// +///The entry is not modifiable and should not be freed. +/// +/// Throws error if entry isn't found. +Pointer getByPath( + Pointer index, + String path, + int stage, +) { + final pathC = path.toNativeUtf8().cast(); + final result = libgit2.git_index_get_bypath(index, pathC, stage); + calloc.free(pathC); + + if (result == nullptr) { + throw ArgumentError.value('$path was not found'); + } else { + return result; + } +} + +/// Clear the contents (all the entries) of an index object. +/// +/// This clears the index object in memory; changes must be explicitly written to +/// disk for them to take effect persistently. +/// +/// Throws a [LibGit2Error] if error occured. +void clear(Pointer index) { + final error = libgit2.git_index_clear(index); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Add or update an index entry from an in-memory struct. +/// +/// If a previous index entry exists that has the same path and stage as the given `sourceEntry`, +/// it will be replaced. Otherwise, the `sourceEntry` will be added. +/// +/// Throws a [LibGit2Error] if error occured. +void add(Pointer index, Pointer sourceEntry) { + final error = libgit2.git_index_add(index, sourceEntry); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Add or update an index entry from a file on disk. +/// +/// The file path must be relative to the repository's working folder and must be readable. +/// +/// This method will fail in bare index instances. +/// +/// 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 addByPath(Pointer index, String path) { + final pathC = path.toNativeUtf8().cast(); + final error = libgit2.git_index_add_bypath(index, pathC); + calloc.free(pathC); + + 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. +/// +/// The `pathspec` is a list of file names or shell glob patterns that will be matched +/// against files in the repository's working directory. Each file that matches will be +/// added to the index (either updating an existing entry or adding a new entry). +/// +/// Throws a [LibGit2Error] if error occured. +void addAll(Pointer index, List pathspec) { + var pathspecC = calloc(); + final List> pathPointers = + pathspec.map((e) => e.toNativeUtf8().cast()).toList(); + final Pointer> 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_add_all( + index, + pathspecC, + 0, + nullptr, + nullptr, + ); + + calloc.free(pathspecC); + calloc.free(strArray); + for (var p in pathPointers) { + calloc.free(p); + } + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Write an existing index object from memory back to disk using an atomic file lock. +/// +/// Throws a [LibGit2Error] if error occured. +void write(Pointer index) { + final error = libgit2.git_index_write(index); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Remove an entry from the index. +/// +/// Throws a [LibGit2Error] if error occured. +void remove(Pointer index, String path, int stage) { + final pathC = path.toNativeUtf8().cast(); + final error = libgit2.git_index_remove(index, pathC, stage); + calloc.free(pathC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Remove all matching index entries. +/// +/// Throws a [LibGit2Error] if error occured. +void removeAll(Pointer index, List pathspec) { + final pathspecC = calloc(); + final List> pathPointers = + pathspec.map((e) => e.toNativeUtf8().cast()).toList(); + final Pointer> 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_remove_all( + index, + pathspecC, + nullptr, + nullptr, + ); + + calloc.free(pathspecC); + calloc.free(strArray); + for (var p in pathPointers) { + calloc.free(p); + } + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// Free an existing index object. +void free(Pointer index) => libgit2.git_index_free(index); diff --git a/lib/src/index.dart b/lib/src/index.dart new file mode 100644 index 0000000..d917a5f --- /dev/null +++ b/lib/src/index.dart @@ -0,0 +1,133 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'bindings/libgit2_bindings.dart'; +import 'bindings/index.dart' as bindings; +import 'util.dart'; + +class Index { + /// Initializes a new instance of [Index] class from provided + /// pointer to index object in memory. + /// + /// Should be freed with `free()` to release allocated memory. + Index(this._indexPointer) { + libgit2.git_libgit2_init(); + } + + /// Pointer to memory address for allocated index object. + late final Pointer _indexPointer; + + /// Returns index entry located at provided 0-based position or string path. + /// + /// Throws error if position is out of bounds or entry isn't found at path. + IndexEntry operator [](Object value) { + if (value is int) { + return IndexEntry(bindings.getByIndex(_indexPointer, value)); + } else { + return IndexEntry(bindings.getByPath(_indexPointer, value as String, 0)); + } + } + + /// Checks whether entry at provided [path] is in the git index or not. + bool contains(String path) => bindings.find(_indexPointer, path); + + /// Returns the count of entries currently in the index. + int get count => bindings.entryCount(_indexPointer); + + /// Clears the contents (all the entries) of an index object. + /// + /// This clears the index object in memory; changes must be explicitly written to + /// disk for them to take effect persistently. + /// + /// Throws a [LibGit2Error] if error occured. + void clear() => bindings.clear(_indexPointer); + + /// Adds or updates an index entry from an [IndexEntry] or from a file on disk. + /// + /// If a previous index entry exists that has the same path and stage as the given `entry`, + /// it will be replaced. Otherwise, the `entry` will be added. + /// + /// The file path must be relative to the repository's working folder and must be readable. + /// + /// This method will fail in bare index instances. + /// + /// Throws a [LibGit2Error] if error occured. + void add(Object entry) { + if (entry is IndexEntry) { + bindings.add(_indexPointer, entry._indexEntryPointer); + } else { + bindings.addByPath(_indexPointer, entry as String); + } + } + + /// Adds or updates index entries matching files in the working directory. + /// + /// This method will fail in bare index instances. + /// + /// The `pathspec` is a list of file names or shell glob patterns that will be matched + /// against files in the repository's working directory. Each file that matches will be + /// added to the index (either updating an existing entry or adding a new entry). + /// + /// Throws a [LibGit2Error] if error occured. + void addAll(List pathspec) { + bindings.addAll(_indexPointer, pathspec); + } + + /// Updates the contents of an existing index object in memory by reading from the hard disk. + /// + /// If force is true (default), this performs a "hard" read that discards in-memory changes and + /// always reloads the on-disk index data. If there is no on-disk version, + /// the index will be cleared. + /// + /// If force is false, this does a "soft" read that reloads the index data from disk only + /// if it has changed since the last time it was loaded. Purely in-memory index data + /// will be untouched. Be aware: if there are changes on disk, unwritten in-memory changes + /// are discarded. + /// + /// Throws a [LibGit2Error] if error occured. + void read({bool force = true}) => bindings.read(_indexPointer, force); + + /// Writes an existing index object from memory back to disk using an atomic file lock. + /// + /// Throws a [LibGit2Error] if error occured. + void write() => bindings.write(_indexPointer); + + /// Removes an entry from the index. + /// + /// Throws a [LibGit2Error] if error occured. + void remove(String path, [int stage = 0]) => + bindings.remove(_indexPointer, path, stage); + + /// Remove all matching index entries. + /// + /// Throws a [LibGit2Error] if error occured. + void removeAll(List path) => bindings.removeAll(_indexPointer, path); + + /// Releases memory allocated for index object. + void free() { + bindings.free(_indexPointer); + libgit2.git_libgit2_shutdown(); + } +} + +class IndexEntry { + /// Initializes a new instance of [IndexEntry] class. + IndexEntry(this._indexEntryPointer); + + /// Pointer to memory address for allocated index entry object. + late final Pointer _indexEntryPointer; + + /// Returns path to file. + String get path => _indexEntryPointer.ref.path.cast().toDartString(); + + /// Returns sha-1 of file. + String get sha { + var hex = StringBuffer(); + for (var i = 0; i < 20; i++) { + hex.write(_indexEntryPointer.ref.id.id[i].toRadixString(16)); + } + return hex.toString(); + } + + /// Returns mode of file. + int get mode => _indexEntryPointer.ref.mode; +} diff --git a/lib/src/reference.dart b/lib/src/reference.dart index 2cbeffb..6ca68a9 100644 --- a/lib/src/reference.dart +++ b/lib/src/reference.dart @@ -44,8 +44,8 @@ class Reference { late final Oid oid; late final bool isDirect; - if (target.runtimeType == Oid) { - oid = target as Oid; + if (target is Oid) { + oid = target; isDirect = true; } else if (isValidShaHex(target as String)) { if (target.length == 40) { diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 0a31064..b50ff02 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -1,4 +1,5 @@ import 'dart:ffi'; +import 'index.dart'; import 'odb.dart'; import 'oid.dart'; import 'reference.dart'; @@ -188,8 +189,15 @@ class Repository { } /// Returns [Reference] object pointing to repository head. + /// + /// Must be freed once it's no longer being used. Reference get head => Reference(bindings.head(_repoPointer)); + /// Returns [Index] file for this repository. + /// + /// Must be freed once it's no longer being used. + Index get index => Index(bindings.index(_repoPointer)); + /// Returns [Odb] for this repository. /// /// ODB Object must be freed once it's no longer being used. diff --git a/test/index_test.dart b/test/index_test.dart new file mode 100644 index 0000000..2a22e2f --- /dev/null +++ b/test/index_test.dart @@ -0,0 +1,153 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:libgit2dart/src/index.dart'; +import 'package:libgit2dart/src/repository.dart'; +import 'package:libgit2dart/src/error.dart'; + +import 'helpers/util.dart'; + +void main() { + late Repository repo; + late Index index; + final tmpDir = '${Directory.systemTemp.path}/index_testrepo/'; + + setUp(() async { + if (await Directory(tmpDir).exists()) { + await Directory(tmpDir).delete(recursive: true); + } + await copyRepo( + from: Directory('test/assets/testrepo/'), + to: await Directory(tmpDir).create(), + ); + repo = Repository.open(tmpDir); + index = repo.index; + }); + + tearDown(() async { + index.free(); + repo.free(); + await Directory(tmpDir).delete(recursive: true); + }); + + group('Index', () { + const fileSha = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'; + const featureFileSha = '9c78c21d6680a7ffebc76f7ac68cacc11d8f48bc'; + + test('returns number of entries', () { + expect(index.count, 3); + }); + + test('returns mode of index entry', () { + expect(index['file'].mode, 33188); + }); + + test('returns index entry at provided position', () { + expect(index[2].path, 'file'); + expect(index['file'].sha, fileSha); + }); + + test('returns index entry at provided path', () { + expect(index['file'].path, 'file'); + expect(index['file'].sha, fileSha); + }); + + test('throws if provided entry position is out of bounds', () { + expect(() => index[10], throwsA(isA())); + }); + + test('throws if provided entry path is not found', () { + expect(() => index[10], throwsA(isA())); + }); + + test('clears the contents', () { + expect(index.count, 3); + index.clear(); + expect(index.count, 0); + }); + + group('add()', () { + test('successfully adds with provided IndexEntry', () { + final entry = index['file']; + + index.add(entry); + expect(index['file'].sha, fileSha); + expect(index.count, 3); + }); + + test('successfully adds with provided path string', () { + index.add('file'); + expect(index['file'].sha, fileSha); + expect(index.count, 3); + }); + + test('throws if file not found at provided path', () { + expect(() => index.add('not_there'), throwsA(isA())); + }); + + test('throws if index of bare repository', () { + final bare = Repository.open('test/assets/empty_bare.git'); + final bareIndex = bare.index; + + expect(() => bareIndex.add('config'), throwsA(isA())); + + bareIndex.free(); + bare.free(); + }); + }); + + group('addAll()', () { + test('successfully adds with provided pathspec', () { + index.clear(); + index.addAll(['file', 'feature_file']); + + expect(index.count, 2); + expect(index['file'].sha, fileSha); + expect(index['feature_file'].sha, featureFileSha); + + index.clear(); + index.addAll(['[f]*']); + + expect(index.count, 2); + expect(index['file'].sha, fileSha); + expect(index['feature_file'].sha, featureFileSha); + + index.clear(); + index.addAll(['feature_f???']); + + expect(index.count, 1); + expect(index['feature_file'].sha, featureFileSha); + }); + }); + + test('writes to disk', () { + expect(index.count, 3); + + File('$tmpDir/new_file').createSync(); + + index.add('new_file'); + index.write(); + + index.clear(); + index.read(); + expect(index['new_file'].path, 'new_file'); + expect(index.count, 4); + }); + + test('removes an entry', () { + expect(index.contains('feature_file'), true); + index.remove('feature_file'); + expect(index.contains('feature_file'), false); + }); + + test('removes all entries with matching pathspec', () { + expect(index.contains('file'), true); + expect(index.contains('feature_file'), true); + + index.removeAll(['[f]*']); + + expect(index.contains('file'), false); + expect(index.contains('feature_file'), false); + }); + }); +}