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