2dd2b76b1c3057df3b484cf5973e978d7d37654c
   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                optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
 857                optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
 858        ]
 859        self.description = "Submit changes from git to the perforce depot."
 860        self.usage += " [name of git branch to submit into perforce depot]"
 861        self.origin = ""
 862        self.detectRenames = False
 863        self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
 864        self.dry_run = False
 865        self.prepare_p4_only = False
 866        self.isWindows = (platform.system() == "Windows")
 867        self.exportLabels = False
 868        self.p4HasMoveCommand = p4_has_command("move")
 869
 870    def check(self):
 871        if len(p4CmdList("opened ...")) > 0:
 872            die("You have files opened with perforce! Close them before starting the sync.")
 873
 874    def separate_jobs_from_description(self, message):
 875        """Extract and return a possible Jobs field in the commit
 876           message.  It goes into a separate section in the p4 change
 877           specification.
 878
 879           A jobs line starts with "Jobs:" and looks like a new field
 880           in a form.  Values are white-space separated on the same
 881           line or on following lines that start with a tab.
 882
 883           This does not parse and extract the full git commit message
 884           like a p4 form.  It just sees the Jobs: line as a marker
 885           to pass everything from then on directly into the p4 form,
 886           but outside the description section.
 887
 888           Return a tuple (stripped log message, jobs string)."""
 889
 890        m = re.search(r'^Jobs:', message, re.MULTILINE)
 891        if m is None:
 892            return (message, None)
 893
 894        jobtext = message[m.start():]
 895        stripped_message = message[:m.start()].rstrip()
 896        return (stripped_message, jobtext)
 897
 898    def prepareLogMessage(self, template, message, jobs):
 899        """Edits the template returned from "p4 change -o" to insert
 900           the message in the Description field, and the jobs text in
 901           the Jobs field."""
 902        result = ""
 903
 904        inDescriptionSection = False
 905
 906        for line in template.split("\n"):
 907            if line.startswith("#"):
 908                result += line + "\n"
 909                continue
 910
 911            if inDescriptionSection:
 912                if line.startswith("Files:") or line.startswith("Jobs:"):
 913                    inDescriptionSection = False
 914                    # insert Jobs section
 915                    if jobs:
 916                        result += jobs + "\n"
 917                else:
 918                    continue
 919            else:
 920                if line.startswith("Description:"):
 921                    inDescriptionSection = True
 922                    line += "\n"
 923                    for messageLine in message.split("\n"):
 924                        line += "\t" + messageLine + "\n"
 925
 926            result += line + "\n"
 927
 928        return result
 929
 930    def patchRCSKeywords(self, file, pattern):
 931        # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
 932        (handle, outFileName) = tempfile.mkstemp(dir='.')
 933        try:
 934            outFile = os.fdopen(handle, "w+")
 935            inFile = open(file, "r")
 936            regexp = re.compile(pattern, re.VERBOSE)
 937            for line in inFile.readlines():
 938                line = regexp.sub(r'$\1$', line)
 939                outFile.write(line)
 940            inFile.close()
 941            outFile.close()
 942            # Forcibly overwrite the original file
 943            os.unlink(file)
 944            shutil.move(outFileName, file)
 945        except:
 946            # cleanup our temporary file
 947            os.unlink(outFileName)
 948            print "Failed to strip RCS keywords in %s" % file
 949            raise
 950
 951        print "Patched up RCS keywords in %s" % file
 952
 953    def p4UserForCommit(self,id):
 954        # Return the tuple (perforce user,git email) for a given git commit id
 955        self.getUserMapFromPerforceServer()
 956        gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
 957        gitEmail = gitEmail.strip()
 958        if not self.emails.has_key(gitEmail):
 959            return (None,gitEmail)
 960        else:
 961            return (self.emails[gitEmail],gitEmail)
 962
 963    def checkValidP4Users(self,commits):
 964        # check if any git authors cannot be mapped to p4 users
 965        for id in commits:
 966            (user,email) = self.p4UserForCommit(id)
 967            if not user:
 968                msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
 969                if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
 970                    print "%s" % msg
 971                else:
 972                    die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
 973
 974    def lastP4Changelist(self):
 975        # Get back the last changelist number submitted in this client spec. This
 976        # then gets used to patch up the username in the change. If the same
 977        # client spec is being used by multiple processes then this might go
 978        # wrong.
 979        results = p4CmdList("client -o")        # find the current client
 980        client = None
 981        for r in results:
 982            if r.has_key('Client'):
 983                client = r['Client']
 984                break
 985        if not client:
 986            die("could not get client spec")
 987        results = p4CmdList(["changes", "-c", client, "-m", "1"])
 988        for r in results:
 989            if r.has_key('change'):
 990                return r['change']
 991        die("Could not get changelist number for last submit - cannot patch up user details")
 992
 993    def modifyChangelistUser(self, changelist, newUser):
 994        # fixup the user field of a changelist after it has been submitted.
 995        changes = p4CmdList("change -o %s" % changelist)
 996        if len(changes) != 1:
 997            die("Bad output from p4 change modifying %s to user %s" %
 998                (changelist, newUser))
 999
1000        c = changes[0]
1001        if c['User'] == newUser: return   # nothing to do
1002        c['User'] = newUser
1003        input = marshal.dumps(c)
1004
1005        result = p4CmdList("change -f -i", stdin=input)
1006        for r in result:
1007            if r.has_key('code'):
1008                if r['code'] == 'error':
1009                    die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1010            if r.has_key('data'):
1011                print("Updated user field for changelist %s to %s" % (changelist, newUser))
1012                return
1013        die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1014
1015    def canChangeChangelists(self):
1016        # check to see if we have p4 admin or super-user permissions, either of
1017        # which are required to modify changelists.
1018        results = p4CmdList(["protects", self.depotPath])
1019        for r in results:
1020            if r.has_key('perm'):
1021                if r['perm'] == 'admin':
1022                    return 1
1023                if r['perm'] == 'super':
1024                    return 1
1025        return 0
1026
1027    def prepareSubmitTemplate(self):
1028        """Run "p4 change -o" to grab a change specification template.
1029           This does not use "p4 -G", as it is nice to keep the submission
1030           template in original order, since a human might edit it.
1031
1032           Remove lines in the Files section that show changes to files
1033           outside the depot path we're committing into."""
1034
1035        template = ""
1036        inFilesSection = False
1037        for line in p4_read_pipe_lines(['change', '-o']):
1038            if line.endswith("\r\n"):
1039                line = line[:-2] + "\n"
1040            if inFilesSection:
1041                if line.startswith("\t"):
1042                    # path starts and ends with a tab
1043                    path = line[1:]
1044                    lastTab = path.rfind("\t")
1045                    if lastTab != -1:
1046                        path = path[:lastTab]
1047                        if not p4PathStartsWith(path, self.depotPath):
1048                            continue
1049                else:
1050                    inFilesSection = False
1051            else:
1052                if line.startswith("Files:"):
1053                    inFilesSection = True
1054
1055            template += line
1056
1057        return template
1058
1059    def edit_template(self, template_file):
1060        """Invoke the editor to let the user change the submission
1061           message.  Return true if okay to continue with the submit."""
1062
1063        # if configured to skip the editing part, just submit
1064        if gitConfig("git-p4.skipSubmitEdit") == "true":
1065            return True
1066
1067        # look at the modification time, to check later if the user saved
1068        # the file
1069        mtime = os.stat(template_file).st_mtime
1070
1071        # invoke the editor
1072        if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1073            editor = os.environ.get("P4EDITOR")
1074        else:
1075            editor = read_pipe("git var GIT_EDITOR").strip()
1076        system(editor + " " + template_file)
1077
1078        # If the file was not saved, prompt to see if this patch should
1079        # be skipped.  But skip this verification step if configured so.
1080        if gitConfig("git-p4.skipSubmitEditCheck") == "true":
1081            return True
1082
1083        # modification time updated means user saved the file
1084        if os.stat(template_file).st_mtime > mtime:
1085            return True
1086
1087        while True:
1088            response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1089            if response == 'y':
1090                return True
1091            if response == 'n':
1092                return False
1093
1094    def applyCommit(self, id):
1095        """Apply one commit, return True if it succeeded."""
1096
1097        print "Applying", read_pipe(["git", "show", "-s",
1098                                     "--format=format:%h %s", id])
1099
1100        (p4User, gitEmail) = self.p4UserForCommit(id)
1101
1102        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1103        filesToAdd = set()
1104        filesToDelete = set()
1105        editedFiles = set()
1106        pureRenameCopy = set()
1107        filesToChangeExecBit = {}
1108
1109        for line in diff:
1110            diff = parseDiffTreeEntry(line)
1111            modifier = diff['status']
1112            path = diff['src']
1113            if modifier == "M":
1114                p4_edit(path)
1115                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1116                    filesToChangeExecBit[path] = diff['dst_mode']
1117                editedFiles.add(path)
1118            elif modifier == "A":
1119                filesToAdd.add(path)
1120                filesToChangeExecBit[path] = diff['dst_mode']
1121                if path in filesToDelete:
1122                    filesToDelete.remove(path)
1123            elif modifier == "D":
1124                filesToDelete.add(path)
1125                if path in filesToAdd:
1126                    filesToAdd.remove(path)
1127            elif modifier == "C":
1128                src, dest = diff['src'], diff['dst']
1129                p4_integrate(src, dest)
1130                pureRenameCopy.add(dest)
1131                if diff['src_sha1'] != diff['dst_sha1']:
1132                    p4_edit(dest)
1133                    pureRenameCopy.discard(dest)
1134                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1135                    p4_edit(dest)
1136                    pureRenameCopy.discard(dest)
1137                    filesToChangeExecBit[dest] = diff['dst_mode']
1138                os.unlink(dest)
1139                editedFiles.add(dest)
1140            elif modifier == "R":
1141                src, dest = diff['src'], diff['dst']
1142                if self.p4HasMoveCommand:
1143                    p4_edit(src)        # src must be open before move
1144                    p4_move(src, dest)  # opens for (move/delete, move/add)
1145                else:
1146                    p4_integrate(src, dest)
1147                    if diff['src_sha1'] != diff['dst_sha1']:
1148                        p4_edit(dest)
1149                    else:
1150                        pureRenameCopy.add(dest)
1151                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1152                    if not self.p4HasMoveCommand:
1153                        p4_edit(dest)   # with move: already open, writable
1154                    filesToChangeExecBit[dest] = diff['dst_mode']
1155                if not self.p4HasMoveCommand:
1156                    os.unlink(dest)
1157                    filesToDelete.add(src)
1158                editedFiles.add(dest)
1159            else:
1160                die("unknown modifier %s for %s" % (modifier, path))
1161
1162        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
1163        patchcmd = diffcmd + " | git apply "
1164        tryPatchCmd = patchcmd + "--check -"
1165        applyPatchCmd = patchcmd + "--check --apply -"
1166        patch_succeeded = True
1167
1168        if os.system(tryPatchCmd) != 0:
1169            fixed_rcs_keywords = False
1170            patch_succeeded = False
1171            print "Unfortunately applying the change failed!"
1172
1173            # Patch failed, maybe it's just RCS keyword woes. Look through
1174            # the patch to see if that's possible.
1175            if gitConfig("git-p4.attemptRCSCleanup","--bool") == "true":
1176                file = None
1177                pattern = None
1178                kwfiles = {}
1179                for file in editedFiles | filesToDelete:
1180                    # did this file's delta contain RCS keywords?
1181                    pattern = p4_keywords_regexp_for_file(file)
1182
1183                    if pattern:
1184                        # this file is a possibility...look for RCS keywords.
1185                        regexp = re.compile(pattern, re.VERBOSE)
1186                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1187                            if regexp.search(line):
1188                                if verbose:
1189                                    print "got keyword match on %s in %s in %s" % (pattern, line, file)
1190                                kwfiles[file] = pattern
1191                                break
1192
1193                for file in kwfiles:
1194                    if verbose:
1195                        print "zapping %s with %s" % (line,pattern)
1196                    self.patchRCSKeywords(file, kwfiles[file])
1197                    fixed_rcs_keywords = True
1198
1199            if fixed_rcs_keywords:
1200                print "Retrying the patch with RCS keywords cleaned up"
1201                if os.system(tryPatchCmd) == 0:
1202                    patch_succeeded = True
1203
1204        if not patch_succeeded:
1205            for f in editedFiles:
1206                p4_revert(f)
1207            return False
1208
1209        #
1210        # Apply the patch for real, and do add/delete/+x handling.
1211        #
1212        system(applyPatchCmd)
1213
1214        for f in filesToAdd:
1215            p4_add(f)
1216        for f in filesToDelete:
1217            p4_revert(f)
1218            p4_delete(f)
1219
1220        # Set/clear executable bits
1221        for f in filesToChangeExecBit.keys():
1222            mode = filesToChangeExecBit[f]
1223            setP4ExecBit(f, mode)
1224
1225        #
1226        # Build p4 change description, starting with the contents
1227        # of the git commit message.
1228        #
1229        logMessage = extractLogMessageFromGitCommit(id)
1230        logMessage = logMessage.strip()
1231        (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1232
1233        template = self.prepareSubmitTemplate()
1234        submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1235
1236        if self.preserveUser:
1237           submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1238
1239        if self.checkAuthorship and not self.p4UserIsMe(p4User):
1240            submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1241            submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1242            submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1243
1244        separatorLine = "######## everything below this line is just the diff #######\n"
1245
1246        # diff
1247        if os.environ.has_key("P4DIFF"):
1248            del(os.environ["P4DIFF"])
1249        diff = ""
1250        for editedFile in editedFiles:
1251            diff += p4_read_pipe(['diff', '-du',
1252                                  wildcard_encode(editedFile)])
1253
1254        # new file diff
1255        newdiff = ""
1256        for newFile in filesToAdd:
1257            newdiff += "==== new file ====\n"
1258            newdiff += "--- /dev/null\n"
1259            newdiff += "+++ %s\n" % newFile
1260            f = open(newFile, "r")
1261            for line in f.readlines():
1262                newdiff += "+" + line
1263            f.close()
1264
1265        # change description file: submitTemplate, separatorLine, diff, newdiff
1266        (handle, fileName) = tempfile.mkstemp()
1267        tmpFile = os.fdopen(handle, "w+")
1268        if self.isWindows:
1269            submitTemplate = submitTemplate.replace("\n", "\r\n")
1270            separatorLine = separatorLine.replace("\n", "\r\n")
1271            newdiff = newdiff.replace("\n", "\r\n")
1272        tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
1273        tmpFile.close()
1274
1275        if self.prepare_p4_only:
1276            #
1277            # Leave the p4 tree prepared, and the submit template around
1278            # and let the user decide what to do next
1279            #
1280            print
1281            print "P4 workspace prepared for submission."
1282            print "To submit or revert, go to client workspace"
1283            print "  " + self.clientPath
1284            print
1285            print "To submit, use \"p4 submit\" to write a new description,"
1286            print "or \"p4 submit -i %s\" to use the one prepared by" \
1287                  " \"git p4\"." % fileName
1288            print "You can delete the file \"%s\" when finished." % fileName
1289
1290            if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1291                print "To preserve change ownership by user %s, you must\n" \
1292                      "do \"p4 change -f <change>\" after submitting and\n" \
1293                      "edit the User field."
1294            if pureRenameCopy:
1295                print "After submitting, renamed files must be re-synced."
1296                print "Invoke \"p4 sync -f\" on each of these files:"
1297                for f in pureRenameCopy:
1298                    print "  " + f
1299
1300            print
1301            print "To revert the changes, use \"p4 revert ...\", and delete"
1302            print "the submit template file \"%s\"" % fileName
1303            if filesToAdd:
1304                print "Since the commit adds new files, they must be deleted:"
1305                for f in filesToAdd:
1306                    print "  " + f
1307            print
1308            return True
1309
1310        #
1311        # Let the user edit the change description, then submit it.
1312        #
1313        if self.edit_template(fileName):
1314            # read the edited message and submit
1315            ret = True
1316            tmpFile = open(fileName, "rb")
1317            message = tmpFile.read()
1318            tmpFile.close()
1319            submitTemplate = message[:message.index(separatorLine)]
1320            if self.isWindows:
1321                submitTemplate = submitTemplate.replace("\r\n", "\n")
1322            p4_write_pipe(['submit', '-i'], submitTemplate)
1323
1324            if self.preserveUser:
1325                if p4User:
1326                    # Get last changelist number. Cannot easily get it from
1327                    # the submit command output as the output is
1328                    # unmarshalled.
1329                    changelist = self.lastP4Changelist()
1330                    self.modifyChangelistUser(changelist, p4User)
1331
1332            # The rename/copy happened by applying a patch that created a
1333            # new file.  This leaves it writable, which confuses p4.
1334            for f in pureRenameCopy:
1335                p4_sync(f, "-f")
1336
1337        else:
1338            # skip this patch
1339            ret = False
1340            print "Submission cancelled, undoing p4 changes."
1341            for f in editedFiles:
1342                p4_revert(f)
1343            for f in filesToAdd:
1344                p4_revert(f)
1345                os.remove(f)
1346            for f in filesToDelete:
1347                p4_revert(f)
1348
1349        os.remove(fileName)
1350        return ret
1351
1352    # Export git tags as p4 labels. Create a p4 label and then tag
1353    # with that.
1354    def exportGitTags(self, gitTags):
1355        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1356        if len(validLabelRegexp) == 0:
1357            validLabelRegexp = defaultLabelRegexp
1358        m = re.compile(validLabelRegexp)
1359
1360        for name in gitTags:
1361
1362            if not m.match(name):
1363                if verbose:
1364                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1365                continue
1366
1367            # Get the p4 commit this corresponds to
1368            logMessage = extractLogMessageFromGitCommit(name)
1369            values = extractSettingsGitLog(logMessage)
1370
1371            if not values.has_key('change'):
1372                # a tag pointing to something not sent to p4; ignore
1373                if verbose:
1374                    print "git tag %s does not give a p4 commit" % name
1375                continue
1376            else:
1377                changelist = values['change']
1378
1379            # Get the tag details.
1380            inHeader = True
1381            isAnnotated = False
1382            body = []
1383            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1384                l = l.strip()
1385                if inHeader:
1386                    if re.match(r'tag\s+', l):
1387                        isAnnotated = True
1388                    elif re.match(r'\s*$', l):
1389                        inHeader = False
1390                        continue
1391                else:
1392                    body.append(l)
1393
1394            if not isAnnotated:
1395                body = ["lightweight tag imported by git p4\n"]
1396
1397            # Create the label - use the same view as the client spec we are using
1398            clientSpec = getClientSpec()
1399
1400            labelTemplate  = "Label: %s\n" % name
1401            labelTemplate += "Description:\n"
1402            for b in body:
1403                labelTemplate += "\t" + b + "\n"
1404            labelTemplate += "View:\n"
1405            for mapping in clientSpec.mappings:
1406                labelTemplate += "\t%s\n" % mapping.depot_side.path
1407
1408            if self.dry_run:
1409                print "Would create p4 label %s for tag" % name
1410            elif self.prepare_p4_only:
1411                print "Not creating p4 label %s for tag due to option" \
1412                      " --prepare-p4-only" % name
1413            else:
1414                p4_write_pipe(["label", "-i"], labelTemplate)
1415
1416                # Use the label
1417                p4_system(["tag", "-l", name] +
1418                          ["%s@%s" % (mapping.depot_side.path, changelist) for mapping in clientSpec.mappings])
1419
1420                if verbose:
1421                    print "created p4 label for tag %s" % name
1422
1423    def run(self, args):
1424        if len(args) == 0:
1425            self.master = currentGitBranch()
1426            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1427                die("Detecting current git branch failed!")
1428        elif len(args) == 1:
1429            self.master = args[0]
1430            if not branchExists(self.master):
1431                die("Branch %s does not exist" % self.master)
1432        else:
1433            return False
1434
1435        allowSubmit = gitConfig("git-p4.allowSubmit")
1436        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1437            die("%s is not in git-p4.allowSubmit" % self.master)
1438
1439        [upstream, settings] = findUpstreamBranchPoint()
1440        self.depotPath = settings['depot-paths'][0]
1441        if len(self.origin) == 0:
1442            self.origin = upstream
1443
1444        if self.preserveUser:
1445            if not self.canChangeChangelists():
1446                die("Cannot preserve user names without p4 super-user or admin permissions")
1447
1448        if self.verbose:
1449            print "Origin branch is " + self.origin
1450
1451        if len(self.depotPath) == 0:
1452            print "Internal error: cannot locate perforce depot path from existing branches"
1453            sys.exit(128)
1454
1455        self.useClientSpec = False
1456        if gitConfig("git-p4.useclientspec", "--bool") == "true":
1457            self.useClientSpec = True
1458        if self.useClientSpec:
1459            self.clientSpecDirs = getClientSpec()
1460
1461        if self.useClientSpec:
1462            # all files are relative to the client spec
1463            self.clientPath = getClientRoot()
1464        else:
1465            self.clientPath = p4Where(self.depotPath)
1466
1467        if self.clientPath == "":
1468            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1469
1470        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1471        self.oldWorkingDirectory = os.getcwd()
1472
1473        # ensure the clientPath exists
1474        new_client_dir = False
1475        if not os.path.exists(self.clientPath):
1476            new_client_dir = True
1477            os.makedirs(self.clientPath)
1478
1479        chdir(self.clientPath)
1480        if self.dry_run:
1481            print "Would synchronize p4 checkout in %s" % self.clientPath
1482        else:
1483            print "Synchronizing p4 checkout..."
1484            if new_client_dir:
1485                # old one was destroyed, and maybe nobody told p4
1486                p4_sync("...", "-f")
1487            else:
1488                p4_sync("...")
1489        self.check()
1490
1491        commits = []
1492        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1493            commits.append(line.strip())
1494        commits.reverse()
1495
1496        if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1497            self.checkAuthorship = False
1498        else:
1499            self.checkAuthorship = True
1500
1501        if self.preserveUser:
1502            self.checkValidP4Users(commits)
1503
1504        #
1505        # Build up a set of options to be passed to diff when
1506        # submitting each commit to p4.
1507        #
1508        if self.detectRenames:
1509            # command-line -M arg
1510            self.diffOpts = "-M"
1511        else:
1512            # If not explicitly set check the config variable
1513            detectRenames = gitConfig("git-p4.detectRenames")
1514
1515            if detectRenames.lower() == "false" or detectRenames == "":
1516                self.diffOpts = ""
1517            elif detectRenames.lower() == "true":
1518                self.diffOpts = "-M"
1519            else:
1520                self.diffOpts = "-M%s" % detectRenames
1521
1522        # no command-line arg for -C or --find-copies-harder, just
1523        # config variables
1524        detectCopies = gitConfig("git-p4.detectCopies")
1525        if detectCopies.lower() == "false" or detectCopies == "":
1526            pass
1527        elif detectCopies.lower() == "true":
1528            self.diffOpts += " -C"
1529        else:
1530            self.diffOpts += " -C%s" % detectCopies
1531
1532        if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
1533            self.diffOpts += " --find-copies-harder"
1534
1535        #
1536        # Apply the commits, one at a time.  On failure, ask if should
1537        # continue to try the rest of the patches, or quit.
1538        #
1539        if self.dry_run:
1540            print "Would apply"
1541        applied = []
1542        last = len(commits) - 1
1543        for i, commit in enumerate(commits):
1544            if self.dry_run:
1545                print " ", read_pipe(["git", "show", "-s",
1546                                      "--format=format:%h %s", commit])
1547                ok = True
1548            else:
1549                ok = self.applyCommit(commit)
1550            if ok:
1551                applied.append(commit)
1552            else:
1553                if self.prepare_p4_only and i < last:
1554                    print "Processing only the first commit due to option" \
1555                          " --prepare-p4-only"
1556                    break
1557                if i < last:
1558                    quit = False
1559                    while True:
1560                        print "What do you want to do?"
1561                        response = raw_input("[s]kip this commit but apply"
1562                                             " the rest, or [q]uit? ")
1563                        if not response:
1564                            continue
1565                        if response[0] == "s":
1566                            print "Skipping this commit, but applying the rest"
1567                            break
1568                        if response[0] == "q":
1569                            print "Quitting"
1570                            quit = True
1571                            break
1572                    if quit:
1573                        break
1574
1575        chdir(self.oldWorkingDirectory)
1576
1577        if self.dry_run:
1578            pass
1579        elif self.prepare_p4_only:
1580            pass
1581        elif len(commits) == len(applied):
1582            print "All commits applied!"
1583
1584            sync = P4Sync()
1585            sync.run([])
1586
1587            rebase = P4Rebase()
1588            rebase.rebase()
1589
1590        else:
1591            if len(applied) == 0:
1592                print "No commits applied."
1593            else:
1594                print "Applied only the commits marked with '*':"
1595                for c in commits:
1596                    if c in applied:
1597                        star = "*"
1598                    else:
1599                        star = " "
1600                    print star, read_pipe(["git", "show", "-s",
1601                                           "--format=format:%h %s",  c])
1602                print "You will have to do 'git p4 sync' and rebase."
1603
1604        if gitConfig("git-p4.exportLabels", "--bool") == "true":
1605            self.exportLabels = True
1606
1607        if self.exportLabels:
1608            p4Labels = getP4Labels(self.depotPath)
1609            gitTags = getGitTags()
1610
1611            missingGitTags = gitTags - p4Labels
1612            self.exportGitTags(missingGitTags)
1613
1614        # exit with error unless everything applied perfecly
1615        if len(commits) != len(applied):
1616                sys.exit(1)
1617
1618        return True
1619
1620class View(object):
1621    """Represent a p4 view ("p4 help views"), and map files in a
1622       repo according to the view."""
1623
1624    class Path(object):
1625        """A depot or client path, possibly containing wildcards.
1626           The only one supported is ... at the end, currently.
1627           Initialize with the full path, with //depot or //client."""
1628
1629        def __init__(self, path, is_depot):
1630            self.path = path
1631            self.is_depot = is_depot
1632            self.find_wildcards()
1633            # remember the prefix bit, useful for relative mappings
1634            m = re.match("(//[^/]+/)", self.path)
1635            if not m:
1636                die("Path %s does not start with //prefix/" % self.path)
1637            prefix = m.group(1)
1638            if not self.is_depot:
1639                # strip //client/ on client paths
1640                self.path = self.path[len(prefix):]
1641
1642        def find_wildcards(self):
1643            """Make sure wildcards are valid, and set up internal
1644               variables."""
1645
1646            self.ends_triple_dot = False
1647            # There are three wildcards allowed in p4 views
1648            # (see "p4 help views").  This code knows how to
1649            # handle "..." (only at the end), but cannot deal with
1650            # "%%n" or "*".  Only check the depot_side, as p4 should
1651            # validate that the client_side matches too.
1652            if re.search(r'%%[1-9]', self.path):
1653                die("Can't handle %%n wildcards in view: %s" % self.path)
1654            if self.path.find("*") >= 0:
1655                die("Can't handle * wildcards in view: %s" % self.path)
1656            triple_dot_index = self.path.find("...")
1657            if triple_dot_index >= 0:
1658                if triple_dot_index != len(self.path) - 3:
1659                    die("Can handle only single ... wildcard, at end: %s" %
1660                        self.path)
1661                self.ends_triple_dot = True
1662
1663        def ensure_compatible(self, other_path):
1664            """Make sure the wildcards agree."""
1665            if self.ends_triple_dot != other_path.ends_triple_dot:
1666                 die("Both paths must end with ... if either does;\n" +
1667                     "paths: %s %s" % (self.path, other_path.path))
1668
1669        def match_wildcards(self, test_path):
1670            """See if this test_path matches us, and fill in the value
1671               of the wildcards if so.  Returns a tuple of
1672               (True|False, wildcards[]).  For now, only the ... at end
1673               is supported, so at most one wildcard."""
1674            if self.ends_triple_dot:
1675                dotless = self.path[:-3]
1676                if test_path.startswith(dotless):
1677                    wildcard = test_path[len(dotless):]
1678                    return (True, [ wildcard ])
1679            else:
1680                if test_path == self.path:
1681                    return (True, [])
1682            return (False, [])
1683
1684        def match(self, test_path):
1685            """Just return if it matches; don't bother with the wildcards."""
1686            b, _ = self.match_wildcards(test_path)
1687            return b
1688
1689        def fill_in_wildcards(self, wildcards):
1690            """Return the relative path, with the wildcards filled in
1691               if there are any."""
1692            if self.ends_triple_dot:
1693                return self.path[:-3] + wildcards[0]
1694            else:
1695                return self.path
1696
1697    class Mapping(object):
1698        def __init__(self, depot_side, client_side, overlay, exclude):
1699            # depot_side is without the trailing /... if it had one
1700            self.depot_side = View.Path(depot_side, is_depot=True)
1701            self.client_side = View.Path(client_side, is_depot=False)
1702            self.overlay = overlay  # started with "+"
1703            self.exclude = exclude  # started with "-"
1704            assert not (self.overlay and self.exclude)
1705            self.depot_side.ensure_compatible(self.client_side)
1706
1707        def __str__(self):
1708            c = " "
1709            if self.overlay:
1710                c = "+"
1711            if self.exclude:
1712                c = "-"
1713            return "View.Mapping: %s%s -> %s" % \
1714                   (c, self.depot_side.path, self.client_side.path)
1715
1716        def map_depot_to_client(self, depot_path):
1717            """Calculate the client path if using this mapping on the
1718               given depot path; does not consider the effect of other
1719               mappings in a view.  Even excluded mappings are returned."""
1720            matches, wildcards = self.depot_side.match_wildcards(depot_path)
1721            if not matches:
1722                return ""
1723            client_path = self.client_side.fill_in_wildcards(wildcards)
1724            return client_path
1725
1726    #
1727    # View methods
1728    #
1729    def __init__(self):
1730        self.mappings = []
1731
1732    def append(self, view_line):
1733        """Parse a view line, splitting it into depot and client
1734           sides.  Append to self.mappings, preserving order."""
1735
1736        # Split the view line into exactly two words.  P4 enforces
1737        # structure on these lines that simplifies this quite a bit.
1738        #
1739        # Either or both words may be double-quoted.
1740        # Single quotes do not matter.
1741        # Double-quote marks cannot occur inside the words.
1742        # A + or - prefix is also inside the quotes.
1743        # There are no quotes unless they contain a space.
1744        # The line is already white-space stripped.
1745        # The two words are separated by a single space.
1746        #
1747        if view_line[0] == '"':
1748            # First word is double quoted.  Find its end.
1749            close_quote_index = view_line.find('"', 1)
1750            if close_quote_index <= 0:
1751                die("No first-word closing quote found: %s" % view_line)
1752            depot_side = view_line[1:close_quote_index]
1753            # skip closing quote and space
1754            rhs_index = close_quote_index + 1 + 1
1755        else:
1756            space_index = view_line.find(" ")
1757            if space_index <= 0:
1758                die("No word-splitting space found: %s" % view_line)
1759            depot_side = view_line[0:space_index]
1760            rhs_index = space_index + 1
1761
1762        if view_line[rhs_index] == '"':
1763            # Second word is double quoted.  Make sure there is a
1764            # double quote at the end too.
1765            if not view_line.endswith('"'):
1766                die("View line with rhs quote should end with one: %s" %
1767                    view_line)
1768            # skip the quotes
1769            client_side = view_line[rhs_index+1:-1]
1770        else:
1771            client_side = view_line[rhs_index:]
1772
1773        # prefix + means overlay on previous mapping
1774        overlay = False
1775        if depot_side.startswith("+"):
1776            overlay = True
1777            depot_side = depot_side[1:]
1778
1779        # prefix - means exclude this path
1780        exclude = False
1781        if depot_side.startswith("-"):
1782            exclude = True
1783            depot_side = depot_side[1:]
1784
1785        m = View.Mapping(depot_side, client_side, overlay, exclude)
1786        self.mappings.append(m)
1787
1788    def map_in_client(self, depot_path):
1789        """Return the relative location in the client where this
1790           depot file should live.  Returns "" if the file should
1791           not be mapped in the client."""
1792
1793        paths_filled = []
1794        client_path = ""
1795
1796        # look at later entries first
1797        for m in self.mappings[::-1]:
1798
1799            # see where will this path end up in the client
1800            p = m.map_depot_to_client(depot_path)
1801
1802            if p == "":
1803                # Depot path does not belong in client.  Must remember
1804                # this, as previous items should not cause files to
1805                # exist in this path either.  Remember that the list is
1806                # being walked from the end, which has higher precedence.
1807                # Overlap mappings do not exclude previous mappings.
1808                if not m.overlay:
1809                    paths_filled.append(m.client_side)
1810
1811            else:
1812                # This mapping matched; no need to search any further.
1813                # But, the mapping could be rejected if the client path
1814                # has already been claimed by an earlier mapping (i.e.
1815                # one later in the list, which we are walking backwards).
1816                already_mapped_in_client = False
1817                for f in paths_filled:
1818                    # this is View.Path.match
1819                    if f.match(p):
1820                        already_mapped_in_client = True
1821                        break
1822                if not already_mapped_in_client:
1823                    # Include this file, unless it is from a line that
1824                    # explicitly said to exclude it.
1825                    if not m.exclude:
1826                        client_path = p
1827
1828                # a match, even if rejected, always stops the search
1829                break
1830
1831        return client_path
1832
1833class P4Sync(Command, P4UserMap):
1834    delete_actions = ( "delete", "move/delete", "purge" )
1835
1836    def __init__(self):
1837        Command.__init__(self)
1838        P4UserMap.__init__(self)
1839        self.options = [
1840                optparse.make_option("--branch", dest="branch"),
1841                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1842                optparse.make_option("--changesfile", dest="changesFile"),
1843                optparse.make_option("--silent", dest="silent", action="store_true"),
1844                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1845                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1846                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1847                                     help="Import into refs/heads/ , not refs/remotes"),
1848                optparse.make_option("--max-changes", dest="maxChanges"),
1849                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1850                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1851                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1852                                     help="Only sync files that are included in the Perforce Client Spec")
1853        ]
1854        self.description = """Imports from Perforce into a git repository.\n
1855    example:
1856    //depot/my/project/ -- to import the current head
1857    //depot/my/project/@all -- to import everything
1858    //depot/my/project/@1,6 -- to import only from revision 1 to 6
1859
1860    (a ... is not needed in the path p4 specification, it's added implicitly)"""
1861
1862        self.usage += " //depot/path[@revRange]"
1863        self.silent = False
1864        self.createdBranches = set()
1865        self.committedChanges = set()
1866        self.branch = ""
1867        self.detectBranches = False
1868        self.detectLabels = False
1869        self.importLabels = False
1870        self.changesFile = ""
1871        self.syncWithOrigin = True
1872        self.importIntoRemotes = True
1873        self.maxChanges = ""
1874        self.isWindows = (platform.system() == "Windows")
1875        self.keepRepoPath = False
1876        self.depotPaths = None
1877        self.p4BranchesInGit = []
1878        self.cloneExclude = []
1879        self.useClientSpec = False
1880        self.useClientSpec_from_options = False
1881        self.clientSpecDirs = None
1882        self.tempBranches = []
1883        self.tempBranchLocation = "git-p4-tmp"
1884
1885        if gitConfig("git-p4.syncFromOrigin") == "false":
1886            self.syncWithOrigin = False
1887
1888    # Force a checkpoint in fast-import and wait for it to finish
1889    def checkpoint(self):
1890        self.gitStream.write("checkpoint\n\n")
1891        self.gitStream.write("progress checkpoint\n\n")
1892        out = self.gitOutput.readline()
1893        if self.verbose:
1894            print "checkpoint finished: " + out
1895
1896    def extractFilesFromCommit(self, commit):
1897        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1898                             for path in self.cloneExclude]
1899        files = []
1900        fnum = 0
1901        while commit.has_key("depotFile%s" % fnum):
1902            path =  commit["depotFile%s" % fnum]
1903
1904            if [p for p in self.cloneExclude
1905                if p4PathStartsWith(path, p)]:
1906                found = False
1907            else:
1908                found = [p for p in self.depotPaths
1909                         if p4PathStartsWith(path, p)]
1910            if not found:
1911                fnum = fnum + 1
1912                continue
1913
1914            file = {}
1915            file["path"] = path
1916            file["rev"] = commit["rev%s" % fnum]
1917            file["action"] = commit["action%s" % fnum]
1918            file["type"] = commit["type%s" % fnum]
1919            files.append(file)
1920            fnum = fnum + 1
1921        return files
1922
1923    def stripRepoPath(self, path, prefixes):
1924        if self.useClientSpec:
1925            return self.clientSpecDirs.map_in_client(path)
1926
1927        if self.keepRepoPath:
1928            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1929
1930        for p in prefixes:
1931            if p4PathStartsWith(path, p):
1932                path = path[len(p):]
1933
1934        return path
1935
1936    def splitFilesIntoBranches(self, commit):
1937        branches = {}
1938        fnum = 0
1939        while commit.has_key("depotFile%s" % fnum):
1940            path =  commit["depotFile%s" % fnum]
1941            found = [p for p in self.depotPaths
1942                     if p4PathStartsWith(path, p)]
1943            if not found:
1944                fnum = fnum + 1
1945                continue
1946
1947            file = {}
1948            file["path"] = path
1949            file["rev"] = commit["rev%s" % fnum]
1950            file["action"] = commit["action%s" % fnum]
1951            file["type"] = commit["type%s" % fnum]
1952            fnum = fnum + 1
1953
1954            relPath = self.stripRepoPath(path, self.depotPaths)
1955            relPath = wildcard_decode(relPath)
1956
1957            for branch in self.knownBranches.keys():
1958
1959                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1960                if relPath.startswith(branch + "/"):
1961                    if branch not in branches:
1962                        branches[branch] = []
1963                    branches[branch].append(file)
1964                    break
1965
1966        return branches
1967
1968    # output one file from the P4 stream
1969    # - helper for streamP4Files
1970
1971    def streamOneP4File(self, file, contents):
1972        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1973        relPath = wildcard_decode(relPath)
1974        if verbose:
1975            sys.stderr.write("%s\n" % relPath)
1976
1977        (type_base, type_mods) = split_p4_type(file["type"])
1978
1979        git_mode = "100644"
1980        if "x" in type_mods:
1981            git_mode = "100755"
1982        if type_base == "symlink":
1983            git_mode = "120000"
1984            # p4 print on a symlink contains "target\n"; remove the newline
1985            data = ''.join(contents)
1986            contents = [data[:-1]]
1987
1988        if type_base == "utf16":
1989            # p4 delivers different text in the python output to -G
1990            # than it does when using "print -o", or normal p4 client
1991            # operations.  utf16 is converted to ascii or utf8, perhaps.
1992            # But ascii text saved as -t utf16 is completely mangled.
1993            # Invoke print -o to get the real contents.
1994            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
1995            contents = [ text ]
1996
1997        if type_base == "apple":
1998            # Apple filetype files will be streamed as a concatenation of
1999            # its appledouble header and the contents.  This is useless
2000            # on both macs and non-macs.  If using "print -q -o xx", it
2001            # will create "xx" with the data, and "%xx" with the header.
2002            # This is also not very useful.
2003            #
2004            # Ideally, someday, this script can learn how to generate
2005            # appledouble files directly and import those to git, but
2006            # non-mac machines can never find a use for apple filetype.
2007            print "\nIgnoring apple filetype file %s" % file['depotFile']
2008            return
2009
2010        # Perhaps windows wants unicode, utf16 newlines translated too;
2011        # but this is not doing it.
2012        if self.isWindows and type_base == "text":
2013            mangled = []
2014            for data in contents:
2015                data = data.replace("\r\n", "\n")
2016                mangled.append(data)
2017            contents = mangled
2018
2019        # Note that we do not try to de-mangle keywords on utf16 files,
2020        # even though in theory somebody may want that.
2021        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2022        if pattern:
2023            regexp = re.compile(pattern, re.VERBOSE)
2024            text = ''.join(contents)
2025            text = regexp.sub(r'$\1$', text)
2026            contents = [ text ]
2027
2028        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2029
2030        # total length...
2031        length = 0
2032        for d in contents:
2033            length = length + len(d)
2034
2035        self.gitStream.write("data %d\n" % length)
2036        for d in contents:
2037            self.gitStream.write(d)
2038        self.gitStream.write("\n")
2039
2040    def streamOneP4Deletion(self, file):
2041        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2042        relPath = wildcard_decode(relPath)
2043        if verbose:
2044            sys.stderr.write("delete %s\n" % relPath)
2045        self.gitStream.write("D %s\n" % relPath)
2046
2047    # handle another chunk of streaming data
2048    def streamP4FilesCb(self, marshalled):
2049
2050        if marshalled.has_key('depotFile') and self.stream_have_file_info:
2051            # start of a new file - output the old one first
2052            self.streamOneP4File(self.stream_file, self.stream_contents)
2053            self.stream_file = {}
2054            self.stream_contents = []
2055            self.stream_have_file_info = False
2056
2057        # pick up the new file information... for the
2058        # 'data' field we need to append to our array
2059        for k in marshalled.keys():
2060            if k == 'data':
2061                self.stream_contents.append(marshalled['data'])
2062            else:
2063                self.stream_file[k] = marshalled[k]
2064
2065        self.stream_have_file_info = True
2066
2067    # Stream directly from "p4 files" into "git fast-import"
2068    def streamP4Files(self, files):
2069        filesForCommit = []
2070        filesToRead = []
2071        filesToDelete = []
2072
2073        for f in files:
2074            # if using a client spec, only add the files that have
2075            # a path in the client
2076            if self.clientSpecDirs:
2077                if self.clientSpecDirs.map_in_client(f['path']) == "":
2078                    continue
2079
2080            filesForCommit.append(f)
2081            if f['action'] in self.delete_actions:
2082                filesToDelete.append(f)
2083            else:
2084                filesToRead.append(f)
2085
2086        # deleted files...
2087        for f in filesToDelete:
2088            self.streamOneP4Deletion(f)
2089
2090        if len(filesToRead) > 0:
2091            self.stream_file = {}
2092            self.stream_contents = []
2093            self.stream_have_file_info = False
2094
2095            # curry self argument
2096            def streamP4FilesCbSelf(entry):
2097                self.streamP4FilesCb(entry)
2098
2099            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2100
2101            p4CmdList(["-x", "-", "print"],
2102                      stdin=fileArgs,
2103                      cb=streamP4FilesCbSelf)
2104
2105            # do the last chunk
2106            if self.stream_file.has_key('depotFile'):
2107                self.streamOneP4File(self.stream_file, self.stream_contents)
2108
2109    def make_email(self, userid):
2110        if userid in self.users:
2111            return self.users[userid]
2112        else:
2113            return "%s <a@b>" % userid
2114
2115    # Stream a p4 tag
2116    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2117        if verbose:
2118            print "writing tag %s for commit %s" % (labelName, commit)
2119        gitStream.write("tag %s\n" % labelName)
2120        gitStream.write("from %s\n" % commit)
2121
2122        if labelDetails.has_key('Owner'):
2123            owner = labelDetails["Owner"]
2124        else:
2125            owner = None
2126
2127        # Try to use the owner of the p4 label, or failing that,
2128        # the current p4 user id.
2129        if owner:
2130            email = self.make_email(owner)
2131        else:
2132            email = self.make_email(self.p4UserId())
2133        tagger = "%s %s %s" % (email, epoch, self.tz)
2134
2135        gitStream.write("tagger %s\n" % tagger)
2136
2137        print "labelDetails=",labelDetails
2138        if labelDetails.has_key('Description'):
2139            description = labelDetails['Description']
2140        else:
2141            description = 'Label from git p4'
2142
2143        gitStream.write("data %d\n" % len(description))
2144        gitStream.write(description)
2145        gitStream.write("\n")
2146
2147    def commit(self, details, files, branch, branchPrefixes, parent = ""):
2148        epoch = details["time"]
2149        author = details["user"]
2150        self.branchPrefixes = branchPrefixes
2151
2152        if self.verbose:
2153            print "commit into %s" % branch
2154
2155        # start with reading files; if that fails, we should not
2156        # create a commit.
2157        new_files = []
2158        for f in files:
2159            if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
2160                new_files.append (f)
2161            else:
2162                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2163
2164        self.gitStream.write("commit %s\n" % branch)
2165#        gitStream.write("mark :%s\n" % details["change"])
2166        self.committedChanges.add(int(details["change"]))
2167        committer = ""
2168        if author not in self.users:
2169            self.getUserMapFromPerforceServer()
2170        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2171
2172        self.gitStream.write("committer %s\n" % committer)
2173
2174        self.gitStream.write("data <<EOT\n")
2175        self.gitStream.write(details["desc"])
2176        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
2177                             % (','.join (branchPrefixes), details["change"]))
2178        if len(details['options']) > 0:
2179            self.gitStream.write(": options = %s" % details['options'])
2180        self.gitStream.write("]\nEOT\n\n")
2181
2182        if len(parent) > 0:
2183            if self.verbose:
2184                print "parent %s" % parent
2185            self.gitStream.write("from %s\n" % parent)
2186
2187        self.streamP4Files(new_files)
2188        self.gitStream.write("\n")
2189
2190        change = int(details["change"])
2191
2192        if self.labels.has_key(change):
2193            label = self.labels[change]
2194            labelDetails = label[0]
2195            labelRevisions = label[1]
2196            if self.verbose:
2197                print "Change %s is labelled %s" % (change, labelDetails)
2198
2199            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2200                                                    for p in branchPrefixes])
2201
2202            if len(files) == len(labelRevisions):
2203
2204                cleanedFiles = {}
2205                for info in files:
2206                    if info["action"] in self.delete_actions:
2207                        continue
2208                    cleanedFiles[info["depotFile"]] = info["rev"]
2209
2210                if cleanedFiles == labelRevisions:
2211                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2212
2213                else:
2214                    if not self.silent:
2215                        print ("Tag %s does not match with change %s: files do not match."
2216                               % (labelDetails["label"], change))
2217
2218            else:
2219                if not self.silent:
2220                    print ("Tag %s does not match with change %s: file count is different."
2221                           % (labelDetails["label"], change))
2222
2223    # Build a dictionary of changelists and labels, for "detect-labels" option.
2224    def getLabels(self):
2225        self.labels = {}
2226
2227        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2228        if len(l) > 0 and not self.silent:
2229            print "Finding files belonging to labels in %s" % `self.depotPaths`
2230
2231        for output in l:
2232            label = output["label"]
2233            revisions = {}
2234            newestChange = 0
2235            if self.verbose:
2236                print "Querying files for label %s" % label
2237            for file in p4CmdList(["files"] +
2238                                      ["%s...@%s" % (p, label)
2239                                          for p in self.depotPaths]):
2240                revisions[file["depotFile"]] = file["rev"]
2241                change = int(file["change"])
2242                if change > newestChange:
2243                    newestChange = change
2244
2245            self.labels[newestChange] = [output, revisions]
2246
2247        if self.verbose:
2248            print "Label changes: %s" % self.labels.keys()
2249
2250    # Import p4 labels as git tags. A direct mapping does not
2251    # exist, so assume that if all the files are at the same revision
2252    # then we can use that, or it's something more complicated we should
2253    # just ignore.
2254    def importP4Labels(self, stream, p4Labels):
2255        if verbose:
2256            print "import p4 labels: " + ' '.join(p4Labels)
2257
2258        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2259        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2260        if len(validLabelRegexp) == 0:
2261            validLabelRegexp = defaultLabelRegexp
2262        m = re.compile(validLabelRegexp)
2263
2264        for name in p4Labels:
2265            commitFound = False
2266
2267            if not m.match(name):
2268                if verbose:
2269                    print "label %s does not match regexp %s" % (name,validLabelRegexp)
2270                continue
2271
2272            if name in ignoredP4Labels:
2273                continue
2274
2275            labelDetails = p4CmdList(['label', "-o", name])[0]
2276
2277            # get the most recent changelist for each file in this label
2278            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2279                                for p in self.depotPaths])
2280
2281            if change.has_key('change'):
2282                # find the corresponding git commit; take the oldest commit
2283                changelist = int(change['change'])
2284                gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2285                     "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2286                if len(gitCommit) == 0:
2287                    print "could not find git commit for changelist %d" % changelist
2288                else:
2289                    gitCommit = gitCommit.strip()
2290                    commitFound = True
2291                    # Convert from p4 time format
2292                    try:
2293                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2294                    except ValueError:
2295                        print "Could not convert label time %s" % labelDetail['Update']
2296                        tmwhen = 1
2297
2298                    when = int(time.mktime(tmwhen))
2299                    self.streamTag(stream, name, labelDetails, gitCommit, when)
2300                    if verbose:
2301                        print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2302            else:
2303                if verbose:
2304                    print "Label %s has no changelists - possibly deleted?" % name
2305
2306            if not commitFound:
2307                # We can't import this label; don't try again as it will get very
2308                # expensive repeatedly fetching all the files for labels that will
2309                # never be imported. If the label is moved in the future, the
2310                # ignore will need to be removed manually.
2311                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2312
2313    def guessProjectName(self):
2314        for p in self.depotPaths:
2315            if p.endswith("/"):
2316                p = p[:-1]
2317            p = p[p.strip().rfind("/") + 1:]
2318            if not p.endswith("/"):
2319               p += "/"
2320            return p
2321
2322    def getBranchMapping(self):
2323        lostAndFoundBranches = set()
2324
2325        user = gitConfig("git-p4.branchUser")
2326        if len(user) > 0:
2327            command = "branches -u %s" % user
2328        else:
2329            command = "branches"
2330
2331        for info in p4CmdList(command):
2332            details = p4Cmd(["branch", "-o", info["branch"]])
2333            viewIdx = 0
2334            while details.has_key("View%s" % viewIdx):
2335                paths = details["View%s" % viewIdx].split(" ")
2336                viewIdx = viewIdx + 1
2337                # require standard //depot/foo/... //depot/bar/... mapping
2338                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2339                    continue
2340                source = paths[0]
2341                destination = paths[1]
2342                ## HACK
2343                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2344                    source = source[len(self.depotPaths[0]):-4]
2345                    destination = destination[len(self.depotPaths[0]):-4]
2346
2347                    if destination in self.knownBranches:
2348                        if not self.silent:
2349                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2350                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2351                        continue
2352
2353                    self.knownBranches[destination] = source
2354
2355                    lostAndFoundBranches.discard(destination)
2356
2357                    if source not in self.knownBranches:
2358                        lostAndFoundBranches.add(source)
2359
2360        # Perforce does not strictly require branches to be defined, so we also
2361        # check git config for a branch list.
2362        #
2363        # Example of branch definition in git config file:
2364        # [git-p4]
2365        #   branchList=main:branchA
2366        #   branchList=main:branchB
2367        #   branchList=branchA:branchC
2368        configBranches = gitConfigList("git-p4.branchList")
2369        for branch in configBranches:
2370            if branch:
2371                (source, destination) = branch.split(":")
2372                self.knownBranches[destination] = source
2373
2374                lostAndFoundBranches.discard(destination)
2375
2376                if source not in self.knownBranches:
2377                    lostAndFoundBranches.add(source)
2378
2379
2380        for branch in lostAndFoundBranches:
2381            self.knownBranches[branch] = branch
2382
2383    def getBranchMappingFromGitBranches(self):
2384        branches = p4BranchesInGit(self.importIntoRemotes)
2385        for branch in branches.keys():
2386            if branch == "master":
2387                branch = "main"
2388            else:
2389                branch = branch[len(self.projectName):]
2390            self.knownBranches[branch] = branch
2391
2392    def listExistingP4GitBranches(self):
2393        # branches holds mapping from name to commit
2394        branches = p4BranchesInGit(self.importIntoRemotes)
2395        self.p4BranchesInGit = branches.keys()
2396        for branch in branches.keys():
2397            self.initialParents[self.refPrefix + branch] = branches[branch]
2398
2399    def updateOptionDict(self, d):
2400        option_keys = {}
2401        if self.keepRepoPath:
2402            option_keys['keepRepoPath'] = 1
2403
2404        d["options"] = ' '.join(sorted(option_keys.keys()))
2405
2406    def readOptions(self, d):
2407        self.keepRepoPath = (d.has_key('options')
2408                             and ('keepRepoPath' in d['options']))
2409
2410    def gitRefForBranch(self, branch):
2411        if branch == "main":
2412            return self.refPrefix + "master"
2413
2414        if len(branch) <= 0:
2415            return branch
2416
2417        return self.refPrefix + self.projectName + branch
2418
2419    def gitCommitByP4Change(self, ref, change):
2420        if self.verbose:
2421            print "looking in ref " + ref + " for change %s using bisect..." % change
2422
2423        earliestCommit = ""
2424        latestCommit = parseRevision(ref)
2425
2426        while True:
2427            if self.verbose:
2428                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2429            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2430            if len(next) == 0:
2431                if self.verbose:
2432                    print "argh"
2433                return ""
2434            log = extractLogMessageFromGitCommit(next)
2435            settings = extractSettingsGitLog(log)
2436            currentChange = int(settings['change'])
2437            if self.verbose:
2438                print "current change %s" % currentChange
2439
2440            if currentChange == change:
2441                if self.verbose:
2442                    print "found %s" % next
2443                return next
2444
2445            if currentChange < change:
2446                earliestCommit = "^%s" % next
2447            else:
2448                latestCommit = "%s" % next
2449
2450        return ""
2451
2452    def importNewBranch(self, branch, maxChange):
2453        # make fast-import flush all changes to disk and update the refs using the checkpoint
2454        # command so that we can try to find the branch parent in the git history
2455        self.gitStream.write("checkpoint\n\n");
2456        self.gitStream.flush();
2457        branchPrefix = self.depotPaths[0] + branch + "/"
2458        range = "@1,%s" % maxChange
2459        #print "prefix" + branchPrefix
2460        changes = p4ChangesForPaths([branchPrefix], range)
2461        if len(changes) <= 0:
2462            return False
2463        firstChange = changes[0]
2464        #print "first change in branch: %s" % firstChange
2465        sourceBranch = self.knownBranches[branch]
2466        sourceDepotPath = self.depotPaths[0] + sourceBranch
2467        sourceRef = self.gitRefForBranch(sourceBranch)
2468        #print "source " + sourceBranch
2469
2470        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2471        #print "branch parent: %s" % branchParentChange
2472        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2473        if len(gitParent) > 0:
2474            self.initialParents[self.gitRefForBranch(branch)] = gitParent
2475            #print "parent git commit: %s" % gitParent
2476
2477        self.importChanges(changes)
2478        return True
2479
2480    def searchParent(self, parent, branch, target):
2481        parentFound = False
2482        for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]):
2483            blob = blob.strip()
2484            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2485                parentFound = True
2486                if self.verbose:
2487                    print "Found parent of %s in commit %s" % (branch, blob)
2488                break
2489        if parentFound:
2490            return blob
2491        else:
2492            return None
2493
2494    def importChanges(self, changes):
2495        cnt = 1
2496        for change in changes:
2497            description = p4Cmd(["describe", str(change)])
2498            self.updateOptionDict(description)
2499
2500            if not self.silent:
2501                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2502                sys.stdout.flush()
2503            cnt = cnt + 1
2504
2505            try:
2506                if self.detectBranches:
2507                    branches = self.splitFilesIntoBranches(description)
2508                    for branch in branches.keys():
2509                        ## HACK  --hwn
2510                        branchPrefix = self.depotPaths[0] + branch + "/"
2511
2512                        parent = ""
2513
2514                        filesForCommit = branches[branch]
2515
2516                        if self.verbose:
2517                            print "branch is %s" % branch
2518
2519                        self.updatedBranches.add(branch)
2520
2521                        if branch not in self.createdBranches:
2522                            self.createdBranches.add(branch)
2523                            parent = self.knownBranches[branch]
2524                            if parent == branch:
2525                                parent = ""
2526                            else:
2527                                fullBranch = self.projectName + branch
2528                                if fullBranch not in self.p4BranchesInGit:
2529                                    if not self.silent:
2530                                        print("\n    Importing new branch %s" % fullBranch);
2531                                    if self.importNewBranch(branch, change - 1):
2532                                        parent = ""
2533                                        self.p4BranchesInGit.append(fullBranch)
2534                                    if not self.silent:
2535                                        print("\n    Resuming with change %s" % change);
2536
2537                                if self.verbose:
2538                                    print "parent determined through known branches: %s" % parent
2539
2540                        branch = self.gitRefForBranch(branch)
2541                        parent = self.gitRefForBranch(parent)
2542
2543                        if self.verbose:
2544                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2545
2546                        if len(parent) == 0 and branch in self.initialParents:
2547                            parent = self.initialParents[branch]
2548                            del self.initialParents[branch]
2549
2550                        blob = None
2551                        if len(parent) > 0:
2552                            tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change))
2553                            if self.verbose:
2554                                print "Creating temporary branch: " + tempBranch
2555                            self.commit(description, filesForCommit, tempBranch, [branchPrefix])
2556                            self.tempBranches.append(tempBranch)
2557                            self.checkpoint()
2558                            blob = self.searchParent(parent, branch, tempBranch)
2559                        if blob:
2560                            self.commit(description, filesForCommit, branch, [branchPrefix], blob)
2561                        else:
2562                            if self.verbose:
2563                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2564                            self.commit(description, filesForCommit, branch, [branchPrefix], parent)
2565                else:
2566                    files = self.extractFilesFromCommit(description)
2567                    self.commit(description, files, self.branch, self.depotPaths,
2568                                self.initialParent)
2569                    self.initialParent = ""
2570            except IOError:
2571                print self.gitError.read()
2572                sys.exit(1)
2573
2574    def importHeadRevision(self, revision):
2575        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2576
2577        details = {}
2578        details["user"] = "git perforce import user"
2579        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2580                           % (' '.join(self.depotPaths), revision))
2581        details["change"] = revision
2582        newestRevision = 0
2583
2584        fileCnt = 0
2585        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2586
2587        for info in p4CmdList(["files"] + fileArgs):
2588
2589            if 'code' in info and info['code'] == 'error':
2590                sys.stderr.write("p4 returned an error: %s\n"
2591                                 % info['data'])
2592                if info['data'].find("must refer to client") >= 0:
2593                    sys.stderr.write("This particular p4 error is misleading.\n")
2594                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
2595                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2596                sys.exit(1)
2597            if 'p4ExitCode' in info:
2598                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2599                sys.exit(1)
2600
2601
2602            change = int(info["change"])
2603            if change > newestRevision:
2604                newestRevision = change
2605
2606            if info["action"] in self.delete_actions:
2607                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2608                #fileCnt = fileCnt + 1
2609                continue
2610
2611            for prop in ["depotFile", "rev", "action", "type" ]:
2612                details["%s%s" % (prop, fileCnt)] = info[prop]
2613
2614            fileCnt = fileCnt + 1
2615
2616        details["change"] = newestRevision
2617
2618        # Use time from top-most change so that all git p4 clones of
2619        # the same p4 repo have the same commit SHA1s.
2620        res = p4CmdList("describe -s %d" % newestRevision)
2621        newestTime = None
2622        for r in res:
2623            if r.has_key('time'):
2624                newestTime = int(r['time'])
2625        if newestTime is None:
2626            die("\"describe -s\" on newest change %d did not give a time")
2627        details["time"] = newestTime
2628
2629        self.updateOptionDict(details)
2630        try:
2631            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
2632        except IOError:
2633            print "IO error with git fast-import. Is your git version recent enough?"
2634            print self.gitError.read()
2635
2636
2637    def run(self, args):
2638        self.depotPaths = []
2639        self.changeRange = ""
2640        self.initialParent = ""
2641        self.previousDepotPaths = []
2642
2643        # map from branch depot path to parent branch
2644        self.knownBranches = {}
2645        self.initialParents = {}
2646        self.hasOrigin = originP4BranchesExist()
2647        if not self.syncWithOrigin:
2648            self.hasOrigin = False
2649
2650        if self.importIntoRemotes:
2651            self.refPrefix = "refs/remotes/p4/"
2652        else:
2653            self.refPrefix = "refs/heads/p4/"
2654
2655        if self.syncWithOrigin and self.hasOrigin:
2656            if not self.silent:
2657                print "Syncing with origin first by calling git fetch origin"
2658            system("git fetch origin")
2659
2660        if len(self.branch) == 0:
2661            self.branch = self.refPrefix + "master"
2662            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2663                system("git update-ref %s refs/heads/p4" % self.branch)
2664                system("git branch -D p4");
2665            # create it /after/ importing, when master exists
2666            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
2667                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
2668
2669        # accept either the command-line option, or the configuration variable
2670        if self.useClientSpec:
2671            # will use this after clone to set the variable
2672            self.useClientSpec_from_options = True
2673        else:
2674            if gitConfig("git-p4.useclientspec", "--bool") == "true":
2675                self.useClientSpec = True
2676        if self.useClientSpec:
2677            self.clientSpecDirs = getClientSpec()
2678
2679        # TODO: should always look at previous commits,
2680        # merge with previous imports, if possible.
2681        if args == []:
2682            if self.hasOrigin:
2683                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2684            self.listExistingP4GitBranches()
2685
2686            if len(self.p4BranchesInGit) > 1:
2687                if not self.silent:
2688                    print "Importing from/into multiple branches"
2689                self.detectBranches = True
2690
2691            if self.verbose:
2692                print "branches: %s" % self.p4BranchesInGit
2693
2694            p4Change = 0
2695            for branch in self.p4BranchesInGit:
2696                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2697
2698                settings = extractSettingsGitLog(logMsg)
2699
2700                self.readOptions(settings)
2701                if (settings.has_key('depot-paths')
2702                    and settings.has_key ('change')):
2703                    change = int(settings['change']) + 1
2704                    p4Change = max(p4Change, change)
2705
2706                    depotPaths = sorted(settings['depot-paths'])
2707                    if self.previousDepotPaths == []:
2708                        self.previousDepotPaths = depotPaths
2709                    else:
2710                        paths = []
2711                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2712                            prev_list = prev.split("/")
2713                            cur_list = cur.split("/")
2714                            for i in range(0, min(len(cur_list), len(prev_list))):
2715                                if cur_list[i] <> prev_list[i]:
2716                                    i = i - 1
2717                                    break
2718
2719                            paths.append ("/".join(cur_list[:i + 1]))
2720
2721                        self.previousDepotPaths = paths
2722
2723            if p4Change > 0:
2724                self.depotPaths = sorted(self.previousDepotPaths)
2725                self.changeRange = "@%s,#head" % p4Change
2726                if not self.detectBranches:
2727                    self.initialParent = parseRevision(self.branch)
2728                if not self.silent and not self.detectBranches:
2729                    print "Performing incremental import into %s git branch" % self.branch
2730
2731        if not self.branch.startswith("refs/"):
2732            self.branch = "refs/heads/" + self.branch
2733
2734        if len(args) == 0 and self.depotPaths:
2735            if not self.silent:
2736                print "Depot paths: %s" % ' '.join(self.depotPaths)
2737        else:
2738            if self.depotPaths and self.depotPaths != args:
2739                print ("previous import used depot path %s and now %s was specified. "
2740                       "This doesn't work!" % (' '.join (self.depotPaths),
2741                                               ' '.join (args)))
2742                sys.exit(1)
2743
2744            self.depotPaths = sorted(args)
2745
2746        revision = ""
2747        self.users = {}
2748
2749        # Make sure no revision specifiers are used when --changesfile
2750        # is specified.
2751        bad_changesfile = False
2752        if len(self.changesFile) > 0:
2753            for p in self.depotPaths:
2754                if p.find("@") >= 0 or p.find("#") >= 0:
2755                    bad_changesfile = True
2756                    break
2757        if bad_changesfile:
2758            die("Option --changesfile is incompatible with revision specifiers")
2759
2760        newPaths = []
2761        for p in self.depotPaths:
2762            if p.find("@") != -1:
2763                atIdx = p.index("@")
2764                self.changeRange = p[atIdx:]
2765                if self.changeRange == "@all":
2766                    self.changeRange = ""
2767                elif ',' not in self.changeRange:
2768                    revision = self.changeRange
2769                    self.changeRange = ""
2770                p = p[:atIdx]
2771            elif p.find("#") != -1:
2772                hashIdx = p.index("#")
2773                revision = p[hashIdx:]
2774                p = p[:hashIdx]
2775            elif self.previousDepotPaths == []:
2776                # pay attention to changesfile, if given, else import
2777                # the entire p4 tree at the head revision
2778                if len(self.changesFile) == 0:
2779                    revision = "#head"
2780
2781            p = re.sub ("\.\.\.$", "", p)
2782            if not p.endswith("/"):
2783                p += "/"
2784
2785            newPaths.append(p)
2786
2787        self.depotPaths = newPaths
2788
2789        self.loadUserMapFromCache()
2790        self.labels = {}
2791        if self.detectLabels:
2792            self.getLabels();
2793
2794        if self.detectBranches:
2795            ## FIXME - what's a P4 projectName ?
2796            self.projectName = self.guessProjectName()
2797
2798            if self.hasOrigin:
2799                self.getBranchMappingFromGitBranches()
2800            else:
2801                self.getBranchMapping()
2802            if self.verbose:
2803                print "p4-git branches: %s" % self.p4BranchesInGit
2804                print "initial parents: %s" % self.initialParents
2805            for b in self.p4BranchesInGit:
2806                if b != "master":
2807
2808                    ## FIXME
2809                    b = b[len(self.projectName):]
2810                self.createdBranches.add(b)
2811
2812        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2813
2814        importProcess = subprocess.Popen(["git", "fast-import"],
2815                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2816                                         stderr=subprocess.PIPE);
2817        self.gitOutput = importProcess.stdout
2818        self.gitStream = importProcess.stdin
2819        self.gitError = importProcess.stderr
2820
2821        if revision:
2822            self.importHeadRevision(revision)
2823        else:
2824            changes = []
2825
2826            if len(self.changesFile) > 0:
2827                output = open(self.changesFile).readlines()
2828                changeSet = set()
2829                for line in output:
2830                    changeSet.add(int(line))
2831
2832                for change in changeSet:
2833                    changes.append(change)
2834
2835                changes.sort()
2836            else:
2837                # catch "git p4 sync" with no new branches, in a repo that
2838                # does not have any existing p4 branches
2839                if len(args) == 0 and not self.p4BranchesInGit:
2840                    die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
2841                if self.verbose:
2842                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2843                                                              self.changeRange)
2844                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2845
2846                if len(self.maxChanges) > 0:
2847                    changes = changes[:min(int(self.maxChanges), len(changes))]
2848
2849            if len(changes) == 0:
2850                if not self.silent:
2851                    print "No changes to import!"
2852            else:
2853                if not self.silent and not self.detectBranches:
2854                    print "Import destination: %s" % self.branch
2855
2856                self.updatedBranches = set()
2857
2858                self.importChanges(changes)
2859
2860                if not self.silent:
2861                    print ""
2862                    if len(self.updatedBranches) > 0:
2863                        sys.stdout.write("Updated branches: ")
2864                        for b in self.updatedBranches:
2865                            sys.stdout.write("%s " % b)
2866                        sys.stdout.write("\n")
2867
2868        if gitConfig("git-p4.importLabels", "--bool") == "true":
2869            self.importLabels = True
2870
2871        if self.importLabels:
2872            p4Labels = getP4Labels(self.depotPaths)
2873            gitTags = getGitTags()
2874
2875            missingP4Labels = p4Labels - gitTags
2876            self.importP4Labels(self.gitStream, missingP4Labels)
2877
2878        self.gitStream.close()
2879        if importProcess.wait() != 0:
2880            die("fast-import failed: %s" % self.gitError.read())
2881        self.gitOutput.close()
2882        self.gitError.close()
2883
2884        # Cleanup temporary branches created during import
2885        if self.tempBranches != []:
2886            for branch in self.tempBranches:
2887                read_pipe("git update-ref -d %s" % branch)
2888            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
2889
2890        return True
2891
2892class P4Rebase(Command):
2893    def __init__(self):
2894        Command.__init__(self)
2895        self.options = [
2896                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2897        ]
2898        self.importLabels = False
2899        self.description = ("Fetches the latest revision from perforce and "
2900                            + "rebases the current work (branch) against it")
2901
2902    def run(self, args):
2903        sync = P4Sync()
2904        sync.importLabels = self.importLabels
2905        sync.run([])
2906
2907        return self.rebase()
2908
2909    def rebase(self):
2910        if os.system("git update-index --refresh") != 0:
2911            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.");
2912        if len(read_pipe("git diff-index HEAD --")) > 0:
2913            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2914
2915        [upstream, settings] = findUpstreamBranchPoint()
2916        if len(upstream) == 0:
2917            die("Cannot find upstream branchpoint for rebase")
2918
2919        # the branchpoint may be p4/foo~3, so strip off the parent
2920        upstream = re.sub("~[0-9]+$", "", upstream)
2921
2922        print "Rebasing the current branch onto %s" % upstream
2923        oldHead = read_pipe("git rev-parse HEAD").strip()
2924        system("git rebase %s" % upstream)
2925        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
2926        return True
2927
2928class P4Clone(P4Sync):
2929    def __init__(self):
2930        P4Sync.__init__(self)
2931        self.description = "Creates a new git repository and imports from Perforce into it"
2932        self.usage = "usage: %prog [options] //depot/path[@revRange]"
2933        self.options += [
2934            optparse.make_option("--destination", dest="cloneDestination",
2935                                 action='store', default=None,
2936                                 help="where to leave result of the clone"),
2937            optparse.make_option("-/", dest="cloneExclude",
2938                                 action="append", type="string",
2939                                 help="exclude depot path"),
2940            optparse.make_option("--bare", dest="cloneBare",
2941                                 action="store_true", default=False),
2942        ]
2943        self.cloneDestination = None
2944        self.needsGit = False
2945        self.cloneBare = False
2946
2947    # This is required for the "append" cloneExclude action
2948    def ensure_value(self, attr, value):
2949        if not hasattr(self, attr) or getattr(self, attr) is None:
2950            setattr(self, attr, value)
2951        return getattr(self, attr)
2952
2953    def defaultDestination(self, args):
2954        ## TODO: use common prefix of args?
2955        depotPath = args[0]
2956        depotDir = re.sub("(@[^@]*)$", "", depotPath)
2957        depotDir = re.sub("(#[^#]*)$", "", depotDir)
2958        depotDir = re.sub(r"\.\.\.$", "", depotDir)
2959        depotDir = re.sub(r"/$", "", depotDir)
2960        return os.path.split(depotDir)[1]
2961
2962    def run(self, args):
2963        if len(args) < 1:
2964            return False
2965
2966        if self.keepRepoPath and not self.cloneDestination:
2967            sys.stderr.write("Must specify destination for --keep-path\n")
2968            sys.exit(1)
2969
2970        depotPaths = args
2971
2972        if not self.cloneDestination and len(depotPaths) > 1:
2973            self.cloneDestination = depotPaths[-1]
2974            depotPaths = depotPaths[:-1]
2975
2976        self.cloneExclude = ["/"+p for p in self.cloneExclude]
2977        for p in depotPaths:
2978            if not p.startswith("//"):
2979                return False
2980
2981        if not self.cloneDestination:
2982            self.cloneDestination = self.defaultDestination(args)
2983
2984        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2985
2986        if not os.path.exists(self.cloneDestination):
2987            os.makedirs(self.cloneDestination)
2988        chdir(self.cloneDestination)
2989
2990        init_cmd = [ "git", "init" ]
2991        if self.cloneBare:
2992            init_cmd.append("--bare")
2993        subprocess.check_call(init_cmd)
2994
2995        if not P4Sync.run(self, depotPaths):
2996            return False
2997        if self.branch != "master":
2998            if self.importIntoRemotes:
2999                masterbranch = "refs/remotes/p4/master"
3000            else:
3001                masterbranch = "refs/heads/p4/master"
3002            if gitBranchExists(masterbranch):
3003                system("git branch master %s" % masterbranch)
3004                if not self.cloneBare:
3005                    system("git checkout -f")
3006            else:
3007                print "Could not detect main branch. No checkout/master branch created."
3008
3009        # auto-set this variable if invoked with --use-client-spec
3010        if self.useClientSpec_from_options:
3011            system("git config --bool git-p4.useclientspec true")
3012
3013        return True
3014
3015class P4Branches(Command):
3016    def __init__(self):
3017        Command.__init__(self)
3018        self.options = [ ]
3019        self.description = ("Shows the git branches that hold imports and their "
3020                            + "corresponding perforce depot paths")
3021        self.verbose = False
3022
3023    def run(self, args):
3024        if originP4BranchesExist():
3025            createOrUpdateBranchesFromOrigin()
3026
3027        cmdline = "git rev-parse --symbolic "
3028        cmdline += " --remotes"
3029
3030        for line in read_pipe_lines(cmdline):
3031            line = line.strip()
3032
3033            if not line.startswith('p4/') or line == "p4/HEAD":
3034                continue
3035            branch = line
3036
3037            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3038            settings = extractSettingsGitLog(log)
3039
3040            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3041        return True
3042
3043class HelpFormatter(optparse.IndentedHelpFormatter):
3044    def __init__(self):
3045        optparse.IndentedHelpFormatter.__init__(self)
3046
3047    def format_description(self, description):
3048        if description:
3049            return description + "\n"
3050        else:
3051            return ""
3052
3053def printUsage(commands):
3054    print "usage: %s <command> [options]" % sys.argv[0]
3055    print ""
3056    print "valid commands: %s" % ", ".join(commands)
3057    print ""
3058    print "Try %s <command> --help for command specific help." % sys.argv[0]
3059    print ""
3060
3061commands = {
3062    "debug" : P4Debug,
3063    "submit" : P4Submit,
3064    "commit" : P4Submit,
3065    "sync" : P4Sync,
3066    "rebase" : P4Rebase,
3067    "clone" : P4Clone,
3068    "rollback" : P4RollBack,
3069    "branches" : P4Branches
3070}
3071
3072
3073def main():
3074    if len(sys.argv[1:]) == 0:
3075        printUsage(commands.keys())
3076        sys.exit(2)
3077
3078    cmd = ""
3079    cmdName = sys.argv[1]
3080    try:
3081        klass = commands[cmdName]
3082        cmd = klass()
3083    except KeyError:
3084        print "unknown command %s" % cmdName
3085        print ""
3086        printUsage(commands.keys())
3087        sys.exit(2)
3088
3089    options = cmd.options
3090    cmd.gitdir = os.environ.get("GIT_DIR", None)
3091
3092    args = sys.argv[2:]
3093
3094    options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3095    if cmd.needsGit:
3096        options.append(optparse.make_option("--git-dir", dest="gitdir"))
3097
3098    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3099                                   options,
3100                                   description = cmd.description,
3101                                   formatter = HelpFormatter())
3102
3103    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3104    global verbose
3105    verbose = cmd.verbose
3106    if cmd.needsGit:
3107        if cmd.gitdir == None:
3108            cmd.gitdir = os.path.abspath(".git")
3109            if not isValidGitDir(cmd.gitdir):
3110                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3111                if os.path.exists(cmd.gitdir):
3112                    cdup = read_pipe("git rev-parse --show-cdup").strip()
3113                    if len(cdup) > 0:
3114                        chdir(cdup);
3115
3116        if not isValidGitDir(cmd.gitdir):
3117            if isValidGitDir(cmd.gitdir + "/.git"):
3118                cmd.gitdir += "/.git"
3119            else:
3120                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3121
3122        os.environ["GIT_DIR"] = cmd.gitdir
3123
3124    if not cmd.run(args):
3125        parser.print_help()
3126        sys.exit(2)
3127
3128
3129if __name__ == '__main__':
3130    main()