From b3e9f6dd1abc00447fc964f7cbea56d2e874d416 Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Tue, 14 Dec 2021 12:53:17 +0300 Subject: [PATCH] feat(diff): add more bindings and api methods (#18) --- lib/src/bindings/diff.dart | 69 +++++++++++++++ lib/src/repository.dart | 29 +++++++ test/diff_test.dart | 169 ++++++++++++++++++++++++++++++------- 3 files changed, 238 insertions(+), 29 deletions(-) diff --git a/lib/src/bindings/diff.dart b/lib/src/bindings/diff.dart index 7ed5ff9..2dd45dd 100644 --- a/lib/src/bindings/diff.dart +++ b/lib/src/bindings/diff.dart @@ -298,6 +298,24 @@ Pointer addToBuf({ return bufferPointer; } +/// Counter for hunk number being applied. +/// +/// **IMPORTANT**: make sure to reset it to 0 before using since it's a global +/// variable. +int _counter = 0; + +/// When applying a patch, callback that will be made per hunk. +int _hunkCb(Pointer hunk, Pointer payload) { + final index = payload.cast().value; + if (_counter == index) { + _counter++; + return 0; + } else { + _counter++; + return 1; + } +} + /// Apply a diff to the given repository, making changes directly in the /// working directory, the index, or both. /// @@ -305,6 +323,7 @@ Pointer addToBuf({ bool apply({ required Pointer repoPointer, required Pointer diffPointer, + int? hunkIndex, required int location, bool check = false, }) { @@ -313,8 +332,18 @@ bool apply({ if (check) { opts.ref.flags |= git_apply_flags_t.GIT_APPLY_CHECK; } + Pointer payload = nullptr; + if (hunkIndex != null) { + _counter = 0; + const except = -1; + final git_apply_hunk_cb callback = Pointer.fromFunction(_hunkCb, except); + payload = calloc()..value = hunkIndex; + opts.ref.payload = payload.cast(); + opts.ref.hunk_cb = callback; + } final error = libgit2.git_apply(repoPointer, diffPointer, location, opts); + calloc.free(payload); calloc.free(opts); if (error < 0) { @@ -324,6 +353,46 @@ bool apply({ } } +/// Apply a diff to a tree, and return the resulting image as an index. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer applyToTree({ + required Pointer repoPointer, + required Pointer treePointer, + required Pointer diffPointer, + int? hunkIndex, +}) { + final out = calloc>(); + final opts = calloc(); + libgit2.git_apply_options_init(opts, GIT_APPLY_OPTIONS_VERSION); + Pointer payload = nullptr; + if (hunkIndex != null) { + _counter = 0; + const except = -1; + final git_apply_hunk_cb callback = Pointer.fromFunction(_hunkCb, except); + payload = calloc()..value = hunkIndex; + opts.ref.payload = payload.cast(); + opts.ref.hunk_cb = callback; + } + final error = libgit2.git_apply_to_tree( + out, + repoPointer, + treePointer, + diffPointer, + opts, + ); + + calloc.free(payload); + calloc.free(opts); + + if (error < 0) { + calloc.free(out); + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + /// Free a previously allocated diff stats. void statsFree(Pointer stats) => libgit2.git_diff_stats_free(stats); diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 7eb417c..c3312c3 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -1362,27 +1362,56 @@ class Repository { /// [location] (directly in the working directory (default), the index or /// both). /// + /// [hunkIndex] is optional index of the hunk to apply. + /// /// Throws a [LibGit2Error] if error occured. void apply({ required Diff diff, + int? hunkIndex, GitApplyLocation location = GitApplyLocation.workdir, }) { diff_bindings.apply( repoPointer: _repoPointer, diffPointer: diff.pointer, + hunkIndex: hunkIndex, location: location.value, ); } + /// Applies the [diff] to the [tree], and returns the resulting image as an + /// index. + /// + /// [hunkIndex] is optional index of the hunk to apply. + /// + /// Throws a [LibGit2Error] if error occured. + Index applyToTree({ + required Diff diff, + required Tree tree, + int? hunkIndex, + }) { + return Index( + diff_bindings.applyToTree( + repoPointer: _repoPointer, + diffPointer: diff.pointer, + treePointer: tree.pointer, + hunkIndex: hunkIndex, + ), + ); + } + /// Checks if the [diff] will apply to provided [location] (the working /// directory (default), the index or both). + /// + /// [hunkIndex] is optional index of the hunk to apply. bool applies({ required Diff diff, + int? hunkIndex, GitApplyLocation location = GitApplyLocation.workdir, }) { return diff_bindings.apply( repoPointer: _repoPointer, diffPointer: diff.pointer, + hunkIndex: hunkIndex, location: location.value, check: true, ); diff --git a/test/diff_test.dart b/test/diff_test.dart index 6f88381..f53529e 100644 --- a/test/diff_test.dart +++ b/test/diff_test.dart @@ -242,45 +242,156 @@ index e69de29..c217c63 100644 diff.free(); }); - test('checks if diff can be applied to repository', () { - final diff1 = repo.diff(); - expect(repo.applies(diff: diff1, location: GitApplyLocation.both), false); + group('apply', () { + test('checks if diff can be applied to repository', () { + final diff1 = repo.diff(); + expect( + repo.applies(diff: diff1, location: GitApplyLocation.both), + false, + ); - final diff2 = Diff.parse(patchText); - repo.checkout(refName: 'HEAD', strategy: {GitCheckout.force}); - expect(repo.applies(diff: diff2, location: GitApplyLocation.both), true); + final diff2 = Diff.parse(patchText); + repo.checkout(refName: 'HEAD', strategy: {GitCheckout.force}); + expect( + repo.applies(diff: diff2, location: GitApplyLocation.both), + true, + ); - diff1.free(); - diff2.free(); - }); + diff1.free(); + diff2.free(); + }); - test('successfully applies diff to repository', () { - final diff = Diff.parse(patchText); - final file = File('${tmpDir.path}/subdir/modified_file'); + test('checks if hunk with provided index can be applied to repository', + () { + final diff1 = repo.diff(); + expect( + repo.applies(diff: diff1, location: GitApplyLocation.both), + false, + ); - repo.checkout(refName: 'HEAD', strategy: {GitCheckout.force}); - expect(file.readAsStringSync(), ''); + final diff2 = Diff.parse(patchText); + final hunk = diff2.patches.first.hunks.first; + repo.checkout(refName: 'HEAD', strategy: {GitCheckout.force}); + expect( + repo.applies( + diff: diff2, + hunkIndex: hunk.index, + location: GitApplyLocation.both, + ), + true, + ); - repo.apply(diff: diff); - expect(file.readAsStringSync(), 'Modified content\n'); + diff1.free(); + diff2.free(); + }); - diff.free(); - }); + test('successfully applies diff to repository', () { + final diff = Diff.parse(patchText); + final file = File('${tmpDir.path}/subdir/modified_file'); - test('throws when trying to apply diff and error occurs', () { - final nullDiff = Diff(nullptr); - expect(() => repo.apply(diff: nullDiff), throwsA(isA())); - }); + repo.checkout(refName: 'HEAD', strategy: {GitCheckout.force}); + expect(file.readAsStringSync(), ''); - test('successfully creates patch from entry index in diff', () { - final diff = Diff.parse(patchText); - final patch = Patch.fromDiff(diff: diff, index: 0); + repo.apply(diff: diff); + expect(file.readAsStringSync(), 'Modified content\n'); - expect(diff.length, 1); - expect(patch.text, patchText); + diff.free(); + }); - patch.free(); - diff.free(); + test('throws when trying to apply diff and error occurs', () { + final nullDiff = Diff(nullptr); + expect(() => repo.apply(diff: nullDiff), throwsA(isA())); + }); + + test('successfully creates patch from entry index in diff', () { + final diff = Diff.parse(patchText); + final patch = Patch.fromDiff(diff: diff, index: 0); + + expect(diff.length, 1); + expect(patch.text, patchText); + + patch.free(); + diff.free(); + }); + + test('successfully applies hunk with provided index to repository', () { + final diff = Diff.parse(patchText); + final hunk = diff.patches.first.hunks.first; + final file = File('${tmpDir.path}/subdir/modified_file'); + + repo.checkout(refName: 'HEAD', strategy: {GitCheckout.force}); + expect(file.readAsStringSync(), ''); + + repo.apply(diff: diff, hunkIndex: hunk.index); + expect(file.readAsStringSync(), 'Modified content\n'); + + diff.free(); + }); + + test('successfully applies diff to tree', () { + final diff = Diff.parse(patchText); + + repo.checkout(refName: 'HEAD', strategy: {GitCheckout.force}); + final head = repo.head; + final commit = repo.lookupCommit(head.target); + final tree = commit.tree; + + final oldIndex = repo.index; + final oldBlob = repo.lookupBlob(oldIndex['subdir/modified_file'].oid); + expect(oldBlob.content, ''); + + final newIndex = repo.applyToTree(diff: diff, tree: tree); + final newBlob = repo.lookupBlob(newIndex['subdir/modified_file'].oid); + expect(newBlob.content, 'Modified content\n'); + + oldBlob.free(); + newBlob.free(); + oldIndex.free(); + newIndex.free(); + tree.free(); + commit.free(); + head.free(); + diff.free(); + }); + + test('successfully applies hunk with provided index to tree', () { + final diff = Diff.parse(patchText); + final hunk = diff.patches.first.hunks.first; + + repo.checkout(refName: 'HEAD', strategy: {GitCheckout.force}); + final head = repo.head; + final commit = repo.lookupCommit(head.target); + final tree = commit.tree; + + final oldIndex = repo.index; + final oldBlob = repo.lookupBlob(oldIndex['subdir/modified_file'].oid); + expect(oldBlob.content, ''); + + final newIndex = repo.applyToTree( + diff: diff, + tree: tree, + hunkIndex: hunk.index, + ); + final newBlob = repo.lookupBlob(newIndex['subdir/modified_file'].oid); + expect(newBlob.content, 'Modified content\n'); + + oldBlob.free(); + newBlob.free(); + oldIndex.free(); + newIndex.free(); + tree.free(); + commit.free(); + head.free(); + diff.free(); + }); + + test('throws when trying to apply diff to tree and error occurs', () { + final diff = Diff.parse(patchText); + expect( + () => repo.applyToTree(diff: diff, tree: Tree(nullptr)), + throwsA(isA()), + ); + }); }); test('successfully finds similar entries', () {