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