feat(patch): add bindings and api

This commit is contained in:
Aleksey Kulikov 2021-09-16 16:35:37 +03:00
parent f7f4a395c0
commit 344dba60e9
11 changed files with 1087 additions and 47 deletions

View file

@ -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';

View file

@ -15,16 +15,7 @@ Pointer<git_diff> indexToWorkdir(
int interhunkLines,
) {
final out = calloc<Pointer<git_diff>>();
final opts = calloc<git_diff_options>();
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<git_diff> treeToIndex(
int interhunkLines,
) {
final out = calloc<Pointer<git_diff>>();
final opts = calloc<git_diff_options>();
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<git_diff> treeToWorkdir(
int interhunkLines,
) {
final out = calloc<Pointer<git_diff>>();
final opts = calloc<git_diff_options>();
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<git_diff> treeToTree(
int interhunkLines,
) {
final out = calloc<Pointer<git_diff>>();
final opts = calloc<git_diff_options>();
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<git_buf> addToBuf(Pointer<git_patch> patch, Pointer<git_buf> 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<git_diff_stats> stats) =>
libgit2.git_diff_stats_free(stats);
/// Free a previously allocated diff.
void free(Pointer<git_diff> diff) => libgit2.git_diff_free(diff);
Pointer<git_diff_options> _diffOptionsInit(
int flags,
int contextLines,
int interhunkLines,
) {
final opts = calloc<git_diff_options>();
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;
}
}

289
lib/src/bindings/patch.dart Normal file
View file

@ -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<String, dynamic> fromBuffers(
String? oldBuffer,
String? oldAsPath,
String? newBuffer,
String? newAsPath,
int flags,
int contextLines,
int interhunkLines,
) {
final out = calloc<Pointer<git_patch>>();
final oldBufferC = oldBuffer?.toNativeUtf8().cast<Int8>() ?? nullptr;
final oldAsPathC = oldAsPath?.toNativeUtf8().cast<Int8>() ?? nullptr;
final oldLen = oldBuffer?.length ?? 0;
final newBufferC = newBuffer?.toNativeUtf8().cast<Int8>() ?? nullptr;
final newAsPathC = oldAsPath?.toNativeUtf8().cast<Int8>() ?? 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 = <String, dynamic>{};
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<String, dynamic> fromBlobs(
Pointer<git_blob>? oldBlob,
String? oldAsPath,
Pointer<git_blob>? newBlob,
String? newAsPath,
int flags,
int contextLines,
int interhunkLines,
) {
final out = calloc<Pointer<git_patch>>();
final oldAsPathC = oldAsPath?.toNativeUtf8().cast<Int8>() ?? nullptr;
final newAsPathC = oldAsPath?.toNativeUtf8().cast<Int8>() ?? nullptr;
final opts = _diffOptionsInit(flags, contextLines, interhunkLines);
final error = libgit2.git_patch_from_blobs(
out,
oldBlob ?? nullptr,
oldAsPathC,
newBlob ?? nullptr,
newAsPathC,
opts,
);
final result = <String, dynamic>{};
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<String, dynamic> fromBlobAndBuffer(
Pointer<git_blob>? oldBlob,
String? oldAsPath,
String? buffer,
String? bufferAsPath,
int flags,
int contextLines,
int interhunkLines,
) {
final out = calloc<Pointer<git_patch>>();
final oldAsPathC = oldAsPath?.toNativeUtf8().cast<Int8>() ?? nullptr;
final bufferC = buffer?.toNativeUtf8().cast<Int8>() ?? nullptr;
final bufferAsPathC = oldAsPath?.toNativeUtf8().cast<Int8>() ?? 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 = <String, dynamic>{};
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<git_patch> fromDiff(Pointer<git_diff> diff, int idx) {
final out = calloc<Pointer<git_patch>>();
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<git_diff_delta> delta(Pointer<git_patch> patch) =>
libgit2.git_patch_get_delta(patch);
/// Get the number of hunks in a patch.
int numHunks(Pointer<git_patch> 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<String, dynamic> hunk(Pointer<git_patch> patch, int hunkIdx) {
final out = calloc<Pointer<git_diff_hunk>>();
final linesInHunk = calloc<Int32>();
final error = libgit2.git_patch_get_hunk(out, linesInHunk, patch, hunkIdx);
final result = <String, dynamic>{};
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<git_diff_line> lines(
Pointer<git_patch> patch,
int hunkIdx,
int lineOfHunk,
) {
final out = calloc<Pointer<git_diff_line>>();
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<git_patch> patch) {
final out = calloc<git_buf>(sizeOf<git_buf>());
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<Utf8>().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<git_patch> 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<git_patch> patch) => libgit2.git_patch_free(patch);
Pointer<git_diff_options> _diffOptionsInit(
int flags,
int contextLines,
int interhunkLines,
) {
final opts = calloc<git_diff_options>();
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;
}
}

View file

@ -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<GitDiff> 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<GitDiff> 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);
}

View file

@ -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<git_buf>(sizeOf<git_buf>());
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<Utf8>().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<git_diff_hunk> _diffHunkPointer;
/// Pointer to memory address for allocated patch object.
final Pointer<git_patch> _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 = <int>[];
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<DiffLine> get lines {
var lines = <DiffLine>[];
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<git_diff_line> _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<Utf8>().toDartString();
}

View file

@ -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<GitDiffLine> values = [
context,
addition,
deletion,
contextEOFNL,
addEOFNL,
delEOFNL,
fileHeader,
hunkHeader,
binary,
];
int get value => _value;
@override
String toString() => 'GitDiffLine.$_name';
}

154
lib/src/patch.dart Normal file
View file

@ -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<GitDiff> flags = const {GitDiff.normal},
int contextLines = 3,
int interhunkLines = 0,
}) {
final int flagsInt =
flags.fold(0, (previousValue, e) => previousValue | e.value);
var result = <String, dynamic>{};
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<git_patch> _patchPointer;
dynamic _aPointer;
dynamic _bPointer;
/// Pointer to memory address for allocated patch object.
Pointer<git_patch> 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<DiffHunk> get hunks {
final length = bindings.numHunks(_patchPointer);
final hunks = <DiffHunk>[];
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);
}
}

View file

@ -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<GitDiff> 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<GitDiff> flags = const {GitDiff.normal},
int contextLines = 3,
int interhunkLines = 0,
}) {
return a.diff(newBlob: b, oldAsPath: aPath, newAsPath: bPath);
}
}

View file

@ -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();
});
});
});
}

View file

@ -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();
});
});
}

180
test/patch_test.dart Normal file
View file

@ -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<ArgumentError>()),
);
expect(
() => Patch.createFrom(
a: null,
b: commit,
aPath: 'file',
bPath: 'file',
),
throwsA(isA<ArgumentError>()),
);
commit.free();
});
});
}