From 344dba60e97f652e1785576fe7e819e67c4847c4 Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Thu, 16 Sep 2021 16:35:37 +0300 Subject: [PATCH] feat(patch): add bindings and api --- lib/libgit2dart.dart | 1 + lib/src/bindings/diff.dart | 76 +++++----- lib/src/bindings/patch.dart | 289 ++++++++++++++++++++++++++++++++++++ lib/src/blob.dart | 61 ++++++++ lib/src/diff.dart | 110 ++++++++++++++ lib/src/git_types.dart | 43 ++++++ lib/src/patch.dart | 154 +++++++++++++++++++ lib/src/repository.dart | 75 ++++++++++ test/blob_test.dart | 77 ++++++++++ test/diff_test.dart | 68 ++++++++- test/patch_test.dart | 180 ++++++++++++++++++++++ 11 files changed, 1087 insertions(+), 47 deletions(-) create mode 100644 lib/src/bindings/patch.dart create mode 100644 lib/src/patch.dart create mode 100644 test/patch_test.dart diff --git a/lib/libgit2dart.dart b/lib/libgit2dart.dart index 87482b1..6296b3e 100644 --- a/lib/libgit2dart.dart +++ b/lib/libgit2dart.dart @@ -15,5 +15,6 @@ export 'src/treebuilder.dart'; export 'src/branch.dart'; export 'src/worktree.dart'; export 'src/diff.dart'; +export 'src/patch.dart'; export 'src/error.dart'; export 'src/git_types.dart'; diff --git a/lib/src/bindings/diff.dart b/lib/src/bindings/diff.dart index a7425fc..53a13d0 100644 --- a/lib/src/bindings/diff.dart +++ b/lib/src/bindings/diff.dart @@ -15,16 +15,7 @@ Pointer indexToWorkdir( int interhunkLines, ) { final out = calloc>(); - final opts = calloc(); - final optsError = - libgit2.git_diff_options_init(opts, GIT_DIFF_OPTIONS_VERSION); - opts.ref.flags = flags; - opts.ref.context_lines = contextLines; - opts.ref.interhunk_lines = interhunkLines; - - if (optsError < 0) { - throw LibGit2Error(libgit2.git_error_last()); - } + final opts = _diffOptionsInit(flags, contextLines, interhunkLines); libgit2.git_diff_index_to_workdir(out, repo, index, opts); @@ -45,16 +36,7 @@ Pointer treeToIndex( int interhunkLines, ) { final out = calloc>(); - final opts = calloc(); - final optsError = - libgit2.git_diff_options_init(opts, GIT_DIFF_OPTIONS_VERSION); - opts.ref.flags = flags; - opts.ref.context_lines = contextLines; - opts.ref.interhunk_lines = interhunkLines; - - if (optsError < 0) { - throw LibGit2Error(libgit2.git_error_last()); - } + final opts = _diffOptionsInit(flags, contextLines, interhunkLines); libgit2.git_diff_tree_to_index(out, repo, oldTree, index, opts); @@ -74,16 +56,7 @@ Pointer treeToWorkdir( int interhunkLines, ) { final out = calloc>(); - final opts = calloc(); - final optsError = - libgit2.git_diff_options_init(opts, GIT_DIFF_OPTIONS_VERSION); - opts.ref.flags = flags; - opts.ref.context_lines = contextLines; - opts.ref.interhunk_lines = interhunkLines; - - if (optsError < 0) { - throw LibGit2Error(libgit2.git_error_last()); - } + final opts = _diffOptionsInit(flags, contextLines, interhunkLines); libgit2.git_diff_tree_to_workdir(out, repo, oldTree, opts); @@ -104,16 +77,7 @@ Pointer treeToTree( int interhunkLines, ) { final out = calloc>(); - final opts = calloc(); - final optsError = - libgit2.git_diff_options_init(opts, GIT_DIFF_OPTIONS_VERSION); - opts.ref.flags = flags; - opts.ref.context_lines = contextLines; - opts.ref.interhunk_lines = interhunkLines; - - if (optsError < 0) { - throw LibGit2Error(libgit2.git_error_last()); - } + final opts = _diffOptionsInit(flags, contextLines, interhunkLines); libgit2.git_diff_tree_to_tree(out, repo, oldTree, newTree, opts); @@ -290,9 +254,41 @@ String statsPrint( } } +/// Add patch to buffer. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer addToBuf(Pointer patch, Pointer buffer) { + final error = libgit2.git_patch_to_buf(buffer, patch); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return buffer; + } +} + /// Free a previously allocated diff stats. void statsFree(Pointer stats) => libgit2.git_diff_stats_free(stats); /// Free a previously allocated diff. void free(Pointer diff) => libgit2.git_diff_free(diff); + +Pointer _diffOptionsInit( + int flags, + int contextLines, + int interhunkLines, +) { + final opts = calloc(); + final optsError = + libgit2.git_diff_options_init(opts, GIT_DIFF_OPTIONS_VERSION); + opts.ref.flags = flags; + opts.ref.context_lines = contextLines; + opts.ref.interhunk_lines = interhunkLines; + + if (optsError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return opts; + } +} diff --git a/lib/src/bindings/patch.dart b/lib/src/bindings/patch.dart new file mode 100644 index 0000000..a47a54f --- /dev/null +++ b/lib/src/bindings/patch.dart @@ -0,0 +1,289 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import '../error.dart'; +import 'libgit2_bindings.dart'; +import '../util.dart'; + +/// Directly generate a patch from the difference between two buffers. +/// +/// You can use the standard patch accessor functions to read the patch data, and +/// you must call `free()` on the patch when done. +/// +/// Throws a [LibGit2Error] if error occured. +Map fromBuffers( + String? oldBuffer, + String? oldAsPath, + String? newBuffer, + String? newAsPath, + int flags, + int contextLines, + int interhunkLines, +) { + final out = calloc>(); + final oldBufferC = oldBuffer?.toNativeUtf8().cast() ?? nullptr; + final oldAsPathC = oldAsPath?.toNativeUtf8().cast() ?? nullptr; + final oldLen = oldBuffer?.length ?? 0; + final newBufferC = newBuffer?.toNativeUtf8().cast() ?? nullptr; + final newAsPathC = oldAsPath?.toNativeUtf8().cast() ?? nullptr; + final newLen = newBuffer?.length ?? 0; + final opts = _diffOptionsInit(flags, contextLines, interhunkLines); + + final error = libgit2.git_patch_from_buffers( + out, + oldBufferC.cast(), + oldLen, + oldAsPathC, + newBufferC.cast(), + newLen, + newAsPathC, + opts, + ); + + final result = {}; + + calloc.free(oldAsPathC); + calloc.free(newAsPathC); + calloc.free(opts); + + if (error < 0) { + calloc.free(oldBufferC); + calloc.free(newBufferC); + throw LibGit2Error(libgit2.git_error_last()); + } else { + // Returning map with pointers to patch and buffers because patch object does not + // have refenrece to underlying buffers or blobs. So if the buffer or blob is freed/removed + // the patch text becomes corrupted. + result['patch'] = out.value; + result['a'] = oldBufferC; + result['b'] = newBufferC; + return result; + } +} + +/// Directly generate a patch from the difference between two blobs. +/// +/// You can use the standard patch accessor functions to read the patch data, and you +/// must call `free()` on the patch when done. +/// +/// Throws a [LibGit2Error] if error occured. +Map fromBlobs( + Pointer? oldBlob, + String? oldAsPath, + Pointer? newBlob, + String? newAsPath, + int flags, + int contextLines, + int interhunkLines, +) { + final out = calloc>(); + final oldAsPathC = oldAsPath?.toNativeUtf8().cast() ?? nullptr; + final newAsPathC = oldAsPath?.toNativeUtf8().cast() ?? nullptr; + final opts = _diffOptionsInit(flags, contextLines, interhunkLines); + + final error = libgit2.git_patch_from_blobs( + out, + oldBlob ?? nullptr, + oldAsPathC, + newBlob ?? nullptr, + newAsPathC, + opts, + ); + + final result = {}; + + calloc.free(oldAsPathC); + calloc.free(newAsPathC); + calloc.free(opts); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + // Returning map with pointers to patch and blobs because patch object does not + // have refenrece to underlying blobs. So if the blob is freed/removed the patch + // text becomes corrupted. + result['patch'] = out.value; + result['a'] = oldBlob; + result['b'] = newBlob; + return result; + } +} + +/// Directly generate a patch from the difference between a blob and a buffer. +/// +/// You can use the standard patch accessor functions to read the patch data, and you must +/// call `free()` on the patch when done. +/// +/// Throws a [LibGit2Error] if error occured. +Map fromBlobAndBuffer( + Pointer? oldBlob, + String? oldAsPath, + String? buffer, + String? bufferAsPath, + int flags, + int contextLines, + int interhunkLines, +) { + final out = calloc>(); + final oldAsPathC = oldAsPath?.toNativeUtf8().cast() ?? nullptr; + final bufferC = buffer?.toNativeUtf8().cast() ?? nullptr; + final bufferAsPathC = oldAsPath?.toNativeUtf8().cast() ?? nullptr; + final bufferLen = buffer?.length ?? 0; + final opts = _diffOptionsInit(flags, contextLines, interhunkLines); + + final error = libgit2.git_patch_from_blob_and_buffer( + out, + oldBlob ?? nullptr, + oldAsPathC, + bufferC.cast(), + bufferLen, + bufferAsPathC, + opts, + ); + + final result = {}; + + calloc.free(oldAsPathC); + calloc.free(bufferAsPathC); + calloc.free(opts); + + if (error < 0) { + calloc.free(bufferC); + throw LibGit2Error(libgit2.git_error_last()); + } else { + // Returning map with pointers to patch and buffers because patch object does not + // have refenrece to underlying buffers or blobs. So if the buffer or blob is freed/removed + // the patch text becomes corrupted. + result['patch'] = out.value; + result['a'] = oldBlob; + result['b'] = bufferC; + return result; + } +} + +/// Return a patch for an entry in the diff list. +/// +/// The newly created patch object contains the text diffs for the delta. You have to call +/// `free()` when you are done with it. You can use the patch object to loop over all the +/// hunks and lines in the diff of the one delta. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer fromDiff(Pointer diff, int idx) { + final out = calloc>(); + final error = libgit2.git_patch_from_diff(out, diff, idx); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Get the delta associated with a patch. +Pointer delta(Pointer patch) => + libgit2.git_patch_get_delta(patch); + +/// Get the number of hunks in a patch. +int numHunks(Pointer patch) => libgit2.git_patch_num_hunks(patch); + +/// Get the information about a hunk in a patch. +/// +/// Given a patch and a hunk index into the patch, this returns detailed information about that hunk. +/// +/// Throws a [LibGit2Error] if error occured. +Map hunk(Pointer patch, int hunkIdx) { + final out = calloc>(); + final linesInHunk = calloc(); + final error = libgit2.git_patch_get_hunk(out, linesInHunk, patch, hunkIdx); + final result = {}; + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + result['hunk'] = out.value; + result['linesN'] = linesInHunk.value; + return result; + } +} + +/// Get data about a line in a hunk of a patch. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer lines( + Pointer patch, + int hunkIdx, + int lineOfHunk, +) { + final out = calloc>(); + final error = + libgit2.git_patch_get_line_in_hunk(out, patch, hunkIdx, lineOfHunk); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Get the content of a patch as a single diff text. +/// +/// Throws a [LibGit2Error] if error occured. +String text(Pointer patch) { + final out = calloc(sizeOf()); + final error = libgit2.git_patch_to_buf(out, patch); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + final result = out.ref.ptr.cast().toDartString(); + calloc.free(out); + return result; + } +} + +/// Look up size of patch diff data in bytes. +/// +/// This returns the raw size of the patch data. This only includes the actual data from +/// the lines of the diff, not the file or hunk headers. +/// +/// If you pass `includeContext` as true, this will be the size of all of the diff output; +/// if you pass it as false, this will only include the actual changed lines (as if +/// contextLines was 0). +int size( + Pointer patch, + bool includeContext, + bool includeHunkHeaders, + bool includeFileHeaders, +) { + final includeContextC = includeContext ? 1 : 0; + final includeHunkHeadersC = includeHunkHeaders ? 1 : 0; + final includeFileHeadersC = includeFileHeaders ? 1 : 0; + + return libgit2.git_patch_size( + patch, + includeContextC, + includeHunkHeadersC, + includeFileHeadersC, + ); +} + +/// Free a previously allocated patch object. +void free(Pointer patch) => libgit2.git_patch_free(patch); + +Pointer _diffOptionsInit( + int flags, + int contextLines, + int interhunkLines, +) { + final opts = calloc(); + final optsError = + libgit2.git_diff_options_init(opts, GIT_DIFF_OPTIONS_VERSION); + opts.ref.flags = flags; + opts.ref.context_lines = contextLines; + opts.ref.interhunk_lines = interhunkLines; + + if (optsError < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return opts; + } +} diff --git a/lib/src/blob.dart b/lib/src/blob.dart index c518951..7bd9d62 100644 --- a/lib/src/blob.dart +++ b/lib/src/blob.dart @@ -1,6 +1,9 @@ import 'dart:ffi'; import 'bindings/libgit2_bindings.dart'; import 'bindings/blob.dart' as bindings; +import 'bindings/patch.dart' as patch_bindings; +import 'git_types.dart'; +import 'patch.dart'; import 'oid.dart'; import 'repository.dart'; import 'util.dart'; @@ -66,6 +69,64 @@ class Blob { /// Returns the size in bytes of the contents of a blob. int get size => bindings.size(_blobPointer); + /// Directly generate a [Patch] from the difference between two blobs. + /// + /// Should be freed with `free()` to release allocated memory. + /// + /// Throws a [LibGit2Error] if error occured. + Patch diff({ + required Blob? newBlob, + String? oldAsPath, + String? newAsPath, + Set flags = const {GitDiff.normal}, + int contextLines = 3, + int interhunkLines = 0, + }) { + final int flagsInt = + flags.fold(0, (previousValue, e) => previousValue | e.value); + + final result = patch_bindings.fromBlobs( + _blobPointer, + oldAsPath, + newBlob?.pointer, + newAsPath, + flagsInt, + contextLines, + interhunkLines, + ); + + return Patch(result['patch'], result['a'], result['b']); + } + + /// Directly generate a [Patch] from the difference between the blob and a buffer. + /// + /// Should be freed with `free()` to release allocated memory. + /// + /// Throws a [LibGit2Error] if error occured. + Patch diffToBuffer({ + required String? buffer, + String? oldAsPath, + String? bufferAsPath, + Set flags = const {GitDiff.normal}, + int contextLines = 3, + int interhunkLines = 0, + }) { + final int flagsInt = + flags.fold(0, (previousValue, e) => previousValue | e.value); + + final result = patch_bindings.fromBlobAndBuffer( + _blobPointer, + oldAsPath, + buffer, + bufferAsPath, + flagsInt, + contextLines, + interhunkLines, + ); + + return Patch(result['patch'], result['a'], result['b']); + } + /// Releases memory allocated for blob object. void free() => bindings.free(_blobPointer); } diff --git a/lib/src/diff.dart b/lib/src/diff.dart index 261f8f0..dc505b2 100644 --- a/lib/src/diff.dart +++ b/lib/src/diff.dart @@ -2,8 +2,10 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; import 'bindings/libgit2_bindings.dart'; import 'bindings/diff.dart' as bindings; +import 'bindings/patch.dart' as patch_bindings; import 'git_types.dart'; import 'oid.dart'; +import 'patch.dart'; import 'util.dart'; class Diff { @@ -38,6 +40,22 @@ class Diff { return deltas; } + /// Returns a patch diff string. + String get patch { + final length = bindings.length(_diffPointer); + var buffer = calloc(sizeOf()); + + for (var i = 0; i < length; i++) { + final patch = Patch.fromDiff(this, i); + buffer = bindings.addToBuf(patch.pointer, buffer); + patch.free(); + } + + final result = buffer.ref.ptr.cast().toDartString(); + calloc.free(buffer); + return result; + } + /// Accumulates diff statistics for all patches. /// /// Throws a [LibGit2Error] if error occured. @@ -223,3 +241,95 @@ class DiffStats { /// Releases memory allocated for diff stats object. void free() => bindings.statsFree(_diffStatsPointer); } + +class DiffHunk { + /// Initializes a new instance of [DiffHunk] class from provided + /// pointers to patch object and diff hunk object in memory and number of lines in hunk. + DiffHunk( + this._patchPointer, + this._diffHunkPointer, + this.linesCount, + this.index, + ); + + /// Pointer to memory address for allocated diff hunk object. + final Pointer _diffHunkPointer; + + /// Pointer to memory address for allocated patch object. + final Pointer _patchPointer; + + /// Returns count of total lines in this hunk. + late final int linesCount; + + /// Returns index of this hunk in the patch. + late final int index; + + /// Returns starting line number in 'old file'. + int get oldStart => _diffHunkPointer.ref.old_start; + + /// Returns number of lines in 'old file'. + int get oldLines => _diffHunkPointer.ref.old_lines; + + /// Returns starting line number in 'new file'. + int get newStart => _diffHunkPointer.ref.new_start; + + /// Returns number of lines in 'new file'. + int get newLines => _diffHunkPointer.ref.new_lines; + + /// Returns header of a hunk. + String get header { + var list = []; + for (var i = 0; i < _diffHunkPointer.ref.header_len; i++) { + list.add(_diffHunkPointer.ref.header[i]); + } + return String.fromCharCodes(list); + } + + /// Returns list of lines in a hunk of a patch. + /// + /// Throws a [LibGit2Error] if error occured. + List get lines { + var lines = []; + for (var i = 0; i < linesCount; i++) { + lines.add(DiffLine(patch_bindings.lines(_patchPointer, index, i))); + } + return lines; + } +} + +class DiffLine { + /// Initializes a new instance of [DiffLine] class from provided + /// pointer to diff line object in memory. + DiffLine(this._diffLinePointer); + + /// Pointer to memory address for allocated diff line object. + final Pointer _diffLinePointer; + + /// Returns type of the line. + GitDiffLine get origin { + final originInt = _diffLinePointer.ref.origin; + late final GitDiffLine result; + for (var flag in GitDiffLine.values) { + if (originInt == flag.value) { + result = flag; + } + } + return result; + } + + /// Returns line number in old file or -1 for added line. + int get oldLineNumber => _diffLinePointer.ref.old_lineno; + + /// Returns line number in new file or -1 for deleted line. + int get newLineNumber => _diffLinePointer.ref.new_lineno; + + /// Returns number of newline characters in content. + int get numLines => _diffLinePointer.ref.num_lines; + + /// Returns offset in the original file to the content. + int get contentOffset => _diffLinePointer.ref.content_offset; + + /// Returns content of the diff line. + String get content => + _diffLinePointer.ref.content.cast().toDartString(); +} diff --git a/lib/src/git_types.dart b/lib/src/git_types.dart index 4700dbb..b7f9675 100644 --- a/lib/src/git_types.dart +++ b/lib/src/git_types.dart @@ -1003,3 +1003,46 @@ class GitDiffFind { @override String toString() => 'GitDiffFind.$_name'; } + +/// Line origin, describing where a line came from. +class GitDiffLine { + const GitDiffLine._(this._value, this._name); + final int _value; + final String _name; + + static const context = GitDiffLine._(32, 'context'); + static const addition = GitDiffLine._(43, 'addition'); + static const deletion = GitDiffLine._(45, 'deletion'); + + /// Both files have no LF at end. + static const contextEOFNL = GitDiffLine._(61, 'contextEOFNL'); + + /// Old has no LF at end, new does. + static const addEOFNL = GitDiffLine._(62, 'addEOFNL'); + + /// Old has LF at end, new does not. + static const delEOFNL = GitDiffLine._(60, 'delEOFNL'); + + static const fileHeader = GitDiffLine._(70, 'fileHeader'); + static const hunkHeader = GitDiffLine._(72, 'hunkHeader'); + + /// For "Binary files x and y differ" + static const binary = GitDiffLine._(66, 'binary'); + + static const List values = [ + context, + addition, + deletion, + contextEOFNL, + addEOFNL, + delEOFNL, + fileHeader, + hunkHeader, + binary, + ]; + + int get value => _value; + + @override + String toString() => 'GitDiffLine.$_name'; +} diff --git a/lib/src/patch.dart b/lib/src/patch.dart new file mode 100644 index 0000000..fd156cc --- /dev/null +++ b/lib/src/patch.dart @@ -0,0 +1,154 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'bindings/libgit2_bindings.dart'; +import 'bindings/patch.dart' as bindings; +import 'blob.dart'; +import 'diff.dart'; +import 'git_types.dart'; +import 'util.dart'; + +class Patch { + /// Initializes a new instance of [Patch] class from provided + /// pointer to patch object in memory and pointers to old and new blobs/buffers. + /// + /// Should be freed with `free()` to release allocated memory. + Patch(this._patchPointer, this._aPointer, this._bPointer) { + libgit2.git_libgit2_init(); + } + + /// Directly generates a patch from the difference between two blobs, buffers or + /// blob and a buffer. + /// + /// [a] and [b] can be [Blob], [String] or null. + /// + /// Should be freed with `free()` to release allocated memory. + /// + /// Throws a [LibGit2Error] if error occured. + Patch.createFrom({ + required dynamic a, + required dynamic b, + String? aPath, + String? bPath, + Set flags = const {GitDiff.normal}, + int contextLines = 3, + int interhunkLines = 0, + }) { + final int flagsInt = + flags.fold(0, (previousValue, e) => previousValue | e.value); + var result = {}; + + if (a is Blob || a == null) { + if (b is Blob) { + result = bindings.fromBlobs( + a?.pointer, + aPath, + b.pointer, + bPath, + flagsInt, + contextLines, + interhunkLines, + ); + } else if (b is String || b == null) { + result = bindings.fromBlobAndBuffer( + a?.pointer, + aPath, + b, + bPath, + flagsInt, + contextLines, + interhunkLines, + ); + } else { + throw ArgumentError('Provided argument(s) is not Blob or String'); + } + } else if ((a is String || a == null) && (b is String || b == null)) { + result = bindings.fromBuffers( + a, + aPath, + b, + bPath, + flagsInt, + contextLines, + interhunkLines, + ); + } else { + throw ArgumentError('Provided argument(s) is not Blob or String'); + } + + _patchPointer = result['patch']; + _aPointer = result['a']; + _bPointer = result['b']; + } + + /// Returns a patch for an entry in the diff list. + /// + /// Should be freed with `free()` to release allocated memory. + /// + /// Throws a [LibGit2Error] if error occured. + Patch.fromDiff(Diff diff, int index) { + _patchPointer = bindings.fromDiff(diff.pointer, index); + } + + late final Pointer _patchPointer; + + dynamic _aPointer; + dynamic _bPointer; + + /// Pointer to memory address for allocated patch object. + Pointer get pointer => _patchPointer; + + /// Returns the content of a patch as a single diff text. + /// + /// Throws a [LibGit2Error] if error occured. + String get text => bindings.text(_patchPointer); + + /// Looks up size of patch diff data in bytes. + /// + /// This returns the raw size of the patch data. This only includes the actual data from + /// the lines of the diff, not the file or hunk headers. + /// + /// If you pass `includeContext` as true, this will be the size of all of the diff output; + /// if you pass it as false, this will only include the actual changed lines (as if + /// contextLines was 0). + int size({ + bool includeContext = false, + bool includeHunkHeaders = false, + bool includeFileHeaders = false, + }) { + return bindings.size( + _patchPointer, + includeContext, + includeHunkHeaders, + includeFileHeaders, + ); + } + + /// Returns the delta associated with a patch. + DiffDelta get delta => DiffDelta(bindings.delta(_patchPointer)); + + /// Returns the list of hunks in a patch. + /// + /// Throws a [LibGit2Error] if error occured. + List get hunks { + final length = bindings.numHunks(_patchPointer); + final hunks = []; + + for (var i = 0; i < length; i++) { + final hunk = bindings.hunk(_patchPointer, i); + hunks.add(DiffHunk(_patchPointer, hunk['hunk'], hunk['linesN'], i)); + } + + return hunks; + } + + /// Releases memory allocated for patch object. + void free() { + if (_aPointer != null) { + calloc.free(_aPointer); + } + if (_bPointer != null) { + calloc.free(_bPointer); + } + bindings.free(_patchPointer); + } +} diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 07294b6..2ed521a 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -9,6 +9,7 @@ import 'bindings/status.dart' as status_bindings; 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 'branch.dart'; import 'commit.dart'; import 'config.dart'; @@ -723,4 +724,78 @@ class Repository { object_bindings.free(object); } + + /// Returns a [Diff] with changes between the trees, tree and index, tree and workdir or + /// index and workdir. + /// + /// If [b] is null, by default the [a] tree compared to working directory. If [cached] is + /// set to true the [a] tree compared to index/staging area. + /// + /// Throws a [LibGit2Error] if error occured. + Diff diff({ + Tree? a, + Tree? b, + bool cached = false, + Set flags = const {GitDiff.normal}, + int contextLines = 3, + int interhunkLines = 0, + }) { + final int flagsInt = + flags.fold(0, (previousValue, e) => previousValue | e.value); + + if (a is Tree && b is Tree) { + return Diff(diff_bindings.treeToTree( + _repoPointer, + a.pointer, + b.pointer, + flagsInt, + contextLines, + interhunkLines, + )); + } else if (a is Tree && b == null) { + if (cached) { + return Diff(diff_bindings.treeToIndex( + _repoPointer, + a.pointer, + index.pointer, + flagsInt, + contextLines, + interhunkLines, + )); + } else { + return Diff(diff_bindings.treeToWorkdir( + _repoPointer, + a.pointer, + flagsInt, + contextLines, + interhunkLines, + )); + } + } else if (a == null && b == null) { + return Diff(diff_bindings.indexToWorkdir( + _repoPointer, + index.pointer, + flagsInt, + contextLines, + interhunkLines, + )); + } else { + throw ArgumentError.notNull('a'); + } + } + + /// Returns a [Patch] with changes between the blobs. + /// + /// Throws a [LibGit2Error] if error occured. + Patch diffBlobs({ + required Blob a, + required Blob b, + String? aPath, + String? bPath, + Set flags = const {GitDiff.normal}, + int contextLines = 3, + int interhunkLines = 0, + }) { + return a.diff(newBlob: b, oldAsPath: aPath, newAsPath: bPath); + } } diff --git a/test/blob_test.dart b/test/blob_test.dart index 7e16fc6..3b08bf1 100644 --- a/test/blob_test.dart +++ b/test/blob_test.dart @@ -96,5 +96,82 @@ void main() { newBlob.free(); }); + + group('diff', () { + const path = 'feature_file'; + const oldBlobSha = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'; + const newBlobSha = '9c78c21d6680a7ffebc76f7ac68cacc11d8f48bc'; + const blobPatch = """ +diff --git a/feature_file b/feature_file +index e69de29..9c78c21 100644 +--- a/feature_file ++++ b/feature_file +@@ -0,0 +1 @@ ++Feature edit +"""; + + const blobPatchDelete = """ +diff --git a/feature_file b/feature_file +deleted file mode 100644 +index e69de29..0000000 +--- a/feature_file ++++ /dev/null +"""; + test('successfully creates from blobs', () { + final a = repo[oldBlobSha] as Blob; + final b = repo[newBlobSha] as Blob; + final patch = repo.diffBlobs( + a: a, + b: b, + aPath: path, + bPath: path, + ); + + expect(patch.text, blobPatch); + + patch.free(); + }); + + test('successfully creates from one blob (delete)', () { + final a = repo[oldBlobSha] as Blob; + final patch = a.diff( + newBlob: null, + oldAsPath: path, + newAsPath: path, + ); + + expect(patch.text, blobPatchDelete); + + patch.free(); + }); + + test('successfully creates from blob and buffer', () { + final a = repo[oldBlobSha] as Blob; + final patch = Patch.createFrom( + a: a, + b: 'Feature edit\n', + aPath: path, + bPath: path, + ); + + expect(patch.text, blobPatch); + + patch.free(); + }); + + test('successfully creates from blob and buffer (delete)', () { + final a = repo[oldBlobSha] as Blob; + final patch = Patch.createFrom( + a: a, + b: null, + aPath: path, + bPath: path, + ); + + expect(patch.text, blobPatchDelete); + + patch.free(); + }); + }); }); } diff --git a/test/diff_test.dart b/test/diff_test.dart index 54bff98..0e5497d 100644 --- a/test/diff_test.dart +++ b/test/diff_test.dart @@ -54,7 +54,7 @@ void main() { 'subdir/modified_file', ]; - const patch = """ + const patchText = """ diff --git a/subdir/modified_file b/subdir/modified_file index e69de29..c217c63 100644 --- a/subdir/modified_file @@ -94,7 +94,7 @@ index e69de29..c217c63 100644 group('Diff', () { test('successfully returns diff between index and workdir', () { final index = repo.index; - final diff = index.diffToWorkdir(); + final diff = repo.diff(); expect(diff.length, 8); for (var i = 0; i < diff.deltas.length; i++) { @@ -122,7 +122,7 @@ index e69de29..c217c63 100644 test('successfully returns diff between tree and workdir', () { final tree = (repo[repo.head.target.sha] as Commit).tree; - final diff = tree.diffToWorkdir(); + final diff = repo.diff(a: tree); expect(diff.length, 9); for (var i = 0; i < diff.deltas.length; i++) { @@ -136,7 +136,7 @@ index e69de29..c217c63 100644 test('successfully returns diff between tree and index', () { final index = repo.index; final tree = (repo[repo.head.target.sha] as Commit).tree; - final diff = tree.diffToIndex(index: index); + final diff = repo.diff(a: tree, cached: true); expect(diff.length, 8); for (var i = 0; i < diff.deltas.length; i++) { @@ -151,7 +151,7 @@ index e69de29..c217c63 100644 test('successfully returns diff between tree and tree', () { final tree1 = (repo[repo.head.target.sha] as Commit).tree; final tree2 = repo['b85d53c9236e89aff2b62558adaa885fd1d6ff1c'] as Tree; - final diff = tree1.diffToTree(tree: tree2); + final diff = repo.diff(a: tree1, b: tree2); expect(diff.length, 10); for (var i = 0; i < diff.deltas.length; i++) { @@ -182,7 +182,7 @@ index e69de29..c217c63 100644 }); test('successfully parses provided diff', () { - final diff = Diff.parse(patch); + final diff = Diff.parse(patchText); final stats = diff.stats; expect(diff.length, 1); @@ -194,6 +194,17 @@ index e69de29..c217c63 100644 diff.free(); }); + test('successfully creates patch from entry index in diff', () { + final diff = Diff.parse(patchText); + final patch = Patch.fromDiff(diff, 0); + + expect(diff.length, 1); + expect(patch.text, patchText); + + patch.free(); + diff.free(); + }); + test('successfully finds similar entries', () { final index = repo.index; final oldTree = (repo[repo.head.target.sha] as Commit).tree; @@ -217,7 +228,7 @@ index e69de29..c217c63 100644 newTree.free(); }); - test('returns deltas and patches', () { + test('returns deltas', () { final index = repo.index; final diff = index.diffToWorkdir(); @@ -268,5 +279,48 @@ index e69de29..c217c63 100644 diff.free(); index.free(); }); + + test('returns patch diff string', () { + final diff = Diff.parse(patchText); + + expect(diff.patch, patchText); + + diff.free(); + }); + + test('returns hunks in a patch', () { + final diff = Diff.parse(patchText); + final patch = Patch.fromDiff(diff, 0); + final hunk = patch.hunks[0]; + + expect(patch.hunks.length, 1); + expect(hunk.linesCount, 1); + expect(hunk.oldStart, 0); + expect(hunk.oldLines, 0); + expect(hunk.newStart, 1); + expect(hunk.newLines, 1); + expect(hunk.header, '\x00\x00\x00\x00@@ -0,0 +1'); + + patch.free(); + diff.free(); + }); + + test('returns lines in a hunk', () { + final diff = Diff.parse(patchText); + final patch = Patch.fromDiff(diff, 0); + final hunk = patch.hunks[0]; + final line = hunk.lines[0]; + + expect(hunk.lines.length, 1); + expect(line.origin, GitDiffLine.addition); + expect(line.oldLineNumber, -1); + expect(line.newLineNumber, 1); + expect(line.numLines, 1); + expect(line.contentOffset, 155); + expect(line.content, 'Modified content\n'); + + patch.free(); + diff.free(); + }); }); } diff --git a/test/patch_test.dart b/test/patch_test.dart new file mode 100644 index 0000000..499bdc7 --- /dev/null +++ b/test/patch_test.dart @@ -0,0 +1,180 @@ +import 'dart:io'; +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'helpers/util.dart'; + +void main() { + late Repository repo; + final tmpDir = '${Directory.systemTemp.path}/patch_testrepo/'; + const oldBlob = ''; + const newBlob = 'Feature edit\n'; + const path = 'feature_file'; + const oldBlobSha = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'; + const newBlobSha = '9c78c21d6680a7ffebc76f7ac68cacc11d8f48bc'; + const blobPatch = """ +diff --git a/feature_file b/feature_file +index e69de29..9c78c21 100644 +--- a/feature_file ++++ b/feature_file +@@ -0,0 +1 @@ ++Feature edit +"""; + + const blobPatchAdd = """ +diff --git a/feature_file b/feature_file +new file mode 100644 +index 0000000..9c78c21 +--- /dev/null ++++ b/feature_file +@@ -0,0 +1 @@ ++Feature edit +"""; + + const blobPatchDelete = """ +diff --git a/feature_file b/feature_file +deleted file mode 100644 +index e69de29..0000000 +--- a/feature_file ++++ /dev/null +"""; + + 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); + }); + + tearDown(() async { + repo.free(); + await Directory(tmpDir).delete(recursive: true); + }); + + group('Patch', () { + test('successfully creates from buffers', () { + final patch = Patch.createFrom( + a: oldBlob, + b: newBlob, + aPath: path, + bPath: path, + ); + + expect(patch.size(), 14); + expect(patch.text, blobPatch); + + patch.free(); + }); + + test('successfully creates from one buffer (add)', () { + final patch = Patch.createFrom( + a: null, + b: newBlob, + aPath: path, + bPath: path, + ); + + expect(patch.text, blobPatchAdd); + + patch.free(); + }); + + test('successfully creates from one buffer (delete)', () { + final patch = Patch.createFrom( + a: oldBlob, + b: null, + aPath: path, + bPath: path, + ); + + expect(patch.text, blobPatchDelete); + + patch.free(); + }); + + test('successfully creates from blobs', () { + final a = repo[oldBlobSha] as Blob; + final b = repo[newBlobSha] as Blob; + final patch = Patch.createFrom( + a: a, + b: b, + aPath: path, + bPath: path, + ); + + expect(patch.text, blobPatch); + + patch.free(); + }); + + test('successfully creates from one blob (add)', () { + final b = repo[newBlobSha] as Blob; + final patch = Patch.createFrom( + a: null, + b: b, + aPath: path, + bPath: path, + ); + + expect(patch.text, blobPatchAdd); + + patch.free(); + }); + + test('successfully creates from one blob (delete)', () { + final a = repo[oldBlobSha] as Blob; + final patch = Patch.createFrom( + a: a, + b: null, + aPath: path, + bPath: path, + ); + + expect(patch.text, blobPatchDelete); + + patch.free(); + }); + + test('successfully creates from blob and buffer', () { + final a = repo[oldBlobSha] as Blob; + final patch = Patch.createFrom( + a: a, + b: newBlob, + aPath: path, + bPath: path, + ); + + expect(patch.text, blobPatch); + + patch.free(); + }); + + test('throws when argument is not Blob or String', () { + final commit = repo['fc38877b2552ab554752d9a77e1f48f738cca79b'] as Commit; + expect( + () => Patch.createFrom( + a: commit, + b: null, + aPath: 'file', + bPath: 'file', + ), + throwsA(isA()), + ); + + expect( + () => Patch.createFrom( + a: null, + b: commit, + aPath: 'file', + bPath: 'file', + ), + throwsA(isA()), + ); + + commit.free(); + }); + }); +}