def updateexternals(ui, meta, current): # TODO fix and re-enable externals for single-directory clones if not current.externals or meta.layout == 'single': return # accumulate externals records for all branches revnum = current.rev.revnum branches = {} for path, entry in current.externals.iteritems(): if not meta.is_path_valid(path): continue p, b, bp = meta.split_branch_path(path) if bp not in branches: parent = meta.get_parent_revision(revnum, b) pctx = meta.repo[parent] branches[bp] = (svnexternals.parse(ui, pctx), pctx) branches[bp][0][p] = entry # register externals file changes for bp, (external, pctx) in branches.iteritems(): if bp and bp[-1] != '/': bp += '/' updates = svnexternals.getchanges(ui, meta.repo, pctx, external) for fn, data in updates.iteritems(): path = (bp and bp + fn) or fn if data is not None: current.set(path, data, False, False) else: current.delete(path)
def fetch_externals(ui, svn, branchpath, r, parentctx): """Extract svn:externals for the current revision and branch Return an externalsfile instance or None if there are no externals to convert and never were. """ externals = svnexternals.parse(ui, parentctx) # Detect property additions only, changes are handled by checking # existing entries individually. Projects are unlikely to store # externals on many different root directories, so we trade code # duplication and complexity for a constant lookup price at every # revision in the common case. dirs = set(externals) if parentctx.node() == revlog.nullid: dirs.update( [p for p, k in svn.list_files(branchpath, r.revnum) if k == 'd']) dirs.add('') else: branchprefix = (branchpath and branchpath + '/') or branchpath for path, e in r.paths.iteritems(): if e.action == 'D': continue if not path.startswith(branchprefix) and path != branchpath: continue kind = svn.checkpath(path, r.revnum) if kind != 'd': continue path = path[len(branchprefix):] dirs.add(path) if e.action == 'M' or (e.action == 'A' and e.copyfrom_path): # Do not recurse in copied directories, changes are marked # as 'M', except for the copied one. continue for child, k in svn.list_files(branchprefix + path, r.revnum): if k == 'd': dirs.add((path + '/' + child).strip('/')) # Retrieve new or updated values for dir in dirs: try: dpath = (branchpath and branchpath + '/' + dir) or dir values = svn.list_props(dpath, r.revnum) externals[dir] = values.get('svn:externals', '') except IOError: externals[dir] = '' return externals
def fetch_externals(ui, svn, branchpath, r, parentctx): """Extract svn:externals for the current revision and branch Return an externalsfile instance or None if there are no externals to convert and never were. """ externals = svnexternals.parse(ui, parentctx) # Detect property additions only, changes are handled by checking # existing entries individually. Projects are unlikely to store # externals on many different root directories, so we trade code # duplication and complexity for a constant lookup price at every # revision in the common case. dirs = set(externals) if parentctx.node() == revlog.nullid: dirs.update([p for p,k in svn.list_files(branchpath, r.revnum) if k == 'd']) dirs.add('') else: branchprefix = (branchpath and branchpath + '/') or branchpath for path, e in r.paths.iteritems(): if e.action == 'D': continue if not path.startswith(branchprefix) and path != branchpath: continue kind = svn.checkpath(path, r.revnum) if kind != 'd': continue path = path[len(branchprefix):] dirs.add(path) if e.action == 'M' or (e.action == 'A' and e.copyfrom_path): # Do not recurse in copied directories, changes are marked # as 'M', except for the copied one. continue for child, k in svn.list_files(branchprefix + path, r.revnum): if k == 'd': dirs.add((path + '/' + child).strip('/')) # Retrieve new or updated values for dir in dirs: try: dpath = (branchpath and branchpath + '/' + dir) or dir values = svn.list_props(dpath, r.revnum) externals[dir] = values.get('svn:externals', '') except IOError: externals[dir] = '' return externals
def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revision, dir_pool=None): self._checkparentdir(parent_baton) baton = self._opendir(path) br_path, branch = self.meta.split_branch_path(path)[:2] if br_path is not None: if not copyfrom_path and not br_path: # This handles the case where a branch root is # replaced without copy info. It will show up as a # deletion and then an add. self.meta.closebranches.discard(branch) self.current.emptybranches[branch] = True else: self.current.emptybranches[branch] = False if br_path is None or not copyfrom_path: return baton if self.meta.get_path_tag(path): del self.current.emptybranches[branch] return baton tag = self.meta.get_path_tag(copyfrom_path) if tag not in self.meta.tags: tag = None if not self.meta.is_path_valid(copyfrom_path, existing=False): # The source path only exists at copyfrom_revision, use # existing=False to guess a possible branch location and # test it against the filemap. The actual path and # revision will be resolved below if necessary. self.addmissing(path, isdir=True) return baton if tag: changeid = self.meta.tags[tag] source_rev, source_branch = self.meta.get_source_rev(changeid)[:2] frompath = '' else: source_rev = copyfrom_revision frompath, source_branch = self.meta.split_branch_path(copyfrom_path)[:2] new_hash = self.meta.get_parent_revision(source_rev + 1, source_branch, True) if frompath is None or new_hash == node.nullid: self.addmissing(path, isdir=True) return baton fromctx = self._getctx(new_hash) if frompath != '/' and frompath != '': frompath = '%s/' % frompath else: frompath = '' copyfromparent = False if frompath == '' and br_path == '': pnode = self.meta.get_parent_revision( self.current.rev.revnum, branch) if pnode == new_hash: # Data parent is topological parent and relative paths # are the same, not need to do anything but restore # files marked as deleted. copyfromparent = True # Get the parent which would have been used for this branch # without the replace action. oldpnode = self.meta.get_parent_revision( self.current.rev.revnum, branch, exact=True) if (oldpnode != revlog.nullid and util.isancestor(self._getctx(oldpnode), fromctx)): # Branch-wide replacement, unmark the branch as deleted self.meta.closebranches.discard(branch) svncopies = {} copies = {} for f in fromctx: if not f.startswith(frompath): continue dest = path + '/' + f[len(frompath):] if not self.meta.is_path_valid(dest): continue if dest in self._deleted: self._deleted.remove(dest) if copyfromparent: continue svncopies[dest] = CopiedFile(new_hash, f, None) if branch == source_branch: copies[dest] = f if copies: # Preserve the directory copy records if no file was changed between # the source and destination revisions, or discard it completely. parentid = self.meta.get_parent_revision( self.current.rev.revnum, branch) if parentid != revlog.nullid: parentctx = self._getctx(parentid) for k, v in copies.iteritems(): if util.issamefile(parentctx, fromctx, v): svncopies[k].copypath = v self._svncopies.update(svncopies) # Copy the externals definitions of copied directories fromext = svnexternals.parse(self.ui, fromctx) for p, v in fromext.iteritems(): pp = p and (p + '/') or '' if pp.startswith(frompath): dest = (path + '/' + pp[len(frompath):]).rstrip('/') self.current.externals[dest] = v return baton
def commit(ui, repo, rev_ctx, meta, base_revision, svn): """Build and send a commit from Mercurial to Subversion. """ file_data = {} parent = rev_ctx.parents()[0] parent_branch = rev_ctx.parents()[0].branch() branch_path = meta.layoutobj.remotename(parent_branch) extchanges = svnexternals.diff(svnexternals.parse(ui, parent), svnexternals.parse(ui, rev_ctx)) addeddirs, deleteddirs = _getdirchanges(svn, branch_path, parent, rev_ctx, rev_ctx.files(), extchanges) deleteddirs = set(deleteddirs) props = {} copies = {} for file in rev_ctx.files(): if file in util.ignoredfiles: continue new_data = base_data = '' action = '' if file in rev_ctx: fctx = rev_ctx.filectx(file) new_data = fctx.data() if 'x' in fctx.flags(): props.setdefault(file, {})['svn:executable'] = '*' if 'l' in fctx.flags(): props.setdefault(file, {})['svn:special'] = '*' isbinary = hgutil.binary(new_data) if isbinary: props.setdefault( file, {})['svn:mime-type'] = 'application/octet-stream' if file not in parent: renamed = fctx.renamed() if renamed: # TODO current model (and perhaps svn model) does not support # this kind of renames: a -> b, b -> c copies[file] = renamed[0] base_data = parent[renamed[0]].data() if 'l' in parent[renamed[0]].flags(): base_data = 'link ' + base_data else: autoprops = svn.autoprops_config.properties(file) if autoprops: props.setdefault(file, {}).update(autoprops) action = 'add' dirname = '/'.join(file.split('/')[:-1] + ['']) else: base_data = parent.filectx(file).data() if ('x' in parent.filectx(file).flags() and 'x' not in rev_ctx.filectx(file).flags()): props.setdefault(file, {})['svn:executable'] = None if 'l' in parent.filectx(file).flags(): base_data = 'link ' + base_data if 'l' not in rev_ctx.filectx(file).flags(): props.setdefault(file, {})['svn:special'] = None if hgutil.binary(base_data) and not isbinary: props.setdefault(file, {})['svn:mime-type'] = None action = 'modify' else: pos = file.rfind('/') if pos >= 0: if file[:pos] in deleteddirs: # This file will be removed when its directory is removed continue action = 'delete' file_data[file] = base_data, new_data, action def svnpath(p): return ('%s/%s' % (branch_path, p)).strip('/') changeddirs = [] for d, v1, v2 in extchanges: props.setdefault(svnpath(d), {})['svn:externals'] = v2 if d not in deleteddirs and d not in addeddirs: changeddirs.append(svnpath(d)) # Now we are done with files, we can prune deleted directories # against themselves: ignore a/b if a/ is already removed deleteddirs2 = list(deleteddirs) deleteddirs2.sort(reverse=True) for d in deleteddirs2: pos = d.rfind('/') if pos >= 0 and d[:pos] in deleteddirs: deleteddirs.remove(d) newcopies = {} for source, dest in copies.iteritems(): newcopies[svnpath(source)] = (svnpath(dest), base_revision) new_target_files = [svnpath(f) for f in file_data] for tf, ntf in zip(file_data, new_target_files): if tf in file_data and tf != ntf: file_data[ntf] = file_data[tf] if tf in props: props[ntf] = props.pop(tf) del file_data[tf] addeddirs = [svnpath(d) for d in addeddirs] deleteddirs = [svnpath(d) for d in deleteddirs] new_target_files += addeddirs + deleteddirs + changeddirs if not new_target_files: raise NoFilesException() try: return svn.commit(new_target_files, rev_ctx.description(), file_data, base_revision, set(addeddirs), set(deleteddirs), props, newcopies) except svnwrap.SubversionException, e: ui.traceback() if len(e.args) > 0 and e.args[1] in (svnwrap.ERR_FS_TXN_OUT_OF_DATE, svnwrap.ERR_FS_CONFLICT, svnwrap.ERR_FS_ALREADY_EXISTS): raise hgutil.Abort('Outgoing changesets parent is not at ' 'subversion HEAD\n' '(pull again and rebase on a newer revision)') elif len(e.args) > 0 and e.args[1] == svnwrap.ERR_REPOS_HOOK_FAILURE: # Special handling for svn hooks blocking error raise hgutil.Abort(e.args[0]) else: raise
def commit(ui, repo, rev_ctx, meta, base_revision, svn): """Build and send a commit from Mercurial to Subversion. """ file_data = {} parent = rev_ctx.parents()[0] parent_branch = rev_ctx.parents()[0].branch() branch_path = 'trunk' if meta.layout == 'single': branch_path = '' elif parent_branch and parent_branch != 'default': branch_path = 'branches/%s' % parent_branch extchanges = svnexternals.diff(svnexternals.parse(ui, parent), svnexternals.parse(ui, rev_ctx)) addeddirs, deleteddirs = _getdirchanges(svn, branch_path, parent, rev_ctx, rev_ctx.files(), extchanges) deleteddirs = set(deleteddirs) props = {} copies = {} for file in rev_ctx.files(): if file in util.ignoredfiles: continue new_data = base_data = '' action = '' if file in rev_ctx: fctx = rev_ctx.filectx(file) new_data = fctx.data() if 'x' in fctx.flags(): props.setdefault(file, {})['svn:executable'] = '*' if 'l' in fctx.flags(): props.setdefault(file, {})['svn:special'] = '*' isbinary = hgutil.binary(new_data) if isbinary: props.setdefault(file, {})['svn:mime-type'] = 'application/octet-stream' if file not in parent: renamed = fctx.renamed() if renamed: # TODO current model (and perhaps svn model) does not support # this kind of renames: a -> b, b -> c copies[file] = renamed[0] base_data = parent[renamed[0]].data() action = 'add' dirname = '/'.join(file.split('/')[:-1] + ['']) else: base_data = parent.filectx(file).data() if ('x' in parent.filectx(file).flags() and 'x' not in rev_ctx.filectx(file).flags()): props.setdefault(file, {})['svn:executable'] = None if ('l' in parent.filectx(file).flags() and 'l' not in rev_ctx.filectx(file).flags()): props.setdefault(file, {})['svn:special'] = None if hgutil.binary(base_data) and not isbinary: props.setdefault(file, {})['svn:mime-type'] = None action = 'modify' else: pos = file.rfind('/') if pos >= 0: if file[:pos] in deleteddirs: # This file will be removed when its directory is removed continue action = 'delete' file_data[file] = base_data, new_data, action def svnpath(p): return ('%s/%s' % (branch_path, p)).strip('/') changeddirs = [] for d, v1, v2 in extchanges: props.setdefault(svnpath(d), {})['svn:externals'] = v2 if d not in deleteddirs and d not in addeddirs: changeddirs.append(svnpath(d)) # Now we are done with files, we can prune deleted directories # against themselves: ignore a/b if a/ is already removed deleteddirs2 = list(deleteddirs) deleteddirs2.sort(reverse=True) for d in deleteddirs2: pos = d.rfind('/') if pos >= 0 and d[:pos] in deleteddirs: deleteddirs.remove(d) newcopies = {} for source, dest in copies.iteritems(): newcopies[svnpath(source)] = (svnpath(dest), base_revision) new_target_files = [svnpath(f) for f in file_data] for tf, ntf in zip(file_data, new_target_files): if tf in file_data and tf != ntf: file_data[ntf] = file_data[tf] if tf in props: props[ntf] = props.pop(tf) del file_data[tf] addeddirs = [svnpath(d) for d in addeddirs] deleteddirs = [svnpath(d) for d in deleteddirs] new_target_files += addeddirs + deleteddirs + changeddirs if not new_target_files: raise NoFilesException() try: svn.commit(new_target_files, rev_ctx.description(), file_data, base_revision, set(addeddirs), set(deleteddirs), props, newcopies) except svnwrap.SubversionException, e: if len(e.args) > 0 and e.args[1] in (svnwrap.ERR_FS_TXN_OUT_OF_DATE, svnwrap.ERR_FS_CONFLICT): raise hgutil.Abort('Outgoing changesets parent is not at ' 'subversion HEAD\n' '(pull again and rebase on a newer revision)') else: raise
def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revision, dir_pool=None): self.current.batons[path] = path br_path, branch = self.meta.split_branch_path(path)[:2] if br_path is not None: if not copyfrom_path and not br_path: self.current.emptybranches[branch] = True else: self.current.emptybranches[branch] = False if br_path is None or not copyfrom_path: return path if self.meta.get_path_tag(path): del self.current.emptybranches[branch] return path tag = self.meta.get_path_tag(copyfrom_path) if tag not in self.meta.tags: tag = None if not self.meta.is_path_valid(copyfrom_path): self.current.missing.add('%s/' % path) return path if tag: changeid = self.meta.tags[tag] source_rev, source_branch = self.meta.get_source_rev(changeid)[:2] frompath = '' else: source_rev = copyfrom_revision frompath, source_branch = self.meta.split_branch_path(copyfrom_path)[:2] if frompath == '' and br_path == '': assert br_path is not None tmp = source_branch, source_rev, self.current.rev.revnum self.meta.branches[branch] = tmp new_hash = self.meta.get_parent_revision(source_rev + 1, source_branch, True) if new_hash == node.nullid: self.current.missing.add('%s/' % path) return path fromctx = self.repo.changectx(new_hash) if frompath != '/' and frompath != '': frompath = '%s/' % frompath else: frompath = '' copies = {} for f in fromctx: if not f.startswith(frompath): continue fctx = fromctx.filectx(f) dest = path + '/' + f[len(frompath):] self.current.set(dest, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags()) if dest in self.current.deleted: del self.current.deleted[dest] if branch == source_branch: copies[dest] = f if copies: # Preserve the directory copy records if no file was changed between # the source and destination revisions, or discard it completely. parentid = self.meta.get_parent_revision(self.current.rev.revnum, branch) if parentid != revlog.nullid: parentctx = self.repo.changectx(parentid) for k, v in copies.iteritems(): if util.issamefile(parentctx, fromctx, v): self.current.copies[k] = v # Copy the externals definitions of copied directories fromext = svnexternals.parse(self.ui, fromctx) for p, v in fromext.iteritems(): pp = p and (p + '/') or '' if pp.startswith(frompath): dest = (path + '/' + pp[len(frompath):]).rstrip('/') self.current.externals[dest] = v return path