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