contrib / fast-import / git-p4on commit Merge branch 'jk/diffstat-binary' (1538f21)
   1#!/usr/bin/env python
   2#
   3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
   4#
   5# Author: Simon Hausmann <simon@lst.de>
   6# Copyright: 2007 Simon Hausmann <simon@lst.de>
   7#            2007 Trolltech ASA
   8# License: MIT <http://www.opensource.org/licenses/mit-license.php>
   9#
  10
  11import optparse, sys, os, marshal, subprocess, shelve
  12import tempfile, getopt, os.path, time, platform
  13import re
  14
  15verbose = False
  16
  17
  18def p4_build_cmd(cmd):
  19    """Build a suitable p4 command line.
  20
  21    This consolidates building and returning a p4 command line into one
  22    location. It means that hooking into the environment, or other configuration
  23    can be done more easily.
  24    """
  25    real_cmd = "%s " % "p4"
  26
  27    user = gitConfig("git-p4.user")
  28    if len(user) > 0:
  29        real_cmd += "-u %s " % user
  30
  31    password = gitConfig("git-p4.password")
  32    if len(password) > 0:
  33        real_cmd += "-P %s " % password
  34
  35    port = gitConfig("git-p4.port")
  36    if len(port) > 0:
  37        real_cmd += "-p %s " % port
  38
  39    host = gitConfig("git-p4.host")
  40    if len(host) > 0:
  41        real_cmd += "-h %s " % host
  42
  43    client = gitConfig("git-p4.client")
  44    if len(client) > 0:
  45        real_cmd += "-c %s " % client
  46
  47    real_cmd += "%s" % (cmd)
  48    if verbose:
  49        print real_cmd
  50    return real_cmd
  51
  52def chdir(dir):
  53    if os.name == 'nt':
  54        os.environ['PWD']=dir
  55    os.chdir(dir)
  56
  57def die(msg):
  58    if verbose:
  59        raise Exception(msg)
  60    else:
  61        sys.stderr.write(msg + "\n")
  62        sys.exit(1)
  63
  64def write_pipe(c, str):
  65    if verbose:
  66        sys.stderr.write('Writing pipe: %s\n' % c)
  67
  68    pipe = os.popen(c, 'w')
  69    val = pipe.write(str)
  70    if pipe.close():
  71        die('Command failed: %s' % c)
  72
  73    return val
  74
  75def p4_write_pipe(c, str):
  76    real_cmd = p4_build_cmd(c)
  77    return write_pipe(real_cmd, str)
  78
  79def read_pipe(c, ignore_error=False):
  80    if verbose:
  81        sys.stderr.write('Reading pipe: %s\n' % c)
  82
  83    pipe = os.popen(c, 'rb')
  84    val = pipe.read()
  85    if pipe.close() and not ignore_error:
  86        die('Command failed: %s' % c)
  87
  88    return val
  89
  90def p4_read_pipe(c, ignore_error=False):
  91    real_cmd = p4_build_cmd(c)
  92    return read_pipe(real_cmd, ignore_error)
  93
  94def read_pipe_lines(c):
  95    if verbose:
  96        sys.stderr.write('Reading pipe: %s\n' % c)
  97    ## todo: check return status
  98    pipe = os.popen(c, 'rb')
  99    val = pipe.readlines()
 100    if pipe.close():
 101        die('Command failed: %s' % c)
 102
 103    return val
 104
 105def p4_read_pipe_lines(c):
 106    """Specifically invoke p4 on the command supplied. """
 107    real_cmd = p4_build_cmd(c)
 108    return read_pipe_lines(real_cmd)
 109
 110def system(cmd):
 111    if verbose:
 112        sys.stderr.write("executing %s\n" % cmd)
 113    if os.system(cmd) != 0:
 114        die("command failed: %s" % cmd)
 115
 116def p4_system(cmd):
 117    """Specifically invoke p4 as the system command. """
 118    real_cmd = p4_build_cmd(cmd)
 119    return system(real_cmd)
 120
 121def isP4Exec(kind):
 122    """Determine if a Perforce 'kind' should have execute permission
 123
 124    'p4 help filetypes' gives a list of the types.  If it starts with 'x',
 125    or x follows one of a few letters.  Otherwise, if there is an 'x' after
 126    a plus sign, it is also executable"""
 127    return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
 128
 129def setP4ExecBit(file, mode):
 130    # Reopens an already open file and changes the execute bit to match
 131    # the execute bit setting in the passed in mode.
 132
 133    p4Type = "+x"
 134
 135    if not isModeExec(mode):
 136        p4Type = getP4OpenedType(file)
 137        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
 138        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
 139        if p4Type[-1] == "+":
 140            p4Type = p4Type[0:-1]
 141
 142    p4_system("reopen -t %s %s" % (p4Type, file))
 143
 144def getP4OpenedType(file):
 145    # Returns the perforce file type for the given file.
 146
 147    result = p4_read_pipe("opened %s" % file)
 148    match = re.match(".*\((.+)\)\r?$", result)
 149    if match:
 150        return match.group(1)
 151    else:
 152        die("Could not determine file type for %s (result: '%s')" % (file, result))
 153
 154def diffTreePattern():
 155    # This is a simple generator for the diff tree regex pattern. This could be
 156    # a class variable if this and parseDiffTreeEntry were a part of a class.
 157    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
 158    while True:
 159        yield pattern
 160
 161def parseDiffTreeEntry(entry):
 162    """Parses a single diff tree entry into its component elements.
 163
 164    See git-diff-tree(1) manpage for details about the format of the diff
 165    output. This method returns a dictionary with the following elements:
 166
 167    src_mode - The mode of the source file
 168    dst_mode - The mode of the destination file
 169    src_sha1 - The sha1 for the source file
 170    dst_sha1 - The sha1 fr the destination file
 171    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
 172    status_score - The score for the status (applicable for 'C' and 'R'
 173                   statuses). This is None if there is no score.
 174    src - The path for the source file.
 175    dst - The path for the destination file. This is only present for
 176          copy or renames. If it is not present, this is None.
 177
 178    If the pattern is not matched, None is returned."""
 179
 180    match = diffTreePattern().next().match(entry)
 181    if match:
 182        return {
 183            'src_mode': match.group(1),
 184            'dst_mode': match.group(2),
 185            'src_sha1': match.group(3),
 186            'dst_sha1': match.group(4),
 187            'status': match.group(5),
 188            'status_score': match.group(6),
 189            'src': match.group(7),
 190            'dst': match.group(10)
 191        }
 192    return None
 193
 194def isModeExec(mode):
 195    # Returns True if the given git mode represents an executable file,
 196    # otherwise False.
 197    return mode[-3:] == "755"
 198
 199def isModeExecChanged(src_mode, dst_mode):
 200    return isModeExec(src_mode) != isModeExec(dst_mode)
 201
 202def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
 203    cmd = p4_build_cmd("-G %s" % (cmd))
 204    if verbose:
 205        sys.stderr.write("Opening pipe: %s\n" % cmd)
 206
 207    # Use a temporary file to avoid deadlocks without
 208    # subprocess.communicate(), which would put another copy
 209    # of stdout into memory.
 210    stdin_file = None
 211    if stdin is not None:
 212        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
 213        stdin_file.write(stdin)
 214        stdin_file.flush()
 215        stdin_file.seek(0)
 216
 217    p4 = subprocess.Popen(cmd, shell=True,
 218                          stdin=stdin_file,
 219                          stdout=subprocess.PIPE)
 220
 221    result = []
 222    try:
 223        while True:
 224            entry = marshal.load(p4.stdout)
 225            if cb is not None:
 226                cb(entry)
 227            else:
 228                result.append(entry)
 229    except EOFError:
 230        pass
 231    exitCode = p4.wait()
 232    if exitCode != 0:
 233        entry = {}
 234        entry["p4ExitCode"] = exitCode
 235        result.append(entry)
 236
 237    return result
 238
 239def p4Cmd(cmd):
 240    list = p4CmdList(cmd)
 241    result = {}
 242    for entry in list:
 243        result.update(entry)
 244    return result;
 245
 246def p4Where(depotPath):
 247    if not depotPath.endswith("/"):
 248        depotPath += "/"
 249    depotPath = depotPath + "..."
 250    outputList = p4CmdList("where %s" % depotPath)
 251    output = None
 252    for entry in outputList:
 253        if "depotFile" in entry:
 254            if entry["depotFile"] == depotPath:
 255                output = entry
 256                break
 257        elif "data" in entry:
 258            data = entry.get("data")
 259            space = data.find(" ")
 260            if data[:space] == depotPath:
 261                output = entry
 262                break
 263    if output == None:
 264        return ""
 265    if output["code"] == "error":
 266        return ""
 267    clientPath = ""
 268    if "path" in output:
 269        clientPath = output.get("path")
 270    elif "data" in output:
 271        data = output.get("data")
 272        lastSpace = data.rfind(" ")
 273        clientPath = data[lastSpace + 1:]
 274
 275    if clientPath.endswith("..."):
 276        clientPath = clientPath[:-3]
 277    return clientPath
 278
 279def currentGitBranch():
 280    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
 281
 282def isValidGitDir(path):
 283    if (os.path.exists(path + "/HEAD")
 284        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
 285        return True;
 286    return False
 287
 288def parseRevision(ref):
 289    return read_pipe("git rev-parse %s" % ref).strip()
 290
 291def extractLogMessageFromGitCommit(commit):
 292    logMessage = ""
 293
 294    ## fixme: title is first line of commit, not 1st paragraph.
 295    foundTitle = False
 296    for log in read_pipe_lines("git cat-file commit %s" % commit):
 297       if not foundTitle:
 298           if len(log) == 1:
 299               foundTitle = True
 300           continue
 301
 302       logMessage += log
 303    return logMessage
 304
 305def extractSettingsGitLog(log):
 306    values = {}
 307    for line in log.split("\n"):
 308        line = line.strip()
 309        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
 310        if not m:
 311            continue
 312
 313        assignments = m.group(1).split (':')
 314        for a in assignments:
 315            vals = a.split ('=')
 316            key = vals[0].strip()
 317            val = ('='.join (vals[1:])).strip()
 318            if val.endswith ('\"') and val.startswith('"'):
 319                val = val[1:-1]
 320
 321            values[key] = val
 322
 323    paths = values.get("depot-paths")
 324    if not paths:
 325        paths = values.get("depot-path")
 326    if paths:
 327        values['depot-paths'] = paths.split(',')
 328    return values
 329
 330def gitBranchExists(branch):
 331    proc = subprocess.Popen(["git", "rev-parse", branch],
 332                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
 333    return proc.wait() == 0;
 334
 335_gitConfig = {}
 336def gitConfig(key):
 337    if not _gitConfig.has_key(key):
 338        _gitConfig[key] = read_pipe("git config %s" % key, ignore_error=True).strip()
 339    return _gitConfig[key]
 340
 341def p4BranchesInGit(branchesAreInRemotes = True):
 342    branches = {}
 343
 344    cmdline = "git rev-parse --symbolic "
 345    if branchesAreInRemotes:
 346        cmdline += " --remotes"
 347    else:
 348        cmdline += " --branches"
 349
 350    for line in read_pipe_lines(cmdline):
 351        line = line.strip()
 352
 353        ## only import to p4/
 354        if not line.startswith('p4/') or line == "p4/HEAD":
 355            continue
 356        branch = line
 357
 358        # strip off p4
 359        branch = re.sub ("^p4/", "", line)
 360
 361        branches[branch] = parseRevision(line)
 362    return branches
 363
 364def findUpstreamBranchPoint(head = "HEAD"):
 365    branches = p4BranchesInGit()
 366    # map from depot-path to branch name
 367    branchByDepotPath = {}
 368    for branch in branches.keys():
 369        tip = branches[branch]
 370        log = extractLogMessageFromGitCommit(tip)
 371        settings = extractSettingsGitLog(log)
 372        if settings.has_key("depot-paths"):
 373            paths = ",".join(settings["depot-paths"])
 374            branchByDepotPath[paths] = "remotes/p4/" + branch
 375
 376    settings = None
 377    parent = 0
 378    while parent < 65535:
 379        commit = head + "~%s" % parent
 380        log = extractLogMessageFromGitCommit(commit)
 381        settings = extractSettingsGitLog(log)
 382        if settings.has_key("depot-paths"):
 383            paths = ",".join(settings["depot-paths"])
 384            if branchByDepotPath.has_key(paths):
 385                return [branchByDepotPath[paths], settings]
 386
 387        parent = parent + 1
 388
 389    return ["", settings]
 390
 391def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
 392    if not silent:
 393        print ("Creating/updating branch(es) in %s based on origin branch(es)"
 394               % localRefPrefix)
 395
 396    originPrefix = "origin/p4/"
 397
 398    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 399        line = line.strip()
 400        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
 401            continue
 402
 403        headName = line[len(originPrefix):]
 404        remoteHead = localRefPrefix + headName
 405        originHead = line
 406
 407        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
 408        if (not original.has_key('depot-paths')
 409            or not original.has_key('change')):
 410            continue
 411
 412        update = False
 413        if not gitBranchExists(remoteHead):
 414            if verbose:
 415                print "creating %s" % remoteHead
 416            update = True
 417        else:
 418            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
 419            if settings.has_key('change') > 0:
 420                if settings['depot-paths'] == original['depot-paths']:
 421                    originP4Change = int(original['change'])
 422                    p4Change = int(settings['change'])
 423                    if originP4Change > p4Change:
 424                        print ("%s (%s) is newer than %s (%s). "
 425                               "Updating p4 branch from origin."
 426                               % (originHead, originP4Change,
 427                                  remoteHead, p4Change))
 428                        update = True
 429                else:
 430                    print ("Ignoring: %s was imported from %s while "
 431                           "%s was imported from %s"
 432                           % (originHead, ','.join(original['depot-paths']),
 433                              remoteHead, ','.join(settings['depot-paths'])))
 434
 435        if update:
 436            system("git update-ref %s %s" % (remoteHead, originHead))
 437
 438def originP4BranchesExist():
 439        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
 440
 441def p4ChangesForPaths(depotPaths, changeRange):
 442    assert depotPaths
 443    output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
 444                                                        for p in depotPaths]))
 445
 446    changes = {}
 447    for line in output:
 448        changeNum = int(line.split(" ")[1])
 449        changes[changeNum] = True
 450
 451    changelist = changes.keys()
 452    changelist.sort()
 453    return changelist
 454
 455class Command:
 456    def __init__(self):
 457        self.usage = "usage: %prog [options]"
 458        self.needsGit = True
 459
 460class P4Debug(Command):
 461    def __init__(self):
 462        Command.__init__(self)
 463        self.options = [
 464            optparse.make_option("--verbose", dest="verbose", action="store_true",
 465                                 default=False),
 466            ]
 467        self.description = "A tool to debug the output of p4 -G."
 468        self.needsGit = False
 469        self.verbose = False
 470
 471    def run(self, args):
 472        j = 0
 473        for output in p4CmdList(" ".join(args)):
 474            print 'Element: %d' % j
 475            j += 1
 476            print output
 477        return True
 478
 479class P4RollBack(Command):
 480    def __init__(self):
 481        Command.__init__(self)
 482        self.options = [
 483            optparse.make_option("--verbose", dest="verbose", action="store_true"),
 484            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
 485        ]
 486        self.description = "A tool to debug the multi-branch import. Don't use :)"
 487        self.verbose = False
 488        self.rollbackLocalBranches = False
 489
 490    def run(self, args):
 491        if len(args) != 1:
 492            return False
 493        maxChange = int(args[0])
 494
 495        if "p4ExitCode" in p4Cmd("changes -m 1"):
 496            die("Problems executing p4");
 497
 498        if self.rollbackLocalBranches:
 499            refPrefix = "refs/heads/"
 500            lines = read_pipe_lines("git rev-parse --symbolic --branches")
 501        else:
 502            refPrefix = "refs/remotes/"
 503            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
 504
 505        for line in lines:
 506            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
 507                line = line.strip()
 508                ref = refPrefix + line
 509                log = extractLogMessageFromGitCommit(ref)
 510                settings = extractSettingsGitLog(log)
 511
 512                depotPaths = settings['depot-paths']
 513                change = settings['change']
 514
 515                changed = False
 516
 517                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
 518                                                           for p in depotPaths]))) == 0:
 519                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
 520                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
 521                    continue
 522
 523                while change and int(change) > maxChange:
 524                    changed = True
 525                    if self.verbose:
 526                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
 527                    system("git update-ref %s \"%s^\"" % (ref, ref))
 528                    log = extractLogMessageFromGitCommit(ref)
 529                    settings =  extractSettingsGitLog(log)
 530
 531
 532                    depotPaths = settings['depot-paths']
 533                    change = settings['change']
 534
 535                if changed:
 536                    print "%s rewound to %s" % (ref, change)
 537
 538        return True
 539
 540class P4Submit(Command):
 541    def __init__(self):
 542        Command.__init__(self)
 543        self.options = [
 544                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 545                optparse.make_option("--origin", dest="origin"),
 546                optparse.make_option("-M", dest="detectRenames", action="store_true"),
 547        ]
 548        self.description = "Submit changes from git to the perforce depot."
 549        self.usage += " [name of git branch to submit into perforce depot]"
 550        self.interactive = True
 551        self.origin = ""
 552        self.detectRenames = False
 553        self.verbose = False
 554        self.isWindows = (platform.system() == "Windows")
 555
 556    def check(self):
 557        if len(p4CmdList("opened ...")) > 0:
 558            die("You have files opened with perforce! Close them before starting the sync.")
 559
 560    # replaces everything between 'Description:' and the next P4 submit template field with the
 561    # commit message
 562    def prepareLogMessage(self, template, message):
 563        result = ""
 564
 565        inDescriptionSection = False
 566
 567        for line in template.split("\n"):
 568            if line.startswith("#"):
 569                result += line + "\n"
 570                continue
 571
 572            if inDescriptionSection:
 573                if line.startswith("Files:"):
 574                    inDescriptionSection = False
 575                else:
 576                    continue
 577            else:
 578                if line.startswith("Description:"):
 579                    inDescriptionSection = True
 580                    line += "\n"
 581                    for messageLine in message.split("\n"):
 582                        line += "\t" + messageLine + "\n"
 583
 584            result += line + "\n"
 585
 586        return result
 587
 588    def prepareSubmitTemplate(self):
 589        # remove lines in the Files section that show changes to files outside the depot path we're committing into
 590        template = ""
 591        inFilesSection = False
 592        for line in p4_read_pipe_lines("change -o"):
 593            if line.endswith("\r\n"):
 594                line = line[:-2] + "\n"
 595            if inFilesSection:
 596                if line.startswith("\t"):
 597                    # path starts and ends with a tab
 598                    path = line[1:]
 599                    lastTab = path.rfind("\t")
 600                    if lastTab != -1:
 601                        path = path[:lastTab]
 602                        if not path.startswith(self.depotPath):
 603                            continue
 604                else:
 605                    inFilesSection = False
 606            else:
 607                if line.startswith("Files:"):
 608                    inFilesSection = True
 609
 610            template += line
 611
 612        return template
 613
 614    def applyCommit(self, id):
 615        print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
 616
 617        if not self.detectRenames:
 618            # If not explicitly set check the config variable
 619            self.detectRenames = gitConfig("git-p4.detectRenames").lower() == "true"
 620
 621        if self.detectRenames:
 622            diffOpts = "-M"
 623        else:
 624            diffOpts = ""
 625
 626        if gitConfig("git-p4.detectCopies").lower() == "true":
 627            diffOpts += " -C"
 628
 629        if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
 630            diffOpts += " --find-copies-harder"
 631
 632        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
 633        filesToAdd = set()
 634        filesToDelete = set()
 635        editedFiles = set()
 636        filesToChangeExecBit = {}
 637        for line in diff:
 638            diff = parseDiffTreeEntry(line)
 639            modifier = diff['status']
 640            path = diff['src']
 641            if modifier == "M":
 642                p4_system("edit \"%s\"" % path)
 643                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 644                    filesToChangeExecBit[path] = diff['dst_mode']
 645                editedFiles.add(path)
 646            elif modifier == "A":
 647                filesToAdd.add(path)
 648                filesToChangeExecBit[path] = diff['dst_mode']
 649                if path in filesToDelete:
 650                    filesToDelete.remove(path)
 651            elif modifier == "D":
 652                filesToDelete.add(path)
 653                if path in filesToAdd:
 654                    filesToAdd.remove(path)
 655            elif modifier == "C":
 656                src, dest = diff['src'], diff['dst']
 657                p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
 658                if diff['src_sha1'] != diff['dst_sha1']:
 659                    p4_system("edit \"%s\"" % (dest))
 660                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 661                    p4_system("edit \"%s\"" % (dest))
 662                    filesToChangeExecBit[dest] = diff['dst_mode']
 663                os.unlink(dest)
 664                editedFiles.add(dest)
 665            elif modifier == "R":
 666                src, dest = diff['src'], diff['dst']
 667                p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
 668                if diff['src_sha1'] != diff['dst_sha1']:
 669                    p4_system("edit \"%s\"" % (dest))
 670                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 671                    p4_system("edit \"%s\"" % (dest))
 672                    filesToChangeExecBit[dest] = diff['dst_mode']
 673                os.unlink(dest)
 674                editedFiles.add(dest)
 675                filesToDelete.add(src)
 676            else:
 677                die("unknown modifier %s for %s" % (modifier, path))
 678
 679        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
 680        patchcmd = diffcmd + " | git apply "
 681        tryPatchCmd = patchcmd + "--check -"
 682        applyPatchCmd = patchcmd + "--check --apply -"
 683
 684        if os.system(tryPatchCmd) != 0:
 685            print "Unfortunately applying the change failed!"
 686            print "What do you want to do?"
 687            response = "x"
 688            while response != "s" and response != "a" and response != "w":
 689                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
 690                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
 691            if response == "s":
 692                print "Skipping! Good luck with the next patches..."
 693                for f in editedFiles:
 694                    p4_system("revert \"%s\"" % f);
 695                for f in filesToAdd:
 696                    system("rm %s" %f)
 697                return
 698            elif response == "a":
 699                os.system(applyPatchCmd)
 700                if len(filesToAdd) > 0:
 701                    print "You may also want to call p4 add on the following files:"
 702                    print " ".join(filesToAdd)
 703                if len(filesToDelete):
 704                    print "The following files should be scheduled for deletion with p4 delete:"
 705                    print " ".join(filesToDelete)
 706                die("Please resolve and submit the conflict manually and "
 707                    + "continue afterwards with git-p4 submit --continue")
 708            elif response == "w":
 709                system(diffcmd + " > patch.txt")
 710                print "Patch saved to patch.txt in %s !" % self.clientPath
 711                die("Please resolve and submit the conflict manually and "
 712                    "continue afterwards with git-p4 submit --continue")
 713
 714        system(applyPatchCmd)
 715
 716        for f in filesToAdd:
 717            p4_system("add \"%s\"" % f)
 718        for f in filesToDelete:
 719            p4_system("revert \"%s\"" % f)
 720            p4_system("delete \"%s\"" % f)
 721
 722        # Set/clear executable bits
 723        for f in filesToChangeExecBit.keys():
 724            mode = filesToChangeExecBit[f]
 725            setP4ExecBit(f, mode)
 726
 727        logMessage = extractLogMessageFromGitCommit(id)
 728        logMessage = logMessage.strip()
 729
 730        template = self.prepareSubmitTemplate()
 731
 732        if self.interactive:
 733            submitTemplate = self.prepareLogMessage(template, logMessage)
 734            if os.environ.has_key("P4DIFF"):
 735                del(os.environ["P4DIFF"])
 736            diff = ""
 737            for editedFile in editedFiles:
 738                diff += p4_read_pipe("diff -du %r" % editedFile)
 739
 740            newdiff = ""
 741            for newFile in filesToAdd:
 742                newdiff += "==== new file ====\n"
 743                newdiff += "--- /dev/null\n"
 744                newdiff += "+++ %s\n" % newFile
 745                f = open(newFile, "r")
 746                for line in f.readlines():
 747                    newdiff += "+" + line
 748                f.close()
 749
 750            separatorLine = "######## everything below this line is just the diff #######\n"
 751
 752            [handle, fileName] = tempfile.mkstemp()
 753            tmpFile = os.fdopen(handle, "w+")
 754            if self.isWindows:
 755                submitTemplate = submitTemplate.replace("\n", "\r\n")
 756                separatorLine = separatorLine.replace("\n", "\r\n")
 757                newdiff = newdiff.replace("\n", "\r\n")
 758            tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
 759            tmpFile.close()
 760            mtime = os.stat(fileName).st_mtime
 761            if os.environ.has_key("P4EDITOR"):
 762                editor = os.environ.get("P4EDITOR")
 763            else:
 764                editor = read_pipe("git var GIT_EDITOR").strip()
 765            system(editor + " " + fileName)
 766
 767            response = "y"
 768            if os.stat(fileName).st_mtime <= mtime:
 769                response = "x"
 770                while response != "y" and response != "n":
 771                    response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
 772
 773            if response == "y":
 774                tmpFile = open(fileName, "rb")
 775                message = tmpFile.read()
 776                tmpFile.close()
 777                submitTemplate = message[:message.index(separatorLine)]
 778                if self.isWindows:
 779                    submitTemplate = submitTemplate.replace("\r\n", "\n")
 780                p4_write_pipe("submit -i", submitTemplate)
 781            else:
 782                for f in editedFiles:
 783                    p4_system("revert \"%s\"" % f);
 784                for f in filesToAdd:
 785                    p4_system("revert \"%s\"" % f);
 786                    system("rm %s" %f)
 787
 788            os.remove(fileName)
 789        else:
 790            fileName = "submit.txt"
 791            file = open(fileName, "w+")
 792            file.write(self.prepareLogMessage(template, logMessage))
 793            file.close()
 794            print ("Perforce submit template written as %s. "
 795                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
 796                   % (fileName, fileName))
 797
 798    def run(self, args):
 799        if len(args) == 0:
 800            self.master = currentGitBranch()
 801            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
 802                die("Detecting current git branch failed!")
 803        elif len(args) == 1:
 804            self.master = args[0]
 805        else:
 806            return False
 807
 808        allowSubmit = gitConfig("git-p4.allowSubmit")
 809        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
 810            die("%s is not in git-p4.allowSubmit" % self.master)
 811
 812        [upstream, settings] = findUpstreamBranchPoint()
 813        self.depotPath = settings['depot-paths'][0]
 814        if len(self.origin) == 0:
 815            self.origin = upstream
 816
 817        if self.verbose:
 818            print "Origin branch is " + self.origin
 819
 820        if len(self.depotPath) == 0:
 821            print "Internal error: cannot locate perforce depot path from existing branches"
 822            sys.exit(128)
 823
 824        self.clientPath = p4Where(self.depotPath)
 825
 826        if len(self.clientPath) == 0:
 827            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
 828            sys.exit(128)
 829
 830        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
 831        self.oldWorkingDirectory = os.getcwd()
 832
 833        chdir(self.clientPath)
 834        print "Synchronizing p4 checkout..."
 835        p4_system("sync ...")
 836
 837        self.check()
 838
 839        commits = []
 840        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
 841            commits.append(line.strip())
 842        commits.reverse()
 843
 844        while len(commits) > 0:
 845            commit = commits[0]
 846            commits = commits[1:]
 847            self.applyCommit(commit)
 848            if not self.interactive:
 849                break
 850
 851        if len(commits) == 0:
 852            print "All changes applied!"
 853            chdir(self.oldWorkingDirectory)
 854
 855            sync = P4Sync()
 856            sync.run([])
 857
 858            rebase = P4Rebase()
 859            rebase.rebase()
 860
 861        return True
 862
 863class P4Sync(Command):
 864    delete_actions = ( "delete", "move/delete", "purge" )
 865
 866    def __init__(self):
 867        Command.__init__(self)
 868        self.options = [
 869                optparse.make_option("--branch", dest="branch"),
 870                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
 871                optparse.make_option("--changesfile", dest="changesFile"),
 872                optparse.make_option("--silent", dest="silent", action="store_true"),
 873                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
 874                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 875                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
 876                                     help="Import into refs/heads/ , not refs/remotes"),
 877                optparse.make_option("--max-changes", dest="maxChanges"),
 878                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
 879                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
 880                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
 881                                     help="Only sync files that are included in the Perforce Client Spec")
 882        ]
 883        self.description = """Imports from Perforce into a git repository.\n
 884    example:
 885    //depot/my/project/ -- to import the current head
 886    //depot/my/project/@all -- to import everything
 887    //depot/my/project/@1,6 -- to import only from revision 1 to 6
 888
 889    (a ... is not needed in the path p4 specification, it's added implicitly)"""
 890
 891        self.usage += " //depot/path[@revRange]"
 892        self.silent = False
 893        self.createdBranches = set()
 894        self.committedChanges = set()
 895        self.branch = ""
 896        self.detectBranches = False
 897        self.detectLabels = False
 898        self.changesFile = ""
 899        self.syncWithOrigin = True
 900        self.verbose = False
 901        self.importIntoRemotes = True
 902        self.maxChanges = ""
 903        self.isWindows = (platform.system() == "Windows")
 904        self.keepRepoPath = False
 905        self.depotPaths = None
 906        self.p4BranchesInGit = []
 907        self.cloneExclude = []
 908        self.useClientSpec = False
 909        self.clientSpecDirs = []
 910
 911        if gitConfig("git-p4.syncFromOrigin") == "false":
 912            self.syncWithOrigin = False
 913
 914    #
 915    # P4 wildcards are not allowed in filenames.  P4 complains
 916    # if you simply add them, but you can force it with "-f", in
 917    # which case it translates them into %xx encoding internally.
 918    # Search for and fix just these four characters.  Do % last so
 919    # that fixing it does not inadvertently create new %-escapes.
 920    #
 921    def wildcard_decode(self, path):
 922        # Cannot have * in a filename in windows; untested as to
 923        # what p4 would do in such a case.
 924        if not self.isWindows:
 925            path = path.replace("%2A", "*")
 926        path = path.replace("%23", "#") \
 927                   .replace("%40", "@") \
 928                   .replace("%25", "%")
 929        return path
 930
 931    def extractFilesFromCommit(self, commit):
 932        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
 933                             for path in self.cloneExclude]
 934        files = []
 935        fnum = 0
 936        while commit.has_key("depotFile%s" % fnum):
 937            path =  commit["depotFile%s" % fnum]
 938
 939            if [p for p in self.cloneExclude
 940                if path.startswith (p)]:
 941                found = False
 942            else:
 943                found = [p for p in self.depotPaths
 944                         if path.startswith (p)]
 945            if not found:
 946                fnum = fnum + 1
 947                continue
 948
 949            file = {}
 950            file["path"] = path
 951            file["rev"] = commit["rev%s" % fnum]
 952            file["action"] = commit["action%s" % fnum]
 953            file["type"] = commit["type%s" % fnum]
 954            files.append(file)
 955            fnum = fnum + 1
 956        return files
 957
 958    def stripRepoPath(self, path, prefixes):
 959        if self.useClientSpec:
 960
 961            # if using the client spec, we use the output directory
 962            # specified in the client.  For example, a view
 963            #   //depot/foo/branch/... //client/branch/foo/...
 964            # will end up putting all foo/branch files into
 965            #  branch/foo/
 966            for val in self.clientSpecDirs:
 967                if path.startswith(val[0]):
 968                    # replace the depot path with the client path
 969                    path = path.replace(val[0], val[1][1])
 970                    # now strip out the client (//client/...)
 971                    path = re.sub("^(//[^/]+/)", '', path)
 972                    # the rest is all path
 973                    return path
 974
 975        if self.keepRepoPath:
 976            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
 977
 978        for p in prefixes:
 979            if path.startswith(p):
 980                path = path[len(p):]
 981
 982        return path
 983
 984    def splitFilesIntoBranches(self, commit):
 985        branches = {}
 986        fnum = 0
 987        while commit.has_key("depotFile%s" % fnum):
 988            path =  commit["depotFile%s" % fnum]
 989            found = [p for p in self.depotPaths
 990                     if path.startswith (p)]
 991            if not found:
 992                fnum = fnum + 1
 993                continue
 994
 995            file = {}
 996            file["path"] = path
 997            file["rev"] = commit["rev%s" % fnum]
 998            file["action"] = commit["action%s" % fnum]
 999            file["type"] = commit["type%s" % fnum]
1000            fnum = fnum + 1
1001
1002            relPath = self.stripRepoPath(path, self.depotPaths)
1003
1004            for branch in self.knownBranches.keys():
1005
1006                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1007                if relPath.startswith(branch + "/"):
1008                    if branch not in branches:
1009                        branches[branch] = []
1010                    branches[branch].append(file)
1011                    break
1012
1013        return branches
1014
1015    # output one file from the P4 stream
1016    # - helper for streamP4Files
1017
1018    def streamOneP4File(self, file, contents):
1019        if file["type"] == "apple":
1020            print "\nfile %s is a strange apple file that forks. Ignoring" % \
1021                file['depotFile']
1022            return
1023
1024        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1025        relPath = self.wildcard_decode(relPath)
1026        if verbose:
1027            sys.stderr.write("%s\n" % relPath)
1028
1029        mode = "644"
1030        if isP4Exec(file["type"]):
1031            mode = "755"
1032        elif file["type"] == "symlink":
1033            mode = "120000"
1034            # p4 print on a symlink contains "target\n", so strip it off
1035            data = ''.join(contents)
1036            contents = [data[:-1]]
1037
1038        if self.isWindows and file["type"].endswith("text"):
1039            mangled = []
1040            for data in contents:
1041                data = data.replace("\r\n", "\n")
1042                mangled.append(data)
1043            contents = mangled
1044
1045        if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1046            contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1047        elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1048            contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1049
1050        self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1051
1052        # total length...
1053        length = 0
1054        for d in contents:
1055            length = length + len(d)
1056
1057        self.gitStream.write("data %d\n" % length)
1058        for d in contents:
1059            self.gitStream.write(d)
1060        self.gitStream.write("\n")
1061
1062    def streamOneP4Deletion(self, file):
1063        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1064        if verbose:
1065            sys.stderr.write("delete %s\n" % relPath)
1066        self.gitStream.write("D %s\n" % relPath)
1067
1068    # handle another chunk of streaming data
1069    def streamP4FilesCb(self, marshalled):
1070
1071        if marshalled.has_key('depotFile') and self.stream_have_file_info:
1072            # start of a new file - output the old one first
1073            self.streamOneP4File(self.stream_file, self.stream_contents)
1074            self.stream_file = {}
1075            self.stream_contents = []
1076            self.stream_have_file_info = False
1077
1078        # pick up the new file information... for the
1079        # 'data' field we need to append to our array
1080        for k in marshalled.keys():
1081            if k == 'data':
1082                self.stream_contents.append(marshalled['data'])
1083            else:
1084                self.stream_file[k] = marshalled[k]
1085
1086        self.stream_have_file_info = True
1087
1088    # Stream directly from "p4 files" into "git fast-import"
1089    def streamP4Files(self, files):
1090        filesForCommit = []
1091        filesToRead = []
1092        filesToDelete = []
1093
1094        for f in files:
1095            includeFile = True
1096            for val in self.clientSpecDirs:
1097                if f['path'].startswith(val[0]):
1098                    if val[1][0] <= 0:
1099                        includeFile = False
1100                    break
1101
1102            if includeFile:
1103                filesForCommit.append(f)
1104                if f['action'] in self.delete_actions:
1105                    filesToDelete.append(f)
1106                else:
1107                    filesToRead.append(f)
1108
1109        # deleted files...
1110        for f in filesToDelete:
1111            self.streamOneP4Deletion(f)
1112
1113        if len(filesToRead) > 0:
1114            self.stream_file = {}
1115            self.stream_contents = []
1116            self.stream_have_file_info = False
1117
1118            # curry self argument
1119            def streamP4FilesCbSelf(entry):
1120                self.streamP4FilesCb(entry)
1121
1122            p4CmdList("-x - print",
1123                '\n'.join(['%s#%s' % (f['path'], f['rev'])
1124                                                  for f in filesToRead]),
1125                cb=streamP4FilesCbSelf)
1126
1127            # do the last chunk
1128            if self.stream_file.has_key('depotFile'):
1129                self.streamOneP4File(self.stream_file, self.stream_contents)
1130
1131    def commit(self, details, files, branch, branchPrefixes, parent = ""):
1132        epoch = details["time"]
1133        author = details["user"]
1134        self.branchPrefixes = branchPrefixes
1135
1136        if self.verbose:
1137            print "commit into %s" % branch
1138
1139        # start with reading files; if that fails, we should not
1140        # create a commit.
1141        new_files = []
1142        for f in files:
1143            if [p for p in branchPrefixes if f['path'].startswith(p)]:
1144                new_files.append (f)
1145            else:
1146                sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
1147
1148        self.gitStream.write("commit %s\n" % branch)
1149#        gitStream.write("mark :%s\n" % details["change"])
1150        self.committedChanges.add(int(details["change"]))
1151        committer = ""
1152        if author not in self.users:
1153            self.getUserMapFromPerforceServer()
1154        if author in self.users:
1155            committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1156        else:
1157            committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1158
1159        self.gitStream.write("committer %s\n" % committer)
1160
1161        self.gitStream.write("data <<EOT\n")
1162        self.gitStream.write(details["desc"])
1163        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1164                             % (','.join (branchPrefixes), details["change"]))
1165        if len(details['options']) > 0:
1166            self.gitStream.write(": options = %s" % details['options'])
1167        self.gitStream.write("]\nEOT\n\n")
1168
1169        if len(parent) > 0:
1170            if self.verbose:
1171                print "parent %s" % parent
1172            self.gitStream.write("from %s\n" % parent)
1173
1174        self.streamP4Files(new_files)
1175        self.gitStream.write("\n")
1176
1177        change = int(details["change"])
1178
1179        if self.labels.has_key(change):
1180            label = self.labels[change]
1181            labelDetails = label[0]
1182            labelRevisions = label[1]
1183            if self.verbose:
1184                print "Change %s is labelled %s" % (change, labelDetails)
1185
1186            files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1187                                                    for p in branchPrefixes]))
1188
1189            if len(files) == len(labelRevisions):
1190
1191                cleanedFiles = {}
1192                for info in files:
1193                    if info["action"] in self.delete_actions:
1194                        continue
1195                    cleanedFiles[info["depotFile"]] = info["rev"]
1196
1197                if cleanedFiles == labelRevisions:
1198                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1199                    self.gitStream.write("from %s\n" % branch)
1200
1201                    owner = labelDetails["Owner"]
1202                    tagger = ""
1203                    if author in self.users:
1204                        tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1205                    else:
1206                        tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1207                    self.gitStream.write("tagger %s\n" % tagger)
1208                    self.gitStream.write("data <<EOT\n")
1209                    self.gitStream.write(labelDetails["Description"])
1210                    self.gitStream.write("EOT\n\n")
1211
1212                else:
1213                    if not self.silent:
1214                        print ("Tag %s does not match with change %s: files do not match."
1215                               % (labelDetails["label"], change))
1216
1217            else:
1218                if not self.silent:
1219                    print ("Tag %s does not match with change %s: file count is different."
1220                           % (labelDetails["label"], change))
1221
1222    def getUserCacheFilename(self):
1223        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1224        return home + "/.gitp4-usercache.txt"
1225
1226    def getUserMapFromPerforceServer(self):
1227        if self.userMapFromPerforceServer:
1228            return
1229        self.users = {}
1230
1231        for output in p4CmdList("users"):
1232            if not output.has_key("User"):
1233                continue
1234            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1235
1236
1237        s = ''
1238        for (key, val) in self.users.items():
1239            s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1240
1241        open(self.getUserCacheFilename(), "wb").write(s)
1242        self.userMapFromPerforceServer = True
1243
1244    def loadUserMapFromCache(self):
1245        self.users = {}
1246        self.userMapFromPerforceServer = False
1247        try:
1248            cache = open(self.getUserCacheFilename(), "rb")
1249            lines = cache.readlines()
1250            cache.close()
1251            for line in lines:
1252                entry = line.strip().split("\t")
1253                self.users[entry[0]] = entry[1]
1254        except IOError:
1255            self.getUserMapFromPerforceServer()
1256
1257    def getLabels(self):
1258        self.labels = {}
1259
1260        l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1261        if len(l) > 0 and not self.silent:
1262            print "Finding files belonging to labels in %s" % `self.depotPaths`
1263
1264        for output in l:
1265            label = output["label"]
1266            revisions = {}
1267            newestChange = 0
1268            if self.verbose:
1269                print "Querying files for label %s" % label
1270            for file in p4CmdList("files "
1271                                  +  ' '.join (["%s...@%s" % (p, label)
1272                                                for p in self.depotPaths])):
1273                revisions[file["depotFile"]] = file["rev"]
1274                change = int(file["change"])
1275                if change > newestChange:
1276                    newestChange = change
1277
1278            self.labels[newestChange] = [output, revisions]
1279
1280        if self.verbose:
1281            print "Label changes: %s" % self.labels.keys()
1282
1283    def guessProjectName(self):
1284        for p in self.depotPaths:
1285            if p.endswith("/"):
1286                p = p[:-1]
1287            p = p[p.strip().rfind("/") + 1:]
1288            if not p.endswith("/"):
1289               p += "/"
1290            return p
1291
1292    def getBranchMapping(self):
1293        lostAndFoundBranches = set()
1294
1295        for info in p4CmdList("branches"):
1296            details = p4Cmd("branch -o %s" % info["branch"])
1297            viewIdx = 0
1298            while details.has_key("View%s" % viewIdx):
1299                paths = details["View%s" % viewIdx].split(" ")
1300                viewIdx = viewIdx + 1
1301                # require standard //depot/foo/... //depot/bar/... mapping
1302                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1303                    continue
1304                source = paths[0]
1305                destination = paths[1]
1306                ## HACK
1307                if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1308                    source = source[len(self.depotPaths[0]):-4]
1309                    destination = destination[len(self.depotPaths[0]):-4]
1310
1311                    if destination in self.knownBranches:
1312                        if not self.silent:
1313                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1314                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1315                        continue
1316
1317                    self.knownBranches[destination] = source
1318
1319                    lostAndFoundBranches.discard(destination)
1320
1321                    if source not in self.knownBranches:
1322                        lostAndFoundBranches.add(source)
1323
1324
1325        for branch in lostAndFoundBranches:
1326            self.knownBranches[branch] = branch
1327
1328    def getBranchMappingFromGitBranches(self):
1329        branches = p4BranchesInGit(self.importIntoRemotes)
1330        for branch in branches.keys():
1331            if branch == "master":
1332                branch = "main"
1333            else:
1334                branch = branch[len(self.projectName):]
1335            self.knownBranches[branch] = branch
1336
1337    def listExistingP4GitBranches(self):
1338        # branches holds mapping from name to commit
1339        branches = p4BranchesInGit(self.importIntoRemotes)
1340        self.p4BranchesInGit = branches.keys()
1341        for branch in branches.keys():
1342            self.initialParents[self.refPrefix + branch] = branches[branch]
1343
1344    def updateOptionDict(self, d):
1345        option_keys = {}
1346        if self.keepRepoPath:
1347            option_keys['keepRepoPath'] = 1
1348
1349        d["options"] = ' '.join(sorted(option_keys.keys()))
1350
1351    def readOptions(self, d):
1352        self.keepRepoPath = (d.has_key('options')
1353                             and ('keepRepoPath' in d['options']))
1354
1355    def gitRefForBranch(self, branch):
1356        if branch == "main":
1357            return self.refPrefix + "master"
1358
1359        if len(branch) <= 0:
1360            return branch
1361
1362        return self.refPrefix + self.projectName + branch
1363
1364    def gitCommitByP4Change(self, ref, change):
1365        if self.verbose:
1366            print "looking in ref " + ref + " for change %s using bisect..." % change
1367
1368        earliestCommit = ""
1369        latestCommit = parseRevision(ref)
1370
1371        while True:
1372            if self.verbose:
1373                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1374            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1375            if len(next) == 0:
1376                if self.verbose:
1377                    print "argh"
1378                return ""
1379            log = extractLogMessageFromGitCommit(next)
1380            settings = extractSettingsGitLog(log)
1381            currentChange = int(settings['change'])
1382            if self.verbose:
1383                print "current change %s" % currentChange
1384
1385            if currentChange == change:
1386                if self.verbose:
1387                    print "found %s" % next
1388                return next
1389
1390            if currentChange < change:
1391                earliestCommit = "^%s" % next
1392            else:
1393                latestCommit = "%s" % next
1394
1395        return ""
1396
1397    def importNewBranch(self, branch, maxChange):
1398        # make fast-import flush all changes to disk and update the refs using the checkpoint
1399        # command so that we can try to find the branch parent in the git history
1400        self.gitStream.write("checkpoint\n\n");
1401        self.gitStream.flush();
1402        branchPrefix = self.depotPaths[0] + branch + "/"
1403        range = "@1,%s" % maxChange
1404        #print "prefix" + branchPrefix
1405        changes = p4ChangesForPaths([branchPrefix], range)
1406        if len(changes) <= 0:
1407            return False
1408        firstChange = changes[0]
1409        #print "first change in branch: %s" % firstChange
1410        sourceBranch = self.knownBranches[branch]
1411        sourceDepotPath = self.depotPaths[0] + sourceBranch
1412        sourceRef = self.gitRefForBranch(sourceBranch)
1413        #print "source " + sourceBranch
1414
1415        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1416        #print "branch parent: %s" % branchParentChange
1417        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1418        if len(gitParent) > 0:
1419            self.initialParents[self.gitRefForBranch(branch)] = gitParent
1420            #print "parent git commit: %s" % gitParent
1421
1422        self.importChanges(changes)
1423        return True
1424
1425    def importChanges(self, changes):
1426        cnt = 1
1427        for change in changes:
1428            description = p4Cmd("describe %s" % change)
1429            self.updateOptionDict(description)
1430
1431            if not self.silent:
1432                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1433                sys.stdout.flush()
1434            cnt = cnt + 1
1435
1436            try:
1437                if self.detectBranches:
1438                    branches = self.splitFilesIntoBranches(description)
1439                    for branch in branches.keys():
1440                        ## HACK  --hwn
1441                        branchPrefix = self.depotPaths[0] + branch + "/"
1442
1443                        parent = ""
1444
1445                        filesForCommit = branches[branch]
1446
1447                        if self.verbose:
1448                            print "branch is %s" % branch
1449
1450                        self.updatedBranches.add(branch)
1451
1452                        if branch not in self.createdBranches:
1453                            self.createdBranches.add(branch)
1454                            parent = self.knownBranches[branch]
1455                            if parent == branch:
1456                                parent = ""
1457                            else:
1458                                fullBranch = self.projectName + branch
1459                                if fullBranch not in self.p4BranchesInGit:
1460                                    if not self.silent:
1461                                        print("\n    Importing new branch %s" % fullBranch);
1462                                    if self.importNewBranch(branch, change - 1):
1463                                        parent = ""
1464                                        self.p4BranchesInGit.append(fullBranch)
1465                                    if not self.silent:
1466                                        print("\n    Resuming with change %s" % change);
1467
1468                                if self.verbose:
1469                                    print "parent determined through known branches: %s" % parent
1470
1471                        branch = self.gitRefForBranch(branch)
1472                        parent = self.gitRefForBranch(parent)
1473
1474                        if self.verbose:
1475                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1476
1477                        if len(parent) == 0 and branch in self.initialParents:
1478                            parent = self.initialParents[branch]
1479                            del self.initialParents[branch]
1480
1481                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1482                else:
1483                    files = self.extractFilesFromCommit(description)
1484                    self.commit(description, files, self.branch, self.depotPaths,
1485                                self.initialParent)
1486                    self.initialParent = ""
1487            except IOError:
1488                print self.gitError.read()
1489                sys.exit(1)
1490
1491    def importHeadRevision(self, revision):
1492        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1493
1494        details = { "user" : "git perforce import user", "time" : int(time.time()) }
1495        details["desc"] = ("Initial import of %s from the state at revision %s\n"
1496                           % (' '.join(self.depotPaths), revision))
1497        details["change"] = revision
1498        newestRevision = 0
1499
1500        fileCnt = 0
1501        for info in p4CmdList("files "
1502                              +  ' '.join(["%s...%s"
1503                                           % (p, revision)
1504                                           for p in self.depotPaths])):
1505
1506            if 'code' in info and info['code'] == 'error':
1507                sys.stderr.write("p4 returned an error: %s\n"
1508                                 % info['data'])
1509                if info['data'].find("must refer to client") >= 0:
1510                    sys.stderr.write("This particular p4 error is misleading.\n")
1511                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
1512                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
1513                sys.exit(1)
1514            if 'p4ExitCode' in info:
1515                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
1516                sys.exit(1)
1517
1518
1519            change = int(info["change"])
1520            if change > newestRevision:
1521                newestRevision = change
1522
1523            if info["action"] in self.delete_actions:
1524                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1525                #fileCnt = fileCnt + 1
1526                continue
1527
1528            for prop in ["depotFile", "rev", "action", "type" ]:
1529                details["%s%s" % (prop, fileCnt)] = info[prop]
1530
1531            fileCnt = fileCnt + 1
1532
1533        details["change"] = newestRevision
1534        self.updateOptionDict(details)
1535        try:
1536            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1537        except IOError:
1538            print "IO error with git fast-import. Is your git version recent enough?"
1539            print self.gitError.read()
1540
1541
1542    def getClientSpec(self):
1543        specList = p4CmdList( "client -o" )
1544        temp = {}
1545        for entry in specList:
1546            for k,v in entry.iteritems():
1547                if k.startswith("View"):
1548
1549                    # p4 has these %%1 to %%9 arguments in specs to
1550                    # reorder paths; which we can't handle (yet :)
1551                    if re.match('%%\d', v) != None:
1552                        print "Sorry, can't handle %%n arguments in client specs"
1553                        sys.exit(1)
1554
1555                    if v.startswith('"'):
1556                        start = 1
1557                    else:
1558                        start = 0
1559                    index = v.find("...")
1560
1561                    # save the "client view"; i.e the RHS of the view
1562                    # line that tells the client where to put the
1563                    # files for this view.
1564                    cv = v[index+3:].strip() # +3 to remove previous '...'
1565
1566                    # if the client view doesn't end with a
1567                    # ... wildcard, then we're going to mess up the
1568                    # output directory, so fail gracefully.
1569                    if not cv.endswith('...'):
1570                        print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1571                        sys.exit(1)
1572                    cv=cv[:-3]
1573
1574                    # now save the view; +index means included, -index
1575                    # means it should be filtered out.
1576                    v = v[start:index]
1577                    if v.startswith("-"):
1578                        v = v[1:]
1579                        include = -len(v)
1580                    else:
1581                        include = len(v)
1582
1583                    temp[v] = (include, cv)
1584
1585        self.clientSpecDirs = temp.items()
1586        self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1587
1588    def run(self, args):
1589        self.depotPaths = []
1590        self.changeRange = ""
1591        self.initialParent = ""
1592        self.previousDepotPaths = []
1593
1594        # map from branch depot path to parent branch
1595        self.knownBranches = {}
1596        self.initialParents = {}
1597        self.hasOrigin = originP4BranchesExist()
1598        if not self.syncWithOrigin:
1599            self.hasOrigin = False
1600
1601        if self.importIntoRemotes:
1602            self.refPrefix = "refs/remotes/p4/"
1603        else:
1604            self.refPrefix = "refs/heads/p4/"
1605
1606        if self.syncWithOrigin and self.hasOrigin:
1607            if not self.silent:
1608                print "Syncing with origin first by calling git fetch origin"
1609            system("git fetch origin")
1610
1611        if len(self.branch) == 0:
1612            self.branch = self.refPrefix + "master"
1613            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1614                system("git update-ref %s refs/heads/p4" % self.branch)
1615                system("git branch -D p4");
1616            # create it /after/ importing, when master exists
1617            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1618                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1619
1620        if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1621            self.getClientSpec()
1622
1623        # TODO: should always look at previous commits,
1624        # merge with previous imports, if possible.
1625        if args == []:
1626            if self.hasOrigin:
1627                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1628            self.listExistingP4GitBranches()
1629
1630            if len(self.p4BranchesInGit) > 1:
1631                if not self.silent:
1632                    print "Importing from/into multiple branches"
1633                self.detectBranches = True
1634
1635            if self.verbose:
1636                print "branches: %s" % self.p4BranchesInGit
1637
1638            p4Change = 0
1639            for branch in self.p4BranchesInGit:
1640                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1641
1642                settings = extractSettingsGitLog(logMsg)
1643
1644                self.readOptions(settings)
1645                if (settings.has_key('depot-paths')
1646                    and settings.has_key ('change')):
1647                    change = int(settings['change']) + 1
1648                    p4Change = max(p4Change, change)
1649
1650                    depotPaths = sorted(settings['depot-paths'])
1651                    if self.previousDepotPaths == []:
1652                        self.previousDepotPaths = depotPaths
1653                    else:
1654                        paths = []
1655                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1656                            for i in range(0, min(len(cur), len(prev))):
1657                                if cur[i] <> prev[i]:
1658                                    i = i - 1
1659                                    break
1660
1661                            paths.append (cur[:i + 1])
1662
1663                        self.previousDepotPaths = paths
1664
1665            if p4Change > 0:
1666                self.depotPaths = sorted(self.previousDepotPaths)
1667                self.changeRange = "@%s,#head" % p4Change
1668                if not self.detectBranches:
1669                    self.initialParent = parseRevision(self.branch)
1670                if not self.silent and not self.detectBranches:
1671                    print "Performing incremental import into %s git branch" % self.branch
1672
1673        if not self.branch.startswith("refs/"):
1674            self.branch = "refs/heads/" + self.branch
1675
1676        if len(args) == 0 and self.depotPaths:
1677            if not self.silent:
1678                print "Depot paths: %s" % ' '.join(self.depotPaths)
1679        else:
1680            if self.depotPaths and self.depotPaths != args:
1681                print ("previous import used depot path %s and now %s was specified. "
1682                       "This doesn't work!" % (' '.join (self.depotPaths),
1683                                               ' '.join (args)))
1684                sys.exit(1)
1685
1686            self.depotPaths = sorted(args)
1687
1688        revision = ""
1689        self.users = {}
1690
1691        newPaths = []
1692        for p in self.depotPaths:
1693            if p.find("@") != -1:
1694                atIdx = p.index("@")
1695                self.changeRange = p[atIdx:]
1696                if self.changeRange == "@all":
1697                    self.changeRange = ""
1698                elif ',' not in self.changeRange:
1699                    revision = self.changeRange
1700                    self.changeRange = ""
1701                p = p[:atIdx]
1702            elif p.find("#") != -1:
1703                hashIdx = p.index("#")
1704                revision = p[hashIdx:]
1705                p = p[:hashIdx]
1706            elif self.previousDepotPaths == []:
1707                revision = "#head"
1708
1709            p = re.sub ("\.\.\.$", "", p)
1710            if not p.endswith("/"):
1711                p += "/"
1712
1713            newPaths.append(p)
1714
1715        self.depotPaths = newPaths
1716
1717
1718        self.loadUserMapFromCache()
1719        self.labels = {}
1720        if self.detectLabels:
1721            self.getLabels();
1722
1723        if self.detectBranches:
1724            ## FIXME - what's a P4 projectName ?
1725            self.projectName = self.guessProjectName()
1726
1727            if self.hasOrigin:
1728                self.getBranchMappingFromGitBranches()
1729            else:
1730                self.getBranchMapping()
1731            if self.verbose:
1732                print "p4-git branches: %s" % self.p4BranchesInGit
1733                print "initial parents: %s" % self.initialParents
1734            for b in self.p4BranchesInGit:
1735                if b != "master":
1736
1737                    ## FIXME
1738                    b = b[len(self.projectName):]
1739                self.createdBranches.add(b)
1740
1741        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1742
1743        importProcess = subprocess.Popen(["git", "fast-import"],
1744                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1745                                         stderr=subprocess.PIPE);
1746        self.gitOutput = importProcess.stdout
1747        self.gitStream = importProcess.stdin
1748        self.gitError = importProcess.stderr
1749
1750        if revision:
1751            self.importHeadRevision(revision)
1752        else:
1753            changes = []
1754
1755            if len(self.changesFile) > 0:
1756                output = open(self.changesFile).readlines()
1757                changeSet = set()
1758                for line in output:
1759                    changeSet.add(int(line))
1760
1761                for change in changeSet:
1762                    changes.append(change)
1763
1764                changes.sort()
1765            else:
1766                if not self.p4BranchesInGit:
1767                    die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
1768                if self.verbose:
1769                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1770                                                              self.changeRange)
1771                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1772
1773                if len(self.maxChanges) > 0:
1774                    changes = changes[:min(int(self.maxChanges), len(changes))]
1775
1776            if len(changes) == 0:
1777                if not self.silent:
1778                    print "No changes to import!"
1779                return True
1780
1781            if not self.silent and not self.detectBranches:
1782                print "Import destination: %s" % self.branch
1783
1784            self.updatedBranches = set()
1785
1786            self.importChanges(changes)
1787
1788            if not self.silent:
1789                print ""
1790                if len(self.updatedBranches) > 0:
1791                    sys.stdout.write("Updated branches: ")
1792                    for b in self.updatedBranches:
1793                        sys.stdout.write("%s " % b)
1794                    sys.stdout.write("\n")
1795
1796        self.gitStream.close()
1797        if importProcess.wait() != 0:
1798            die("fast-import failed: %s" % self.gitError.read())
1799        self.gitOutput.close()
1800        self.gitError.close()
1801
1802        return True
1803
1804class P4Rebase(Command):
1805    def __init__(self):
1806        Command.__init__(self)
1807        self.options = [ ]
1808        self.description = ("Fetches the latest revision from perforce and "
1809                            + "rebases the current work (branch) against it")
1810        self.verbose = False
1811
1812    def run(self, args):
1813        sync = P4Sync()
1814        sync.run([])
1815
1816        return self.rebase()
1817
1818    def rebase(self):
1819        if os.system("git update-index --refresh") != 0:
1820            die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up-to-date or stash away all your changes with git stash.");
1821        if len(read_pipe("git diff-index HEAD --")) > 0:
1822            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1823
1824        [upstream, settings] = findUpstreamBranchPoint()
1825        if len(upstream) == 0:
1826            die("Cannot find upstream branchpoint for rebase")
1827
1828        # the branchpoint may be p4/foo~3, so strip off the parent
1829        upstream = re.sub("~[0-9]+$", "", upstream)
1830
1831        print "Rebasing the current branch onto %s" % upstream
1832        oldHead = read_pipe("git rev-parse HEAD").strip()
1833        system("git rebase %s" % upstream)
1834        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1835        return True
1836
1837class P4Clone(P4Sync):
1838    def __init__(self):
1839        P4Sync.__init__(self)
1840        self.description = "Creates a new git repository and imports from Perforce into it"
1841        self.usage = "usage: %prog [options] //depot/path[@revRange]"
1842        self.options += [
1843            optparse.make_option("--destination", dest="cloneDestination",
1844                                 action='store', default=None,
1845                                 help="where to leave result of the clone"),
1846            optparse.make_option("-/", dest="cloneExclude",
1847                                 action="append", type="string",
1848                                 help="exclude depot path"),
1849            optparse.make_option("--bare", dest="cloneBare",
1850                                 action="store_true", default=False),
1851        ]
1852        self.cloneDestination = None
1853        self.needsGit = False
1854        self.cloneBare = False
1855
1856    # This is required for the "append" cloneExclude action
1857    def ensure_value(self, attr, value):
1858        if not hasattr(self, attr) or getattr(self, attr) is None:
1859            setattr(self, attr, value)
1860        return getattr(self, attr)
1861
1862    def defaultDestination(self, args):
1863        ## TODO: use common prefix of args?
1864        depotPath = args[0]
1865        depotDir = re.sub("(@[^@]*)$", "", depotPath)
1866        depotDir = re.sub("(#[^#]*)$", "", depotDir)
1867        depotDir = re.sub(r"\.\.\.$", "", depotDir)
1868        depotDir = re.sub(r"/$", "", depotDir)
1869        return os.path.split(depotDir)[1]
1870
1871    def run(self, args):
1872        if len(args) < 1:
1873            return False
1874
1875        if self.keepRepoPath and not self.cloneDestination:
1876            sys.stderr.write("Must specify destination for --keep-path\n")
1877            sys.exit(1)
1878
1879        depotPaths = args
1880
1881        if not self.cloneDestination and len(depotPaths) > 1:
1882            self.cloneDestination = depotPaths[-1]
1883            depotPaths = depotPaths[:-1]
1884
1885        self.cloneExclude = ["/"+p for p in self.cloneExclude]
1886        for p in depotPaths:
1887            if not p.startswith("//"):
1888                return False
1889
1890        if not self.cloneDestination:
1891            self.cloneDestination = self.defaultDestination(args)
1892
1893        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1894
1895        if not os.path.exists(self.cloneDestination):
1896            os.makedirs(self.cloneDestination)
1897        chdir(self.cloneDestination)
1898
1899        init_cmd = [ "git", "init" ]
1900        if self.cloneBare:
1901            init_cmd.append("--bare")
1902        subprocess.check_call(init_cmd)
1903
1904        if not P4Sync.run(self, depotPaths):
1905            return False
1906        if self.branch != "master":
1907            if self.importIntoRemotes:
1908                masterbranch = "refs/remotes/p4/master"
1909            else:
1910                masterbranch = "refs/heads/p4/master"
1911            if gitBranchExists(masterbranch):
1912                system("git branch master %s" % masterbranch)
1913                if not self.cloneBare:
1914                    system("git checkout -f")
1915            else:
1916                print "Could not detect main branch. No checkout/master branch created."
1917
1918        return True
1919
1920class P4Branches(Command):
1921    def __init__(self):
1922        Command.__init__(self)
1923        self.options = [ ]
1924        self.description = ("Shows the git branches that hold imports and their "
1925                            + "corresponding perforce depot paths")
1926        self.verbose = False
1927
1928    def run(self, args):
1929        if originP4BranchesExist():
1930            createOrUpdateBranchesFromOrigin()
1931
1932        cmdline = "git rev-parse --symbolic "
1933        cmdline += " --remotes"
1934
1935        for line in read_pipe_lines(cmdline):
1936            line = line.strip()
1937
1938            if not line.startswith('p4/') or line == "p4/HEAD":
1939                continue
1940            branch = line
1941
1942            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1943            settings = extractSettingsGitLog(log)
1944
1945            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1946        return True
1947
1948class HelpFormatter(optparse.IndentedHelpFormatter):
1949    def __init__(self):
1950        optparse.IndentedHelpFormatter.__init__(self)
1951
1952    def format_description(self, description):
1953        if description:
1954            return description + "\n"
1955        else:
1956            return ""
1957
1958def printUsage(commands):
1959    print "usage: %s <command> [options]" % sys.argv[0]
1960    print ""
1961    print "valid commands: %s" % ", ".join(commands)
1962    print ""
1963    print "Try %s <command> --help for command specific help." % sys.argv[0]
1964    print ""
1965
1966commands = {
1967    "debug" : P4Debug,
1968    "submit" : P4Submit,
1969    "commit" : P4Submit,
1970    "sync" : P4Sync,
1971    "rebase" : P4Rebase,
1972    "clone" : P4Clone,
1973    "rollback" : P4RollBack,
1974    "branches" : P4Branches
1975}
1976
1977
1978def main():
1979    if len(sys.argv[1:]) == 0:
1980        printUsage(commands.keys())
1981        sys.exit(2)
1982
1983    cmd = ""
1984    cmdName = sys.argv[1]
1985    try:
1986        klass = commands[cmdName]
1987        cmd = klass()
1988    except KeyError:
1989        print "unknown command %s" % cmdName
1990        print ""
1991        printUsage(commands.keys())
1992        sys.exit(2)
1993
1994    options = cmd.options
1995    cmd.gitdir = os.environ.get("GIT_DIR", None)
1996
1997    args = sys.argv[2:]
1998
1999    if len(options) > 0:
2000        options.append(optparse.make_option("--git-dir", dest="gitdir"))
2001
2002        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2003                                       options,
2004                                       description = cmd.description,
2005                                       formatter = HelpFormatter())
2006
2007        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2008    global verbose
2009    verbose = cmd.verbose
2010    if cmd.needsGit:
2011        if cmd.gitdir == None:
2012            cmd.gitdir = os.path.abspath(".git")
2013            if not isValidGitDir(cmd.gitdir):
2014                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2015                if os.path.exists(cmd.gitdir):
2016                    cdup = read_pipe("git rev-parse --show-cdup").strip()
2017                    if len(cdup) > 0:
2018                        chdir(cdup);
2019
2020        if not isValidGitDir(cmd.gitdir):
2021            if isValidGitDir(cmd.gitdir + "/.git"):
2022                cmd.gitdir += "/.git"
2023            else:
2024                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2025
2026        os.environ["GIT_DIR"] = cmd.gitdir
2027
2028    if not cmd.run(args):
2029        parser.print_help()
2030
2031
2032if __name__ == '__main__':
2033    main()