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