comparison hg_delta_editor.py @ 304:ce676eff002b

First merge, totally untested.
author Dan Villiom Podlaski Christiansen <danchr@gmail.com>
date Fri, 01 May 2009 10:28:59 +0200
parents 4aba7542f6a9 153266401676
children e6853c7fa3af
comparison
equal deleted inserted replaced
303:f423a8780832 304:ce676eff002b
6 import traceback 6 import traceback
7 7
8 from mercurial import context 8 from mercurial import context
9 from mercurial import hg 9 from mercurial import hg
10 from mercurial import ui 10 from mercurial import ui
11 from mercurial import util 11 from mercurial import util as hgutil
12 from mercurial import revlog 12 from mercurial import revlog
13 from mercurial import node 13 from mercurial import node
14 from svn import delta 14 from svn import delta
15 from svn import core 15 from svn import core
16 16
17 import svnexternals 17 import svnexternals
18 import util as our_util 18 import util
19 19
20 def pickle_atomic(data, file_path, dir=None): 20 def pickle_atomic(data, file_path, dir=None):
21 """pickle some data to a path atomically. 21 """pickle some data to a path atomically.
22 22
23 This is present because I kept corrupting my revmap by managing to hit ^C 23 This is present because I kept corrupting my revmap by managing to hit ^C
29 pickle.dump(data, f) 29 pickle.dump(data, f)
30 f.close() 30 f.close()
31 except: #pragma: no cover 31 except: #pragma: no cover
32 raise 32 raise
33 else: 33 else:
34 util.rename(path, file_path) 34 hgutil.rename(path, file_path)
35 35
36 def stash_exception_on_self(fn): 36 def stash_exception_on_self(fn):
37 """Stash any exception raised in the method on self. 37 """Stash any exception raised in the method on self.
38 38
39 This is required because the SWIG bindings just mutate any exception into 39 This is required because the SWIG bindings just mutate any exception into
58 f.flush() 58 f.flush()
59 f.close() 59 f.close()
60 self.revmap[revnum, branch] = node_hash 60 self.revmap[revnum, branch] = node_hash
61 61
62 def last_known_revision(self): 62 def last_known_revision(self):
63 ''' Obtain the highest numbered -- i.e. latest -- revision known. 63 """Obtain the highest numbered -- i.e. latest -- revision known.
64 64
65 Currently, this function just iterates over the entire revision map 65 Currently, this function just iterates over the entire revision map
66 using the max() builtin. This may be slow for extremely large 66 using the max() builtin. This may be slow for extremely large
67 repositories, but for now, it's fast enough. 67 repositories, but for now, it's fast enough.
68 ''' 68 """
69 try: 69 try:
70 return max(k[0] for k in self.revmap.iterkeys()) 70 return max(k[0] for k in self.revmap.iterkeys())
71 except ValueError: 71 except ValueError:
72 return 0 72 return 0
73 73
79 """path is the path to the target hg repo. 79 """path is the path to the target hg repo.
80 80
81 subdir is the subdirectory of the edits *on the svn server*. 81 subdir is the subdirectory of the edits *on the svn server*.
82 It is needed for stripping paths off in certain cases. 82 It is needed for stripping paths off in certain cases.
83 """ 83 """
84 if repo and repo.ui and not ui_:
85 ui_ = repo.ui
84 if not ui_: 86 if not ui_:
85 ui_ = ui.ui() 87 ui_ = ui.ui()
86 self.ui = ui_ 88 self.ui = ui_
87 if repo: 89 if repo:
88 self.repo = repo 90 self.repo = repo
96 self.subdir = subdir 98 self.subdir = subdir
97 if self.subdir and self.subdir[0] == '/': 99 if self.subdir and self.subdir[0] == '/':
98 self.subdir = self.subdir[1:] 100 self.subdir = self.subdir[1:]
99 self.revmap = {} 101 self.revmap = {}
100 if os.path.exists(self.revmap_file): 102 if os.path.exists(self.revmap_file):
101 self.revmap = our_util.parse_revmap(self.revmap_file) 103 self.revmap = util.parse_revmap(self.revmap_file)
102 self.branches = {} 104 self.branches = {}
103 if os.path.exists(self.branch_info_file): 105 if os.path.exists(self.branch_info_file):
104 f = open(self.branch_info_file) 106 f = open(self.branch_info_file)
105 self.branches = pickle.load(f) 107 self.branches = pickle.load(f)
106 f.close() 108 f.close()
128 self.readauthors(self.authors_file) 130 self.readauthors(self.authors_file)
129 if authors and os.path.exists(authors): 131 if authors and os.path.exists(authors):
130 self.readauthors(authors) 132 self.readauthors(authors)
131 if self.authors: 133 if self.authors:
132 self.writeauthors() 134 self.writeauthors()
135
136 self.lastdate = '1970-01-01 00:00:00 -0000'
133 self.includepaths = {} 137 self.includepaths = {}
134 self.excludepaths = {} 138 self.excludepaths = {}
135 if filemap and os.path.exists(filemap): 139 if filemap and os.path.exists(filemap):
136 self.readfilemap(filemap) 140 self.readfilemap(filemap)
141
142 def fixdate(self, date):
143 if date is not None:
144 date = date.replace('T', ' ').replace('Z', '').split('.')[0]
145 date += ' -0000'
146 self.lastdate = date
147 else:
148 date = self.lastdate
149 return date
137 150
138 def __setup_repo(self, repo_path): 151 def __setup_repo(self, repo_path):
139 """Verify the repo is going to work out for us. 152 """Verify the repo is going to work out for us.
140 153
141 This method will fail an assertion if the repo exists but doesn't have 154 This method will fail an assertion if the repo exists but doesn't have
148 assert os.path.isfile(self.uuid_file) 161 assert os.path.isfile(self.uuid_file)
149 else: 162 else:
150 self.repo = hg.repository(self.ui, repo_path, create=True) 163 self.repo = hg.repository(self.ui, repo_path, create=True)
151 os.makedirs(os.path.dirname(self.uuid_file)) 164 os.makedirs(os.path.dirname(self.uuid_file))
152 f = open(self.revmap_file, 'w') 165 f = open(self.revmap_file, 'w')
153 f.write('%s\n' % our_util.REVMAP_FILE_VERSION) 166 f.write('%s\n' % util.REVMAP_FILE_VERSION)
154 f.flush() 167 f.flush()
155 f.close() 168 f.close()
156 169
157 def clear_current_info(self): 170 def clear_current_info(self):
158 '''Clear the info relevant to a replayed revision so that the next 171 '''Clear the info relevant to a replayed revision so that the next
204 paths_need_discovery = [p[1] for p in paths_need_discovery] 217 paths_need_discovery = [p[1] for p in paths_need_discovery]
205 actually_files = [] 218 actually_files = []
206 while paths_need_discovery: 219 while paths_need_discovery:
207 p = paths_need_discovery.pop(0) 220 p = paths_need_discovery.pop(0)
208 path_could_be_file = True 221 path_could_be_file = True
209 # TODO(augie) Figure out if you can use break here in a for loop, quick
210 # testing of that failed earlier.
211 ind = 0 222 ind = 0
212 while ind < len(paths_need_discovery) and not paths_need_discovery: 223 while ind < len(paths_need_discovery) and not paths_need_discovery:
213 if op.startswith(p): 224 if op.startswith(p):
214 path_could_be_file = False 225 path_could_be_file = False
215 ind += 1 226 ind += 1
231 while filepaths: 242 while filepaths:
232 path = filepaths.pop(0) 243 path = filepaths.pop(0)
233 parentdir = '/'.join(path[:-1]) 244 parentdir = '/'.join(path[:-1])
234 filepaths = [p for p in filepaths if not '/'.join(p).startswith(parentdir)] 245 filepaths = [p for p in filepaths if not '/'.join(p).startswith(parentdir)]
235 branchpath = self._normalize_path(parentdir) 246 branchpath = self._normalize_path(parentdir)
247 if branchpath.startswith('tags/'):
248 continue
236 branchname = self._localname(branchpath) 249 branchname = self._localname(branchpath)
237 if branchpath.startswith('trunk/'): 250 if branchpath.startswith('trunk/'):
238 branches[self._localname('trunk')] = 'trunk' 251 branches[self._localname('trunk')] = 'trunk'
239 continue 252 continue
253 if branchname and branchname.startswith('../'):
254 continue
240 branches[branchname] = branchpath 255 branches[branchname] = branchpath
241 256
242 return branches 257 return branches
243 258
244 def _path_and_branch_for_path(self, path, existing=True): 259 def _path_and_branch_for_path(self, path, existing=True):
248 return self._path_and_branch_for_path(path, existing=existing)[1] 263 return self._path_and_branch_for_path(path, existing=existing)[1]
249 264
250 def _localname(self, path): 265 def _localname(self, path):
251 """Compute the local name for a branch located at path. 266 """Compute the local name for a branch located at path.
252 """ 267 """
268 assert not path.startswith('tags/')
253 if path == 'trunk': 269 if path == 'trunk':
254 return None 270 return None
255 elif path.startswith('branches/'): 271 elif path.startswith('branches/'):
256 return path[len('branches/'):] 272 return path[len('branches/'):]
257 return '../%s' % path 273 return '../%s' % path
272 If existing=True, will return None, None, None if the file isn't on some known 288 If existing=True, will return None, None, None if the file isn't on some known
273 branch. If existing=False, then it will guess what the branch would be if it were 289 branch. If existing=False, then it will guess what the branch would be if it were
274 known. 290 known.
275 """ 291 """
276 path = self._normalize_path(path) 292 path = self._normalize_path(path)
293 if path.startswith('tags/'):
294 return None, None, None
277 test = '' 295 test = ''
278 path_comps = path.split('/') 296 path_comps = path.split('/')
279 while self._localname(test) not in self.branches and len(path_comps): 297 while self._localname(test) not in self.branches and len(path_comps):
280 if not test: 298 if not test:
281 test = path_comps.pop(0) 299 test = path_comps.pop(0)
286 if existing: 304 if existing:
287 return None, None, None 305 return None, None, None
288 if path.startswith('trunk/'): 306 if path.startswith('trunk/'):
289 path = test.split('/')[1:] 307 path = test.split('/')[1:]
290 test = 'trunk' 308 test = 'trunk'
309 elif path.startswith('branches/'):
310 elts = path.split('/')
311 test = '/'.join(elts[:2])
312 path = '/'.join(elts[2:])
291 else: 313 else:
292 path = test.split('/')[-1] 314 path = test.split('/')[-1]
293 test = '/'.join(test.split('/')[:-1]) 315 test = '/'.join(test.split('/')[:-1])
294 return path, self._localname(test), test 316 ln = self._localname(test)
317 if ln and ln.startswith('../'):
318 return None, None, None
319 return path, ln, test
295 320
296 def set_current_rev(self, rev): 321 def set_current_rev(self, rev):
297 """Set the revision we're currently converting. 322 """Set the revision we're currently converting.
298 """ 323 """
299 self.current_rev = rev 324 self.current_rev = rev
356 if inc is None or exc is not None: 381 if inc is None or exc is not None:
357 return False 382 return False
358 return True 383 return True
359 384
360 def _is_path_valid(self, path): 385 def _is_path_valid(self, path):
386 if path is None:
387 return False
361 subpath = self._split_branch_path(path)[0] 388 subpath = self._split_branch_path(path)[0]
362 if subpath is None: 389 if subpath is None:
363 return False 390 return False
364 return self._is_file_included(subpath) 391 return self._is_file_included(subpath)
365 392
502 continue # case 1 529 continue # case 1
503 if paths[p].action == 'D': 530 if paths[p].action == 'D':
504 # check for case 5 531 # check for case 5
505 for known in self.branches: 532 for known in self.branches:
506 if self._svnpath(known).startswith(p): 533 if self._svnpath(known).startswith(p):
507 self.branches_to_delete.add(br) # case 5 534 self.branches_to_delete.add(known) # case 5
508 added_branches.update(self.__determine_parent_branch(p, paths[p].copyfrom_path, 535 added_branches.update(self.__determine_parent_branch(p, paths[p].copyfrom_path,
509 paths[p].copyfrom_rev, revision.revnum)) 536 paths[p].copyfrom_rev, revision.revnum))
510 for t in tags_to_delete: 537 for t in tags_to_delete:
511 del self.tags[t] 538 del self.tags[t]
512 for br in self.branches_to_delete: 539 for br in self.branches_to_delete:
563 # back to a list and sort so we get sane behavior 590 # back to a list and sort so we get sane behavior
564 files_to_commit = list(files_to_commit) 591 files_to_commit = list(files_to_commit)
565 files_to_commit.sort() 592 files_to_commit.sort()
566 branch_batches = {} 593 branch_batches = {}
567 rev = self.current_rev 594 rev = self.current_rev
568 date = rev.date.replace('T', ' ').replace('Z', '').split('.')[0] 595 date = self.fixdate(rev.date)
569 date += ' -0000'
570 596
571 # build up the branches that have files on them 597 # build up the branches that have files on them
572 for f in files_to_commit: 598 for f in files_to_commit:
573 if not self._is_path_valid(f): 599 if not self._is_path_valid(f):
574 continue 600 continue
613 639
614 parents = (self.get_parent_revision(rev.revnum, branch), 640 parents = (self.get_parent_revision(rev.revnum, branch),
615 revlog.nullid) 641 revlog.nullid)
616 if parents[0] in closed_revs and branch in self.branches_to_delete: 642 if parents[0] in closed_revs and branch in self.branches_to_delete:
617 continue 643 continue
618 # TODO this needs to be fixed with the new revmap 644 extra = util.build_extra(rev.revnum, branch,
619 extra = our_util.build_extra(rev.revnum, branch, 645 open(self.uuid_file).read(),
620 open(self.uuid_file).read(), 646 self.subdir)
621 self.subdir)
622 if branch is not None: 647 if branch is not None:
623 if (branch not in self.branches 648 if (branch not in self.branches
624 and branch not in self.repo.branchtags()): 649 and branch not in self.repo.branchtags()):
625 continue 650 continue
626 parent_ctx = self.repo.changectx(parents[0]) 651 parent_ctx = self.repo.changectx(parents[0])
656 filectxfn, 681 filectxfn,
657 self.authorforsvnauthor(rev.author), 682 self.authorforsvnauthor(rev.author),
658 date, 683 date,
659 extra) 684 extra)
660 new_hash = self.repo.commitctx(current_ctx) 685 new_hash = self.repo.commitctx(current_ctx)
661 our_util.describe_commit(self.ui, new_hash, branch) 686 util.describe_commit(self.ui, new_hash, branch)
662 if (rev.revnum, branch) not in self.revmap: 687 if (rev.revnum, branch) not in self.revmap:
663 self.add_to_revmap(rev.revnum, branch, new_hash) 688 self.add_to_revmap(rev.revnum, branch, new_hash)
664 # now we handle branches that need to be committed without any files 689 # now we handle branches that need to be committed without any files
665 for branch in self.commit_branches_empty: 690 for branch in self.commit_branches_empty:
666 ha = self.get_parent_revision(rev.revnum, branch) 691 ha = self.get_parent_revision(rev.revnum, branch)
669 parent_ctx = self.repo.changectx(ha) 694 parent_ctx = self.repo.changectx(ha)
670 def del_all_files(*args): 695 def del_all_files(*args):
671 raise IOError 696 raise IOError
672 # True here meant nuke all files, shouldn't happen with branch closing 697 # True here meant nuke all files, shouldn't happen with branch closing
673 if self.commit_branches_empty[branch]: #pragma: no cover 698 if self.commit_branches_empty[branch]: #pragma: no cover
674 raise util.Abort('Empty commit to an open branch attempted. ' 699 raise hgutil.Abort('Empty commit to an open branch attempted. '
675 'Please report this issue.') 700 'Please report this issue.')
676 extra = our_util.build_extra(rev.revnum, branch, 701 extra = util.build_extra(rev.revnum, branch,
677 open(self.uuid_file).read(), 702 open(self.uuid_file).read(),
678 self.subdir) 703 self.subdir)
679 current_ctx = context.memctx(self.repo, 704 current_ctx = context.memctx(self.repo,
680 (ha, node.nullid), 705 (ha, node.nullid),
681 rev.message or ' ', 706 rev.message or ' ',
683 del_all_files, 708 del_all_files,
684 self.authorforsvnauthor(rev.author), 709 self.authorforsvnauthor(rev.author),
685 date, 710 date,
686 extra) 711 extra)
687 new_hash = self.repo.commitctx(current_ctx) 712 new_hash = self.repo.commitctx(current_ctx)
688 our_util.describe_commit(self.ui, new_hash, branch) 713 util.describe_commit(self.ui, new_hash, branch)
689 if (rev.revnum, branch) not in self.revmap: 714 if (rev.revnum, branch) not in self.revmap:
690 self.add_to_revmap(rev.revnum, branch, new_hash) 715 self.add_to_revmap(rev.revnum, branch, new_hash)
691 self._save_metadata() 716 self._save_metadata()
692 self.clear_current_info() 717 self.clear_current_info()
693 718
694 def authorforsvnauthor(self, author): 719 def authorforsvnauthor(self, author):
695 if(author in self.authors): 720 if author in self.authors:
696 return self.authors[author] 721 return self.authors[author]
697 return '%s%s' %(author, self.author_host) 722 return '%s%s' % (author, self.author_host)
698 723
699 def svnauthorforauthor(self, author): 724 def svnauthorforauthor(self, author):
700 for svnauthor, hgauthor in self.authors.iteritems(): 725 for svnauthor, hgauthor in self.authors.iteritems():
701 if author == hgauthor: 726 if author == hgauthor:
702 return svnauthor 727 return svnauthor
703 else: 728 else:
704 # Mercurial incorrectly splits at e.g. '.', so we roll our own. 729 # return the original svn-side author
705 return author.rsplit('@', 1)[0] 730 return author.rsplit('@', 1)[0]
706 731
707 def readauthors(self, authorfile): 732 def readauthors(self, authorfile):
708 self.ui.note(('Reading authormap from %s\n') % authorfile) 733 self.ui.note(('Reading authormap from %s\n') % authorfile)
709 f = open(authorfile, 'r') 734 f = open(authorfile, 'r')
835 if br_path != '': 860 if br_path != '':
836 br_path2 = br_path + '/' 861 br_path2 = br_path + '/'
837 # assuming it is a directory 862 # assuming it is a directory
838 self.externals[path] = None 863 self.externals[path] = None
839 map(self.delete_file, [pat for pat in self.current_files.iterkeys() 864 map(self.delete_file, [pat for pat in self.current_files.iterkeys()
840 if pat.startswith(path)]) 865 if pat.startswith(path+'/')])
841 for f in ctx.walk(our_util.PrefixMatch(br_path2)): 866 for f in ctx.walk(util.PrefixMatch(br_path2)):
842 f_p = '%s/%s' % (path, f[len(br_path2):]) 867 f_p = '%s/%s' % (path, f[len(br_path2):])
843 if f_p not in self.current_files: 868 if f_p not in self.current_files:
844 self.delete_file(f_p) 869 self.delete_file(f_p)
845 self.delete_file(path) 870 self.delete_file(path)
846 delete_entry = stash_exception_on_self(delete_entry) 871 delete_entry = stash_exception_on_self(delete_entry)
847 872
848 def open_file(self, path, parent_baton, base_revision, p=None): 873 def open_file(self, path, parent_baton, base_revision, p=None):
849 self.current_file = 'foobaz' 874 self.current_file = None
850 fpath, branch = self._path_and_branch_for_path(path) 875 fpath, branch = self._path_and_branch_for_path(path)
851 if fpath: 876 if fpath:
852 self.current_file = path 877 self.current_file = path
853 self.ui.note('M %s\n' % path) 878 self.ui.note('M %s\n' % path)
854 if base_revision != -1: 879 if base_revision != -1:
889 # parentctx is not an ancestor of childctx, files are unrelated 914 # parentctx is not an ancestor of childctx, files are unrelated
890 return False 915 return False
891 916
892 def add_file(self, path, parent_baton=None, copyfrom_path=None, 917 def add_file(self, path, parent_baton=None, copyfrom_path=None,
893 copyfrom_revision=None, file_pool=None): 918 copyfrom_revision=None, file_pool=None):
894 self.current_file = 'foobaz' 919 self.current_file = None
895 self.base_revision = None 920 self.base_revision = None
896 if path in self.deleted_files: 921 if path in self.deleted_files:
897 del self.deleted_files[path] 922 del self.deleted_files[path]
898 fpath, branch = self._path_and_branch_for_path(path, existing=False) 923 fpath, branch = self._path_and_branch_for_path(path, existing=False)
899 if not fpath: 924 if not fpath:
951 cp_f = '' 976 cp_f = ''
952 else: 977 else:
953 source_rev = copyfrom_revision 978 source_rev = copyfrom_revision
954 cp_f, source_branch = self._path_and_branch_for_path(copyfrom_path) 979 cp_f, source_branch = self._path_and_branch_for_path(copyfrom_path)
955 if cp_f == '' and br_path == '': 980 if cp_f == '' and br_path == '':
981 assert br_path is not None
956 self.branches[branch] = source_branch, source_rev, self.current_rev.revnum 982 self.branches[branch] = source_branch, source_rev, self.current_rev.revnum
957 new_hash = self.get_parent_revision(source_rev + 1, 983 new_hash = self.get_parent_revision(source_rev + 1,
958 source_branch) 984 source_branch)
959 if new_hash == node.nullid: 985 if new_hash == node.nullid:
960 self.missing_plaintexts.add('%s/' % path) 986 self.missing_plaintexts.add('%s/' % path)
1034 target = cStringIO.StringIO() 1060 target = cStringIO.StringIO()
1035 self.stream = target 1061 self.stream = target
1036 1062
1037 handler, baton = delta.svn_txdelta_apply(source, target, None) 1063 handler, baton = delta.svn_txdelta_apply(source, target, None)
1038 if not callable(handler): #pragma: no cover 1064 if not callable(handler): #pragma: no cover
1039 raise util.Abort('Error in Subversion bindings: ' 1065 raise hgutil.Abort('Error in Subversion bindings: '
1040 'cannot call handler!') 1066 'cannot call handler!')
1041 def txdelt_window(window): 1067 def txdelt_window(window):
1042 try: 1068 try:
1043 if not self._is_path_valid(self.current_file): 1069 if not self._is_path_valid(self.current_file):
1044 return 1070 return
1045 handler(window, baton) 1071 handler(window, baton)
1048 self.current_files[self.current_file] = target.getvalue() 1074 self.current_files[self.current_file] = target.getvalue()
1049 except core.SubversionException, e: #pragma: no cover 1075 except core.SubversionException, e: #pragma: no cover
1050 if e.apr_err == core.SVN_ERR_INCOMPLETE_DATA: 1076 if e.apr_err == core.SVN_ERR_INCOMPLETE_DATA:
1051 self.missing_plaintexts.add(self.current_file) 1077 self.missing_plaintexts.add(self.current_file)
1052 else: #pragma: no cover 1078 else: #pragma: no cover
1053 raise util.Abort(*e.args) 1079 raise hgutil.Abort(*e.args)
1054 except: #pragma: no cover 1080 except: #pragma: no cover
1055 print len(base), self.current_file 1081 print len(base), self.current_file
1056 self._exception_info = sys.exc_info() 1082 self._exception_info = sys.exc_info()
1057 raise 1083 raise
1058 return txdelt_window 1084 return txdelt_window