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