mirror of
https://github.com/SkinnyMind/libgit2dart.git
synced 2025-05-04 20:29:08 -04:00
feat(rebase): add bindings and api
This commit is contained in:
parent
52707dcc63
commit
2ce419d7c4
5 changed files with 476 additions and 0 deletions
|
@ -23,6 +23,7 @@ export 'src/callbacks.dart';
|
||||||
export 'src/credentials.dart';
|
export 'src/credentials.dart';
|
||||||
export 'src/blame.dart';
|
export 'src/blame.dart';
|
||||||
export 'src/note.dart';
|
export 'src/note.dart';
|
||||||
|
export 'src/rebase.dart';
|
||||||
export 'src/features.dart';
|
export 'src/features.dart';
|
||||||
export 'src/error.dart';
|
export 'src/error.dart';
|
||||||
export 'src/git_types.dart';
|
export 'src/git_types.dart';
|
||||||
|
|
129
lib/src/bindings/rebase.dart
Normal file
129
lib/src/bindings/rebase.dart
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'libgit2_bindings.dart';
|
||||||
|
import '../error.dart';
|
||||||
|
import '../util.dart';
|
||||||
|
|
||||||
|
/// Initializes a rebase operation to rebase the changes in [branchPointer] relative
|
||||||
|
/// to [upstreamPointer] onto [ontoPointer] another branch. To begin the rebase process, call
|
||||||
|
/// `next()`. When you have finished with this object, call `free()`.
|
||||||
|
///
|
||||||
|
/// [branchPointer] is the terminal commit to rebase, or null to rebase the current branch.
|
||||||
|
///
|
||||||
|
/// [upstreamPointer] is the commit to begin rebasing from, or null to rebase all
|
||||||
|
/// reachable commits.
|
||||||
|
///
|
||||||
|
/// [ontoPointer] is the branch to rebase onto, or null to rebase onto the given upstream.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
Pointer<git_rebase> init({
|
||||||
|
required Pointer<git_repository> repoPointer,
|
||||||
|
required Pointer<git_annotated_commit>? branchPointer,
|
||||||
|
required Pointer<git_annotated_commit>? upstreamPointer,
|
||||||
|
required Pointer<git_annotated_commit>? ontoPointer,
|
||||||
|
}) {
|
||||||
|
final out = calloc<Pointer<git_rebase>>();
|
||||||
|
final opts = calloc<git_rebase_options>();
|
||||||
|
|
||||||
|
final optsError = libgit2.git_rebase_options_init(
|
||||||
|
opts,
|
||||||
|
GIT_REBASE_OPTIONS_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (optsError < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
}
|
||||||
|
|
||||||
|
final error = libgit2.git_rebase_init(
|
||||||
|
out,
|
||||||
|
repoPointer,
|
||||||
|
branchPointer ?? nullptr,
|
||||||
|
upstreamPointer ?? nullptr,
|
||||||
|
ontoPointer ?? nullptr,
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
} else {
|
||||||
|
return out.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the count of rebase operations that are to be applied.
|
||||||
|
int operationsCount(Pointer<git_rebase> rebase) {
|
||||||
|
return libgit2.git_rebase_operation_entrycount(rebase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs the next rebase operation and returns the information about it.
|
||||||
|
/// If the operation is one that applies a patch (which is any operation except
|
||||||
|
/// GIT_REBASE_OPERATION_EXEC) then the patch will be applied and the index and
|
||||||
|
/// working directory will be updated with the changes. If there are conflicts,
|
||||||
|
/// you will need to address those before committing the changes.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
Pointer<git_rebase_operation> next(Pointer<git_rebase> rebase) {
|
||||||
|
final operation = calloc<Pointer<git_rebase_operation>>();
|
||||||
|
final error = libgit2.git_rebase_next(operation, rebase);
|
||||||
|
|
||||||
|
if (error < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
} else {
|
||||||
|
return operation.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commits the current patch. You must have resolved any conflicts that were
|
||||||
|
/// introduced during the patch application from the `next()` invocation.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void commit({
|
||||||
|
required Pointer<git_rebase> rebasePointer,
|
||||||
|
required Pointer<git_signature>? authorPointer,
|
||||||
|
required Pointer<git_signature> committerPointer,
|
||||||
|
required String? message,
|
||||||
|
}) {
|
||||||
|
final id = calloc<git_oid>();
|
||||||
|
final messageC = message?.toNativeUtf8().cast<Int8>() ?? nullptr;
|
||||||
|
|
||||||
|
final error = libgit2.git_rebase_commit(
|
||||||
|
id,
|
||||||
|
rebasePointer,
|
||||||
|
authorPointer ?? nullptr,
|
||||||
|
committerPointer,
|
||||||
|
nullptr,
|
||||||
|
messageC,
|
||||||
|
);
|
||||||
|
|
||||||
|
calloc.free(messageC);
|
||||||
|
|
||||||
|
if (error < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finishes a rebase that is currently in progress once all patches have been applied.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void finish(Pointer<git_rebase> rebase) {
|
||||||
|
final error = libgit2.git_rebase_finish(rebase, nullptr);
|
||||||
|
|
||||||
|
if (error < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aborts a rebase that is currently in progress, resetting the repository and working
|
||||||
|
/// directory to their state before rebase began.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void abort(Pointer<git_rebase> rebase) {
|
||||||
|
final error = libgit2.git_rebase_abort(rebase);
|
||||||
|
|
||||||
|
if (error < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Free memory allocated for rebase object.
|
||||||
|
void free(Pointer<git_rebase> rebase) => libgit2.git_rebase_free(rebase);
|
|
@ -1453,3 +1453,48 @@ class GitBlameFlag {
|
||||||
@override
|
@override
|
||||||
String toString() => 'GitBlameFlag.$_name';
|
String toString() => 'GitBlameFlag.$_name';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Type of rebase operation in-progress after calling rebase's `next()`.
|
||||||
|
class GitRebaseOperation {
|
||||||
|
const GitRebaseOperation._(this._value, this._name);
|
||||||
|
final int _value;
|
||||||
|
final String _name;
|
||||||
|
|
||||||
|
/// The given commit is to be cherry-picked. The client should commit
|
||||||
|
/// the changes and continue if there are no conflicts.
|
||||||
|
static const pick = GitRebaseOperation._(0, 'pick');
|
||||||
|
|
||||||
|
/// The given commit is to be cherry-picked, but the client should prompt
|
||||||
|
/// the user to provide an updated commit message.
|
||||||
|
static const reword = GitRebaseOperation._(1, 'reword');
|
||||||
|
|
||||||
|
/// The given commit is to be cherry-picked, but the client should stop
|
||||||
|
/// to allow the user to edit the changes before committing them.
|
||||||
|
static const edit = GitRebaseOperation._(2, 'edit');
|
||||||
|
|
||||||
|
/// The given commit is to be squashed into the previous commit. The
|
||||||
|
/// commit message will be merged with the previous message.
|
||||||
|
static const squash = GitRebaseOperation._(3, 'squash');
|
||||||
|
|
||||||
|
/// The given commit is to be squashed into the previous commit. The
|
||||||
|
/// commit message from this commit will be discarded.
|
||||||
|
static const fixup = GitRebaseOperation._(4, 'fixup');
|
||||||
|
|
||||||
|
/// No commit will be cherry-picked. The client should run the given
|
||||||
|
/// command and (if successful) continue.
|
||||||
|
static const exec = GitRebaseOperation._(5, 'exec');
|
||||||
|
|
||||||
|
static const List<GitRebaseOperation> values = [
|
||||||
|
pick,
|
||||||
|
reword,
|
||||||
|
edit,
|
||||||
|
squash,
|
||||||
|
fixup,
|
||||||
|
exec,
|
||||||
|
];
|
||||||
|
|
||||||
|
int get value => _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'GitRebaseOperation.$_name';
|
||||||
|
}
|
||||||
|
|
148
lib/src/rebase.dart
Normal file
148
lib/src/rebase.dart
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'bindings/libgit2_bindings.dart';
|
||||||
|
import 'bindings/rebase.dart' as bindings;
|
||||||
|
import 'bindings/commit.dart' as commit_bindings;
|
||||||
|
import 'git_types.dart';
|
||||||
|
import 'oid.dart';
|
||||||
|
import 'repository.dart';
|
||||||
|
import 'signature.dart';
|
||||||
|
|
||||||
|
class Rebase {
|
||||||
|
/// Initializes a new instance of the [Rebase] class by initializing a
|
||||||
|
/// rebase operation to rebase the changes in [branch] relative to [upstream]
|
||||||
|
/// [onto] another branch. To begin the rebase process, call `next()`.
|
||||||
|
/// When you have finished with this object, call `free()`.
|
||||||
|
///
|
||||||
|
/// [branch] is the terminal commit to rebase, default is to rebase the current branch.
|
||||||
|
///
|
||||||
|
/// [upstream] is the commit to begin rebasing from, default is to rebase all
|
||||||
|
/// reachable commits.
|
||||||
|
///
|
||||||
|
/// [onto] is the branch to rebase onto, default is to rebase onto the given [upstream]
|
||||||
|
/// (throws if [upstream] is not provided).
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
Rebase.init({
|
||||||
|
required Repository repo,
|
||||||
|
Oid? branch,
|
||||||
|
Oid? upstream,
|
||||||
|
Oid? onto,
|
||||||
|
}) {
|
||||||
|
Pointer<git_annotated_commit>? _branch, _upstream, _onto;
|
||||||
|
if (branch != null) {
|
||||||
|
_branch = commit_bindings
|
||||||
|
.annotatedLookup(
|
||||||
|
repoPointer: repo.pointer,
|
||||||
|
oidPointer: branch.pointer,
|
||||||
|
)
|
||||||
|
.value;
|
||||||
|
}
|
||||||
|
if (upstream != null) {
|
||||||
|
_upstream = commit_bindings
|
||||||
|
.annotatedLookup(
|
||||||
|
repoPointer: repo.pointer,
|
||||||
|
oidPointer: upstream.pointer,
|
||||||
|
)
|
||||||
|
.value;
|
||||||
|
}
|
||||||
|
if (onto != null) {
|
||||||
|
_onto = commit_bindings
|
||||||
|
.annotatedLookup(
|
||||||
|
repoPointer: repo.pointer,
|
||||||
|
oidPointer: onto.pointer,
|
||||||
|
)
|
||||||
|
.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rebasePointer = bindings.init(
|
||||||
|
repoPointer: repo.pointer,
|
||||||
|
branchPointer: _branch,
|
||||||
|
upstreamPointer: _upstream,
|
||||||
|
ontoPointer: _onto,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pointer to memory address for allocated rebase object.
|
||||||
|
late final Pointer<git_rebase> _rebasePointer;
|
||||||
|
|
||||||
|
/// The count of rebase operations that are to be applied.
|
||||||
|
int get operationsCount {
|
||||||
|
return bindings.operationsCount(_rebasePointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs the next rebase operation and returns the information about it.
|
||||||
|
/// If the operation is one that applies a patch (which is any operation except
|
||||||
|
/// [GitRebaseOperation.exec]) then the patch will be applied and the index and
|
||||||
|
/// working directory will be updated with the changes. If there are conflicts,
|
||||||
|
/// you will need to address those before committing the changes.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
RebaseOperation next() {
|
||||||
|
return RebaseOperation(bindings.next(_rebasePointer));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commits the current patch. You must have resolved any conflicts that were
|
||||||
|
/// introduced during the patch application from the `next()` invocation.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void commit({
|
||||||
|
required Signature committer,
|
||||||
|
String? message,
|
||||||
|
Signature? author,
|
||||||
|
}) {
|
||||||
|
bindings.commit(
|
||||||
|
rebasePointer: _rebasePointer,
|
||||||
|
authorPointer: author?.pointer,
|
||||||
|
committerPointer: committer.pointer,
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finishes a rebase that is currently in progress once all patches have been applied.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void finish() => bindings.finish(_rebasePointer);
|
||||||
|
|
||||||
|
/// Aborts a rebase that is currently in progress, resetting the repository and working
|
||||||
|
/// directory to their state before rebase began.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void abort() => bindings.abort(_rebasePointer);
|
||||||
|
|
||||||
|
/// Releases memory allocated for rebase object.
|
||||||
|
void free() => bindings.free(_rebasePointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RebaseOperation {
|
||||||
|
/// Initializes a new instance of the [RebaseOperation] class from
|
||||||
|
/// provided pointer to rebase operation object in memory.
|
||||||
|
const RebaseOperation(this._rebaseOperationPointer);
|
||||||
|
|
||||||
|
/// Pointer to memory address for allocated rebase operation object.
|
||||||
|
final Pointer<git_rebase_operation> _rebaseOperationPointer;
|
||||||
|
|
||||||
|
/// Returns the type of rebase operation.
|
||||||
|
GitRebaseOperation get type {
|
||||||
|
late final GitRebaseOperation result;
|
||||||
|
for (var operation in GitRebaseOperation.values) {
|
||||||
|
if (_rebaseOperationPointer.ref.type == operation.value) {
|
||||||
|
result = operation;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The commit ID being cherry-picked. This will be populated for
|
||||||
|
/// all operations except those of type [GitRebaseOperation.exec].
|
||||||
|
Oid get id => Oid.fromRaw(_rebaseOperationPointer.ref.id);
|
||||||
|
|
||||||
|
/// The executable the user has requested be run. This will only
|
||||||
|
/// be populated for operations of type [GitRebaseOperation.exec].
|
||||||
|
String get exec {
|
||||||
|
return _rebaseOperationPointer.ref.exec == nullptr
|
||||||
|
? ''
|
||||||
|
: _rebaseOperationPointer.ref.exec.cast<Utf8>().toDartString();
|
||||||
|
}
|
||||||
|
}
|
153
test/rebase_test.dart
Normal file
153
test/rebase_test.dart
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
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;
|
||||||
|
late Directory tmpDir;
|
||||||
|
const shas = [
|
||||||
|
'c68ff54aabf660fcdd9a2838d401583fe31249e3',
|
||||||
|
'821ed6e80627b8769d170a293862f9fc60825226',
|
||||||
|
'14905459d775f3f56a39ebc2ff081163f7da3529',
|
||||||
|
];
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
tmpDir = await setupRepo(Directory('test/assets/mergerepo/'));
|
||||||
|
repo = Repository.open(tmpDir.path);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
repo.free();
|
||||||
|
await tmpDir.delete(recursive: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Rebase', () {
|
||||||
|
test('successfully performs rebase when there is no conflicts', () {
|
||||||
|
final signature = repo.defaultSignature;
|
||||||
|
final master = repo.references['refs/heads/master'];
|
||||||
|
final feature = repo.references['refs/heads/feature'];
|
||||||
|
|
||||||
|
repo.checkout(refName: feature.name);
|
||||||
|
expect(() => repo.index['.gitignore'], throwsA(isA<ArgumentError>()));
|
||||||
|
|
||||||
|
final rebase = Rebase.init(
|
||||||
|
repo: repo,
|
||||||
|
branch: master.target,
|
||||||
|
onto: feature.target,
|
||||||
|
);
|
||||||
|
|
||||||
|
final operationsCount = rebase.operationsCount;
|
||||||
|
expect(operationsCount, 3);
|
||||||
|
|
||||||
|
for (var i = 0; i < operationsCount; i++) {
|
||||||
|
final operation = rebase.next();
|
||||||
|
expect(operation.type, GitRebaseOperation.pick);
|
||||||
|
expect(operation.id.sha, shas[i]);
|
||||||
|
expect(operation.exec, '');
|
||||||
|
|
||||||
|
rebase.commit(committer: signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
rebase.finish();
|
||||||
|
expect(repo.index['.gitignore'], isA<IndexEntry>());
|
||||||
|
|
||||||
|
rebase.free();
|
||||||
|
feature.free();
|
||||||
|
master.free();
|
||||||
|
signature.free();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successfully performs rebase with provided upstream', () {
|
||||||
|
final signature = repo.defaultSignature;
|
||||||
|
final master = repo.references['refs/heads/master'];
|
||||||
|
final feature = repo.references['refs/heads/feature'];
|
||||||
|
final startCommit = repo[shas[1]] as Commit;
|
||||||
|
|
||||||
|
repo.checkout(refName: feature.name);
|
||||||
|
expect(
|
||||||
|
() => repo.index['conflict_file'],
|
||||||
|
throwsA(isA<ArgumentError>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
final rebase = Rebase.init(
|
||||||
|
repo: repo,
|
||||||
|
branch: master.target,
|
||||||
|
onto: feature.target,
|
||||||
|
upstream: startCommit.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
final operationsCount = rebase.operationsCount;
|
||||||
|
expect(operationsCount, 1);
|
||||||
|
|
||||||
|
for (var i = 0; i < operationsCount; i++) {
|
||||||
|
rebase.next();
|
||||||
|
rebase.commit(committer: signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
rebase.finish();
|
||||||
|
expect(repo.index['conflict_file'], isA<IndexEntry>());
|
||||||
|
|
||||||
|
rebase.free();
|
||||||
|
startCommit.free();
|
||||||
|
feature.free();
|
||||||
|
master.free();
|
||||||
|
signature.free();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stops when there is conflicts', () {
|
||||||
|
final signature = repo.defaultSignature;
|
||||||
|
final master = repo.references['refs/heads/master'];
|
||||||
|
final conflict = repo.references['refs/heads/conflict-branch'];
|
||||||
|
|
||||||
|
repo.checkout(refName: conflict.name);
|
||||||
|
|
||||||
|
final rebase = Rebase.init(
|
||||||
|
repo: repo,
|
||||||
|
branch: master.target,
|
||||||
|
onto: conflict.target,
|
||||||
|
);
|
||||||
|
expect(rebase.operationsCount, 1);
|
||||||
|
|
||||||
|
rebase.next();
|
||||||
|
expect(repo.status['conflict_file'], {GitStatus.conflicted});
|
||||||
|
expect(repo.state, GitRepositoryState.rebaseMerge);
|
||||||
|
expect(
|
||||||
|
() => rebase.commit(committer: signature),
|
||||||
|
throwsA(isA<LibGit2Error>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
rebase.free();
|
||||||
|
conflict.free();
|
||||||
|
master.free();
|
||||||
|
signature.free();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successfully aborts rebase in progress', () {
|
||||||
|
final master = repo.references['refs/heads/master'];
|
||||||
|
final conflict = repo.references['refs/heads/conflict-branch'];
|
||||||
|
|
||||||
|
repo.checkout(refName: conflict.name);
|
||||||
|
|
||||||
|
final rebase = Rebase.init(
|
||||||
|
repo: repo,
|
||||||
|
branch: master.target,
|
||||||
|
onto: conflict.target,
|
||||||
|
);
|
||||||
|
expect(rebase.operationsCount, 1);
|
||||||
|
|
||||||
|
rebase.next();
|
||||||
|
expect(repo.status['conflict_file'], {GitStatus.conflicted});
|
||||||
|
expect(repo.state, GitRepositoryState.rebaseMerge);
|
||||||
|
|
||||||
|
rebase.abort();
|
||||||
|
expect(repo.status, isEmpty);
|
||||||
|
expect(repo.state, GitRepositoryState.none);
|
||||||
|
|
||||||
|
rebase.free();
|
||||||
|
conflict.free();
|
||||||
|
master.free();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue