feat(blame): add bindings and api

This commit is contained in:
Aleksey Kulikov 2021-10-01 13:43:44 +03:00
parent 9686d93935
commit 5ee0662376
62 changed files with 1390 additions and 4 deletions

104
lib/src/bindings/blame.dart Normal file
View file

@ -0,0 +1,104 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import '../error.dart';
import '../oid.dart';
import '../util.dart';
import 'libgit2_bindings.dart';
/// Get the blame for a single file.
///
/// Throws a [LibGit2Error] if error occured.
Pointer<git_blame> file({
required Pointer<git_repository> repoPointer,
required String path,
int? flags,
int? minMatchCharacters,
Oid? newestCommit,
Oid? oldestCommit,
int? minLine,
int? maxLine,
}) {
final out = calloc<Pointer<git_blame>>();
final pathC = path.toNativeUtf8().cast<Int8>();
final options = calloc<git_blame_options>();
final optionsError = libgit2.git_blame_options_init(
options,
GIT_BLAME_OPTIONS_VERSION,
);
if (optionsError < 0) {
throw LibGit2Error(libgit2.git_error_last());
}
if (flags != null) {
options.ref.flags = flags;
}
if (minMatchCharacters != null) {
options.ref.min_match_characters = minMatchCharacters;
}
if (newestCommit != null) {
options.ref.newest_commit = newestCommit.pointer.ref;
}
if (oldestCommit != null) {
options.ref.oldest_commit = oldestCommit.pointer.ref;
}
if (minLine != null) {
options.ref.min_line = minLine;
}
if (maxLine != null) {
options.ref.max_line = maxLine;
}
final error = libgit2.git_blame_file(out, repoPointer, pathC, options);
if (error < 0) {
throw LibGit2Error(libgit2.git_error_last());
} else {
return out.value;
}
}
/// Gets the number of hunks that exist in the blame structure.
int hunkCount(Pointer<git_blame> blame) {
return libgit2.git_blame_get_hunk_count(blame);
}
/// Gets the blame hunk at the given index.
///
/// Throws [RangeError] if index out of range.
Pointer<git_blame_hunk> getHunkByIndex({
required Pointer<git_blame> blamePointer,
required int index,
}) {
final result = libgit2.git_blame_get_hunk_byindex(blamePointer, index);
if (result == nullptr) {
throw RangeError('$index is out of bounds');
} else {
return result;
}
}
/// Gets the hunk that relates to the given line number (1-based) in the newest commit.
///
/// Throws [RangeError] if [lineNumber] is out of range.
Pointer<git_blame_hunk> getHunkByLine({
required Pointer<git_blame> blamePointer,
required int lineNumber,
}) {
final result = libgit2.git_blame_get_hunk_byline(blamePointer, lineNumber);
if (result == nullptr) {
throw RangeError('$lineNumber is out of bounds');
} else {
return result;
}
}
/// Free memory allocated for blame object.
void free(Pointer<git_blame> blame) => libgit2.git_blame_free(blame);

172
lib/src/blame.dart Normal file
View file

@ -0,0 +1,172 @@
import 'dart:collection';
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'bindings/libgit2_bindings.dart';
import 'bindings/blame.dart' as bindings;
import 'util.dart';
import 'git_types.dart';
import 'oid.dart';
import 'repository.dart';
import 'signature.dart';
class Blame with IterableMixin<BlameHunk> {
/// Initializes a new instance of the [Blame] class from
/// provided pointer to blame object in memory.
Blame(this._blamePointer);
/// Initializes a new instance of the [Blame] class by getting
/// the blame for a single file.
///
/// [flags] is a combination of [GitBlameFlag]s.
///
/// [minMatchCharacters] is the lower bound on the number of alphanumeric
/// characters that must be detected as moving/copying within a file for
/// it to associate those lines with the parent commit. The default value is 20.
/// This value only takes effect if any of the [GitBlameFlag.trackCopies*]
/// flags are specified.
///
/// [newestCommit] is the id of the newest commit to consider. The default is HEAD.
///
/// [oldestCommit] is the id of the oldest commit to consider. The default is the
/// first commit encountered with no parent.
///
/// [minLine] is the first line in the file to blame. The default is 1
/// (line numbers start with 1).
///
/// [maxLine] is the last line in the file to blame. The default is the last
/// line of the file.
///
/// Throws a [LibGit2Error] if error occured.
Blame.file({
required Repository repo,
required String path,
Set<GitBlameFlag> flags = const {GitBlameFlag.normal},
int? minMatchCharacters,
Oid? newestCommit,
Oid? oldestCommit,
int? minLine,
int? maxLine,
}) {
libgit2.git_libgit2_init();
final int flagsInt =
flags.fold(0, (previousValue, e) => previousValue | e.value);
_blamePointer = bindings.file(
repoPointer: repo.pointer,
path: path,
flags: flagsInt,
minMatchCharacters: minMatchCharacters,
newestCommit: newestCommit,
oldestCommit: oldestCommit,
minLine: minLine,
maxLine: maxLine,
);
}
/// Pointer to memory address for allocated blame object.
late final Pointer<git_blame> _blamePointer;
/// Returns the blame hunk at the given index.
///
/// Throws [RangeError] if index out of range.
BlameHunk operator [](int index) {
return BlameHunk(bindings.getHunkByIndex(
blamePointer: _blamePointer,
index: index,
));
}
/// Gets the hunk that relates to the given line number (1-based) in the newest commit.
///
/// Throws [RangeError] if [lineNumber] is out of range.
BlameHunk forLine(int lineNumber) {
return BlameHunk(bindings.getHunkByLine(
blamePointer: _blamePointer,
lineNumber: lineNumber,
));
}
/// Releases memory allocated for blame object.
void free() => bindings.free(_blamePointer);
@override
Iterator<BlameHunk> get iterator => _BlameIterator(_blamePointer);
}
class BlameHunk {
/// Initializes a new instance of the [BlameHunk] class from
/// provided pointer to blame hunk object in memory.
const BlameHunk(this._blameHunkPointer);
/// Pointer to memory address for allocated blame hunk object.
final Pointer<git_blame_hunk> _blameHunkPointer;
/// Returns the number of lines in this hunk.
int get linesCount => _blameHunkPointer.ref.lines_in_hunk;
/// Checks if the hunk has been tracked to a boundary commit
/// (the root, or the commit specified in [oldestCommit] argument)
bool get isBoundary {
return _blameHunkPointer.ref.boundary == 1 ? true : false;
}
/// Returns the 1-based line number where this hunk begins, in the final
/// version of the file.
int get finalStartLineNumber => _blameHunkPointer.ref.final_start_line_number;
/// Returns the author of [finalCommitId]. If [GitBlameFlag.useMailmap] has been
/// specified, it will contain the canonical real name and email address.
Signature get finalCommitter =>
Signature(_blameHunkPointer.ref.final_signature);
/// Returns the [Oid] of the commit where this line was last changed.
Oid get finalCommitId => Oid.fromRaw(_blameHunkPointer.ref.final_commit_id);
/// Returns the 1-based line number where this hunk begins, in the file
/// named by [originPath] in the commit specified by [originCommitId].
int get originStartLineNumber => _blameHunkPointer.ref.orig_start_line_number;
/// Returns the author of [originCommitId]. If [GitBlameFlag.useMailmap] has been
/// specified, it will contain the canonical real name and email address.
Signature get originCommitter =>
Signature(_blameHunkPointer.ref.orig_signature);
/// Returns the [Oid] of the commit where this hunk was found. This will usually be
/// the same as [finalCommitId], except when [GitBlameFlag.trackCopiesAnyCommitCopies]
/// been specified.
Oid get originCommitId => Oid.fromRaw(_blameHunkPointer.ref.orig_commit_id);
/// Returns the path to the file where this hunk originated, as of the commit
/// specified by [originCommitId].
String get originPath =>
_blameHunkPointer.ref.orig_path.cast<Utf8>().toDartString();
}
class _BlameIterator implements Iterator<BlameHunk> {
_BlameIterator(this._blamePointer) {
count = bindings.hunkCount(_blamePointer);
}
final Pointer<git_blame> _blamePointer;
BlameHunk? currentHunk;
late final int count;
int index = 0;
@override
BlameHunk get current => currentHunk!;
@override
bool moveNext() {
if (index == count) {
return false;
} else {
currentHunk = BlameHunk(bindings.getHunkByIndex(
blamePointer: _blamePointer,
index: index,
));
index++;
return true;
}
}
}

View file

@ -1384,3 +1384,72 @@ class GitAttributeCheck {
@override
String toString() => 'GitAttributeCheck.$_name';
}
/// Flags for indicating option behavior for git blame APIs.
class GitBlameFlag {
const GitBlameFlag._(this._value, this._name);
final int _value;
final String _name;
/// Normal blame, the default.
static const normal = GitBlameFlag._(0, 'normal');
/// Track lines that have moved within a file (like `git blame -M`).
///
/// This is not yet implemented and reserved for future use.
static const trackCopiesSameFile = GitBlameFlag._(1, 'trackCopiesSameFile');
/// Track lines that have moved across files in the same commit
/// (like `git blame -C`).
///
/// This is not yet implemented and reserved for future use.
static const trackCopiesSameCommitMoves =
GitBlameFlag._(2, 'trackCopiesSameCommitMoves');
/// Track lines that have been copied from another file that exists
/// in the same commit (like `git blame -CC`). Implies SAME_FILE.
///
/// This is not yet implemented and reserved for future use.
static const trackCopiesSameCommitCopies = GitBlameFlag._(
4,
'trackCopiesSameCommitCopies',
);
/// Track lines that have been copied from another file that exists in
/// *any* commit (like `git blame -CCC`). Implies SAME_COMMIT_COPIES.
///
/// This is not yet implemented and reserved for future use.
static const trackCopiesAnyCommitCopies = GitBlameFlag._(
8,
'trackCopiesAnyCommitCopies',
);
/// Restrict the search of commits to those reachable following only
/// the first parents.
static const firstParent = GitBlameFlag._(16, 'firstParent');
/// Use mailmap file to map author and committer names and email
/// addresses to canonical real names and email addresses. The
/// mailmap will be read from the working directory, or HEAD in a
/// bare repository.
static const useMailmap = GitBlameFlag._(32, 'useMailmap');
/// Ignore whitespace differences.
static const ignoreWhitespace = GitBlameFlag._(64, 'ignoreWhitespace');
static const List<GitBlameFlag> values = [
normal,
trackCopiesSameFile,
trackCopiesSameCommitMoves,
trackCopiesSameCommitCopies,
trackCopiesAnyCommitCopies,
firstParent,
useMailmap,
ignoreWhitespace,
];
int get value => _value;
@override
String toString() => 'GitBlameFlag.$_name';
}

View file

@ -1,15 +1,14 @@
import 'dart:ffi';
import 'package:libgit2dart/libgit2dart.dart';
import 'bindings/libgit2_bindings.dart';
import 'bindings/remote.dart' as bindings;
import 'callbacks.dart';
import 'git_types.dart';
import 'refspec.dart';
import 'repository.dart';
class Remotes {
/// Initializes a new instance of the [References] class
/// from provided [Repository] object.
/// Initializes a new instance of the [Remotes] class from
/// provided [Repository] object.
Remotes(Repository repo) {
_repoPointer = repo.pointer;
}

View file

@ -1075,4 +1075,47 @@ class Repository {
name: name,
);
}
/// Gets the blame for a single file.
///
/// [flags] is a combination of [GitBlameFlag]s.
///
/// [minMatchCharacters] is the lower bound on the number of alphanumeric
/// characters that must be detected as moving/copying within a file for
/// it to associate those lines with the parent commit. The default value is 20.
/// This value only takes effect if any of the [GitBlameFlag.trackCopies*]
/// flags are specified.
///
/// [newestCommit] is the id of the newest commit to consider. The default is HEAD.
///
/// [oldestCommit] is the id of the oldest commit to consider. The default is the
/// first commit encountered with no parent.
///
/// [minLine] is the first line in the file to blame. The default is 1
/// (line numbers start with 1).
///
/// [maxLine] is the last line in the file to blame. The default is the last
/// line of the file.
///
/// Throws a [LibGit2Error] if error occured.
Blame blame({
required String path,
Set<GitBlameFlag> flags = const {GitBlameFlag.normal},
int? minMatchCharacters,
Oid? newestCommit,
Oid? oldestCommit,
int? minLine,
int? maxLine,
}) {
return Blame.file(
repo: this,
path: path,
flags: flags,
minMatchCharacters: minMatchCharacters,
newestCommit: newestCommit,
oldestCommit: oldestCommit,
minLine: minLine,
maxLine: maxLine,
);
}
}

View file

@ -80,4 +80,9 @@ class Signature {
/// Releases memory allocated for signature object.
void free() => bindings.free(_signaturePointer);
@override
String toString() {
return 'Signature{name: $name, email: $email, time: $time, offset: $offset}';
}
}