mirror of
https://github.com/SkinnyMind/libgit2dart.git
synced 2025-05-04 20:29:08 -04:00
feat(checkout): add bindings and api
This commit is contained in:
parent
659e69b1f2
commit
628aa610d8
4 changed files with 358 additions and 5 deletions
127
lib/src/bindings/checkout.dart
Normal file
127
lib/src/bindings/checkout.dart
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
96
test/checkout_test.dart
Normal file
96
test/checkout_test.dart
Normal file
|
@ -0,0 +1,96 @@
|
|||
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}/checkout_testrepo/';
|
||||
|
||||
setUp(() async {
|
||||
if (await Directory(tmpDir).exists()) {
|
||||
await Directory(tmpDir).delete(recursive: true);
|
||||
}
|
||||
await copyRepo(
|
||||
from: Directory('test/assets/testrepo/'),
|
||||
to: await Directory(tmpDir).create(),
|
||||
);
|
||||
repo = Repository.open(tmpDir);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
repo.free();
|
||||
await Directory(tmpDir).delete(recursive: true);
|
||||
});
|
||||
|
||||
group('Checkout', () {
|
||||
test('successfully checkouts head', () {
|
||||
File('${tmpDir}feature_file').writeAsStringSync('edit');
|
||||
expect(repo.status, contains('feature_file'));
|
||||
|
||||
repo.checkout(refName: 'HEAD', strategy: [GitCheckout.force]);
|
||||
expect(repo.status, isEmpty);
|
||||
});
|
||||
|
||||
test('successfully checkouts index', () {
|
||||
File('${repo.workdir}feature_file').writeAsStringSync('edit');
|
||||
expect(repo.status, contains('feature_file'));
|
||||
|
||||
repo.checkout(strategy: [GitCheckout.force]);
|
||||
expect(repo.status, isEmpty);
|
||||
});
|
||||
|
||||
test('successfully checkouts tree', () {
|
||||
final masterHead =
|
||||
repo['821ed6e80627b8769d170a293862f9fc60825226'] as Commit;
|
||||
final masterTree = repo[masterHead.tree.sha] as Tree;
|
||||
expect(
|
||||
masterTree.entries.any((e) => e.name == 'another_feature_file'),
|
||||
false,
|
||||
);
|
||||
|
||||
repo.checkout(refName: 'refs/heads/feature');
|
||||
final featureHead =
|
||||
repo['5aecfa0fb97eadaac050ccb99f03c3fb65460ad4'] as Commit;
|
||||
final featureTree = repo[featureHead.tree.sha] as Tree;
|
||||
final repoHead = repo.head;
|
||||
expect(repoHead.target.sha, featureHead.id.sha);
|
||||
expect(repo.status, isEmpty);
|
||||
expect(
|
||||
featureTree.entries.any((e) => e.name == 'another_feature_file'),
|
||||
true,
|
||||
);
|
||||
|
||||
repoHead.free();
|
||||
featureTree.free();
|
||||
featureHead.free();
|
||||
masterTree.free();
|
||||
masterHead.free();
|
||||
});
|
||||
|
||||
test('successfully checkouts with alrenative directory', () {
|
||||
final altDir = '${Directory.systemTemp.path}/alt_dir';
|
||||
// making sure there is no directory
|
||||
if (Directory(altDir).existsSync()) {
|
||||
Directory(altDir).deleteSync(recursive: true);
|
||||
}
|
||||
Directory(altDir).createSync();
|
||||
expect(Directory(altDir).listSync().length, 0);
|
||||
|
||||
repo.checkout(refName: 'refs/heads/feature', directory: altDir);
|
||||
expect(Directory(altDir).listSync().length, isNot(0));
|
||||
|
||||
Directory(altDir).deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
test('successfully checkouts file with provided path', () {
|
||||
expect(repo.status, isEmpty);
|
||||
repo.checkout(
|
||||
refName: 'refs/heads/feature',
|
||||
paths: ['another_feature_file'],
|
||||
);
|
||||
expect(repo.status, {'another_feature_file': GitStatus.indexNew.value});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue