1"""hgpushsvn must be run in a repository created by hgimportsvn. It pushes
2local Mercurial changesets one by one or optionally collapsed into a single
3commit back to the SVN repository.
4
5"""
6
7import codecs
8import os
9import stat
10import sys
11import re
12from optparse import OptionParser
13import copy
14import traceback
15
16from hgsvn.shell  import (once_or_more
17    , get_encoding
18    )
19from hgsvn.hgclient import *
20from hgsvn import ui
21from hgsvn.common import *
22from hgsvn.errors import *
23from hgsvn.run.common import run_parser, display_parser_error
24from hgsvn.run.common import locked_main
25from hgsvn.svnclient import *
26
27import six
28from six import print_
29
30svn_branch = ""
31repos_url   = ""
32
33def IsDebug():
34    return (ui._level >= ui.DEBUG)
35
36
37def get_hg_revs(first_rev, svn_branch, last_rev="tip"):
38    """
39    Get a chronological list of revisions (changeset IDs) between the two
40    revisions (included).
41    """
42    args = ["log", "--template", r'{rev}:{node|short}\n', "-b", svn_branch,
43            '--limit', '1000',
44            "-r", "%s::%s" % (strip_hg_rev(first_rev),
45                             strip_hg_rev(last_rev))]
46    out = run_hg(args)
47    return [strip_hg_rev(s) for s in out.splitlines()]
48
49class hglog_node(object):
50    parents = []
51    childs = []
52    rev_no = -1
53    mark   = None
54
55    def __repr__(self):
56        return "parents:%s ; childs: %s ; rev:%s\n"%(self.parents, self.childs, self.rev_no)
57
58class hglog_tree():
59    hg_tree = None
60    revs    = None
61    can_merge_anonims = False
62
63    def __init__(self, tree, revs):
64        self.hg_tree = tree
65        self.revs = revs
66
67    def close(self):
68        self.hg_tree = None
69        self.revs    = None
70
71    def clean_revs_after(self, rev_id, safe_mark = -1):
72        ui.status("clean hg history after rev%s marked by parent %s "%(rev_id, safe_mark), level = ui.DEBUG)
73        rev_stop = self.hg_tree[rev_id].rev_no
74        drop_revs = list()
75        for rev in self.revs:
76            node = self.hg_tree[rev]
77            rev_no = node.rev_no
78            if rev_no > rev_stop:
79                if node.mark != safe_mark:
80                    ui.status("remove hg history rev%s"%rev, level = ui.DEBUG)
81                    drop_revs.append(rev);
82
83        for rev in drop_revs:
84            del self.hg_tree[rev]
85            self.revs.remove(rev)
86
87    def anonim_mark_safe_childs(self, safe_rev, mark):
88        ui.status("anonims mark childs for rev%s by parent %s "%(rev_id, safe_mark), level = ui.TRACE)
89        #recursively mark all descendants
90        safenode = self.hg_tree[safe_rev]
91        safe_no  = safenode.rev_no
92        childs   = safenode.childs
93        for child in childs:
94            childid = strip_hg_revid(child)
95            self.anonim_mark_safe_childs(childid, mark)
96        safenode.mark = mark
97
98    def anonim_remove(self, child, not_parent):
99        ui.status("anonims removes for child %s"%child, level = ui.DEBUG)
100        #remove childs that have no parent not_parent
101        self.anonim_mark_safe_childs(not_parent, not_parent)
102        #now remove all nonmarked childs
103        self.clean_revs_after(not_parent)
104
105    def anonim_check_start(self, rev_id):
106        ui.status("anonim check for %s"%rev_id, level = ui.PARSE)
107        #if IsDebug():
108        #    print "hg log is %s"%self.hg_tree
109        node = self.hg_tree[rev_id]
110        if len(node.childs) > 1:
111            #check that only one parent of all are in revs
112            childs_in_branch = None
113            for child in node.childs:
114                childid = strip_hg_revid(child)
115                if childid in self.revs:
116                    if childs_in_branch is None:
117                        childs_in_branch = childid
118                    else:
119                        if self.can_merge_anonims :
120                            #remove alternate anonimous branch from tree if uses soft merge-push
121                            self.anonim_remove(childid, childs_in_branch)
122                        else:
123                            ui.status("anonimous branch parent detected on hg:%s so stop on it"%rev_id)
124                            self.clean_revs_after(rev_id)
125                            #raise HgSVNError("anonimous branch parent detected on hg:%s" % rev_id)
126
127    def clean_anonims(self, can_merge_anonims):
128        if IsDebug():
129            print_("clean hystory tree:")
130            for node in self.hg_tree.items():
131                print_(node)
132            print_("hystory set:")
133            for node in self.revs:
134                print_(node)
135
136        self.can_merge_anonims = can_merge_anonims
137        #now check tree for anonimous branches
138        for rev_id in self.revs:
139            self.anonim_check_start( rev_id );
140
141
142def get_hg_revs_with_graph(first_rev, svn_branch, last_rev="tip", opts = None ):
143    """
144    Get a chronological list of revisions (changeset IDs) between the two
145    revisions (included).
146    tree contains nodes description as [parents, childs]
147    """
148    args = ["log", "--template", r'{rev}:{node|short};{parents};{children}\n', "-b", svn_branch,
149            '--limit', '1000',
150            "-r", "%s::%s" % (strip_hg_rev(first_rev),
151                             strip_hg_rev(last_rev))]
152    out = run_hg(args)
153    lines = [s.strip() for s in out.splitlines()]
154    revs = list()
155    hg_tree = dict()
156    parents = []
157    childs = []
158
159    can_merge_anonims = False
160    if opts:
161        can_merge_anonims = opts.force or (opts.merge_branches=="skip") or (opts.merge_branches=="trypush")
162
163    for line in lines:
164        (rev, parent, child) = re.split(";",line)
165        rev_no, rev_id = re.split(":", rev)
166        revs.append(rev_id) #strip_hg_rev(rev));
167        node = hglog_node()
168        node.parents = re.split(" ",parent.strip());
169        node.childs = re.split(" ",child.strip());
170        node.rev_no = rev_no
171        hg_tree[rev_id] = node
172
173    tree = hglog_tree(hg_tree, revs)
174    tree.clean_anonims(can_merge_anonims)
175    tree.close()
176
177    if IsDebug():
178        print_("hg have hystory tree:")
179        for node in hg_tree.items():
180            print_(node)
181    return revs, hg_tree
182
183def is_anonimous_branch_head(rev, hg_tree):
184    """
185    checks chat revision have more than one child in branch
186    """
187    if not (rev in hg_tree):
188        return False
189    childs = hg_tree[rev].childs
190    if len(childs) <= 1:
191        return False
192    ChildsInTree = 0
193    for s in childs:
194        if strip_hg_rev(s) in hg_tree:
195            ChildsInTree = ChildsInTree+1
196    return (ChildsInTree > 1)
197
198def get_pairs(L):
199    """
200    Given a list, return a list of consecutive pairs of values.
201    """
202    return [(L[i], L[i+1]) for i in range(len(L) - 1)]
203
204def get_hg_changes(start_rev, end_rev):
205    """
206    Get paths of changed files from a previous revision.
207    Returns a tuple of (added files, removed files, modified files, copied files)
208    Copied files are dict of (dest=>src), others are lists.
209    """
210    if IsDebug():
211        ui.status("get_hg_changes for hg: %s::%s"%(start_rev, end_rev))
212    args = ["st", "-armC"]
213    if start_rev != end_rev:
214        rev_string = "%s::%s"%(start_rev, end_rev)
215        args += ["--rev", rev_string]
216    else:
217        args += ["--change", end_rev]
218    out = run_hg(args, output_is_locale_encoding=True)
219    added = []
220    removed = []
221    modified = []
222    copied = {}
223    skipnext = False
224    for line in out.splitlines():
225        st = line[0]
226        path = line[2:]
227        if (is_ignore4svn(path)): continue
228        if st == 'A':
229            added.append(path)
230            lastadded=path
231        elif st == 'R':
232            removed.append(path)
233        elif st == 'M':
234            modified.append(path)
235        elif st == ' ':
236            added.remove(lastadded)
237            copied[lastadded] = path
238    #print "added:", added
239    #print "removed:", removed
240    #print "modified:", modified
241    return added, removed, modified, copied
242
243def cleanup_list(target, drops):
244    for x in drops:
245        if x in target:
246            target.remove(x)
247
248def cleanup_svn_type(files, svn_status=None, state='unversioned'):
249    """
250        removes svn:<state> entry from files, status=<None> - removes all recognised in svn entries
251    """
252    if svn_status is None:
253        svn_status = get_svn_status(".")
254    stated = list()
255    for entry in svn_status:
256        if (entry['path'] in files):
257          if (state is None) or (entry['type'] == state):
258            files.remove(entry['path'])
259            stated.append(entry['path'])
260    return files, stated
261
262def cleanup_svn_status(files, svn_status=None, state='unversioned'):
263    """
264        removes svn:<state> entry from files, status=<None> - removes all recognised in svn entries
265    """
266    if svn_status is None:
267        svn_status = get_svn_status(".")
268    stated = list()
269    for entry in svn_status:
270        if (entry['path'] in files):
271          if (state is None) or (entry['status'] == state):
272            files.remove(entry['path'])
273            stated.append(entry['path'])
274    return files, stated
275
276def cleanup_svn_notatype(files, svn_status=None, state='unversioned'):
277    """
278        leaves only svn:<state> entry in files, status=<None> - leaves all recognised in svn entries
279    """
280    if svn_status is None:
281        svn_status = get_svn_status(".")
282    nostatedfile = list()
283    statedfile = list()
284    for entry in svn_status:
285        if (entry['path'] in files):
286          files.remove(entry['path'])
287          if (state is None) or (entry['type'] == state):
288            statedfile.append(entry['path'])
289          else:
290            nostatedfile.append(entry['path'])
291    nostatedfile.extend(files)
292    del files[:]
293    files.extend(statedfile)
294    return files, nostatedfile
295
296def cleanup_svn_versioned(files, svn_status=None):
297    files, verfiles = cleanup_svn_notatype(files, svn_status, 'unversioned')
298    verfiles, absentfiles = cleanup_svn_notatype(verfiles, svn_status, None)
299    files.extend(absentfiles)
300    return files, verfiles
301
302def cleanup_svn_unversioned(files, use_svn_status=None):
303    files, unverfiles = cleanup_svn_type(files, use_svn_status, 'unversioned')
304    files, absentfiles = cleanup_svn_notatype(files, use_svn_status, None)
305    unverfiles.extend(absentfiles)
306    return files, unverfiles
307
308def svn_unversioned(files, svn_status = None):
309    if svn_status is None:
310        svn_status = get_svn_status(".")
311    res = list([])
312    for entry in svn_status:
313        if(entry['type'] == 'unversioned') and entry['path'] in files:
314            res.append(entry['path'])
315    return res
316
317def get_ordered_dirs(l):
318    if l is None :
319        return []
320    """
321    Given a list of relative file paths, return an ordered list of dirs such that
322    creating those dirs creates the directory structure necessary to hold those files.
323    """
324    dirs = set()
325    for path in l:
326        while True:
327            if not path :
328                break
329            path = os.path.dirname(path)
330            if not path or path in dirs:
331                break
332            dirs.add(path)
333    return list(sorted(dirs))
334
335def get_hg_csets_description(start_rev, end_rev):
336    """Get description of a given changeset range."""
337    return run_hg(["log", "--template", r"{desc|strip}\n", "--follow"
338                    , "--branch", svn_branch
339                    , "--rev", start_rev+":"+end_rev, "--prune", start_rev])
340
341def get_hg_cset_description(rev_string):
342    """Get description of a given changeset."""
343    return run_hg(["log", "--template", "{desc|strip}", "-r", rev_string])
344
345def get_hg_log(start_rev, end_rev):
346    """Get log messages for given changeset range."""
347
348    log_args=["log", "--verbose", "--follow", "--rev", start_rev+"::"+end_rev, "--prune", start_rev]
349    return run_hg(log_args)
350
351def get_svn_subdirs(top_dir):
352    """
353    Given the top directory of a working copy, get the list of subdirectories
354    (relative to the top directory) tracked by SVN.
355    """
356    subdirs = []
357    def _walk_subdir(d):
358        svn_dir = os.path.join(top_dir, d, ".svn")
359        if os.path.exists(svn_dir) and os.path.isdir(svn_dir):
360            subdirs.append(d)
361            for f in os.listdir(os.path.join(top_dir, d)):
362                d2 = os.path.join(d, f)
363                if f != ".svn" and os.path.isdir(os.path.join(top_dir, d2)):
364                    _walk_subdir(d2)
365    _walk_subdir(".")
366    return subdirs
367
368def strip_nested_removes(targets):
369    """Strip files within removed folders and return a cleaned up list."""
370    # We're going a safe way here: "svn info" fails on items within removed
371    # dirs.
372    # TODO - it is very slooow design, maybe more optimal strategy can be?
373    clean_targets = []
374    for target in targets:
375        try:
376            run_svn(['info', '--xml', target], mask_atsign=True)
377        except ExternalCommandFailed as err:
378            ui.status(str(err), level=ui.DEBUG)
379            continue
380        clean_targets.append(target)
381    return clean_targets
382
383def adjust_executable_property(files, svn_status = None):
384    execflags = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
385    svn_map = {}
386    check_files = files
387
388    if svn_status is not None:
389        #if have status cached, then use it to filter only files that have props
390        check_files = []
391        for entry in svn_status:
392            if (entry['path'] in files):
393                if (entry['props'] != "none"):
394                    check_files.append(entry['path'])
395        if IsDebug():
396            print_("adjust_executable_property: select files with props:%s" % check_files)
397
398    for fname in check_files:
399        if svn_get_prop('svn:executable', fname).strip():
400            svn_map[fname] = True
401        else:
402            svn_map[fname] = False
403    for fname in check_files:
404        m = os.stat(fname).st_mode & 0o777
405        is_exec = bool(m & execflags)
406        actual_exec = fname in svn_map;
407        if actual_exec:
408              actual_exec = svn_map[fname];
409        if is_exec and not actual_exec:
410            run_svn(['propset', 'svn:executable', 'ON', fname],
411                    mask_atsign=True)
412        elif not is_exec and actual_exec:
413            run_svn(['propdel', 'svn:executable', fname], mask_atsign=True)
414
415def do_svn_copy(src, dest, alredy_added):
416    """
417    Call Svn copy command to record copying file src to file dest.
418    If src is present then backup src before other tasks.
419    Before issuing copy command move dest to src and remove src after
420    """
421    backup_src = ''
422    added = list()
423    if os.path.exists(src):
424        backup_src = os.path.join(hgsvn_private_dir, "hgpushsvn_backup.tmp")
425        os.rename(src, backup_src)
426
427    try:
428        try:
429            # We assume that src is cotrolled by svn
430            os.rename(dest, src)
431
432            # Create svn subdirectories if needed
433            # We know that subdirectories themselves are present
434            # as dest is present
435            pattern = re.compile(r"[/\\]")
436            pos = 0
437            while(True):
438                match = pattern.search(dest, pos + 1)
439                if match == None:
440                    break
441
442                pos = match.start()
443                path = dest[:pos]
444                try:
445                    if not (path in alredy_added):
446                        run_svn(['add', '--depth=empty'], [path], mask_atsign=True)
447                        alredy_added.append(path)
448                        added.append(path)
449                except (ExternalCommandFailed) as e:
450                    if not str(e).find("is already under version control") > 0:
451                        raise
452                pos = match.end() - 1
453
454            # Do the copy itself
455            run_svn(['copy', src, dest], mask_atsign=True)
456        except ExternalCommandFailed:
457            # Revert rename
458            os.rename(src, dest)
459            raise
460    finally:
461        if os.path.isfile(src):
462            os.remove(src)
463
464        if(backup_src):
465            os.rename(backup_src, src)
466    return added
467
468def hg_push_svn(start_rev, end_rev, edit, username, password, cache, options, use_svn_wc = False):
469    """
470    Commit the changes between two hg revisions into the SVN repository.
471    Returns the SVN revision object, or None if nothing needed checking in.
472    use_svn_wc - prevents reverting wc state to start_rev, and thus accepts changes that
473                prepared here
474    """
475
476    # Before replicating changes revert directory to previous state...
477    run_hg(['revert', '--all', '--no-backup', '-r', end_rev])
478
479    if not use_svn_wc:
480        # ... and restore .svn directories, if we lost some of them due to removes
481        run_svn(['revert', '--recursive', '.'])
482
483    # Do the rest over up-to-date working copy
484    # Issue 94 - moved this line to prevent do_svn_copy crashing when its not the first changeset
485    run_hg(["up", "-C", "-r", end_rev])
486
487    added, removed, modified, copied = get_hg_changes(start_rev, end_rev)
488
489    svn_status_cache = get_svn_status(".")
490
491    added, modified_add = cleanup_svn_status(added, svn_status_cache, 'modified')
492    added, alredy_add = cleanup_svn_status(added, svn_status_cache, 'normal')
493    cleanup_svn_unversioned(removed, svn_status_cache)
494
495    modified, unversioned_change  = cleanup_svn_unversioned(modified, svn_status_cache)
496    if unversioned_change:
497        print_("this changes %s unverioned in svn" % unversioned_change)
498        if options.on_noversioned_change == "add" :
499            added.extend(unversioned_change)
500        elif options.on_noversioned_change == "skip":
501            unversioned_change = None   #this is dummy - for skip choose nothig to do
502        else:
503            raise HgSVNError("unckonwn action %s for unversioned changes" % options.on_noversioned_change)
504
505    if modified_add:
506        ui.status("this added files alredy versioned in svn:%s"
507                  " just state it as modify" % modified_add)
508        modified.extend(modified_add)
509
510    cleanup_svn_versioned(added, svn_status_cache)  #drop alredy versioned nodes from addition
511
512    if alredy_add:
513        ui.status("this added %s alredy verioned in svn" % alredy_add)
514        modified.extend(alredy_add)    #and place them to modified
515
516    # Record copyies into svn
517    for dest, src in six.iteritems(copied):
518        copy_added = do_svn_copy(src,dest, alredy_add)
519        modified.extend(copy_added)
520
521    # Add new files and dirs
522    if added:
523        ui.status("this added %s alredy verioned in svn" % alredy_add)
524        svnadd = list(added)
525        cleanup_svn_status(svnadd, svn_status_cache, 'added') #not add alredy added filess
526        #cleanup_list(svnadd, alredy_add)
527        if svnadd:
528            bulk_args = cleanup_svn_versioned(get_ordered_dirs(svnadd), svn_status_cache)[0]
529            bulk_args += svnadd
530            cleanup_list(bulk_args, alredy_add)
531            if len(bulk_args) > 0:
532                run_svn(["add", "--depth=empty"], bulk_args, mask_atsign=True)
533
534    if IsDebug():
535        print_("svn to add:", added)
536        print_("svn to remov:", removed)
537        print_("svn to modify:", modified)
538
539    # Remove old files and empty dirs
540    if removed:
541        svnremove = list(removed)
542        cleanup_svn_type(svnremove, svn_status_cache, 'removed') #drop alredy removed nodes from removing
543        empty_dirs = [d for d in reversed(get_ordered_dirs(svnremove))
544                      if not run_hg(["st", "-c", "%s" %d])]
545        # When running "svn rm" all files within removed folders needs to
546        # be removed from "removed". Otherwise "svn rm" will fail. For example
547        # instead of "svn rm foo/bar foo" it should be "svn rm foo".
548        # See issue15.
549        svn_removed = strip_nested_removes(svnremove + empty_dirs)
550        if svn_removed:
551            run_svn(["rm"], svn_removed, mask_atsign=True)
552    if added or removed or modified or copied:
553        svn_sep_line = "--This line, and those below, will be ignored--"
554        adjust_executable_property(added+modified, svn_status_cache)  # issue24
555        description = get_hg_csets_description(start_rev, end_rev)
556        if (ui._level >= ui.PARSE):
557            ui.status("use svn commit message:\n%s"%description, level=ui.PARSE);
558        fname = os.path.join(hgsvn_private_dir, 'commit-%s.txt' % end_rev)
559        lines = description.splitlines()+[""]
560        lines.append(svn_sep_line)
561        lines.append("To cancel commit just delete text in top message part")
562        lines.append("")
563        lines.append("Changes to be committed into svn:")
564        for item in svn_status_cache:
565            if item['type'] != 'unversioned':
566                lines.append( "%s       %s" % (item['type'], item['path']))
567        lines.append("")
568        lines.append(("These changes are produced from the following "
569                      "Hg changesets:"))
570        lines.extend(get_hg_log(start_rev, end_rev).splitlines())
571        f = codecs.open(fname, "wb", "utf-8")
572        f.write(os.linesep.join(lines))
573        f.close()
574
575        try:
576            if edit:
577                editor=(os.environ.get("HGEDITOR") or
578                        os.environ.get("SVNEDITOR") or
579                        os.environ.get("VISUAL") or
580                        os.environ.get("EDITOR", "vi"))
581
582                rc = os.system("%s \"%s\"" % (editor, fname) )
583                if(rc):
584                    raise ExternalCommandFailed("Can't launch editor")
585
586                empty = True
587
588                f=open(fname, "r")
589                for line in f:
590                    if(line == svn_sep_line):
591                        break
592
593                    if(line.strip() != ""):
594                        empty = False
595                        break
596                f.close()
597
598                if(empty):
599                    raise EmptySVNLog("Commit cancelled by user\n")
600
601            svn_rev = None
602            svn_commands = ["commit", "--encoding", "utf-8", "-F", fname ]#get_encoding()]
603            if username is not None:
604                svn_commands += ["--username", username]
605            if password is not None:
606                svn_commands += ["--password", password]
607            if cache:
608                svn_commands.append("--no-auth-cache")
609            out = run_svn(svn_commands)
610
611            if IsDebug():
612                print_("svn commit:%s" % out)
613
614            svn_rev = None
615            try:
616              outlines = out.splitlines(True)
617              outlines.reverse()
618              for binline in outlines:
619                line = svn_decode(binline);
620                # one of the last lines holds the revision.
621                # The previous approach to set LC_ALL to C and search
622                # for "Committed revision XXX" doesn't work since
623                # svn is unable to convert filenames containing special
624                # chars:
625                # http://svnbook.red-bean.com/en/1.2/svn.advanced.l10n.html
626                match = re.search("(\d+)", line)
627                if match:
628                    svn_rev = int(match.group(1))
629                    break
630            finally:
631              if svn_rev is None:
632                #if retrieve revision from commit report fails? do it with additional svn request
633                svn_info = get_svn_info(".")
634                svn_rev = svn_info["revision"]
635
636
637            return svn_rev
638        finally:
639            # Exceptions are handled by main().
640            if (ui._level < ui.PARSE):
641                os.remove(fname)
642        return None
643    else:
644        ui.status("*", "svn: nothing to do")
645        return -1
646
647def svn_merge_proof(args):
648    try:
649        return run_svn(args)
650    except (RunCommandError) as e:
651        if e.err_have("Cannot merge into mixed-revision"):
652            # on this error try to update to current svnrev, with potential externals,
653            #   or others mixed-versions
654            svn_info        = get_svn_info(".")
655            svn_current_rev = svn_info["revision"] #last_changed_rev
656            #run_svn(['up', '-r', svn_current_rev]);
657            svn_switch_to(svn_info["url"], 'HEAD', clean=False, ignore_ancetry=True, propagate_rev=True)
658            return run_svn(args)
659        else:
660            raise
661
662def svn_merge_2wc(rev_number, svn_base, svnacception = "working"):
663    args = ["merge","--non-interactive","--accept=%s"%svnacception]
664    if rev_number is not None:
665        svn_base += '@%d'%rev_number
666    ui.status("merging %s" % (svn_base), level=ui.VERBOSE)
667    svn_base = svn_base.lstrip('/\\');
668    args += [repos_url +'/'+svn_base]
669    return svn_merge_proof(args)
670
671def svn_merge_range_2wc(svn_base, rev_first, rev_last, svnacception = "working"):
672    args = ["merge","--non-interactive","--accept=%s"%svnacception]
673    svn_base = svn_base.lstrip('/\\');
674    svn_target = svn_base;
675    svn_range = '%d:%d'%(rev_first, rev_last)
676    ui.status("merging %s with %s" % (svn_target, svn_range), level=ui.VERBOSE)
677    try:
678        result = svn_merge_proof(args + ["-r", svn_range , repos_url +'/'+svn_target])
679    except:
680        ui.status("failed merge revrange, so try merge different tree " % (svn_target, svn_range), level=ui.ALERT)
681        svn_revert_all();
682        svn_first = '%s@%d'%(svn_base, rev_first);
683        svn_last  = '%s@%d'%(svn_base, rev_last);
684        ui.status("merging %s : %s" % (svn_first, svn_last), level=ui.VERBOSE)
685        args += ["--ignore-ancestry"]
686        result = svn_merge_proof(args + [repos_url +'/'+svn_first, repos_url +'/'+svn_last])
687    return result
688
689def hg_sync_merge_svn(start_rev, rev, hg_tree, target_hgbranch, options):
690    #   start_rev - parent rev on wich merges rev
691    #   rev - mergepoint rev
692    #   check all parents for branch name, and try to push one branches if can
693    #   after all branches complete, merge svn work copy and resolves it for hg_push_svn
694
695    # Before replicating changes revert directory to previous state...
696    run_hg(['revert', '--all', '--no-backup', '-r', start_rev])
697    # ... and restore .svn directories, if we lost some of them due to removes
698    run_svn(['revert', '--recursive', '.'])
699
700    parents = hg_tree[rev].parents
701    if len(parents) <= 1:
702        return True
703
704    if IsDebug():
705        print_("merge for parents:%s"%parents)
706    branches = dict()
707
708    for s in parents:
709        r = strip_hg_rev(s)
710        branchname = target_hgbranch
711        BranchSVNRev = None
712        if not (r in hg_tree):
713            branchname, BranchSVNRev = get_branchinfo_from_hg(s)
714
715        headinfo = [s,BranchSVNRev]
716        if branchname in branches:
717            branches[branchname].extend(headinfo)
718        else:
719            branches[branchname] = [headinfo]
720    if IsDebug():
721        print_("merge have branches:")
722        for node in branches.items():
723            print_(node)
724
725    for bp in branches.values():
726        #check that all parent is one from its branch
727        if len(bp) > 1:
728            ui.status("Mercurial repository rev:%s have multiple parents %s of branch %s,"
729                      " cant distinguish how to push it" % (s, bp, target_hgbranch)
730                     )
731            return False
732
733    if target_hgbranch in branches:
734        del branches[target_hgbranch]
735
736    branch_affected =False
737    merge_svn_revs = []
738    branch_switched = False
739    current_branch = target_hgbranch
740    for node in branches.items():
741        use_branch = node[0]
742        head = node[1][0]
743        hg_parent = head[0]
744        svn_parent = head[1]
745        svn_base = svn_base_of_branch(use_branch)
746        if svn_base is None:
747                ui.status("rev:%s need push for parent %s of branch %s, that is uncknown location in svn" % (rev, hg_parent, use_branch ))
748                return False
749        if svn_parent is None:
750            #there is no svn tag attached
751            # this is unversioned head, so its branch should be pushed for one rev
752            ui.status("rev:%s need push for parent %s of branch hg:%s svn:%s" % (rev, hg_parent, use_branch, svn_base ))
753            try:
754                current_branch = use_branch
755                if hg_push_branch(options, hg_parent, use_branch, svn_base) != 0:
756                    raise HgSVNError("failed to push branch <%s>" % use_branch)
757                svn_parent = get_svn_rev_from_hg(strip_hg_rev(hg_parent))
758                merge_svn_revs.append([svn_parent, hg_parent, use_branch, svn_base])
759            except:
760                if not options.force:
761                    raise HgSVNError("failed to push branch <%s>" % use_branch)
762        else:
763            merge_svn_revs.append([svn_parent, hg_parent, use_branch, svn_base])
764
765
766    if current_branch != target_hgbranch: #branch_switched:
767        ui.status("switch from branch %s back to %s " % (current_branch, target_hgbranch))
768        svn_base = svn_base_of_branch(target_hgbranch)
769        svn_switch_to(repos_url +'/'+svn_base, clean=True, ignore_ancetry=True)
770        hg_force_switch_branch(target_hgbranch, start_rev)
771
772    if len(merge_svn_revs) == 0:
773        return options.force
774
775    #now all parents is ok, lets merge it
776    #do the merge in svn rev order
777    if len(merge_svn_revs) > 1:
778        ui.status("sorting list %s"% merge_svn_revs)
779        merge_svn_revs.sort(key=lambda rev_node: rev_node[0])
780    for elem in merge_svn_revs:
781        try:
782            svn_merge_2wc(elem[0], elem[3], svnacception = options.svnacception)
783        except:
784            #since svn normal merge fails? try to merge revs range
785            #try to find last common incoming rev
786            ui.status("fail to merge svn:%s"%(elem[0]), loglevel = ui.ALERT);
787
788            args = ["log", "--template", r'{rev}:{node|short}\n',
789                    "-r", "last(ancestors(%s) and branchpoint() and branch(\"%s\"))" % (strip_hg_rev(start_rev), elem[2])
790                    ]
791            last_branch_incoming_rev = run_hg(args)
792            if len(last_branch_incoming_rev) <= 0 :
793                # there is no merge-in from desired branch, therefore merge from it`s base rev
794                args = ["log", "--template", r'{rev}:{node|short}\n',
795                        "-r", "first(branch(%s))" % (elem[2])
796                        ]
797                last_branch_incoming_rev = run_hg(args)
798                if len(last_branch_incoming_rev) <= 0 :
799                    return options.force
800
801            ui.status("try merge revs range from hg:%s"%(last_branch_incoming_rev), level=ui.VERBOSE);
802            last_branch_incoming_svnrev = get_svn_rev_from_hg( get_hg_cset(last_branch_incoming_rev) );
803            if (last_branch_incoming_svnrev is None):
804                ui.status("hg:%s not synched 2 svn"%(last_branch_incoming_rev), level=ui.WARNING);
805                return options.force
806
807            ui.status("try merge revs range from svn:%s to svn:%s"%(last_branch_incoming_svnrev, elem[0]), level=ui.VERBOSE);
808            svn_merge_range_2wc(elem[3], last_branch_incoming_svnrev, elem[0], svnacception = options.svnacception)
809
810    #now resolve all conflicts
811    svn_resolve_all()
812    return True
813
814def autostart_branch(hg_branch, options):
815    ui.status("svn_branch of hg:%s absent. so try to tag it from branch origin"%hg_branch, level=ui.VERBOSE)
816    svn_sync = get_svn_origin_of_hgbranch(hg_branch)
817    ui.status("at sync:%s"%svn_sync, level=ui.DEBUG)
818    svn_branch = svn_base_of_branch(hg_branch)
819    if (not (svn_sync.svnrev is None)) or (svn_sync.hgrev is None):
820        ui.status("svn %s is confused with hg %s. cant determine what to do, try to define valid svn.NNN tags according to svn-branch"%(svn_branch, hg_branch), level=ui.ERROR )
821        raise HgSVNError("cant autostart branch %s"%hg_branch)
822    orig_hgrev = strip_hg_rev(svn_sync.hgrev)
823
824    if not (svn_sync.svn_base is None):
825        ui.status("looks svn have alredy branch %s@%s created, try sync it`s base"%(svn_branch, svn_base) , level = ui.NOTE)
826        map_svn_rev_to_hg(svn_sync.svn_base, orig_hgrev, local=True)
827        svn_switch_to(repos_url+svn_branch+"@%s"%svn_sync.svn_base, ignore_ancetry=True, clean=True)
828        return 0
829
830    orig_branch = get_branch_from_hg(orig_hgrev)
831    svn_rev = svn_sync.svn_src
832    if (svn_sync.svn_src is None):
833        ui.status("looks even origin of hg:%s absent, try to autostart ones origin %s@%s"%(hg_branch, orig_branch, orig_hgrev), level=ui.NOTE)
834        hg_push_branch(options, orig_hgrev, orig_branch)
835        svn_rev = get_svn_rev_from_hg(orig_hgrev)
836
837    orig_svn_branch = svn_base_of_branch(orig_branch)
838
839    ui.status("reaches origin hg:%s, now try to tag it from svn:%s@%s"%(hg_branch, orig_svn_branch, svn_rev), level=ui.WARNING )
840    automessage = '"start hg:%s here. [hg:%s@%s]"'%(hg_branch, orig_branch, orig_hgrev)
841    run_svn(['copy', '--parents', '-m', automessage, "%s@%s"%(repos_url+orig_svn_branch, svn_rev), repos_url+svn_branch])
842    svn_origin = svn_branch_revset()
843    svn_origin.eval_path('^'+svn_branch)
844    if (svn_origin.baserev == svn_origin.headrev) and (svn_origin.srcrev == svn_rev) and (svn_origin.source == orig_svn_branch):
845        map_svn_rev_to_hg(svn_origin.baserev, orig_hgrev, local=True)
846        svn_switch_to(repos_url+svn_branch, ignore_ancetry=True, clean=True)
847    else:
848        ui.status("at sync:%s"%svn_origin, level=ui.DEBUG)
849        ui.status("failed to make sync hg:%s at svn %s , orinated from hg:%s@%s"%(hg_branch, svn_branch, orig_branch, orig_hgrev), level=ui.ERROR)
850        return -1
851    return 0
852
853def hg_push_branch(options, hg_parent, use_branch = None, svn_base = None):
854            # hg_parent - hgrevision of requred branch
855            # this is unversioned head, so its branch should be pushed for one rev
856            BranchSVNRev = None
857            if use_branch is None:
858                branchname, BranchSVNRev = get_branchinfo_from_hg(hg_parent)
859                use_branch = branchname
860
861            if svn_base is None:
862                svn_base = svn_base_of_branch(use_branch)
863
864            if not (BranchSVNRev is None):
865                ui.status("hg:%s alredy synced at svn.%s"%(hg_parent, BranchSVNRev))
866                svn_switch_to(repos_url +svn_base+"@%s"%BranchSVNRev, ignore_ancetry=True, clean=True)
867                return 0
868
869            ui.status("need push of branch hg:%s rev %s  - svn:%s" % (use_branch, hg_parent, svn_base ))
870            current_branch = use_branch
871            branch_switched = True
872            #first switch svn to desired branch. after that, switch hg to ones - this provides clean hg state
873            #   that is requred by real_main
874            try:
875                svn_switch_to(repos_url +svn_base, ignore_ancetry=True) #clean=True
876            except (RunCommandError, ExternalCommandFailed) as e:
877                if (isinstance(e, RunCommandError)):
878                    ui.status("cmd:%s"%e.cmd_string, level = ui.ERROR)
879                    ui.status("ret:%s"%e.returncode, level = ui.ERROR)
880                    ui.status("err:%s"%e.err_msg, level = ui.ERROR)
881                    ui.status("out:%s"%e.out_msg, level = ui.ERROR)
882                else:
883                    ui.status("err:%s"%str(e), level = ui.ERROR)
884                if e.err_have("E200009") or e.err_have("E160013") or e.err_have("path not found") or e.err_have("does not appear to be a URL"):
885                    if (not options.auto_branch):
886                        ui.status("looks that branch %s absent, so try to create it and start, and run again"%svn_base, level = ui.ERROR)
887                        raise e
888                    if autostart_branch(use_branch, options) < 0:
889                        raise HgSVNError("cant autostart branch %s"%use_branch)
890                else:
891                    raise e
892            except Exception as e:
893                ui.status("err uncknown:%s"%str(e), level = ui.ERROR)
894                raise e
895
896            ui.status("hg switch follow svn")
897            try:
898                if not hg_force_switch_branch(use_branch, strip_hg_rev(hg_parent) ):
899                    if not options.force:
900                        raise HgSVNError("failed to switch to branch <%s> for push" % use_branch)
901                use_options = copy.deepcopy(options)
902                use_options.svn_branch = use_branch
903                use_options.tip_rev = hg_parent
904                use_options.prefere2hg = True
905                res = real_main(use_options, [], clean=False )
906            except Exception as e:
907                ui.status("err uncknown:%s"%str(e), level = ui.ERROR)
908                raise e
909            return res
910
911S_OK                    = 0
912ERR_HGSTATUS_BAD        = 1
913ERR_ANONIMOUS_BRACHES   = 2
914ERR_MERGE_FAIL          = 3
915ERR_MERGE_DISABLED      = 4
916ERR_NOT_OK_SOME         = 5
917ERR_BRANCH_AUTOSTART    = 6
918ERR_SYNC_LOST           = 7
919
920def real_main(options, args, clean = True):
921    global repos_url
922
923    ui.status("start push branch %s to @%s"%(options.svn_branch, options.tip_rev), level=ui.DEBUG)
924    if run_hg(["st", "-S", "-m"]):
925        ui.status("There are uncommitted changes possibly in subrepos. Either commit them or put "
926               "them aside before running hgpushsvn.")
927        return ERR_HGSTATUS_BAD
928    if check_for_applied_patches():
929        ui.status("There are applied mq patches. Put them aside before running hgpushsvn.")
930        return ERR_HGSTATUS_BAD
931
932    # through_hgrev preserves rev of hystory tree through that will push. for option prefere2hg this will use current hg-revision
933    through_hgrev = None
934    orig_branch = run_hg(["branch"]).strip()
935
936    is_at_pull_point = False;
937    while True:
938        svn_info = get_svn_info(".")
939        # in last_changed_rev - rev of last modification of current branch!
940        svn_current_rev = svn_info["revision"] #last_changed_rev
941        # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted'
942        repos_url = svn_info["repos_url"]
943        # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted/branches/xmpp-subprotocols-2178-2'
944        wc_url = svn_info["url"]
945        assert wc_url.startswith(repos_url)
946        # e.g. u'/branches/xmpp-subprotocols-2178-2'
947        wc_base = wc_url[len(repos_url):]
948
949        svn_branch = wc_url.split("/")[-1]
950
951        svn_branch = check_branchmap(svn_branch, wc_base, options)
952
953        svn_base = svn_base_of_branch(orig_branch)
954
955        if is_at_pull_point:
956            break
957        is_at_pull_point = True;
958
959        if options.prefere2hg: # and not options.tip_rev
960            # Prepare and/or switch named branches
961            if hg_is_clean(orig_branch) or svn_is_clean(wc_base):
962                sync_point =get_svn_rev_of_hgbranch(orig_branch)
963                svn_syncrev = sync_point.svnrev
964                hg_syncrev  = sync_point.hgrev
965                hg_ok = not (hg_syncrev is None)
966                svn_ok = not (svn_syncrev is None)
967                synck_ok = hg_ok and svn_ok
968                if synck_ok and same_hg_rev(hg_syncrev, options.tip_rev):
969                    #this is lucky day
970                    ui.status("requred hgrev:%s alredy synced to svn:%s .. done"%(hg_syncrev, svn_syncrev))
971                    return 0;
972
973                # through_hgrev preserves rev of hystory tree through that will push. for option prefere2hg this will use current hg-revision
974                #if (options.tip_rev is None) or (len(options.tip_rev) == 0):
975                through_hgrev = get_hg_current_cset()
976
977                if IsDebug():
978                    ui.status('at sync point:%s'%sync_point)
979                if orig_branch != svn_branch:
980                    # Update to or create the "pristine" branch
981                    if IsDebug():
982                        ui.status('differ branch from hg:%s svn:%s@%s'%(orig_branch, svn_branch, str(svn_syncrev)), level=ui.DEBUG)
983                    if svn_ok:
984                        ui.status("svn follow hg(%s-%s) to %s@%d",orig_branch, hg_syncrev, svn_base, svn_syncrev);
985                        svn_switch_to(repos_url+svn_base, svn_syncrev, clean=False, ignore_ancetry=True, propagate_rev=True)
986                        continue;
987                    elif not hg_ok:
988                        ui.status("cant find sync origin. last checked branch %s"%orig_branch, level=ui.ERROR);
989                        return ERR_SYNC_LOST
990                elif (svn_current_rev != svn_syncrev) :
991                    if IsDebug():
992                        ui.status('differ sync rev current:%s synched:%s'%(str(svn_current_rev), str(svn_syncrev)), level=ui.DEBUG)
993                    if svn_ok:
994                        args = ["up", "--ignore-externals"]
995                        if get_svn_client_version() >= (1, 5):
996                            args.extend(['--accept', 'working'])
997                        ui.status('Attempting to update to last hg-synched revision %s...', str(svn_syncrev))
998                        run_svn(args + ["-r", svn_syncrev, '.']);
999                        continue;
1000                    elif not hg_ok:
1001                        # look like this hg-branch is not pushed, so go svn to branch-parent point, and try to push from it
1002                        ui.status("cant autostart branch %s, switch svn to it`s base and push from that"%orig_branch, level=ui.ERROR);
1003                        return ERR_BRANCH_AUTOSTART
1004                else:
1005                    break
1006                #here is because !svn_ok
1007                if not (sync_point.svn_base or sync_point.svn_src) is None:
1008                        #possibly svn-branch started but not synced with hg
1009                        if check_svn_branch_sync_origin(sync_point, repos_url+svn_base):
1010                            svn_syncrev = sync_point.svnrev
1011                            ui.status("svn follow hg(%s-%s) to %s@%d",orig_branch, hg_syncrev, svn_base, svn_syncrev);
1012                            continue;
1013                        ui.status("cant sync origin to svn. last checked branch %s"%orig_branch, level=ui.ERROR);
1014                        return ERR_SYNC_LOST
1015                if hg_ok:
1016                        from_hgrev = strip_hg_rev(hg_syncrev)
1017                        if int(from_hgrev) > 0:
1018                            res = hg_push_branch(options, hg_syncrev, orig_branch, svn_base)
1019                            if res != 0:
1020                                return res
1021                            continue
1022                        else:
1023                            #from_hgrev <= 0 for empty repository
1024                            if options.auto_import:
1025                                if svn_branch_empty(repos_url+svn_base):
1026                                    ui.status("svn branch %s empty, so import repository here"%svn_base, level=ui.ERROR);
1027                                    svn_switch_to(repos_url+svn_base, "HEAD", clean=False, ignore_ancetry=True, propagate_rev=True)
1028                                    continue;
1029                                else:
1030                                    ui.status("svn branch %s not empty, so can`t import"%svn_base, level=ui.ERROR);
1031                            ui.status("repository is not imported to svn", level=ui.ERROR);
1032                            return ERR_BRANCH_AUTOSTART
1033            else:
1034                ui.status("svn branch:%s not mutch prefered one hg:%s"%(svn_branch, orig_branch), level = ui.WARNING)
1035        break
1036
1037    # Get remote SVN info
1038    svn_greatest_rev = get_svn_info(wc_url)['last_changed_rev']
1039    ui.status("push at svn rev%s (greatest rev%s)"%(svn_current_rev , svn_greatest_rev), level=ui.DEBUG)
1040
1041    if svn_current_rev > svn_greatest_rev :
1042        #up to date svn rev may be far away from last commited rev
1043        svn_current_rev = svn_greatest_rev
1044
1045    if svn_greatest_rev != svn_current_rev and not options.prefere2hg :
1046        # We can't go on if the pristine branch isn't up to date.
1047        # If the pristine branch lacks some revisions from SVN we are not
1048        # able to pull them afterwards.
1049        # For example, if the last SVN revision in out hgsvn repository is
1050        # r100 and the latest SVN revision is r101, hgpushsvn would create
1051        # a tag svn.102 on top of svn.100, but svn.101 isn't in hg history.
1052        ui.status("Branch '%s' out of date. Run 'hgpullsvn' first." % svn_branch)
1053        return ERR_HGSTATUS_BAD
1054
1055    # Switch branches if necessary.
1056    if orig_branch != svn_branch:
1057        if not hg_switch_branch(orig_branch, svn_branch):
1058            return ERR_HGSTATUS_BAD
1059
1060    hg_start_rev = "svn.%d" % svn_current_rev
1061    hg_revs = None
1062    hg_tree = None
1063    #proceed current branch till its head
1064    hg_tip  = strip_hg_rev(options.tip_rev or "")
1065    try:
1066        hg_start_cset = get_hg_cset(hg_start_rev)
1067    except (RuntimeError, RunCommandError):
1068        if not options.auto_import or not svn_branch_empty(repos_url+svn_base):
1069            ui.status("no %s in hg!!! try to proceed from last sync point by switches -l or-f "
1070                      "  or try --autoimport if repository not imported yet"%hg_start_rev
1071                      )
1072            raise
1073        hg_start_cset = get_hg_cset("0")
1074        ui.status( "Warning: revision '%s' not found, forcing to first rev '%s'" % (hg_start_rev, hg_start_cset) )
1075
1076    #hg_start_branch = get_branch_from_hg(hg_start_cset)
1077    if same_hg_rev(hg_start_cset, hg_tip):
1078        #this is lucky day
1079        ui.status("requred hgrev:%s alredy synced to svn:%s .. done"%(hg_start_cset, svn_current_rev))
1080        return 0;
1081
1082    if not options.collapse:
1083            if (through_hgrev is None) or ( int(strip_hg_rev(hg_start_cset)) >= int(strip_hg_rev(through_hgrev)) ):
1084                hg_revs,hg_tree = get_hg_revs_with_graph(hg_start_cset, svn_branch, last_rev=hg_tip, opts = options)
1085            else:
1086                ui.status("use current hg rev %s as pass-though specifier\n", through_hgrev);
1087                if (len(hg_tip) == 0):
1088                    through_rev = through_hgrev
1089                else:
1090                    tip_rev = get_hg_cset(hg_tip)
1091                    through_rev = tip_rev if ( int(strip_hg_rev(tip_rev)) < int(strip_hg_rev(through_hgrev)) ) else through_hgrev
1092                hg_revs,hg_tree = get_hg_revs_with_graph(hg_start_cset, svn_branch, last_rev=through_rev, opts = options)
1093                if len(hg_revs) > 0:
1094                    if IsDebug():
1095                        ui.status("have processing revisions set until pass-through:%s" % hg_revs, level=ui.DEBUG)
1096                    if same_hg_rev(through_rev, hg_revs[len(hg_revs)-1] ) :
1097                        hg_revs_2,hg_tree_2 = get_hg_revs_with_graph(through_rev, svn_branch, last_rev=hg_tip, opts = options)
1098                        if len(hg_revs_2) > 0:
1099                            if IsDebug():
1100                                ui.status("appends processing revisions set:%s" % hg_revs_2, level=ui.DEBUG)
1101                            if same_hg_rev(through_hgrev, hg_revs_2[0] ) :
1102                                if len(hg_revs) > 0:
1103                                    del hg_tree_2[hg_revs_2[0]]
1104                                    del hg_revs_2[0]
1105                                hg_revs = hg_revs + hg_revs_2
1106                                hg_tree.update(hg_tree_2)
1107                                #raise HgSVNError("pass-though detected on hg:%s " % through_hgrev)
1108
1109            if len(hg_revs) <= 0:
1110                ui.status("looks like hg_tip:%s in another branch, and nothing to push in current branch:%s\n", hg_tip, svn_branch);
1111                return S_OK
1112
1113            # if branch started, branch head not enlisted in hg_revs,
1114            # thus use hg_start_cset as start from wich 1st branch revision borns
1115            hg_startid = strip_hg_revid(hg_start_cset)
1116            def check_synched(hg_startid):
1117                if ( get_svn_rev_from_hg(hg_startid) is None) :
1118                    ui.status("start rev %s not synched" %(hg_startid));
1119                    return False
1120                return True
1121            def check_havestart(hg_startid):
1122                if (hg_startid not in hg_revs) :
1123                    if IsDebug():
1124                        print_( "start rev %s not in push set %s" %(hg_startid, hg_revs) )
1125                    return False
1126                return True
1127            if (not check_havestart(hg_startid)) or (not check_synched(hg_startid)):
1128                hg_startrev = strip_hg_rev(hg_start_cset)
1129                start_parents = None
1130                if (int(hg_startrev) != 0):
1131                    start_parents = get_parents_from_hg(hg_revs[0])
1132                elif options.auto_import:
1133                    start_parents = [hg_start_cset]
1134                if IsDebug():
1135                    print_( "start rev parent %s expects at start point %s" %(start_parents, hg_start_cset) )
1136                if ([hg_start_cset] == start_parents) or options.force:
1137                    # hgbranch starts right from svn_curent, so can commit branch head
1138                    hg_revs = [strip_hg_revid(hg_start_cset)] + hg_revs;
1139                else:
1140                    raise HgSVNError("head of branch <%s> is not in svn yet, have to push ones head-branch first" % svn_branch)
1141
1142    if hg_revs is None:
1143        hg_revs = [strip_hg_rev(hg_start_cset), strip_hg_rev(get_hg_cset(hg_tip))]
1144
1145
1146    pushed_svn_revs = []
1147    allok = True;
1148    try:
1149        if options.dryrun:
1150            ui.status( "Outgoing revisions that would be pushed to SVN:" )
1151        try:
1152            if IsDebug():
1153                ui.status("processing revisions set:%s" % hg_revs, level=ui.DEBUG)
1154            last_commited_rev = -1
1155            svn_rev = None
1156            svn_switch_to(wc_url, 'HEAD', clean=False, ignore_ancetry=True, propagate_rev=True)
1157            #run_svn(['up', '--non-interactive', '--accept=working', '--ignore-externals', '-q'])
1158            for (prev_rev, next_rev) in get_pairs(hg_revs):
1159                ui.status("Committing changes up to revision %s", get_hg_cset(next_rev))
1160                if not options.dryrun:
1161                    #if not options.edit:
1162                    #   ???
1163                    username = options.username
1164                    if options.keep_author:
1165                        username = run_hg(["log", "-r", next_rev, "--template", "{author}"])
1166                    if is_anonimous_branch_head(prev_rev, hg_tree):
1167                        ui.status("revision %s is a switch point for multiple anonimous branches"
1168                                  ", cant distingish wich way to up" % prev_rev)
1169                        return ERR_ANONIMOUS_BRACHES
1170
1171                    svn_merged = False
1172                    if next_rev in hg_tree:
1173                        if IsDebug():
1174                            print_( "target rev %s have parents %s" %(next_rev, hg_tree[next_rev].parents) )
1175                        if len(hg_tree[next_rev].parents)>1:
1176                            ui.status("revision %s is a merge point" % next_rev)
1177                            if (options.merge_branches == "break") or (options.merge_branches == ""):
1178                                ui.status("break due to merging not enabled", level=ui.DEFAULT)
1179                                return ERR_MERGE_DISABLED
1180                            if options.merge_branches == "skip":
1181                                ui.status("skip branches of merge")
1182                            else:
1183                              if hg_sync_merge_svn(prev_rev, next_rev, hg_tree, svn_branch, options=options):
1184                                svn_merged = True
1185                                ui.status("ok: revision %s merges fine" % next_rev)
1186                              else:
1187                                if options.merge_branches != "trypush":
1188                                    return ERR_MERGE_FAIL
1189                                ui.status("failed to branches of merge, so normal commit")
1190
1191                    svn_rev = hg_push_svn(prev_rev, next_rev,
1192                                            edit=options.edit,
1193                                            username=username,
1194                                            password=options.password,
1195                                            cache=options.cache,
1196                                            options=options
1197                                            , use_svn_wc = svn_merged
1198                                            )
1199                    if svn_rev:
1200                      if svn_rev >= 0:
1201                        # Issue 95 - added update to prevent add/modify/delete crash
1202                        run_svn(["up", "--ignore-externals"])
1203                        map_svn_rev_to_hg(svn_rev, next_rev, local=True)
1204                        pushed_svn_revs.append(svn_rev)
1205                        last_commited_rev = svn_rev
1206                        #if prev_rev == hg_revs[0]:
1207                        hg_tag_svn_head(branch=svn_branch)
1208                        #activate this head bookmark will not take effect cause we always use update -r rev
1209                        #run_hg(["update", "-r", svn_branch])
1210                else:
1211                    ui.status( run_hg(["log", "-r", next_rev,
1212                              "--template", "{rev}:{node|short} {desc}"]) )
1213            if svn_rev:
1214                if (svn_rev < 0) and (last_commited_rev > 0):
1215                    # Issue 89 - empty changesets are should be market by svn tag or else
1216                    #   witout one it cant by idetnifyed for branch merge parent
1217                    map_svn_rev_to_hg(last_commited_rev, next_rev, local=True, force=True)
1218        except (RunCommandError) as e:
1219            ui.status( "cmd:%s"%(e.cmd_string), level = ui.ERROR )
1220            ui.status( "ret:%s"%(e.returncode), level = ui.ERROR )
1221            ui.status( "err:%s"%(e.err_msg), level = ui.ERROR )
1222            ui.status( "out:%S"%(e.out_msg), level = ui.ERROR )
1223            run_hg(["revert", "--all"])
1224            raise
1225
1226        except Exception as e:
1227            ui.status("exception was:%s"%e, level = ui.ERROR)
1228            print_(traceback.format_exc())
1229            # TODO: Add --no-backup to leave a "clean" repo behind if something
1230            #   fails?
1231            run_hg(["revert", "--all"])
1232            allok = False
1233
1234    except Exception as e:
1235        ui.status("exception was:%s"%e, level = ui.ERROR)
1236        print_(traceback.format_exc())
1237        allok = False
1238
1239    if last_commited_rev > 0:
1240        hg_refresh_svn_head( svn_rev = last_commited_rev )
1241        hg_release_svn_head()
1242
1243    if clean and not IsDebug():
1244        work_branch = orig_branch
1245        if work_branch != svn_branch:
1246            run_hg(["up", "-C", work_branch])
1247            run_hg(["branch", work_branch])
1248
1249    if not allok:
1250        return ERR_NOT_OK_SOME
1251
1252    if pushed_svn_revs:
1253        if len(pushed_svn_revs) == 1:
1254            msg = "Pushed one revision to SVN: "
1255        else:
1256            msg = "Pushed %d revisions to SVN: " % len(pushed_svn_revs)
1257        run_svn(["up", "-r", pushed_svn_revs[-1]])
1258        ui.status("%s %s", msg, ", ".join(str(x) for x in pushed_svn_revs))
1259        try:
1260          for line in run_hg(["st"]).splitlines():
1261            if line.startswith("M"):
1262                ui.status(("Mercurial repository has local changes after "
1263                           "SVN update."))
1264                ui.status(("This may happen with SVN keyword expansions."))
1265                break
1266        except:
1267            ui.status("Cant check repository status")
1268
1269    elif not options.dryrun:
1270        ui.status("Nothing to do.")
1271
1272    return S_OK
1273
1274def on_option_svnignore_add(option, opt, value, parser):
1275    if (opt == "--svnignore-use"):
1276        append_ignore4svn(value)
1277    elif (opt == "--svnignore-save"):
1278        save_svnignores()
1279
1280def main(argv=sys.argv):
1281    # Defined as entry point. Must be callable without arguments.
1282    usage = "usage: %prog [-cf]"
1283    parser = OptionParser(usage)
1284    parser.add_option("-f", "--force", default=False, action="store_true",
1285                      dest="force",
1286                      help="push even if no hg tag found for current SVN rev.")
1287    parser.add_option("-c", "--collapse", default=False, action="store_true",
1288                      dest="collapse",
1289                      help="collapse all hg changesets in a single SVN commit")
1290    parser.add_option("-r", type="string", dest="tip_rev", default=None,
1291                        help="limit push up to specified revision")
1292    parser.add_option("--branch", type="string", dest="svn_branch",
1293        help="override branch name (defaults to last path component of <SVN URL>)")
1294    parser.add_option("-n", "--dry-run", default=False, action="store_true",
1295                      dest="dryrun",
1296                      help="show outgoing changes to SVN without pushing them")
1297    parser.add_option("-e", "--edit", default=False, action="store_true",
1298                      dest="edit",
1299                      help="edit commit message using external editor")
1300    parser.add_option("-a", "--noversioned_change", type="string", default="abort",
1301                      dest="on_noversioned_change",
1302                      help="=<add> - add to svn repo files that noverioned, <skip> - ignore this changes")
1303    parser.add_option("--merge-branches", default="break", type="string",
1304                      dest="merge_branches",
1305                      help=("<push> - try to push named branches at merge point,"
1306                            "registered in branches map                         "
1307                            "\n<skip> - commit as ordinary revision             "
1308                            "\n<trypush> - if cant push branches, commit as ordinary revision"
1309                            "\n<>|<break> - default:do not merge, and break pushing"
1310                           )
1311                      )
1312    parser.add_option("-u", "--username", default=None, action="store", type="string",
1313                      dest="username",
1314                      help="specify a username ARG as same as svn --username")
1315    parser.add_option("-p", "--password", default=None, action="store", type="string",
1316                      dest="password",
1317                      help="specify a password ARG as same as svn --password")
1318    parser.add_option("--no-auth-cache", default=False, action="store_true",
1319                      dest="cache",
1320                      help="Prevents caching of authentication information")
1321    parser.add_option("--keep-author", default=False, action="store_true",
1322                      dest="keep_author",
1323                      help="keep the author when committing to SVN")
1324    parser.add_option("--svnignore-use", type="string", action="callback", callback=on_option_svnignore_add,
1325                      help="ignore hg-versioned file from committing to SVN")
1326    parser.add_option("--svnignore-save", action="callback", callback=on_option_svnignore_add,
1327                      help=("save svnignores permanently: - add it to .svnignore list")
1328                     )
1329    parser.add_option("--svn-accept", type="string", default="working", dest="svnacception",
1330                      help=("defines avn accept options for merge cmd")
1331                     )
1332    parser.add_option("--autoimport", default=False, action="store_true", dest="auto_import",
1333                      help=("allow import repository at detected svn branch root"
1334                            "       (branch nust be empty!!!)"
1335                            )
1336                     )
1337    parser.add_option("--auto-start-branch", default=False, action="store_true", dest="auto_branch",
1338                      help=("allow create mapped branch at svn branch root if one absent")
1339                     )
1340    load_svnignores()
1341    load_hgsvn_branches_map()
1342    (options, args) = run_parser(parser, __doc__, argv)
1343    if args:
1344        display_parser_error(parser, "incorrect number of arguments")
1345    return locked_main(real_main, options, args)
1346
1347if __name__ == "__main__":
1348    sys.exit(main() or 0)
1349
1350