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