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