ae1e9ea52e1a61034f9e6b48d2ad25c10403a590
   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#
  10import sys
  11if sys.hexversion < 0x02040000:
  12    # The limiter is the subprocess module
  13    sys.stderr.write("git-p4: requires Python 2.4 or later.\n")
  14    sys.exit(1)
  15import os
  16import optparse
  17import marshal
  18import subprocess
  19import tempfile
  20import time
  21import platform
  22import re
  23import shutil
  24import stat
  25
  26try:
  27    from subprocess import CalledProcessError
  28except ImportError:
  29    # from python2.7:subprocess.py
  30    # Exception classes used by this module.
  31    class CalledProcessError(Exception):
  32        """This exception is raised when a process run by check_call() returns
  33        a non-zero exit status.  The exit status will be stored in the
  34        returncode attribute."""
  35        def __init__(self, returncode, cmd):
  36            self.returncode = returncode
  37            self.cmd = cmd
  38        def __str__(self):
  39            return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
  40
  41verbose = False
  42
  43# Only labels/tags matching this will be imported/exported
  44defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
  45
  46# Grab changes in blocks of this many revisions, unless otherwise requested
  47defaultBlockSize = 512
  48
  49def p4_build_cmd(cmd):
  50    """Build a suitable p4 command line.
  51
  52    This consolidates building and returning a p4 command line into one
  53    location. It means that hooking into the environment, or other configuration
  54    can be done more easily.
  55    """
  56    real_cmd = ["p4"]
  57
  58    user = gitConfig("git-p4.user")
  59    if len(user) > 0:
  60        real_cmd += ["-u",user]
  61
  62    password = gitConfig("git-p4.password")
  63    if len(password) > 0:
  64        real_cmd += ["-P", password]
  65
  66    port = gitConfig("git-p4.port")
  67    if len(port) > 0:
  68        real_cmd += ["-p", port]
  69
  70    host = gitConfig("git-p4.host")
  71    if len(host) > 0:
  72        real_cmd += ["-H", host]
  73
  74    client = gitConfig("git-p4.client")
  75    if len(client) > 0:
  76        real_cmd += ["-c", client]
  77
  78
  79    if isinstance(cmd,basestring):
  80        real_cmd = ' '.join(real_cmd) + ' ' + cmd
  81    else:
  82        real_cmd += cmd
  83    return real_cmd
  84
  85def chdir(path, is_client_path=False):
  86    """Do chdir to the given path, and set the PWD environment
  87       variable for use by P4.  It does not look at getcwd() output.
  88       Since we're not using the shell, it is necessary to set the
  89       PWD environment variable explicitly.
  90
  91       Normally, expand the path to force it to be absolute.  This
  92       addresses the use of relative path names inside P4 settings,
  93       e.g. P4CONFIG=.p4config.  P4 does not simply open the filename
  94       as given; it looks for .p4config using PWD.
  95
  96       If is_client_path, the path was handed to us directly by p4,
  97       and may be a symbolic link.  Do not call os.getcwd() in this
  98       case, because it will cause p4 to think that PWD is not inside
  99       the client path.
 100       """
 101
 102    os.chdir(path)
 103    if not is_client_path:
 104        path = os.getcwd()
 105    os.environ['PWD'] = path
 106
 107def die(msg):
 108    if verbose:
 109        raise Exception(msg)
 110    else:
 111        sys.stderr.write(msg + "\n")
 112        sys.exit(1)
 113
 114def write_pipe(c, stdin):
 115    if verbose:
 116        sys.stderr.write('Writing pipe: %s\n' % str(c))
 117
 118    expand = isinstance(c,basestring)
 119    p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
 120    pipe = p.stdin
 121    val = pipe.write(stdin)
 122    pipe.close()
 123    if p.wait():
 124        die('Command failed: %s' % str(c))
 125
 126    return val
 127
 128def p4_write_pipe(c, stdin):
 129    real_cmd = p4_build_cmd(c)
 130    return write_pipe(real_cmd, stdin)
 131
 132def read_pipe(c, ignore_error=False):
 133    if verbose:
 134        sys.stderr.write('Reading pipe: %s\n' % str(c))
 135
 136    expand = isinstance(c,basestring)
 137    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
 138    pipe = p.stdout
 139    val = pipe.read()
 140    if p.wait() and not ignore_error:
 141        die('Command failed: %s' % str(c))
 142
 143    return val
 144
 145def p4_read_pipe(c, ignore_error=False):
 146    real_cmd = p4_build_cmd(c)
 147    return read_pipe(real_cmd, ignore_error)
 148
 149def read_pipe_lines(c):
 150    if verbose:
 151        sys.stderr.write('Reading pipe: %s\n' % str(c))
 152
 153    expand = isinstance(c, basestring)
 154    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
 155    pipe = p.stdout
 156    val = pipe.readlines()
 157    if pipe.close() or p.wait():
 158        die('Command failed: %s' % str(c))
 159
 160    return val
 161
 162def p4_read_pipe_lines(c):
 163    """Specifically invoke p4 on the command supplied. """
 164    real_cmd = p4_build_cmd(c)
 165    return read_pipe_lines(real_cmd)
 166
 167def p4_has_command(cmd):
 168    """Ask p4 for help on this command.  If it returns an error, the
 169       command does not exist in this version of p4."""
 170    real_cmd = p4_build_cmd(["help", cmd])
 171    p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
 172                                   stderr=subprocess.PIPE)
 173    p.communicate()
 174    return p.returncode == 0
 175
 176def p4_has_move_command():
 177    """See if the move command exists, that it supports -k, and that
 178       it has not been administratively disabled.  The arguments
 179       must be correct, but the filenames do not have to exist.  Use
 180       ones with wildcards so even if they exist, it will fail."""
 181
 182    if not p4_has_command("move"):
 183        return False
 184    cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
 185    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 186    (out, err) = p.communicate()
 187    # return code will be 1 in either case
 188    if err.find("Invalid option") >= 0:
 189        return False
 190    if err.find("disabled") >= 0:
 191        return False
 192    # assume it failed because @... was invalid changelist
 193    return True
 194
 195def system(cmd):
 196    expand = isinstance(cmd,basestring)
 197    if verbose:
 198        sys.stderr.write("executing %s\n" % str(cmd))
 199    retcode = subprocess.call(cmd, shell=expand)
 200    if retcode:
 201        raise CalledProcessError(retcode, cmd)
 202
 203def p4_system(cmd):
 204    """Specifically invoke p4 as the system command. """
 205    real_cmd = p4_build_cmd(cmd)
 206    expand = isinstance(real_cmd, basestring)
 207    retcode = subprocess.call(real_cmd, shell=expand)
 208    if retcode:
 209        raise CalledProcessError(retcode, real_cmd)
 210
 211_p4_version_string = None
 212def p4_version_string():
 213    """Read the version string, showing just the last line, which
 214       hopefully is the interesting version bit.
 215
 216       $ p4 -V
 217       Perforce - The Fast Software Configuration Management System.
 218       Copyright 1995-2011 Perforce Software.  All rights reserved.
 219       Rev. P4/NTX86/2011.1/393975 (2011/12/16).
 220    """
 221    global _p4_version_string
 222    if not _p4_version_string:
 223        a = p4_read_pipe_lines(["-V"])
 224        _p4_version_string = a[-1].rstrip()
 225    return _p4_version_string
 226
 227def p4_integrate(src, dest):
 228    p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
 229
 230def p4_sync(f, *options):
 231    p4_system(["sync"] + list(options) + [wildcard_encode(f)])
 232
 233def p4_add(f):
 234    # forcibly add file names with wildcards
 235    if wildcard_present(f):
 236        p4_system(["add", "-f", f])
 237    else:
 238        p4_system(["add", f])
 239
 240def p4_delete(f):
 241    p4_system(["delete", wildcard_encode(f)])
 242
 243def p4_edit(f):
 244    p4_system(["edit", wildcard_encode(f)])
 245
 246def p4_revert(f):
 247    p4_system(["revert", wildcard_encode(f)])
 248
 249def p4_reopen(type, f):
 250    p4_system(["reopen", "-t", type, wildcard_encode(f)])
 251
 252def p4_move(src, dest):
 253    p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
 254
 255def p4_last_change():
 256    results = p4CmdList(["changes", "-m", "1"])
 257    return int(results[0]['change'])
 258
 259def p4_describe(change):
 260    """Make sure it returns a valid result by checking for
 261       the presence of field "time".  Return a dict of the
 262       results."""
 263
 264    ds = p4CmdList(["describe", "-s", str(change)])
 265    if len(ds) != 1:
 266        die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
 267
 268    d = ds[0]
 269
 270    if "p4ExitCode" in d:
 271        die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
 272                                                      str(d)))
 273    if "code" in d:
 274        if d["code"] == "error":
 275            die("p4 describe -s %d returned error code: %s" % (change, str(d)))
 276
 277    if "time" not in d:
 278        die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
 279
 280    return d
 281
 282#
 283# Canonicalize the p4 type and return a tuple of the
 284# base type, plus any modifiers.  See "p4 help filetypes"
 285# for a list and explanation.
 286#
 287def split_p4_type(p4type):
 288
 289    p4_filetypes_historical = {
 290        "ctempobj": "binary+Sw",
 291        "ctext": "text+C",
 292        "cxtext": "text+Cx",
 293        "ktext": "text+k",
 294        "kxtext": "text+kx",
 295        "ltext": "text+F",
 296        "tempobj": "binary+FSw",
 297        "ubinary": "binary+F",
 298        "uresource": "resource+F",
 299        "uxbinary": "binary+Fx",
 300        "xbinary": "binary+x",
 301        "xltext": "text+Fx",
 302        "xtempobj": "binary+Swx",
 303        "xtext": "text+x",
 304        "xunicode": "unicode+x",
 305        "xutf16": "utf16+x",
 306    }
 307    if p4type in p4_filetypes_historical:
 308        p4type = p4_filetypes_historical[p4type]
 309    mods = ""
 310    s = p4type.split("+")
 311    base = s[0]
 312    mods = ""
 313    if len(s) > 1:
 314        mods = s[1]
 315    return (base, mods)
 316
 317#
 318# return the raw p4 type of a file (text, text+ko, etc)
 319#
 320def p4_type(f):
 321    results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
 322    return results[0]['headType']
 323
 324#
 325# Given a type base and modifier, return a regexp matching
 326# the keywords that can be expanded in the file
 327#
 328def p4_keywords_regexp_for_type(base, type_mods):
 329    if base in ("text", "unicode", "binary"):
 330        kwords = None
 331        if "ko" in type_mods:
 332            kwords = 'Id|Header'
 333        elif "k" in type_mods:
 334            kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
 335        else:
 336            return None
 337        pattern = r"""
 338            \$              # Starts with a dollar, followed by...
 339            (%s)            # one of the keywords, followed by...
 340            (:[^$\n]+)?     # possibly an old expansion, followed by...
 341            \$              # another dollar
 342            """ % kwords
 343        return pattern
 344    else:
 345        return None
 346
 347#
 348# Given a file, return a regexp matching the possible
 349# RCS keywords that will be expanded, or None for files
 350# with kw expansion turned off.
 351#
 352def p4_keywords_regexp_for_file(file):
 353    if not os.path.exists(file):
 354        return None
 355    else:
 356        (type_base, type_mods) = split_p4_type(p4_type(file))
 357        return p4_keywords_regexp_for_type(type_base, type_mods)
 358
 359def setP4ExecBit(file, mode):
 360    # Reopens an already open file and changes the execute bit to match
 361    # the execute bit setting in the passed in mode.
 362
 363    p4Type = "+x"
 364
 365    if not isModeExec(mode):
 366        p4Type = getP4OpenedType(file)
 367        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
 368        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
 369        if p4Type[-1] == "+":
 370            p4Type = p4Type[0:-1]
 371
 372    p4_reopen(p4Type, file)
 373
 374def getP4OpenedType(file):
 375    # Returns the perforce file type for the given file.
 376
 377    result = p4_read_pipe(["opened", wildcard_encode(file)])
 378    match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
 379    if match:
 380        return match.group(1)
 381    else:
 382        die("Could not determine file type for %s (result: '%s')" % (file, result))
 383
 384# Return the set of all p4 labels
 385def getP4Labels(depotPaths):
 386    labels = set()
 387    if isinstance(depotPaths,basestring):
 388        depotPaths = [depotPaths]
 389
 390    for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
 391        label = l['label']
 392        labels.add(label)
 393
 394    return labels
 395
 396# Return the set of all git tags
 397def getGitTags():
 398    gitTags = set()
 399    for line in read_pipe_lines(["git", "tag"]):
 400        tag = line.strip()
 401        gitTags.add(tag)
 402    return gitTags
 403
 404def diffTreePattern():
 405    # This is a simple generator for the diff tree regex pattern. This could be
 406    # a class variable if this and parseDiffTreeEntry were a part of a class.
 407    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
 408    while True:
 409        yield pattern
 410
 411def parseDiffTreeEntry(entry):
 412    """Parses a single diff tree entry into its component elements.
 413
 414    See git-diff-tree(1) manpage for details about the format of the diff
 415    output. This method returns a dictionary with the following elements:
 416
 417    src_mode - The mode of the source file
 418    dst_mode - The mode of the destination file
 419    src_sha1 - The sha1 for the source file
 420    dst_sha1 - The sha1 fr the destination file
 421    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
 422    status_score - The score for the status (applicable for 'C' and 'R'
 423                   statuses). This is None if there is no score.
 424    src - The path for the source file.
 425    dst - The path for the destination file. This is only present for
 426          copy or renames. If it is not present, this is None.
 427
 428    If the pattern is not matched, None is returned."""
 429
 430    match = diffTreePattern().next().match(entry)
 431    if match:
 432        return {
 433            'src_mode': match.group(1),
 434            'dst_mode': match.group(2),
 435            'src_sha1': match.group(3),
 436            'dst_sha1': match.group(4),
 437            'status': match.group(5),
 438            'status_score': match.group(6),
 439            'src': match.group(7),
 440            'dst': match.group(10)
 441        }
 442    return None
 443
 444def isModeExec(mode):
 445    # Returns True if the given git mode represents an executable file,
 446    # otherwise False.
 447    return mode[-3:] == "755"
 448
 449def isModeExecChanged(src_mode, dst_mode):
 450    return isModeExec(src_mode) != isModeExec(dst_mode)
 451
 452def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
 453
 454    if isinstance(cmd,basestring):
 455        cmd = "-G " + cmd
 456        expand = True
 457    else:
 458        cmd = ["-G"] + cmd
 459        expand = False
 460
 461    cmd = p4_build_cmd(cmd)
 462    if verbose:
 463        sys.stderr.write("Opening pipe: %s\n" % str(cmd))
 464
 465    # Use a temporary file to avoid deadlocks without
 466    # subprocess.communicate(), which would put another copy
 467    # of stdout into memory.
 468    stdin_file = None
 469    if stdin is not None:
 470        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
 471        if isinstance(stdin,basestring):
 472            stdin_file.write(stdin)
 473        else:
 474            for i in stdin:
 475                stdin_file.write(i + '\n')
 476        stdin_file.flush()
 477        stdin_file.seek(0)
 478
 479    p4 = subprocess.Popen(cmd,
 480                          shell=expand,
 481                          stdin=stdin_file,
 482                          stdout=subprocess.PIPE)
 483
 484    result = []
 485    try:
 486        while True:
 487            entry = marshal.load(p4.stdout)
 488            if cb is not None:
 489                cb(entry)
 490            else:
 491                result.append(entry)
 492    except EOFError:
 493        pass
 494    exitCode = p4.wait()
 495    if exitCode != 0:
 496        entry = {}
 497        entry["p4ExitCode"] = exitCode
 498        result.append(entry)
 499
 500    return result
 501
 502def p4Cmd(cmd):
 503    list = p4CmdList(cmd)
 504    result = {}
 505    for entry in list:
 506        result.update(entry)
 507    return result;
 508
 509def p4Where(depotPath):
 510    if not depotPath.endswith("/"):
 511        depotPath += "/"
 512    depotPathLong = depotPath + "..."
 513    outputList = p4CmdList(["where", depotPathLong])
 514    output = None
 515    for entry in outputList:
 516        if "depotFile" in entry:
 517            # Search for the base client side depot path, as long as it starts with the branch's P4 path.
 518            # The base path always ends with "/...".
 519            if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
 520                output = entry
 521                break
 522        elif "data" in entry:
 523            data = entry.get("data")
 524            space = data.find(" ")
 525            if data[:space] == depotPath:
 526                output = entry
 527                break
 528    if output == None:
 529        return ""
 530    if output["code"] == "error":
 531        return ""
 532    clientPath = ""
 533    if "path" in output:
 534        clientPath = output.get("path")
 535    elif "data" in output:
 536        data = output.get("data")
 537        lastSpace = data.rfind(" ")
 538        clientPath = data[lastSpace + 1:]
 539
 540    if clientPath.endswith("..."):
 541        clientPath = clientPath[:-3]
 542    return clientPath
 543
 544def currentGitBranch():
 545    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
 546
 547def isValidGitDir(path):
 548    if (os.path.exists(path + "/HEAD")
 549        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
 550        return True;
 551    return False
 552
 553def parseRevision(ref):
 554    return read_pipe("git rev-parse %s" % ref).strip()
 555
 556def branchExists(ref):
 557    rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
 558                     ignore_error=True)
 559    return len(rev) > 0
 560
 561def extractLogMessageFromGitCommit(commit):
 562    logMessage = ""
 563
 564    ## fixme: title is first line of commit, not 1st paragraph.
 565    foundTitle = False
 566    for log in read_pipe_lines("git cat-file commit %s" % commit):
 567       if not foundTitle:
 568           if len(log) == 1:
 569               foundTitle = True
 570           continue
 571
 572       logMessage += log
 573    return logMessage
 574
 575def extractSettingsGitLog(log):
 576    values = {}
 577    for line in log.split("\n"):
 578        line = line.strip()
 579        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
 580        if not m:
 581            continue
 582
 583        assignments = m.group(1).split (':')
 584        for a in assignments:
 585            vals = a.split ('=')
 586            key = vals[0].strip()
 587            val = ('='.join (vals[1:])).strip()
 588            if val.endswith ('\"') and val.startswith('"'):
 589                val = val[1:-1]
 590
 591            values[key] = val
 592
 593    paths = values.get("depot-paths")
 594    if not paths:
 595        paths = values.get("depot-path")
 596    if paths:
 597        values['depot-paths'] = paths.split(',')
 598    return values
 599
 600def gitBranchExists(branch):
 601    proc = subprocess.Popen(["git", "rev-parse", branch],
 602                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
 603    return proc.wait() == 0;
 604
 605_gitConfig = {}
 606
 607def gitConfig(key, typeSpecifier=None):
 608    if not _gitConfig.has_key(key):
 609        cmd = [ "git", "config" ]
 610        if typeSpecifier:
 611            cmd += [ typeSpecifier ]
 612        cmd += [ key ]
 613        s = read_pipe(cmd, ignore_error=True)
 614        _gitConfig[key] = s.strip()
 615    return _gitConfig[key]
 616
 617def gitConfigBool(key):
 618    """Return a bool, using git config --bool.  It is True only if the
 619       variable is set to true, and False if set to false or not present
 620       in the config."""
 621
 622    if not _gitConfig.has_key(key):
 623        _gitConfig[key] = gitConfig(key, '--bool') == "true"
 624    return _gitConfig[key]
 625
 626def gitConfigInt(key):
 627    if not _gitConfig.has_key(key):
 628        cmd = [ "git", "config", "--int", key ]
 629        s = read_pipe(cmd, ignore_error=True)
 630        v = s.strip()
 631        try:
 632            _gitConfig[key] = int(gitConfig(key, '--int'))
 633        except ValueError:
 634            _gitConfig[key] = None
 635    return _gitConfig[key]
 636
 637def gitConfigList(key):
 638    if not _gitConfig.has_key(key):
 639        s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
 640        _gitConfig[key] = s.strip().split(os.linesep)
 641        if _gitConfig[key] == ['']:
 642            _gitConfig[key] = []
 643    return _gitConfig[key]
 644
 645def p4BranchesInGit(branchesAreInRemotes=True):
 646    """Find all the branches whose names start with "p4/", looking
 647       in remotes or heads as specified by the argument.  Return
 648       a dictionary of { branch: revision } for each one found.
 649       The branch names are the short names, without any
 650       "p4/" prefix."""
 651
 652    branches = {}
 653
 654    cmdline = "git rev-parse --symbolic "
 655    if branchesAreInRemotes:
 656        cmdline += "--remotes"
 657    else:
 658        cmdline += "--branches"
 659
 660    for line in read_pipe_lines(cmdline):
 661        line = line.strip()
 662
 663        # only import to p4/
 664        if not line.startswith('p4/'):
 665            continue
 666        # special symbolic ref to p4/master
 667        if line == "p4/HEAD":
 668            continue
 669
 670        # strip off p4/ prefix
 671        branch = line[len("p4/"):]
 672
 673        branches[branch] = parseRevision(line)
 674
 675    return branches
 676
 677def branch_exists(branch):
 678    """Make sure that the given ref name really exists."""
 679
 680    cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
 681    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 682    out, _ = p.communicate()
 683    if p.returncode:
 684        return False
 685    # expect exactly one line of output: the branch name
 686    return out.rstrip() == branch
 687
 688def findUpstreamBranchPoint(head = "HEAD"):
 689    branches = p4BranchesInGit()
 690    # map from depot-path to branch name
 691    branchByDepotPath = {}
 692    for branch in branches.keys():
 693        tip = branches[branch]
 694        log = extractLogMessageFromGitCommit(tip)
 695        settings = extractSettingsGitLog(log)
 696        if settings.has_key("depot-paths"):
 697            paths = ",".join(settings["depot-paths"])
 698            branchByDepotPath[paths] = "remotes/p4/" + branch
 699
 700    settings = None
 701    parent = 0
 702    while parent < 65535:
 703        commit = head + "~%s" % parent
 704        log = extractLogMessageFromGitCommit(commit)
 705        settings = extractSettingsGitLog(log)
 706        if settings.has_key("depot-paths"):
 707            paths = ",".join(settings["depot-paths"])
 708            if branchByDepotPath.has_key(paths):
 709                return [branchByDepotPath[paths], settings]
 710
 711        parent = parent + 1
 712
 713    return ["", settings]
 714
 715def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
 716    if not silent:
 717        print ("Creating/updating branch(es) in %s based on origin branch(es)"
 718               % localRefPrefix)
 719
 720    originPrefix = "origin/p4/"
 721
 722    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 723        line = line.strip()
 724        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
 725            continue
 726
 727        headName = line[len(originPrefix):]
 728        remoteHead = localRefPrefix + headName
 729        originHead = line
 730
 731        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
 732        if (not original.has_key('depot-paths')
 733            or not original.has_key('change')):
 734            continue
 735
 736        update = False
 737        if not gitBranchExists(remoteHead):
 738            if verbose:
 739                print "creating %s" % remoteHead
 740            update = True
 741        else:
 742            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
 743            if settings.has_key('change') > 0:
 744                if settings['depot-paths'] == original['depot-paths']:
 745                    originP4Change = int(original['change'])
 746                    p4Change = int(settings['change'])
 747                    if originP4Change > p4Change:
 748                        print ("%s (%s) is newer than %s (%s). "
 749                               "Updating p4 branch from origin."
 750                               % (originHead, originP4Change,
 751                                  remoteHead, p4Change))
 752                        update = True
 753                else:
 754                    print ("Ignoring: %s was imported from %s while "
 755                           "%s was imported from %s"
 756                           % (originHead, ','.join(original['depot-paths']),
 757                              remoteHead, ','.join(settings['depot-paths'])))
 758
 759        if update:
 760            system("git update-ref %s %s" % (remoteHead, originHead))
 761
 762def originP4BranchesExist():
 763        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
 764
 765
 766def p4ParseNumericChangeRange(parts):
 767    changeStart = int(parts[0][1:])
 768    if parts[1] == '#head':
 769        changeEnd = p4_last_change()
 770    else:
 771        changeEnd = int(parts[1])
 772
 773    return (changeStart, changeEnd)
 774
 775def chooseBlockSize(blockSize):
 776    if blockSize:
 777        return blockSize
 778    else:
 779        return defaultBlockSize
 780
 781def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
 782    assert depotPaths
 783
 784    # Parse the change range into start and end. Try to find integer
 785    # revision ranges as these can be broken up into blocks to avoid
 786    # hitting server-side limits (maxrows, maxscanresults). But if
 787    # that doesn't work, fall back to using the raw revision specifier
 788    # strings, without using block mode.
 789
 790    if changeRange is None or changeRange == '':
 791        changeStart = 1
 792        changeEnd = p4_last_change()
 793        block_size = chooseBlockSize(requestedBlockSize)
 794    else:
 795        parts = changeRange.split(',')
 796        assert len(parts) == 2
 797        try:
 798            (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
 799            block_size = chooseBlockSize(requestedBlockSize)
 800        except:
 801            changeStart = parts[0][1:]
 802            changeEnd = parts[1]
 803            if requestedBlockSize:
 804                die("cannot use --changes-block-size with non-numeric revisions")
 805            block_size = None
 806
 807    # Accumulate change numbers in a dictionary to avoid duplicates
 808    changes = {}
 809
 810    for p in depotPaths:
 811        # Retrieve changes a block at a time, to prevent running
 812        # into a MaxResults/MaxScanRows error from the server.
 813
 814        while True:
 815            cmd = ['changes']
 816
 817            if block_size:
 818                end = min(changeEnd, changeStart + block_size)
 819                revisionRange = "%d,%d" % (changeStart, end)
 820            else:
 821                revisionRange = "%s,%s" % (changeStart, changeEnd)
 822
 823            cmd += ["%s...@%s" % (p, revisionRange)]
 824
 825            for line in p4_read_pipe_lines(cmd):
 826                changeNum = int(line.split(" ")[1])
 827                changes[changeNum] = True
 828
 829            if not block_size:
 830                break
 831
 832            if end >= changeEnd:
 833                break
 834
 835            changeStart = end + 1
 836
 837    changelist = changes.keys()
 838    changelist.sort()
 839    return changelist
 840
 841def p4PathStartsWith(path, prefix):
 842    # This method tries to remedy a potential mixed-case issue:
 843    #
 844    # If UserA adds  //depot/DirA/file1
 845    # and UserB adds //depot/dira/file2
 846    #
 847    # we may or may not have a problem. If you have core.ignorecase=true,
 848    # we treat DirA and dira as the same directory
 849    if gitConfigBool("core.ignorecase"):
 850        return path.lower().startswith(prefix.lower())
 851    return path.startswith(prefix)
 852
 853def getClientSpec():
 854    """Look at the p4 client spec, create a View() object that contains
 855       all the mappings, and return it."""
 856
 857    specList = p4CmdList("client -o")
 858    if len(specList) != 1:
 859        die('Output from "client -o" is %d lines, expecting 1' %
 860            len(specList))
 861
 862    # dictionary of all client parameters
 863    entry = specList[0]
 864
 865    # the //client/ name
 866    client_name = entry["Client"]
 867
 868    # just the keys that start with "View"
 869    view_keys = [ k for k in entry.keys() if k.startswith("View") ]
 870
 871    # hold this new View
 872    view = View(client_name)
 873
 874    # append the lines, in order, to the view
 875    for view_num in range(len(view_keys)):
 876        k = "View%d" % view_num
 877        if k not in view_keys:
 878            die("Expected view key %s missing" % k)
 879        view.append(entry[k])
 880
 881    return view
 882
 883def getClientRoot():
 884    """Grab the client directory."""
 885
 886    output = p4CmdList("client -o")
 887    if len(output) != 1:
 888        die('Output from "client -o" is %d lines, expecting 1' % len(output))
 889
 890    entry = output[0]
 891    if "Root" not in entry:
 892        die('Client has no "Root"')
 893
 894    return entry["Root"]
 895
 896#
 897# P4 wildcards are not allowed in filenames.  P4 complains
 898# if you simply add them, but you can force it with "-f", in
 899# which case it translates them into %xx encoding internally.
 900#
 901def wildcard_decode(path):
 902    # Search for and fix just these four characters.  Do % last so
 903    # that fixing it does not inadvertently create new %-escapes.
 904    # Cannot have * in a filename in windows; untested as to
 905    # what p4 would do in such a case.
 906    if not platform.system() == "Windows":
 907        path = path.replace("%2A", "*")
 908    path = path.replace("%23", "#") \
 909               .replace("%40", "@") \
 910               .replace("%25", "%")
 911    return path
 912
 913def wildcard_encode(path):
 914    # do % first to avoid double-encoding the %s introduced here
 915    path = path.replace("%", "%25") \
 916               .replace("*", "%2A") \
 917               .replace("#", "%23") \
 918               .replace("@", "%40")
 919    return path
 920
 921def wildcard_present(path):
 922    m = re.search("[*#@%]", path)
 923    return m is not None
 924
 925class Command:
 926    def __init__(self):
 927        self.usage = "usage: %prog [options]"
 928        self.needsGit = True
 929        self.verbose = False
 930
 931class P4UserMap:
 932    def __init__(self):
 933        self.userMapFromPerforceServer = False
 934        self.myP4UserId = None
 935
 936    def p4UserId(self):
 937        if self.myP4UserId:
 938            return self.myP4UserId
 939
 940        results = p4CmdList("user -o")
 941        for r in results:
 942            if r.has_key('User'):
 943                self.myP4UserId = r['User']
 944                return r['User']
 945        die("Could not find your p4 user id")
 946
 947    def p4UserIsMe(self, p4User):
 948        # return True if the given p4 user is actually me
 949        me = self.p4UserId()
 950        if not p4User or p4User != me:
 951            return False
 952        else:
 953            return True
 954
 955    def getUserCacheFilename(self):
 956        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
 957        return home + "/.gitp4-usercache.txt"
 958
 959    def getUserMapFromPerforceServer(self):
 960        if self.userMapFromPerforceServer:
 961            return
 962        self.users = {}
 963        self.emails = {}
 964
 965        for output in p4CmdList("users"):
 966            if not output.has_key("User"):
 967                continue
 968            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
 969            self.emails[output["Email"]] = output["User"]
 970
 971
 972        s = ''
 973        for (key, val) in self.users.items():
 974            s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
 975
 976        open(self.getUserCacheFilename(), "wb").write(s)
 977        self.userMapFromPerforceServer = True
 978
 979    def loadUserMapFromCache(self):
 980        self.users = {}
 981        self.userMapFromPerforceServer = False
 982        try:
 983            cache = open(self.getUserCacheFilename(), "rb")
 984            lines = cache.readlines()
 985            cache.close()
 986            for line in lines:
 987                entry = line.strip().split("\t")
 988                self.users[entry[0]] = entry[1]
 989        except IOError:
 990            self.getUserMapFromPerforceServer()
 991
 992class P4Debug(Command):
 993    def __init__(self):
 994        Command.__init__(self)
 995        self.options = []
 996        self.description = "A tool to debug the output of p4 -G."
 997        self.needsGit = False
 998
 999    def run(self, args):
1000        j = 0
1001        for output in p4CmdList(args):
1002            print 'Element: %d' % j
1003            j += 1
1004            print output
1005        return True
1006
1007class P4RollBack(Command):
1008    def __init__(self):
1009        Command.__init__(self)
1010        self.options = [
1011            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
1012        ]
1013        self.description = "A tool to debug the multi-branch import. Don't use :)"
1014        self.rollbackLocalBranches = False
1015
1016    def run(self, args):
1017        if len(args) != 1:
1018            return False
1019        maxChange = int(args[0])
1020
1021        if "p4ExitCode" in p4Cmd("changes -m 1"):
1022            die("Problems executing p4");
1023
1024        if self.rollbackLocalBranches:
1025            refPrefix = "refs/heads/"
1026            lines = read_pipe_lines("git rev-parse --symbolic --branches")
1027        else:
1028            refPrefix = "refs/remotes/"
1029            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1030
1031        for line in lines:
1032            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1033                line = line.strip()
1034                ref = refPrefix + line
1035                log = extractLogMessageFromGitCommit(ref)
1036                settings = extractSettingsGitLog(log)
1037
1038                depotPaths = settings['depot-paths']
1039                change = settings['change']
1040
1041                changed = False
1042
1043                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
1044                                                           for p in depotPaths]))) == 0:
1045                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
1046                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1047                    continue
1048
1049                while change and int(change) > maxChange:
1050                    changed = True
1051                    if self.verbose:
1052                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
1053                    system("git update-ref %s \"%s^\"" % (ref, ref))
1054                    log = extractLogMessageFromGitCommit(ref)
1055                    settings =  extractSettingsGitLog(log)
1056
1057
1058                    depotPaths = settings['depot-paths']
1059                    change = settings['change']
1060
1061                if changed:
1062                    print "%s rewound to %s" % (ref, change)
1063
1064        return True
1065
1066class P4Submit(Command, P4UserMap):
1067
1068    conflict_behavior_choices = ("ask", "skip", "quit")
1069
1070    def __init__(self):
1071        Command.__init__(self)
1072        P4UserMap.__init__(self)
1073        self.options = [
1074                optparse.make_option("--origin", dest="origin"),
1075                optparse.make_option("-M", dest="detectRenames", action="store_true"),
1076                # preserve the user, requires relevant p4 permissions
1077                optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1078                optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1079                optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1080                optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1081                optparse.make_option("--conflict", dest="conflict_behavior",
1082                                     choices=self.conflict_behavior_choices),
1083                optparse.make_option("--branch", dest="branch"),
1084        ]
1085        self.description = "Submit changes from git to the perforce depot."
1086        self.usage += " [name of git branch to submit into perforce depot]"
1087        self.origin = ""
1088        self.detectRenames = False
1089        self.preserveUser = gitConfigBool("git-p4.preserveUser")
1090        self.dry_run = False
1091        self.prepare_p4_only = False
1092        self.conflict_behavior = None
1093        self.isWindows = (platform.system() == "Windows")
1094        self.exportLabels = False
1095        self.p4HasMoveCommand = p4_has_move_command()
1096        self.branch = None
1097
1098    def check(self):
1099        if len(p4CmdList("opened ...")) > 0:
1100            die("You have files opened with perforce! Close them before starting the sync.")
1101
1102    def separate_jobs_from_description(self, message):
1103        """Extract and return a possible Jobs field in the commit
1104           message.  It goes into a separate section in the p4 change
1105           specification.
1106
1107           A jobs line starts with "Jobs:" and looks like a new field
1108           in a form.  Values are white-space separated on the same
1109           line or on following lines that start with a tab.
1110
1111           This does not parse and extract the full git commit message
1112           like a p4 form.  It just sees the Jobs: line as a marker
1113           to pass everything from then on directly into the p4 form,
1114           but outside the description section.
1115
1116           Return a tuple (stripped log message, jobs string)."""
1117
1118        m = re.search(r'^Jobs:', message, re.MULTILINE)
1119        if m is None:
1120            return (message, None)
1121
1122        jobtext = message[m.start():]
1123        stripped_message = message[:m.start()].rstrip()
1124        return (stripped_message, jobtext)
1125
1126    def prepareLogMessage(self, template, message, jobs):
1127        """Edits the template returned from "p4 change -o" to insert
1128           the message in the Description field, and the jobs text in
1129           the Jobs field."""
1130        result = ""
1131
1132        inDescriptionSection = False
1133
1134        for line in template.split("\n"):
1135            if line.startswith("#"):
1136                result += line + "\n"
1137                continue
1138
1139            if inDescriptionSection:
1140                if line.startswith("Files:") or line.startswith("Jobs:"):
1141                    inDescriptionSection = False
1142                    # insert Jobs section
1143                    if jobs:
1144                        result += jobs + "\n"
1145                else:
1146                    continue
1147            else:
1148                if line.startswith("Description:"):
1149                    inDescriptionSection = True
1150                    line += "\n"
1151                    for messageLine in message.split("\n"):
1152                        line += "\t" + messageLine + "\n"
1153
1154            result += line + "\n"
1155
1156        return result
1157
1158    def patchRCSKeywords(self, file, pattern):
1159        # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1160        (handle, outFileName) = tempfile.mkstemp(dir='.')
1161        try:
1162            outFile = os.fdopen(handle, "w+")
1163            inFile = open(file, "r")
1164            regexp = re.compile(pattern, re.VERBOSE)
1165            for line in inFile.readlines():
1166                line = regexp.sub(r'$\1$', line)
1167                outFile.write(line)
1168            inFile.close()
1169            outFile.close()
1170            # Forcibly overwrite the original file
1171            os.unlink(file)
1172            shutil.move(outFileName, file)
1173        except:
1174            # cleanup our temporary file
1175            os.unlink(outFileName)
1176            print "Failed to strip RCS keywords in %s" % file
1177            raise
1178
1179        print "Patched up RCS keywords in %s" % file
1180
1181    def p4UserForCommit(self,id):
1182        # Return the tuple (perforce user,git email) for a given git commit id
1183        self.getUserMapFromPerforceServer()
1184        gitEmail = read_pipe(["git", "log", "--max-count=1",
1185                              "--format=%ae", id])
1186        gitEmail = gitEmail.strip()
1187        if not self.emails.has_key(gitEmail):
1188            return (None,gitEmail)
1189        else:
1190            return (self.emails[gitEmail],gitEmail)
1191
1192    def checkValidP4Users(self,commits):
1193        # check if any git authors cannot be mapped to p4 users
1194        for id in commits:
1195            (user,email) = self.p4UserForCommit(id)
1196            if not user:
1197                msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1198                if gitConfigBool("git-p4.allowMissingP4Users"):
1199                    print "%s" % msg
1200                else:
1201                    die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1202
1203    def lastP4Changelist(self):
1204        # Get back the last changelist number submitted in this client spec. This
1205        # then gets used to patch up the username in the change. If the same
1206        # client spec is being used by multiple processes then this might go
1207        # wrong.
1208        results = p4CmdList("client -o")        # find the current client
1209        client = None
1210        for r in results:
1211            if r.has_key('Client'):
1212                client = r['Client']
1213                break
1214        if not client:
1215            die("could not get client spec")
1216        results = p4CmdList(["changes", "-c", client, "-m", "1"])
1217        for r in results:
1218            if r.has_key('change'):
1219                return r['change']
1220        die("Could not get changelist number for last submit - cannot patch up user details")
1221
1222    def modifyChangelistUser(self, changelist, newUser):
1223        # fixup the user field of a changelist after it has been submitted.
1224        changes = p4CmdList("change -o %s" % changelist)
1225        if len(changes) != 1:
1226            die("Bad output from p4 change modifying %s to user %s" %
1227                (changelist, newUser))
1228
1229        c = changes[0]
1230        if c['User'] == newUser: return   # nothing to do
1231        c['User'] = newUser
1232        input = marshal.dumps(c)
1233
1234        result = p4CmdList("change -f -i", stdin=input)
1235        for r in result:
1236            if r.has_key('code'):
1237                if r['code'] == 'error':
1238                    die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1239            if r.has_key('data'):
1240                print("Updated user field for changelist %s to %s" % (changelist, newUser))
1241                return
1242        die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1243
1244    def canChangeChangelists(self):
1245        # check to see if we have p4 admin or super-user permissions, either of
1246        # which are required to modify changelists.
1247        results = p4CmdList(["protects", self.depotPath])
1248        for r in results:
1249            if r.has_key('perm'):
1250                if r['perm'] == 'admin':
1251                    return 1
1252                if r['perm'] == 'super':
1253                    return 1
1254        return 0
1255
1256    def prepareSubmitTemplate(self):
1257        """Run "p4 change -o" to grab a change specification template.
1258           This does not use "p4 -G", as it is nice to keep the submission
1259           template in original order, since a human might edit it.
1260
1261           Remove lines in the Files section that show changes to files
1262           outside the depot path we're committing into."""
1263
1264        template = ""
1265        inFilesSection = False
1266        for line in p4_read_pipe_lines(['change', '-o']):
1267            if line.endswith("\r\n"):
1268                line = line[:-2] + "\n"
1269            if inFilesSection:
1270                if line.startswith("\t"):
1271                    # path starts and ends with a tab
1272                    path = line[1:]
1273                    lastTab = path.rfind("\t")
1274                    if lastTab != -1:
1275                        path = path[:lastTab]
1276                        if not p4PathStartsWith(path, self.depotPath):
1277                            continue
1278                else:
1279                    inFilesSection = False
1280            else:
1281                if line.startswith("Files:"):
1282                    inFilesSection = True
1283
1284            template += line
1285
1286        return template
1287
1288    def edit_template(self, template_file):
1289        """Invoke the editor to let the user change the submission
1290           message.  Return true if okay to continue with the submit."""
1291
1292        # if configured to skip the editing part, just submit
1293        if gitConfigBool("git-p4.skipSubmitEdit"):
1294            return True
1295
1296        # look at the modification time, to check later if the user saved
1297        # the file
1298        mtime = os.stat(template_file).st_mtime
1299
1300        # invoke the editor
1301        if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1302            editor = os.environ.get("P4EDITOR")
1303        else:
1304            editor = read_pipe("git var GIT_EDITOR").strip()
1305        system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1306
1307        # If the file was not saved, prompt to see if this patch should
1308        # be skipped.  But skip this verification step if configured so.
1309        if gitConfigBool("git-p4.skipSubmitEditCheck"):
1310            return True
1311
1312        # modification time updated means user saved the file
1313        if os.stat(template_file).st_mtime > mtime:
1314            return True
1315
1316        while True:
1317            response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1318            if response == 'y':
1319                return True
1320            if response == 'n':
1321                return False
1322
1323    def get_diff_description(self, editedFiles, filesToAdd):
1324        # diff
1325        if os.environ.has_key("P4DIFF"):
1326            del(os.environ["P4DIFF"])
1327        diff = ""
1328        for editedFile in editedFiles:
1329            diff += p4_read_pipe(['diff', '-du',
1330                                  wildcard_encode(editedFile)])
1331
1332        # new file diff
1333        newdiff = ""
1334        for newFile in filesToAdd:
1335            newdiff += "==== new file ====\n"
1336            newdiff += "--- /dev/null\n"
1337            newdiff += "+++ %s\n" % newFile
1338            f = open(newFile, "r")
1339            for line in f.readlines():
1340                newdiff += "+" + line
1341            f.close()
1342
1343        return (diff + newdiff).replace('\r\n', '\n')
1344
1345    def applyCommit(self, id):
1346        """Apply one commit, return True if it succeeded."""
1347
1348        print "Applying", read_pipe(["git", "show", "-s",
1349                                     "--format=format:%h %s", id])
1350
1351        (p4User, gitEmail) = self.p4UserForCommit(id)
1352
1353        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1354        filesToAdd = set()
1355        filesToDelete = set()
1356        editedFiles = set()
1357        pureRenameCopy = set()
1358        filesToChangeExecBit = {}
1359
1360        for line in diff:
1361            diff = parseDiffTreeEntry(line)
1362            modifier = diff['status']
1363            path = diff['src']
1364            if modifier == "M":
1365                p4_edit(path)
1366                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1367                    filesToChangeExecBit[path] = diff['dst_mode']
1368                editedFiles.add(path)
1369            elif modifier == "A":
1370                filesToAdd.add(path)
1371                filesToChangeExecBit[path] = diff['dst_mode']
1372                if path in filesToDelete:
1373                    filesToDelete.remove(path)
1374            elif modifier == "D":
1375                filesToDelete.add(path)
1376                if path in filesToAdd:
1377                    filesToAdd.remove(path)
1378            elif modifier == "C":
1379                src, dest = diff['src'], diff['dst']
1380                p4_integrate(src, dest)
1381                pureRenameCopy.add(dest)
1382                if diff['src_sha1'] != diff['dst_sha1']:
1383                    p4_edit(dest)
1384                    pureRenameCopy.discard(dest)
1385                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1386                    p4_edit(dest)
1387                    pureRenameCopy.discard(dest)
1388                    filesToChangeExecBit[dest] = diff['dst_mode']
1389                if self.isWindows:
1390                    # turn off read-only attribute
1391                    os.chmod(dest, stat.S_IWRITE)
1392                os.unlink(dest)
1393                editedFiles.add(dest)
1394            elif modifier == "R":
1395                src, dest = diff['src'], diff['dst']
1396                if self.p4HasMoveCommand:
1397                    p4_edit(src)        # src must be open before move
1398                    p4_move(src, dest)  # opens for (move/delete, move/add)
1399                else:
1400                    p4_integrate(src, dest)
1401                    if diff['src_sha1'] != diff['dst_sha1']:
1402                        p4_edit(dest)
1403                    else:
1404                        pureRenameCopy.add(dest)
1405                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1406                    if not self.p4HasMoveCommand:
1407                        p4_edit(dest)   # with move: already open, writable
1408                    filesToChangeExecBit[dest] = diff['dst_mode']
1409                if not self.p4HasMoveCommand:
1410                    if self.isWindows:
1411                        os.chmod(dest, stat.S_IWRITE)
1412                    os.unlink(dest)
1413                    filesToDelete.add(src)
1414                editedFiles.add(dest)
1415            else:
1416                die("unknown modifier %s for %s" % (modifier, path))
1417
1418        diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1419        patchcmd = diffcmd + " | git apply "
1420        tryPatchCmd = patchcmd + "--check -"
1421        applyPatchCmd = patchcmd + "--check --apply -"
1422        patch_succeeded = True
1423
1424        if os.system(tryPatchCmd) != 0:
1425            fixed_rcs_keywords = False
1426            patch_succeeded = False
1427            print "Unfortunately applying the change failed!"
1428
1429            # Patch failed, maybe it's just RCS keyword woes. Look through
1430            # the patch to see if that's possible.
1431            if gitConfigBool("git-p4.attemptRCSCleanup"):
1432                file = None
1433                pattern = None
1434                kwfiles = {}
1435                for file in editedFiles | filesToDelete:
1436                    # did this file's delta contain RCS keywords?
1437                    pattern = p4_keywords_regexp_for_file(file)
1438
1439                    if pattern:
1440                        # this file is a possibility...look for RCS keywords.
1441                        regexp = re.compile(pattern, re.VERBOSE)
1442                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1443                            if regexp.search(line):
1444                                if verbose:
1445                                    print "got keyword match on %s in %s in %s" % (pattern, line, file)
1446                                kwfiles[file] = pattern
1447                                break
1448
1449                for file in kwfiles:
1450                    if verbose:
1451                        print "zapping %s with %s" % (line,pattern)
1452                    # File is being deleted, so not open in p4.  Must
1453                    # disable the read-only bit on windows.
1454                    if self.isWindows and file not in editedFiles:
1455                        os.chmod(file, stat.S_IWRITE)
1456                    self.patchRCSKeywords(file, kwfiles[file])
1457                    fixed_rcs_keywords = True
1458
1459            if fixed_rcs_keywords:
1460                print "Retrying the patch with RCS keywords cleaned up"
1461                if os.system(tryPatchCmd) == 0:
1462                    patch_succeeded = True
1463
1464        if not patch_succeeded:
1465            for f in editedFiles:
1466                p4_revert(f)
1467            return False
1468
1469        #
1470        # Apply the patch for real, and do add/delete/+x handling.
1471        #
1472        system(applyPatchCmd)
1473
1474        for f in filesToAdd:
1475            p4_add(f)
1476        for f in filesToDelete:
1477            p4_revert(f)
1478            p4_delete(f)
1479
1480        # Set/clear executable bits
1481        for f in filesToChangeExecBit.keys():
1482            mode = filesToChangeExecBit[f]
1483            setP4ExecBit(f, mode)
1484
1485        #
1486        # Build p4 change description, starting with the contents
1487        # of the git commit message.
1488        #
1489        logMessage = extractLogMessageFromGitCommit(id)
1490        logMessage = logMessage.strip()
1491        (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1492
1493        template = self.prepareSubmitTemplate()
1494        submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1495
1496        if self.preserveUser:
1497           submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1498
1499        if self.checkAuthorship and not self.p4UserIsMe(p4User):
1500            submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1501            submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1502            submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1503
1504        separatorLine = "######## everything below this line is just the diff #######\n"
1505        if not self.prepare_p4_only:
1506            submitTemplate += separatorLine
1507            submitTemplate += self.get_diff_description(editedFiles, filesToAdd)
1508
1509        (handle, fileName) = tempfile.mkstemp()
1510        tmpFile = os.fdopen(handle, "w+b")
1511        if self.isWindows:
1512            submitTemplate = submitTemplate.replace("\n", "\r\n")
1513        tmpFile.write(submitTemplate)
1514        tmpFile.close()
1515
1516        if self.prepare_p4_only:
1517            #
1518            # Leave the p4 tree prepared, and the submit template around
1519            # and let the user decide what to do next
1520            #
1521            print
1522            print "P4 workspace prepared for submission."
1523            print "To submit or revert, go to client workspace"
1524            print "  " + self.clientPath
1525            print
1526            print "To submit, use \"p4 submit\" to write a new description,"
1527            print "or \"p4 submit -i <%s\" to use the one prepared by" \
1528                  " \"git p4\"." % fileName
1529            print "You can delete the file \"%s\" when finished." % fileName
1530
1531            if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1532                print "To preserve change ownership by user %s, you must\n" \
1533                      "do \"p4 change -f <change>\" after submitting and\n" \
1534                      "edit the User field."
1535            if pureRenameCopy:
1536                print "After submitting, renamed files must be re-synced."
1537                print "Invoke \"p4 sync -f\" on each of these files:"
1538                for f in pureRenameCopy:
1539                    print "  " + f
1540
1541            print
1542            print "To revert the changes, use \"p4 revert ...\", and delete"
1543            print "the submit template file \"%s\"" % fileName
1544            if filesToAdd:
1545                print "Since the commit adds new files, they must be deleted:"
1546                for f in filesToAdd:
1547                    print "  " + f
1548            print
1549            return True
1550
1551        #
1552        # Let the user edit the change description, then submit it.
1553        #
1554        if self.edit_template(fileName):
1555            # read the edited message and submit
1556            ret = True
1557            tmpFile = open(fileName, "rb")
1558            message = tmpFile.read()
1559            tmpFile.close()
1560            if self.isWindows:
1561                message = message.replace("\r\n", "\n")
1562            submitTemplate = message[:message.index(separatorLine)]
1563            p4_write_pipe(['submit', '-i'], submitTemplate)
1564
1565            if self.preserveUser:
1566                if p4User:
1567                    # Get last changelist number. Cannot easily get it from
1568                    # the submit command output as the output is
1569                    # unmarshalled.
1570                    changelist = self.lastP4Changelist()
1571                    self.modifyChangelistUser(changelist, p4User)
1572
1573            # The rename/copy happened by applying a patch that created a
1574            # new file.  This leaves it writable, which confuses p4.
1575            for f in pureRenameCopy:
1576                p4_sync(f, "-f")
1577
1578        else:
1579            # skip this patch
1580            ret = False
1581            print "Submission cancelled, undoing p4 changes."
1582            for f in editedFiles:
1583                p4_revert(f)
1584            for f in filesToAdd:
1585                p4_revert(f)
1586                os.remove(f)
1587            for f in filesToDelete:
1588                p4_revert(f)
1589
1590        os.remove(fileName)
1591        return ret
1592
1593    # Export git tags as p4 labels. Create a p4 label and then tag
1594    # with that.
1595    def exportGitTags(self, gitTags):
1596        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1597        if len(validLabelRegexp) == 0:
1598            validLabelRegexp = defaultLabelRegexp
1599        m = re.compile(validLabelRegexp)
1600
1601        for name in gitTags:
1602
1603            if not m.match(name):
1604                if verbose:
1605                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1606                continue
1607
1608            # Get the p4 commit this corresponds to
1609            logMessage = extractLogMessageFromGitCommit(name)
1610            values = extractSettingsGitLog(logMessage)
1611
1612            if not values.has_key('change'):
1613                # a tag pointing to something not sent to p4; ignore
1614                if verbose:
1615                    print "git tag %s does not give a p4 commit" % name
1616                continue
1617            else:
1618                changelist = values['change']
1619
1620            # Get the tag details.
1621            inHeader = True
1622            isAnnotated = False
1623            body = []
1624            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1625                l = l.strip()
1626                if inHeader:
1627                    if re.match(r'tag\s+', l):
1628                        isAnnotated = True
1629                    elif re.match(r'\s*$', l):
1630                        inHeader = False
1631                        continue
1632                else:
1633                    body.append(l)
1634
1635            if not isAnnotated:
1636                body = ["lightweight tag imported by git p4\n"]
1637
1638            # Create the label - use the same view as the client spec we are using
1639            clientSpec = getClientSpec()
1640
1641            labelTemplate  = "Label: %s\n" % name
1642            labelTemplate += "Description:\n"
1643            for b in body:
1644                labelTemplate += "\t" + b + "\n"
1645            labelTemplate += "View:\n"
1646            for depot_side in clientSpec.mappings:
1647                labelTemplate += "\t%s\n" % depot_side
1648
1649            if self.dry_run:
1650                print "Would create p4 label %s for tag" % name
1651            elif self.prepare_p4_only:
1652                print "Not creating p4 label %s for tag due to option" \
1653                      " --prepare-p4-only" % name
1654            else:
1655                p4_write_pipe(["label", "-i"], labelTemplate)
1656
1657                # Use the label
1658                p4_system(["tag", "-l", name] +
1659                          ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1660
1661                if verbose:
1662                    print "created p4 label for tag %s" % name
1663
1664    def run(self, args):
1665        if len(args) == 0:
1666            self.master = currentGitBranch()
1667            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1668                die("Detecting current git branch failed!")
1669        elif len(args) == 1:
1670            self.master = args[0]
1671            if not branchExists(self.master):
1672                die("Branch %s does not exist" % self.master)
1673        else:
1674            return False
1675
1676        allowSubmit = gitConfig("git-p4.allowSubmit")
1677        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1678            die("%s is not in git-p4.allowSubmit" % self.master)
1679
1680        [upstream, settings] = findUpstreamBranchPoint()
1681        self.depotPath = settings['depot-paths'][0]
1682        if len(self.origin) == 0:
1683            self.origin = upstream
1684
1685        if self.preserveUser:
1686            if not self.canChangeChangelists():
1687                die("Cannot preserve user names without p4 super-user or admin permissions")
1688
1689        # if not set from the command line, try the config file
1690        if self.conflict_behavior is None:
1691            val = gitConfig("git-p4.conflict")
1692            if val:
1693                if val not in self.conflict_behavior_choices:
1694                    die("Invalid value '%s' for config git-p4.conflict" % val)
1695            else:
1696                val = "ask"
1697            self.conflict_behavior = val
1698
1699        if self.verbose:
1700            print "Origin branch is " + self.origin
1701
1702        if len(self.depotPath) == 0:
1703            print "Internal error: cannot locate perforce depot path from existing branches"
1704            sys.exit(128)
1705
1706        self.useClientSpec = False
1707        if gitConfigBool("git-p4.useclientspec"):
1708            self.useClientSpec = True
1709        if self.useClientSpec:
1710            self.clientSpecDirs = getClientSpec()
1711
1712        # Check for the existance of P4 branches
1713        branchesDetected = (len(p4BranchesInGit().keys()) > 1)
1714
1715        if self.useClientSpec and not branchesDetected:
1716            # all files are relative to the client spec
1717            self.clientPath = getClientRoot()
1718        else:
1719            self.clientPath = p4Where(self.depotPath)
1720
1721        if self.clientPath == "":
1722            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1723
1724        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1725        self.oldWorkingDirectory = os.getcwd()
1726
1727        # ensure the clientPath exists
1728        new_client_dir = False
1729        if not os.path.exists(self.clientPath):
1730            new_client_dir = True
1731            os.makedirs(self.clientPath)
1732
1733        chdir(self.clientPath, is_client_path=True)
1734        if self.dry_run:
1735            print "Would synchronize p4 checkout in %s" % self.clientPath
1736        else:
1737            print "Synchronizing p4 checkout..."
1738            if new_client_dir:
1739                # old one was destroyed, and maybe nobody told p4
1740                p4_sync("...", "-f")
1741            else:
1742                p4_sync("...")
1743        self.check()
1744
1745        commits = []
1746        for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, self.master)]):
1747            commits.append(line.strip())
1748        commits.reverse()
1749
1750        if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1751            self.checkAuthorship = False
1752        else:
1753            self.checkAuthorship = True
1754
1755        if self.preserveUser:
1756            self.checkValidP4Users(commits)
1757
1758        #
1759        # Build up a set of options to be passed to diff when
1760        # submitting each commit to p4.
1761        #
1762        if self.detectRenames:
1763            # command-line -M arg
1764            self.diffOpts = "-M"
1765        else:
1766            # If not explicitly set check the config variable
1767            detectRenames = gitConfig("git-p4.detectRenames")
1768
1769            if detectRenames.lower() == "false" or detectRenames == "":
1770                self.diffOpts = ""
1771            elif detectRenames.lower() == "true":
1772                self.diffOpts = "-M"
1773            else:
1774                self.diffOpts = "-M%s" % detectRenames
1775
1776        # no command-line arg for -C or --find-copies-harder, just
1777        # config variables
1778        detectCopies = gitConfig("git-p4.detectCopies")
1779        if detectCopies.lower() == "false" or detectCopies == "":
1780            pass
1781        elif detectCopies.lower() == "true":
1782            self.diffOpts += " -C"
1783        else:
1784            self.diffOpts += " -C%s" % detectCopies
1785
1786        if gitConfigBool("git-p4.detectCopiesHarder"):
1787            self.diffOpts += " --find-copies-harder"
1788
1789        #
1790        # Apply the commits, one at a time.  On failure, ask if should
1791        # continue to try the rest of the patches, or quit.
1792        #
1793        if self.dry_run:
1794            print "Would apply"
1795        applied = []
1796        last = len(commits) - 1
1797        for i, commit in enumerate(commits):
1798            if self.dry_run:
1799                print " ", read_pipe(["git", "show", "-s",
1800                                      "--format=format:%h %s", commit])
1801                ok = True
1802            else:
1803                ok = self.applyCommit(commit)
1804            if ok:
1805                applied.append(commit)
1806            else:
1807                if self.prepare_p4_only and i < last:
1808                    print "Processing only the first commit due to option" \
1809                          " --prepare-p4-only"
1810                    break
1811                if i < last:
1812                    quit = False
1813                    while True:
1814                        # prompt for what to do, or use the option/variable
1815                        if self.conflict_behavior == "ask":
1816                            print "What do you want to do?"
1817                            response = raw_input("[s]kip this commit but apply"
1818                                                 " the rest, or [q]uit? ")
1819                            if not response:
1820                                continue
1821                        elif self.conflict_behavior == "skip":
1822                            response = "s"
1823                        elif self.conflict_behavior == "quit":
1824                            response = "q"
1825                        else:
1826                            die("Unknown conflict_behavior '%s'" %
1827                                self.conflict_behavior)
1828
1829                        if response[0] == "s":
1830                            print "Skipping this commit, but applying the rest"
1831                            break
1832                        if response[0] == "q":
1833                            print "Quitting"
1834                            quit = True
1835                            break
1836                    if quit:
1837                        break
1838
1839        chdir(self.oldWorkingDirectory)
1840
1841        if self.dry_run:
1842            pass
1843        elif self.prepare_p4_only:
1844            pass
1845        elif len(commits) == len(applied):
1846            print "All commits applied!"
1847
1848            sync = P4Sync()
1849            if self.branch:
1850                sync.branch = self.branch
1851            sync.run([])
1852
1853            rebase = P4Rebase()
1854            rebase.rebase()
1855
1856        else:
1857            if len(applied) == 0:
1858                print "No commits applied."
1859            else:
1860                print "Applied only the commits marked with '*':"
1861                for c in commits:
1862                    if c in applied:
1863                        star = "*"
1864                    else:
1865                        star = " "
1866                    print star, read_pipe(["git", "show", "-s",
1867                                           "--format=format:%h %s",  c])
1868                print "You will have to do 'git p4 sync' and rebase."
1869
1870        if gitConfigBool("git-p4.exportLabels"):
1871            self.exportLabels = True
1872
1873        if self.exportLabels:
1874            p4Labels = getP4Labels(self.depotPath)
1875            gitTags = getGitTags()
1876
1877            missingGitTags = gitTags - p4Labels
1878            self.exportGitTags(missingGitTags)
1879
1880        # exit with error unless everything applied perfectly
1881        if len(commits) != len(applied):
1882                sys.exit(1)
1883
1884        return True
1885
1886class View(object):
1887    """Represent a p4 view ("p4 help views"), and map files in a
1888       repo according to the view."""
1889
1890    def __init__(self, client_name):
1891        self.mappings = []
1892        self.client_prefix = "//%s/" % client_name
1893        # cache results of "p4 where" to lookup client file locations
1894        self.client_spec_path_cache = {}
1895
1896    def append(self, view_line):
1897        """Parse a view line, splitting it into depot and client
1898           sides.  Append to self.mappings, preserving order.  This
1899           is only needed for tag creation."""
1900
1901        # Split the view line into exactly two words.  P4 enforces
1902        # structure on these lines that simplifies this quite a bit.
1903        #
1904        # Either or both words may be double-quoted.
1905        # Single quotes do not matter.
1906        # Double-quote marks cannot occur inside the words.
1907        # A + or - prefix is also inside the quotes.
1908        # There are no quotes unless they contain a space.
1909        # The line is already white-space stripped.
1910        # The two words are separated by a single space.
1911        #
1912        if view_line[0] == '"':
1913            # First word is double quoted.  Find its end.
1914            close_quote_index = view_line.find('"', 1)
1915            if close_quote_index <= 0:
1916                die("No first-word closing quote found: %s" % view_line)
1917            depot_side = view_line[1:close_quote_index]
1918            # skip closing quote and space
1919            rhs_index = close_quote_index + 1 + 1
1920        else:
1921            space_index = view_line.find(" ")
1922            if space_index <= 0:
1923                die("No word-splitting space found: %s" % view_line)
1924            depot_side = view_line[0:space_index]
1925            rhs_index = space_index + 1
1926
1927        # prefix + means overlay on previous mapping
1928        if depot_side.startswith("+"):
1929            depot_side = depot_side[1:]
1930
1931        # prefix - means exclude this path, leave out of mappings
1932        exclude = False
1933        if depot_side.startswith("-"):
1934            exclude = True
1935            depot_side = depot_side[1:]
1936
1937        if not exclude:
1938            self.mappings.append(depot_side)
1939
1940    def convert_client_path(self, clientFile):
1941        # chop off //client/ part to make it relative
1942        if not clientFile.startswith(self.client_prefix):
1943            die("No prefix '%s' on clientFile '%s'" %
1944                (self.client_prefix, clientFile))
1945        return clientFile[len(self.client_prefix):]
1946
1947    def update_client_spec_path_cache(self, files):
1948        """ Caching file paths by "p4 where" batch query """
1949
1950        # List depot file paths exclude that already cached
1951        fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
1952
1953        if len(fileArgs) == 0:
1954            return  # All files in cache
1955
1956        where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
1957        for res in where_result:
1958            if "code" in res and res["code"] == "error":
1959                # assume error is "... file(s) not in client view"
1960                continue
1961            if "clientFile" not in res:
1962                die("No clientFile in 'p4 where' output")
1963            if "unmap" in res:
1964                # it will list all of them, but only one not unmap-ped
1965                continue
1966            if gitConfigBool("core.ignorecase"):
1967                res['depotFile'] = res['depotFile'].lower()
1968            self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
1969
1970        # not found files or unmap files set to ""
1971        for depotFile in fileArgs:
1972            if gitConfigBool("core.ignorecase"):
1973                depotFile = depotFile.lower()
1974            if depotFile not in self.client_spec_path_cache:
1975                self.client_spec_path_cache[depotFile] = ""
1976
1977    def map_in_client(self, depot_path):
1978        """Return the relative location in the client where this
1979           depot file should live.  Returns "" if the file should
1980           not be mapped in the client."""
1981
1982        if gitConfigBool("core.ignorecase"):
1983            depot_path = depot_path.lower()
1984
1985        if depot_path in self.client_spec_path_cache:
1986            return self.client_spec_path_cache[depot_path]
1987
1988        die( "Error: %s is not found in client spec path" % depot_path )
1989        return ""
1990
1991class P4Sync(Command, P4UserMap):
1992    delete_actions = ( "delete", "move/delete", "purge" )
1993
1994    def __init__(self):
1995        Command.__init__(self)
1996        P4UserMap.__init__(self)
1997        self.options = [
1998                optparse.make_option("--branch", dest="branch"),
1999                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2000                optparse.make_option("--changesfile", dest="changesFile"),
2001                optparse.make_option("--silent", dest="silent", action="store_true"),
2002                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2003                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2004                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2005                                     help="Import into refs/heads/ , not refs/remotes"),
2006                optparse.make_option("--max-changes", dest="maxChanges",
2007                                     help="Maximum number of changes to import"),
2008                optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2009                                     help="Internal block size to use when iteratively calling p4 changes"),
2010                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2011                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2012                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2013                                     help="Only sync files that are included in the Perforce Client Spec"),
2014                optparse.make_option("-/", dest="cloneExclude",
2015                                     action="append", type="string",
2016                                     help="exclude depot path"),
2017        ]
2018        self.description = """Imports from Perforce into a git repository.\n
2019    example:
2020    //depot/my/project/ -- to import the current head
2021    //depot/my/project/@all -- to import everything
2022    //depot/my/project/@1,6 -- to import only from revision 1 to 6
2023
2024    (a ... is not needed in the path p4 specification, it's added implicitly)"""
2025
2026        self.usage += " //depot/path[@revRange]"
2027        self.silent = False
2028        self.createdBranches = set()
2029        self.committedChanges = set()
2030        self.branch = ""
2031        self.detectBranches = False
2032        self.detectLabels = False
2033        self.importLabels = False
2034        self.changesFile = ""
2035        self.syncWithOrigin = True
2036        self.importIntoRemotes = True
2037        self.maxChanges = ""
2038        self.changes_block_size = None
2039        self.keepRepoPath = False
2040        self.depotPaths = None
2041        self.p4BranchesInGit = []
2042        self.cloneExclude = []
2043        self.useClientSpec = False
2044        self.useClientSpec_from_options = False
2045        self.clientSpecDirs = None
2046        self.tempBranches = []
2047        self.tempBranchLocation = "git-p4-tmp"
2048
2049        if gitConfig("git-p4.syncFromOrigin") == "false":
2050            self.syncWithOrigin = False
2051
2052    # This is required for the "append" cloneExclude action
2053    def ensure_value(self, attr, value):
2054        if not hasattr(self, attr) or getattr(self, attr) is None:
2055            setattr(self, attr, value)
2056        return getattr(self, attr)
2057
2058    # Force a checkpoint in fast-import and wait for it to finish
2059    def checkpoint(self):
2060        self.gitStream.write("checkpoint\n\n")
2061        self.gitStream.write("progress checkpoint\n\n")
2062        out = self.gitOutput.readline()
2063        if self.verbose:
2064            print "checkpoint finished: " + out
2065
2066    def extractFilesFromCommit(self, commit):
2067        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2068                             for path in self.cloneExclude]
2069        files = []
2070        fnum = 0
2071        while commit.has_key("depotFile%s" % fnum):
2072            path =  commit["depotFile%s" % fnum]
2073
2074            if [p for p in self.cloneExclude
2075                if p4PathStartsWith(path, p)]:
2076                found = False
2077            else:
2078                found = [p for p in self.depotPaths
2079                         if p4PathStartsWith(path, p)]
2080            if not found:
2081                fnum = fnum + 1
2082                continue
2083
2084            file = {}
2085            file["path"] = path
2086            file["rev"] = commit["rev%s" % fnum]
2087            file["action"] = commit["action%s" % fnum]
2088            file["type"] = commit["type%s" % fnum]
2089            files.append(file)
2090            fnum = fnum + 1
2091        return files
2092
2093    def stripRepoPath(self, path, prefixes):
2094        """When streaming files, this is called to map a p4 depot path
2095           to where it should go in git.  The prefixes are either
2096           self.depotPaths, or self.branchPrefixes in the case of
2097           branch detection."""
2098
2099        if self.useClientSpec:
2100            # branch detection moves files up a level (the branch name)
2101            # from what client spec interpretation gives
2102            path = self.clientSpecDirs.map_in_client(path)
2103            if self.detectBranches:
2104                for b in self.knownBranches:
2105                    if path.startswith(b + "/"):
2106                        path = path[len(b)+1:]
2107
2108        elif self.keepRepoPath:
2109            # Preserve everything in relative path name except leading
2110            # //depot/; just look at first prefix as they all should
2111            # be in the same depot.
2112            depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2113            if p4PathStartsWith(path, depot):
2114                path = path[len(depot):]
2115
2116        else:
2117            for p in prefixes:
2118                if p4PathStartsWith(path, p):
2119                    path = path[len(p):]
2120                    break
2121
2122        path = wildcard_decode(path)
2123        return path
2124
2125    def splitFilesIntoBranches(self, commit):
2126        """Look at each depotFile in the commit to figure out to what
2127           branch it belongs."""
2128
2129        if self.clientSpecDirs:
2130            files = self.extractFilesFromCommit(commit)
2131            self.clientSpecDirs.update_client_spec_path_cache(files)
2132
2133        branches = {}
2134        fnum = 0
2135        while commit.has_key("depotFile%s" % fnum):
2136            path =  commit["depotFile%s" % fnum]
2137            found = [p for p in self.depotPaths
2138                     if p4PathStartsWith(path, p)]
2139            if not found:
2140                fnum = fnum + 1
2141                continue
2142
2143            file = {}
2144            file["path"] = path
2145            file["rev"] = commit["rev%s" % fnum]
2146            file["action"] = commit["action%s" % fnum]
2147            file["type"] = commit["type%s" % fnum]
2148            fnum = fnum + 1
2149
2150            # start with the full relative path where this file would
2151            # go in a p4 client
2152            if self.useClientSpec:
2153                relPath = self.clientSpecDirs.map_in_client(path)
2154            else:
2155                relPath = self.stripRepoPath(path, self.depotPaths)
2156
2157            for branch in self.knownBranches.keys():
2158                # add a trailing slash so that a commit into qt/4.2foo
2159                # doesn't end up in qt/4.2, e.g.
2160                if relPath.startswith(branch + "/"):
2161                    if branch not in branches:
2162                        branches[branch] = []
2163                    branches[branch].append(file)
2164                    break
2165
2166        return branches
2167
2168    # output one file from the P4 stream
2169    # - helper for streamP4Files
2170
2171    def streamOneP4File(self, file, contents):
2172        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2173        if verbose:
2174            sys.stderr.write("%s\n" % relPath)
2175
2176        (type_base, type_mods) = split_p4_type(file["type"])
2177
2178        git_mode = "100644"
2179        if "x" in type_mods:
2180            git_mode = "100755"
2181        if type_base == "symlink":
2182            git_mode = "120000"
2183            # p4 print on a symlink sometimes contains "target\n";
2184            # if it does, remove the newline
2185            data = ''.join(contents)
2186            if not data:
2187                # Some version of p4 allowed creating a symlink that pointed
2188                # to nothing.  This causes p4 errors when checking out such
2189                # a change, and errors here too.  Work around it by ignoring
2190                # the bad symlink; hopefully a future change fixes it.
2191                print "\nIgnoring empty symlink in %s" % file['depotFile']
2192                return
2193            elif data[-1] == '\n':
2194                contents = [data[:-1]]
2195            else:
2196                contents = [data]
2197
2198        if type_base == "utf16":
2199            # p4 delivers different text in the python output to -G
2200            # than it does when using "print -o", or normal p4 client
2201            # operations.  utf16 is converted to ascii or utf8, perhaps.
2202            # But ascii text saved as -t utf16 is completely mangled.
2203            # Invoke print -o to get the real contents.
2204            #
2205            # On windows, the newlines will always be mangled by print, so put
2206            # them back too.  This is not needed to the cygwin windows version,
2207            # just the native "NT" type.
2208            #
2209            text = p4_read_pipe(['print', '-q', '-o', '-', "%s@%s" % (file['depotFile'], file['change']) ])
2210            if p4_version_string().find("/NT") >= 0:
2211                text = text.replace("\r\n", "\n")
2212            contents = [ text ]
2213
2214        if type_base == "apple":
2215            # Apple filetype files will be streamed as a concatenation of
2216            # its appledouble header and the contents.  This is useless
2217            # on both macs and non-macs.  If using "print -q -o xx", it
2218            # will create "xx" with the data, and "%xx" with the header.
2219            # This is also not very useful.
2220            #
2221            # Ideally, someday, this script can learn how to generate
2222            # appledouble files directly and import those to git, but
2223            # non-mac machines can never find a use for apple filetype.
2224            print "\nIgnoring apple filetype file %s" % file['depotFile']
2225            return
2226
2227        # Note that we do not try to de-mangle keywords on utf16 files,
2228        # even though in theory somebody may want that.
2229        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2230        if pattern:
2231            regexp = re.compile(pattern, re.VERBOSE)
2232            text = ''.join(contents)
2233            text = regexp.sub(r'$\1$', text)
2234            contents = [ text ]
2235
2236        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2237
2238        # total length...
2239        length = 0
2240        for d in contents:
2241            length = length + len(d)
2242
2243        self.gitStream.write("data %d\n" % length)
2244        for d in contents:
2245            self.gitStream.write(d)
2246        self.gitStream.write("\n")
2247
2248    def streamOneP4Deletion(self, file):
2249        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2250        if verbose:
2251            sys.stderr.write("delete %s\n" % relPath)
2252        self.gitStream.write("D %s\n" % relPath)
2253
2254    # handle another chunk of streaming data
2255    def streamP4FilesCb(self, marshalled):
2256
2257        # catch p4 errors and complain
2258        err = None
2259        if "code" in marshalled:
2260            if marshalled["code"] == "error":
2261                if "data" in marshalled:
2262                    err = marshalled["data"].rstrip()
2263        if err:
2264            f = None
2265            if self.stream_have_file_info:
2266                if "depotFile" in self.stream_file:
2267                    f = self.stream_file["depotFile"]
2268            # force a failure in fast-import, else an empty
2269            # commit will be made
2270            self.gitStream.write("\n")
2271            self.gitStream.write("die-now\n")
2272            self.gitStream.close()
2273            # ignore errors, but make sure it exits first
2274            self.importProcess.wait()
2275            if f:
2276                die("Error from p4 print for %s: %s" % (f, err))
2277            else:
2278                die("Error from p4 print: %s" % err)
2279
2280        if marshalled.has_key('depotFile') and self.stream_have_file_info:
2281            # start of a new file - output the old one first
2282            self.streamOneP4File(self.stream_file, self.stream_contents)
2283            self.stream_file = {}
2284            self.stream_contents = []
2285            self.stream_have_file_info = False
2286
2287        # pick up the new file information... for the
2288        # 'data' field we need to append to our array
2289        for k in marshalled.keys():
2290            if k == 'data':
2291                self.stream_contents.append(marshalled['data'])
2292            else:
2293                self.stream_file[k] = marshalled[k]
2294
2295        self.stream_have_file_info = True
2296
2297    # Stream directly from "p4 files" into "git fast-import"
2298    def streamP4Files(self, files):
2299        filesForCommit = []
2300        filesToRead = []
2301        filesToDelete = []
2302
2303        for f in files:
2304            # if using a client spec, only add the files that have
2305            # a path in the client
2306            if self.clientSpecDirs:
2307                if self.clientSpecDirs.map_in_client(f['path']) == "":
2308                    continue
2309
2310            filesForCommit.append(f)
2311            if f['action'] in self.delete_actions:
2312                filesToDelete.append(f)
2313            else:
2314                filesToRead.append(f)
2315
2316        # deleted files...
2317        for f in filesToDelete:
2318            self.streamOneP4Deletion(f)
2319
2320        if len(filesToRead) > 0:
2321            self.stream_file = {}
2322            self.stream_contents = []
2323            self.stream_have_file_info = False
2324
2325            # curry self argument
2326            def streamP4FilesCbSelf(entry):
2327                self.streamP4FilesCb(entry)
2328
2329            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2330
2331            p4CmdList(["-x", "-", "print"],
2332                      stdin=fileArgs,
2333                      cb=streamP4FilesCbSelf)
2334
2335            # do the last chunk
2336            if self.stream_file.has_key('depotFile'):
2337                self.streamOneP4File(self.stream_file, self.stream_contents)
2338
2339    def make_email(self, userid):
2340        if userid in self.users:
2341            return self.users[userid]
2342        else:
2343            return "%s <a@b>" % userid
2344
2345    # Stream a p4 tag
2346    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2347        if verbose:
2348            print "writing tag %s for commit %s" % (labelName, commit)
2349        gitStream.write("tag %s\n" % labelName)
2350        gitStream.write("from %s\n" % commit)
2351
2352        if labelDetails.has_key('Owner'):
2353            owner = labelDetails["Owner"]
2354        else:
2355            owner = None
2356
2357        # Try to use the owner of the p4 label, or failing that,
2358        # the current p4 user id.
2359        if owner:
2360            email = self.make_email(owner)
2361        else:
2362            email = self.make_email(self.p4UserId())
2363        tagger = "%s %s %s" % (email, epoch, self.tz)
2364
2365        gitStream.write("tagger %s\n" % tagger)
2366
2367        print "labelDetails=",labelDetails
2368        if labelDetails.has_key('Description'):
2369            description = labelDetails['Description']
2370        else:
2371            description = 'Label from git p4'
2372
2373        gitStream.write("data %d\n" % len(description))
2374        gitStream.write(description)
2375        gitStream.write("\n")
2376
2377    def commit(self, details, files, branch, parent = ""):
2378        epoch = details["time"]
2379        author = details["user"]
2380
2381        if self.verbose:
2382            print "commit into %s" % branch
2383
2384        # start with reading files; if that fails, we should not
2385        # create a commit.
2386        new_files = []
2387        for f in files:
2388            if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2389                new_files.append (f)
2390            else:
2391                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2392
2393        if self.clientSpecDirs:
2394            self.clientSpecDirs.update_client_spec_path_cache(files)
2395
2396        self.gitStream.write("commit %s\n" % branch)
2397#        gitStream.write("mark :%s\n" % details["change"])
2398        self.committedChanges.add(int(details["change"]))
2399        committer = ""
2400        if author not in self.users:
2401            self.getUserMapFromPerforceServer()
2402        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2403
2404        self.gitStream.write("committer %s\n" % committer)
2405
2406        self.gitStream.write("data <<EOT\n")
2407        self.gitStream.write(details["desc"])
2408        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2409                             (','.join(self.branchPrefixes), details["change"]))
2410        if len(details['options']) > 0:
2411            self.gitStream.write(": options = %s" % details['options'])
2412        self.gitStream.write("]\nEOT\n\n")
2413
2414        if len(parent) > 0:
2415            if self.verbose:
2416                print "parent %s" % parent
2417            self.gitStream.write("from %s\n" % parent)
2418
2419        self.streamP4Files(new_files)
2420        self.gitStream.write("\n")
2421
2422        change = int(details["change"])
2423
2424        if self.labels.has_key(change):
2425            label = self.labels[change]
2426            labelDetails = label[0]
2427            labelRevisions = label[1]
2428            if self.verbose:
2429                print "Change %s is labelled %s" % (change, labelDetails)
2430
2431            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2432                                                for p in self.branchPrefixes])
2433
2434            if len(files) == len(labelRevisions):
2435
2436                cleanedFiles = {}
2437                for info in files:
2438                    if info["action"] in self.delete_actions:
2439                        continue
2440                    cleanedFiles[info["depotFile"]] = info["rev"]
2441
2442                if cleanedFiles == labelRevisions:
2443                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2444
2445                else:
2446                    if not self.silent:
2447                        print ("Tag %s does not match with change %s: files do not match."
2448                               % (labelDetails["label"], change))
2449
2450            else:
2451                if not self.silent:
2452                    print ("Tag %s does not match with change %s: file count is different."
2453                           % (labelDetails["label"], change))
2454
2455    # Build a dictionary of changelists and labels, for "detect-labels" option.
2456    def getLabels(self):
2457        self.labels = {}
2458
2459        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2460        if len(l) > 0 and not self.silent:
2461            print "Finding files belonging to labels in %s" % `self.depotPaths`
2462
2463        for output in l:
2464            label = output["label"]
2465            revisions = {}
2466            newestChange = 0
2467            if self.verbose:
2468                print "Querying files for label %s" % label
2469            for file in p4CmdList(["files"] +
2470                                      ["%s...@%s" % (p, label)
2471                                          for p in self.depotPaths]):
2472                revisions[file["depotFile"]] = file["rev"]
2473                change = int(file["change"])
2474                if change > newestChange:
2475                    newestChange = change
2476
2477            self.labels[newestChange] = [output, revisions]
2478
2479        if self.verbose:
2480            print "Label changes: %s" % self.labels.keys()
2481
2482    # Import p4 labels as git tags. A direct mapping does not
2483    # exist, so assume that if all the files are at the same revision
2484    # then we can use that, or it's something more complicated we should
2485    # just ignore.
2486    def importP4Labels(self, stream, p4Labels):
2487        if verbose:
2488            print "import p4 labels: " + ' '.join(p4Labels)
2489
2490        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2491        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2492        if len(validLabelRegexp) == 0:
2493            validLabelRegexp = defaultLabelRegexp
2494        m = re.compile(validLabelRegexp)
2495
2496        for name in p4Labels:
2497            commitFound = False
2498
2499            if not m.match(name):
2500                if verbose:
2501                    print "label %s does not match regexp %s" % (name,validLabelRegexp)
2502                continue
2503
2504            if name in ignoredP4Labels:
2505                continue
2506
2507            labelDetails = p4CmdList(['label', "-o", name])[0]
2508
2509            # get the most recent changelist for each file in this label
2510            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2511                                for p in self.depotPaths])
2512
2513            if change.has_key('change'):
2514                # find the corresponding git commit; take the oldest commit
2515                changelist = int(change['change'])
2516                gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2517                     "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2518                if len(gitCommit) == 0:
2519                    print "could not find git commit for changelist %d" % changelist
2520                else:
2521                    gitCommit = gitCommit.strip()
2522                    commitFound = True
2523                    # Convert from p4 time format
2524                    try:
2525                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2526                    except ValueError:
2527                        print "Could not convert label time %s" % labelDetails['Update']
2528                        tmwhen = 1
2529
2530                    when = int(time.mktime(tmwhen))
2531                    self.streamTag(stream, name, labelDetails, gitCommit, when)
2532                    if verbose:
2533                        print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2534            else:
2535                if verbose:
2536                    print "Label %s has no changelists - possibly deleted?" % name
2537
2538            if not commitFound:
2539                # We can't import this label; don't try again as it will get very
2540                # expensive repeatedly fetching all the files for labels that will
2541                # never be imported. If the label is moved in the future, the
2542                # ignore will need to be removed manually.
2543                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2544
2545    def guessProjectName(self):
2546        for p in self.depotPaths:
2547            if p.endswith("/"):
2548                p = p[:-1]
2549            p = p[p.strip().rfind("/") + 1:]
2550            if not p.endswith("/"):
2551               p += "/"
2552            return p
2553
2554    def getBranchMapping(self):
2555        lostAndFoundBranches = set()
2556
2557        user = gitConfig("git-p4.branchUser")
2558        if len(user) > 0:
2559            command = "branches -u %s" % user
2560        else:
2561            command = "branches"
2562
2563        for info in p4CmdList(command):
2564            details = p4Cmd(["branch", "-o", info["branch"]])
2565            viewIdx = 0
2566            while details.has_key("View%s" % viewIdx):
2567                paths = details["View%s" % viewIdx].split(" ")
2568                viewIdx = viewIdx + 1
2569                # require standard //depot/foo/... //depot/bar/... mapping
2570                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2571                    continue
2572                source = paths[0]
2573                destination = paths[1]
2574                ## HACK
2575                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2576                    source = source[len(self.depotPaths[0]):-4]
2577                    destination = destination[len(self.depotPaths[0]):-4]
2578
2579                    if destination in self.knownBranches:
2580                        if not self.silent:
2581                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2582                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2583                        continue
2584
2585                    self.knownBranches[destination] = source
2586
2587                    lostAndFoundBranches.discard(destination)
2588
2589                    if source not in self.knownBranches:
2590                        lostAndFoundBranches.add(source)
2591
2592        # Perforce does not strictly require branches to be defined, so we also
2593        # check git config for a branch list.
2594        #
2595        # Example of branch definition in git config file:
2596        # [git-p4]
2597        #   branchList=main:branchA
2598        #   branchList=main:branchB
2599        #   branchList=branchA:branchC
2600        configBranches = gitConfigList("git-p4.branchList")
2601        for branch in configBranches:
2602            if branch:
2603                (source, destination) = branch.split(":")
2604                self.knownBranches[destination] = source
2605
2606                lostAndFoundBranches.discard(destination)
2607
2608                if source not in self.knownBranches:
2609                    lostAndFoundBranches.add(source)
2610
2611
2612        for branch in lostAndFoundBranches:
2613            self.knownBranches[branch] = branch
2614
2615    def getBranchMappingFromGitBranches(self):
2616        branches = p4BranchesInGit(self.importIntoRemotes)
2617        for branch in branches.keys():
2618            if branch == "master":
2619                branch = "main"
2620            else:
2621                branch = branch[len(self.projectName):]
2622            self.knownBranches[branch] = branch
2623
2624    def updateOptionDict(self, d):
2625        option_keys = {}
2626        if self.keepRepoPath:
2627            option_keys['keepRepoPath'] = 1
2628
2629        d["options"] = ' '.join(sorted(option_keys.keys()))
2630
2631    def readOptions(self, d):
2632        self.keepRepoPath = (d.has_key('options')
2633                             and ('keepRepoPath' in d['options']))
2634
2635    def gitRefForBranch(self, branch):
2636        if branch == "main":
2637            return self.refPrefix + "master"
2638
2639        if len(branch) <= 0:
2640            return branch
2641
2642        return self.refPrefix + self.projectName + branch
2643
2644    def gitCommitByP4Change(self, ref, change):
2645        if self.verbose:
2646            print "looking in ref " + ref + " for change %s using bisect..." % change
2647
2648        earliestCommit = ""
2649        latestCommit = parseRevision(ref)
2650
2651        while True:
2652            if self.verbose:
2653                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2654            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2655            if len(next) == 0:
2656                if self.verbose:
2657                    print "argh"
2658                return ""
2659            log = extractLogMessageFromGitCommit(next)
2660            settings = extractSettingsGitLog(log)
2661            currentChange = int(settings['change'])
2662            if self.verbose:
2663                print "current change %s" % currentChange
2664
2665            if currentChange == change:
2666                if self.verbose:
2667                    print "found %s" % next
2668                return next
2669
2670            if currentChange < change:
2671                earliestCommit = "^%s" % next
2672            else:
2673                latestCommit = "%s" % next
2674
2675        return ""
2676
2677    def importNewBranch(self, branch, maxChange):
2678        # make fast-import flush all changes to disk and update the refs using the checkpoint
2679        # command so that we can try to find the branch parent in the git history
2680        self.gitStream.write("checkpoint\n\n");
2681        self.gitStream.flush();
2682        branchPrefix = self.depotPaths[0] + branch + "/"
2683        range = "@1,%s" % maxChange
2684        #print "prefix" + branchPrefix
2685        changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
2686        if len(changes) <= 0:
2687            return False
2688        firstChange = changes[0]
2689        #print "first change in branch: %s" % firstChange
2690        sourceBranch = self.knownBranches[branch]
2691        sourceDepotPath = self.depotPaths[0] + sourceBranch
2692        sourceRef = self.gitRefForBranch(sourceBranch)
2693        #print "source " + sourceBranch
2694
2695        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2696        #print "branch parent: %s" % branchParentChange
2697        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2698        if len(gitParent) > 0:
2699            self.initialParents[self.gitRefForBranch(branch)] = gitParent
2700            #print "parent git commit: %s" % gitParent
2701
2702        self.importChanges(changes)
2703        return True
2704
2705    def searchParent(self, parent, branch, target):
2706        parentFound = False
2707        for blob in read_pipe_lines(["git", "rev-list", "--reverse",
2708                                     "--no-merges", parent]):
2709            blob = blob.strip()
2710            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2711                parentFound = True
2712                if self.verbose:
2713                    print "Found parent of %s in commit %s" % (branch, blob)
2714                break
2715        if parentFound:
2716            return blob
2717        else:
2718            return None
2719
2720    def importChanges(self, changes):
2721        cnt = 1
2722        for change in changes:
2723            description = p4_describe(change)
2724            self.updateOptionDict(description)
2725
2726            if not self.silent:
2727                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2728                sys.stdout.flush()
2729            cnt = cnt + 1
2730
2731            try:
2732                if self.detectBranches:
2733                    branches = self.splitFilesIntoBranches(description)
2734                    for branch in branches.keys():
2735                        ## HACK  --hwn
2736                        branchPrefix = self.depotPaths[0] + branch + "/"
2737                        self.branchPrefixes = [ branchPrefix ]
2738
2739                        parent = ""
2740
2741                        filesForCommit = branches[branch]
2742
2743                        if self.verbose:
2744                            print "branch is %s" % branch
2745
2746                        self.updatedBranches.add(branch)
2747
2748                        if branch not in self.createdBranches:
2749                            self.createdBranches.add(branch)
2750                            parent = self.knownBranches[branch]
2751                            if parent == branch:
2752                                parent = ""
2753                            else:
2754                                fullBranch = self.projectName + branch
2755                                if fullBranch not in self.p4BranchesInGit:
2756                                    if not self.silent:
2757                                        print("\n    Importing new branch %s" % fullBranch);
2758                                    if self.importNewBranch(branch, change - 1):
2759                                        parent = ""
2760                                        self.p4BranchesInGit.append(fullBranch)
2761                                    if not self.silent:
2762                                        print("\n    Resuming with change %s" % change);
2763
2764                                if self.verbose:
2765                                    print "parent determined through known branches: %s" % parent
2766
2767                        branch = self.gitRefForBranch(branch)
2768                        parent = self.gitRefForBranch(parent)
2769
2770                        if self.verbose:
2771                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2772
2773                        if len(parent) == 0 and branch in self.initialParents:
2774                            parent = self.initialParents[branch]
2775                            del self.initialParents[branch]
2776
2777                        blob = None
2778                        if len(parent) > 0:
2779                            tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2780                            if self.verbose:
2781                                print "Creating temporary branch: " + tempBranch
2782                            self.commit(description, filesForCommit, tempBranch)
2783                            self.tempBranches.append(tempBranch)
2784                            self.checkpoint()
2785                            blob = self.searchParent(parent, branch, tempBranch)
2786                        if blob:
2787                            self.commit(description, filesForCommit, branch, blob)
2788                        else:
2789                            if self.verbose:
2790                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2791                            self.commit(description, filesForCommit, branch, parent)
2792                else:
2793                    files = self.extractFilesFromCommit(description)
2794                    self.commit(description, files, self.branch,
2795                                self.initialParent)
2796                    # only needed once, to connect to the previous commit
2797                    self.initialParent = ""
2798            except IOError:
2799                print self.gitError.read()
2800                sys.exit(1)
2801
2802    def importHeadRevision(self, revision):
2803        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2804
2805        details = {}
2806        details["user"] = "git perforce import user"
2807        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2808                           % (' '.join(self.depotPaths), revision))
2809        details["change"] = revision
2810        newestRevision = 0
2811
2812        fileCnt = 0
2813        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2814
2815        for info in p4CmdList(["files"] + fileArgs):
2816
2817            if 'code' in info and info['code'] == 'error':
2818                sys.stderr.write("p4 returned an error: %s\n"
2819                                 % info['data'])
2820                if info['data'].find("must refer to client") >= 0:
2821                    sys.stderr.write("This particular p4 error is misleading.\n")
2822                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
2823                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2824                sys.exit(1)
2825            if 'p4ExitCode' in info:
2826                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2827                sys.exit(1)
2828
2829
2830            change = int(info["change"])
2831            if change > newestRevision:
2832                newestRevision = change
2833
2834            if info["action"] in self.delete_actions:
2835                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2836                #fileCnt = fileCnt + 1
2837                continue
2838
2839            for prop in ["depotFile", "rev", "action", "type" ]:
2840                details["%s%s" % (prop, fileCnt)] = info[prop]
2841
2842            fileCnt = fileCnt + 1
2843
2844        details["change"] = newestRevision
2845
2846        # Use time from top-most change so that all git p4 clones of
2847        # the same p4 repo have the same commit SHA1s.
2848        res = p4_describe(newestRevision)
2849        details["time"] = res["time"]
2850
2851        self.updateOptionDict(details)
2852        try:
2853            self.commit(details, self.extractFilesFromCommit(details), self.branch)
2854        except IOError:
2855            print "IO error with git fast-import. Is your git version recent enough?"
2856            print self.gitError.read()
2857
2858
2859    def run(self, args):
2860        self.depotPaths = []
2861        self.changeRange = ""
2862        self.previousDepotPaths = []
2863        self.hasOrigin = False
2864
2865        # map from branch depot path to parent branch
2866        self.knownBranches = {}
2867        self.initialParents = {}
2868
2869        if self.importIntoRemotes:
2870            self.refPrefix = "refs/remotes/p4/"
2871        else:
2872            self.refPrefix = "refs/heads/p4/"
2873
2874        if self.syncWithOrigin:
2875            self.hasOrigin = originP4BranchesExist()
2876            if self.hasOrigin:
2877                if not self.silent:
2878                    print 'Syncing with origin first, using "git fetch origin"'
2879                system("git fetch origin")
2880
2881        branch_arg_given = bool(self.branch)
2882        if len(self.branch) == 0:
2883            self.branch = self.refPrefix + "master"
2884            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2885                system("git update-ref %s refs/heads/p4" % self.branch)
2886                system("git branch -D p4")
2887
2888        # accept either the command-line option, or the configuration variable
2889        if self.useClientSpec:
2890            # will use this after clone to set the variable
2891            self.useClientSpec_from_options = True
2892        else:
2893            if gitConfigBool("git-p4.useclientspec"):
2894                self.useClientSpec = True
2895        if self.useClientSpec:
2896            self.clientSpecDirs = getClientSpec()
2897
2898        # TODO: should always look at previous commits,
2899        # merge with previous imports, if possible.
2900        if args == []:
2901            if self.hasOrigin:
2902                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2903
2904            # branches holds mapping from branch name to sha1
2905            branches = p4BranchesInGit(self.importIntoRemotes)
2906
2907            # restrict to just this one, disabling detect-branches
2908            if branch_arg_given:
2909                short = self.branch.split("/")[-1]
2910                if short in branches:
2911                    self.p4BranchesInGit = [ short ]
2912            else:
2913                self.p4BranchesInGit = branches.keys()
2914
2915            if len(self.p4BranchesInGit) > 1:
2916                if not self.silent:
2917                    print "Importing from/into multiple branches"
2918                self.detectBranches = True
2919                for branch in branches.keys():
2920                    self.initialParents[self.refPrefix + branch] = \
2921                        branches[branch]
2922
2923            if self.verbose:
2924                print "branches: %s" % self.p4BranchesInGit
2925
2926            p4Change = 0
2927            for branch in self.p4BranchesInGit:
2928                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2929
2930                settings = extractSettingsGitLog(logMsg)
2931
2932                self.readOptions(settings)
2933                if (settings.has_key('depot-paths')
2934                    and settings.has_key ('change')):
2935                    change = int(settings['change']) + 1
2936                    p4Change = max(p4Change, change)
2937
2938                    depotPaths = sorted(settings['depot-paths'])
2939                    if self.previousDepotPaths == []:
2940                        self.previousDepotPaths = depotPaths
2941                    else:
2942                        paths = []
2943                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2944                            prev_list = prev.split("/")
2945                            cur_list = cur.split("/")
2946                            for i in range(0, min(len(cur_list), len(prev_list))):
2947                                if cur_list[i] <> prev_list[i]:
2948                                    i = i - 1
2949                                    break
2950
2951                            paths.append ("/".join(cur_list[:i + 1]))
2952
2953                        self.previousDepotPaths = paths
2954
2955            if p4Change > 0:
2956                self.depotPaths = sorted(self.previousDepotPaths)
2957                self.changeRange = "@%s,#head" % p4Change
2958                if not self.silent and not self.detectBranches:
2959                    print "Performing incremental import into %s git branch" % self.branch
2960
2961        # accept multiple ref name abbreviations:
2962        #    refs/foo/bar/branch -> use it exactly
2963        #    p4/branch -> prepend refs/remotes/ or refs/heads/
2964        #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2965        if not self.branch.startswith("refs/"):
2966            if self.importIntoRemotes:
2967                prepend = "refs/remotes/"
2968            else:
2969                prepend = "refs/heads/"
2970            if not self.branch.startswith("p4/"):
2971                prepend += "p4/"
2972            self.branch = prepend + self.branch
2973
2974        if len(args) == 0 and self.depotPaths:
2975            if not self.silent:
2976                print "Depot paths: %s" % ' '.join(self.depotPaths)
2977        else:
2978            if self.depotPaths and self.depotPaths != args:
2979                print ("previous import used depot path %s and now %s was specified. "
2980                       "This doesn't work!" % (' '.join (self.depotPaths),
2981                                               ' '.join (args)))
2982                sys.exit(1)
2983
2984            self.depotPaths = sorted(args)
2985
2986        revision = ""
2987        self.users = {}
2988
2989        # Make sure no revision specifiers are used when --changesfile
2990        # is specified.
2991        bad_changesfile = False
2992        if len(self.changesFile) > 0:
2993            for p in self.depotPaths:
2994                if p.find("@") >= 0 or p.find("#") >= 0:
2995                    bad_changesfile = True
2996                    break
2997        if bad_changesfile:
2998            die("Option --changesfile is incompatible with revision specifiers")
2999
3000        newPaths = []
3001        for p in self.depotPaths:
3002            if p.find("@") != -1:
3003                atIdx = p.index("@")
3004                self.changeRange = p[atIdx:]
3005                if self.changeRange == "@all":
3006                    self.changeRange = ""
3007                elif ',' not in self.changeRange:
3008                    revision = self.changeRange
3009                    self.changeRange = ""
3010                p = p[:atIdx]
3011            elif p.find("#") != -1:
3012                hashIdx = p.index("#")
3013                revision = p[hashIdx:]
3014                p = p[:hashIdx]
3015            elif self.previousDepotPaths == []:
3016                # pay attention to changesfile, if given, else import
3017                # the entire p4 tree at the head revision
3018                if len(self.changesFile) == 0:
3019                    revision = "#head"
3020
3021            p = re.sub ("\.\.\.$", "", p)
3022            if not p.endswith("/"):
3023                p += "/"
3024
3025            newPaths.append(p)
3026
3027        self.depotPaths = newPaths
3028
3029        # --detect-branches may change this for each branch
3030        self.branchPrefixes = self.depotPaths
3031
3032        self.loadUserMapFromCache()
3033        self.labels = {}
3034        if self.detectLabels:
3035            self.getLabels();
3036
3037        if self.detectBranches:
3038            ## FIXME - what's a P4 projectName ?
3039            self.projectName = self.guessProjectName()
3040
3041            if self.hasOrigin:
3042                self.getBranchMappingFromGitBranches()
3043            else:
3044                self.getBranchMapping()
3045            if self.verbose:
3046                print "p4-git branches: %s" % self.p4BranchesInGit
3047                print "initial parents: %s" % self.initialParents
3048            for b in self.p4BranchesInGit:
3049                if b != "master":
3050
3051                    ## FIXME
3052                    b = b[len(self.projectName):]
3053                self.createdBranches.add(b)
3054
3055        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
3056
3057        self.importProcess = subprocess.Popen(["git", "fast-import"],
3058                                              stdin=subprocess.PIPE,
3059                                              stdout=subprocess.PIPE,
3060                                              stderr=subprocess.PIPE);
3061        self.gitOutput = self.importProcess.stdout
3062        self.gitStream = self.importProcess.stdin
3063        self.gitError = self.importProcess.stderr
3064
3065        if revision:
3066            self.importHeadRevision(revision)
3067        else:
3068            changes = []
3069
3070            if len(self.changesFile) > 0:
3071                output = open(self.changesFile).readlines()
3072                changeSet = set()
3073                for line in output:
3074                    changeSet.add(int(line))
3075
3076                for change in changeSet:
3077                    changes.append(change)
3078
3079                changes.sort()
3080            else:
3081                # catch "git p4 sync" with no new branches, in a repo that
3082                # does not have any existing p4 branches
3083                if len(args) == 0:
3084                    if not self.p4BranchesInGit:
3085                        die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
3086
3087                    # The default branch is master, unless --branch is used to
3088                    # specify something else.  Make sure it exists, or complain
3089                    # nicely about how to use --branch.
3090                    if not self.detectBranches:
3091                        if not branch_exists(self.branch):
3092                            if branch_arg_given:
3093                                die("Error: branch %s does not exist." % self.branch)
3094                            else:
3095                                die("Error: no branch %s; perhaps specify one with --branch." %
3096                                    self.branch)
3097
3098                if self.verbose:
3099                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3100                                                              self.changeRange)
3101                changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3102
3103                if len(self.maxChanges) > 0:
3104                    changes = changes[:min(int(self.maxChanges), len(changes))]
3105
3106            if len(changes) == 0:
3107                if not self.silent:
3108                    print "No changes to import!"
3109            else:
3110                if not self.silent and not self.detectBranches:
3111                    print "Import destination: %s" % self.branch
3112
3113                self.updatedBranches = set()
3114
3115                if not self.detectBranches:
3116                    if args:
3117                        # start a new branch
3118                        self.initialParent = ""
3119                    else:
3120                        # build on a previous revision
3121                        self.initialParent = parseRevision(self.branch)
3122
3123                self.importChanges(changes)
3124
3125                if not self.silent:
3126                    print ""
3127                    if len(self.updatedBranches) > 0:
3128                        sys.stdout.write("Updated branches: ")
3129                        for b in self.updatedBranches:
3130                            sys.stdout.write("%s " % b)
3131                        sys.stdout.write("\n")
3132
3133        if gitConfigBool("git-p4.importLabels"):
3134            self.importLabels = True
3135
3136        if self.importLabels:
3137            p4Labels = getP4Labels(self.depotPaths)
3138            gitTags = getGitTags()
3139
3140            missingP4Labels = p4Labels - gitTags
3141            self.importP4Labels(self.gitStream, missingP4Labels)
3142
3143        self.gitStream.close()
3144        if self.importProcess.wait() != 0:
3145            die("fast-import failed: %s" % self.gitError.read())
3146        self.gitOutput.close()
3147        self.gitError.close()
3148
3149        # Cleanup temporary branches created during import
3150        if self.tempBranches != []:
3151            for branch in self.tempBranches:
3152                read_pipe("git update-ref -d %s" % branch)
3153            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3154
3155        # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3156        # a convenient shortcut refname "p4".
3157        if self.importIntoRemotes:
3158            head_ref = self.refPrefix + "HEAD"
3159            if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3160                system(["git", "symbolic-ref", head_ref, self.branch])
3161
3162        return True
3163
3164class P4Rebase(Command):
3165    def __init__(self):
3166        Command.__init__(self)
3167        self.options = [
3168                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3169        ]
3170        self.importLabels = False
3171        self.description = ("Fetches the latest revision from perforce and "
3172                            + "rebases the current work (branch) against it")
3173
3174    def run(self, args):
3175        sync = P4Sync()
3176        sync.importLabels = self.importLabels
3177        sync.run([])
3178
3179        return self.rebase()
3180
3181    def rebase(self):
3182        if os.system("git update-index --refresh") != 0:
3183            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.");
3184        if len(read_pipe("git diff-index HEAD --")) > 0:
3185            die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3186
3187        [upstream, settings] = findUpstreamBranchPoint()
3188        if len(upstream) == 0:
3189            die("Cannot find upstream branchpoint for rebase")
3190
3191        # the branchpoint may be p4/foo~3, so strip off the parent
3192        upstream = re.sub("~[0-9]+$", "", upstream)
3193
3194        print "Rebasing the current branch onto %s" % upstream
3195        oldHead = read_pipe("git rev-parse HEAD").strip()
3196        system("git rebase %s" % upstream)
3197        system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3198        return True
3199
3200class P4Clone(P4Sync):
3201    def __init__(self):
3202        P4Sync.__init__(self)
3203        self.description = "Creates a new git repository and imports from Perforce into it"
3204        self.usage = "usage: %prog [options] //depot/path[@revRange]"
3205        self.options += [
3206            optparse.make_option("--destination", dest="cloneDestination",
3207                                 action='store', default=None,
3208                                 help="where to leave result of the clone"),
3209            optparse.make_option("--bare", dest="cloneBare",
3210                                 action="store_true", default=False),
3211        ]
3212        self.cloneDestination = None
3213        self.needsGit = False
3214        self.cloneBare = False
3215
3216    def defaultDestination(self, args):
3217        ## TODO: use common prefix of args?
3218        depotPath = args[0]
3219        depotDir = re.sub("(@[^@]*)$", "", depotPath)
3220        depotDir = re.sub("(#[^#]*)$", "", depotDir)
3221        depotDir = re.sub(r"\.\.\.$", "", depotDir)
3222        depotDir = re.sub(r"/$", "", depotDir)
3223        return os.path.split(depotDir)[1]
3224
3225    def run(self, args):
3226        if len(args) < 1:
3227            return False
3228
3229        if self.keepRepoPath and not self.cloneDestination:
3230            sys.stderr.write("Must specify destination for --keep-path\n")
3231            sys.exit(1)
3232
3233        depotPaths = args
3234
3235        if not self.cloneDestination and len(depotPaths) > 1:
3236            self.cloneDestination = depotPaths[-1]
3237            depotPaths = depotPaths[:-1]
3238
3239        self.cloneExclude = ["/"+p for p in self.cloneExclude]
3240        for p in depotPaths:
3241            if not p.startswith("//"):
3242                sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3243                return False
3244
3245        if not self.cloneDestination:
3246            self.cloneDestination = self.defaultDestination(args)
3247
3248        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3249
3250        if not os.path.exists(self.cloneDestination):
3251            os.makedirs(self.cloneDestination)
3252        chdir(self.cloneDestination)
3253
3254        init_cmd = [ "git", "init" ]
3255        if self.cloneBare:
3256            init_cmd.append("--bare")
3257        retcode = subprocess.call(init_cmd)
3258        if retcode:
3259            raise CalledProcessError(retcode, init_cmd)
3260
3261        if not P4Sync.run(self, depotPaths):
3262            return False
3263
3264        # create a master branch and check out a work tree
3265        if gitBranchExists(self.branch):
3266            system([ "git", "branch", "master", self.branch ])
3267            if not self.cloneBare:
3268                system([ "git", "checkout", "-f" ])
3269        else:
3270            print 'Not checking out any branch, use ' \
3271                  '"git checkout -q -b master <branch>"'
3272
3273        # auto-set this variable if invoked with --use-client-spec
3274        if self.useClientSpec_from_options:
3275            system("git config --bool git-p4.useclientspec true")
3276
3277        return True
3278
3279class P4Branches(Command):
3280    def __init__(self):
3281        Command.__init__(self)
3282        self.options = [ ]
3283        self.description = ("Shows the git branches that hold imports and their "
3284                            + "corresponding perforce depot paths")
3285        self.verbose = False
3286
3287    def run(self, args):
3288        if originP4BranchesExist():
3289            createOrUpdateBranchesFromOrigin()
3290
3291        cmdline = "git rev-parse --symbolic "
3292        cmdline += " --remotes"
3293
3294        for line in read_pipe_lines(cmdline):
3295            line = line.strip()
3296
3297            if not line.startswith('p4/') or line == "p4/HEAD":
3298                continue
3299            branch = line
3300
3301            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3302            settings = extractSettingsGitLog(log)
3303
3304            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3305        return True
3306
3307class HelpFormatter(optparse.IndentedHelpFormatter):
3308    def __init__(self):
3309        optparse.IndentedHelpFormatter.__init__(self)
3310
3311    def format_description(self, description):
3312        if description:
3313            return description + "\n"
3314        else:
3315            return ""
3316
3317def printUsage(commands):
3318    print "usage: %s <command> [options]" % sys.argv[0]
3319    print ""
3320    print "valid commands: %s" % ", ".join(commands)
3321    print ""
3322    print "Try %s <command> --help for command specific help." % sys.argv[0]
3323    print ""
3324
3325commands = {
3326    "debug" : P4Debug,
3327    "submit" : P4Submit,
3328    "commit" : P4Submit,
3329    "sync" : P4Sync,
3330    "rebase" : P4Rebase,
3331    "clone" : P4Clone,
3332    "rollback" : P4RollBack,
3333    "branches" : P4Branches
3334}
3335
3336
3337def main():
3338    if len(sys.argv[1:]) == 0:
3339        printUsage(commands.keys())
3340        sys.exit(2)
3341
3342    cmdName = sys.argv[1]
3343    try:
3344        klass = commands[cmdName]
3345        cmd = klass()
3346    except KeyError:
3347        print "unknown command %s" % cmdName
3348        print ""
3349        printUsage(commands.keys())
3350        sys.exit(2)
3351
3352    options = cmd.options
3353    cmd.gitdir = os.environ.get("GIT_DIR", None)
3354
3355    args = sys.argv[2:]
3356
3357    options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3358    if cmd.needsGit:
3359        options.append(optparse.make_option("--git-dir", dest="gitdir"))
3360
3361    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3362                                   options,
3363                                   description = cmd.description,
3364                                   formatter = HelpFormatter())
3365
3366    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3367    global verbose
3368    verbose = cmd.verbose
3369    if cmd.needsGit:
3370        if cmd.gitdir == None:
3371            cmd.gitdir = os.path.abspath(".git")
3372            if not isValidGitDir(cmd.gitdir):
3373                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3374                if os.path.exists(cmd.gitdir):
3375                    cdup = read_pipe("git rev-parse --show-cdup").strip()
3376                    if len(cdup) > 0:
3377                        chdir(cdup);
3378
3379        if not isValidGitDir(cmd.gitdir):
3380            if isValidGitDir(cmd.gitdir + "/.git"):
3381                cmd.gitdir += "/.git"
3382            else:
3383                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3384
3385        os.environ["GIT_DIR"] = cmd.gitdir
3386
3387    if not cmd.run(args):
3388        parser.print_help()
3389        sys.exit(2)
3390
3391
3392if __name__ == '__main__':
3393    main()