Skip to content

Commit

Permalink
Move blame to cffi
Browse files Browse the repository at this point in the history
This requires fairly little work on the pygit2 side to kick off all the
searching on the libgit2 side, so it's a fairly good candidate.

This changes the return value for the commit ids to Oid instead of
strings, which is what we generally try to return.
  • Loading branch information
carlosmn committed Aug 31, 2014
1 parent a53d8b2 commit cd08425
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 543 deletions.
161 changes: 161 additions & 0 deletions pygit2/blame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
#
# Copyright 2010-2014 The pygit2 contributors
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2,
# as published by the Free Software Foundation.
#
# In addition to the permissions in the GNU General Public License,
# the authors give you unlimited permission to link the compiled
# version of this file into combinations with other programs,
# and to distribute those combinations without any restriction
# coming from the use of this file. (The General Public License
# restrictions do apply in other respects; for example, they cover
# modification of the file, and distribution when not linked into
# a combined executable.)
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; see the file COPYING. If not, write to
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.

# Import from the future
from __future__ import absolute_import, unicode_literals

# Import from pygit2
from .errors import check_error
from .ffi import ffi, C
from .utils import to_bytes, is_string, to_str
from _pygit2 import Signature, Oid

def wrap_signature(csig):
if not csig:
return None

return Signature(ffi.string(csig.name).decode('utf-8'),
ffi.string(csig.email).decode('utf-8'),
csig.when.time, csig.when.offset, 'utf-8')

class BlameHunk(object):

@classmethod
def _from_c(cls, blame, ptr):
hunk = cls.__new__(cls)
hunk._blame = blame
hunk._hunk = ptr
return hunk

@property
def lines_in_hunk(self):
"""Number of lines"""
return self._hunk.lines_in_hunk

@property
def boundary(self):
"""Tracked to a boundary commit"""
# Casting directly to bool via cffi does not seem to work
return int(ffi.cast('int', self._hunk.boundary)) != 0

@property
def final_start_line_number(self):
"""Final start line number"""
return self._hunk.final_start_line_number

@property
def final_committer(self):
"""Final committer"""
return wrap_signature(self._hunk.final_signature)

@property
def final_commit_id(self):
return Oid(raw=bytes(ffi.buffer(ffi.addressof(self._hunk, 'final_commit_id'))[:]))

@property
def orig_start_line_number(self):
"""Origin start line number"""
return self._hunk.orig_start_line_number

@property
def orig_committer(self):
"""Original committer"""
return wrap_signature(self._hunk.orig_signature)

@property
def orig_commit_id(self):
return Oid(raw=bytes(ffi.buffer(ffi.addressof(self._hunk, 'orig_commit_id'))[:]))

@property
def orig_path(self):
"""Original path"""
path = self._hunk.orig_path
if not path:
return None

return ffi.string(path).decode()


class Blame(object):

@classmethod
def _from_c(cls, repo, ptr):
blame = cls.__new__(cls)
blame._repo = repo
blame._blame = ptr
return blame

def __del__(self):
C.git_blame_free(self._blame)

def __len__(self):
return C.git_blame_get_hunk_count(self._blame)

def __getitem__(self, index):
chunk = C.git_blame_get_hunk_byindex(self._blame, index)
if not chunk:
raise IndexError

return BlameHunk._from_c(self, chunk)

def for_line(self, line_no):
"""for_line(line_no) -> BlameHunk
Returns the blame hunk data for a given line given its number
in the current Blame.
Arguments:
line_no
Line number, starts at 1.
"""
if line_no < 0:
raise IndexError

chunk = C.git_blame_get_hunk_byline(self._blame, line_no)
if not chunk:
raise IndexError

return BlameHunk._from_c(self, chunk)

class BlameIterator(object):
def __init__(self, blame):
self._count = len(blame)
self._index = 0
self._blame = blame

def __next__(self):
if self._index >= self._count:
raise StopIteration

hunk = self._blame[self._blame]
self._index += 1

return hunk

def next(self):
return self.__next__()
54 changes: 53 additions & 1 deletion pygit2/decl.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ typedef ... git_push;
typedef ... git_cred;
typedef ... git_object;
typedef ... git_tree;
typedef ... git_signature;
typedef ... git_index;
typedef ... git_diff;
typedef ... git_index_conflict_iterator;
Expand All @@ -29,6 +28,7 @@ typedef struct git_strarray {
} git_strarray;

typedef int64_t git_off_t;
typedef int64_t git_time_t;

typedef enum {
GIT_OK = 0,
Expand All @@ -55,6 +55,17 @@ typedef struct {
int klass;
} git_error;

typedef struct git_time {
git_time_t time;
int offset;
} git_time;

typedef struct git_signature {
char *name;
char *email;
git_time when;
} git_signature;

const git_error * giterr_last(void);

void git_strarray_free(git_strarray *array);
Expand Down Expand Up @@ -506,3 +517,44 @@ int git_index_conflict_iterator_new(git_index_conflict_iterator **iterator_out,
int git_index_conflict_get(const git_index_entry **ancestor_out, const git_index_entry **our_out, const git_index_entry **their_out, git_index *index, const char *path);
int git_index_conflict_next(const git_index_entry **ancestor_out, const git_index_entry **our_out, const git_index_entry **their_out, git_index_conflict_iterator *iterator);
int git_index_conflict_remove(git_index *index, const char *path);

/*
* git_blame
*/

typedef ... git_blame;

typedef struct git_blame_options {
unsigned int version;

uint32_t flags;
uint16_t min_match_characters;
git_oid newest_commit;
git_oid oldest_commit;
uint32_t min_line;
uint32_t max_line;
} git_blame_options;

#define GIT_BLAME_OPTIONS_VERSION ...

typedef struct git_blame_hunk {
uint16_t lines_in_hunk;

git_oid final_commit_id;
uint16_t final_start_line_number;
git_signature *final_signature;

git_oid orig_commit_id;
const char *orig_path;
uint16_t orig_start_line_number;
git_signature *orig_signature;

char boundary;
} git_blame_hunk;

int git_blame_init_options(git_blame_options *opts, unsigned int version);
uint32_t git_blame_get_hunk_count(git_blame *blame);
const git_blame_hunk *git_blame_get_hunk_byindex(git_blame *blame, uint32_t index);
const git_blame_hunk *git_blame_get_hunk_byline(git_blame *blame, uint32_t lineno);
int git_blame_file(git_blame **out, git_repository *repo, const char *path, git_blame_options *options);
void git_blame_free(git_blame *blame);
58 changes: 57 additions & 1 deletion pygit2/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
from .ffi import ffi, C
from .index import Index
from .remote import Remote
from .utils import to_bytes
from .blame import Blame
from .utils import to_bytes, to_str


class Repository(_Repository):
Expand Down Expand Up @@ -370,6 +371,61 @@ def state_cleanup(self):
"""
C.git_repository_state_cleanup(self._repo)

#
# blame
#
def blame(self, path, flags=None, min_match_characters=None, newest_commit=None, oldest_commit=None, min_line=None, max_line=None):
"""blame(path, [flags, min_match_characters, newest_commit, oldest_commit,\n"
min_line, max_line]) -> Blame
Get the blame for a single file.
Arguments:
path
Path to the file to blame.
flags
A GIT_BLAME_* constant.
min_match_characters
The number of alphanum chars that must be detected as moving/copying
within a file for it to associate those lines with the parent commit.
newest_commit
The id of the newest commit to consider.
oldest_commit
The id of the oldest commit to consider.
min_line
The first line in the file to blame.
max_line
The last line in the file to blame.
Examples::
repo.blame('foo.c', flags=GIT_BLAME_TRACK_COPIES_SAME_FILE)");
"""

options = ffi.new('git_blame_options *')
C.git_blame_init_options(options, C.GIT_BLAME_OPTIONS_VERSION)
if min_match_characters:
options.min_match_characters = min_match_characters
if newest_commit:
if not isinstance(newest_commit, Oid):
newest_commit = Oid(hex=newest_commit)
ffi.buffer(ffi.addressof(options, 'newest_commit'))[:] = newest_commit.raw
if oldest_commit:
if not isinstance(oldest_commit, Oid):
oldest_commit = Oid(hex=oldest_commit)
ffi.buffer(ffi.addressof(options, 'oldest_commit'))[:] = oldest_commit.raw
if min_line:
options.min_line = min_line
if max_line:
options.max_line = max_line

cblame = ffi.new('git_blame **')
err = C.git_blame_file(cblame, self._repo, to_bytes(path), options)
check_error(err)

return Blame._from_c(self, cblame[0])

#
# Index
#
Expand Down
Loading

0 comments on commit cd08425

Please sign in to comment.