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