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