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 if message is not None:
215 message = _escape_commit_message(base64.b64decode(message))
216 return (author, message, date)
218 def find_latest_change(self, path, revnum, recurse=False):
219 """Find latest revision that touched path.
221 :param path: Path to check for changes
222 :param revnum: First revision to check
224 assert isinstance(path, basestring)
225 assert isinstance(revnum, int) and revnum >= 0
226 if revnum > self.saved_revnum:
227 self.fetch_revisions(revnum)
230 extra = " or path like '%s/%%'" % path.strip("/")
233 query = "select rev from changed_path where (path='%s' or ('%s' like (path || '/%%') and (action = 'R' or action = 'A'))%s) and rev <= %d order by rev desc limit 1" % (path.strip("/"), path.strip("/"), extra, revnum)
235 row = self.db.execute(query).fetchone()
236 if row is None and path == "":
244 def touches_path(self, path, revnum):
245 """Check whether path was changed in specified revision.
247 :param path: Path to check
248 :param revnum: Revision to check
250 if revnum > self.saved_revnum:
251 self.fetch_revisions(revnum)
254 return (self.db.execute("select 1 from changed_path where path='%s' and rev=%d" % (path, revnum)).fetchone() is not None)
256 def find_children(self, path, revnum):
257 """Find all children of path in revnum."""
258 path = path.strip("/")
259 if self.transport.check_path(path, revnum) == svn.core.svn_node_file:
261 class TreeLister(svn.delta.Editor):
262 def __init__(self, base):
266 def set_target_revision(self, revnum):
269 def open_root(self, revnum, baton):
272 def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revnum, pool):
273 self.files.append(os.path.join(self.base, path))
276 def change_dir_prop(self, id, name, value, pool):
279 def change_file_prop(self, id, name, value, pool):
282 def add_file(self, path, parent_id, copyfrom_path, copyfrom_revnum, baton):
283 self.files.append(os.path.join(self.base, path))
286 def close_dir(self, id):
289 def close_file(self, path, checksum):
292 def close_edit(self):
295 def abort_edit(self):
298 def apply_textdelta(self, file_id, base_checksum):
301 editor = TreeLister(path)
302 edit, baton = svn.delta.make_editor(editor, pool)
303 root_repos = self.transport.get_repos_root()
304 self.transport.reparent(os.path.join(root_repos, path))
305 reporter = self.transport.do_update(
306 revnum, "", True, edit, baton, pool)
307 reporter.set_path("", revnum, True, None, pool)
308 reporter.finish_report(pool)
311 def get_previous(self, path, revnum):
312 """Return path,revnum pair specified pair was derived from.
314 :param path: Path to check
315 :param revnum: Revision to check
318 if revnum > self.saved_revnum:
319 self.fetch_revisions(revnum)
322 row = self.db.execute("select action, copyfrom_path, copyfrom_rev from changed_path where path='%s' and rev=%d" % (path, revnum)).fetchone()
326 return (path, revnum-1)
327 return (row[1], row[2])