Fix first couple of tests.
[jelmer/subvertpy.git] / logwalker.py
1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
2
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.
7
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.
12
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
17 from bzrlib.errors import NoSuchRevision, BzrError, NotBranchError
18 from bzrlib.progress import ProgressBar, DummyProgress
19 from bzrlib.trace import mutter
20
21 import os
22
23 from svn.core import SubversionException
24 from transport import SvnRaTransport
25 import svn.core
26
27 from bsddb import dbshelve as shelve
28
29 shelves = {}
30
31 class NotSvnBranchPath(BzrError):
32     _fmt = """{%(branch_path)s}:%(revnum)s is not a valid Svn branch path"""
33
34     def __init__(self, branch_path, revnum=None):
35         BzrError.__init__(self)
36         self.branch_path = branch_path
37         self.revnum = revnum
38
39
40 class LogWalker(object):
41     def __init__(self, scheme, transport=None, cache_dir=None, last_revnum=None, repos_url=None, pb=None):
42         if transport is None:
43             transport = SvnRaTransport(repos_url)
44
45         if last_revnum is None:
46             last_revnum = transport.get_latest_revnum()
47
48         self.transport = transport
49         self.scheme = scheme
50
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]
56         else:
57             self.revisions = {}
58         self.saved_revnum = max(len(self.revisions)-1, 0)
59
60         if self.saved_revnum < last_revnum:
61             self.fetch_revisions(self.saved_revnum, last_revnum, pb)
62         else:
63             self.last_revnum = self.saved_revnum
64
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)
68             paths = {}
69             if orig_paths is None:
70                 orig_paths = {}
71             for p in orig_paths:
72                 copyfrom_path = orig_paths[p].copyfrom_path
73                 if copyfrom_path:
74                     copyfrom_path = copyfrom_path.strip("/")
75                 paths[p.strip("/")] = (orig_paths[p].action,
76                             copyfrom_path, orig_paths[p].copyfrom_rev)
77
78             self.revisions[str(rev)] = {
79                     'paths': paths,
80                     'author': author,
81                     'date': date,
82                     'message': message
83                     }
84
85         # Don't bother for only a few revisions
86         if abs(self.saved_revnum-to_revnum) < 10:
87             pb = DummyProgress()
88         else:
89             pb = ProgressBar()
90
91         try:
92             try:
93                 mutter('getting log %r:%r' % (self.saved_revnum, to_revnum))
94                 self.transport.get_log(["/"], self.saved_revnum, to_revnum, 
95                                0, True, True, rcvr)
96                 self.last_revnum = to_revnum
97             finally:
98                 pb.clear()
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)
103             raise
104
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."""
108         assert revnum >= 0
109
110         if not branch_path is None and not self.scheme.is_branch(branch_path):
111             raise NotSvnBranchPath(branch_path, revnum)
112
113         if branch_path:
114             branch_path = branch_path.strip("/")
115
116         if revnum > self.last_revnum:
117             self.fetch_revisions(self.last_revnum, revnum)
118
119         continue_revnum = None
120         for i in range(revnum+1):
121             i = revnum - i
122
123             if i == 0:
124                 continue
125
126             if not (continue_revnum is None or continue_revnum == i):
127                 continue
128
129             continue_revnum = None
130
131             rev = self.revisions[str(i)]
132             changed_paths = {}
133             for p in rev['paths']:
134                 if (branch_path is None or 
135                     p == branch_path or
136                     branch_path == "" or
137                     p.startswith(branch_path+"/")):
138
139                     try:
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:
145                         pass
146
147             assert branch_path is None or len(changed_paths) <= 1
148
149             for bp in changed_paths:
150                 yield (bp, changed_paths[bp], i)
151
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 
156                 # somewhere else
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]
160
161     def find_branches(self, revnum):
162         created_branches = {}
163
164         for i in range(revnum):
165             if i == 0:
166                 continue
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]
172                         yield (p, i, False)
173
174                     if rev['paths'][p][0] in ('A', 'R'): 
175                         created_branches[p] = i
176
177         for p in created_branches:
178             yield (p, i, True)
179
180     def get_revision_info(self, revnum, pb=None):
181         """Obtain basic information for a specific revision.
182
183         :param revnum: Revision number.
184         :returns: Tuple with author, log message and date of the revision.
185         """
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:
190             author = None
191         else:
192             author = rev['author'].decode('utf-8', 'ignore')
193         return (author, rev['message'].decode('utf-8', 'ignore'), 
194                 rev['date'], rev['paths'])
195
196     
197     def find_latest_change(self, path, revnum):
198         while revnum > 0 and not self.touches_path(path, revnum):
199             revnum = revnum - 1
200         return revnum
201
202     def touches_path(self, path, revnum):
203         return (path in self.revisions[str(revnum)]['paths'])
204
205     def find_children(self, path, revnum):
206         # TODO: Find children by walking history, or use 
207         # cache?
208         mutter("svn ls -r %d '%r'" % (revnum, path))
209
210         try:
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:
215                 return
216             raise
217
218         for p in dirents:
219             yield os.path.join(path, p)
220             for c in self.find_children(os.path.join(path, p), revnum):
221                 yield c