feat(merge): add more bindings and api methods

This commit is contained in:
Aleksey Kulikov 2021-09-09 14:39:40 +03:00
parent 223cc7cc14
commit 63dabcdd2c
66 changed files with 1502 additions and 8 deletions

View file

@ -60,6 +60,28 @@ Pointer<git_oid> writeTree(Pointer<git_index> index) {
}
}
/// Write the index as a tree to the given repository.
///
/// This method will do the same as [writeTree], but letting the user choose the repository
/// where the tree will be written.
///
/// The index must not contain any file in conflict.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_oid> writeTreeTo(
Pointer<git_index> index,
Pointer<git_repository> repo,
) {
final out = calloc<git_oid>();
final error = libgit2.git_index_write_tree_to(out, index, repo);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
} else {
return out;
}
}
/// Find the first position of any entries which point to given path in the Git index.
bool find(Pointer<git_index> index, String path) {
final pathC = path.toNativeUtf8().cast<Int8>();
@ -262,6 +284,69 @@ void removeAll(Pointer<git_index> index, List<String> pathspec) {
}
}
/// Determine if the index contains entries representing file conflicts.
bool hasConflicts(Pointer<git_index> index) {
return libgit2.git_index_has_conflicts(index) == 1 ? true : false;
}
/// Return list of conflicts in the index.
///
/// Throws a [LibGit2Error] if error occured.
List<Map<String, Pointer<git_index_entry>>> conflictList(
Pointer<git_index> index) {
final iterator = calloc<Pointer<git_index_conflict_iterator>>();
final iteratorError =
libgit2.git_index_conflict_iterator_new(iterator, index);
if (iteratorError < 0) {
throw LibGit2Error(libgit2.git_error_last());
}
var result = <Map<String, Pointer<git_index_entry>>>[];
var error = 0;
while (error >= 0) {
final ancestorOut = calloc<Pointer<git_index_entry>>();
final ourOut = calloc<Pointer<git_index_entry>>();
final theirOut = calloc<Pointer<git_index_entry>>();
error = libgit2.git_index_conflict_next(
ancestorOut,
ourOut,
theirOut,
iterator.value,
);
if (error >= 0) {
result.add({
'ancestor': ancestorOut.value,
'our': ourOut.value,
'their': theirOut.value,
});
calloc.free(ancestorOut);
calloc.free(ourOut);
calloc.free(theirOut);
} else {
break;
}
}
libgit2.git_index_conflict_iterator_free(iterator.value);
return result;
}
/// Removes the index entries that represent a conflict of a single file.
///
/// Throws a [LibGit2Error] if error occured.
void conflictRemove(Pointer<git_index> index, String path) {
final pathC = path.toNativeUtf8().cast<Int8>();
final error = libgit2.git_index_conflict_remove(index, pathC);
calloc.free(pathC);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
}
}
/// Get the repository this index relates to.
Pointer<git_repository> owner(Pointer<git_index> index) =>
libgit2.git_index_owner(index);

View file

@ -54,3 +54,115 @@ List<int> analysis(
return result;
}
}
/// Merges the given commit(s) into HEAD, writing the results into the working directory.
/// Any changes are staged for commit and any conflicts are written to the index. Callers
/// should inspect the repository's index after this completes, resolve any conflicts and
/// prepare a commit.
///
/// Throws a [LibGit2Error] if error occured.
void merge(
Pointer<git_repository> repo,
Pointer<Pointer<git_annotated_commit>> theirHeads,
int theirHeadsLen,
) {
final mergeOpts = calloc<git_merge_options>(sizeOf<git_merge_options>());
libgit2.git_merge_options_init(mergeOpts, 1);
final checkoutOpts =
calloc<git_checkout_options>(sizeOf<git_checkout_options>());
libgit2.git_checkout_options_init(checkoutOpts, 1);
checkoutOpts.ref.checkout_strategy =
git_checkout_strategy_t.GIT_CHECKOUT_SAFE +
git_checkout_strategy_t.GIT_CHECKOUT_RECREATE_MISSING;
final error = libgit2.git_merge(
repo,
theirHeads,
theirHeadsLen,
mergeOpts,
checkoutOpts,
);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
}
}
/// Merge two commits, producing a git_index that reflects the result of the merge.
/// The index may be written as-is to the working directory or checked out. If the index
/// is to be converted to a tree, the caller should resolve any conflicts that arose as
/// part of the merge.
///
/// The returned index must be freed explicitly.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_index> mergeCommits(
Pointer<git_repository> repo,
Pointer<git_commit> ourCommit,
Pointer<git_commit> theirCommit,
Map<String, int> opts,
) {
final out = calloc<Pointer<git_index>>();
final optsC = calloc<git_merge_options>(sizeOf<git_merge_options>());
optsC.ref.file_favor = opts['favor']!;
optsC.ref.flags = opts['mergeFlags']!;
optsC.ref.file_flags = opts['fileFlags']!;
optsC.ref.version = GIT_MERGE_OPTIONS_VERSION;
final error = libgit2.git_merge_commits(
out,
repo,
ourCommit,
theirCommit,
optsC,
);
calloc.free(optsC);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
} else {
return out.value;
}
}
/// Merge two trees, producing a git_index that reflects the result of the merge.
/// The index may be written as-is to the working directory or checked out. If the index
/// is to be converted to a tree, the caller should resolve any conflicts that arose as part
/// of the merge.
///
/// The returned index must be freed explicitly.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_index> mergeTrees(
Pointer<git_repository> repo,
Pointer<git_tree> ancestorTree,
Pointer<git_tree> ourTree,
Pointer<git_tree> theirTree,
Map<String, int> opts,
) {
final out = calloc<Pointer<git_index>>();
final optsC = calloc<git_merge_options>(sizeOf<git_merge_options>());
optsC.ref.file_favor = opts['favor']!;
optsC.ref.flags = opts['mergeFlags']!;
optsC.ref.file_flags = opts['fileFlags']!;
optsC.ref.version = GIT_MERGE_OPTIONS_VERSION;
final error = libgit2.git_merge_trees(
out,
repo,
ancestorTree,
ourTree,
theirTree,
optsC,
);
calloc.free(optsC);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
} else {
return out.value;
}
}

View file

@ -195,3 +195,117 @@ class GitMergePreference {
int get value => _value;
}
/// Repository state.
///
/// These values represent possible states for the repository to be in,
/// based on the current operation which is ongoing.
class GitRepositoryState {
const GitRepositoryState._(this._value);
final int _value;
static const none = GitRepositoryState._(0);
static const merge = GitRepositoryState._(1);
static const revert = GitRepositoryState._(2);
static const reverSequence = GitRepositoryState._(3);
static const cherrypick = GitRepositoryState._(4);
static const cherrypickSequence = GitRepositoryState._(5);
static const bisect = GitRepositoryState._(6);
static const rebase = GitRepositoryState._(7);
static const rebaseInteractive = GitRepositoryState._(8);
static const rebaseMerge = GitRepositoryState._(9);
static const applyMailbox = GitRepositoryState._(10);
static const applyMailboxOrRebase = GitRepositoryState._(11);
int get value => _value;
}
/// Flags for merge options.
class GitMergeFlag {
const GitMergeFlag._(this._value);
final int _value;
/// Detect renames that occur between the common ancestor and the "ours"
/// side or the common ancestor and the "theirs" side. This will enable
/// the ability to merge between a modified and renamed file.
static const findRenames = GitMergeFlag._(1);
/// If a conflict occurs, exit immediately instead of attempting to
/// continue resolving conflicts. The merge operation will fail with
/// and no index will be returned.
static const failOnConflict = GitMergeFlag._(2);
/// Do not write the REUC extension on the generated index.
static const skipREUC = GitMergeFlag._(4);
/// If the commits being merged have multiple merge bases, do not build
/// a recursive merge base (by merging the multiple merge bases),
/// instead simply use the first base.
static const noRecursive = GitMergeFlag._(8);
int get value => _value;
}
/// Merge file favor options to instruct the file-level merging functionality
/// on how to deal with conflicting regions of the files.
class GitMergeFileFavor {
const GitMergeFileFavor._(this._value);
final int _value;
/// When a region of a file is changed in both branches, a conflict
/// will be recorded in the index. This is the default.
static const normal = GitMergeFileFavor._(0);
/// When a region of a file is changed in both branches, the file
/// created in the index will contain the "ours" side of any conflicting
/// region. The index will not record a conflict.
static const ours = GitMergeFileFavor._(1);
/// When a region of a file is changed in both branches, the file
/// created in the index will contain the "theirs" side of any conflicting
/// region. The index will not record a conflict.
static const theirs = GitMergeFileFavor._(2);
/// When a region of a file is changed in both branches, the file
/// created in the index will contain each unique line from each side,
/// which has the result of combining both files. The index will not
/// record a conflict.
static const union = GitMergeFileFavor._(3);
int get value => _value;
}
/// File merging flags.
class GitMergeFileFlag {
const GitMergeFileFlag._(this._value);
final int _value;
/// Defaults.
static const defaults = GitMergeFileFlag._(0);
/// Create standard conflicted merge files.
static const styleMerge = GitMergeFileFlag._(1);
/// Create diff3-style files.
static const styleDiff3 = GitMergeFileFlag._(2);
/// Condense non-alphanumeric regions for simplified diff file.
static const simplifyAlnum = GitMergeFileFlag._(4);
/// Ignore all whitespace.
static const ignoreWhitespace = GitMergeFileFlag._(8);
/// Ignore changes in amount of whitespace.
static const ignoreWhitespaceChange = GitMergeFileFlag._(16);
/// Ignore whitespace at end of line.
static const ignoreWhitespaceEOL = GitMergeFileFlag._(32);
/// Use the "patience diff" algorithm.
static const diffPatience = GitMergeFileFlag._(64);
/// Take extra time to find minimal diff.
static const diffMinimal = GitMergeFileFlag._(128);
int get value => _value;
}

View file

@ -37,6 +37,43 @@ class Index {
/// Returns the count of entries currently in the index.
int get count => bindings.entryCount(_indexPointer);
/// Checks if the index contains entries representing file conflicts.
bool get hasConflicts => bindings.hasConflicts(_indexPointer);
/// Returns map of conflicts in the index with key as conflicted file path and
/// value as [ConflictEntry] object.
///
/// Throws a [LibGit2Error] if error occured.
Map<String, ConflictEntry> get conflicts {
final conflicts = bindings.conflictList(_indexPointer);
var result = <String, ConflictEntry>{};
for (var entry in conflicts) {
IndexEntry? ancestor, our, their;
String path;
entry['ancestor'] == nullptr
? ancestor = null
: ancestor = IndexEntry(entry['ancestor']!);
entry['our'] == nullptr ? our = null : our = IndexEntry(entry['our']!);
entry['their'] == nullptr
? their = null
: their = IndexEntry(entry['their']!);
if (ancestor != null) {
path = ancestor.path;
} else if (our != null) {
path = our.path;
} else {
path = their!.path;
}
result[path] = ConflictEntry(_indexPointer, path, ancestor, our, their);
}
return result;
}
/// Clears the contents (all the entries) of an index object.
///
/// This clears the index object in memory; changes must be explicitly written to
@ -124,7 +161,15 @@ class Index {
/// returns the OID of the root tree. This is the OID that can be used e.g. to create a commit.
///
/// The index must not contain any file in conflict.
Oid writeTree() => Oid(bindings.writeTree(_indexPointer));
///
/// Throws a [LibGit2Error] if error occured or there is no associated repository and no [repo] passed.
Oid writeTree([Repository? repo]) {
if (repo == null) {
return Oid(bindings.writeTree(_indexPointer));
} else {
return Oid(bindings.writeTreeTo(_indexPointer, repo.pointer));
}
}
/// Removes an entry from the index.
///
@ -176,3 +221,34 @@ class IndexEntry {
return hex.toString();
}
}
class ConflictEntry {
/// Initializes a new instance of [ConflictEntry] class.
const ConflictEntry(
this._indexPointer,
this._path,
this.ancestor,
this.our,
this.their,
);
/// Common ancestor.
final IndexEntry? ancestor;
/// "Our" side of the conflict.
final IndexEntry? our;
/// "Their" side of the conflict.
final IndexEntry? their;
/// Pointer to memory address for allocated index object.
final Pointer<git_index> _indexPointer;
/// Path to conflicted file.
final String _path;
/// Removes the index entry that represent a conflict of a single file.
///
/// Throws a [LibGit2Error] if error occured.
void remove() => bindings.conflictRemove(_indexPointer, _path);
}

View file

@ -1,5 +1,6 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:libgit2dart/libgit2dart.dart';
import 'bindings/libgit2_bindings.dart';
import 'bindings/repository.dart' as bindings;
import 'bindings/merge.dart' as merge_bindings;
@ -192,7 +193,6 @@ class Repository {
/// Returns the status of a git repository - ie, whether an operation
/// (merge, cherry-pick, etc) is in progress.
// git_repository_state_t from libgit2_bindings.dart represents possible states
int get state => bindings.state(_repoPointer);
/// Removes all the metadata associated with an ongoing command like
@ -517,4 +517,95 @@ class Repository {
return result;
}
/// Merges the given commit(s) oid into HEAD, writing the results into the working directory.
/// Any changes are staged for commit and any conflicts are written to the index. Callers
/// should inspect the repository's index after this completes, resolve any conflicts and
/// prepare a commit.
///
/// Throws a [LibGit2Error] if error occured.
void merge(Oid oid) {
final theirHead = commit_bindings.annotatedLookup(
_repoPointer,
oid.pointer,
);
merge_bindings.merge(_repoPointer, theirHead, 1);
commit_bindings.annotatedFree(theirHead.value);
}
/// Merges two commits, producing a git_index that reflects the result of the merge.
/// The index may be written as-is to the working directory or checked out. If the index
/// is to be converted to a tree, the caller should resolve any conflicts that arose as
/// part of the merge.
///
/// The returned index must be freed explicitly.
///
/// Throws a [LibGit2Error] if error occured.
Index mergeCommits({
required Commit ourCommit,
required Commit theirCommit,
GitMergeFileFavor favor = GitMergeFileFavor.normal,
List<GitMergeFlag> mergeFlags = const [GitMergeFlag.findRenames],
List<GitMergeFileFlag> fileFlags = const [GitMergeFileFlag.defaults],
}) {
var opts = <String, int>{};
opts['favor'] = favor.value;
opts['mergeFlags'] = mergeFlags.fold(
0,
(previousValue, element) => previousValue + element.value,
);
opts['fileFlags'] = fileFlags.fold(
0,
(previousValue, element) => previousValue + element.value,
);
final result = merge_bindings.mergeCommits(
_repoPointer,
ourCommit.pointer,
theirCommit.pointer,
opts,
);
return Index(result);
}
/// Merge two trees, producing a git_index that reflects the result of the merge.
/// The index may be written as-is to the working directory or checked out. If the index
/// is to be converted to a tree, the caller should resolve any conflicts that arose as part
/// of the merge.
///
/// The returned index must be freed explicitly.
///
/// Throws a [LibGit2Error] if error occured.
Index mergeTrees({
required Tree ancestorTree,
required Tree ourTree,
required Tree theirTree,
GitMergeFileFavor favor = GitMergeFileFavor.normal,
List<GitMergeFlag> mergeFlags = const [GitMergeFlag.findRenames],
List<GitMergeFileFlag> fileFlags = const [GitMergeFileFlag.defaults],
}) {
var opts = <String, int>{};
opts['favor'] = favor.value;
opts['mergeFlags'] = mergeFlags.fold(
0,
(previousValue, element) => previousValue + element.value,
);
opts['fileFlags'] = fileFlags.fold(
0,
(previousValue, element) => previousValue + element.value,
);
final result = merge_bindings.mergeTrees(
_repoPointer,
ancestorTree.pointer,
ourTree.pointer,
theirTree.pointer,
opts,
);
return Index(result);
}
}