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
16 """Cache of the Subversion history log."""
18 from bzrlib import urlutils
19 from bzrlib.errors import NoSuchRevision
20 import bzrlib.ui as ui
23 from svn.core import SubversionException, Pool
24 from transport import SvnRaTransport
27 from cache import sqlite3
29 LOG_CHUNK_LIMIT = 1000
31 class LogWalker(object):
32 """Easy way to access the history of a Subversion repository."""
33 def __init__(self, transport, cache_db=None, limit=None):
34 """Create a new instance.
36 :param transport: SvnRaTransport to use to access the repository.
37 :param cache_db: Optional sql database connection to use. Doesn't
40 assert isinstance(transport, SvnRaTransport)
42 self.url = transport.base
43 self._transport = None
48 self._limit = LOG_CHUNK_LIMIT
51 self.db = sqlite3.connect(":memory:")
55 self.db.executescript("""
56 create table if not exists changed_path(rev integer, action text, path text, copyfrom_path text, copyfrom_rev integer);
57 create index if not exists path_rev on changed_path(rev);
58 create unique index if not exists path_rev_path on changed_path(rev, path);
59 create unique index if not exists path_rev_path_action on changed_path(rev, path, action);
62 self.saved_revnum = self.db.execute("SELECT MAX(rev) FROM changed_path").fetchone()[0]
63 if self.saved_revnum is None:
66 def _get_transport(self):
67 if self._transport is not None:
68 return self._transport
69 self._transport = SvnRaTransport(self.url)
70 return self._transport
72 def fetch_revisions(self, to_revnum=None):
73 """Fetch information about all revisions in the remote repository
76 :param to_revnum: End of range to fetch information for
78 if to_revnum <= self.saved_revnum:
80 latest_revnum = self._get_transport().get_latest_revnum()
81 to_revnum = max(latest_revnum, to_revnum)
83 pb = ui.ui_factory.nested_progress_bar()
85 def rcvr(log_entry, pool):
86 pb.update('fetching svn revision info', log_entry.revision, to_revnum)
87 orig_paths = log_entry.changed_paths
88 if orig_paths is None:
91 copyfrom_path = orig_paths[p].copyfrom_path
92 if copyfrom_path is not None:
93 copyfrom_path = copyfrom_path.strip("/")
96 "replace into changed_path (rev, path, action, copyfrom_path, copyfrom_rev) values (?, ?, ?, ?, ?)",
97 (log_entry.revision, p.strip("/"), orig_paths[p].action, copyfrom_path, orig_paths[p].copyfrom_rev))
98 # Work around nasty memory leak in Subversion
99 orig_paths[p]._parent_pool.destroy()
101 self.saved_revnum = log_entry.revision
102 if self.saved_revnum % 1000 == 0:
107 while self.saved_revnum < to_revnum:
109 self._get_transport().get_log("", self.saved_revnum,
110 to_revnum, self._limit, True,
111 True, [], rcvr, pool)
115 except SubversionException, (_, num):
116 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
117 raise NoSuchRevision(branch=self,
118 revision="Revision number %d" % to_revnum)
122 def follow_path(self, path, revnum):
123 """Return iterator over all the revisions between revnum and 0 named path or inside path.
125 :param path: Branch path to start reporting (in revnum)
126 :param revnum: Start revision.
127 :return: An iterator that yields tuples with (path, paths, revnum)
128 where paths is a dictionary with all changes that happened in path
133 if revnum == 0 and path == "":
136 recurse = (path != "")
138 path = path.strip("/")
141 assert revnum > 0 or path == ""
142 revpaths = self.get_revision_paths(revnum, path, recurse=recurse)
145 yield (path, copy(revpaths), revnum)
151 if revpaths.has_key(path):
152 if revpaths[path][1] is None:
153 if revpaths[path][0] in ('A', 'R'):
154 # this path didn't exist before this revision
157 # In this revision, this path was copied from
159 revnum = revpaths[path][2]
160 path = revpaths[path][1]
161 assert path == "" or revnum > 0
164 for p in sorted(revpaths.keys()):
165 if path.startswith(p+"/") and revpaths[p][0] in ('A', 'R'):
166 assert revpaths[p][1]
167 path = path.replace(p, revpaths[p][1])
168 revnum = revpaths[p][2]
171 def get_revision_paths(self, revnum, path=None, recurse=False):
172 """Obtain dictionary with all the changes in a particular revision.
174 :param revnum: Subversion revision number
175 :param path: optional path under which to return all entries
176 :param recurse: Report changes to parents as well
177 :returns: dictionary with paths as keys and
178 (action, copyfrom_path, copyfrom_rev) as values.
182 assert path is None or path == ""
183 return {'': ('A', None, -1)}
185 self.fetch_revisions(revnum)
187 query = "select path, action, copyfrom_path, copyfrom_rev from changed_path where rev="+str(revnum)
188 if path is not None and path != "":
189 query += " and (path='%s' or path like '%s/%%'" % (path, path)
191 query += " or ('%s' LIKE path || '/%%')" % path
195 for p, act, cf, cr in self.db.execute(query):
196 paths[p.encode("utf-8")] = (act, cf, cr)
199 def find_latest_change(self, path, revnum, include_parents=False,
200 include_children=False):
201 """Find latest revision that touched path.
203 :param path: Path to check for changes
204 :param revnum: First revision to check
206 assert isinstance(path, basestring)
207 assert isinstance(revnum, int) and revnum >= 0
208 self.fetch_revisions(revnum)
213 extra += " OR path LIKE '%'"
215 extra += " OR path LIKE '%s/%%'" % path.strip("/")
217 extra += " OR ('%s' LIKE (path || '/%%') AND (action = 'R' OR action = 'A'))" % path.strip("/")
218 query = "SELECT rev FROM changed_path WHERE (path='%s'%s) AND rev <= %d ORDER BY rev DESC LIMIT 1" % (path.strip("/"), extra, revnum)
220 row = self.db.execute(query).fetchone()
221 if row is None and path == "":
229 def touches_path(self, path, revnum):
230 """Check whether path was changed in specified revision.
232 :param path: Path to check
233 :param revnum: Revision to check
235 self.fetch_revisions(revnum)
238 return (self.db.execute("select 1 from changed_path where path='%s' and rev=%d" % (path, revnum)).fetchone() is not None)
240 def find_children(self, path, revnum):
241 """Find all children of path in revnum.
243 :param path: Path to check
244 :param revnum: Revision to check
246 path = path.strip("/")
247 transport = self._get_transport()
248 ft = transport.check_path(path, revnum)
249 if ft == svn.core.svn_node_file:
251 assert ft == svn.core.svn_node_dir
253 class TreeLister(svn.delta.Editor):
254 def __init__(self, base):
258 def set_target_revision(self, revnum):
259 """See Editor.set_target_revision()."""
262 def open_root(self, revnum, baton):
263 """See Editor.open_root()."""
266 def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revnum, pool):
267 """See Editor.add_directory()."""
268 self.files.append(urlutils.join(self.base, path))
271 def change_dir_prop(self, id, name, value, pool):
274 def change_file_prop(self, id, name, value, pool):
277 def add_file(self, path, parent_id, copyfrom_path, copyfrom_revnum, baton):
278 self.files.append(urlutils.join(self.base, path))
281 def close_dir(self, id):
284 def close_file(self, path, checksum):
287 def close_edit(self):
290 def abort_edit(self):
293 def apply_textdelta(self, file_id, base_checksum):
296 editor = TreeLister(path)
297 old_base = transport.base
299 root_repos = transport.get_svn_repos_root()
300 transport.reparent(urlutils.join(root_repos, path))
301 reporter = transport.do_update(revnum, True, editor, pool)
302 reporter.set_path("", revnum, True, None, pool)
303 reporter.finish_report(pool)
305 transport.reparent(old_base)
308 def get_previous(self, path, revnum):
309 """Return path,revnum pair specified pair was derived from.
311 :param path: Path to check
312 :param revnum: Revision to check
315 self.fetch_revisions(revnum)
318 row = self.db.execute("select action, copyfrom_path, copyfrom_rev from changed_path where path='%s' and rev=%d" % (path, revnum)).fetchone()
322 return (path, revnum-1)
323 return (row[1], row[2])