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