feat(diff): add bindings and api

This commit is contained in:
Aleksey Kulikov 2021-09-14 19:55:25 +03:00
parent 2ae5751efa
commit a7b714c2f3
47 changed files with 1789 additions and 4 deletions

277
lib/src/bindings/diff.dart Normal file
View file

@ -0,0 +1,277 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import '../error.dart';
import 'libgit2_bindings.dart';
import '../util.dart';
/// Create a diff between the repository index and the workdir directory.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_diff> indexToWorkdir(
Pointer<git_repository> repo,
Pointer<git_index> index,
int flags,
int contextLines,
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());
}
libgit2.git_diff_index_to_workdir(out, repo, index, opts);
calloc.free(opts);
return out.value;
}
/// Create a diff between a tree and repository index.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_diff> treeToIndex(
Pointer<git_repository> repo,
Pointer<git_tree> oldTree,
Pointer<git_index> index,
int flags,
int contextLines,
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());
}
libgit2.git_diff_tree_to_index(out, repo, oldTree, index, opts);
calloc.free(opts);
return out.value;
}
/// Create a diff between a tree and the working directory.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_diff> treeToWorkdir(
Pointer<git_repository> repo,
Pointer<git_tree> oldTree,
int flags,
int contextLines,
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());
}
libgit2.git_diff_tree_to_workdir(out, repo, oldTree, opts);
calloc.free(opts);
return out.value;
}
/// Create a diff with the difference between two tree objects.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_diff> treeToTree(
Pointer<git_repository> repo,
Pointer<git_tree> oldTree,
Pointer<git_tree> newTree,
int flags,
int contextLines,
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());
}
libgit2.git_diff_tree_to_tree(out, repo, oldTree, newTree, opts);
calloc.free(opts);
return out.value;
}
/// Query how many diff records are there in a diff.
int length(Pointer<git_diff> diff) => libgit2.git_diff_num_deltas(diff);
/// Merge one diff into another.
///
/// This merges items from the "from" list into the "onto" list. The resulting diff
/// will have all items that appear in either list. If an item appears in both lists,
/// then it will be "merged" to appear as if the old version was from the "onto" list
/// and the new version is from the "from" list (with the exception that if the item
/// has a pending DELETE in the middle, then it will show as deleted).
void merge(Pointer<git_diff> onto, Pointer<git_diff> from) {
libgit2.git_diff_merge(onto, from);
}
/// Read the contents of a git patch file into a git diff object.
///
/// The diff object produced is similar to the one that would be produced if you actually
/// produced it computationally by comparing two trees, however there may be subtle differences.
/// For example, a patch file likely contains abbreviated object IDs, so the object IDs in a
/// diff delta produced by this function will also be abbreviated.
///
/// This function will only read patch files created by a git implementation, it will not
/// read unified diffs produced by the `diff` program, nor any other types of patch files.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_diff> parse(String content) {
final out = calloc<Pointer<git_diff>>();
final contentC = content.toNativeUtf8().cast<Int8>();
final error = libgit2.git_diff_from_buffer(out, contentC, content.length);
calloc.free(contentC);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
} else {
return out.value;
}
}
/// Transform a diff marking file renames, copies, etc.
///
/// This modifies a diff in place, replacing old entries that look like renames or copies
/// with new entries reflecting those changes. This also will, if requested, break modified
/// files into add/remove pairs if the amount of change is above a threshold.
///
/// Throws a [LibGit2Error] if error occured.
void findSimilar(
Pointer<git_diff> diff,
int flags,
int renameThreshold,
int copyThreshold,
int renameFromRewriteThreshold,
int breakRewriteThreshold,
int renameLimit) {
final opts = calloc<git_diff_find_options>();
final optsError =
libgit2.git_diff_find_options_init(opts, GIT_DIFF_FIND_OPTIONS_VERSION);
opts.ref.flags = flags;
opts.ref.rename_threshold = renameThreshold;
opts.ref.copy_threshold = copyThreshold;
opts.ref.rename_from_rewrite_threshold = renameFromRewriteThreshold;
opts.ref.break_rewrite_threshold = breakRewriteThreshold;
opts.ref.rename_limit = renameLimit;
if (optsError < 0) {
throw LibGit2Error(libgit2.git_error_last());
}
final error = libgit2.git_diff_find_similar(diff, opts);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
}
calloc.free(opts);
}
/// Return the diff delta for an entry in the diff list.
///
/// Throws [RangeError] if index out of range.
Pointer<git_diff_delta> getDeltaByIndex(Pointer<git_diff> diff, int idx) {
final result = libgit2.git_diff_get_delta(diff, idx);
if (result == nullptr) {
throw RangeError('$idx is out of bounds');
} else {
return result;
}
}
/// Look up the single character abbreviation for a delta status code.
///
/// When you run `git diff --name-status` it uses single letter codes in the output such as
/// 'A' for added, 'D' for deleted, 'M' for modified, etc. This function converts a [GitDelta]
/// value into these letters for your own purposes. [GitDelta.untracked] will return
/// a space (i.e. ' ').
String statusChar(int status) {
final result = libgit2.git_diff_status_char(status);
return String.fromCharCode(result);
}
/// Accumulate diff statistics for all patches.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_diff_stats> stats(Pointer<git_diff> diff) {
final out = calloc<Pointer<git_diff_stats>>();
final error = libgit2.git_diff_get_stats(out, diff);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
} else {
return out.value;
}
}
/// Get the total number of insertions in a diff.
int statsInsertions(Pointer<git_diff_stats> stats) =>
libgit2.git_diff_stats_insertions(stats);
/// Get the total number of deletions in a diff.
int statsDeletions(Pointer<git_diff_stats> stats) =>
libgit2.git_diff_stats_deletions(stats);
/// Get the total number of files changed in a diff.
int statsFilesChanged(Pointer<git_diff_stats> stats) =>
libgit2.git_diff_stats_files_changed(stats);
/// Print diff statistics.
///
/// Throws a [LibGit2Error] if error occured.
String statsPrint(
Pointer<git_diff_stats> stats,
int format,
int width,
) {
final out = calloc<git_buf>(sizeOf<git_buf>());
final error = libgit2.git_diff_stats_to_buf(out, stats, format, width);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
} else {
final result = out.ref.ptr.cast<Utf8>().toDartString();
calloc.free(out);
return result;
}
}
/// 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);

213
lib/src/diff.dart Normal file
View file

@ -0,0 +1,213 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'bindings/libgit2_bindings.dart';
import 'bindings/diff.dart' as bindings;
import 'git_types.dart';
import 'oid.dart';
import 'util.dart';
class Diff {
/// Initializes a new instance of [Diff] class from provided
/// pointer to diff object in memory.
///
/// Should be freed with `free()` to release allocated memory.
Diff(this._diffPointer) {
libgit2.git_libgit2_init();
}
Diff.parse(String content) {
libgit2.git_libgit2_init();
_diffPointer = bindings.parse(content);
}
late final Pointer<git_diff> _diffPointer;
/// Pointer to memory address for allocated diff object.
Pointer<git_diff> get pointer => _diffPointer;
/// Queries how many diff records are there in a diff.
int get length => bindings.length(_diffPointer);
/// Returns a list of [DiffDelta]s containing file pairs with and old and new revisions.
List<DiffDelta> get deltas {
final length = bindings.length(_diffPointer);
var deltas = <DiffDelta>[];
for (var i = 0; i < length; i++) {
deltas.add(DiffDelta(bindings.getDeltaByIndex(_diffPointer, i)));
}
return deltas;
}
/// Accumulates diff statistics for all patches.
///
/// Throws a [LibGit2Error] if error occured.
DiffStats get stats => DiffStats(bindings.stats(_diffPointer));
/// Merges one diff into another.
void merge(Diff diff) => bindings.merge(_diffPointer, diff.pointer);
/// Transforms a diff marking file renames, copies, etc.
///
/// This modifies a diff in place, replacing old entries that look like renames or copies
/// with new entries reflecting those changes. This also will, if requested, break modified
/// files into add/remove pairs if the amount of change is above a threshold.
///
/// Throws a [LibGit2Error] if error occured.
void findSimilar({
Set<GitDiffFind> flags = const {GitDiffFind.byConfig},
int renameThreshold = 50,
int copyThreshold = 50,
int renameFromRewriteThreshold = 50,
int breakRewriteThreshold = 60,
int renameLimit = 200,
}) {
final int flagsInt =
flags.fold(0, (previousValue, e) => previousValue | e.value);
bindings.findSimilar(
_diffPointer,
flagsInt,
renameThreshold,
copyThreshold,
renameFromRewriteThreshold,
breakRewriteThreshold,
renameLimit,
);
}
/// Releases memory allocated for diff object.
void free() => bindings.free(_diffPointer);
}
class DiffDelta {
/// Initializes a new instance of [DiffDelta] class from provided
/// pointer to diff delta object in memory.
DiffDelta(this._diffDeltaPointer);
/// Pointer to memory address for allocated diff delta object.
final Pointer<git_diff_delta> _diffDeltaPointer;
/// Returns type of change.
GitDelta get status {
late final GitDelta status;
for (var type in GitDelta.values) {
if (_diffDeltaPointer.ref.status == type.value) {
status = type;
}
}
return status;
}
/// Looks up the single character abbreviation for a delta status code.
///
/// When you run `git diff --name-status` it uses single letter codes in the output such as
/// 'A' for added, 'D' for deleted, 'M' for modified, etc. This function converts a [GitDelta]
/// value into these letters for your own purposes. [GitDelta.untracked] will return
/// a space (i.e. ' ').
String get statusChar => bindings.statusChar(_diffDeltaPointer.ref.status);
/// Returns flags for the delta object.
Set<GitDiffFlag> get flags {
var flags = <GitDiffFlag>{};
for (var flag in GitDiffFlag.values) {
if (_diffDeltaPointer.ref.flags & flag.value == flag.value) {
flags.add(flag);
}
}
return flags;
}
/// Returns a similarity score for renamed or copied files between 0 and 100
/// indicating how similar the old and new sides are.
///
/// The similarity score is zero unless you call `find_similar()` which does
/// a similarity analysis of files in the diff.
int get similarity => _diffDeltaPointer.ref.similarity;
/// Returns number of files in this delta.
int get numberOfFiles => _diffDeltaPointer.ref.nfiles;
/// Represents the "from" side of the diff.
DiffFile get oldFile => DiffFile(_diffDeltaPointer.ref.old_file);
/// Represents the "to" side of the diff.
DiffFile get newFile => DiffFile(_diffDeltaPointer.ref.new_file);
}
/// Description of one side of a delta.
///
/// Although this is called a "file", it could represent a file, a symbolic
/// link, a submodule commit id, or even a tree (although that only if you
/// are tracking type changes or ignored/untracked directories).
class DiffFile {
/// Initializes a new instance of [DiffFile] class from provided diff file object.
DiffFile(this._diffFile);
final git_diff_file _diffFile;
/// Returns oid of the item. If the entry represents an absent side of a diff
/// then the oid will be zeroes.
Oid get id => Oid.fromRaw(_diffFile.id);
/// Returns path to the entry relative to the working directory of the repository.
String get path => _diffFile.path.cast<Utf8>().toDartString();
/// Returns the size of the entry in bytes.
int get size => _diffFile.size;
/// Returns flags for the diff file object.
Set<GitDiffFlag> get flags {
var flags = <GitDiffFlag>{};
for (var flag in GitDiffFlag.values) {
if (_diffFile.flags & flag.value == flag.value) {
flags.add(flag);
}
}
return flags;
}
/// Returns one of the [GitFilemode] values.
GitFilemode get mode {
late final GitFilemode result;
for (var mode in GitFilemode.values) {
if (_diffFile.mode == mode.value) {
result = mode;
}
}
return result;
}
}
class DiffStats {
/// Initializes a new instance of [DiffStats] class from provided
/// pointer to diff stats object in memory.
DiffStats(this._diffStatsPointer);
/// Pointer to memory address for allocated diff delta object.
final Pointer<git_diff_stats> _diffStatsPointer;
/// Returns the total number of insertions.
int get insertions => bindings.statsInsertions(_diffStatsPointer);
/// Returns the total number of deletions.
int get deletions => bindings.statsDeletions(_diffStatsPointer);
/// Returns the total number of files changed.
int get filesChanged => bindings.statsFilesChanged(_diffStatsPointer);
/// Print diff statistics.
///
/// Width for output only affects formatting of [GitDiffStats.full].
///
/// Throws a [LibGit2Error] if error occured.
String print(Set<GitDiffStats> format, int width) {
final int formatInt =
format.fold(0, (previousValue, e) => previousValue | e.value);
return bindings.statsPrint(_diffStatsPointer, formatInt, width);
}
/// Releases memory allocated for diff stats object.
void free() => bindings.statsFree(_diffStatsPointer);
}

View file

@ -601,3 +601,405 @@ class GitReset {
@override
String toString() => 'GitReset.$_name';
}
/// Flags for diff options. A combination of these flags can be passed.
class GitDiff {
const GitDiff._(this._value, this._name);
final int _value;
final String _name;
/// Normal diff, the default.
static const normal = GitDiff._(0, 'normal');
/// Reverse the sides of the diff.
static const reverse = GitDiff._(1, 'reverse');
/// Include ignored files in the diff.
static const includeIgnored = GitDiff._(2, 'includeIgnored');
/// Even with [GitDiff.includeUntracked], an entire ignored directory
/// will be marked with only a single entry in the diff; this flag
/// adds all files under the directory as IGNORED entries, too.
static const recurseIgnoredDirs = GitDiff._(4, 'recurseIgnoredDirs');
/// Include untracked files in the diff.
static const includeUntracked = GitDiff._(8, 'includeUntracked');
/// Even with [GitDiff.includeUntracked], an entire untracked
/// directory will be marked with only a single entry in the diff
/// (a la what core Git does in `git status`); this flag adds *all*
/// files under untracked directories as UNTRACKED entries, too.
static const recurseUntrackedDirs = GitDiff._(16, 'recurseUntrackedDirs');
/// Include unmodified files in the diff.
static const includeUnmodified = GitDiff._(32, 'includeUnmodified');
/// Normally, a type change between files will be converted into a
/// DELETED record for the old and an ADDED record for the new; this
/// options enabled the generation of TYPECHANGE delta records.
static const includeTypechange = GitDiff._(64, 'includeTypechange');
/// Even with [GitDiff.includeTypechange], blob->tree changes still
/// generally show as a DELETED blob. This flag tries to correctly
/// label blob->tree transitions as TYPECHANGE records with new_file's
/// mode set to tree. Note: the tree SHA will not be available.
static const includeTypechangeTrees =
GitDiff._(128, 'includeTypechangeTrees');
/// Ignore file mode changes.
static const ignoreFilemode = GitDiff._(256, 'ignoreFilemode');
/// Treat all submodules as unmodified.
static const ignoreSubmodules = GitDiff._(512, 'ignoreSubmodules');
/// Use case insensitive filename comparisons.
static const ignoreCase = GitDiff._(1024, 'ignoreCase');
/// May be combined with [GitDiff.ignoreCase] to specify that a file
/// that has changed case will be returned as an add/delete pair.
static const includeCaseChange = GitDiff._(2048, 'includeCaseChange');
/// If the pathspec is set in the diff options, this flags indicates
/// that the paths will be treated as literal paths instead of
/// fnmatch patterns. Each path in the list must either be a full
/// path to a file or a directory. (A trailing slash indicates that
/// the path will _only_ match a directory). If a directory is
/// specified, all children will be included.
static const disablePathspecMatch = GitDiff._(4096, 'disablePathspecMatch');
/// Disable updating of the `binary` flag in delta records. This is
/// useful when iterating over a diff if you don't need hunk and data
/// callbacks and want to avoid having to load file completely.
static const skipBinaryCheck = GitDiff._(8192, 'skipBinaryCheck');
/// When diff finds an untracked directory, to match the behavior of
/// core Git, it scans the contents for IGNORED and UNTRACKED files.
/// If *all* contents are IGNORED, then the directory is IGNORED; if
/// any contents are not IGNORED, then the directory is UNTRACKED.
/// This is extra work that may not matter in many cases. This flag
/// turns off that scan and immediately labels an untracked directory
/// as UNTRACKED (changing the behavior to not match core Git).
static const enableFastUntrackedDirs =
GitDiff._(16384, 'enableFastUntrackedDirs');
/// When diff finds a file in the working directory with stat
/// information different from the index, but the OID ends up being the
/// same, write the correct stat information into the index. Note:
/// without this flag, diff will always leave the index untouched.
static const updateIndex = GitDiff._(32768, 'updateIndex');
/// Include unreadable files in the diff.
static const includeUnreadable = GitDiff._(65536, 'includeUnreadable');
/// Include unreadable files in the diff.
static const includeUnreadableAsUntracked =
GitDiff._(131072, 'includeUnreadableAsUntracked');
/// Use a heuristic that takes indentation and whitespace into account
/// which generally can produce better diffs when dealing with ambiguous
/// diff hunks.
static const indentHeuristic = GitDiff._(262144, 'indentHeuristic');
/// Treat all files as text, disabling binary attributes & detection.
static const forceText = GitDiff._(1048576, 'forceText');
/// Treat all files as binary, disabling text diffs.
static const forceBinary = GitDiff._(2097152, 'forceBinary');
/// Ignore all whitespace.
static const ignoreWhitespace = GitDiff._(4194304, 'ignoreWhitespace');
/// Ignore changes in amount of whitespace.
static const ignoreWhitespaceChange =
GitDiff._(8388608, 'ignoreWhitespaceChange');
/// Ignore whitespace at end of line.
static const ignoreWhitespaceEOL = GitDiff._(16777216, 'ignoreWhitespaceEOL');
/// When generating patch text, include the content of untracked
/// files. This automatically turns on [GitDiff.includeUntracked] but
/// it does not turn on [GitDiff.recurseUntrackedDirs]. Add that
/// flag if you want the content of every single UNTRACKED file.
static const showUntrackedContent =
GitDiff._(33554432, 'showUntrackedContent');
/// When generating output, include the names of unmodified files if
/// they are included in the git diff. Normally these are skipped in
/// the formats that list files (e.g. name-only, name-status, raw).
/// Even with this, these will not be included in patch format.
static const showUnmodified = GitDiff._(67108864, 'showUnmodified');
/// Use the "patience diff" algorithm.
static const patience = GitDiff._(268435456, 'patience');
/// Take extra time to find minimal diff.
static const minimal = GitDiff._(536870912, 'minimal');
/// Include the necessary deflate / delta information so that `git-apply`
/// can apply given diff information to binary files.
static const showBinary = GitDiff._(1073741824, 'showBinary');
static const List<GitDiff> values = [
normal,
reverse,
includeIgnored,
recurseIgnoredDirs,
includeUntracked,
recurseUntrackedDirs,
includeUnmodified,
includeTypechange,
includeTypechangeTrees,
ignoreFilemode,
ignoreSubmodules,
ignoreCase,
includeCaseChange,
disablePathspecMatch,
skipBinaryCheck,
enableFastUntrackedDirs,
updateIndex,
includeUnreadable,
includeUnreadableAsUntracked,
indentHeuristic,
forceText,
forceBinary,
ignoreWhitespace,
ignoreWhitespaceChange,
ignoreWhitespaceEOL,
showUntrackedContent,
showUnmodified,
patience,
minimal,
showBinary,
];
int get value => _value;
@override
String toString() => 'GitDiff.$_name';
}
/// What type of change is described by a git_diff_delta?
///
/// [GitDelta.renamed] and [GitDelta.copied] will only show up if you run
/// `findSimilar()` on the diff object.
///
/// [GitDelta.typechange] only shows up given [GitDiff.includeTypechange]
/// in the option flags (otherwise type changes will be split into ADDED /
/// DELETED pairs).
class GitDelta {
const GitDelta._(this._value, this._name);
final int _value;
final String _name;
/// No changes.
static const unmodified = GitDelta._(0, 'unmodified');
/// Entry does not exist in old version.
static const added = GitDelta._(1, 'added');
/// Entry does not exist in new version.
static const deleted = GitDelta._(2, 'deleted');
/// Entry content changed between old and new.
static const modified = GitDelta._(3, 'modified');
/// Entry was renamed between old and new.
static const renamed = GitDelta._(4, 'renamed');
/// Entry was copied from another old entry.
static const copied = GitDelta._(5, 'copied');
/// Entry is ignored item in workdir.
static const ignored = GitDelta._(6, 'ignored');
/// Entry is is untracked item in workdir.
static const untracked = GitDelta._(7, 'untracked');
/// Type of entry changed between old and new.
static const typechange = GitDelta._(8, 'typechange');
/// Entry is unreadable.
static const unreadable = GitDelta._(9, 'unreadable');
/// Entry in the index is conflicted.
static const conflicted = GitDelta._(10, 'conflicted');
static const List<GitDelta> values = [
unmodified,
added,
deleted,
modified,
renamed,
copied,
ignored,
untracked,
typechange,
unreadable,
conflicted,
];
int get value => _value;
@override
String toString() => 'GitDelta.$_name';
}
/// Flags for the delta object and the file objects on each side.
class GitDiffFlag {
const GitDiffFlag._(this._value, this._name);
final int _value;
final String _name;
/// File(s) treated as binary data.
static const binary = GitDiffFlag._(1, 'binary');
/// File(s) treated as text data.
static const notBinary = GitDiffFlag._(2, 'notBinary');
/// `id` value is known correct.
static const validId = GitDiffFlag._(4, 'validId');
/// File exists at this side of the delta.
static const exists = GitDiffFlag._(8, 'exists');
static const List<GitDiffFlag> values = [binary, notBinary, validId, exists];
int get value => _value;
@override
String toString() => 'GitDiffFlag.$_name';
}
/// Formatting options for diff stats.
class GitDiffStats {
const GitDiffStats._(this._value, this._name);
final int _value;
final String _name;
/// No stats.
static const none = GitDiffStats._(0, 'none');
/// Full statistics, equivalent of `--stat`.
static const full = GitDiffStats._(1, 'full');
/// Short statistics, equivalent of `--shortstat`.
static const short = GitDiffStats._(2, 'short');
/// Number statistics, equivalent of `--numstat`.
static const number = GitDiffStats._(4, 'number');
/// Extended header information such as creations, renames and mode changes,
/// equivalent of `--summary`.
static const includeSummary = GitDiffStats._(8, 'includeSummary');
static const List<GitDiffStats> values = [
none,
full,
short,
number,
includeSummary,
];
int get value => _value;
@override
String toString() => 'GitDiffStats.$_name';
}
/// Formatting options for diff stats.
class GitDiffFind {
const GitDiffFind._(this._value, this._name);
final int _value;
final String _name;
/// Obey `diff.renames`. Overridden by any other [GitDiffFind] flag.
static const byConfig = GitDiffFind._(0, 'byConfig');
/// Look for renames? (`--find-renames`)
static const renames = GitDiffFind._(1, 'renames');
/// Consider old side of MODIFIED for renames? (`--break-rewrites=N`)
static const renamesFromRewrites = GitDiffFind._(2, 'renamesFromRewrites');
/// Look for copies? (a la `--find-copies`)
static const copies = GitDiffFind._(4, 'copies');
/// Consider UNMODIFIED as copy sources? (`--find-copies-harder`)
///
/// For this to work correctly, use [GitDiff.includeUnmodified] when
/// the initial git diff is being generated.
static const copiesFromUnmodified = GitDiffFind._(8, 'copiesFromUnmodified');
/// Mark significant rewrites for split (`--break-rewrites=/M`)
static const rewrites = GitDiffFind._(16, 'rewrites');
/// Actually split large rewrites into delete/add pairs.
static const breakRewrites = GitDiffFind._(32, 'breakRewrites');
/// Mark rewrites for split and break into delete/add pairs.
static const andBreakRewrites = GitDiffFind._(48, 'andBreakRewrites');
/// Find renames/copies for UNTRACKED items in working directory.
///
/// For this to work correctly, use [GitDiff.includeUntracked] when the
/// initial git diff is being generated (and obviously the diff must
/// be against the working directory for this to make sense).
static const forUntracked = GitDiffFind._(64, 'forUntracked');
/// Turn on all finding features.
static const all = GitDiffFind._(255, 'all');
/// Measure similarity ignoring all whitespace.
static const ignoreWhitespace = GitDiffFind._(4096, 'ignoreWhitespace');
/// Measure similarity including all data.
static const dontIgnoreWhitespace =
GitDiffFind._(8192, 'dontIgnoreWhitespace');
/// Measure similarity only by comparing SHAs (fast and cheap).
static const exactMatchOnly = GitDiffFind._(16384, 'exactMatchOnly');
/// Do not break rewrites unless they contribute to a rename.
///
/// Normally, [GitDiffFind.andBreakRewrites] will measure the self-
/// similarity of modified files and split the ones that have changed a
/// lot into a DELETE / ADD pair. Then the sides of that pair will be
/// considered candidates for rename and copy detection.
///
/// If you add this flag in and the split pair is *not* used for an
/// actual rename or copy, then the modified record will be restored to
/// a regular MODIFIED record instead of being split.
static const breakRewritesForRenamesOnly =
GitDiffFind._(32768, 'breakRewritesForRenamesOnly');
/// Remove any UNMODIFIED deltas after find_similar is done.
///
/// Using [GitDiffFind.copiesFromUnmodified] to emulate the
/// --find-copies-harder behavior requires building a diff with the
/// [GitDiff.includeUnmodified] flag. If you do not want UNMODIFIED
/// records in the final result, pass this flag to have them removed.
static const removeUnmodified = GitDiffFind._(65536, 'removeUnmodified');
static const List<GitDiffFind> values = [
byConfig,
renames,
renamesFromRewrites,
copies,
copiesFromUnmodified,
rewrites,
breakRewrites,
andBreakRewrites,
forUntracked,
all,
ignoreWhitespace,
dontIgnoreWhitespace,
exactMatchOnly,
breakRewritesForRenamesOnly,
removeUnmodified,
];
int get value => _value;
@override
String toString() => 'GitDiffFind.$_name';
}

View file

@ -1,8 +1,10 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:libgit2dart/libgit2dart.dart';
import 'package:libgit2dart/src/tree.dart';
import 'bindings/libgit2_bindings.dart';
import 'bindings/index.dart' as bindings;
import 'bindings/diff.dart' as diff_bindings;
import 'oid.dart';
import 'git_types.dart';
import 'repository.dart';
@ -17,9 +19,11 @@ class Index {
libgit2.git_libgit2_init();
}
/// Pointer to memory address for allocated index object.
late final Pointer<git_index> _indexPointer;
/// Pointer to memory address for allocated index object.
Pointer<git_index> get pointer => _indexPointer;
/// Returns index entry located at provided 0-based position or string path.
///
/// Throws error if position is out of bounds or entry isn't found at path.
@ -154,7 +158,7 @@ class Index {
/// Throws a [LibGit2Error] if error occured.
void write() => bindings.write(_indexPointer);
/// Write the index as a tree.
/// Writes the index as a tree.
///
/// This method will scan the index and write a representation of its current state back to disk;
/// it recursively creates tree objects for each of the subtrees stored in the index, but only
@ -177,11 +181,55 @@ class Index {
void remove(String path, [int stage = 0]) =>
bindings.remove(_indexPointer, path, stage);
/// Remove all matching index entries.
/// Removes all matching index entries.
///
/// Throws a [LibGit2Error] if error occured.
void removeAll(List<String> path) => bindings.removeAll(_indexPointer, path);
/// Creates a diff between the repository index and the workdir directory.
///
/// Throws a [LibGit2Error] if error occured.
Diff diffToWorkdir({
Set<GitDiff> flags = const {GitDiff.normal},
int contextLines = 3,
int interhunkLines = 0,
}) {
final repo = bindings.owner(_indexPointer);
final int flagsInt =
flags.fold(0, (previousValue, e) => previousValue | e.value);
return Diff(diff_bindings.indexToWorkdir(
repo,
_indexPointer,
flagsInt,
contextLines,
interhunkLines,
));
}
/// Creates a diff between a tree and repository index.
///
/// Throws a [LibGit2Error] if error occured.
Diff diffToTree({
required Tree tree,
Set<GitDiff> flags = const {GitDiff.normal},
int contextLines = 3,
int interhunkLines = 0,
}) {
final repo = bindings.owner(_indexPointer);
final int flagsInt =
flags.fold(0, (previousValue, e) => previousValue | e.value);
return Diff(diff_bindings.treeToIndex(
repo,
tree.pointer,
_indexPointer,
flagsInt,
contextLines,
interhunkLines,
));
}
/// Releases memory allocated for index object.
void free() => bindings.free(_indexPointer);
}

View file

@ -1,6 +1,9 @@
import 'dart:ffi';
import 'bindings/libgit2_bindings.dart';
import 'bindings/tree.dart' as bindings;
import 'bindings/diff.dart' as diff_bindings;
import 'diff.dart';
import 'index.dart';
import 'repository.dart';
import 'oid.dart';
import 'git_types.dart';
@ -67,6 +70,73 @@ class Tree {
/// Get the number of entries listed in a tree.
int get length => bindings.entryCount(_treePointer);
/// Creates a diff between a tree and the working directory.
///
/// Throws a [LibGit2Error] if error occured.
Diff diffToWorkdir({
Set<GitDiff> flags = const {GitDiff.normal},
int contextLines = 3,
int interhunkLines = 0,
}) {
final repo = bindings.owner(_treePointer);
final int flagsInt =
flags.fold(0, (previousValue, e) => previousValue | e.value);
return Diff(diff_bindings.treeToWorkdir(
repo,
_treePointer,
flagsInt,
contextLines,
interhunkLines,
));
}
/// Creates a diff between a tree and repository index.
///
/// Throws a [LibGit2Error] if error occured.
Diff diffToIndex({
required Index index,
Set<GitDiff> flags = const {GitDiff.normal},
int contextLines = 3,
int interhunkLines = 0,
}) {
final repo = bindings.owner(_treePointer);
final int flagsInt =
flags.fold(0, (previousValue, e) => previousValue | e.value);
return Diff(diff_bindings.treeToIndex(
repo,
_treePointer,
index.pointer,
flagsInt,
contextLines,
interhunkLines,
));
}
/// Creates a diff with the difference between two tree objects.
///
/// Throws a [LibGit2Error] if error occured.
Diff diffToTree({
required Tree tree,
Set<GitDiff> flags = const {GitDiff.normal},
int contextLines = 3,
int interhunkLines = 0,
}) {
final repo = bindings.owner(_treePointer);
final int flagsInt =
flags.fold(0, (previousValue, e) => previousValue | e.value);
return Diff(diff_bindings.treeToTree(
repo,
_treePointer,
tree.pointer,
flagsInt,
contextLines,
interhunkLines,
));
}
/// Releases memory allocated for tree object.
void free() => bindings.free(_treePointer);
}