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