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 |
