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