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

View file

@ -14,5 +14,6 @@ export 'src/tag.dart';
export 'src/treebuilder.dart';
export 'src/branch.dart';
export 'src/worktree.dart';
export 'src/diff.dart';
export 'src/error.dart';
export 'src/git_types.dart';

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

View file

@ -0,0 +1 @@
Initial commit of test files

View file

@ -0,0 +1 @@
ref: refs/heads/master

View file

@ -0,0 +1,5 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true

View file

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View file

@ -0,0 +1,15 @@
#!/bin/sh
#
# An example hook script to check the commit log message taken by
# applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit. The hook is
# allowed to edit the commit message file.
#
# To enable this hook, rename this file to "applypatch-msg".
. git-sh-setup
test -x "$GIT_DIR/hooks/commit-msg" &&
exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
:

View file

@ -0,0 +1,24 @@
#!/bin/sh
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".
# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
# This example catches duplicate Signed-off-by lines.
test "" = "$(grep '^Signed-off-by: ' "$1" |
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
echo >&2 Duplicate Signed-off-by lines.
exit 1
}

View file

@ -0,0 +1,8 @@
#!/bin/sh
#
# An example hook script that is called after a successful
# commit is made.
#
# To enable this hook, rename this file to "post-commit".
: Nothing

View file

@ -0,0 +1,15 @@
#!/bin/sh
#
# An example hook script for the "post-receive" event.
#
# The "post-receive" script is run after receive-pack has accepted a pack
# and the repository has been updated. It is passed arguments in through
# stdin in the form
# <oldrev> <newrev> <refname>
# For example:
# aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master
#
# see contrib/hooks/ for a sample, or uncomment the next line and
# rename the file to "post-receive".
#. /usr/share/doc/git-core/contrib/hooks/post-receive-email

View file

@ -0,0 +1,8 @@
#!/bin/sh
#
# An example hook script to prepare a packed repository for use over
# dumb transports.
#
# To enable this hook, rename this file to "post-update".
exec git update-server-info

View file

@ -0,0 +1,14 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed
# by applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-applypatch".
. git-sh-setup
test -x "$GIT_DIR/hooks/pre-commit" &&
exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
:

View file

@ -0,0 +1,46 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# If you want to allow non-ascii filenames set this variable to true.
allownonascii=$(git config hooks.allownonascii)
# Cross platform projects tend to avoid non-ascii filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test "$(git diff --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0')"
then
echo "Error: Attempt to add a non-ascii file name."
echo
echo "This can cause problems if you want to work"
echo "with people on other platforms."
echo
echo "To be portable it is advisable to rename the file ..."
echo
echo "If you know what you are doing you can disable this"
echo "check using:"
echo
echo " git config hooks.allownonascii true"
echo
exit 1
fi
exec git diff-index --check --cached $against --

View file

@ -0,0 +1,172 @@
#!/bin/sh
#
# Copyright (c) 2006, 2008 Junio C Hamano
#
# The "pre-rebase" hook is run just before "git rebase" starts doing
# its job, and can prevent the command from running by exiting with
# non-zero status.
#
# The hook is called with the following parameters:
#
# $1 -- the upstream the series was forked from.
# $2 -- the branch being rebased (or empty when rebasing the current branch).
#
# This sample shows how to prevent topic branches that are already
# merged to 'next' branch from getting rebased, because allowing it
# would result in rebasing already published history.
publish=next
basebranch="$1"
if test "$#" = 2
then
topic="refs/heads/$2"
else
topic=`git symbolic-ref HEAD` ||
exit 0 ;# we do not interrupt rebasing detached HEAD
fi
case "$topic" in
refs/heads/??/*)
;;
*)
exit 0 ;# we do not interrupt others.
;;
esac
# Now we are dealing with a topic branch being rebased
# on top of master. Is it OK to rebase it?
# Does the topic really exist?
git show-ref -q "$topic" || {
echo >&2 "No such branch $topic"
exit 1
}
# Is topic fully merged to master?
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
if test -z "$not_in_master"
then
echo >&2 "$topic is fully merged to master; better remove it."
exit 1 ;# we could allow it, but there is no point.
fi
# Is topic ever merged to next? If so you should not be rebasing it.
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
only_next_2=`git rev-list ^master ${publish} | sort`
if test "$only_next_1" = "$only_next_2"
then
not_in_topic=`git rev-list "^$topic" master`
if test -z "$not_in_topic"
then
echo >&2 "$topic is already up-to-date with master"
exit 1 ;# we could allow it, but there is no point.
else
exit 0
fi
else
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
/usr/bin/perl -e '
my $topic = $ARGV[0];
my $msg = "* $topic has commits already merged to public branch:\n";
my (%not_in_next) = map {
/^([0-9a-f]+) /;
($1 => 1);
} split(/\n/, $ARGV[1]);
for my $elem (map {
/^([0-9a-f]+) (.*)$/;
[$1 => $2];
} split(/\n/, $ARGV[2])) {
if (!exists $not_in_next{$elem->[0]}) {
if ($msg) {
print STDERR $msg;
undef $msg;
}
print STDERR " $elem->[1]\n";
}
}
' "$topic" "$not_in_next" "$not_in_master"
exit 1
fi
exit 0
<<\DOC_END
################################################################
This sample hook safeguards topic branches that have been
published from being rewound.
The workflow assumed here is:
* Once a topic branch forks from "master", "master" is never
merged into it again (either directly or indirectly).
* Once a topic branch is fully cooked and merged into "master",
it is deleted. If you need to build on top of it to correct
earlier mistakes, a new topic branch is created by forking at
the tip of the "master". This is not strictly necessary, but
it makes it easier to keep your history simple.
* Whenever you need to test or publish your changes to topic
branches, merge them into "next" branch.
The script, being an example, hardcodes the publish branch name
to be "next", but it is trivial to make it configurable via
$GIT_DIR/config mechanism.
With this workflow, you would want to know:
(1) ... if a topic branch has ever been merged to "next". Young
topic branches can have stupid mistakes you would rather
clean up before publishing, and things that have not been
merged into other branches can be easily rebased without
affecting other people. But once it is published, you would
not want to rewind it.
(2) ... if a topic branch has been fully merged to "master".
Then you can delete it. More importantly, you should not
build on top of it -- other people may already want to
change things related to the topic as patches against your
"master", so if you need further changes, it is better to
fork the topic (perhaps with the same name) afresh from the
tip of "master".
Let's look at this example:
o---o---o---o---o---o---o---o---o---o "next"
/ / / /
/ a---a---b A / /
/ / / /
/ / c---c---c---c B /
/ / / \ /
/ / / b---b C \ /
/ / / / \ /
---o---o---o---o---o---o---o---o---o---o---o "master"
A, B and C are topic branches.
* A has one fix since it was merged up to "next".
* B has finished. It has been fully merged up to "master" and "next",
and is ready to be deleted.
* C has not merged to "next" at all.
We would want to allow C to be rebased, refuse A, and encourage
B to be deleted.
To compute (1):
git rev-list ^master ^topic next
git rev-list ^master next
if these match, topic has not merged in next at all.
To compute (2):
git rev-list master..topic
if this is empty, it is fully merged to "master".
DOC_END

View file

@ -0,0 +1,36 @@
#!/bin/sh
#
# An example hook script to prepare the commit log message.
# Called by "git commit" with the name of the file that has the
# commit message, followed by the description of the commit
# message's source. The hook's purpose is to edit the commit
# message file. If the hook fails with a non-zero status,
# the commit is aborted.
#
# To enable this hook, rename this file to "prepare-commit-msg".
# This hook includes three examples. The first comments out the
# "Conflicts:" part of a merge commit.
#
# The second includes the output of "git diff --name-status -r"
# into the message, just before the "git status" output. It is
# commented because it doesn't cope with --amend or with squashed
# commits.
#
# The third example adds a Signed-off-by line to the message, that can
# still be edited. This is rarely a good idea.
case "$2,$3" in
merge,)
/usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;;
# ,|template,)
# /usr/bin/perl -i.bak -pe '
# print "\n" . `git diff --cached --name-status -r`
# if /^#/ && $first++ == 0' "$1" ;;
*) ;;
esac
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"

View file

@ -0,0 +1,128 @@
#!/bin/sh
#
# An example hook script to blocks unannotated tags from entering.
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
#
# To enable this hook, rename this file to "update".
#
# Config
# ------
# hooks.allowunannotated
# This boolean sets whether unannotated tags will be allowed into the
# repository. By default they won't be.
# hooks.allowdeletetag
# This boolean sets whether deleting tags will be allowed in the
# repository. By default they won't be.
# hooks.allowmodifytag
# This boolean sets whether a tag may be modified after creation. By default
# it won't be.
# hooks.allowdeletebranch
# This boolean sets whether deleting branches will be allowed in the
# repository. By default they won't be.
# hooks.denycreatebranch
# This boolean sets whether remotely creating branches will be denied
# in the repository. By default this is allowed.
#
# --- Command line
refname="$1"
oldrev="$2"
newrev="$3"
# --- Safety check
if [ -z "$GIT_DIR" ]; then
echo "Don't run this script from the command line." >&2
echo " (if you want, you could supply GIT_DIR then run" >&2
echo " $0 <ref> <oldrev> <newrev>)" >&2
exit 1
fi
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
echo "Usage: $0 <ref> <oldrev> <newrev>" >&2
exit 1
fi
# --- Config
allowunannotated=$(git config --bool hooks.allowunannotated)
allowdeletebranch=$(git config --bool hooks.allowdeletebranch)
denycreatebranch=$(git config --bool hooks.denycreatebranch)
allowdeletetag=$(git config --bool hooks.allowdeletetag)
allowmodifytag=$(git config --bool hooks.allowmodifytag)
# check for no description
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
case "$projectdesc" in
"Unnamed repository"* | "")
echo "*** Project description file hasn't been set" >&2
exit 1
;;
esac
# --- Check types
# if $newrev is 0000...0000, it's a commit to delete a ref.
zero="0000000000000000000000000000000000000000"
if [ "$newrev" = "$zero" ]; then
newrev_type=delete
else
newrev_type=$(git cat-file -t $newrev)
fi
case "$refname","$newrev_type" in
refs/tags/*,commit)
# un-annotated tag
short_refname=${refname##refs/tags/}
if [ "$allowunannotated" != "true" ]; then
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
exit 1
fi
;;
refs/tags/*,delete)
# delete tag
if [ "$allowdeletetag" != "true" ]; then
echo "*** Deleting a tag is not allowed in this repository" >&2
exit 1
fi
;;
refs/tags/*,tag)
# annotated tag
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
then
echo "*** Tag '$refname' already exists." >&2
echo "*** Modifying a tag is not allowed in this repository." >&2
exit 1
fi
;;
refs/heads/*,commit)
# branch
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
echo "*** Creating a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/heads/*,delete)
# delete branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/remotes/*,commit)
# tracking branch
;;
refs/remotes/*,delete)
# delete tracking branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
exit 1
fi
;;
*)
# Anything else (is there anything else?)
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
exit 1
;;
esac
# --- Finished
exit 0

Binary file not shown.

View file

@ -0,0 +1,6 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

View file

@ -0,0 +1 @@
0000000000000000000000000000000000000000 a763aa560953e7cfb87ccbc2f536d665aa4dff22 Julien Miotte <mike.perdide@gmail.com> 1311240164 +0200 commit (initial): Initial commit of test files

View file

@ -0,0 +1 @@
0000000000000000000000000000000000000000 a763aa560953e7cfb87ccbc2f536d665aa4dff22 Julien Miotte <mike.perdide@gmail.com> 1311240164 +0200 commit (initial): Initial commit of test files

View file

@ -0,0 +1,2 @@
x<01>ÍK
Â0€a×9Åì…Џ1™è`ÒH;½¿oàö_|ê­ƒ–öÀ " ñ:Ä”0¥3Ñ¡ F&«<>=_òÃGlqãW_à¶UÂîÔ™¦Fo>¸dÊxy¶HuH½<48>A¥ô(•á(µ”b¯û—ñA\gbŠ~ôŒ+C¡Š«ørÍC¨

View file

@ -0,0 +1 @@
a763aa560953e7cfb87ccbc2f536d665aa4dff22

View file

View file

@ -0,0 +1 @@
Modified content

View file

View file

@ -0,0 +1 @@
Modified content

View file

@ -0,0 +1 @@
Other modified content

View file

@ -0,0 +1 @@
Modified content

View file

View file

@ -0,0 +1 @@
Other modified content

View file

@ -0,0 +1 @@
Modified content

View file

271
test/diff_test.dart Normal file
View file

@ -0,0 +1,271 @@
import 'dart:io';
import 'package:libgit2dart/src/git_types.dart';
import 'package:test/test.dart';
import 'package:libgit2dart/libgit2dart.dart';
import 'helpers/util.dart';
void main() {
late Repository repo;
final tmpDir = '${Directory.systemTemp.path}/diff_testrepo/';
const indexToWorkdir = [
'file_deleted',
'modified_file',
'staged_changes_file_deleted',
'staged_changes_file_modified',
'staged_new_file_deleted',
'staged_new_file_modified',
'subdir/deleted_file',
'subdir/modified_file',
];
const indexToTree = [
'staged_changes',
'staged_changes_file_deleted',
'staged_changes_file_modified',
'staged_delete',
'staged_delete_file_modified',
'staged_new',
'staged_new_file_deleted',
'staged_new_file_modified',
];
const treeToWorkdir = [
'file_deleted',
'modified_file',
'staged_changes',
'staged_changes_file_deleted',
'staged_changes_file_modified',
'staged_delete',
'staged_delete_file_modified',
'subdir/deleted_file',
'subdir/modified_file',
];
const treeToTree = [
'deleted_file',
'file_deleted',
'staged_changes',
'staged_changes_file_deleted',
'staged_changes_file_modified',
'staged_delete',
'staged_delete_file_modified',
'subdir/current_file',
'subdir/deleted_file',
'subdir/modified_file',
];
const patch = """
diff --git a/subdir/modified_file b/subdir/modified_file
index e69de29..c217c63 100644
--- a/subdir/modified_file
+++ b/subdir/modified_file
@@ -0,0 +1 @@
+Modified content
""";
const statsPrint = """
file_deleted | 0
modified_file | 1 +
staged_changes_file_deleted | 1 -
staged_changes_file_modified | 2 +-
staged_new_file_deleted | 0
staged_new_file_modified | 1 +
subdir/deleted_file | 0
subdir/modified_file | 1 +
8 files changed, 4 insertions(+), 2 deletions(-)
""";
setUp(() async {
if (await Directory(tmpDir).exists()) {
await Directory(tmpDir).delete(recursive: true);
}
await copyRepo(
from: Directory('test/assets/dirtyrepo/'),
to: await Directory(tmpDir).create(),
);
repo = Repository.open(tmpDir);
});
tearDown(() async {
repo.free();
await Directory(tmpDir).delete(recursive: true);
});
group('Diff', () {
test('successfully returns diff between index and workdir', () {
final index = repo.index;
final diff = index.diffToWorkdir();
expect(diff.length, 8);
for (var i = 0; i < diff.deltas.length; i++) {
expect(diff.deltas[i].newFile.path, indexToWorkdir[i]);
}
diff.free();
index.free();
});
test('successfully returns diff between index and tree', () {
final index = repo.index;
final tree = (repo[repo.head.target.sha] as Commit).tree;
final diff = index.diffToTree(tree: tree);
expect(diff.length, 8);
for (var i = 0; i < diff.deltas.length; i++) {
expect(diff.deltas[i].newFile.path, indexToTree[i]);
}
tree.free();
diff.free();
index.free();
});
test('successfully returns diff between tree and workdir', () {
final tree = (repo[repo.head.target.sha] as Commit).tree;
final diff = tree.diffToWorkdir();
expect(diff.length, 9);
for (var i = 0; i < diff.deltas.length; i++) {
expect(diff.deltas[i].newFile.path, treeToWorkdir[i]);
}
tree.free();
diff.free();
});
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);
expect(diff.length, 8);
for (var i = 0; i < diff.deltas.length; i++) {
expect(diff.deltas[i].newFile.path, indexToTree[i]);
}
tree.free();
diff.free();
index.free();
});
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);
expect(diff.length, 10);
for (var i = 0; i < diff.deltas.length; i++) {
expect(diff.deltas[i].newFile.path, treeToTree[i]);
}
tree1.free();
tree2.free();
diff.free();
});
test('successfully merges diffs', () {
final tree1 = (repo[repo.head.target.sha] as Commit).tree;
final tree2 = repo['b85d53c9236e89aff2b62558adaa885fd1d6ff1c'] as Tree;
final diff1 = tree1.diffToTree(tree: tree2);
final diff2 = tree1.diffToWorkdir();
expect(diff1.length, 10);
expect(diff2.length, 9);
diff1.merge(diff2);
expect(diff1.length, 11);
tree1.free();
tree2.free();
diff1.free();
diff2.free();
});
test('successfully parses provided diff', () {
final diff = Diff.parse(patch);
final stats = diff.stats;
expect(diff.length, 1);
expect(stats.filesChanged, 1);
expect(stats.insertions, 1);
stats.free();
diff.free();
});
test('successfully finds similar entries', () {
final index = repo.index;
final oldTree = (repo[repo.head.target.sha] as Commit).tree;
final newTree = repo[index.writeTree().sha] as Tree;
final diff = oldTree.diffToTree(tree: newTree);
expect(
diff.deltas.singleWhere((e) => e.newFile.path == 'staged_new').status,
GitDelta.added,
);
diff.findSimilar();
expect(
diff.deltas.singleWhere((e) => e.newFile.path == 'staged_new').status,
GitDelta.renamed,
);
diff.free();
index.free();
oldTree.free();
newTree.free();
});
test('returns deltas and patches', () {
final index = repo.index;
final diff = index.diffToWorkdir();
expect(diff.deltas[0].numberOfFiles, 1);
expect(diff.deltas[0].status, GitDelta.deleted);
expect(diff.deltas[0].statusChar, 'D');
expect(diff.deltas[0].flags, isEmpty);
expect(diff.deltas[0].similarity, 0);
expect(diff.deltas[0].oldFile.path, indexToWorkdir[0]);
expect(
diff.deltas[0].oldFile.id.sha,
'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391',
);
expect(
diff.deltas[0].newFile.id.sha,
'0000000000000000000000000000000000000000',
);
expect(diff.deltas[2].oldFile.size, 17);
expect(
diff.deltas[0].oldFile.flags,
{GitDiffFlag.validId, GitDiffFlag.exists},
);
expect(
diff.deltas[0].newFile.flags,
{GitDiffFlag.validId},
);
expect(diff.deltas[0].oldFile.mode, GitFilemode.blob);
diff.free();
index.free();
});
test('returns stats', () {
final index = repo.index;
final diff = index.diffToWorkdir();
final stats = diff.stats;
expect(stats.insertions, 4);
expect(stats.deletions, 2);
expect(stats.filesChanged, 8);
expect(stats.print({GitDiffStats.full}, 80), statsPrint);
stats.free();
diff.free();
index.free();
});
});
}

View file

@ -43,15 +43,27 @@ void main() {
repo.reset(sha, GitReset.soft);
contents = file.readAsStringSync();
expect(contents, 'Feature edit\n');
final index = repo.index;
final diff = index.diffToWorkdir();
expect(diff.deltas, isEmpty);
index.free();
});
test('successfully resets with soft', () {
test('successfully resets with mixed', () {
var contents = file.readAsStringSync();
expect(contents, 'Feature edit\n');
repo.reset(sha, GitReset.mixed);
contents = file.readAsStringSync();
expect(contents, 'Feature edit\n');
final index = repo.index;
final diff = index.diffToWorkdir();
expect(diff.deltas.length, 1);
index.free();
});
});
}