codereview/browser/vcs.py

220 lines
7.1 KiB
Python

import os
import re
import difflib
from datetime import datetime
class VCS(object):
def __init__(self, path):
self._path = path
self._ref = None
def branches(self):
return []
def tags(self):
return []
def ref(self):
return self._ref
def log(self, commit=None, path=None, max=50, offset=0):
return []
def create(type, path):
cls = {
0: Git,
}.get(type, None)
if not cls:
raise Exception('Unknown VCS Type')
return cls(path)
class Blob(object):
def __init__(self, path, data):
self.path = path
self.data = data
class Diff(object):
Types = {
'Add': 0,
'Delete': 1,
'Rename': 2,
'Modify': 3,
}
def __init__(self):
self.a = None
self.b = None
self.type = 0
def unified(self, context=3):
a = self.a.data.split('\n') if self.a else []
b = self.b.data.split('\n') if self.b else []
diff = difflib.unified_diff(
a,
b,
fromfile=self.a.path if self.a else '/dev/null',
tofile=self.b.path if self.b else '/dev/null',
n=context,
lineterm='')
return "\n".join(diff)
def changes(self, context=3):
"Parses the unified diff into a data structure for easy display"
changes = []
line_a = 0
line_b = 0
if context == None:
context = max(
len(self.a.data.split('\n')) if self.a else 0,
len(self.b.data.split('\n')) if self.b else 0)
for line in self.unified(context).split('\n')[2:]:
if line.startswith('@@'):
pattern = r'\-(\d+)(,\d+)? \+(\d+)(,\d+)?'
info = re.findall(pattern, line)
line_a = int(info[0][0])
line_b = int(info[0][2])
change = {
'type': '@',
'text': line,
'line_a': line_a,
'line_b': line_b,
}
changes.append(change)
continue
type = line[0]
text = line[1:]
change = {
'type': type,
'text': text,
'line_a': line_a,
'line_b': line_b,
}
if type == '+':
line_b += 1
elif type == '-':
line_a += 1
else:
line_a += 1
line_b += 1
changes.append(change)
return changes
class Commit(object):
def __init__(self):
self.id = None
self.tree = None
self.message = None
self.author = None
self.author_email = None
self.committer = None
self.committer_email = None
self.authored_date = None
self.committed_date = None
self.parents = []
import git
class Git(VCS):
type = 'Git'
def __init__(self, path):
super(Git, self).__init__(path);
self._repo = git.Repo(self._path)
self._branches = None
self._tags = None
# Set default branch ref
if 'master' in self.branches():
self._ref = 'master'
else:
self._ref = self.branches()[0]
def branches(self):
if not self._branches:
self._branches = dict([(head.name, self.commit(head.commit)) for head in self._repo.heads])
return self._branches
def tags(self):
if not self._tags:
self._tags = dict([(tag.name, self.commit(tag.commit)) for tag in self._repo.tags])
return self._tags
def commit(self, commit):
if type(commit) in [str, unicode]:
commit = self._repo.commit(commit)
c = Commit()
c.id = commit.hexsha
c.tree = commit.tree.hexsha
c.message = commit.message
c.author = commit.author.name
c.author_email = commit.author.email
c.committer = commit.committer.name
c.committer_email = commit.committer.email
c.authored_date = datetime.fromtimestamp(commit.authored_date)
c.committed_date = datetime.fromtimestamp(commit.committed_date)
c.parents = [parent.hexsha for parent in commit.parents]
return c
def log(self, commit=None, path=None, max=50, offset=0):
commit = commit if commit else self._ref
result = []
for c in self._repo.iter_commits(rev=commit, paths=path, max_count=max,
skip=offset):
result.append(self.commit(c))
return result
def diff(self, b, a=None):
"""Get the diff for ref b from ref a
If ref a is not provided, b's parent refs will be used.
*** Note: The parameter order is backwards, since we default the origin
ref to the target's parents.
"""
result = []
b = self._repo.commit(b)
if not a:
# FIXME:
# Only using the first parent for now. Some merge commits seem to be
# causing nasty problems, while others diff just fine.
a = b.parents[:1]
else:
a = self._repo.commit(a)
if a:
diffs = b.diff(a)
else:
# No parents, use the default behaviour (safe for bare repos)
diffs = b.diff()
for diff in diffs:
# b and a are swapped so the parent diff will work as a list of
# parents. Therefore, we'll swap them back when we put them into our
# Diff object.
d = Diff()
if diff.a_blob: d.b = Blob(diff.a_blob.path,
diff.a_blob.data_stream.read())
if diff.b_blob: d.a = Blob(diff.b_blob.path,
diff.b_blob.data_stream.read())
result.append(d)
return result
def browse(self, commit=None, path=''):
if not commit:
commit = self.ref()
files = []
dirs = []
# Locate the tree matching the requested path
tree = self._repo.commit(commit).tree
if path:
for i in tree.traverse():
if type(i) == git.objects.Tree and i.path == path:
tree = i
if path != tree.path:
raise Exception('Path not found')
for node in tree:
if type(node) == git.objects.Blob:
files.append(node.path)
elif type(node) == git.objects.Tree:
dirs.append(node.path)
return dirs, files
def blob(self, commit, path):
tree = self._repo.commit(commit).tree
dir = os.path.dirname(path)
if dir:
for i in tree.traverse():
if type(i) == git.objects.Tree and i.path == dir:
tree = i
if dir != tree.path:
raise Exception('Path not found')
for node in tree:
if type(node) == git.objects.Blob and node.path == path:
return Blob(node.path, node.data_stream.read())
raise Exception('Blob Path not found')
if __name__ == '__main__':
g = Git('/home/correlr/code/voiceaxis')