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