mirror of
https://github.com/SkinnyMind/libgit2dart.git
synced 2025-05-04 20:29:08 -04:00
feat(merge): add more bindings and api methods
This commit is contained in:
parent
223cc7cc14
commit
63dabcdd2c
66 changed files with 1502 additions and 8 deletions
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue