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