diff --git a/lib/libgit2dart.dart b/lib/libgit2dart.dart index 6296b3e..2df0fa4 100644 --- a/lib/libgit2dart.dart +++ b/lib/libgit2dart.dart @@ -16,5 +16,6 @@ export 'src/branch.dart'; export 'src/worktree.dart'; export 'src/diff.dart'; export 'src/patch.dart'; +export 'src/stash.dart'; export 'src/error.dart'; export 'src/git_types.dart'; diff --git a/lib/src/bindings/checkout.dart b/lib/src/bindings/checkout.dart index a52012d..c3a4300 100644 --- a/lib/src/bindings/checkout.dart +++ b/lib/src/bindings/checkout.dart @@ -19,10 +19,10 @@ void head( String? directory, List? paths, ) { - final initOptions = _initOptions(strategy, directory, paths); - final optsC = initOptions[0]; - final pathPointers = initOptions[1]; - final strArray = initOptions[2]; + final initOpts = initOptions(strategy, directory, paths); + final optsC = initOpts[0]; + final pathPointers = initOpts[1]; + final strArray = initOpts[2]; final error = libgit2.git_checkout_head(repo, optsC); @@ -47,10 +47,10 @@ void index( String? directory, List? paths, ) { - final initOptions = _initOptions(strategy, directory, paths); - final optsC = initOptions[0]; - final pathPointers = initOptions[1]; - final strArray = initOptions[2]; + final initOpts = initOptions(strategy, directory, paths); + final optsC = initOpts[0]; + final pathPointers = initOpts[1]; + final strArray = initOpts[2]; final error = libgit2.git_checkout_index(repo, nullptr, optsC); @@ -77,10 +77,10 @@ void tree( String? directory, List? paths, ) { - final initOptions = _initOptions(strategy, directory, paths); - final optsC = initOptions[0]; - final pathPointers = initOptions[1]; - final strArray = initOptions[2]; + final initOpts = initOptions(strategy, directory, paths); + final optsC = initOpts[0]; + final pathPointers = initOpts[1]; + final strArray = initOpts[2]; final error = libgit2.git_checkout_tree(repo, treeish, optsC); @@ -96,7 +96,7 @@ void tree( } } -List _initOptions( +List initOptions( int strategy, String? directory, List? paths, diff --git a/lib/src/bindings/stash.dart b/lib/src/bindings/stash.dart new file mode 100644 index 0000000..bb91e1b --- /dev/null +++ b/lib/src/bindings/stash.dart @@ -0,0 +1,148 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import '../stash.dart'; +import 'checkout.dart' as checkout_bindings; +import 'libgit2_bindings.dart'; +import '../util.dart'; + +/// Save the local modifications to a new stash. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer stash( + Pointer repo, + Pointer stasher, + String message, + int flags, +) { + final out = calloc(); + final messageC = + message.isNotEmpty ? message.toNativeUtf8().cast() : nullptr; + final error = libgit2.git_stash_save(out, repo, stasher, messageC, flags); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} + +/// Apply a single stashed state from the stash list. +/// +/// Throws a [LibGit2Error] if error occured. +void apply( + Pointer repo, + int index, + int flags, + int strategy, + String? directory, + List? paths, +) { + final options = + calloc(sizeOf()); + final optionsError = libgit2.git_stash_apply_options_init( + options, GIT_STASH_APPLY_OPTIONS_VERSION); + + if (optionsError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + final checkoutOptions = + checkout_bindings.initOptions(strategy, directory, paths); + final optsC = checkoutOptions[0]; + final pathPointers = checkoutOptions[1]; + final strArray = checkoutOptions[2]; + + options.ref.flags = flags; + options.ref.checkout_options = (optsC as Pointer).ref; + + final error = libgit2.git_stash_apply(repo, index, options); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + for (var p in pathPointers) { + calloc.free(p); + } + + calloc.free(strArray); + calloc.free(optsC); + calloc.free(options); +} + +/// Remove a single stashed state from the stash list. +/// +/// Throws a [LibGit2Error] if error occured. +void drop(Pointer repo, int index) { + libgit2.git_stash_drop(repo, index); +} + +/// Apply a single stashed state from the stash list and remove it from the list if successful. +/// +/// Throws a [LibGit2Error] if error occured. +void pop( + Pointer repo, + int index, + int flags, + int strategy, + String? directory, + List? paths, +) { + final options = + calloc(sizeOf()); + final optionsError = libgit2.git_stash_apply_options_init( + options, GIT_STASH_APPLY_OPTIONS_VERSION); + + if (optionsError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + final checkoutOptions = + checkout_bindings.initOptions(strategy, directory, paths); + final optsC = checkoutOptions[0]; + final pathPointers = checkoutOptions[1]; + final strArray = checkoutOptions[2]; + + options.ref.flags = flags; + options.ref.checkout_options = (optsC as Pointer).ref; + + final error = libgit2.git_stash_pop(repo, index, options); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } + + for (var p in pathPointers) { + calloc.free(p); + } + + calloc.free(strArray); + calloc.free(optsC); + calloc.free(options); +} + +var _stashList = []; + +/// Loop over all the stashed states. +List list(Pointer repo) { + const except = -1; + _stashList.clear(); + git_stash_cb callBack = Pointer.fromFunction(_stashCb, except); + libgit2.git_stash_foreach(repo, callBack, nullptr); + return _stashList; +} + +int _stashCb( + int index, + Pointer message, + Pointer oid, + Pointer payload, +) { + _stashList.add(Stash( + index: index, + message: message.cast().toDartString(), + oid: oid, + )); + return 0; +} diff --git a/lib/src/git_types.dart b/lib/src/git_types.dart index 60d04ca..52d3cbf 100644 --- a/lib/src/git_types.dart +++ b/lib/src/git_types.dart @@ -1120,3 +1120,57 @@ class GitConfigLevel { @override String toString() => 'GitConfigLevel.$_name'; } + +/// Stash flags. +class GitStash { + const GitStash._(this._value, this._name); + final int _value; + final String _name; + + /// No option, default. + static const defaults = GitStash._(0, 'defaults'); + + /// All changes already added to the index are left intact in + /// the working directory. + static const keepIndex = GitStash._(1, 'keepIndex'); + + /// All untracked files are also stashed and then cleaned up + /// from the working directory. + static const includeUntracked = GitStash._(2, 'includeUntracked'); + + /// All ignored files are also stashed and then cleaned up from + /// the working directory. + static const includeIgnored = GitStash._(4, 'includeIgnored'); + + static const List values = [ + defaults, + keepIndex, + includeUntracked, + includeIgnored, + ]; + + int get value => _value; + + @override + String toString() => 'GitStash.$_name'; +} + +/// Stash application flags. +class GitStashApply { + const GitStashApply._(this._value, this._name); + final int _value; + final String _name; + + static const defaults = GitStashApply._(0, 'defaults'); + + /// Try to reinstate not only the working tree's changes, + /// but also the index's changes. + static const reinstateIndex = GitStashApply._(1, 'reinstateIndex'); + + static const List values = [defaults, reinstateIndex]; + + int get value => _value; + + @override + String toString() => 'GitStashApply.$_name'; +} diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 1f048e6..7bdc363 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -10,6 +10,7 @@ import 'bindings/commit.dart' as commit_bindings; import 'bindings/checkout.dart' as checkout_bindings; import 'bindings/reset.dart' as reset_bindings; import 'bindings/diff.dart' as diff_bindings; +import 'bindings/stash.dart' as stash_bindings; import 'branch.dart'; import 'commit.dart'; import 'config.dart'; @@ -837,4 +838,77 @@ class Repository { true, ); } + + /// Saves the local modifications to a new stash. + /// + /// Throws a [LibGit2Error] if error occured. + Oid stash({ + required Signature stasher, + String message = '', + bool keepIndex = false, + bool includeUntracked = false, + bool includeIgnored = false, + }) { + int flags = 0; + if (keepIndex) flags |= GitStash.keepIndex.value; + if (includeUntracked) flags |= GitStash.includeUntracked.value; + if (includeIgnored) flags |= GitStash.includeIgnored.value; + + return Oid(stash_bindings.stash( + _repoPointer, + stasher.pointer, + message, + flags, + )); + } + + /// Applies a single stashed state from the stash list. + /// + /// Throws a [LibGit2Error] if error occured. + void stashApply({ + int index = 0, + bool reinstateIndex = false, + Set strategy = const { + GitCheckout.safe, + GitCheckout.recreateMissing + }, + String? directory, + List? paths, + }) { + int flags = reinstateIndex ? GitStashApply.reinstateIndex.value : 0; + final int strat = + strategy.fold(0, (previousValue, e) => previousValue | e.value); + + stash_bindings.apply(_repoPointer, index, flags, strat, directory, paths); + } + + /// Removes a single stashed state from the stash list. + /// + /// Throws a [LibGit2Error] if error occured. + void stashDrop([int index = 0]) => stash_bindings.drop(_repoPointer, index); + + /// Applies a single stashed state from the stash list and remove it from the list if successful. + /// + /// Throws a [LibGit2Error] if error occured. + void stashPop({ + int index = 0, + bool reinstateIndex = false, + Set strategy = const { + GitCheckout.safe, + GitCheckout.recreateMissing + }, + String? directory, + List? paths, + }) { + int flags = reinstateIndex ? GitStashApply.reinstateIndex.value : 0; + final int strat = + strategy.fold(0, (previousValue, e) => previousValue | e.value); + + stash_bindings.pop(_repoPointer, index, flags, strat, directory, paths); + } + + /// Returns list of all the stashed states, first being the most recent. + List get stashList { + return stash_bindings.list(_repoPointer); + } } diff --git a/lib/src/stash.dart b/lib/src/stash.dart new file mode 100644 index 0000000..3e677ef --- /dev/null +++ b/lib/src/stash.dart @@ -0,0 +1,28 @@ +import 'dart:ffi'; +import 'bindings/libgit2_bindings.dart'; +import 'oid.dart'; + +class Stash { + /// Initializes a new instance of [Stash] class. + Stash({ + required this.index, + required this.message, + required Pointer oid, + }) { + this.oid = Oid(oid); + } + + /// The position within the stash list. + final int index; + + /// The stash message. + final String message; + + /// The commit oid of the stashed state. + late final Oid oid; + + @override + String toString() { + return 'Stash{index: $index, message: $message, sha: ${oid.sha}}'; + } +} diff --git a/test/repository_test.dart b/test/repository_test.dart index 7a047c9..457ccfb 100644 --- a/test/repository_test.dart +++ b/test/repository_test.dart @@ -374,6 +374,74 @@ void main() { throwsA(isA()), ); }); + + group('stash', () { + late Signature stasher; + setUp(() { + stasher = Signature.create( + name: 'Stasher', + email: 'stasher@email.com', + ); + }); + + tearDown(() => stasher.free()); + + test('successfully saves changes to stash', () { + File('${tmpDir}file').writeAsStringSync( + 'edit', + mode: FileMode.append, + ); + + repo.stash(stasher: stasher, includeUntracked: true); + expect(repo.status.isEmpty, true); + }); + + test('successfully applies changes from stash', () { + File('${tmpDir}file').writeAsStringSync( + 'edit', + mode: FileMode.append, + ); + + repo.stash(stasher: stasher); + expect(repo.status.isEmpty, true); + + repo.stashApply(); + expect(repo.status, contains('file')); + }); + + test('successfully drops stash', () { + File('${tmpDir}file').writeAsStringSync( + 'edit', + mode: FileMode.append, + ); + + repo.stash(stasher: stasher); + repo.stashDrop(); + expect(() => repo.stashApply(), throwsA(isA())); + }); + + test('successfully pops from stash', () { + File('${tmpDir}file').writeAsStringSync( + 'edit', + mode: FileMode.append, + ); + + repo.stash(stasher: stasher); + repo.stashPop(); + expect(repo.status, contains('file')); + expect(() => repo.stashApply(), throwsA(isA())); + }); + + test('returns list of stashes', () { + File('${tmpDir}file').writeAsStringSync( + 'edit', + mode: FileMode.append, + ); + + repo.stash(stasher: stasher); + expect(repo.stashList.length, 1); + }); + }); }); }); }