Fix bug in revid caching.
[jelmer/subvertpy.git] / fileids.py
1 # Copyright (C) 2006-2007 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 """Generation of file-ids."""
17
18 from bzrlib.errors import NotBranchError
19 from bzrlib.revision import NULL_REVISION
20 from bzrlib.trace import mutter
21 import bzrlib.ui as ui
22
23 import sha
24
25 from revids import escape_svn_path
26
27 def generate_svn_file_id(uuid, revnum, branch, path):
28     """Create a file id identifying a Subversion file.
29
30     :param uuid: UUID of the repository
31     :param revnu: Revision number at which the file was introduced.
32     :param branch: Branch path of the branch in which the file was introduced.
33     :param path: Original path of the file within the branch
34     """
35     ret = "%d@%s:%s:%s" % (revnum, uuid, escape_svn_path(branch), escape_svn_path(path))
36     if len(ret) > 150:
37         ret = "%d@%s:%s;%s" % (revnum, uuid, 
38                             escape_svn_path(branch),
39                             sha.new(path).hexdigest())
40     assert isinstance(ret, str)
41     return ret
42
43
44 def generate_file_id(repos, revid, path):
45     (branch, revnum) = repos.lookup_revision_id(revid)
46     return generate_svn_file_id(repos.uuid, revnum, branch, path)
47
48
49 def get_local_changes(paths, scheme, generate_revid, get_children=None):
50     new_paths = {}
51     names = paths.keys()
52     names.sort()
53     for p in names:
54         data = paths[p]
55         new_p = scheme.unprefix(p)[1]
56         if data[1] is not None:
57             try:
58                 (cbp, crp) = scheme.unprefix(data[1])
59
60                 # Branch copy
61                 if (crp == "" and new_p == ""):
62                     data = ('M', None, None)
63                 else:
64                     data = (data[0], crp, generate_revid(data[2], cbp))
65             except NotBranchError:
66                 # Copied from outside of a known branch
67                 # Make it look like the files were added in this revision
68                 if get_children is not None:
69                     for c in get_children(data[1], data[2]):
70                         mutter('oops: %r child %r' % (data[1], c))
71                         new_paths[(new_p+"/"+c[len(data[1]):].strip("/")).strip("/")] = (data[0], None, -1)
72                 data = (data[0], None, -1)
73
74         new_paths[new_p] = data
75     return new_paths
76
77
78 class FileIdMap(object):
79     """ File id store. 
80
81     Keeps a map
82
83     revnum -> branch -> path -> fileid
84     """
85     def __init__(self, repos, cache_db):
86         self.repos = repos
87         self.cachedb = cache_db
88         self.cachedb.executescript("""
89         create table if not exists filemap (filename text, id integer, create_revid text, revid text);
90         create index if not exists revid on filemap(revid);
91         """)
92         self.cachedb.commit()
93
94     def save(self, revid, parent_revids, _map):
95         mutter('saving file id map for %r' % revid)
96         for filename in _map:
97             self.cachedb.execute("insert into filemap (filename, id, create_revid, revid) values(?,?,?,?)", (filename, _map[filename][0], _map[filename][1], revid))
98         self.cachedb.commit()
99
100     def load(self, revid):
101         map = {}
102         for filename, create_revid, id in self.cachedb.execute("select filename, create_revid, id from filemap where revid='%s'"%revid):
103             map[filename] = (id.encode("utf-8"), create_revid.encode("utf-8"))
104             assert isinstance(map[filename][0], str)
105
106         return map
107
108     def apply_changes(self, uuid, revnum, branch, global_changes, 
109                       renames, find_children=None):
110         """Change file id map to incorporate specified changes.
111
112         :param uuid: UUID of repository changes happen in
113         :param revnum: Revno for revision in which changes happened
114         :param branch: Branch path where changes happened
115         :param global_changes: Dict with global changes that happened
116         """
117         changes = get_local_changes(global_changes, self.repos.scheme,
118                     self.repos.generate_revision_id, find_children)
119         if find_children is not None:
120             def get_children(path, revid):
121                 (bp, revnum) = self.repos.lookup_revision_id(revid)
122                 for p in find_children(bp+"/"+path, revnum):
123                     yield self.repos.scheme.unprefix(p)[1]
124         else:
125             get_children = None
126
127         revid = self.repos.generate_revision_id(revnum, branch)
128
129         def new_file_id(x):
130             if renames.has_key(x):
131                 return renames[x]
132             return generate_file_id(self.repos, revid, x)
133          
134         return self._apply_changes(new_file_id, changes, get_children)
135
136     def get_map(self, uuid, revnum, branch, renames_cb):
137         """Make sure the map is up to date until revnum."""
138         # First, find the last cached map
139         todo = []
140         next_parent_revs = []
141         if revnum == 0:
142             assert branch == ""
143             return {"": (generate_svn_file_id(uuid, revnum, branch, ""), 
144                     self.repos.generate_revision_id(revnum, branch))}
145
146         # No history -> empty map
147         for (bp, paths, rev) in self.repos.follow_branch_history(branch, revnum):
148             revid = self.repos.generate_revision_id(rev, bp)
149             map = self.load(revid)
150             if map != {}:
151                 # found the nearest cached map
152                 next_parent_revs = [revid]
153                 break
154             todo.append((revid, paths))
155    
156         # target revision was present
157         if len(todo) == 0:
158             return map
159
160         if len(next_parent_revs) == 0:
161             if self.repos.scheme.is_branch(""):
162                 map = {"": (generate_svn_file_id(uuid, 0, "", ""), NULL_REVISION)}
163             else:
164                 map = {}
165
166         todo.reverse()
167         
168         pb = ui.ui_factory.nested_progress_bar()
169
170         try:
171             i = 1
172             for (revid, global_changes) in todo:
173                 changes = get_local_changes(global_changes, self.repos.scheme,
174                                             self.repos.generate_revision_id, 
175                                             self.repos._log.find_children)
176                 pb.update('generating file id map', i, len(todo))
177
178                 def find_children(path, revid):
179                     (bp, revnum) = self.repos.lookup_revision_id(revid)
180                     for p in self.repos._log.find_children(bp+"/"+path, revnum):
181                         yield self.repos.scheme.unprefix(p)[1]
182
183                 parent_revs = next_parent_revs
184
185                 renames = renames_cb(revid)
186
187                 def new_file_id(x):
188                     if renames.has_key(x):
189                         return renames[x]
190                     return generate_file_id(self.repos, revid, x)
191                 
192                 revmap = self._apply_changes(new_file_id, changes, find_children)
193                 for p in changes:
194                     if changes[p][0] == 'M' and not revmap.has_key(p):
195                         revmap[p] = map[p][0]
196
197                 map.update(dict([(x, (str(revmap[x]), revid)) for x in revmap]))
198
199                 # Mark all parent paths as changed
200                 for p in revmap:
201                     parts = p.split("/")
202                     for j in range(1, len(parts)+1):
203                         parent = "/".join(parts[0:len(parts)-j])
204                         assert map.has_key(parent), "Parent item %s of %s doesn't exist in map" % (parent, p)
205                         if map[parent][1] == revid:
206                             break
207                         map[parent] = map[parent][0], revid
208                         
209                 next_parent_revs = [revid]
210                 i += 1
211         finally:
212             pb.finished()
213         self.save(revid, parent_revs, map)
214         return map
215
216
217 class SimpleFileIdMap(FileIdMap):
218     @staticmethod
219     def _apply_changes(new_file_id, changes, find_children=None):
220         map = {}
221         sorted_paths = changes.keys()
222         sorted_paths.sort()
223         for p in sorted_paths:
224             data = changes[p]
225
226             if data[0] in ('A', 'R'):
227                 map[p] = new_file_id(p)
228
229                 if data[1] is not None:
230                     mutter('%r copied from %r:%s' % (p, data[1], data[2]))
231                     if find_children is not None:
232                         for c in find_children(data[1], data[2]):
233                             path = c.replace(data[1], p+"/", 1).replace("//", "/")
234                             map[path] = new_file_id(path)
235                             mutter('added mapping %r -> %r' % (path, map[path]))
236
237         return map