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