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