git-p4.pyon commit git p4: accept -v for --verbose (b0ccc80)
   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            for f in filesToDelete:
1308                p4_revert(f)
1309
1310        os.remove(fileName)
1311        return ret
1312
1313    # Export git tags as p4 labels. Create a p4 label and then tag
1314    # with that.
1315    def exportGitTags(self, gitTags):
1316        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1317        if len(validLabelRegexp) == 0:
1318            validLabelRegexp = defaultLabelRegexp
1319        m = re.compile(validLabelRegexp)
1320
1321        for name in gitTags:
1322
1323            if not m.match(name):
1324                if verbose:
1325                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1326                continue
1327
1328            # Get the p4 commit this corresponds to
1329            logMessage = extractLogMessageFromGitCommit(name)
1330            values = extractSettingsGitLog(logMessage)
1331
1332            if not values.has_key('change'):
1333                # a tag pointing to something not sent to p4; ignore
1334                if verbose:
1335                    print "git tag %s does not give a p4 commit" % name
1336                continue
1337            else:
1338                changelist = values['change']
1339
1340            # Get the tag details.
1341            inHeader = True
1342            isAnnotated = False
1343            body = []
1344            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1345                l = l.strip()
1346                if inHeader:
1347                    if re.match(r'tag\s+', l):
1348                        isAnnotated = True
1349                    elif re.match(r'\s*$', l):
1350                        inHeader = False
1351                        continue
1352                else:
1353                    body.append(l)
1354
1355            if not isAnnotated:
1356                body = ["lightweight tag imported by git p4\n"]
1357
1358            # Create the label - use the same view as the client spec we are using
1359            clientSpec = getClientSpec()
1360
1361            labelTemplate  = "Label: %s\n" % name
1362            labelTemplate += "Description:\n"
1363            for b in body:
1364                labelTemplate += "\t" + b + "\n"
1365            labelTemplate += "View:\n"
1366            for mapping in clientSpec.mappings:
1367                labelTemplate += "\t%s\n" % mapping.depot_side.path
1368
1369            p4_write_pipe(["label", "-i"], labelTemplate)
1370
1371            # Use the label
1372            p4_system(["tag", "-l", name] +
1373                      ["%s@%s" % (mapping.depot_side.path, changelist) for mapping in clientSpec.mappings])
1374
1375            if verbose:
1376                print "created p4 label for tag %s" % name
1377
1378    def run(self, args):
1379        if len(args) == 0:
1380            self.master = currentGitBranch()
1381            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1382                die("Detecting current git branch failed!")
1383        elif len(args) == 1:
1384            self.master = args[0]
1385            if not branchExists(self.master):
1386                die("Branch %s does not exist" % self.master)
1387        else:
1388            return False
1389
1390        allowSubmit = gitConfig("git-p4.allowSubmit")
1391        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1392            die("%s is not in git-p4.allowSubmit" % self.master)
1393
1394        [upstream, settings] = findUpstreamBranchPoint()
1395        self.depotPath = settings['depot-paths'][0]
1396        if len(self.origin) == 0:
1397            self.origin = upstream
1398
1399        if self.preserveUser:
1400            if not self.canChangeChangelists():
1401                die("Cannot preserve user names without p4 super-user or admin permissions")
1402
1403        if self.verbose:
1404            print "Origin branch is " + self.origin
1405
1406        if len(self.depotPath) == 0:
1407            print "Internal error: cannot locate perforce depot path from existing branches"
1408            sys.exit(128)
1409
1410        self.useClientSpec = False
1411        if gitConfig("git-p4.useclientspec", "--bool") == "true":
1412            self.useClientSpec = True
1413        if self.useClientSpec:
1414            self.clientSpecDirs = getClientSpec()
1415
1416        if self.useClientSpec:
1417            # all files are relative to the client spec
1418            self.clientPath = getClientRoot()
1419        else:
1420            self.clientPath = p4Where(self.depotPath)
1421
1422        if self.clientPath == "":
1423            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1424
1425        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1426        self.oldWorkingDirectory = os.getcwd()
1427
1428        # ensure the clientPath exists
1429        new_client_dir = False
1430        if not os.path.exists(self.clientPath):
1431            new_client_dir = True
1432            os.makedirs(self.clientPath)
1433
1434        chdir(self.clientPath)
1435        print "Synchronizing p4 checkout..."
1436        if new_client_dir:
1437            # old one was destroyed, and maybe nobody told p4
1438            p4_sync("...", "-f")
1439        else:
1440            p4_sync("...")
1441        self.check()
1442
1443        commits = []
1444        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1445            commits.append(line.strip())
1446        commits.reverse()
1447
1448        if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1449            self.checkAuthorship = False
1450        else:
1451            self.checkAuthorship = True
1452
1453        if self.preserveUser:
1454            self.checkValidP4Users(commits)
1455
1456        #
1457        # Build up a set of options to be passed to diff when
1458        # submitting each commit to p4.
1459        #
1460        if self.detectRenames:
1461            # command-line -M arg
1462            self.diffOpts = "-M"
1463        else:
1464            # If not explicitly set check the config variable
1465            detectRenames = gitConfig("git-p4.detectRenames")
1466
1467            if detectRenames.lower() == "false" or detectRenames == "":
1468                self.diffOpts = ""
1469            elif detectRenames.lower() == "true":
1470                self.diffOpts = "-M"
1471            else:
1472                self.diffOpts = "-M%s" % detectRenames
1473
1474        # no command-line arg for -C or --find-copies-harder, just
1475        # config variables
1476        detectCopies = gitConfig("git-p4.detectCopies")
1477        if detectCopies.lower() == "false" or detectCopies == "":
1478            pass
1479        elif detectCopies.lower() == "true":
1480            self.diffOpts += " -C"
1481        else:
1482            self.diffOpts += " -C%s" % detectCopies
1483
1484        if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
1485            self.diffOpts += " --find-copies-harder"
1486
1487        #
1488        # Apply the commits, one at a time.  On failure, ask if should
1489        # continue to try the rest of the patches, or quit.
1490        #
1491        applied = []
1492        last = len(commits) - 1
1493        for i, commit in enumerate(commits):
1494            ok = self.applyCommit(commit)
1495            if ok:
1496                applied.append(commit)
1497            else:
1498                if i < last:
1499                    quit = False
1500                    while True:
1501                        print "What do you want to do?"
1502                        response = raw_input("[s]kip this commit but apply"
1503                                             " the rest, or [q]uit? ")
1504                        if not response:
1505                            continue
1506                        if response[0] == "s":
1507                            print "Skipping this commit, but applying the rest"
1508                            break
1509                        if response[0] == "q":
1510                            print "Quitting"
1511                            quit = True
1512                            break
1513                    if quit:
1514                        break
1515
1516        chdir(self.oldWorkingDirectory)
1517
1518        if len(commits) == len(applied):
1519            print "All commits applied!"
1520
1521            sync = P4Sync()
1522            sync.run([])
1523
1524            rebase = P4Rebase()
1525            rebase.rebase()
1526
1527        else:
1528            if len(applied) == 0:
1529                print "No commits applied."
1530            else:
1531                print "Applied only the commits marked with '*':"
1532                for c in commits:
1533                    if c in applied:
1534                        star = "*"
1535                    else:
1536                        star = " "
1537                    print star, read_pipe(["git", "show", "-s",
1538                                           "--format=format:%h %s",  c])
1539                print "You will have to do 'git p4 sync' and rebase."
1540
1541        if gitConfig("git-p4.exportLabels", "--bool") == "true":
1542            self.exportLabels = True
1543
1544        if self.exportLabels:
1545            p4Labels = getP4Labels(self.depotPath)
1546            gitTags = getGitTags()
1547
1548            missingGitTags = gitTags - p4Labels
1549            self.exportGitTags(missingGitTags)
1550
1551        # exit with error unless everything applied perfecly
1552        if len(commits) != len(applied):
1553                sys.exit(1)
1554
1555        return True
1556
1557class View(object):
1558    """Represent a p4 view ("p4 help views"), and map files in a
1559       repo according to the view."""
1560
1561    class Path(object):
1562        """A depot or client path, possibly containing wildcards.
1563           The only one supported is ... at the end, currently.
1564           Initialize with the full path, with //depot or //client."""
1565
1566        def __init__(self, path, is_depot):
1567            self.path = path
1568            self.is_depot = is_depot
1569            self.find_wildcards()
1570            # remember the prefix bit, useful for relative mappings
1571            m = re.match("(//[^/]+/)", self.path)
1572            if not m:
1573                die("Path %s does not start with //prefix/" % self.path)
1574            prefix = m.group(1)
1575            if not self.is_depot:
1576                # strip //client/ on client paths
1577                self.path = self.path[len(prefix):]
1578
1579        def find_wildcards(self):
1580            """Make sure wildcards are valid, and set up internal
1581               variables."""
1582
1583            self.ends_triple_dot = False
1584            # There are three wildcards allowed in p4 views
1585            # (see "p4 help views").  This code knows how to
1586            # handle "..." (only at the end), but cannot deal with
1587            # "%%n" or "*".  Only check the depot_side, as p4 should
1588            # validate that the client_side matches too.
1589            if re.search(r'%%[1-9]', self.path):
1590                die("Can't handle %%n wildcards in view: %s" % self.path)
1591            if self.path.find("*") >= 0:
1592                die("Can't handle * wildcards in view: %s" % self.path)
1593            triple_dot_index = self.path.find("...")
1594            if triple_dot_index >= 0:
1595                if triple_dot_index != len(self.path) - 3:
1596                    die("Can handle only single ... wildcard, at end: %s" %
1597                        self.path)
1598                self.ends_triple_dot = True
1599
1600        def ensure_compatible(self, other_path):
1601            """Make sure the wildcards agree."""
1602            if self.ends_triple_dot != other_path.ends_triple_dot:
1603                 die("Both paths must end with ... if either does;\n" +
1604                     "paths: %s %s" % (self.path, other_path.path))
1605
1606        def match_wildcards(self, test_path):
1607            """See if this test_path matches us, and fill in the value
1608               of the wildcards if so.  Returns a tuple of
1609               (True|False, wildcards[]).  For now, only the ... at end
1610               is supported, so at most one wildcard."""
1611            if self.ends_triple_dot:
1612                dotless = self.path[:-3]
1613                if test_path.startswith(dotless):
1614                    wildcard = test_path[len(dotless):]
1615                    return (True, [ wildcard ])
1616            else:
1617                if test_path == self.path:
1618                    return (True, [])
1619            return (False, [])
1620
1621        def match(self, test_path):
1622            """Just return if it matches; don't bother with the wildcards."""
1623            b, _ = self.match_wildcards(test_path)
1624            return b
1625
1626        def fill_in_wildcards(self, wildcards):
1627            """Return the relative path, with the wildcards filled in
1628               if there are any."""
1629            if self.ends_triple_dot:
1630                return self.path[:-3] + wildcards[0]
1631            else:
1632                return self.path
1633
1634    class Mapping(object):
1635        def __init__(self, depot_side, client_side, overlay, exclude):
1636            # depot_side is without the trailing /... if it had one
1637            self.depot_side = View.Path(depot_side, is_depot=True)
1638            self.client_side = View.Path(client_side, is_depot=False)
1639            self.overlay = overlay  # started with "+"
1640            self.exclude = exclude  # started with "-"
1641            assert not (self.overlay and self.exclude)
1642            self.depot_side.ensure_compatible(self.client_side)
1643
1644        def __str__(self):
1645            c = " "
1646            if self.overlay:
1647                c = "+"
1648            if self.exclude:
1649                c = "-"
1650            return "View.Mapping: %s%s -> %s" % \
1651                   (c, self.depot_side.path, self.client_side.path)
1652
1653        def map_depot_to_client(self, depot_path):
1654            """Calculate the client path if using this mapping on the
1655               given depot path; does not consider the effect of other
1656               mappings in a view.  Even excluded mappings are returned."""
1657            matches, wildcards = self.depot_side.match_wildcards(depot_path)
1658            if not matches:
1659                return ""
1660            client_path = self.client_side.fill_in_wildcards(wildcards)
1661            return client_path
1662
1663    #
1664    # View methods
1665    #
1666    def __init__(self):
1667        self.mappings = []
1668
1669    def append(self, view_line):
1670        """Parse a view line, splitting it into depot and client
1671           sides.  Append to self.mappings, preserving order."""
1672
1673        # Split the view line into exactly two words.  P4 enforces
1674        # structure on these lines that simplifies this quite a bit.
1675        #
1676        # Either or both words may be double-quoted.
1677        # Single quotes do not matter.
1678        # Double-quote marks cannot occur inside the words.
1679        # A + or - prefix is also inside the quotes.
1680        # There are no quotes unless they contain a space.
1681        # The line is already white-space stripped.
1682        # The two words are separated by a single space.
1683        #
1684        if view_line[0] == '"':
1685            # First word is double quoted.  Find its end.
1686            close_quote_index = view_line.find('"', 1)
1687            if close_quote_index <= 0:
1688                die("No first-word closing quote found: %s" % view_line)
1689            depot_side = view_line[1:close_quote_index]
1690            # skip closing quote and space
1691            rhs_index = close_quote_index + 1 + 1
1692        else:
1693            space_index = view_line.find(" ")
1694            if space_index <= 0:
1695                die("No word-splitting space found: %s" % view_line)
1696            depot_side = view_line[0:space_index]
1697            rhs_index = space_index + 1
1698
1699        if view_line[rhs_index] == '"':
1700            # Second word is double quoted.  Make sure there is a
1701            # double quote at the end too.
1702            if not view_line.endswith('"'):
1703                die("View line with rhs quote should end with one: %s" %
1704                    view_line)
1705            # skip the quotes
1706            client_side = view_line[rhs_index+1:-1]
1707        else:
1708            client_side = view_line[rhs_index:]
1709
1710        # prefix + means overlay on previous mapping
1711        overlay = False
1712        if depot_side.startswith("+"):
1713            overlay = True
1714            depot_side = depot_side[1:]
1715
1716        # prefix - means exclude this path
1717        exclude = False
1718        if depot_side.startswith("-"):
1719            exclude = True
1720            depot_side = depot_side[1:]
1721
1722        m = View.Mapping(depot_side, client_side, overlay, exclude)
1723        self.mappings.append(m)
1724
1725    def map_in_client(self, depot_path):
1726        """Return the relative location in the client where this
1727           depot file should live.  Returns "" if the file should
1728           not be mapped in the client."""
1729
1730        paths_filled = []
1731        client_path = ""
1732
1733        # look at later entries first
1734        for m in self.mappings[::-1]:
1735
1736            # see where will this path end up in the client
1737            p = m.map_depot_to_client(depot_path)
1738
1739            if p == "":
1740                # Depot path does not belong in client.  Must remember
1741                # this, as previous items should not cause files to
1742                # exist in this path either.  Remember that the list is
1743                # being walked from the end, which has higher precedence.
1744                # Overlap mappings do not exclude previous mappings.
1745                if not m.overlay:
1746                    paths_filled.append(m.client_side)
1747
1748            else:
1749                # This mapping matched; no need to search any further.
1750                # But, the mapping could be rejected if the client path
1751                # has already been claimed by an earlier mapping (i.e.
1752                # one later in the list, which we are walking backwards).
1753                already_mapped_in_client = False
1754                for f in paths_filled:
1755                    # this is View.Path.match
1756                    if f.match(p):
1757                        already_mapped_in_client = True
1758                        break
1759                if not already_mapped_in_client:
1760                    # Include this file, unless it is from a line that
1761                    # explicitly said to exclude it.
1762                    if not m.exclude:
1763                        client_path = p
1764
1765                # a match, even if rejected, always stops the search
1766                break
1767
1768        return client_path
1769
1770class P4Sync(Command, P4UserMap):
1771    delete_actions = ( "delete", "move/delete", "purge" )
1772
1773    def __init__(self):
1774        Command.__init__(self)
1775        P4UserMap.__init__(self)
1776        self.options = [
1777                optparse.make_option("--branch", dest="branch"),
1778                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1779                optparse.make_option("--changesfile", dest="changesFile"),
1780                optparse.make_option("--silent", dest="silent", action="store_true"),
1781                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1782                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1783                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1784                                     help="Import into refs/heads/ , not refs/remotes"),
1785                optparse.make_option("--max-changes", dest="maxChanges"),
1786                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1787                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1788                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1789                                     help="Only sync files that are included in the Perforce Client Spec")
1790        ]
1791        self.description = """Imports from Perforce into a git repository.\n
1792    example:
1793    //depot/my/project/ -- to import the current head
1794    //depot/my/project/@all -- to import everything
1795    //depot/my/project/@1,6 -- to import only from revision 1 to 6
1796
1797    (a ... is not needed in the path p4 specification, it's added implicitly)"""
1798
1799        self.usage += " //depot/path[@revRange]"
1800        self.silent = False
1801        self.createdBranches = set()
1802        self.committedChanges = set()
1803        self.branch = ""
1804        self.detectBranches = False
1805        self.detectLabels = False
1806        self.importLabels = False
1807        self.changesFile = ""
1808        self.syncWithOrigin = True
1809        self.importIntoRemotes = True
1810        self.maxChanges = ""
1811        self.isWindows = (platform.system() == "Windows")
1812        self.keepRepoPath = False
1813        self.depotPaths = None
1814        self.p4BranchesInGit = []
1815        self.cloneExclude = []
1816        self.useClientSpec = False
1817        self.useClientSpec_from_options = False
1818        self.clientSpecDirs = None
1819        self.tempBranches = []
1820        self.tempBranchLocation = "git-p4-tmp"
1821
1822        if gitConfig("git-p4.syncFromOrigin") == "false":
1823            self.syncWithOrigin = False
1824
1825    # Force a checkpoint in fast-import and wait for it to finish
1826    def checkpoint(self):
1827        self.gitStream.write("checkpoint\n\n")
1828        self.gitStream.write("progress checkpoint\n\n")
1829        out = self.gitOutput.readline()
1830        if self.verbose:
1831            print "checkpoint finished: " + out
1832
1833    def extractFilesFromCommit(self, commit):
1834        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1835                             for path in self.cloneExclude]
1836        files = []
1837        fnum = 0
1838        while commit.has_key("depotFile%s" % fnum):
1839            path =  commit["depotFile%s" % fnum]
1840
1841            if [p for p in self.cloneExclude
1842                if p4PathStartsWith(path, p)]:
1843                found = False
1844            else:
1845                found = [p for p in self.depotPaths
1846                         if p4PathStartsWith(path, p)]
1847            if not found:
1848                fnum = fnum + 1
1849                continue
1850
1851            file = {}
1852            file["path"] = path
1853            file["rev"] = commit["rev%s" % fnum]
1854            file["action"] = commit["action%s" % fnum]
1855            file["type"] = commit["type%s" % fnum]
1856            files.append(file)
1857            fnum = fnum + 1
1858        return files
1859
1860    def stripRepoPath(self, path, prefixes):
1861        if self.useClientSpec:
1862            return self.clientSpecDirs.map_in_client(path)
1863
1864        if self.keepRepoPath:
1865            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1866
1867        for p in prefixes:
1868            if p4PathStartsWith(path, p):
1869                path = path[len(p):]
1870
1871        return path
1872
1873    def splitFilesIntoBranches(self, commit):
1874        branches = {}
1875        fnum = 0
1876        while commit.has_key("depotFile%s" % fnum):
1877            path =  commit["depotFile%s" % fnum]
1878            found = [p for p in self.depotPaths
1879                     if p4PathStartsWith(path, p)]
1880            if not found:
1881                fnum = fnum + 1
1882                continue
1883
1884            file = {}
1885            file["path"] = path
1886            file["rev"] = commit["rev%s" % fnum]
1887            file["action"] = commit["action%s" % fnum]
1888            file["type"] = commit["type%s" % fnum]
1889            fnum = fnum + 1
1890
1891            relPath = self.stripRepoPath(path, self.depotPaths)
1892            relPath = wildcard_decode(relPath)
1893
1894            for branch in self.knownBranches.keys():
1895
1896                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1897                if relPath.startswith(branch + "/"):
1898                    if branch not in branches:
1899                        branches[branch] = []
1900                    branches[branch].append(file)
1901                    break
1902
1903        return branches
1904
1905    # output one file from the P4 stream
1906    # - helper for streamP4Files
1907
1908    def streamOneP4File(self, file, contents):
1909        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1910        relPath = wildcard_decode(relPath)
1911        if verbose:
1912            sys.stderr.write("%s\n" % relPath)
1913
1914        (type_base, type_mods) = split_p4_type(file["type"])
1915
1916        git_mode = "100644"
1917        if "x" in type_mods:
1918            git_mode = "100755"
1919        if type_base == "symlink":
1920            git_mode = "120000"
1921            # p4 print on a symlink contains "target\n"; remove the newline
1922            data = ''.join(contents)
1923            contents = [data[:-1]]
1924
1925        if type_base == "utf16":
1926            # p4 delivers different text in the python output to -G
1927            # than it does when using "print -o", or normal p4 client
1928            # operations.  utf16 is converted to ascii or utf8, perhaps.
1929            # But ascii text saved as -t utf16 is completely mangled.
1930            # Invoke print -o to get the real contents.
1931            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
1932            contents = [ text ]
1933
1934        if type_base == "apple":
1935            # Apple filetype files will be streamed as a concatenation of
1936            # its appledouble header and the contents.  This is useless
1937            # on both macs and non-macs.  If using "print -q -o xx", it
1938            # will create "xx" with the data, and "%xx" with the header.
1939            # This is also not very useful.
1940            #
1941            # Ideally, someday, this script can learn how to generate
1942            # appledouble files directly and import those to git, but
1943            # non-mac machines can never find a use for apple filetype.
1944            print "\nIgnoring apple filetype file %s" % file['depotFile']
1945            return
1946
1947        # Perhaps windows wants unicode, utf16 newlines translated too;
1948        # but this is not doing it.
1949        if self.isWindows and type_base == "text":
1950            mangled = []
1951            for data in contents:
1952                data = data.replace("\r\n", "\n")
1953                mangled.append(data)
1954            contents = mangled
1955
1956        # Note that we do not try to de-mangle keywords on utf16 files,
1957        # even though in theory somebody may want that.
1958        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
1959        if pattern:
1960            regexp = re.compile(pattern, re.VERBOSE)
1961            text = ''.join(contents)
1962            text = regexp.sub(r'$\1$', text)
1963            contents = [ text ]
1964
1965        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
1966
1967        # total length...
1968        length = 0
1969        for d in contents:
1970            length = length + len(d)
1971
1972        self.gitStream.write("data %d\n" % length)
1973        for d in contents:
1974            self.gitStream.write(d)
1975        self.gitStream.write("\n")
1976
1977    def streamOneP4Deletion(self, file):
1978        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1979        relPath = wildcard_decode(relPath)
1980        if verbose:
1981            sys.stderr.write("delete %s\n" % relPath)
1982        self.gitStream.write("D %s\n" % relPath)
1983
1984    # handle another chunk of streaming data
1985    def streamP4FilesCb(self, marshalled):
1986
1987        if marshalled.has_key('depotFile') and self.stream_have_file_info:
1988            # start of a new file - output the old one first
1989            self.streamOneP4File(self.stream_file, self.stream_contents)
1990            self.stream_file = {}
1991            self.stream_contents = []
1992            self.stream_have_file_info = False
1993
1994        # pick up the new file information... for the
1995        # 'data' field we need to append to our array
1996        for k in marshalled.keys():
1997            if k == 'data':
1998                self.stream_contents.append(marshalled['data'])
1999            else:
2000                self.stream_file[k] = marshalled[k]
2001
2002        self.stream_have_file_info = True
2003
2004    # Stream directly from "p4 files" into "git fast-import"
2005    def streamP4Files(self, files):
2006        filesForCommit = []
2007        filesToRead = []
2008        filesToDelete = []
2009
2010        for f in files:
2011            # if using a client spec, only add the files that have
2012            # a path in the client
2013            if self.clientSpecDirs:
2014                if self.clientSpecDirs.map_in_client(f['path']) == "":
2015                    continue
2016
2017            filesForCommit.append(f)
2018            if f['action'] in self.delete_actions:
2019                filesToDelete.append(f)
2020            else:
2021                filesToRead.append(f)
2022
2023        # deleted files...
2024        for f in filesToDelete:
2025            self.streamOneP4Deletion(f)
2026
2027        if len(filesToRead) > 0:
2028            self.stream_file = {}
2029            self.stream_contents = []
2030            self.stream_have_file_info = False
2031
2032            # curry self argument
2033            def streamP4FilesCbSelf(entry):
2034                self.streamP4FilesCb(entry)
2035
2036            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2037
2038            p4CmdList(["-x", "-", "print"],
2039                      stdin=fileArgs,
2040                      cb=streamP4FilesCbSelf)
2041
2042            # do the last chunk
2043            if self.stream_file.has_key('depotFile'):
2044                self.streamOneP4File(self.stream_file, self.stream_contents)
2045
2046    def make_email(self, userid):
2047        if userid in self.users:
2048            return self.users[userid]
2049        else:
2050            return "%s <a@b>" % userid
2051
2052    # Stream a p4 tag
2053    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2054        if verbose:
2055            print "writing tag %s for commit %s" % (labelName, commit)
2056        gitStream.write("tag %s\n" % labelName)
2057        gitStream.write("from %s\n" % commit)
2058
2059        if labelDetails.has_key('Owner'):
2060            owner = labelDetails["Owner"]
2061        else:
2062            owner = None
2063
2064        # Try to use the owner of the p4 label, or failing that,
2065        # the current p4 user id.
2066        if owner:
2067            email = self.make_email(owner)
2068        else:
2069            email = self.make_email(self.p4UserId())
2070        tagger = "%s %s %s" % (email, epoch, self.tz)
2071
2072        gitStream.write("tagger %s\n" % tagger)
2073
2074        print "labelDetails=",labelDetails
2075        if labelDetails.has_key('Description'):
2076            description = labelDetails['Description']
2077        else:
2078            description = 'Label from git p4'
2079
2080        gitStream.write("data %d\n" % len(description))
2081        gitStream.write(description)
2082        gitStream.write("\n")
2083
2084    def commit(self, details, files, branch, branchPrefixes, parent = ""):
2085        epoch = details["time"]
2086        author = details["user"]
2087        self.branchPrefixes = branchPrefixes
2088
2089        if self.verbose:
2090            print "commit into %s" % branch
2091
2092        # start with reading files; if that fails, we should not
2093        # create a commit.
2094        new_files = []
2095        for f in files:
2096            if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
2097                new_files.append (f)
2098            else:
2099                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2100
2101        self.gitStream.write("commit %s\n" % branch)
2102#        gitStream.write("mark :%s\n" % details["change"])
2103        self.committedChanges.add(int(details["change"]))
2104        committer = ""
2105        if author not in self.users:
2106            self.getUserMapFromPerforceServer()
2107        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2108
2109        self.gitStream.write("committer %s\n" % committer)
2110
2111        self.gitStream.write("data <<EOT\n")
2112        self.gitStream.write(details["desc"])
2113        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
2114                             % (','.join (branchPrefixes), details["change"]))
2115        if len(details['options']) > 0:
2116            self.gitStream.write(": options = %s" % details['options'])
2117        self.gitStream.write("]\nEOT\n\n")
2118
2119        if len(parent) > 0:
2120            if self.verbose:
2121                print "parent %s" % parent
2122            self.gitStream.write("from %s\n" % parent)
2123
2124        self.streamP4Files(new_files)
2125        self.gitStream.write("\n")
2126
2127        change = int(details["change"])
2128
2129        if self.labels.has_key(change):
2130            label = self.labels[change]
2131            labelDetails = label[0]
2132            labelRevisions = label[1]
2133            if self.verbose:
2134                print "Change %s is labelled %s" % (change, labelDetails)
2135
2136            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2137                                                    for p in branchPrefixes])
2138
2139            if len(files) == len(labelRevisions):
2140
2141                cleanedFiles = {}
2142                for info in files:
2143                    if info["action"] in self.delete_actions:
2144                        continue
2145                    cleanedFiles[info["depotFile"]] = info["rev"]
2146
2147                if cleanedFiles == labelRevisions:
2148                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2149
2150                else:
2151                    if not self.silent:
2152                        print ("Tag %s does not match with change %s: files do not match."
2153                               % (labelDetails["label"], change))
2154
2155            else:
2156                if not self.silent:
2157                    print ("Tag %s does not match with change %s: file count is different."
2158                           % (labelDetails["label"], change))
2159
2160    # Build a dictionary of changelists and labels, for "detect-labels" option.
2161    def getLabels(self):
2162        self.labels = {}
2163
2164        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2165        if len(l) > 0 and not self.silent:
2166            print "Finding files belonging to labels in %s" % `self.depotPaths`
2167
2168        for output in l:
2169            label = output["label"]
2170            revisions = {}
2171            newestChange = 0
2172            if self.verbose:
2173                print "Querying files for label %s" % label
2174            for file in p4CmdList(["files"] +
2175                                      ["%s...@%s" % (p, label)
2176                                          for p in self.depotPaths]):
2177                revisions[file["depotFile"]] = file["rev"]
2178                change = int(file["change"])
2179                if change > newestChange:
2180                    newestChange = change
2181
2182            self.labels[newestChange] = [output, revisions]
2183
2184        if self.verbose:
2185            print "Label changes: %s" % self.labels.keys()
2186
2187    # Import p4 labels as git tags. A direct mapping does not
2188    # exist, so assume that if all the files are at the same revision
2189    # then we can use that, or it's something more complicated we should
2190    # just ignore.
2191    def importP4Labels(self, stream, p4Labels):
2192        if verbose:
2193            print "import p4 labels: " + ' '.join(p4Labels)
2194
2195        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2196        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2197        if len(validLabelRegexp) == 0:
2198            validLabelRegexp = defaultLabelRegexp
2199        m = re.compile(validLabelRegexp)
2200
2201        for name in p4Labels:
2202            commitFound = False
2203
2204            if not m.match(name):
2205                if verbose:
2206                    print "label %s does not match regexp %s" % (name,validLabelRegexp)
2207                continue
2208
2209            if name in ignoredP4Labels:
2210                continue
2211
2212            labelDetails = p4CmdList(['label', "-o", name])[0]
2213
2214            # get the most recent changelist for each file in this label
2215            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2216                                for p in self.depotPaths])
2217
2218            if change.has_key('change'):
2219                # find the corresponding git commit; take the oldest commit
2220                changelist = int(change['change'])
2221                gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2222                     "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2223                if len(gitCommit) == 0:
2224                    print "could not find git commit for changelist %d" % changelist
2225                else:
2226                    gitCommit = gitCommit.strip()
2227                    commitFound = True
2228                    # Convert from p4 time format
2229                    try:
2230                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2231                    except ValueError:
2232                        print "Could not convert label time %s" % labelDetail['Update']
2233                        tmwhen = 1
2234
2235                    when = int(time.mktime(tmwhen))
2236                    self.streamTag(stream, name, labelDetails, gitCommit, when)
2237                    if verbose:
2238                        print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2239            else:
2240                if verbose:
2241                    print "Label %s has no changelists - possibly deleted?" % name
2242
2243            if not commitFound:
2244                # We can't import this label; don't try again as it will get very
2245                # expensive repeatedly fetching all the files for labels that will
2246                # never be imported. If the label is moved in the future, the
2247                # ignore will need to be removed manually.
2248                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2249
2250    def guessProjectName(self):
2251        for p in self.depotPaths:
2252            if p.endswith("/"):
2253                p = p[:-1]
2254            p = p[p.strip().rfind("/") + 1:]
2255            if not p.endswith("/"):
2256               p += "/"
2257            return p
2258
2259    def getBranchMapping(self):
2260        lostAndFoundBranches = set()
2261
2262        user = gitConfig("git-p4.branchUser")
2263        if len(user) > 0:
2264            command = "branches -u %s" % user
2265        else:
2266            command = "branches"
2267
2268        for info in p4CmdList(command):
2269            details = p4Cmd(["branch", "-o", info["branch"]])
2270            viewIdx = 0
2271            while details.has_key("View%s" % viewIdx):
2272                paths = details["View%s" % viewIdx].split(" ")
2273                viewIdx = viewIdx + 1
2274                # require standard //depot/foo/... //depot/bar/... mapping
2275                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2276                    continue
2277                source = paths[0]
2278                destination = paths[1]
2279                ## HACK
2280                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2281                    source = source[len(self.depotPaths[0]):-4]
2282                    destination = destination[len(self.depotPaths[0]):-4]
2283
2284                    if destination in self.knownBranches:
2285                        if not self.silent:
2286                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2287                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2288                        continue
2289
2290                    self.knownBranches[destination] = source
2291
2292                    lostAndFoundBranches.discard(destination)
2293
2294                    if source not in self.knownBranches:
2295                        lostAndFoundBranches.add(source)
2296
2297        # Perforce does not strictly require branches to be defined, so we also
2298        # check git config for a branch list.
2299        #
2300        # Example of branch definition in git config file:
2301        # [git-p4]
2302        #   branchList=main:branchA
2303        #   branchList=main:branchB
2304        #   branchList=branchA:branchC
2305        configBranches = gitConfigList("git-p4.branchList")
2306        for branch in configBranches:
2307            if branch:
2308                (source, destination) = branch.split(":")
2309                self.knownBranches[destination] = source
2310
2311                lostAndFoundBranches.discard(destination)
2312
2313                if source not in self.knownBranches:
2314                    lostAndFoundBranches.add(source)
2315
2316
2317        for branch in lostAndFoundBranches:
2318            self.knownBranches[branch] = branch
2319
2320    def getBranchMappingFromGitBranches(self):
2321        branches = p4BranchesInGit(self.importIntoRemotes)
2322        for branch in branches.keys():
2323            if branch == "master":
2324                branch = "main"
2325            else:
2326                branch = branch[len(self.projectName):]
2327            self.knownBranches[branch] = branch
2328
2329    def listExistingP4GitBranches(self):
2330        # branches holds mapping from name to commit
2331        branches = p4BranchesInGit(self.importIntoRemotes)
2332        self.p4BranchesInGit = branches.keys()
2333        for branch in branches.keys():
2334            self.initialParents[self.refPrefix + branch] = branches[branch]
2335
2336    def updateOptionDict(self, d):
2337        option_keys = {}
2338        if self.keepRepoPath:
2339            option_keys['keepRepoPath'] = 1
2340
2341        d["options"] = ' '.join(sorted(option_keys.keys()))
2342
2343    def readOptions(self, d):
2344        self.keepRepoPath = (d.has_key('options')
2345                             and ('keepRepoPath' in d['options']))
2346
2347    def gitRefForBranch(self, branch):
2348        if branch == "main":
2349            return self.refPrefix + "master"
2350
2351        if len(branch) <= 0:
2352            return branch
2353
2354        return self.refPrefix + self.projectName + branch
2355
2356    def gitCommitByP4Change(self, ref, change):
2357        if self.verbose:
2358            print "looking in ref " + ref + " for change %s using bisect..." % change
2359
2360        earliestCommit = ""
2361        latestCommit = parseRevision(ref)
2362
2363        while True:
2364            if self.verbose:
2365                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2366            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2367            if len(next) == 0:
2368                if self.verbose:
2369                    print "argh"
2370                return ""
2371            log = extractLogMessageFromGitCommit(next)
2372            settings = extractSettingsGitLog(log)
2373            currentChange = int(settings['change'])
2374            if self.verbose:
2375                print "current change %s" % currentChange
2376
2377            if currentChange == change:
2378                if self.verbose:
2379                    print "found %s" % next
2380                return next
2381
2382            if currentChange < change:
2383                earliestCommit = "^%s" % next
2384            else:
2385                latestCommit = "%s" % next
2386
2387        return ""
2388
2389    def importNewBranch(self, branch, maxChange):
2390        # make fast-import flush all changes to disk and update the refs using the checkpoint
2391        # command so that we can try to find the branch parent in the git history
2392        self.gitStream.write("checkpoint\n\n");
2393        self.gitStream.flush();
2394        branchPrefix = self.depotPaths[0] + branch + "/"
2395        range = "@1,%s" % maxChange
2396        #print "prefix" + branchPrefix
2397        changes = p4ChangesForPaths([branchPrefix], range)
2398        if len(changes) <= 0:
2399            return False
2400        firstChange = changes[0]
2401        #print "first change in branch: %s" % firstChange
2402        sourceBranch = self.knownBranches[branch]
2403        sourceDepotPath = self.depotPaths[0] + sourceBranch
2404        sourceRef = self.gitRefForBranch(sourceBranch)
2405        #print "source " + sourceBranch
2406
2407        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2408        #print "branch parent: %s" % branchParentChange
2409        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2410        if len(gitParent) > 0:
2411            self.initialParents[self.gitRefForBranch(branch)] = gitParent
2412            #print "parent git commit: %s" % gitParent
2413
2414        self.importChanges(changes)
2415        return True
2416
2417    def searchParent(self, parent, branch, target):
2418        parentFound = False
2419        for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]):
2420            blob = blob.strip()
2421            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2422                parentFound = True
2423                if self.verbose:
2424                    print "Found parent of %s in commit %s" % (branch, blob)
2425                break
2426        if parentFound:
2427            return blob
2428        else:
2429            return None
2430
2431    def importChanges(self, changes):
2432        cnt = 1
2433        for change in changes:
2434            description = p4Cmd(["describe", str(change)])
2435            self.updateOptionDict(description)
2436
2437            if not self.silent:
2438                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2439                sys.stdout.flush()
2440            cnt = cnt + 1
2441
2442            try:
2443                if self.detectBranches:
2444                    branches = self.splitFilesIntoBranches(description)
2445                    for branch in branches.keys():
2446                        ## HACK  --hwn
2447                        branchPrefix = self.depotPaths[0] + branch + "/"
2448
2449                        parent = ""
2450
2451                        filesForCommit = branches[branch]
2452
2453                        if self.verbose:
2454                            print "branch is %s" % branch
2455
2456                        self.updatedBranches.add(branch)
2457
2458                        if branch not in self.createdBranches:
2459                            self.createdBranches.add(branch)
2460                            parent = self.knownBranches[branch]
2461                            if parent == branch:
2462                                parent = ""
2463                            else:
2464                                fullBranch = self.projectName + branch
2465                                if fullBranch not in self.p4BranchesInGit:
2466                                    if not self.silent:
2467                                        print("\n    Importing new branch %s" % fullBranch);
2468                                    if self.importNewBranch(branch, change - 1):
2469                                        parent = ""
2470                                        self.p4BranchesInGit.append(fullBranch)
2471                                    if not self.silent:
2472                                        print("\n    Resuming with change %s" % change);
2473
2474                                if self.verbose:
2475                                    print "parent determined through known branches: %s" % parent
2476
2477                        branch = self.gitRefForBranch(branch)
2478                        parent = self.gitRefForBranch(parent)
2479
2480                        if self.verbose:
2481                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2482
2483                        if len(parent) == 0 and branch in self.initialParents:
2484                            parent = self.initialParents[branch]
2485                            del self.initialParents[branch]
2486
2487                        blob = None
2488                        if len(parent) > 0:
2489                            tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change))
2490                            if self.verbose:
2491                                print "Creating temporary branch: " + tempBranch
2492                            self.commit(description, filesForCommit, tempBranch, [branchPrefix])
2493                            self.tempBranches.append(tempBranch)
2494                            self.checkpoint()
2495                            blob = self.searchParent(parent, branch, tempBranch)
2496                        if blob:
2497                            self.commit(description, filesForCommit, branch, [branchPrefix], blob)
2498                        else:
2499                            if self.verbose:
2500                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2501                            self.commit(description, filesForCommit, branch, [branchPrefix], parent)
2502                else:
2503                    files = self.extractFilesFromCommit(description)
2504                    self.commit(description, files, self.branch, self.depotPaths,
2505                                self.initialParent)
2506                    self.initialParent = ""
2507            except IOError:
2508                print self.gitError.read()
2509                sys.exit(1)
2510
2511    def importHeadRevision(self, revision):
2512        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2513
2514        details = {}
2515        details["user"] = "git perforce import user"
2516        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2517                           % (' '.join(self.depotPaths), revision))
2518        details["change"] = revision
2519        newestRevision = 0
2520
2521        fileCnt = 0
2522        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2523
2524        for info in p4CmdList(["files"] + fileArgs):
2525
2526            if 'code' in info and info['code'] == 'error':
2527                sys.stderr.write("p4 returned an error: %s\n"
2528                                 % info['data'])
2529                if info['data'].find("must refer to client") >= 0:
2530                    sys.stderr.write("This particular p4 error is misleading.\n")
2531                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
2532                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2533                sys.exit(1)
2534            if 'p4ExitCode' in info:
2535                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2536                sys.exit(1)
2537
2538
2539            change = int(info["change"])
2540            if change > newestRevision:
2541                newestRevision = change
2542
2543            if info["action"] in self.delete_actions:
2544                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2545                #fileCnt = fileCnt + 1
2546                continue
2547
2548            for prop in ["depotFile", "rev", "action", "type" ]:
2549                details["%s%s" % (prop, fileCnt)] = info[prop]
2550
2551            fileCnt = fileCnt + 1
2552
2553        details["change"] = newestRevision
2554
2555        # Use time from top-most change so that all git p4 clones of
2556        # the same p4 repo have the same commit SHA1s.
2557        res = p4CmdList("describe -s %d" % newestRevision)
2558        newestTime = None
2559        for r in res:
2560            if r.has_key('time'):
2561                newestTime = int(r['time'])
2562        if newestTime is None:
2563            die("\"describe -s\" on newest change %d did not give a time")
2564        details["time"] = newestTime
2565
2566        self.updateOptionDict(details)
2567        try:
2568            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
2569        except IOError:
2570            print "IO error with git fast-import. Is your git version recent enough?"
2571            print self.gitError.read()
2572
2573
2574    def run(self, args):
2575        self.depotPaths = []
2576        self.changeRange = ""
2577        self.initialParent = ""
2578        self.previousDepotPaths = []
2579
2580        # map from branch depot path to parent branch
2581        self.knownBranches = {}
2582        self.initialParents = {}
2583        self.hasOrigin = originP4BranchesExist()
2584        if not self.syncWithOrigin:
2585            self.hasOrigin = False
2586
2587        if self.importIntoRemotes:
2588            self.refPrefix = "refs/remotes/p4/"
2589        else:
2590            self.refPrefix = "refs/heads/p4/"
2591
2592        if self.syncWithOrigin and self.hasOrigin:
2593            if not self.silent:
2594                print "Syncing with origin first by calling git fetch origin"
2595            system("git fetch origin")
2596
2597        if len(self.branch) == 0:
2598            self.branch = self.refPrefix + "master"
2599            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2600                system("git update-ref %s refs/heads/p4" % self.branch)
2601                system("git branch -D p4");
2602            # create it /after/ importing, when master exists
2603            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
2604                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
2605
2606        # accept either the command-line option, or the configuration variable
2607        if self.useClientSpec:
2608            # will use this after clone to set the variable
2609            self.useClientSpec_from_options = True
2610        else:
2611            if gitConfig("git-p4.useclientspec", "--bool") == "true":
2612                self.useClientSpec = True
2613        if self.useClientSpec:
2614            self.clientSpecDirs = getClientSpec()
2615
2616        # TODO: should always look at previous commits,
2617        # merge with previous imports, if possible.
2618        if args == []:
2619            if self.hasOrigin:
2620                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2621            self.listExistingP4GitBranches()
2622
2623            if len(self.p4BranchesInGit) > 1:
2624                if not self.silent:
2625                    print "Importing from/into multiple branches"
2626                self.detectBranches = True
2627
2628            if self.verbose:
2629                print "branches: %s" % self.p4BranchesInGit
2630
2631            p4Change = 0
2632            for branch in self.p4BranchesInGit:
2633                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2634
2635                settings = extractSettingsGitLog(logMsg)
2636
2637                self.readOptions(settings)
2638                if (settings.has_key('depot-paths')
2639                    and settings.has_key ('change')):
2640                    change = int(settings['change']) + 1
2641                    p4Change = max(p4Change, change)
2642
2643                    depotPaths = sorted(settings['depot-paths'])
2644                    if self.previousDepotPaths == []:
2645                        self.previousDepotPaths = depotPaths
2646                    else:
2647                        paths = []
2648                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2649                            prev_list = prev.split("/")
2650                            cur_list = cur.split("/")
2651                            for i in range(0, min(len(cur_list), len(prev_list))):
2652                                if cur_list[i] <> prev_list[i]:
2653                                    i = i - 1
2654                                    break
2655
2656                            paths.append ("/".join(cur_list[:i + 1]))
2657
2658                        self.previousDepotPaths = paths
2659
2660            if p4Change > 0:
2661                self.depotPaths = sorted(self.previousDepotPaths)
2662                self.changeRange = "@%s,#head" % p4Change
2663                if not self.detectBranches:
2664                    self.initialParent = parseRevision(self.branch)
2665                if not self.silent and not self.detectBranches:
2666                    print "Performing incremental import into %s git branch" % self.branch
2667
2668        if not self.branch.startswith("refs/"):
2669            self.branch = "refs/heads/" + self.branch
2670
2671        if len(args) == 0 and self.depotPaths:
2672            if not self.silent:
2673                print "Depot paths: %s" % ' '.join(self.depotPaths)
2674        else:
2675            if self.depotPaths and self.depotPaths != args:
2676                print ("previous import used depot path %s and now %s was specified. "
2677                       "This doesn't work!" % (' '.join (self.depotPaths),
2678                                               ' '.join (args)))
2679                sys.exit(1)
2680
2681            self.depotPaths = sorted(args)
2682
2683        revision = ""
2684        self.users = {}
2685
2686        # Make sure no revision specifiers are used when --changesfile
2687        # is specified.
2688        bad_changesfile = False
2689        if len(self.changesFile) > 0:
2690            for p in self.depotPaths:
2691                if p.find("@") >= 0 or p.find("#") >= 0:
2692                    bad_changesfile = True
2693                    break
2694        if bad_changesfile:
2695            die("Option --changesfile is incompatible with revision specifiers")
2696
2697        newPaths = []
2698        for p in self.depotPaths:
2699            if p.find("@") != -1:
2700                atIdx = p.index("@")
2701                self.changeRange = p[atIdx:]
2702                if self.changeRange == "@all":
2703                    self.changeRange = ""
2704                elif ',' not in self.changeRange:
2705                    revision = self.changeRange
2706                    self.changeRange = ""
2707                p = p[:atIdx]
2708            elif p.find("#") != -1:
2709                hashIdx = p.index("#")
2710                revision = p[hashIdx:]
2711                p = p[:hashIdx]
2712            elif self.previousDepotPaths == []:
2713                # pay attention to changesfile, if given, else import
2714                # the entire p4 tree at the head revision
2715                if len(self.changesFile) == 0:
2716                    revision = "#head"
2717
2718            p = re.sub ("\.\.\.$", "", p)
2719            if not p.endswith("/"):
2720                p += "/"
2721
2722            newPaths.append(p)
2723
2724        self.depotPaths = newPaths
2725
2726        self.loadUserMapFromCache()
2727        self.labels = {}
2728        if self.detectLabels:
2729            self.getLabels();
2730
2731        if self.detectBranches:
2732            ## FIXME - what's a P4 projectName ?
2733            self.projectName = self.guessProjectName()
2734
2735            if self.hasOrigin:
2736                self.getBranchMappingFromGitBranches()
2737            else:
2738                self.getBranchMapping()
2739            if self.verbose:
2740                print "p4-git branches: %s" % self.p4BranchesInGit
2741                print "initial parents: %s" % self.initialParents
2742            for b in self.p4BranchesInGit:
2743                if b != "master":
2744
2745                    ## FIXME
2746                    b = b[len(self.projectName):]
2747                self.createdBranches.add(b)
2748
2749        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2750
2751        importProcess = subprocess.Popen(["git", "fast-import"],
2752                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2753                                         stderr=subprocess.PIPE);
2754        self.gitOutput = importProcess.stdout
2755        self.gitStream = importProcess.stdin
2756        self.gitError = importProcess.stderr
2757
2758        if revision:
2759            self.importHeadRevision(revision)
2760        else:
2761            changes = []
2762
2763            if len(self.changesFile) > 0:
2764                output = open(self.changesFile).readlines()
2765                changeSet = set()
2766                for line in output:
2767                    changeSet.add(int(line))
2768
2769                for change in changeSet:
2770                    changes.append(change)
2771
2772                changes.sort()
2773            else:
2774                # catch "git p4 sync" with no new branches, in a repo that
2775                # does not have any existing p4 branches
2776                if len(args) == 0 and not self.p4BranchesInGit:
2777                    die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
2778                if self.verbose:
2779                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2780                                                              self.changeRange)
2781                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2782
2783                if len(self.maxChanges) > 0:
2784                    changes = changes[:min(int(self.maxChanges), len(changes))]
2785
2786            if len(changes) == 0:
2787                if not self.silent:
2788                    print "No changes to import!"
2789            else:
2790                if not self.silent and not self.detectBranches:
2791                    print "Import destination: %s" % self.branch
2792
2793                self.updatedBranches = set()
2794
2795                self.importChanges(changes)
2796
2797                if not self.silent:
2798                    print ""
2799                    if len(self.updatedBranches) > 0:
2800                        sys.stdout.write("Updated branches: ")
2801                        for b in self.updatedBranches:
2802                            sys.stdout.write("%s " % b)
2803                        sys.stdout.write("\n")
2804
2805        if gitConfig("git-p4.importLabels", "--bool") == "true":
2806            self.importLabels = True
2807
2808        if self.importLabels:
2809            p4Labels = getP4Labels(self.depotPaths)
2810            gitTags = getGitTags()
2811
2812            missingP4Labels = p4Labels - gitTags
2813            self.importP4Labels(self.gitStream, missingP4Labels)
2814
2815        self.gitStream.close()
2816        if importProcess.wait() != 0:
2817            die("fast-import failed: %s" % self.gitError.read())
2818        self.gitOutput.close()
2819        self.gitError.close()
2820
2821        # Cleanup temporary branches created during import
2822        if self.tempBranches != []:
2823            for branch in self.tempBranches:
2824                read_pipe("git update-ref -d %s" % branch)
2825            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
2826
2827        return True
2828
2829class P4Rebase(Command):
2830    def __init__(self):
2831        Command.__init__(self)
2832        self.options = [
2833                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2834        ]
2835        self.importLabels = False
2836        self.description = ("Fetches the latest revision from perforce and "
2837                            + "rebases the current work (branch) against it")
2838
2839    def run(self, args):
2840        sync = P4Sync()
2841        sync.importLabels = self.importLabels
2842        sync.run([])
2843
2844        return self.rebase()
2845
2846    def rebase(self):
2847        if os.system("git update-index --refresh") != 0:
2848            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.");
2849        if len(read_pipe("git diff-index HEAD --")) > 0:
2850            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2851
2852        [upstream, settings] = findUpstreamBranchPoint()
2853        if len(upstream) == 0:
2854            die("Cannot find upstream branchpoint for rebase")
2855
2856        # the branchpoint may be p4/foo~3, so strip off the parent
2857        upstream = re.sub("~[0-9]+$", "", upstream)
2858
2859        print "Rebasing the current branch onto %s" % upstream
2860        oldHead = read_pipe("git rev-parse HEAD").strip()
2861        system("git rebase %s" % upstream)
2862        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
2863        return True
2864
2865class P4Clone(P4Sync):
2866    def __init__(self):
2867        P4Sync.__init__(self)
2868        self.description = "Creates a new git repository and imports from Perforce into it"
2869        self.usage = "usage: %prog [options] //depot/path[@revRange]"
2870        self.options += [
2871            optparse.make_option("--destination", dest="cloneDestination",
2872                                 action='store', default=None,
2873                                 help="where to leave result of the clone"),
2874            optparse.make_option("-/", dest="cloneExclude",
2875                                 action="append", type="string",
2876                                 help="exclude depot path"),
2877            optparse.make_option("--bare", dest="cloneBare",
2878                                 action="store_true", default=False),
2879        ]
2880        self.cloneDestination = None
2881        self.needsGit = False
2882        self.cloneBare = False
2883
2884    # This is required for the "append" cloneExclude action
2885    def ensure_value(self, attr, value):
2886        if not hasattr(self, attr) or getattr(self, attr) is None:
2887            setattr(self, attr, value)
2888        return getattr(self, attr)
2889
2890    def defaultDestination(self, args):
2891        ## TODO: use common prefix of args?
2892        depotPath = args[0]
2893        depotDir = re.sub("(@[^@]*)$", "", depotPath)
2894        depotDir = re.sub("(#[^#]*)$", "", depotDir)
2895        depotDir = re.sub(r"\.\.\.$", "", depotDir)
2896        depotDir = re.sub(r"/$", "", depotDir)
2897        return os.path.split(depotDir)[1]
2898
2899    def run(self, args):
2900        if len(args) < 1:
2901            return False
2902
2903        if self.keepRepoPath and not self.cloneDestination:
2904            sys.stderr.write("Must specify destination for --keep-path\n")
2905            sys.exit(1)
2906
2907        depotPaths = args
2908
2909        if not self.cloneDestination and len(depotPaths) > 1:
2910            self.cloneDestination = depotPaths[-1]
2911            depotPaths = depotPaths[:-1]
2912
2913        self.cloneExclude = ["/"+p for p in self.cloneExclude]
2914        for p in depotPaths:
2915            if not p.startswith("//"):
2916                return False
2917
2918        if not self.cloneDestination:
2919            self.cloneDestination = self.defaultDestination(args)
2920
2921        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2922
2923        if not os.path.exists(self.cloneDestination):
2924            os.makedirs(self.cloneDestination)
2925        chdir(self.cloneDestination)
2926
2927        init_cmd = [ "git", "init" ]
2928        if self.cloneBare:
2929            init_cmd.append("--bare")
2930        subprocess.check_call(init_cmd)
2931
2932        if not P4Sync.run(self, depotPaths):
2933            return False
2934        if self.branch != "master":
2935            if self.importIntoRemotes:
2936                masterbranch = "refs/remotes/p4/master"
2937            else:
2938                masterbranch = "refs/heads/p4/master"
2939            if gitBranchExists(masterbranch):
2940                system("git branch master %s" % masterbranch)
2941                if not self.cloneBare:
2942                    system("git checkout -f")
2943            else:
2944                print "Could not detect main branch. No checkout/master branch created."
2945
2946        # auto-set this variable if invoked with --use-client-spec
2947        if self.useClientSpec_from_options:
2948            system("git config --bool git-p4.useclientspec true")
2949
2950        return True
2951
2952class P4Branches(Command):
2953    def __init__(self):
2954        Command.__init__(self)
2955        self.options = [ ]
2956        self.description = ("Shows the git branches that hold imports and their "
2957                            + "corresponding perforce depot paths")
2958        self.verbose = False
2959
2960    def run(self, args):
2961        if originP4BranchesExist():
2962            createOrUpdateBranchesFromOrigin()
2963
2964        cmdline = "git rev-parse --symbolic "
2965        cmdline += " --remotes"
2966
2967        for line in read_pipe_lines(cmdline):
2968            line = line.strip()
2969
2970            if not line.startswith('p4/') or line == "p4/HEAD":
2971                continue
2972            branch = line
2973
2974            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2975            settings = extractSettingsGitLog(log)
2976
2977            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2978        return True
2979
2980class HelpFormatter(optparse.IndentedHelpFormatter):
2981    def __init__(self):
2982        optparse.IndentedHelpFormatter.__init__(self)
2983
2984    def format_description(self, description):
2985        if description:
2986            return description + "\n"
2987        else:
2988            return ""
2989
2990def printUsage(commands):
2991    print "usage: %s <command> [options]" % sys.argv[0]
2992    print ""
2993    print "valid commands: %s" % ", ".join(commands)
2994    print ""
2995    print "Try %s <command> --help for command specific help." % sys.argv[0]
2996    print ""
2997
2998commands = {
2999    "debug" : P4Debug,
3000    "submit" : P4Submit,
3001    "commit" : P4Submit,
3002    "sync" : P4Sync,
3003    "rebase" : P4Rebase,
3004    "clone" : P4Clone,
3005    "rollback" : P4RollBack,
3006    "branches" : P4Branches
3007}
3008
3009
3010def main():
3011    if len(sys.argv[1:]) == 0:
3012        printUsage(commands.keys())
3013        sys.exit(2)
3014
3015    cmd = ""
3016    cmdName = sys.argv[1]
3017    try:
3018        klass = commands[cmdName]
3019        cmd = klass()
3020    except KeyError:
3021        print "unknown command %s" % cmdName
3022        print ""
3023        printUsage(commands.keys())
3024        sys.exit(2)
3025
3026    options = cmd.options
3027    cmd.gitdir = os.environ.get("GIT_DIR", None)
3028
3029    args = sys.argv[2:]
3030
3031    options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3032    if cmd.needsGit:
3033        options.append(optparse.make_option("--git-dir", dest="gitdir"))
3034
3035    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3036                                   options,
3037                                   description = cmd.description,
3038                                   formatter = HelpFormatter())
3039
3040    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3041    global verbose
3042    verbose = cmd.verbose
3043    if cmd.needsGit:
3044        if cmd.gitdir == None:
3045            cmd.gitdir = os.path.abspath(".git")
3046            if not isValidGitDir(cmd.gitdir):
3047                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3048                if os.path.exists(cmd.gitdir):
3049                    cdup = read_pipe("git rev-parse --show-cdup").strip()
3050                    if len(cdup) > 0:
3051                        chdir(cdup);
3052
3053        if not isValidGitDir(cmd.gitdir):
3054            if isValidGitDir(cmd.gitdir + "/.git"):
3055                cmd.gitdir += "/.git"
3056            else:
3057                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3058
3059        os.environ["GIT_DIR"] = cmd.gitdir
3060
3061    if not cmd.run(args):
3062        parser.print_help()
3063        sys.exit(2)
3064
3065
3066if __name__ == '__main__':
3067    main()