diff --git a/lib/src/bindings/status.dart b/lib/src/bindings/status.dart new file mode 100644 index 0000000..e67b642 --- /dev/null +++ b/lib/src/bindings/status.dart @@ -0,0 +1,81 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'libgit2_bindings.dart'; +import '../error.dart'; +import '../util.dart'; + +/// Gather file status information and populate the git_status_list. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer listNew(Pointer repo) { + final out = calloc>(); + final error = libgit2.git_status_list_new(out, repo, nullptr); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Gets the count of status entries in this list. +/// +/// If there are no changes in status (at least according the options given when +/// the status list was created), this can return 0. +int listEntryCount(Pointer statuslist) { + return libgit2.git_status_list_entrycount(statuslist); +} + +/// Get a pointer to one of the entries in the status list. +/// +/// The entry is not modifiable and should not be freed. +/// +/// Throws [RangeError] if position is out of bounds +Pointer getByIndex( + Pointer statuslist, + int idx, +) { + final result = libgit2.git_status_byindex(statuslist, idx); + + if (result == nullptr) { + throw RangeError('Out of bounds'); + } else { + return result; + } +} + +/// Get file status for a single file. +/// +/// This tries to get status for the filename that you give. If no files match that name +/// (in either the HEAD, index, or working directory), this returns GIT_ENOTFOUND. +/// +/// If the name matches multiple files (for example, if the path names a directory or if +/// running on a case- insensitive filesystem and yet the HEAD has two entries that both +/// match the path), then this returns GIT_EAMBIGUOUS because it cannot give correct results. +/// +/// This does not do any sort of rename detection. Renames require a set of targets and because +/// of the path filtering, there is not enough information to check renames correctly. To check +/// file status with rename detection, there is no choice but to do a full listNew +/// and scan through looking for the path that you are interested in. +/// +/// Throws a [LibGit2Error] if error occured. +int file(Pointer repo, String path) { + final out = calloc(); + final pathC = path.toNativeUtf8().cast(); + final error = libgit2.git_status_file(out, repo, pathC); + + calloc.free(pathC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + final result = out.value; + calloc.free(out); + return result; + } +} + +/// Free an existing status list. +void listFree(Pointer statuslist) => + libgit2.git_status_list_free(statuslist); diff --git a/lib/src/git_types.dart b/lib/src/git_types.dart index 233af16..70d3a8a 100644 --- a/lib/src/git_types.dart +++ b/lib/src/git_types.dart @@ -23,15 +23,10 @@ class GitFilemode { final int _value; static const unreadable = GitFilemode._(0); - static const tree = GitFilemode._(16384); - static const blob = GitFilemode._(33188); - static const blobExecutable = GitFilemode._(33261); - static const link = GitFilemode._(40960); - static const commit = GitFilemode._(57344); int get value => _value; @@ -117,10 +112,38 @@ class GitBranch { final int _value; static const local = GitBranch._(1); - static const remote = GitBranch._(2); - static const all = GitBranch._(3); int get value => _value; } + +/// Status flags for a single file. +/// +/// A combination of these values will be returned to indicate the status of +/// a file. Status compares the working directory, the index, and the +/// current HEAD of the repository. The `GitStatus.index` set of flags +/// represents the status of file in the index relative to the HEAD, and the +/// `GitStatus.wt` set of flags represent the status of the file in the +/// working directory relative to the index. +class GitStatus { + const GitStatus._(this._value); + final int _value; + + static const current = GitStatus._(0); + static const indexNew = GitStatus._(1); + static const indexModified = GitStatus._(2); + static const indexDeleted = GitStatus._(4); + static const indexRenamed = GitStatus._(8); + static const indexTypeChange = GitStatus._(16); + static const wtNew = GitStatus._(128); + static const wtModified = GitStatus._(256); + static const wtDeleted = GitStatus._(512); + static const wtTypeChange = GitStatus._(1024); + static const wtRenamed = GitStatus._(2048); + static const wtUnreadable = GitStatus._(4096); + static const ignored = GitStatus._(16384); + static const conflicted = GitStatus._(32768); + + int get value => _value; +} diff --git a/lib/src/repository.dart b/lib/src/repository.dart index ea889aa..6cb4bc8 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -1,8 +1,10 @@ import 'dart:ffi'; +import 'package:ffi/ffi.dart'; import 'bindings/libgit2_bindings.dart'; import 'bindings/repository.dart' as bindings; import 'bindings/merge.dart' as merge_bindings; import 'bindings/object.dart' as object_bindings; +import 'bindings/status.dart' as status_bindings; import 'branch.dart'; import 'commit.dart'; import 'config.dart'; @@ -459,4 +461,37 @@ class Repository { /// Returns a [Branches] object. Branches get branches => Branches(this); + + /// Checks status of the repository and returns map of file paths and their statuses. + /// + /// Returns empty map if there are no changes in statuses. + Map get status { + var result = {}; + var list = status_bindings.listNew(_repoPointer); + var count = status_bindings.listEntryCount(list); + + for (var i = 0; i < count; i++) { + final entry = status_bindings.getByIndex(list, i); + if (entry.ref.head_to_index != nullptr) { + final path = entry.ref.head_to_index.ref.old_file.path + .cast() + .toDartString(); + result[path] = entry.ref.status; + } + } + + status_bindings.listFree(list); + + return result; + } + + /// Returns file status for a single file. + /// + /// This does not do any sort of rename detection. Renames require a set of targets and because + /// of the path filtering, there is not enough information to check renames correctly. To check + /// file status with rename detection, there is no choice but to do a full [status] and scan + /// through looking for the path that you are interested in. + /// + /// Throws a [LibGit2Error] if error occured. + int statusFile(String path) => status_bindings.file(_repoPointer, path); } diff --git a/test/repository_test.dart b/test/repository_test.dart index c7f1f2c..8af3b6a 100644 --- a/test/repository_test.dart +++ b/test/repository_test.dart @@ -337,6 +337,31 @@ void main() { newTagTarget.free(); signature.free(); }); + + test('returns status of a repository', () { + File('${tmpDir}new_file.txt').createSync(); + final index = repo.index; + index.remove('file'); + index.add('new_file.txt'); + expect(repo.status, {'file': 132, 'new_file.txt': 1}); + + index.free(); + }); + + test('returns status of a single file for provided path', () { + final index = repo.index; + index.remove('file'); + expect(repo.statusFile('file'), 132); + + index.free(); + }); + + test('throws when checking status of a single file for invalid path', () { + expect( + () => repo.statusFile('not-there'), + throwsA(isA()), + ); + }); }); }); }