mirror of
https://github.com/SkinnyMind/libgit2dart.git
synced 2025-05-04 20:29:08 -04:00
feat(revwalk): add bindings and api
This commit is contained in:
parent
e0e3742457
commit
2e0486c641
7 changed files with 424 additions and 0 deletions
|
@ -8,5 +8,6 @@ export 'src/reference.dart';
|
||||||
export 'src/reflog.dart';
|
export 'src/reflog.dart';
|
||||||
export 'src/tree.dart';
|
export 'src/tree.dart';
|
||||||
export 'src/signature.dart';
|
export 'src/signature.dart';
|
||||||
|
export 'src/revwalk.dart';
|
||||||
export 'src/error.dart';
|
export 'src/error.dart';
|
||||||
export 'src/enums.dart';
|
export 'src/enums.dart';
|
||||||
|
|
136
lib/src/bindings/revwalk.dart
Normal file
136
lib/src/bindings/revwalk.dart
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'libgit2_bindings.dart';
|
||||||
|
import 'commit.dart' as commit_bindings;
|
||||||
|
import '../error.dart';
|
||||||
|
import '../util.dart';
|
||||||
|
|
||||||
|
/// Allocate a new revision walker to iterate through a repo.
|
||||||
|
///
|
||||||
|
/// This revision walker uses a custom memory pool and an internal commit cache,
|
||||||
|
/// so it is relatively expensive to allocate.
|
||||||
|
///
|
||||||
|
/// For maximum performance, this revision walker should be reused for different walks.
|
||||||
|
///
|
||||||
|
/// This revision walker is not thread safe: it may only be used to walk a repository
|
||||||
|
/// on a single thread; however, it is possible to have several revision walkers in several
|
||||||
|
/// different threads walking the same repository.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
Pointer<git_revwalk> create(Pointer<git_repository> repo) {
|
||||||
|
final out = calloc<Pointer<git_revwalk>>();
|
||||||
|
final error = libgit2.git_revwalk_new(out, repo);
|
||||||
|
|
||||||
|
if (error < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
} else {
|
||||||
|
return out.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the sorting mode when iterating through the repository's contents.
|
||||||
|
///
|
||||||
|
/// Changing the sorting mode resets the walker.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void sorting(Pointer<git_revwalk> walker, int sortMode) {
|
||||||
|
final error = libgit2.git_revwalk_sorting(walker, sortMode);
|
||||||
|
|
||||||
|
if (error < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new root for the traversal.
|
||||||
|
///
|
||||||
|
/// The pushed commit will be marked as one of the roots from which to start the walk.
|
||||||
|
/// This commit may not be walked if it or a child is hidden.
|
||||||
|
///
|
||||||
|
/// At least one commit must be pushed onto the walker before a walk can be started.
|
||||||
|
///
|
||||||
|
/// The given id must belong to a committish on the walked repository.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void push(Pointer<git_revwalk> walker, Pointer<git_oid> id) {
|
||||||
|
final error = libgit2.git_revwalk_push(walker, id);
|
||||||
|
|
||||||
|
if (error < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the list of commits from the revision walk.
|
||||||
|
///
|
||||||
|
/// The initial call to this method is not blocking when iterating through a repo
|
||||||
|
/// with a time-sorting mode.
|
||||||
|
///
|
||||||
|
/// Iterating with Topological or inverted modes makes the initial call blocking to
|
||||||
|
/// preprocess the commit list, but this block should be mostly unnoticeable on most
|
||||||
|
/// repositories (topological preprocessing times at 0.3s on the git.git repo).
|
||||||
|
///
|
||||||
|
/// The revision walker is reset when the walk is over.
|
||||||
|
List<Pointer<git_commit>> walk(
|
||||||
|
Pointer<git_repository> repo,
|
||||||
|
Pointer<git_revwalk> walker,
|
||||||
|
) {
|
||||||
|
var result = <Pointer<git_commit>>[];
|
||||||
|
var error = 0;
|
||||||
|
|
||||||
|
while (error == 0) {
|
||||||
|
final oid = calloc<git_oid>();
|
||||||
|
error = libgit2.git_revwalk_next(oid, walker);
|
||||||
|
if (error == 0) {
|
||||||
|
final commit = commit_bindings.lookup(repo, oid);
|
||||||
|
result.add(commit);
|
||||||
|
calloc.free(oid);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a commit (and its ancestors) uninteresting for the output.
|
||||||
|
///
|
||||||
|
/// The given id must belong to a committish on the walked repository.
|
||||||
|
///
|
||||||
|
/// The resolved commit and all its parents will be hidden from the output on the revision walk.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void hide(Pointer<git_revwalk> walk, Pointer<git_oid> oid) {
|
||||||
|
final error = libgit2.git_revwalk_hide(walk, oid);
|
||||||
|
|
||||||
|
if (error < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the revision walker for reuse.
|
||||||
|
///
|
||||||
|
/// This will clear all the pushed and hidden commits, and leave the walker in a blank state
|
||||||
|
/// (just like at creation) ready to receive new commit pushes and start a new walk.
|
||||||
|
///
|
||||||
|
/// The revision walk is automatically reset when a walk is over.
|
||||||
|
void reset(Pointer<git_revwalk> walker) => libgit2.git_revwalk_reset(walker);
|
||||||
|
|
||||||
|
/// Simplify the history by first-parent.
|
||||||
|
///
|
||||||
|
/// No parents other than the first for each commit will be enqueued.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void simplifyFirstParent(Pointer<git_revwalk> walk) {
|
||||||
|
final error = libgit2.git_revwalk_simplify_first_parent(walk);
|
||||||
|
|
||||||
|
if (error < 0) {
|
||||||
|
throw LibGit2Error(libgit2.git_error_last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the repository on which this walker is operating.
|
||||||
|
Pointer<git_repository> repository(Pointer<git_revwalk> walker) {
|
||||||
|
return libgit2.git_revwalk_repository(walker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Free a revision walker previously allocated.
|
||||||
|
void free(Pointer<git_revwalk> walk) => libgit2.git_revwalk_free(walk);
|
|
@ -1,3 +1,19 @@
|
||||||
enum ReferenceType { direct, symbolic }
|
enum ReferenceType { direct, symbolic }
|
||||||
|
|
||||||
enum GitFilemode { undreadable, tree, blob, blobExecutable, link, commit }
|
enum GitFilemode { undreadable, tree, blob, blobExecutable, link, commit }
|
||||||
|
|
||||||
|
/// Flags to specify the sorting which a revwalk should perform.
|
||||||
|
///
|
||||||
|
/// [none] sort the output with the same default method from `git`: reverse
|
||||||
|
/// chronological order. This is the default sorting for new walkers.
|
||||||
|
///
|
||||||
|
/// [topological] sort the repository contents in topological order (no parents before
|
||||||
|
/// all of its children are shown); this sorting mode can be combined
|
||||||
|
/// with time sorting to produce `git`'s `--date-order``.
|
||||||
|
///
|
||||||
|
/// [time] sort the repository contents by commit time;
|
||||||
|
/// this sorting mode can be combined with topological sorting.
|
||||||
|
///
|
||||||
|
/// [reverse] Iterate through the repository contents in reverse
|
||||||
|
/// order; this sorting mode can be combined with any of the above.
|
||||||
|
enum GitSort { none, topological, time, reverse }
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
|
import 'package:libgit2dart/src/enums.dart';
|
||||||
|
import 'package:libgit2dart/src/revwalk.dart';
|
||||||
|
|
||||||
import 'commit.dart';
|
import 'commit.dart';
|
||||||
import 'config.dart';
|
import 'config.dart';
|
||||||
import 'index.dart';
|
import 'index.dart';
|
||||||
|
@ -320,4 +323,19 @@ class Repository {
|
||||||
}
|
}
|
||||||
return Commit.lookup(this, oid);
|
return Commit.lookup(this, oid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the list of commits starting from provided [oid].
|
||||||
|
///
|
||||||
|
/// If [sorting] isn't provided default will be used (reverse chronological order, like in git).
|
||||||
|
List<Commit> log(Oid oid, [GitSort sorting = GitSort.none]) {
|
||||||
|
final walker = RevWalk(this);
|
||||||
|
if (sorting != GitSort.none) {
|
||||||
|
walker.sorting(sorting);
|
||||||
|
}
|
||||||
|
walker.push(oid);
|
||||||
|
final result = walker.walk();
|
||||||
|
walker.free();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
89
lib/src/revwalk.dart
Normal file
89
lib/src/revwalk.dart
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'bindings/libgit2_bindings.dart';
|
||||||
|
import 'bindings/revwalk.dart' as bindings;
|
||||||
|
import 'commit.dart';
|
||||||
|
import 'oid.dart';
|
||||||
|
import 'repository.dart';
|
||||||
|
import 'enums.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class RevWalk {
|
||||||
|
/// Initializes a new instance of the [RevWalk] class.
|
||||||
|
/// Should be freed with `free()` to release allocated memory.
|
||||||
|
RevWalk(Repository repo) {
|
||||||
|
libgit2.git_libgit2_init();
|
||||||
|
_revWalkPointer = bindings.create(repo.pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pointer to memory address for allocated [RevWalk] object.
|
||||||
|
late final Pointer<git_revwalk> _revWalkPointer;
|
||||||
|
|
||||||
|
/// Returns the list of commits from the revision walk.
|
||||||
|
///
|
||||||
|
/// Default sorting is reverse chronological order (default in git).
|
||||||
|
List<Commit> walk() {
|
||||||
|
final repoPointer = bindings.repository(_revWalkPointer);
|
||||||
|
var result = <Commit>[];
|
||||||
|
|
||||||
|
final commits = bindings.walk(repoPointer, _revWalkPointer);
|
||||||
|
for (var commit in commits) {
|
||||||
|
result.add(Commit(commit));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the sorting mode when iterating through the repository's contents.
|
||||||
|
///
|
||||||
|
/// Changing the sorting mode resets the walker.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void sorting(GitSort sorting) {
|
||||||
|
bindings.sorting(
|
||||||
|
_revWalkPointer,
|
||||||
|
// in libgit2 GIT_SORT_REVERSE flag is integer 4 so we are adding 1 to our enum index
|
||||||
|
sorting == GitSort.reverse ? sorting.index + 1 : sorting.index,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new root for the traversal.
|
||||||
|
///
|
||||||
|
/// The pushed commit will be marked as one of the roots from which to start the walk.
|
||||||
|
/// This commit may not be walked if it or a child is hidden.
|
||||||
|
///
|
||||||
|
/// At least one commit must be pushed onto the walker before a walk can be started.
|
||||||
|
///
|
||||||
|
/// The given id must belong to a committish on the walked repository.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void push(Oid oid) => bindings.push(_revWalkPointer, oid.pointer);
|
||||||
|
|
||||||
|
/// Marks a commit (and its ancestors) uninteresting for the output.
|
||||||
|
///
|
||||||
|
/// The given id must belong to a committish on the walked repository.
|
||||||
|
///
|
||||||
|
/// The resolved commit and all its parents will be hidden from the output on the revision walk.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void hide(Oid oid) => bindings.hide(_revWalkPointer, oid.pointer);
|
||||||
|
|
||||||
|
/// Resets the revision walker for reuse.
|
||||||
|
///
|
||||||
|
/// This will clear all the pushed and hidden commits, and leave the walker in a blank state
|
||||||
|
/// (just like at creation) ready to receive new commit pushes and start a new walk.
|
||||||
|
///
|
||||||
|
/// The revision walk is automatically reset when a walk is over.
|
||||||
|
void reset() => bindings.reset(_revWalkPointer);
|
||||||
|
|
||||||
|
/// Simplify the history by first-parent.
|
||||||
|
///
|
||||||
|
/// No parents other than the first for each commit will be enqueued.
|
||||||
|
///
|
||||||
|
/// Throws a [LibGit2Error] if error occured.
|
||||||
|
void simplifyFirstParent() => bindings.simplifyFirstParent(_revWalkPointer);
|
||||||
|
|
||||||
|
/// Releases memory allocated for [RevWalk] object.
|
||||||
|
void free() {
|
||||||
|
bindings.free(_revWalkPointer);
|
||||||
|
}
|
||||||
|
}
|
|
@ -179,6 +179,26 @@ void main() {
|
||||||
config.free();
|
config.free();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('returns list of commits by walking from provided starting oid', () {
|
||||||
|
const log = [
|
||||||
|
'78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8',
|
||||||
|
'c68ff54aabf660fcdd9a2838d401583fe31249e3',
|
||||||
|
'fc38877b2552ab554752d9a77e1f48f738cca79b',
|
||||||
|
'6cbc22e509d72758ab4c8d9f287ea846b90c448b',
|
||||||
|
'f17d0d48eae3aa08cecf29128a35e310c97b3521',
|
||||||
|
];
|
||||||
|
final start = Oid.fromSHA(repo, lastCommit);
|
||||||
|
final commits = repo.log(start);
|
||||||
|
|
||||||
|
for (var i = 0; i < commits.length; i++) {
|
||||||
|
expect(commits[i].id.sha, log[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var c in commits) {
|
||||||
|
c.free();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
group('.discover()', () {
|
group('.discover()', () {
|
||||||
test('discovers repository', () async {
|
test('discovers repository', () async {
|
||||||
final subDir = '${tmpDir}subdir1/subdir2/';
|
final subDir = '${tmpDir}subdir1/subdir2/';
|
||||||
|
|
144
test/revwalk_test.dart
Normal file
144
test/revwalk_test.dart
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:libgit2dart/libgit2dart.dart';
|
||||||
|
import 'helpers/util.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
const log = [
|
||||||
|
'78b8bf123e3952c970ae5c1ce0a3ea1d1336f6e8',
|
||||||
|
'c68ff54aabf660fcdd9a2838d401583fe31249e3',
|
||||||
|
'fc38877b2552ab554752d9a77e1f48f738cca79b',
|
||||||
|
'6cbc22e509d72758ab4c8d9f287ea846b90c448b',
|
||||||
|
'f17d0d48eae3aa08cecf29128a35e310c97b3521',
|
||||||
|
];
|
||||||
|
late Repository repo;
|
||||||
|
final tmpDir = '${Directory.systemTemp.path}/revwalk_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('RevWalk', () {
|
||||||
|
test('returns list of commits with default sorting', () {
|
||||||
|
final walker = RevWalk(repo);
|
||||||
|
final start = Oid.fromSHA(repo, log.first);
|
||||||
|
|
||||||
|
walker.push(start);
|
||||||
|
final commits = walker.walk();
|
||||||
|
|
||||||
|
for (var i = 0; i < commits.length; i++) {
|
||||||
|
expect(commits[i].id.sha, log[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var c in commits) {
|
||||||
|
c.free();
|
||||||
|
}
|
||||||
|
walker.free();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns list of commits with reverse sorting', () {
|
||||||
|
final walker = RevWalk(repo);
|
||||||
|
final start = Oid.fromSHA(repo, log.first);
|
||||||
|
|
||||||
|
walker.push(start);
|
||||||
|
walker.sorting(GitSort.reverse);
|
||||||
|
final commits = walker.walk();
|
||||||
|
|
||||||
|
for (var i = 0; i < commits.length; i++) {
|
||||||
|
expect(commits[i].id.sha, log.reversed.toList()[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var c in commits) {
|
||||||
|
c.free();
|
||||||
|
}
|
||||||
|
walker.free();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successfully changes sorting', () {
|
||||||
|
final walker = RevWalk(repo);
|
||||||
|
final start = Oid.fromSHA(repo, log.first);
|
||||||
|
|
||||||
|
walker.push(start);
|
||||||
|
final timeSortedCommits = walker.walk();
|
||||||
|
|
||||||
|
for (var i = 0; i < timeSortedCommits.length; i++) {
|
||||||
|
expect(timeSortedCommits[i].id.sha, log[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
walker.sorting(GitSort.reverse);
|
||||||
|
final reverseSortedCommits = walker.walk();
|
||||||
|
for (var i = 0; i < reverseSortedCommits.length; i++) {
|
||||||
|
expect(reverseSortedCommits[i].id.sha, log.reversed.toList()[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var c in timeSortedCommits) {
|
||||||
|
c.free();
|
||||||
|
}
|
||||||
|
for (var c in reverseSortedCommits) {
|
||||||
|
c.free();
|
||||||
|
}
|
||||||
|
walker.free();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successfully hides commit and its ancestors', () {
|
||||||
|
final walker = RevWalk(repo);
|
||||||
|
final start = Oid.fromSHA(repo, log.first);
|
||||||
|
final oidToHide = Oid.fromSHA(repo, log[2]);
|
||||||
|
|
||||||
|
walker.push(start);
|
||||||
|
walker.hide(oidToHide);
|
||||||
|
final commits = walker.walk();
|
||||||
|
|
||||||
|
expect(commits.length, 2);
|
||||||
|
|
||||||
|
for (var c in commits) {
|
||||||
|
c.free();
|
||||||
|
}
|
||||||
|
walker.free();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successfully resets walker', () {
|
||||||
|
final walker = RevWalk(repo);
|
||||||
|
final start = Oid.fromSHA(repo, log.first);
|
||||||
|
|
||||||
|
walker.push(start);
|
||||||
|
walker.reset();
|
||||||
|
final commits = walker.walk();
|
||||||
|
|
||||||
|
expect(commits, []);
|
||||||
|
|
||||||
|
walker.free();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('simplifies walker by enqueuing only first parent for each commit',
|
||||||
|
() {
|
||||||
|
final walker = RevWalk(repo);
|
||||||
|
final start = Oid.fromSHA(repo, log.first);
|
||||||
|
|
||||||
|
walker.push(start);
|
||||||
|
walker.simplifyFirstParent();
|
||||||
|
final commits = walker.walk();
|
||||||
|
|
||||||
|
for (var i = 0; i < commits.length; i++) {
|
||||||
|
expect(commits.length, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var c in commits) {
|
||||||
|
c.free();
|
||||||
|
}
|
||||||
|
walker.free();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue