1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 from bzrlib.errors import NoSuchRevision, BzrError, NotBranchError
18 from bzrlib.progress import DummyProgress
19 from bzrlib.trace import mutter
20 from bzrlib.ui import ui_factory
24 from svn.core import SubversionException, Pool
25 from transport import SvnRaTransport
33 from pysqlite2 import dbapi2 as sqlite3
37 def _escape_commit_message(message):
38 """Replace xml-incompatible control characters."""
42 # FIXME: RBC 20060419 this should be done by the revision
43 # serialiser not by commit. Then we can also add an unescaper
44 # in the deserializer and start roundtripping revision messages
45 # precisely. See repository_implementations/test_repository.py
47 # Python strings can include characters that can't be
48 # represented in well-formed XML; escape characters that
49 # aren't listed in the XML specification
50 # (http://www.w3.org/TR/REC-xml/#NT-Char).
52 u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
53 lambda match: match.group(0).encode('unicode_escape'),
58 class LogWalker(object):
59 """Easy way to access the history of a Subversion repository."""
60 def __init__(self, transport=None, cache_db=None, last_revnum=None):
61 """Create a new instance.
63 :param transport: SvnRaTransport to use to access the repository.
64 :param cache_db: Optional sql database connection to use. Doesn't
66 :param last_revnum: Last known revnum in the repository. Will be
67 determined if not specified.
69 assert isinstance(transport, SvnRaTransport)
71 if last_revnum is None:
72 last_revnum = transport.get_latest_revnum()
74 self.last_revnum = last_revnum
76 self.transport = SvnRaTransport(transport.base)
79 self.db = sqlite3.connect(":memory:")
83 self.db.executescript("""
84 create table if not exists revision(revno integer unique, author text, message text, date text);
85 create unique index if not exists revision_revno on revision (revno);
86 create table if not exists changed_path(rev integer, action text, path text, copyfrom_path text, copyfrom_rev integer);
87 create index if not exists path_rev on changed_path(rev);
88 create index if not exists path_rev_path on changed_path(rev, path);
91 self.saved_revnum = self.db.execute("SELECT MAX(revno) FROM revision").fetchone()[0]
92 if self.saved_revnum is None:
95 def fetch_revisions(self, to_revnum):
96 """Fetch information about all revisions in the remote repository
99 :param to_revnum: End of range to fetch information for
101 to_revnum = max(self.last_revnum, to_revnum)
103 pb = ui_factory.nested_progress_bar()
105 def rcvr(orig_paths, rev, author, date, message, pool):
106 pb.update('fetching svn revision info', rev, to_revnum)
108 if orig_paths is None:
111 copyfrom_path = orig_paths[p].copyfrom_path
113 copyfrom_path = copyfrom_path.strip("/")
116 "insert into changed_path (rev, path, action, copyfrom_path, copyfrom_rev) values (?, ?, ?, ?, ?)",
117 (rev, p.strip("/"), orig_paths[p].action, copyfrom_path, orig_paths[p].copyfrom_rev))
119 if message is not None:
120 message = base64.b64encode(message)
122 self.db.execute("replace into revision (revno, author, date, message) values (?,?,?,?)", (rev, author, date, message))
124 self.saved_revnum = rev
125 if self.saved_revnum % 1000 == 0:
131 self.transport.get_log("/", self.saved_revnum, to_revnum,
132 0, True, True, rcvr, pool)
135 except SubversionException, (_, num):
136 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
137 raise NoSuchRevision(branch=self,
138 revision="Revision number %d" % to_revnum)
143 def follow_path(self, path, revnum):
144 """Return iterator over all the revisions between revnum and
145 0 named path or inside path.
147 :param path: Branch path to start reporting (in revnum)
148 :param revnum: Start revision.
150 :return: An iterators that yields tuples with (path, paths, revnum)
151 where paths is a dictionary with all changes that happened in path
156 if revnum == 0 and path == "":
159 path = path.strip("/")
162 revpaths = self.get_revision_paths(revnum, path)
165 yield (path, revpaths, revnum)
167 if revpaths.has_key(path):
168 if revpaths[path][1] is None:
169 if revpaths[path][0] in ('A', 'R'):
170 # this path didn't exist before this revision
173 # In this revision, this path was copied from
175 revnum = revpaths[path][2]
176 path = revpaths[path][1]
180 def get_revision_paths(self, revnum, path=None):
181 """Obtain dictionary with all the changes in a particular revision.
183 :param revnum: Subversion revision number
184 :param path: optional path under which to return all entries
185 :returns: dictionary with paths as keys and
186 (action, copyfrom_path, copyfrom_rev) as values.
190 return {'': ('A', None, -1)}
192 if revnum > self.saved_revnum:
193 self.fetch_revisions(revnum)
195 query = "select path, action, copyfrom_path, copyfrom_rev from changed_path where rev="+str(revnum)
196 if path is not None and path != "":
197 query += " and (path='%s' or path like '%s/%%')" % (path, path)
200 for p, act, cf, cr in self.db.execute(query):
201 paths[p] = (act, cf, cr)
204 def get_revision_info(self, revnum):
205 """Obtain basic information for a specific revision.
207 :param revnum: Revision number.
208 :returns: Tuple with author, log message and date of the revision.
211 if revnum > self.saved_revnum:
212 self.fetch_revisions(revnum)
213 (author, message, date) = self.db.execute("select author, message, date from revision where revno="+ str(revnum)).fetchone()
214 return (author, _escape_commit_message(base64.b64decode(message)), date)
216 def find_latest_change(self, path, revnum, recurse=False):
217 """Find latest revision that touched path.
219 :param path: Path to check for changes
220 :param revnum: First revision to check
222 assert isinstance(path, basestring)
223 assert isinstance(revnum, int) and revnum >= 0
224 if revnum > self.saved_revnum:
225 self.fetch_revisions(revnum)
228 extra = " or path like '%s/%%'" % path.strip("/")
231 query = "select rev from changed_path where (path='%s'%s) and rev <= %d order by rev desc limit 1" % (path.strip("/"), extra, revnum)
233 row = self.db.execute(query).fetchone()
234 if row is None and path == "":
242 def touches_path(self, path, revnum):
243 """Check whether path was changed in specified revision.
245 :param path: Path to check
246 :param revnum: Revision to check
248 if revnum > self.saved_revnum:
249 self.fetch_revisions(revnum)
252 return (self.db.execute("select 1 from changed_path where path='%s' and rev=%d" % (path, revnum)).fetchone() is not None)
254 def find_children(self, path, revnum):
255 """Find all children of path in revnum."""
256 path = path.strip("/")
257 if self.transport.check_path(path, revnum) == svn.core.svn_node_file:
259 class TreeLister(svn.delta.Editor):
260 def __init__(self, base):
264 def set_target_revision(self, revnum):
267 def open_root(self, revnum, baton):
270 def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revnum, pool):
271 self.files.append(os.path.join(self.base, path))
274 def change_dir_prop(self, id, name, value, pool):
277 def change_file_prop(self, id, name, value, pool):
280 def add_file(self, path, parent_id, copyfrom_path, copyfrom_revnum, baton):
281 self.files.append(os.path.join(self.base, path))
284 def close_dir(self, id):
287 def close_file(self, path, checksum):
290 def close_edit(self):
293 def abort_edit(self):
296 def apply_textdelta(self, file_id, base_checksum):
299 editor = TreeLister(path)
300 edit, baton = svn.delta.make_editor(editor, pool)
301 root_repos = self.transport.get_repos_root()
302 self.transport.reparent(os.path.join(root_repos, path))
303 reporter = self.transport.do_update(
304 revnum, "", True, edit, baton, pool)
305 reporter.set_path("", revnum, True, None, pool)
306 reporter.finish_report(pool)
309 def get_previous(self, path, revnum):
310 """Return path,revnum pair specified pair was derived from.
312 :param path: Path to check
313 :param revnum: Revision to check
316 if revnum > self.saved_revnum:
317 self.fetch_revisions(revnum)
320 row = self.db.execute("select action, copyfrom_path, copyfrom_rev from changed_path where path='%s' and rev=%d" % (path, revnum)).fetchone()
324 return (path, revnum-1)
325 return (row[1], row[2])