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