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