You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by jo...@apache.org on 2013/06/11 01:31:36 UTC
git commit: [#6272] WIP: Make SCM log indexless
Updated Branches:
refs/heads/cj/6272 [created] 0a36f190e
[#6272] WIP: Make SCM log indexless
Signed-off-by: Cory Johns <cj...@slashdotmedia.com>
Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/0a36f190
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/0a36f190
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/0a36f190
Branch: refs/heads/cj/6272
Commit: 0a36f190e0acebc5c03a3b5983555483c9a77f27
Parents: 22dc0fe
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Mon Jun 10 23:24:51 2013 +0000
Committer: Cory Johns <cj...@slashdotmedia.com>
Committed: Mon Jun 10 23:24:51 2013 +0000
----------------------------------------------------------------------
Allura/allura/controllers/repository.py | 73 ++++++-------
Allura/allura/lib/helpers.py | 11 ++
Allura/allura/model/repository.py | 118 ++++++++-------------
Allura/allura/templates/jinja_master/lib.html | 7 ++
Allura/allura/templates/widgets/repo/log.html | 34 +++---
ForgeGit/forgegit/model/git_repo.py | 79 +++++++++++---
ForgeSVN/forgesvn/model/svn.py | 104 +++++++++++-------
7 files changed, 237 insertions(+), 189 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0a36f190/Allura/allura/controllers/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/repository.py b/Allura/allura/controllers/repository.py
index 084e17a..ecbd1b3 100644
--- a/Allura/allura/controllers/repository.py
+++ b/Allura/allura/controllers/repository.py
@@ -22,6 +22,7 @@ import re
import difflib
from urllib import quote, unquote
from collections import defaultdict
+from itertools import islice
from pylons import tmpl_context as c, app_globals as g
from pylons import request, response
@@ -206,11 +207,11 @@ class RepoRootController(BaseController, FeedController):
@expose('json:')
def commit_browser_data(self):
head_ids = [ head.object_id for head in c.app.repo.get_heads() ]
- commit_ids = list(c.app.repo.commitlog(head_ids))
+ commit_ids = list(c.app.repo.log(head_ids, id_only=True))
log.info('Grab %d commit objects by ID', len(commit_ids))
- commits_by_id = dict(
- (c_obj._id, c_obj)
- for c_obj in M.repo.CommitDoc.m.find(dict(_id={'$in': commit_ids})))
+ commits_by_id = {
+ c_obj._id: c_obj
+ for c_obj in M.repo.CommitDoc.m.find(dict(_id={'$in': commit_ids}))}
log.info('... build graph')
parents = {}
children = defaultdict(list)
@@ -276,32 +277,30 @@ class RepoRestController(RepoRootController):
return dict(commit_count=len(all_commits))
@expose('json:')
- def commits(self, **kw):
- page_size = 25
- offset = (int(kw.get('page',1)) * page_size) - page_size
- revisions = c.app.repo.log(offset=offset, limit=page_size)
-
- return dict(
- commits=[
- dict(
- parents=[dict(id=p) for p in commit.parent_ids],
- author=dict(
- name=commit.authored.name,
- email=commit.authored.email,
- ),
- url=commit.url(),
- id=commit._id,
- committed_date=commit.committed.date,
- authored_date=commit.authored.date,
- message=commit.message,
- tree=commit.tree._id,
- committer=dict(
- name=commit.committed.name,
- email=commit.committed.email,
- ),
- )
+ def commits(self, rev=None, **kw):
+ revisions = islice(c.app.repo.log(rev), 25)
+
+ return {
+ 'commits': [
+ {
+ 'parents': [dict(id=p) for p in commit.parent_ids],
+ 'author': {
+ 'name': commit.authored.name,
+ 'email': commit.authored.email,
+ },
+ 'url': commit.url(),
+ 'id': commit._id,
+ 'committed_date': commit.committed.date,
+ 'authored_date': commit.authored.date,
+ 'message': commit.message,
+ 'tree': commit.tree._id,
+ 'committer': {
+ 'name': commit.committed.name,
+ 'email': commit.committed.email,
+ },
+ }
for commit in revisions
- ])
+ ]}
class MergeRequestsController(object):
mr_filter=SCMMergeRequestFilterWidget()
@@ -478,21 +477,19 @@ class CommitBrowser(BaseController):
if path:
path = path.lstrip('/')
is_file = self.tree._tree.get_blob_by_path(path) is not None
- params = dict(path=path, rev=self._commit._id)
- commits = list(c.app.repo.commits(limit=limit+1, **params))
+ commits = list(islice(c.app.repo.log(
+ revs=self._commit._id,
+ path=path,
+ id_only=False,
+ page_size=limit+1), limit+1))
next_commit = None
if len(commits) > limit:
- next_commit = M.repo.Commit.query.get(_id=commits.pop())
- next_commit.set_context(c.app.repo)
- revisions = list(M.repo.Commit.query.find({'_id': {'$in': commits}}))
- for commit in revisions:
- commit.set_context(c.app.repo)
- revisions = sorted(revisions, key=lambda c:commits.index(c._id))
+ next_commit = commits.pop()
c.log_widget = self.log_widget
return dict(
username=c.user._id and c.user.username,
branch=None,
- log=revisions,
+ log=commits,
next_commit=next_commit,
limit=limit,
path=path,
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0a36f190/Allura/allura/lib/helpers.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index 9f87115..9c2bbdc 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -138,6 +138,17 @@ def really_unicode(s):
yield 'latin-1'
return _attempt_encodings(s, encodings())
+def find_user(email=None, username=None, display_name=None):
+ from allura import model as M
+ user = None
+ if email:
+ user = M.User.by_email_address(email)
+ if not user and username:
+ user = M.User.by_username(username)
+ if not user and display_name:
+ user = M.User.by_display_name(display_name)
+ return user
+
def find_project(url_path):
from allura import model as M
for n in M.Neighborhood.query.find():
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0a36f190/Allura/allura/model/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repository.py b/Allura/allura/model/repository.py
index dfb92f1..e624d55 100644
--- a/Allura/allura/model/repository.py
+++ b/Allura/allura/model/repository.py
@@ -101,11 +101,21 @@ class RepositoryImplementation(object):
the repo. Optionally provide a path from which to copy existing hooks.'''
raise NotImplementedError, '_setup_hooks'
- def log(self, object_id, skip, count): # pragma no cover
- '''Return a list of (object_id, ci) beginning at the given commit ID and continuing
- to the parent nodes in a breadth-first traversal. Also return a list of 'next commit' options
- (these are candidates for he next commit after 'count' commits have been
- exhausted).'''
+ def log(self, revs=None, path=None, id_only=True, **kw): # pragma no cover
+ """
+ Returns a generator that returns information about commits reacable
+ by revs.
+
+ revs can be None or a list or tuple of identifiers, each of which
+ can be anything parsable by self.commit(). If revs is None, the
+ default branch head will be used.
+
+ If path is not None, only commits which modify files under path
+ will be included.
+
+ If id_only is True, returns only the commit ID (which can be faster),
+ otherwise it returns detailed information about each commit.
+ """
raise NotImplementedError, 'log'
def compute_tree_new(self, commit, path='/'): # pragma no cover
@@ -358,14 +368,6 @@ class Repository(Artifact, ActivityObject):
"""
return self._impl.tags
- def _log(self, rev, skip, limit):
- head = self.commit(rev)
- if head is None: return
- for _id in self.commitlog([head._id], skip, limit):
- ci = head.query.get(_id=_id)
- ci.set_context(self)
- yield ci
-
def init_as_clone(self, source_path, source_name, source_url):
self.upstream_repo.name = source_name
self.upstream_repo.url = source_url
@@ -376,68 +378,34 @@ class Repository(Artifact, ActivityObject):
g.post_event('repo_cloned', source_url, source_path)
self.refresh(notify=False, new_clone=True)
- def log(self, branch='master', offset=0, limit=10):
- return list(self._log(branch, offset, limit))
-
- def commitlog(self, commit_ids, skip=0, limit=sys.maxint):
- seen = set()
- def _visit(commit_id):
- if commit_id in seen: return
- run = CommitRunDoc.m.get(commit_ids=commit_id)
- if run is None: return
- index = False
- for pos, (oid, time) in enumerate(izip(run.commit_ids, run.commit_times)):
- if oid == commit_id: index = True
- elif not index: continue
- seen.add(oid)
- ci_times[oid] = time
- if pos+1 < len(run.commit_ids):
- ci_parents[oid] = [ run.commit_ids[pos+1] ]
- else:
- ci_parents[oid] = run.parent_commit_ids
- for oid in run.parent_commit_ids:
- if oid not in seen:
- _visit(oid)
-
- def _gen_ids(commit_ids, skip, limit):
- # Traverse the graph in topo order, yielding commit IDs
- commits = set(commit_ids)
- new_parent = None
- while commits and limit:
- # next commit is latest commit that's valid to log
- if new_parent in commits:
- ci = new_parent
- else:
- ci = max(commits, key=lambda ci:ci_times[ci])
- commits.remove(ci)
- # remove this commit from its parents children and add any childless
- # parents to the 'ready set'
- new_parent = None
- for oid in ci_parents.get(ci, []):
- children = ci_children[oid]
- children.discard(ci)
- if not children:
- commits.add(oid)
- new_parent = oid
- if skip:
- skip -= 1
- continue
- else:
- limit -= 1
- yield ci
-
- # Load all the runs to build a commit graph
- ci_times = {}
- ci_parents = {}
- ci_children = defaultdict(set)
- log.info('Build commit graph')
- for cid in commit_ids:
- _visit(cid)
- for oid, parents in ci_parents.iteritems():
- for ci_parent in parents:
- ci_children[ci_parent].add(oid)
-
- return _gen_ids(commit_ids, skip, limit)
+ def log(self, revs=None, path=None, id_only=True, **kw):
+ """
+ Returns a generator that returns information about commits reacable
+ by revs which modify path.
+
+ revs can either be a single revision identifier or a list or tuple
+ of identifiers, each of which can be anything parsable by self.commit().
+ If revs is None, the default branch head will be used.
+
+ If path is not None, then only commits which change files under path
+ will be included.
+
+ If id_only is True, returns only the commit ID (which can be faster),
+ otherwise it returns detailed information about each commit.
+ """
+ if revs is not None and not isinstance(revs, (list, tuple)):
+ revs = [revs]
+ return self._impl.log(revs, path, id_only, **kw)
+
+ def commitlog(self, revs):
+ """
+ Return a generator that returns Commit model instances reachable by
+ the commits specified by revs.
+ """
+ for ci_id in self.log(revs, id_only=True):
+ commit = self.commit(ci_id)
+ commit.set_context(self)
+ yield commit
def count(self, branch='master'):
try:
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0a36f190/Allura/allura/templates/jinja_master/lib.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/jinja_master/lib.html b/Allura/allura/templates/jinja_master/lib.html
index 3897e5f..c9d5c2f 100644
--- a/Allura/allura/templates/jinja_master/lib.html
+++ b/Allura/allura/templates/jinja_master/lib.html
@@ -58,6 +58,13 @@
{% endif %}
{%- endmacro %}
+{% macro user_link(email, name, size=None) -%}
+ {% set user = h.find_user(email) -%}
+ {% if user %}<a href="{{user.url()}}">{% endif -%}
+ {{ email_gravatar(email, name, size) }} {{ name }}
+ {%- if user %}</a>{% endif -%}
+{%- endmacro %}
+
{% macro file_field(name, label) %}
{% if label %}
<label for="{{name}}">{{label}}</label>
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0a36f190/Allura/allura/templates/widgets/repo/log.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/widgets/repo/log.html b/Allura/allura/templates/widgets/repo/log.html
index 61aadab..183dbd7 100644
--- a/Allura/allura/templates/widgets/repo/log.html
+++ b/Allura/allura/templates/widgets/repo/log.html
@@ -16,7 +16,7 @@
specific language governing permissions and limitations
under the License.
-#}
-{% from 'allura:templates/jinja_master/lib.html' import email_gravatar, abbr_date with context %}
+{% from 'allura:templates/jinja_master/lib.html' import user_link, abbr_date with context %}
{% set app = app or c.app %}
<div>
{%if is_file%}
@@ -36,25 +36,15 @@
<td>
<div>
{%if is_file%}
- <div class="grid-1"><input type="checkbox" class="revision" revision="{{commit._id.split(':')[-1]}}" url_commit="{{commit.url()}}"></div>
+ <div class="grid-1"><input type="checkbox" class="revision" revision="{{commit.id}}" url_commit="{{app.repo.url_for_commit(commit.id)}}"></div>
{%endif%}
- <a href="{{app.repo.url_for_commit(commit)}}">{{commit.shorthand_id()}}</a>
- {% if app.repo.symbolics_for_commit(commit)[1] %}
- ({% for tag in app.repo.symbolics_for_commit(commit)[1] -%}
- <a href="{{app.repo.url_for_commit(tag)}}">{{tag}}</a>{% if not loop.last %} {% endif %}
- {%- endfor %})
- {% endif %}
- {%if is_file%}
- ({{commit.tree.get_obj_by_path(request.params.get('path')).size|filesizeformat}})
- {%endif%}
- by
- {{email_gravatar(commit.authored.email, title=commit.authored.name, size=16)}} {{commit.authored.name}}{%if commit.committed.email != commit.authored.email %}, pushed by
- {% if commit.committer_url %}
- <a href="{{commit.committer_url}}">{{email_gravatar(commit.committed.email, title=commit.committed.name, size=16)}}
- {{commit.committed.name}}</a>
- {% else %}
- {{email_gravatar(commit.committed.email, title=commit.committed.name, size=16)}} {{commit.committed.name}}
- {% endif %}
+ <a href="{{app.repo.url_for_commit(commit.id)}}">{{app.repo.shorthand_for_commit(commit.id)}}</a>
+ ({% for ref in commit.refs -%}
+ <a href="{{app.repo.url_for_commit(ref)}}">{{ref}}</a>{% if not loop.last %} {% endif %}
+ {%- endfor %})
+ by {{ user_link(commit.authored.email, commit.authored.name) }}
+ {%- if commit.committed.email != commit.authored.email %},
+ pushed by {{ user_link(commit.committed.email, commit.committed.name) }}
{% endif %}
{{g.markdown.convert(commit.message)}}
</div>
@@ -63,7 +53,7 @@
{% if commit.committed.date %}{{commit.committed.date|datetimeformat}}{% endif %}
</td>
<td style="text-align: left; vertical-align: text-top">
- <a href="{{commit.url()}}tree{{request.params.get('path', '')}}">
+ <a href="{{app.repo.url_for_commit(commit.id)}}tree{{request.params.get('path', '')}}">
{%if is_file%}
View
{% else %}
@@ -72,7 +62,7 @@
</a>
{%if is_file%}
<br/>
- <a href="{{commit.url()}}tree{{request.params.get('path', '')}}?format=raw">Download</a>
+ <a href="{{app.repo.url_for_commit(commit.id)}}tree{{request.params.get('path', '')}}?format=raw">Download</a>
{%endif%}
</td>
</tr>
@@ -80,6 +70,6 @@
</tbody>
</table>
{% if show_paging and next_commit %}
- <a class="page_list" href="{{next_commit.url()}}log{{tg.url(params=request.params)}}">Older ></a>
+ <a class="page_list" href="{{app.repo.url_for_commit(next_commit)}}log{{tg.url(params=request.params)}}">Older ></a>
{% endif %}
</div>
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0a36f190/ForgeGit/forgegit/model/git_repo.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/model/git_repo.py b/ForgeGit/forgegit/model/git_repo.py
index 27b52d9..c1d1da6 100644
--- a/ForgeGit/forgegit/model/git_repo.py
+++ b/ForgeGit/forgegit/model/git_repo.py
@@ -265,23 +265,70 @@ class GitImplementation(M.RepositoryImplementation):
commit = self._git.commit(rev)
return commit.count(path)
- def log(self, object_id, skip, count):
- obj = self._git.commit(object_id)
- candidates = [ obj ]
- result = []
- seen = set()
- while count and candidates:
- candidates.sort(key=lambda c:c.committed_date)
- obj = candidates.pop(-1)
- if obj.hexsha in seen: continue
- seen.add(obj.hexsha)
- if skip == 0:
- result.append(obj.hexsha)
- count -= 1
+ def log(self, revs=None, path=None, id_only=True, **kw):
+ """
+ Returns a generator that returns information about commits reacable
+ by revs.
+
+ revs can be None or a list or tuple of identifiers, each of which
+ can be anything parsable by self.commit(). If revs is None, the
+ default branch head will be used.
+
+ If path is not None, only commits which modify files under path
+ will be included.
+
+ If id_only is True, returns only the commit ID, otherwise it returns
+ detailed information about each commit.
+ """
+ for ci, refs in self._iter_commits_with_refs(revs, '--', path):
+ if id_only:
+ yield ci.hexsha
else:
- skip -= 1
- candidates += obj.parents
- return result, [ p.hexsha for p in candidates ]
+ yield {
+ 'id': ci.hexsha,
+ 'message': h.really_unicode(ci.message or '--none--'),
+ 'authored': {
+ 'name': h.really_unicode(ci.author.name or '--none--'),
+ 'email': h.really_unicode(ci.author.email),
+ 'date': datetime.utcfromtimestamp(ci.authored_date),
+ },
+ 'committed': {
+ 'name': h.really_unicode(ci.committer.name or '--none--'),
+ 'email': h.really_unicode(ci.committer.email),
+ 'date': datetime.utcfromtimestamp(ci.committed_date),
+ },
+ 'refs': refs,
+ 'parents': [pci.hexsha for pci in ci.parents],
+ }
+
+ def _iter_commits_with_refs(self, *args, **kwargs):
+ """
+ A reimplementation of GitPython's iter_commits that includes
+ the --decorate option.
+
+ Unfortunately, iter_commits discards the additional info returned
+ by adding --decorate, and the ref names are not exposed on the
+ commit objects without making an entirely separate call to log.
+
+ Ideally, since we're reimplementing it anyway, we would prefer
+ to add all the info we need to the format to avoid the additional
+ overhead of the lazy-load of the commit data, but the commit
+ message is a problem since it can contain newlines which breaks
+ parsing of the log lines (iter_commits can be broken this way,
+ too). This does keep the id_only case fast and the overhead
+ of lazy-loading the commit data is probably fine. But if this
+ ends up being a bottleneck, that would be one possibile
+ optimization.
+ """
+ proc = self._git.git.log(*args, format='%H%x00%d', as_process=True, **kwargs)
+ stream = proc.stdout
+ while True:
+ line = stream.readline()
+ if not line:
+ break
+ hexsha, decoration = line.strip().split('\x00')
+ refs = decoration.strip(' ()').split(', ') if decoration else []
+ yield (git.Commit(self._git, gitdb.util.hex_to_bin(hexsha)), refs)
def open_blob(self, blob):
return _OpenedGitBlob(
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0a36f190/ForgeSVN/forgesvn/model/svn.py
----------------------------------------------------------------------
diff --git a/ForgeSVN/forgesvn/model/svn.py b/ForgeSVN/forgesvn/model/svn.py
index 23c1887..b9a9f0f 100644
--- a/ForgeSVN/forgesvn/model/svn.py
+++ b/ForgeSVN/forgesvn/model/svn.py
@@ -81,21 +81,6 @@ class Repository(M.Repository):
# contains the revision, just parse it out from there
return int(self._impl._revno(ci._id))
- def log(self, branch='HEAD', offset=0, limit=10):
- return list(self._log(branch, offset, limit))
-
- def commitlog(self, commit_ids, skip=0, limit=sys.maxint):
- ci_id = commit_ids[0]
- if skip > 0:
- rid, rev = ci_id.split(':')
- rev = int(rev) - skip
- ci_id = '%s:%s' % (rid, rev)
- ci = self._impl.commit(ci_id)
- while ci is not None and limit > 0:
- yield ci._id
- limit -= 1
- ci = ci.get_parent()
-
def latest(self, branch=None):
if self._impl is None: return None
return self._impl.commit('HEAD')
@@ -186,13 +171,13 @@ class SVNImplementation(M.RepositoryImplementation):
return 'file://%s%s' % (self._repo.fs_path, self._repo.name)
def shorthand_for_commit(self, oid):
- return '[r%d]' % self._revno(oid)
+ return '[r%d]' % self._revno(self.rev_parse(oid))
def url_for_commit(self, commit, url_type=None):
- if isinstance(commit, basestring):
- object_id = commit
- else:
+ if isinstance(commit, M.repo.Commit):
object_id = commit._id
+ else:
+ object_id = self.rev_parse(commit)
if ':' in object_id:
object_id = str(self._revno(object_id))
return os.path.join(self._repo.url(), object_id) + '/'
@@ -299,17 +284,20 @@ class SVNImplementation(M.RepositoryImplementation):
self._setup_special_files(source_url)
def commit(self, rev):
- if rev in ('HEAD', None):
- oid = self._oid(self.head)
- elif isinstance(rev, int) or rev.isdigit():
- oid = self._oid(rev)
- else:
- oid = rev
+ oid = self.rev_parse(rev)
result = M.repo.Commit.query.get(_id=oid)
if result:
result.set_context(self._repo)
return result
+ def rev_parse(self, rev):
+ if rev in ('HEAD', None):
+ return self._oid(self.head)
+ elif isinstance(rev, int) or rev.isdigit():
+ return self._oid(rev)
+ else:
+ return rev
+
def all_commit_ids(self):
"""Return a list of commit ids, starting with the head (most recent
commit) and ending with the root (first commit).
@@ -494,20 +482,60 @@ class SVNImplementation(M.RepositoryImplementation):
else:
return self._blob_oid(commit_id, path)
- def log(self, object_id, skip, count):
- revno = self._revno(object_id)
- result = []
- while count and revno:
- if skip == 0:
- result.append(self._oid(revno))
- count -= 1
- else:
- skip -= 1
- revno -= 1
- if revno:
- return result, [ self._oid(revno) ]
+ def log(self, revs=None, path=None, id_only=True, page_size=25):
+ """
+ Returns a generator that returns information about commits reacable
+ by revs.
+
+ revs can be None or a list or tuple of identifiers, each of which
+ can be anything parsable by self.commit(). If revs is None, the
+ default head will be used.
+
+ If path is not None, only commits which modify files under path
+ will be included.
+
+ If id_only is True, returns only the commit ID, otherwise it returns
+ detailed information about each commit.
+
+ Since pysvn doesn't have a generator version of log, this tries to
+ balance pulling too much data from SVN with calling SVN too many
+ times by pulling in pages of page_size at a time.
+ """
+ if revs is None:
+ revno = self.head
+ else:
+ revno = max([self._revno(self.rev_parse(r)) for r in revs])
+ if path is None:
+ url = self._url
else:
- return result, []
+ url = '/'.join([self._url, path])
+ while revno > 0:
+ rev = pysvn.Revision(pysvn.opt_revision_kind.number, revno)
+ for ci in self._svn.log(url, revision_start=rev, limit=page_size):
+ if id_only:
+ yield ci.revision.number
+ else:
+ yield self._map_log(ci)
+ revno = ci.revision.number - 1
+
+ def _map_log(self, ci):
+ revno = ci.revision.number
+ return {
+ 'id': revno,
+ 'message': h.really_unicode(ci.get('message', '--none--')),
+ 'authored': {
+ 'name': h.really_unicode(ci.get('author', '--none--')),
+ 'email': '',
+ 'date': datetime.utcfromtimestamp(ci.date),
+ },
+ 'committed': {
+ 'name': h.really_unicode(ci.get('author', '--none--')),
+ 'email': '',
+ 'date': datetime.utcfromtimestamp(ci.date),
+ },
+ 'refs': ['HEAD'] if revno == self.head else [],
+ 'parents': [revno-1] if revno > 1 else [],
+ }
def open_blob(self, blob):
data = self._svn.cat(