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