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