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