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 ProgressBar, DummyProgress
19 from bzrlib.trace import mutter
23 from svn.core import SubversionException
24 from transport import SvnRaTransport
27 from bsddb import dbshelve as shelve
31 class NotSvnBranchPath(BzrError):
32 _fmt = """{%(branch_path)s}:%(revnum)s is not a valid Svn branch path"""
34 def __init__(self, branch_path, revnum=None):
35 BzrError.__init__(self)
36 self.branch_path = branch_path
40 class LogWalker(object):
41 def __init__(self, scheme, transport=None, cache_dir=None, last_revnum=None, repos_url=None, pb=None):
43 transport = SvnRaTransport(repos_url)
45 if last_revnum is None:
46 last_revnum = transport.get_latest_revnum()
48 self.transport = transport
51 if not cache_dir is None:
52 cache_file = os.path.join(cache_dir, 'log-v2')
53 if not shelves.has_key(cache_file):
54 shelves[cache_file] = shelve.open(cache_file)
55 self.revisions = shelves[cache_file]
58 self.saved_revnum = max(len(self.revisions)-1, 0)
60 if self.saved_revnum < last_revnum:
61 self.fetch_revisions(self.saved_revnum, last_revnum, pb)
63 self.last_revnum = self.saved_revnum
65 def fetch_revisions(self, from_revnum, to_revnum, pb=None):
66 def rcvr(orig_paths, rev, author, date, message, pool):
67 pb.update('fetching svn revision info', rev, to_revnum)
69 if orig_paths is None:
72 copyfrom_path = orig_paths[p].copyfrom_path
74 copyfrom_path = copyfrom_path.strip("/")
75 paths[p.strip("/")] = (orig_paths[p].action,
76 copyfrom_path, orig_paths[p].copyfrom_rev)
78 self.revisions[str(rev)] = {
85 # Don't bother for only a few revisions
86 if abs(self.saved_revnum-to_revnum) < 10:
93 mutter('getting log %r:%r' % (self.saved_revnum, to_revnum))
94 self.transport.get_log(["/"], self.saved_revnum, to_revnum,
96 self.last_revnum = to_revnum
99 except SubversionException, (_, num):
100 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
101 raise NoSuchRevision(branch=self,
102 revision="Revision number %d" % to_revnum)
105 def follow_history(self, branch_path, revnum):
106 """Return iterator over all the revisions between from_revnum and
107 to_revnum that touch branch_path."""
110 if not branch_path is None and not self.scheme.is_branch(branch_path):
111 raise NotSvnBranchPath(branch_path, revnum)
114 branch_path = branch_path.strip("/")
116 if revnum > self.last_revnum:
117 self.fetch_revisions(self.last_revnum, revnum)
119 continue_revnum = None
120 for i in range(revnum+1):
126 if not (continue_revnum is None or continue_revnum == i):
129 continue_revnum = None
131 rev = self.revisions[str(i)]
133 for p in rev['paths']:
134 if (branch_path is None or
137 p.startswith(branch_path+"/")):
140 (bp, rp) = self.scheme.unprefix(p)
141 if not changed_paths.has_key(bp):
142 changed_paths[bp] = {}
143 changed_paths[bp][p] = rev['paths'][p]
144 except NotBranchError:
147 assert branch_path is None or len(changed_paths) <= 1
149 for bp in changed_paths:
150 yield (bp, changed_paths[bp], i)
152 if (not branch_path is None and
153 branch_path in rev['paths'] and
154 not rev['paths'][branch_path][1] is None):
155 # In this revision, this branch was copied from
157 # FIXME: What if copyfrom_path is not a branch path?
158 continue_revnum = rev['paths'][branch_path][2]
159 branch_path = rev['paths'][branch_path][1]
161 def find_branches(self, revnum):
162 created_branches = {}
164 for i in range(revnum):
167 rev = self.revisions[str(i)]
168 for p in rev['paths']:
169 if self.scheme.is_branch(p):
170 if rev['paths'][p][0] in ('R', 'D'):
171 del created_branches[p]
174 if rev['paths'][p][0] in ('A', 'R'):
175 created_branches[p] = i
177 for p in created_branches:
180 def get_revision_info(self, revnum, pb=None):
181 """Obtain basic information for a specific revision.
183 :param revnum: Revision number.
184 :returns: Tuple with author, log message and date of the revision.
186 if revnum > self.last_revnum:
187 self.fetch_revisions(self.saved_revnum, revnum, pb)
188 rev = self.revisions[str(revnum)]
189 if rev['author'] is None:
192 author = rev['author'].decode('utf-8', 'ignore')
193 return (author, rev['message'].decode('utf-8', 'ignore'),
194 rev['date'], rev['paths'])
197 def find_latest_change(self, path, revnum):
198 while revnum > 0 and not self.touches_path(path, revnum):
202 def touches_path(self, path, revnum):
203 return (path in self.revisions[str(revnum)]['paths'])
205 def find_children(self, path, revnum):
206 # TODO: Find children by walking history, or use
208 mutter("svn ls -r %d '%r'" % (revnum, path))
211 (dirents, _, _) = self.transport.get_dir(
212 path.encode('utf8'), revnum)
213 except SubversionException, (_, num):
214 if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
219 yield os.path.join(path, p)
220 for c in self.find_children(os.path.join(path, p), revnum):