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