feat(checkout): add bindings and api

This commit is contained in:
Aleksey Kulikov 2021-09-09 19:56:15 +03:00
parent 659e69b1f2
commit 628aa610d8
4 changed files with 358 additions and 5 deletions

View file

@ -0,0 +1,127 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import '../error.dart';
import 'libgit2_bindings.dart';
import '../util.dart';
/// Updates files in the index and the working tree to match the content of the commit
/// pointed at by HEAD.
///
/// Note that this is not the correct mechanism used to switch branches; do not change
/// your HEAD and then call this method, that would leave you with checkout conflicts
/// since your working directory would then appear to be dirty. Instead, checkout the
/// target of the branch and then update HEAD using `setHead` to point to the branch you checked out.
///
/// Throws a [LibGit2Error] if error occured.
void head(
Pointer<git_repository> repo,
int strategy,
String? directory,
List<String>? paths,
) {
final initOptions = _initOptions(strategy, directory, paths);
final optsC = initOptions[0];
final pathPointers = initOptions[1];
final strArray = initOptions[2];
final error = libgit2.git_checkout_head(repo, optsC);
for (var p in pathPointers) {
calloc.free(p);
}
calloc.free(strArray);
calloc.free(optsC);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
}
}
/// Updates files in the working tree to match the content of the index.
///
/// Throws a [LibGit2Error] if error occured.
void index(
Pointer<git_repository> repo,
int strategy,
String? directory,
List<String>? paths,
) {
final initOptions = _initOptions(strategy, directory, paths);
final optsC = initOptions[0];
final pathPointers = initOptions[1];
final strArray = initOptions[2];
final error = libgit2.git_checkout_index(repo, nullptr, optsC);
for (var p in pathPointers) {
calloc.free(p);
}
calloc.free(strArray);
calloc.free(optsC);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
}
}
/// Updates files in the index and working tree to match the content of the tree
/// pointed at by the treeish.
///
/// Throws a [LibGit2Error] if error occured.
void tree(
Pointer<git_repository> repo,
Pointer<git_object> treeish,
int strategy,
String? directory,
List<String>? paths,
) {
final initOptions = _initOptions(strategy, directory, paths);
final optsC = initOptions[0];
final pathPointers = initOptions[1];
final strArray = initOptions[2];
final error = libgit2.git_checkout_tree(repo, treeish, optsC);
for (var p in pathPointers) {
calloc.free(p);
}
calloc.free(strArray);
calloc.free(optsC);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
}
}
List<dynamic> _initOptions(
int strategy,
String? directory,
List<String>? paths,
) {
final optsC = calloc<git_checkout_options>(sizeOf<git_checkout_options>());
libgit2.git_checkout_options_init(optsC, GIT_CHECKOUT_OPTIONS_VERSION);
optsC.ref.checkout_strategy = strategy;
if (directory != null) {
optsC.ref.target_directory = directory.toNativeUtf8().cast<Int8>();
}
List<Pointer<Int8>> pathPointers = [];
Pointer<Pointer<Int8>> strArray = nullptr;
if (paths != null) {
pathPointers = paths.map((e) => e.toNativeUtf8().cast<Int8>()).toList();
strArray = calloc(paths.length);
for (var i = 0; i < paths.length; i++) {
strArray[i] = pathPointers[i];
}
optsC.ref.paths.strings = strArray;
optsC.ref.paths.count = paths.length;
}
var result = <dynamic>[];
result.add(optsC);
result.add(pathPointers);
result.add(strArray);
return result;
}

View file

@ -309,3 +309,84 @@ class GitMergeFileFlag {
int get value => _value;
}
/// Checkout behavior flags.
///
/// In libgit2, checkout is used to update the working directory and index
/// to match a target tree. Unlike git checkout, it does not move the HEAD
/// commit for you - use `setHead` or the like to do that.
class GitCheckout {
const GitCheckout._(this._value);
final int _value;
/// Default is a dry run, no actual updates.
static const none = GitCheckout._(0);
/// Allow safe updates that cannot overwrite uncommitted data.
/// If the uncommitted changes don't conflict with the checked out files,
/// the checkout will still proceed, leaving the changes intact.
///
/// Mutually exclusive with [GitCheckout.force].
/// [GitCheckout.force] takes precedence over [GitCheckout.safe].
static const safe = GitCheckout._(1);
/// Allow all updates to force working directory to look like index.
///
/// Mutually exclusive with [GitCheckout.safe].
/// [GitCheckout.force] takes precedence over [GitCheckout.safe].
static const force = GitCheckout._(2);
/// Allow checkout to recreate missing files.
static const recreateMissing = GitCheckout._(4);
/// Allow checkout to make safe updates even if conflicts are found.
static const allowConflicts = GitCheckout._(16);
/// Remove untracked files not in index (that are not ignored).
static const removeUntracked = GitCheckout._(32);
/// Remove ignored files not in index.
static const removeIgnored = GitCheckout._(64);
/// Only update existing files, don't create new ones.
static const updateOnly = GitCheckout._(128);
/// Normally checkout updates index entries as it goes; this stops that.
/// Implies [GitCheckout.dontWriteIndex].
static const dontUpdateIndex = GitCheckout._(256);
/// Don't refresh index/config/etc before doing checkout.
static const noRefresh = GitCheckout._(512);
/// Allow checkout to skip unmerged files.
static const skipUnmerged = GitCheckout._(1024);
/// For unmerged files, checkout stage 2 from index.
static const useOurs = GitCheckout._(2048);
/// For unmerged files, checkout stage 3 from index.
static const useTheirs = GitCheckout._(4096);
/// Treat pathspec as simple list of exact match file paths.
static const disablePathspecMatch = GitCheckout._(8192);
/// Ignore directories in use, they will be left empty.
static const skipLockedDirectories = GitCheckout._(262144);
/// Don't overwrite ignored files that exist in the checkout target.
static const dontOverwriteIgnored = GitCheckout._(524288);
/// Write normal merge files for conflicts.
static const conflictStyleMerge = GitCheckout._(1048576);
/// Include common ancestor data in diff3 format files for conflicts.
static const conflictStyleDiff3 = GitCheckout._(2097152);
/// Don't overwrite existing files or folders.
static const dontRemoveExisting = GitCheckout._(4194304);
/// Normally checkout writes the index upon completion; this prevents that.
static const dontWriteIndex = GitCheckout._(8388608);
int get value => _value;
}

View file

@ -7,6 +7,7 @@ import 'bindings/merge.dart' as merge_bindings;
import 'bindings/object.dart' as object_bindings;
import 'bindings/status.dart' as status_bindings;
import 'bindings/commit.dart' as commit_bindings;
import 'bindings/checkout.dart' as checkout_bindings;
import 'branch.dart';
import 'commit.dart';
import 'config.dart';
@ -459,13 +460,18 @@ class Repository {
var count = status_bindings.listEntryCount(list);
for (var i = 0; i < count; i++) {
late String path;
final entry = status_bindings.getByIndex(list, i);
if (entry.ref.head_to_index != nullptr) {
final path = entry.ref.head_to_index.ref.old_file.path
path = entry.ref.head_to_index.ref.old_file.path
.cast<Utf8>()
.toDartString();
} else {
path = entry.ref.index_to_workdir.ref.old_file.path
.cast<Utf8>()
.toDartString();
result[path] = entry.ref.status;
}
result[path] = entry.ref.status;
}
status_bindings.listFree(list);
@ -535,7 +541,7 @@ class Repository {
commit_bindings.annotatedFree(theirHead.value);
}
/// Merges two commits, producing a git_index that reflects the result of the merge.
/// Merges two commits, producing an 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.
@ -571,7 +577,7 @@ class Repository {
return Index(result);
}
/// Merge two trees, producing a git_index that reflects the result of the merge.
/// Merges two trees, producing an 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.
@ -609,7 +615,7 @@ class Repository {
return Index(result);
}
/// Cherry-picks the given commit, producing changes in the index and working directory.
/// Cherry-picks the provided commit, producing changes in the index and 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
@ -618,4 +624,47 @@ class Repository {
/// Throws a [LibGit2Error] if error occured.
void cherryPick(Commit commit) =>
merge_bindings.cherryPick(_repoPointer, commit.pointer);
/// Checkouts the provided reference [refName] using the given strategy, and update the HEAD.
///
/// If no reference [refName] is given, checkouts from the index.
///
/// Default checkout strategy is combination of [GitCheckout.safe] and
/// [GitCheckout.recreateMissing].
///
/// [directory] is alternative checkout path to workdir.
///
/// [paths] is list of files to checkout from provided reference [refName]. If paths are provided
/// HEAD will not be set to the reference [refName].
void checkout({
String refName = '',
List<GitCheckout> strategy = const [
GitCheckout.safe,
GitCheckout.recreateMissing
],
String? directory,
List<String>? paths,
}) {
final int strat = strategy.fold(
0,
(previousValue, element) => previousValue + element.value,
);
if (refName.isEmpty) {
checkout_bindings.index(_repoPointer, strat, directory, paths);
} else if (refName == 'HEAD') {
checkout_bindings.head(_repoPointer, strat, directory, paths);
} else {
final ref = references[refName];
final treeish = object_bindings.lookup(
_repoPointer, ref.target.pointer, GitObject.any.value);
checkout_bindings.tree(_repoPointer, treeish, strat, directory, paths);
if (paths == null) {
setHead(refName);
}
object_bindings.free(treeish);
ref.free();
}
}
}