Mercurial > hgsubversion
comparison hg_delta_editor.py @ 203:907c160c6289
Refactor branch handling to be much more dynamic (and hopefully robust).
This should allow fixing of several outstanding issues with branch handling. Note that this is a *massive* change to one of the oldest parts of hgsubversion, so it might introduce bugs not caught by the testsuite.
author | Augie Fackler <durin42@gmail.com> |
---|---|
date | Mon, 02 Mar 2009 23:58:38 -0600 |
parents | df4611050286 |
children | b20a6c149021 |
comparison
equal
deleted
inserted
replaced
202:125cf3cb7bee | 203:907c160c6289 |
---|---|
164 every revision is created. | 164 every revision is created. |
165 ''' | 165 ''' |
166 pickle_atomic(self.branches, self.branch_info_file, self.meta_data_dir) | 166 pickle_atomic(self.branches, self.branch_info_file, self.meta_data_dir) |
167 pickle_atomic(self.tags, self.tag_info_file, self.meta_data_dir) | 167 pickle_atomic(self.tags, self.tag_info_file, self.meta_data_dir) |
168 | 168 |
169 def branches_in_paths(self, paths): | 169 def branches_in_paths(self, paths, revnum, checkpath, listdir): |
170 '''Given a list of paths, return mapping of all branches touched | 170 '''Given a list of paths, return mapping of all branches touched |
171 to their branch path. | 171 to their branch path. |
172 ''' | 172 ''' |
173 branches = {} | 173 branches = {} |
174 paths_need_discovery = [] | |
174 for p in paths: | 175 for p in paths: |
175 relpath, branch, branchpath = self._split_branch_path(p) | 176 relpath, branch, branchpath = self._split_branch_path(p) |
176 if relpath is not None: | 177 if relpath is not None: |
177 branches[branch] = branchpath | 178 branches[branch] = branchpath |
179 elif paths[p].action == 'D' and not self._is_path_tag(p): | |
180 ln = self._localname(p) | |
181 # must check in branches_to_delete as well, because this runs after we | |
182 # already updated the branch map | |
183 if ln in self.branches or ln in self.branches_to_delete: | |
184 branches[self._localname(p)] = p | |
185 else: | |
186 paths_need_discovery.append(p) | |
187 if paths_need_discovery: | |
188 paths_need_discovery = [(len(p), p) for p in paths_need_discovery] | |
189 paths_need_discovery.sort() | |
190 paths_need_discovery = [p[1] for p in paths_need_discovery] | |
191 actually_files = [] | |
192 while paths_need_discovery: | |
193 p = paths_need_discovery.pop(0) | |
194 path_could_be_file = True | |
195 # TODO(augie) Figure out if you can use break here in a for loop, quick | |
196 # testing of that failed earlier. | |
197 ind = 0 | |
198 while ind < len(paths_need_discovery) and not paths_need_discovery: | |
199 if op.startswith(p): | |
200 path_could_be_file = False | |
201 ind += 1 | |
202 if path_could_be_file: | |
203 if checkpath(p, revnum) == 'f': | |
204 actually_files.append(p) | |
205 # if there's a copyfrom_path and there were files inside that copyfrom, | |
206 # we need to detect those branches. It's a little thorny and slow, but | |
207 # seems to be the best option. | |
208 elif paths[p].copyfrom_path and not p.startswith('tags/'): | |
209 paths_need_discovery.extend(['%s/%s' % (p,x[0]) | |
210 for x in listdir(p, revnum) | |
211 if x[1] == 'f']) | |
212 if actually_files: | |
213 filepaths = [p.split('/') for p in actually_files] | |
214 filepaths = [(len(p), p) for p in filepaths] | |
215 filepaths.sort() | |
216 filepaths = [p[1] for p in filepaths] | |
217 while filepaths: | |
218 path = filepaths.pop(0) | |
219 parentdir = '/'.join(path[:-1]) | |
220 filepaths = [p for p in filepaths if not '/'.join(p).startswith(parentdir)] | |
221 branchpath = self._normalize_path(parentdir) | |
222 branchname = self._localname(branchpath) | |
223 branches[branchname] = branchpath | |
224 | |
178 return branches | 225 return branches |
179 | 226 |
180 def _path_and_branch_for_path(self, path): | 227 def _path_and_branch_for_path(self, path, existing=True): |
181 return self._split_branch_path(path)[:2] | 228 return self._split_branch_path(path, existing=existing)[:2] |
182 | 229 |
183 def _split_branch_path(self, path): | 230 def _localname(self, path): |
231 """Compute the local name for a branch located at path. | |
232 """ | |
233 assert not path.startswith('tags/') | |
234 if path == 'trunk': | |
235 return None | |
236 elif path.startswith('branches/'): | |
237 return path[len('branches/'):] | |
238 return '../%s' % path | |
239 | |
240 def _remotename(self, branch): | |
241 if branch == 'default' or branch is None: | |
242 return 'trunk' | |
243 elif branch.startswith('../'): | |
244 return branch[3:] | |
245 return 'branches/%s' % branch | |
246 | |
247 def _split_branch_path(self, path, existing=True): | |
184 """Figure out which branch inside our repo this path represents, and | 248 """Figure out which branch inside our repo this path represents, and |
185 also figure out which path inside that branch it is. | 249 also figure out which path inside that branch it is. |
186 | 250 |
187 Raises an exception if it can't perform its job. | 251 Returns a tuple of (path within branch, local branch name, server-side branch path). |
252 | |
253 If existing=True, will return None, None, None if the file isn't on some known | |
254 branch. If existing=False, then it will guess what the branch would be if it were | |
255 known. | |
188 """ | 256 """ |
189 path = self._normalize_path(path) | 257 path = self._normalize_path(path) |
190 if path.startswith('trunk'): | 258 if path.startswith('tags/'): |
191 p = path[len('trunk'):] | 259 return None, None, None |
192 if p and p[0] == '/': | 260 test = '' |
193 p = p[1:] | 261 path_comps = path.split('/') |
194 return p, None, 'trunk' | 262 while self._localname(test) not in self.branches and len(path_comps): |
195 elif path.startswith('branches/'): | 263 if not test: |
196 p = path[len('branches/'):] | 264 test = path_comps.pop(0) |
197 br = p.split('/')[0] | 265 else: |
198 if br: | 266 test += '/%s' % path_comps.pop(0) |
199 p = p[len(br)+1:] | 267 if self._localname(test) in self.branches: |
200 if p and p[0] == '/': | 268 return path[len(test)+1:], self._localname(test), test |
201 p = p[1:] | 269 if existing: |
202 return p, br, 'branches/' + br | 270 return None, None, None |
203 return None, None, None | 271 path = test.split('/')[-1] |
272 test = '/'.join(test.split('/')[:-1]) | |
273 return path, self._localname(test), test | |
204 | 274 |
205 def set_current_rev(self, rev): | 275 def set_current_rev(self, rev): |
206 """Set the revision we're currently converting. | 276 """Set the revision we're currently converting. |
207 """ | 277 """ |
208 self.current_rev = rev | 278 self.current_rev = rev |
232 if path and path.startswith(self.subdir): | 302 if path and path.startswith(self.subdir): |
233 path = path[len(self.subdir):] | 303 path = path[len(self.subdir):] |
234 if path and path[0] == '/': | 304 if path and path[0] == '/': |
235 path = path[1:] | 305 path = path[1:] |
236 return path | 306 return path |
237 | 307 |
238 def _is_file_included(self, subpath): | 308 def _is_file_included(self, subpath): |
239 def checkpathinmap(path, mapping): | 309 def checkpathinmap(path, mapping): |
240 def rpairs(name): | 310 def rpairs(name): |
241 yield '.', name | 311 yield '.', name |
242 e = len(name) | 312 e = len(name) |
243 while e != -1: | 313 while e != -1: |
244 yield name[:e], name[e+1:] | 314 yield name[:e], name[e+1:] |
245 e = name.rfind('/', 0, e) | 315 e = name.rfind('/', 0, e) |
246 | 316 |
247 for pre, suf in rpairs(path): | 317 for pre, suf in rpairs(path): |
248 try: | 318 try: |
249 return mapping[pre] | 319 return mapping[pre] |
250 except KeyError, err: | 320 except KeyError, err: |
251 pass | 321 pass |
252 return None | 322 return None |
253 | 323 |
254 if len(self.includepaths) and len(subpath): | 324 if len(self.includepaths) and len(subpath): |
255 inc = checkpathinmap(subpath, self.includepaths) | 325 inc = checkpathinmap(subpath, self.includepaths) |
256 else: | 326 else: |
257 inc = subpath | 327 inc = subpath |
258 if len(self.excludepaths) and len(subpath): | 328 if len(self.excludepaths) and len(subpath): |
266 def _is_path_valid(self, path): | 336 def _is_path_valid(self, path): |
267 subpath = self._split_branch_path(path)[0] | 337 subpath = self._split_branch_path(path)[0] |
268 if subpath is None: | 338 if subpath is None: |
269 return False | 339 return False |
270 return self._is_file_included(subpath) | 340 return self._is_file_included(subpath) |
271 | 341 |
272 | 342 |
273 def _is_path_tag(self, path): | 343 def _is_path_tag(self, path): |
274 """If path represents the path to a tag, returns the tag name. | 344 """If path could represent the path to a tag, returns the potential tag name. |
345 | |
346 Note that it's only a tag if it was copied from the path '' in a branch (or tag) | |
347 we have, for our purposes. | |
275 | 348 |
276 Otherwise, returns False. | 349 Otherwise, returns False. |
277 """ | 350 """ |
278 path = self._normalize_path(path) | 351 path = self._normalize_path(path) |
279 for tags_path in self.tag_locations: | 352 for tags_path in self.tag_locations: |
280 if path and (path.startswith(tags_path) and | 353 if path and (path.startswith(tags_path) and |
281 len(path) > len('%s/' % tags_path)): | 354 len(path) > len('%s/' % tags_path)): |
282 return path[len(tags_path)+1:].split('/')[0] | 355 return path[len(tags_path)+1:] |
283 return False | 356 return False |
284 | 357 |
285 def get_parent_svn_branch_and_rev(self, number, branch): | 358 def get_parent_svn_branch_and_rev(self, number, branch): |
286 number -= 1 | 359 number -= 1 |
287 if (number, branch) in self.revmap: | 360 if (number, branch) in self.revmap: |
319 r, br = self.get_parent_svn_branch_and_rev(number, branch) | 392 r, br = self.get_parent_svn_branch_and_rev(number, branch) |
320 if r is not None: | 393 if r is not None: |
321 return self.revmap[r, br] | 394 return self.revmap[r, br] |
322 return revlog.nullid | 395 return revlog.nullid |
323 | 396 |
397 def _svnpath(self, branch): | |
398 """Return the relative path in svn of branch. | |
399 """ | |
400 if branch == None or branch == 'default': | |
401 return 'trunk' | |
402 elif branch.startswith('../'): | |
403 return branch[3:] | |
404 return 'branches/%s' % branch | |
405 | |
406 def __determine_parent_branch(self, p, src_path, src_rev, revnum): | |
407 if src_path is not None: | |
408 src_file, src_branch = self._path_and_branch_for_path(src_path) | |
409 src_tag = self._is_path_tag(src_path) | |
410 if src_tag != False: | |
411 # also case 2 | |
412 src_branch, src_rev = self.tags[src_tag] | |
413 return {self._localname(p): (src_branch, src_rev, revnum )} | |
414 if src_file == '': | |
415 # case 2 | |
416 return {self._localname(p): (src_branch, src_rev, revnum )} | |
417 return {} | |
418 | |
324 def update_branch_tag_map_for_rev(self, revision): | 419 def update_branch_tag_map_for_rev(self, revision): |
325 paths = revision.paths | 420 paths = revision.paths |
326 added_branches = {} | 421 added_branches = {} |
327 added_tags = {} | 422 added_tags = {} |
328 self.branches_to_delete = set() | 423 self.branches_to_delete = set() |
329 tags_to_delete = set() | 424 tags_to_delete = set() |
330 for p in sorted(paths): | 425 for p in sorted(paths): |
331 fi, br = self._path_and_branch_for_path(p) | 426 t_name = self._is_path_tag(p) |
332 if fi is not None: | 427 if t_name != False: |
333 if fi == '' and paths[p].action != 'D': | |
334 src_p = paths[p].copyfrom_path | |
335 src_rev = paths[p].copyfrom_rev | |
336 src_tag = self._is_path_tag(src_p) | |
337 | |
338 if not ((src_p and self._is_path_valid(src_p)) or | |
339 (src_tag and src_tag in self.tags)): | |
340 # The branch starts here and is not a copy | |
341 src_branch = None | |
342 src_rev = 0 | |
343 elif src_tag: | |
344 # this is a branch created from a tag. Note that this | |
345 # really does happen (see Django) | |
346 src_branch, src_rev = self.tags[src_tag] | |
347 else: | |
348 # Not from a tag, and from a valid repo path | |
349 (src_p, | |
350 src_branch) = self._path_and_branch_for_path(src_p) | |
351 if src_p is None: | |
352 continue | |
353 if (br not in self.branches or | |
354 not (src_rev == 0 and src_branch == None)): | |
355 added_branches[br] = src_branch, src_rev, revision.revnum | |
356 elif fi == '' and br in self.branches: | |
357 self.branches_to_delete.add(br) | |
358 else: | |
359 t_name = self._is_path_tag(p) | |
360 if t_name == False: | |
361 continue | |
362 src_p, src_rev = paths[p].copyfrom_path, paths[p].copyfrom_rev | 428 src_p, src_rev = paths[p].copyfrom_path, paths[p].copyfrom_rev |
363 # if you commit to a tag, I'm calling you stupid and ignoring | 429 # if you commit to a tag, I'm calling you stupid and ignoring |
364 # you. | 430 # you. |
365 if src_p is not None and src_rev is not None: | 431 if src_p is not None and src_rev is not None: |
366 file, branch = self._path_and_branch_for_path(src_p) | 432 file, branch = self._path_and_branch_for_path(src_p) |
376 elif file and src_rev > added_tags[t_name][1]: | 442 elif file and src_rev > added_tags[t_name][1]: |
377 added_tags[t_name] = branch, src_rev | 443 added_tags[t_name] = branch, src_rev |
378 elif (paths[p].action == 'D' and p.endswith(t_name) | 444 elif (paths[p].action == 'D' and p.endswith(t_name) |
379 and t_name in self.tags): | 445 and t_name in self.tags): |
380 tags_to_delete.add(t_name) | 446 tags_to_delete.add(t_name) |
447 continue | |
448 # At this point we know the path is not a tag. In that case, we only care if it | |
449 # is the root of a new branch (in this function). This is determined by the | |
450 # following checks: | |
451 # 1. Is the file located inside any currently known branch? | |
452 # If yes, then we're done with it, this isn't interesting. | |
453 # 2. Does the file have copyfrom information that means it is a copy from the root | |
454 # of some other branch? | |
455 # If yes, then we're done: this is a new branch, and we record the copyfrom in | |
456 # added_branches | |
457 # 3. Neither of the above. This could be a branch, but it might never work out for | |
458 # us. It's only ever a branch (as far as we're concerned) if it gets committed | |
459 # to, which we have to detect at file-write time anyway. So we do nothing here. | |
460 # 4. It's the root of an already-known branch, with an action of 'D'. We mark the | |
461 # branch as deleted. | |
462 # 5. It's the parent directory of one or more already-known branches, so we mark them | |
463 # as deleted. | |
464 # 6. It's a branch being replaced by another branch - the action will be 'R'. | |
465 fi, br = self._path_and_branch_for_path(p) | |
466 if fi is not None: | |
467 if fi == '': | |
468 if paths[p].action == 'D': | |
469 self.branches_to_delete.add(br) # case 4 | |
470 elif paths[p].action == 'R': | |
471 added_branches.update(self.__determine_parent_branch(p, paths[p].copyfrom_path, | |
472 paths[p].copyfrom_rev, | |
473 revision.revnum)) | |
474 continue # case 1 | |
475 if paths[p].action == 'D': | |
476 # check for case 5 | |
477 for known in self.branches: | |
478 if self._svnpath(known).startswith(p): | |
479 self.branches_to_delete.add(br) # case 5 | |
480 added_branches.update(self.__determine_parent_branch(p, paths[p].copyfrom_path, | |
481 paths[p].copyfrom_rev, revision.revnum)) | |
381 for t in tags_to_delete: | 482 for t in tags_to_delete: |
382 del self.tags[t] | 483 del self.tags[t] |
383 for br in self.branches_to_delete: | 484 for br in self.branches_to_delete: |
384 del self.branches[br] | 485 del self.branches[br] |
385 for t, info in added_tags.items(): | 486 for t, info in added_tags.items(): |
554 extra) | 655 extra) |
555 new_hash = self.repo.commitctx(current_ctx) | 656 new_hash = self.repo.commitctx(current_ctx) |
556 our_util.describe_commit(self.ui, new_hash, branch) | 657 our_util.describe_commit(self.ui, new_hash, branch) |
557 if (rev.revnum, branch) not in self.revmap: | 658 if (rev.revnum, branch) not in self.revmap: |
558 self.add_to_revmap(rev.revnum, branch, new_hash) | 659 self.add_to_revmap(rev.revnum, branch, new_hash) |
660 self._save_metadata() | |
559 self.clear_current_info() | 661 self.clear_current_info() |
560 | 662 |
561 def authorforsvnauthor(self, author): | 663 def authorforsvnauthor(self, author): |
562 if(author in self.authors): | 664 if(author in self.authors): |
563 return self.authors[author] | 665 return self.authors[author] |
739 copyfrom_revision, file_pool=None): | 841 copyfrom_revision, file_pool=None): |
740 self.current_file = 'foobaz' | 842 self.current_file = 'foobaz' |
741 self.base_revision = None | 843 self.base_revision = None |
742 if path in self.deleted_files: | 844 if path in self.deleted_files: |
743 del self.deleted_files[path] | 845 del self.deleted_files[path] |
744 fpath, branch = self._path_and_branch_for_path(path) | 846 fpath, branch = self._path_and_branch_for_path(path, existing=False) |
745 if not fpath: | 847 if not fpath: |
746 return | 848 return |
849 if branch not in self.branches: | |
850 # we know this branch will exist now, because it has at least one file. Rock. | |
851 self.branches[branch] = None, 0, self.current_rev.revnum | |
747 self.current_file = path | 852 self.current_file = path |
748 self.should_edit_most_recent_plaintext = False | 853 self.should_edit_most_recent_plaintext = False |
749 if not copyfrom_path: | 854 if not copyfrom_path: |
750 self.ui.note('A %s\n' % path) | 855 self.ui.note('A %s\n' % path) |
751 return | 856 return |
788 if tag not in self.tags: | 893 if tag not in self.tags: |
789 tag = None | 894 tag = None |
790 if not self._is_path_valid(copyfrom_path) and not tag: | 895 if not self._is_path_valid(copyfrom_path) and not tag: |
791 self.missing_plaintexts.add('%s/' % path) | 896 self.missing_plaintexts.add('%s/' % path) |
792 return path | 897 return path |
793 | |
794 if tag: | 898 if tag: |
795 source_branch, source_rev = self.tags[tag] | 899 source_branch, source_rev = self.tags[tag] |
796 cp_f = '' | 900 cp_f = '' |
797 else: | 901 else: |
798 source_rev = copyfrom_revision | 902 source_rev = copyfrom_revision |
799 cp_f, source_branch = self._path_and_branch_for_path(copyfrom_path) | 903 cp_f, source_branch = self._path_and_branch_for_path(copyfrom_path) |
904 if cp_f == '' and br_path == '': | |
905 self.branches[branch] = source_branch, source_rev, self.current_rev.revnum | |
800 new_hash = self.get_parent_revision(source_rev + 1, | 906 new_hash = self.get_parent_revision(source_rev + 1, |
801 source_branch) | 907 source_branch) |
802 if new_hash == node.nullid: | 908 if new_hash == node.nullid: |
803 self.missing_plaintexts.add('%s/' % path) | 909 self.missing_plaintexts.add('%s/' % path) |
804 return path | 910 return path |