Experimental: git driver (untested and unplugged)

git-driver
Vitaliy Filippov 2013-11-28 18:13:52 +04:00
parent 7e83a3bfe7
commit d32e8f42d7
2 changed files with 288 additions and 0 deletions

View File

@ -29,6 +29,7 @@ SIDE_BY_SIDE = 3
# root types returned by Repository.roottype().
CVS = 'cvs'
SVN = 'svn'
GIT = 'git'
# action kinds found in ChangedPath.action
ADDED = 'added'

287
lib/vclib/git/__init__.py Normal file
View File

@ -0,0 +1,287 @@
# -*-python-*-
"Version Control lib driver for locally accessible Git repositories based on the GitPython library"
import vclib
import os
import os.path
import cStringIO
import time
import tempfile
import popen
import re
import urllib
from git import *
def _allow_all(root, path, pool):
"""Generic authz_read_func that permits access to all paths"""
return 1
def _path_parts(path):
return filter(None, path.split('/'))
def _cleanup_path(path):
return '/'.join(_path_parts(path))
def _compare_paths(path1, path2):
path1_len = len (path1);
path2_len = len (path2);
min_len = min(path1_len, path2_len)
i = 0
# Are the paths exactly the same?
if path1 == path2:
return 0
# Skip past common prefix
while (i < min_len) and (path1[i] == path2[i]):
i = i + 1
# Children of paths are greater than their parents, but less than
# greater siblings of their parents
char1 = '\0'
char2 = '\0'
if (i < path1_len):
char1 = path1[i]
if (i < path2_len):
char2 = path2[i]
if (char1 == '/') and (i == path2_len):
return 1
if (char2 == '/') and (i == path1_len):
return -1
if (i < path1_len) and (char1 == '/'):
return -1
if (i < path2_len) and (char2 == '/'):
return 1
# Common prefix was skipped above, next character is compared to
# determine order
return cmp(char1, char2)
def temp_checkout(blob):
temp = tempfile.mktemp()
fp = open(temp, 'wb')
blob.stream_data(fp)
fp.close()
return temp
class LocalGitRepository(vclib.Repository):
def __init__(self, name, rootpath, authorizer, utilities, config_dir):
if not (os.path.isdir(rootpath) and (
os.path.isdir(os.path.join(rootpath, '.git')) or os.path.isfile(os.path.join(rootpath, '.git', 'config')))):
raise vclib.ReposNotFound(name)
# Initialize some stuff.
self.rootpath = rootpath
self.name = name
self.auth = authorizer
self.diff_cmd = utilities.diff or 'diff'
self.config_dir = config_dir or None
# See if this repository is even viewable, authz-wise.
if not vclib.check_root_access(self):
raise vclib.ReposNotFound(name)
def open(self):
# Open the repository and init some other variables.
self.repos = Repo(self.rootpath)
# See if a universal read access determination can be made.
if self.auth and self.auth.check_universal_access(self.name) == 1:
self.auth = None
def rootname(self):
return self.name
def rootpath(self):
return self.rootpath
def roottype(self):
return vclib.GIT
def authorizer(self):
return self.auth
def itemtype(self, path_parts, rev):
c, f, t = self._obj(path_parts, rev) # does authz-check
return t
def openfile(self, path_parts, rev, options):
c, f, t = self._obj(path_parts, rev) # does authz-check
return f.data_stream, c.hexsha
def listdir(self, path_parts, rev, options):
c, f, t = self._obj(path_parts, rev) # does authz-check
if f.type != 'tree':
raise vclib.Error("Path '%s' is not a directory." % self._getpath(path))
entries = []
for i in f.tree:
if i.type == 'tree':
kind = vclib.DIR
else:
kind = vclib.FILE
if vclib.check_path_access(self, path_parts + [ i.name ], kind, rev):
e = vclib.DirEntry(i.name, kind)
# <dirlogs>
h = self.repo.iter_commits(rev, path+'/'+i.name).next()
e.rev = h.binsha
e.date = h.authored_date
e.author = h.author
e.log = h.message
# </dirlogs>
entries.append(e)
return entries
def dirlogs(self, path_parts, rev, entries, options):
pass
def itemlog(self, path_parts, rev, sortby, first, limit, options):
assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
c = self.repo.iter_commits(rev, path)
# FIXME (???) Include c.name_rev into data? (it's the symbolic commit name based on closest reference)
revs = []
i = 0
for c in self.repo.iter_commits(rev, path):
if i >= first:
if path_type == vclib.FILE:
f = c.tree
for p in path_parts:
f = f[p]
s = f.size
else:
s = 0
revs.push(Revision(c.authored_date, c.hexsha, c.authored_date, c.author, c.authored_date, c.message, s, None))
i = i+1
if i >= first+limit:
break
return revs
def itemprops(self, path_parts, rev):
c, f, t = self._obj(path_parts, rev) # does authz-check
return { filemode: f.mode }
def annotate(self, path_parts, rev, include_text=False):
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
if path_type != vclib.FILE:
raise vclib.Error("Path '%s' is not a file." % path)
blame = self.repo.blame(rev, path)
source = []
line_num = 1
youngest_rev = None
for (commit, lines) in blame:
if youngest_rev is None or youngest_rev.authored_date > commit.authored_date:
youngest_rev = commit
for line in lines:
# prev_rev=None
source.append(vclib.Annotation(line, line_num, commit.hexsha, None, commit.author, commit.authored_date))
line_num = line_num+1
return source, youngest_rev.hexsha
def revinfo(self, rev):
commit = self.repo.commit(rev)
changes = []
# FIXME we only pick the first parent commit ;-(
base_rev = commit.parents[0].hexsha
for i in commit.stats.files:
p = _path_parts(i)
t = self.itemtype(p, rev) # does auth-check
# FIXME handle renames... 4th parameter is base path
# FIXME handle filemode change (last param is 'props changed?')
changes.append(ChangedPath(
p, commit.hexsha, t, p, base_rev, action, False, commit.stats.files[i].lines > 0, False
))
return (commit.authored_date, commit.author, commit.message, changes, {})
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
if path_parts1:
c1, f1, t = self._obj(path_parts1, rev1) # does authz-check
if t != vclib.FILE:
raise vclib.Error("Path '%s' is not a file." % self._getpath(path_parts1))
else:
f1 = None
if path_parts2:
c2, f2, t = self._obj(path_parts2, rev2) # does authz-check
if t != vclib.FILE:
raise vclib.Error("Path '%s' is not a file." % self._getpath(path_parts2))
else:
f2 = None
args = vclib._diff_args(type, options)
if p1:
temp1 = temp_checkout(f1)
info1 = p1, c1.authored_date, r1
else:
temp1 = '/dev/null'
info1 = '/dev/null', '', rev1
if p2:
temp2 = temp_checkout(f2)
info2 = p2, c2.authored_date, r2
else:
temp2 = '/dev/null'
info2 = '/dev/null', '', rev1
return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args)
def isexecutable(self, path_parts, rev):
c, f, t = self._obj(path_parts, rev) # does authz-check
if f.mode & 0777:
return True
return False
def filesize(self, path_parts, rev):
c, f, t = self._obj(path_parts, rev) # does authz-check
if f.type != 'blob':
raise vclib.Error("Path '%s' is not a file." % path)
return f.size
##--- helpers ---##
# Returns (commit, object, type)
def _obj(self, path_parts, rev):
c = self.repo.commit(rev)
f = c.tree
for i in path_parts:
if f.type != 'blob':
raise vclib.ItemNotFound(path_parts)
f = f[i]
if f.type == 'blob':
t = vclib.FILE
else:
t = vclib.DIR
if not vclib.check_path_access(self, path_parts, t, rev):
raise vclib.ItemNotFound(t)
return c, f, t
def _getpath(self, path_parts):
return '/'.join(path_parts)
##--- custom ---##
def _getrev(self, rev):
rev = self.repo.commit(rev)
return rev.hexsha
def get_location(self, path, rev, old_rev):
old = self.repo.commit(old_rev)
diff = old.diff(rev)
old_path = None
for i in diff:
if i.b_blob.path == path:
old_path = i.a_blob.path
elif i.rename_to == path:
old_path = i.rename_from
if old_path is None:
raise vclib.ItemNotFound(path)
return _cleanup_path(old_path)
def get_symlink_target(self, path_parts, rev):
"""Return the target of the symbolic link versioned at PATH_PARTS
in REV, or None if that object is not a symlink."""
c, f, t = self._obj(path_parts, rev) # does authz-check
if f.type != 'blob':
raise vclib.Error("Path '%s' is not a file." % path)
if f.mode & 20000:
return f.data_stream.read()
return None