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