contrib / fast-import / git-p4on commit Merge branch 'nd/resolve-ref' (b7f7c07)
   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
  14
  15verbose = False
  16
  17
  18def p4_build_cmd(cmd):
  19    """Build a suitable p4 command line.
  20
  21    This consolidates building and returning a p4 command line into one
  22    location. It means that hooking into the environment, or other configuration
  23    can be done more easily.
  24    """
  25    real_cmd = ["p4"]
  26
  27    user = gitConfig("git-p4.user")
  28    if len(user) > 0:
  29        real_cmd += ["-u",user]
  30
  31    password = gitConfig("git-p4.password")
  32    if len(password) > 0:
  33        real_cmd += ["-P", password]
  34
  35    port = gitConfig("git-p4.port")
  36    if len(port) > 0:
  37        real_cmd += ["-p", port]
  38
  39    host = gitConfig("git-p4.host")
  40    if len(host) > 0:
  41        real_cmd += ["-h", host]
  42
  43    client = gitConfig("git-p4.client")
  44    if len(client) > 0:
  45        real_cmd += ["-c", client]
  46
  47
  48    if isinstance(cmd,basestring):
  49        real_cmd = ' '.join(real_cmd) + ' ' + cmd
  50    else:
  51        real_cmd += cmd
  52    return real_cmd
  53
  54def chdir(dir):
  55    # P4 uses the PWD environment variable rather than getcwd(). Since we're
  56    # not using the shell, we have to set it ourselves.
  57    os.environ['PWD']=dir
  58    os.chdir(dir)
  59
  60def die(msg):
  61    if verbose:
  62        raise Exception(msg)
  63    else:
  64        sys.stderr.write(msg + "\n")
  65        sys.exit(1)
  66
  67def write_pipe(c, stdin):
  68    if verbose:
  69        sys.stderr.write('Writing pipe: %s\n' % str(c))
  70
  71    expand = isinstance(c,basestring)
  72    p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
  73    pipe = p.stdin
  74    val = pipe.write(stdin)
  75    pipe.close()
  76    if p.wait():
  77        die('Command failed: %s' % str(c))
  78
  79    return val
  80
  81def p4_write_pipe(c, stdin):
  82    real_cmd = p4_build_cmd(c)
  83    return write_pipe(real_cmd, stdin)
  84
  85def read_pipe(c, ignore_error=False):
  86    if verbose:
  87        sys.stderr.write('Reading pipe: %s\n' % str(c))
  88
  89    expand = isinstance(c,basestring)
  90    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
  91    pipe = p.stdout
  92    val = pipe.read()
  93    if p.wait() and not ignore_error:
  94        die('Command failed: %s' % str(c))
  95
  96    return val
  97
  98def p4_read_pipe(c, ignore_error=False):
  99    real_cmd = p4_build_cmd(c)
 100    return read_pipe(real_cmd, ignore_error)
 101
 102def read_pipe_lines(c):
 103    if verbose:
 104        sys.stderr.write('Reading pipe: %s\n' % str(c))
 105
 106    expand = isinstance(c, basestring)
 107    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
 108    pipe = p.stdout
 109    val = pipe.readlines()
 110    if pipe.close() or p.wait():
 111        die('Command failed: %s' % str(c))
 112
 113    return val
 114
 115def p4_read_pipe_lines(c):
 116    """Specifically invoke p4 on the command supplied. """
 117    real_cmd = p4_build_cmd(c)
 118    return read_pipe_lines(real_cmd)
 119
 120def system(cmd):
 121    expand = isinstance(cmd,basestring)
 122    if verbose:
 123        sys.stderr.write("executing %s\n" % str(cmd))
 124    subprocess.check_call(cmd, shell=expand)
 125
 126def p4_system(cmd):
 127    """Specifically invoke p4 as the system command. """
 128    real_cmd = p4_build_cmd(cmd)
 129    expand = isinstance(real_cmd, basestring)
 130    subprocess.check_call(real_cmd, shell=expand)
 131
 132def p4_integrate(src, dest):
 133    p4_system(["integrate", "-Dt", src, dest])
 134
 135def p4_sync(path):
 136    p4_system(["sync", path])
 137
 138def p4_add(f):
 139    p4_system(["add", f])
 140
 141def p4_delete(f):
 142    p4_system(["delete", f])
 143
 144def p4_edit(f):
 145    p4_system(["edit", f])
 146
 147def p4_revert(f):
 148    p4_system(["revert", f])
 149
 150def p4_reopen(type, file):
 151    p4_system(["reopen", "-t", type, file])
 152
 153#
 154# Canonicalize the p4 type and return a tuple of the
 155# base type, plus any modifiers.  See "p4 help filetypes"
 156# for a list and explanation.
 157#
 158def split_p4_type(p4type):
 159
 160    p4_filetypes_historical = {
 161        "ctempobj": "binary+Sw",
 162        "ctext": "text+C",
 163        "cxtext": "text+Cx",
 164        "ktext": "text+k",
 165        "kxtext": "text+kx",
 166        "ltext": "text+F",
 167        "tempobj": "binary+FSw",
 168        "ubinary": "binary+F",
 169        "uresource": "resource+F",
 170        "uxbinary": "binary+Fx",
 171        "xbinary": "binary+x",
 172        "xltext": "text+Fx",
 173        "xtempobj": "binary+Swx",
 174        "xtext": "text+x",
 175        "xunicode": "unicode+x",
 176        "xutf16": "utf16+x",
 177    }
 178    if p4type in p4_filetypes_historical:
 179        p4type = p4_filetypes_historical[p4type]
 180    mods = ""
 181    s = p4type.split("+")
 182    base = s[0]
 183    mods = ""
 184    if len(s) > 1:
 185        mods = s[1]
 186    return (base, mods)
 187
 188
 189def setP4ExecBit(file, mode):
 190    # Reopens an already open file and changes the execute bit to match
 191    # the execute bit setting in the passed in mode.
 192
 193    p4Type = "+x"
 194
 195    if not isModeExec(mode):
 196        p4Type = getP4OpenedType(file)
 197        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
 198        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
 199        if p4Type[-1] == "+":
 200            p4Type = p4Type[0:-1]
 201
 202    p4_reopen(p4Type, file)
 203
 204def getP4OpenedType(file):
 205    # Returns the perforce file type for the given file.
 206
 207    result = p4_read_pipe(["opened", file])
 208    match = re.match(".*\((.+)\)\r?$", result)
 209    if match:
 210        return match.group(1)
 211    else:
 212        die("Could not determine file type for %s (result: '%s')" % (file, result))
 213
 214def diffTreePattern():
 215    # This is a simple generator for the diff tree regex pattern. This could be
 216    # a class variable if this and parseDiffTreeEntry were a part of a class.
 217    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
 218    while True:
 219        yield pattern
 220
 221def parseDiffTreeEntry(entry):
 222    """Parses a single diff tree entry into its component elements.
 223
 224    See git-diff-tree(1) manpage for details about the format of the diff
 225    output. This method returns a dictionary with the following elements:
 226
 227    src_mode - The mode of the source file
 228    dst_mode - The mode of the destination file
 229    src_sha1 - The sha1 for the source file
 230    dst_sha1 - The sha1 fr the destination file
 231    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
 232    status_score - The score for the status (applicable for 'C' and 'R'
 233                   statuses). This is None if there is no score.
 234    src - The path for the source file.
 235    dst - The path for the destination file. This is only present for
 236          copy or renames. If it is not present, this is None.
 237
 238    If the pattern is not matched, None is returned."""
 239
 240    match = diffTreePattern().next().match(entry)
 241    if match:
 242        return {
 243            'src_mode': match.group(1),
 244            'dst_mode': match.group(2),
 245            'src_sha1': match.group(3),
 246            'dst_sha1': match.group(4),
 247            'status': match.group(5),
 248            'status_score': match.group(6),
 249            'src': match.group(7),
 250            'dst': match.group(10)
 251        }
 252    return None
 253
 254def isModeExec(mode):
 255    # Returns True if the given git mode represents an executable file,
 256    # otherwise False.
 257    return mode[-3:] == "755"
 258
 259def isModeExecChanged(src_mode, dst_mode):
 260    return isModeExec(src_mode) != isModeExec(dst_mode)
 261
 262def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
 263
 264    if isinstance(cmd,basestring):
 265        cmd = "-G " + cmd
 266        expand = True
 267    else:
 268        cmd = ["-G"] + cmd
 269        expand = False
 270
 271    cmd = p4_build_cmd(cmd)
 272    if verbose:
 273        sys.stderr.write("Opening pipe: %s\n" % str(cmd))
 274
 275    # Use a temporary file to avoid deadlocks without
 276    # subprocess.communicate(), which would put another copy
 277    # of stdout into memory.
 278    stdin_file = None
 279    if stdin is not None:
 280        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
 281        if isinstance(stdin,basestring):
 282            stdin_file.write(stdin)
 283        else:
 284            for i in stdin:
 285                stdin_file.write(i + '\n')
 286        stdin_file.flush()
 287        stdin_file.seek(0)
 288
 289    p4 = subprocess.Popen(cmd,
 290                          shell=expand,
 291                          stdin=stdin_file,
 292                          stdout=subprocess.PIPE)
 293
 294    result = []
 295    try:
 296        while True:
 297            entry = marshal.load(p4.stdout)
 298            if cb is not None:
 299                cb(entry)
 300            else:
 301                result.append(entry)
 302    except EOFError:
 303        pass
 304    exitCode = p4.wait()
 305    if exitCode != 0:
 306        entry = {}
 307        entry["p4ExitCode"] = exitCode
 308        result.append(entry)
 309
 310    return result
 311
 312def p4Cmd(cmd):
 313    list = p4CmdList(cmd)
 314    result = {}
 315    for entry in list:
 316        result.update(entry)
 317    return result;
 318
 319def p4Where(depotPath):
 320    if not depotPath.endswith("/"):
 321        depotPath += "/"
 322    depotPath = depotPath + "..."
 323    outputList = p4CmdList(["where", depotPath])
 324    output = None
 325    for entry in outputList:
 326        if "depotFile" in entry:
 327            if entry["depotFile"] == depotPath:
 328                output = entry
 329                break
 330        elif "data" in entry:
 331            data = entry.get("data")
 332            space = data.find(" ")
 333            if data[:space] == depotPath:
 334                output = entry
 335                break
 336    if output == None:
 337        return ""
 338    if output["code"] == "error":
 339        return ""
 340    clientPath = ""
 341    if "path" in output:
 342        clientPath = output.get("path")
 343    elif "data" in output:
 344        data = output.get("data")
 345        lastSpace = data.rfind(" ")
 346        clientPath = data[lastSpace + 1:]
 347
 348    if clientPath.endswith("..."):
 349        clientPath = clientPath[:-3]
 350    return clientPath
 351
 352def currentGitBranch():
 353    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
 354
 355def isValidGitDir(path):
 356    if (os.path.exists(path + "/HEAD")
 357        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
 358        return True;
 359    return False
 360
 361def parseRevision(ref):
 362    return read_pipe("git rev-parse %s" % ref).strip()
 363
 364def extractLogMessageFromGitCommit(commit):
 365    logMessage = ""
 366
 367    ## fixme: title is first line of commit, not 1st paragraph.
 368    foundTitle = False
 369    for log in read_pipe_lines("git cat-file commit %s" % commit):
 370       if not foundTitle:
 371           if len(log) == 1:
 372               foundTitle = True
 373           continue
 374
 375       logMessage += log
 376    return logMessage
 377
 378def extractSettingsGitLog(log):
 379    values = {}
 380    for line in log.split("\n"):
 381        line = line.strip()
 382        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
 383        if not m:
 384            continue
 385
 386        assignments = m.group(1).split (':')
 387        for a in assignments:
 388            vals = a.split ('=')
 389            key = vals[0].strip()
 390            val = ('='.join (vals[1:])).strip()
 391            if val.endswith ('\"') and val.startswith('"'):
 392                val = val[1:-1]
 393
 394            values[key] = val
 395
 396    paths = values.get("depot-paths")
 397    if not paths:
 398        paths = values.get("depot-path")
 399    if paths:
 400        values['depot-paths'] = paths.split(',')
 401    return values
 402
 403def gitBranchExists(branch):
 404    proc = subprocess.Popen(["git", "rev-parse", branch],
 405                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
 406    return proc.wait() == 0;
 407
 408_gitConfig = {}
 409def gitConfig(key, args = None): # set args to "--bool", for instance
 410    if not _gitConfig.has_key(key):
 411        argsFilter = ""
 412        if args != None:
 413            argsFilter = "%s " % args
 414        cmd = "git config %s%s" % (argsFilter, key)
 415        _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
 416    return _gitConfig[key]
 417
 418def gitConfigList(key):
 419    if not _gitConfig.has_key(key):
 420        _gitConfig[key] = read_pipe("git config --get-all %s" % key, ignore_error=True).strip().split(os.linesep)
 421    return _gitConfig[key]
 422
 423def p4BranchesInGit(branchesAreInRemotes = True):
 424    branches = {}
 425
 426    cmdline = "git rev-parse --symbolic "
 427    if branchesAreInRemotes:
 428        cmdline += " --remotes"
 429    else:
 430        cmdline += " --branches"
 431
 432    for line in read_pipe_lines(cmdline):
 433        line = line.strip()
 434
 435        ## only import to p4/
 436        if not line.startswith('p4/') or line == "p4/HEAD":
 437            continue
 438        branch = line
 439
 440        # strip off p4
 441        branch = re.sub ("^p4/", "", line)
 442
 443        branches[branch] = parseRevision(line)
 444    return branches
 445
 446def findUpstreamBranchPoint(head = "HEAD"):
 447    branches = p4BranchesInGit()
 448    # map from depot-path to branch name
 449    branchByDepotPath = {}
 450    for branch in branches.keys():
 451        tip = branches[branch]
 452        log = extractLogMessageFromGitCommit(tip)
 453        settings = extractSettingsGitLog(log)
 454        if settings.has_key("depot-paths"):
 455            paths = ",".join(settings["depot-paths"])
 456            branchByDepotPath[paths] = "remotes/p4/" + branch
 457
 458    settings = None
 459    parent = 0
 460    while parent < 65535:
 461        commit = head + "~%s" % parent
 462        log = extractLogMessageFromGitCommit(commit)
 463        settings = extractSettingsGitLog(log)
 464        if settings.has_key("depot-paths"):
 465            paths = ",".join(settings["depot-paths"])
 466            if branchByDepotPath.has_key(paths):
 467                return [branchByDepotPath[paths], settings]
 468
 469        parent = parent + 1
 470
 471    return ["", settings]
 472
 473def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
 474    if not silent:
 475        print ("Creating/updating branch(es) in %s based on origin branch(es)"
 476               % localRefPrefix)
 477
 478    originPrefix = "origin/p4/"
 479
 480    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 481        line = line.strip()
 482        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
 483            continue
 484
 485        headName = line[len(originPrefix):]
 486        remoteHead = localRefPrefix + headName
 487        originHead = line
 488
 489        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
 490        if (not original.has_key('depot-paths')
 491            or not original.has_key('change')):
 492            continue
 493
 494        update = False
 495        if not gitBranchExists(remoteHead):
 496            if verbose:
 497                print "creating %s" % remoteHead
 498            update = True
 499        else:
 500            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
 501            if settings.has_key('change') > 0:
 502                if settings['depot-paths'] == original['depot-paths']:
 503                    originP4Change = int(original['change'])
 504                    p4Change = int(settings['change'])
 505                    if originP4Change > p4Change:
 506                        print ("%s (%s) is newer than %s (%s). "
 507                               "Updating p4 branch from origin."
 508                               % (originHead, originP4Change,
 509                                  remoteHead, p4Change))
 510                        update = True
 511                else:
 512                    print ("Ignoring: %s was imported from %s while "
 513                           "%s was imported from %s"
 514                           % (originHead, ','.join(original['depot-paths']),
 515                              remoteHead, ','.join(settings['depot-paths'])))
 516
 517        if update:
 518            system("git update-ref %s %s" % (remoteHead, originHead))
 519
 520def originP4BranchesExist():
 521        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
 522
 523def p4ChangesForPaths(depotPaths, changeRange):
 524    assert depotPaths
 525    cmd = ['changes']
 526    for p in depotPaths:
 527        cmd += ["%s...%s" % (p, changeRange)]
 528    output = p4_read_pipe_lines(cmd)
 529
 530    changes = {}
 531    for line in output:
 532        changeNum = int(line.split(" ")[1])
 533        changes[changeNum] = True
 534
 535    changelist = changes.keys()
 536    changelist.sort()
 537    return changelist
 538
 539def p4PathStartsWith(path, prefix):
 540    # This method tries to remedy a potential mixed-case issue:
 541    #
 542    # If UserA adds  //depot/DirA/file1
 543    # and UserB adds //depot/dira/file2
 544    #
 545    # we may or may not have a problem. If you have core.ignorecase=true,
 546    # we treat DirA and dira as the same directory
 547    ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
 548    if ignorecase:
 549        return path.lower().startswith(prefix.lower())
 550    return path.startswith(prefix)
 551
 552class Command:
 553    def __init__(self):
 554        self.usage = "usage: %prog [options]"
 555        self.needsGit = True
 556
 557class P4UserMap:
 558    def __init__(self):
 559        self.userMapFromPerforceServer = False
 560
 561    def getUserCacheFilename(self):
 562        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
 563        return home + "/.gitp4-usercache.txt"
 564
 565    def getUserMapFromPerforceServer(self):
 566        if self.userMapFromPerforceServer:
 567            return
 568        self.users = {}
 569        self.emails = {}
 570
 571        for output in p4CmdList("users"):
 572            if not output.has_key("User"):
 573                continue
 574            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
 575            self.emails[output["Email"]] = output["User"]
 576
 577
 578        s = ''
 579        for (key, val) in self.users.items():
 580            s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
 581
 582        open(self.getUserCacheFilename(), "wb").write(s)
 583        self.userMapFromPerforceServer = True
 584
 585    def loadUserMapFromCache(self):
 586        self.users = {}
 587        self.userMapFromPerforceServer = False
 588        try:
 589            cache = open(self.getUserCacheFilename(), "rb")
 590            lines = cache.readlines()
 591            cache.close()
 592            for line in lines:
 593                entry = line.strip().split("\t")
 594                self.users[entry[0]] = entry[1]
 595        except IOError:
 596            self.getUserMapFromPerforceServer()
 597
 598class P4Debug(Command):
 599    def __init__(self):
 600        Command.__init__(self)
 601        self.options = [
 602            optparse.make_option("--verbose", dest="verbose", action="store_true",
 603                                 default=False),
 604            ]
 605        self.description = "A tool to debug the output of p4 -G."
 606        self.needsGit = False
 607        self.verbose = False
 608
 609    def run(self, args):
 610        j = 0
 611        for output in p4CmdList(args):
 612            print 'Element: %d' % j
 613            j += 1
 614            print output
 615        return True
 616
 617class P4RollBack(Command):
 618    def __init__(self):
 619        Command.__init__(self)
 620        self.options = [
 621            optparse.make_option("--verbose", dest="verbose", action="store_true"),
 622            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
 623        ]
 624        self.description = "A tool to debug the multi-branch import. Don't use :)"
 625        self.verbose = False
 626        self.rollbackLocalBranches = False
 627
 628    def run(self, args):
 629        if len(args) != 1:
 630            return False
 631        maxChange = int(args[0])
 632
 633        if "p4ExitCode" in p4Cmd("changes -m 1"):
 634            die("Problems executing p4");
 635
 636        if self.rollbackLocalBranches:
 637            refPrefix = "refs/heads/"
 638            lines = read_pipe_lines("git rev-parse --symbolic --branches")
 639        else:
 640            refPrefix = "refs/remotes/"
 641            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
 642
 643        for line in lines:
 644            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
 645                line = line.strip()
 646                ref = refPrefix + line
 647                log = extractLogMessageFromGitCommit(ref)
 648                settings = extractSettingsGitLog(log)
 649
 650                depotPaths = settings['depot-paths']
 651                change = settings['change']
 652
 653                changed = False
 654
 655                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
 656                                                           for p in depotPaths]))) == 0:
 657                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
 658                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
 659                    continue
 660
 661                while change and int(change) > maxChange:
 662                    changed = True
 663                    if self.verbose:
 664                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
 665                    system("git update-ref %s \"%s^\"" % (ref, ref))
 666                    log = extractLogMessageFromGitCommit(ref)
 667                    settings =  extractSettingsGitLog(log)
 668
 669
 670                    depotPaths = settings['depot-paths']
 671                    change = settings['change']
 672
 673                if changed:
 674                    print "%s rewound to %s" % (ref, change)
 675
 676        return True
 677
 678class P4Submit(Command, P4UserMap):
 679    def __init__(self):
 680        Command.__init__(self)
 681        P4UserMap.__init__(self)
 682        self.options = [
 683                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 684                optparse.make_option("--origin", dest="origin"),
 685                optparse.make_option("-M", dest="detectRenames", action="store_true"),
 686                # preserve the user, requires relevant p4 permissions
 687                optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
 688        ]
 689        self.description = "Submit changes from git to the perforce depot."
 690        self.usage += " [name of git branch to submit into perforce depot]"
 691        self.interactive = True
 692        self.origin = ""
 693        self.detectRenames = False
 694        self.verbose = False
 695        self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
 696        self.isWindows = (platform.system() == "Windows")
 697        self.myP4UserId = None
 698
 699    def check(self):
 700        if len(p4CmdList("opened ...")) > 0:
 701            die("You have files opened with perforce! Close them before starting the sync.")
 702
 703    # replaces everything between 'Description:' and the next P4 submit template field with the
 704    # commit message
 705    def prepareLogMessage(self, template, message):
 706        result = ""
 707
 708        inDescriptionSection = False
 709
 710        for line in template.split("\n"):
 711            if line.startswith("#"):
 712                result += line + "\n"
 713                continue
 714
 715            if inDescriptionSection:
 716                if line.startswith("Files:") or line.startswith("Jobs:"):
 717                    inDescriptionSection = False
 718                else:
 719                    continue
 720            else:
 721                if line.startswith("Description:"):
 722                    inDescriptionSection = True
 723                    line += "\n"
 724                    for messageLine in message.split("\n"):
 725                        line += "\t" + messageLine + "\n"
 726
 727            result += line + "\n"
 728
 729        return result
 730
 731    def p4UserForCommit(self,id):
 732        # Return the tuple (perforce user,git email) for a given git commit id
 733        self.getUserMapFromPerforceServer()
 734        gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
 735        gitEmail = gitEmail.strip()
 736        if not self.emails.has_key(gitEmail):
 737            return (None,gitEmail)
 738        else:
 739            return (self.emails[gitEmail],gitEmail)
 740
 741    def checkValidP4Users(self,commits):
 742        # check if any git authors cannot be mapped to p4 users
 743        for id in commits:
 744            (user,email) = self.p4UserForCommit(id)
 745            if not user:
 746                msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
 747                if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
 748                    print "%s" % msg
 749                else:
 750                    die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
 751
 752    def lastP4Changelist(self):
 753        # Get back the last changelist number submitted in this client spec. This
 754        # then gets used to patch up the username in the change. If the same
 755        # client spec is being used by multiple processes then this might go
 756        # wrong.
 757        results = p4CmdList("client -o")        # find the current client
 758        client = None
 759        for r in results:
 760            if r.has_key('Client'):
 761                client = r['Client']
 762                break
 763        if not client:
 764            die("could not get client spec")
 765        results = p4CmdList(["changes", "-c", client, "-m", "1"])
 766        for r in results:
 767            if r.has_key('change'):
 768                return r['change']
 769        die("Could not get changelist number for last submit - cannot patch up user details")
 770
 771    def modifyChangelistUser(self, changelist, newUser):
 772        # fixup the user field of a changelist after it has been submitted.
 773        changes = p4CmdList("change -o %s" % changelist)
 774        if len(changes) != 1:
 775            die("Bad output from p4 change modifying %s to user %s" %
 776                (changelist, newUser))
 777
 778        c = changes[0]
 779        if c['User'] == newUser: return   # nothing to do
 780        c['User'] = newUser
 781        input = marshal.dumps(c)
 782
 783        result = p4CmdList("change -f -i", stdin=input)
 784        for r in result:
 785            if r.has_key('code'):
 786                if r['code'] == 'error':
 787                    die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
 788            if r.has_key('data'):
 789                print("Updated user field for changelist %s to %s" % (changelist, newUser))
 790                return
 791        die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
 792
 793    def canChangeChangelists(self):
 794        # check to see if we have p4 admin or super-user permissions, either of
 795        # which are required to modify changelists.
 796        results = p4CmdList("protects %s" % self.depotPath)
 797        for r in results:
 798            if r.has_key('perm'):
 799                if r['perm'] == 'admin':
 800                    return 1
 801                if r['perm'] == 'super':
 802                    return 1
 803        return 0
 804
 805    def p4UserId(self):
 806        if self.myP4UserId:
 807            return self.myP4UserId
 808
 809        results = p4CmdList("user -o")
 810        for r in results:
 811            if r.has_key('User'):
 812                self.myP4UserId = r['User']
 813                return r['User']
 814        die("Could not find your p4 user id")
 815
 816    def p4UserIsMe(self, p4User):
 817        # return True if the given p4 user is actually me
 818        me = self.p4UserId()
 819        if not p4User or p4User != me:
 820            return False
 821        else:
 822            return True
 823
 824    def prepareSubmitTemplate(self):
 825        # remove lines in the Files section that show changes to files outside the depot path we're committing into
 826        template = ""
 827        inFilesSection = False
 828        for line in p4_read_pipe_lines(['change', '-o']):
 829            if line.endswith("\r\n"):
 830                line = line[:-2] + "\n"
 831            if inFilesSection:
 832                if line.startswith("\t"):
 833                    # path starts and ends with a tab
 834                    path = line[1:]
 835                    lastTab = path.rfind("\t")
 836                    if lastTab != -1:
 837                        path = path[:lastTab]
 838                        if not p4PathStartsWith(path, self.depotPath):
 839                            continue
 840                else:
 841                    inFilesSection = False
 842            else:
 843                if line.startswith("Files:"):
 844                    inFilesSection = True
 845
 846            template += line
 847
 848        return template
 849
 850    def edit_template(self, template_file):
 851        """Invoke the editor to let the user change the submission
 852           message.  Return true if okay to continue with the submit."""
 853
 854        # if configured to skip the editing part, just submit
 855        if gitConfig("git-p4.skipSubmitEdit") == "true":
 856            return True
 857
 858        # look at the modification time, to check later if the user saved
 859        # the file
 860        mtime = os.stat(template_file).st_mtime
 861
 862        # invoke the editor
 863        if os.environ.has_key("P4EDITOR"):
 864            editor = os.environ.get("P4EDITOR")
 865        else:
 866            editor = read_pipe("git var GIT_EDITOR").strip()
 867        system(editor + " " + template_file)
 868
 869        # If the file was not saved, prompt to see if this patch should
 870        # be skipped.  But skip this verification step if configured so.
 871        if gitConfig("git-p4.skipSubmitEditCheck") == "true":
 872            return True
 873
 874        if os.stat(template_file).st_mtime <= mtime:
 875            while True:
 876                response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
 877                if response == 'y':
 878                    return True
 879                if response == 'n':
 880                    return False
 881
 882    def applyCommit(self, id):
 883        print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
 884
 885        (p4User, gitEmail) = self.p4UserForCommit(id)
 886
 887        if not self.detectRenames:
 888            # If not explicitly set check the config variable
 889            self.detectRenames = gitConfig("git-p4.detectRenames")
 890
 891        if self.detectRenames.lower() == "false" or self.detectRenames == "":
 892            diffOpts = ""
 893        elif self.detectRenames.lower() == "true":
 894            diffOpts = "-M"
 895        else:
 896            diffOpts = "-M%s" % self.detectRenames
 897
 898        detectCopies = gitConfig("git-p4.detectCopies")
 899        if detectCopies.lower() == "true":
 900            diffOpts += " -C"
 901        elif detectCopies != "" and detectCopies.lower() != "false":
 902            diffOpts += " -C%s" % detectCopies
 903
 904        if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
 905            diffOpts += " --find-copies-harder"
 906
 907        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
 908        filesToAdd = set()
 909        filesToDelete = set()
 910        editedFiles = set()
 911        filesToChangeExecBit = {}
 912        for line in diff:
 913            diff = parseDiffTreeEntry(line)
 914            modifier = diff['status']
 915            path = diff['src']
 916            if modifier == "M":
 917                p4_edit(path)
 918                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 919                    filesToChangeExecBit[path] = diff['dst_mode']
 920                editedFiles.add(path)
 921            elif modifier == "A":
 922                filesToAdd.add(path)
 923                filesToChangeExecBit[path] = diff['dst_mode']
 924                if path in filesToDelete:
 925                    filesToDelete.remove(path)
 926            elif modifier == "D":
 927                filesToDelete.add(path)
 928                if path in filesToAdd:
 929                    filesToAdd.remove(path)
 930            elif modifier == "C":
 931                src, dest = diff['src'], diff['dst']
 932                p4_integrate(src, dest)
 933                if diff['src_sha1'] != diff['dst_sha1']:
 934                    p4_edit(dest)
 935                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 936                    p4_edit(dest)
 937                    filesToChangeExecBit[dest] = diff['dst_mode']
 938                os.unlink(dest)
 939                editedFiles.add(dest)
 940            elif modifier == "R":
 941                src, dest = diff['src'], diff['dst']
 942                p4_integrate(src, dest)
 943                if diff['src_sha1'] != diff['dst_sha1']:
 944                    p4_edit(dest)
 945                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 946                    p4_edit(dest)
 947                    filesToChangeExecBit[dest] = diff['dst_mode']
 948                os.unlink(dest)
 949                editedFiles.add(dest)
 950                filesToDelete.add(src)
 951            else:
 952                die("unknown modifier %s for %s" % (modifier, path))
 953
 954        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
 955        patchcmd = diffcmd + " | git apply "
 956        tryPatchCmd = patchcmd + "--check -"
 957        applyPatchCmd = patchcmd + "--check --apply -"
 958
 959        if os.system(tryPatchCmd) != 0:
 960            print "Unfortunately applying the change failed!"
 961            print "What do you want to do?"
 962            response = "x"
 963            while response != "s" and response != "a" and response != "w":
 964                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
 965                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
 966            if response == "s":
 967                print "Skipping! Good luck with the next patches..."
 968                for f in editedFiles:
 969                    p4_revert(f)
 970                for f in filesToAdd:
 971                    os.remove(f)
 972                return
 973            elif response == "a":
 974                os.system(applyPatchCmd)
 975                if len(filesToAdd) > 0:
 976                    print "You may also want to call p4 add on the following files:"
 977                    print " ".join(filesToAdd)
 978                if len(filesToDelete):
 979                    print "The following files should be scheduled for deletion with p4 delete:"
 980                    print " ".join(filesToDelete)
 981                die("Please resolve and submit the conflict manually and "
 982                    + "continue afterwards with git-p4 submit --continue")
 983            elif response == "w":
 984                system(diffcmd + " > patch.txt")
 985                print "Patch saved to patch.txt in %s !" % self.clientPath
 986                die("Please resolve and submit the conflict manually and "
 987                    "continue afterwards with git-p4 submit --continue")
 988
 989        system(applyPatchCmd)
 990
 991        for f in filesToAdd:
 992            p4_add(f)
 993        for f in filesToDelete:
 994            p4_revert(f)
 995            p4_delete(f)
 996
 997        # Set/clear executable bits
 998        for f in filesToChangeExecBit.keys():
 999            mode = filesToChangeExecBit[f]
1000            setP4ExecBit(f, mode)
1001
1002        logMessage = extractLogMessageFromGitCommit(id)
1003        logMessage = logMessage.strip()
1004
1005        template = self.prepareSubmitTemplate()
1006
1007        if self.interactive:
1008            submitTemplate = self.prepareLogMessage(template, logMessage)
1009
1010            if self.preserveUser:
1011               submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
1012
1013            if os.environ.has_key("P4DIFF"):
1014                del(os.environ["P4DIFF"])
1015            diff = ""
1016            for editedFile in editedFiles:
1017                diff += p4_read_pipe(['diff', '-du', editedFile])
1018
1019            newdiff = ""
1020            for newFile in filesToAdd:
1021                newdiff += "==== new file ====\n"
1022                newdiff += "--- /dev/null\n"
1023                newdiff += "+++ %s\n" % newFile
1024                f = open(newFile, "r")
1025                for line in f.readlines():
1026                    newdiff += "+" + line
1027                f.close()
1028
1029            if self.checkAuthorship and not self.p4UserIsMe(p4User):
1030                submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1031                submitTemplate += "######## Use git-p4 option --preserve-user to modify authorship\n"
1032                submitTemplate += "######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n"
1033
1034            separatorLine = "######## everything below this line is just the diff #######\n"
1035
1036            (handle, fileName) = tempfile.mkstemp()
1037            tmpFile = os.fdopen(handle, "w+")
1038            if self.isWindows:
1039                submitTemplate = submitTemplate.replace("\n", "\r\n")
1040                separatorLine = separatorLine.replace("\n", "\r\n")
1041                newdiff = newdiff.replace("\n", "\r\n")
1042            tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
1043            tmpFile.close()
1044
1045            if self.edit_template(fileName):
1046                # read the edited message and submit
1047                tmpFile = open(fileName, "rb")
1048                message = tmpFile.read()
1049                tmpFile.close()
1050                submitTemplate = message[:message.index(separatorLine)]
1051                if self.isWindows:
1052                    submitTemplate = submitTemplate.replace("\r\n", "\n")
1053                p4_write_pipe(['submit', '-i'], submitTemplate)
1054
1055                if self.preserveUser:
1056                    if p4User:
1057                        # Get last changelist number. Cannot easily get it from
1058                        # the submit command output as the output is
1059                        # unmarshalled.
1060                        changelist = self.lastP4Changelist()
1061                        self.modifyChangelistUser(changelist, p4User)
1062            else:
1063                # skip this patch
1064                for f in editedFiles:
1065                    p4_revert(f)
1066                for f in filesToAdd:
1067                    p4_revert(f)
1068                    os.remove(f)
1069
1070            os.remove(fileName)
1071        else:
1072            fileName = "submit.txt"
1073            file = open(fileName, "w+")
1074            file.write(self.prepareLogMessage(template, logMessage))
1075            file.close()
1076            print ("Perforce submit template written as %s. "
1077                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
1078                   % (fileName, fileName))
1079
1080    def run(self, args):
1081        if len(args) == 0:
1082            self.master = currentGitBranch()
1083            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1084                die("Detecting current git branch failed!")
1085        elif len(args) == 1:
1086            self.master = args[0]
1087        else:
1088            return False
1089
1090        allowSubmit = gitConfig("git-p4.allowSubmit")
1091        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1092            die("%s is not in git-p4.allowSubmit" % self.master)
1093
1094        [upstream, settings] = findUpstreamBranchPoint()
1095        self.depotPath = settings['depot-paths'][0]
1096        if len(self.origin) == 0:
1097            self.origin = upstream
1098
1099        if self.preserveUser:
1100            if not self.canChangeChangelists():
1101                die("Cannot preserve user names without p4 super-user or admin permissions")
1102
1103        if self.verbose:
1104            print "Origin branch is " + self.origin
1105
1106        if len(self.depotPath) == 0:
1107            print "Internal error: cannot locate perforce depot path from existing branches"
1108            sys.exit(128)
1109
1110        self.clientPath = p4Where(self.depotPath)
1111
1112        if len(self.clientPath) == 0:
1113            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
1114            sys.exit(128)
1115
1116        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1117        self.oldWorkingDirectory = os.getcwd()
1118
1119        chdir(self.clientPath)
1120        print "Synchronizing p4 checkout..."
1121        p4_sync("...")
1122        self.check()
1123
1124        commits = []
1125        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1126            commits.append(line.strip())
1127        commits.reverse()
1128
1129        if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1130            self.checkAuthorship = False
1131        else:
1132            self.checkAuthorship = True
1133
1134        if self.preserveUser:
1135            self.checkValidP4Users(commits)
1136
1137        while len(commits) > 0:
1138            commit = commits[0]
1139            commits = commits[1:]
1140            self.applyCommit(commit)
1141            if not self.interactive:
1142                break
1143
1144        if len(commits) == 0:
1145            print "All changes applied!"
1146            chdir(self.oldWorkingDirectory)
1147
1148            sync = P4Sync()
1149            sync.run([])
1150
1151            rebase = P4Rebase()
1152            rebase.rebase()
1153
1154        return True
1155
1156class P4Sync(Command, P4UserMap):
1157    delete_actions = ( "delete", "move/delete", "purge" )
1158
1159    def __init__(self):
1160        Command.__init__(self)
1161        P4UserMap.__init__(self)
1162        self.options = [
1163                optparse.make_option("--branch", dest="branch"),
1164                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1165                optparse.make_option("--changesfile", dest="changesFile"),
1166                optparse.make_option("--silent", dest="silent", action="store_true"),
1167                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1168                optparse.make_option("--verbose", dest="verbose", action="store_true"),
1169                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1170                                     help="Import into refs/heads/ , not refs/remotes"),
1171                optparse.make_option("--max-changes", dest="maxChanges"),
1172                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1173                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1174                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1175                                     help="Only sync files that are included in the Perforce Client Spec")
1176        ]
1177        self.description = """Imports from Perforce into a git repository.\n
1178    example:
1179    //depot/my/project/ -- to import the current head
1180    //depot/my/project/@all -- to import everything
1181    //depot/my/project/@1,6 -- to import only from revision 1 to 6
1182
1183    (a ... is not needed in the path p4 specification, it's added implicitly)"""
1184
1185        self.usage += " //depot/path[@revRange]"
1186        self.silent = False
1187        self.createdBranches = set()
1188        self.committedChanges = set()
1189        self.branch = ""
1190        self.detectBranches = False
1191        self.detectLabels = False
1192        self.changesFile = ""
1193        self.syncWithOrigin = True
1194        self.verbose = False
1195        self.importIntoRemotes = True
1196        self.maxChanges = ""
1197        self.isWindows = (platform.system() == "Windows")
1198        self.keepRepoPath = False
1199        self.depotPaths = None
1200        self.p4BranchesInGit = []
1201        self.cloneExclude = []
1202        self.useClientSpec = False
1203        self.clientSpecDirs = []
1204
1205        if gitConfig("git-p4.syncFromOrigin") == "false":
1206            self.syncWithOrigin = False
1207
1208    #
1209    # P4 wildcards are not allowed in filenames.  P4 complains
1210    # if you simply add them, but you can force it with "-f", in
1211    # which case it translates them into %xx encoding internally.
1212    # Search for and fix just these four characters.  Do % last so
1213    # that fixing it does not inadvertently create new %-escapes.
1214    #
1215    def wildcard_decode(self, path):
1216        # Cannot have * in a filename in windows; untested as to
1217        # what p4 would do in such a case.
1218        if not self.isWindows:
1219            path = path.replace("%2A", "*")
1220        path = path.replace("%23", "#") \
1221                   .replace("%40", "@") \
1222                   .replace("%25", "%")
1223        return path
1224
1225    def extractFilesFromCommit(self, commit):
1226        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1227                             for path in self.cloneExclude]
1228        files = []
1229        fnum = 0
1230        while commit.has_key("depotFile%s" % fnum):
1231            path =  commit["depotFile%s" % fnum]
1232
1233            if [p for p in self.cloneExclude
1234                if p4PathStartsWith(path, p)]:
1235                found = False
1236            else:
1237                found = [p for p in self.depotPaths
1238                         if p4PathStartsWith(path, p)]
1239            if not found:
1240                fnum = fnum + 1
1241                continue
1242
1243            file = {}
1244            file["path"] = path
1245            file["rev"] = commit["rev%s" % fnum]
1246            file["action"] = commit["action%s" % fnum]
1247            file["type"] = commit["type%s" % fnum]
1248            files.append(file)
1249            fnum = fnum + 1
1250        return files
1251
1252    def stripRepoPath(self, path, prefixes):
1253        if self.useClientSpec:
1254
1255            # if using the client spec, we use the output directory
1256            # specified in the client.  For example, a view
1257            #   //depot/foo/branch/... //client/branch/foo/...
1258            # will end up putting all foo/branch files into
1259            #  branch/foo/
1260            for val in self.clientSpecDirs:
1261                if path.startswith(val[0]):
1262                    # replace the depot path with the client path
1263                    path = path.replace(val[0], val[1][1])
1264                    # now strip out the client (//client/...)
1265                    path = re.sub("^(//[^/]+/)", '', path)
1266                    # the rest is all path
1267                    return path
1268
1269        if self.keepRepoPath:
1270            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1271
1272        for p in prefixes:
1273            if p4PathStartsWith(path, p):
1274                path = path[len(p):]
1275
1276        return path
1277
1278    def splitFilesIntoBranches(self, commit):
1279        branches = {}
1280        fnum = 0
1281        while commit.has_key("depotFile%s" % fnum):
1282            path =  commit["depotFile%s" % fnum]
1283            found = [p for p in self.depotPaths
1284                     if p4PathStartsWith(path, p)]
1285            if not found:
1286                fnum = fnum + 1
1287                continue
1288
1289            file = {}
1290            file["path"] = path
1291            file["rev"] = commit["rev%s" % fnum]
1292            file["action"] = commit["action%s" % fnum]
1293            file["type"] = commit["type%s" % fnum]
1294            fnum = fnum + 1
1295
1296            relPath = self.stripRepoPath(path, self.depotPaths)
1297
1298            for branch in self.knownBranches.keys():
1299
1300                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1301                if relPath.startswith(branch + "/"):
1302                    if branch not in branches:
1303                        branches[branch] = []
1304                    branches[branch].append(file)
1305                    break
1306
1307        return branches
1308
1309    # output one file from the P4 stream
1310    # - helper for streamP4Files
1311
1312    def streamOneP4File(self, file, contents):
1313        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1314        relPath = self.wildcard_decode(relPath)
1315        if verbose:
1316            sys.stderr.write("%s\n" % relPath)
1317
1318        (type_base, type_mods) = split_p4_type(file["type"])
1319
1320        git_mode = "100644"
1321        if "x" in type_mods:
1322            git_mode = "100755"
1323        if type_base == "symlink":
1324            git_mode = "120000"
1325            # p4 print on a symlink contains "target\n"; remove the newline
1326            data = ''.join(contents)
1327            contents = [data[:-1]]
1328
1329        if type_base == "utf16":
1330            # p4 delivers different text in the python output to -G
1331            # than it does when using "print -o", or normal p4 client
1332            # operations.  utf16 is converted to ascii or utf8, perhaps.
1333            # But ascii text saved as -t utf16 is completely mangled.
1334            # Invoke print -o to get the real contents.
1335            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
1336            contents = [ text ]
1337
1338        if type_base == "apple":
1339            # Apple filetype files will be streamed as a concatenation of
1340            # its appledouble header and the contents.  This is useless
1341            # on both macs and non-macs.  If using "print -q -o xx", it
1342            # will create "xx" with the data, and "%xx" with the header.
1343            # This is also not very useful.
1344            #
1345            # Ideally, someday, this script can learn how to generate
1346            # appledouble files directly and import those to git, but
1347            # non-mac machines can never find a use for apple filetype.
1348            print "\nIgnoring apple filetype file %s" % file['depotFile']
1349            return
1350
1351        # Perhaps windows wants unicode, utf16 newlines translated too;
1352        # but this is not doing it.
1353        if self.isWindows and type_base == "text":
1354            mangled = []
1355            for data in contents:
1356                data = data.replace("\r\n", "\n")
1357                mangled.append(data)
1358            contents = mangled
1359
1360        # Note that we do not try to de-mangle keywords on utf16 files,
1361        # even though in theory somebody may want that.
1362        if type_base in ("text", "unicode", "binary"):
1363            if "ko" in type_mods:
1364                text = ''.join(contents)
1365                text = re.sub(r'\$(Id|Header):[^$]*\$', r'$\1$', text)
1366                contents = [ text ]
1367            elif "k" in type_mods:
1368                text = ''.join(contents)
1369                text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$', r'$\1$', text)
1370                contents = [ text ]
1371
1372        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
1373
1374        # total length...
1375        length = 0
1376        for d in contents:
1377            length = length + len(d)
1378
1379        self.gitStream.write("data %d\n" % length)
1380        for d in contents:
1381            self.gitStream.write(d)
1382        self.gitStream.write("\n")
1383
1384    def streamOneP4Deletion(self, file):
1385        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1386        if verbose:
1387            sys.stderr.write("delete %s\n" % relPath)
1388        self.gitStream.write("D %s\n" % relPath)
1389
1390    # handle another chunk of streaming data
1391    def streamP4FilesCb(self, marshalled):
1392
1393        if marshalled.has_key('depotFile') and self.stream_have_file_info:
1394            # start of a new file - output the old one first
1395            self.streamOneP4File(self.stream_file, self.stream_contents)
1396            self.stream_file = {}
1397            self.stream_contents = []
1398            self.stream_have_file_info = False
1399
1400        # pick up the new file information... for the
1401        # 'data' field we need to append to our array
1402        for k in marshalled.keys():
1403            if k == 'data':
1404                self.stream_contents.append(marshalled['data'])
1405            else:
1406                self.stream_file[k] = marshalled[k]
1407
1408        self.stream_have_file_info = True
1409
1410    # Stream directly from "p4 files" into "git fast-import"
1411    def streamP4Files(self, files):
1412        filesForCommit = []
1413        filesToRead = []
1414        filesToDelete = []
1415
1416        for f in files:
1417            includeFile = True
1418            for val in self.clientSpecDirs:
1419                if f['path'].startswith(val[0]):
1420                    if val[1][0] <= 0:
1421                        includeFile = False
1422                    break
1423
1424            if includeFile:
1425                filesForCommit.append(f)
1426                if f['action'] in self.delete_actions:
1427                    filesToDelete.append(f)
1428                else:
1429                    filesToRead.append(f)
1430
1431        # deleted files...
1432        for f in filesToDelete:
1433            self.streamOneP4Deletion(f)
1434
1435        if len(filesToRead) > 0:
1436            self.stream_file = {}
1437            self.stream_contents = []
1438            self.stream_have_file_info = False
1439
1440            # curry self argument
1441            def streamP4FilesCbSelf(entry):
1442                self.streamP4FilesCb(entry)
1443
1444            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
1445
1446            p4CmdList(["-x", "-", "print"],
1447                      stdin=fileArgs,
1448                      cb=streamP4FilesCbSelf)
1449
1450            # do the last chunk
1451            if self.stream_file.has_key('depotFile'):
1452                self.streamOneP4File(self.stream_file, self.stream_contents)
1453
1454    def commit(self, details, files, branch, branchPrefixes, parent = ""):
1455        epoch = details["time"]
1456        author = details["user"]
1457        self.branchPrefixes = branchPrefixes
1458
1459        if self.verbose:
1460            print "commit into %s" % branch
1461
1462        # start with reading files; if that fails, we should not
1463        # create a commit.
1464        new_files = []
1465        for f in files:
1466            if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1467                new_files.append (f)
1468            else:
1469                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1470
1471        self.gitStream.write("commit %s\n" % branch)
1472#        gitStream.write("mark :%s\n" % details["change"])
1473        self.committedChanges.add(int(details["change"]))
1474        committer = ""
1475        if author not in self.users:
1476            self.getUserMapFromPerforceServer()
1477        if author in self.users:
1478            committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1479        else:
1480            committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1481
1482        self.gitStream.write("committer %s\n" % committer)
1483
1484        self.gitStream.write("data <<EOT\n")
1485        self.gitStream.write(details["desc"])
1486        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1487                             % (','.join (branchPrefixes), details["change"]))
1488        if len(details['options']) > 0:
1489            self.gitStream.write(": options = %s" % details['options'])
1490        self.gitStream.write("]\nEOT\n\n")
1491
1492        if len(parent) > 0:
1493            if self.verbose:
1494                print "parent %s" % parent
1495            self.gitStream.write("from %s\n" % parent)
1496
1497        self.streamP4Files(new_files)
1498        self.gitStream.write("\n")
1499
1500        change = int(details["change"])
1501
1502        if self.labels.has_key(change):
1503            label = self.labels[change]
1504            labelDetails = label[0]
1505            labelRevisions = label[1]
1506            if self.verbose:
1507                print "Change %s is labelled %s" % (change, labelDetails)
1508
1509            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
1510                                                    for p in branchPrefixes])
1511
1512            if len(files) == len(labelRevisions):
1513
1514                cleanedFiles = {}
1515                for info in files:
1516                    if info["action"] in self.delete_actions:
1517                        continue
1518                    cleanedFiles[info["depotFile"]] = info["rev"]
1519
1520                if cleanedFiles == labelRevisions:
1521                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1522                    self.gitStream.write("from %s\n" % branch)
1523
1524                    owner = labelDetails["Owner"]
1525                    tagger = ""
1526                    if author in self.users:
1527                        tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1528                    else:
1529                        tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1530                    self.gitStream.write("tagger %s\n" % tagger)
1531                    self.gitStream.write("data <<EOT\n")
1532                    self.gitStream.write(labelDetails["Description"])
1533                    self.gitStream.write("EOT\n\n")
1534
1535                else:
1536                    if not self.silent:
1537                        print ("Tag %s does not match with change %s: files do not match."
1538                               % (labelDetails["label"], change))
1539
1540            else:
1541                if not self.silent:
1542                    print ("Tag %s does not match with change %s: file count is different."
1543                           % (labelDetails["label"], change))
1544
1545    def getLabels(self):
1546        self.labels = {}
1547
1548        l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1549        if len(l) > 0 and not self.silent:
1550            print "Finding files belonging to labels in %s" % `self.depotPaths`
1551
1552        for output in l:
1553            label = output["label"]
1554            revisions = {}
1555            newestChange = 0
1556            if self.verbose:
1557                print "Querying files for label %s" % label
1558            for file in p4CmdList(["files"] +
1559                                      ["%s...@%s" % (p, label)
1560                                          for p in self.depotPaths]):
1561                revisions[file["depotFile"]] = file["rev"]
1562                change = int(file["change"])
1563                if change > newestChange:
1564                    newestChange = change
1565
1566            self.labels[newestChange] = [output, revisions]
1567
1568        if self.verbose:
1569            print "Label changes: %s" % self.labels.keys()
1570
1571    def guessProjectName(self):
1572        for p in self.depotPaths:
1573            if p.endswith("/"):
1574                p = p[:-1]
1575            p = p[p.strip().rfind("/") + 1:]
1576            if not p.endswith("/"):
1577               p += "/"
1578            return p
1579
1580    def getBranchMapping(self):
1581        lostAndFoundBranches = set()
1582
1583        user = gitConfig("git-p4.branchUser")
1584        if len(user) > 0:
1585            command = "branches -u %s" % user
1586        else:
1587            command = "branches"
1588
1589        for info in p4CmdList(command):
1590            details = p4Cmd("branch -o %s" % info["branch"])
1591            viewIdx = 0
1592            while details.has_key("View%s" % viewIdx):
1593                paths = details["View%s" % viewIdx].split(" ")
1594                viewIdx = viewIdx + 1
1595                # require standard //depot/foo/... //depot/bar/... mapping
1596                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1597                    continue
1598                source = paths[0]
1599                destination = paths[1]
1600                ## HACK
1601                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
1602                    source = source[len(self.depotPaths[0]):-4]
1603                    destination = destination[len(self.depotPaths[0]):-4]
1604
1605                    if destination in self.knownBranches:
1606                        if not self.silent:
1607                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1608                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1609                        continue
1610
1611                    self.knownBranches[destination] = source
1612
1613                    lostAndFoundBranches.discard(destination)
1614
1615                    if source not in self.knownBranches:
1616                        lostAndFoundBranches.add(source)
1617
1618        # Perforce does not strictly require branches to be defined, so we also
1619        # check git config for a branch list.
1620        #
1621        # Example of branch definition in git config file:
1622        # [git-p4]
1623        #   branchList=main:branchA
1624        #   branchList=main:branchB
1625        #   branchList=branchA:branchC
1626        configBranches = gitConfigList("git-p4.branchList")
1627        for branch in configBranches:
1628            if branch:
1629                (source, destination) = branch.split(":")
1630                self.knownBranches[destination] = source
1631
1632                lostAndFoundBranches.discard(destination)
1633
1634                if source not in self.knownBranches:
1635                    lostAndFoundBranches.add(source)
1636
1637
1638        for branch in lostAndFoundBranches:
1639            self.knownBranches[branch] = branch
1640
1641    def getBranchMappingFromGitBranches(self):
1642        branches = p4BranchesInGit(self.importIntoRemotes)
1643        for branch in branches.keys():
1644            if branch == "master":
1645                branch = "main"
1646            else:
1647                branch = branch[len(self.projectName):]
1648            self.knownBranches[branch] = branch
1649
1650    def listExistingP4GitBranches(self):
1651        # branches holds mapping from name to commit
1652        branches = p4BranchesInGit(self.importIntoRemotes)
1653        self.p4BranchesInGit = branches.keys()
1654        for branch in branches.keys():
1655            self.initialParents[self.refPrefix + branch] = branches[branch]
1656
1657    def updateOptionDict(self, d):
1658        option_keys = {}
1659        if self.keepRepoPath:
1660            option_keys['keepRepoPath'] = 1
1661
1662        d["options"] = ' '.join(sorted(option_keys.keys()))
1663
1664    def readOptions(self, d):
1665        self.keepRepoPath = (d.has_key('options')
1666                             and ('keepRepoPath' in d['options']))
1667
1668    def gitRefForBranch(self, branch):
1669        if branch == "main":
1670            return self.refPrefix + "master"
1671
1672        if len(branch) <= 0:
1673            return branch
1674
1675        return self.refPrefix + self.projectName + branch
1676
1677    def gitCommitByP4Change(self, ref, change):
1678        if self.verbose:
1679            print "looking in ref " + ref + " for change %s using bisect..." % change
1680
1681        earliestCommit = ""
1682        latestCommit = parseRevision(ref)
1683
1684        while True:
1685            if self.verbose:
1686                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1687            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1688            if len(next) == 0:
1689                if self.verbose:
1690                    print "argh"
1691                return ""
1692            log = extractLogMessageFromGitCommit(next)
1693            settings = extractSettingsGitLog(log)
1694            currentChange = int(settings['change'])
1695            if self.verbose:
1696                print "current change %s" % currentChange
1697
1698            if currentChange == change:
1699                if self.verbose:
1700                    print "found %s" % next
1701                return next
1702
1703            if currentChange < change:
1704                earliestCommit = "^%s" % next
1705            else:
1706                latestCommit = "%s" % next
1707
1708        return ""
1709
1710    def importNewBranch(self, branch, maxChange):
1711        # make fast-import flush all changes to disk and update the refs using the checkpoint
1712        # command so that we can try to find the branch parent in the git history
1713        self.gitStream.write("checkpoint\n\n");
1714        self.gitStream.flush();
1715        branchPrefix = self.depotPaths[0] + branch + "/"
1716        range = "@1,%s" % maxChange
1717        #print "prefix" + branchPrefix
1718        changes = p4ChangesForPaths([branchPrefix], range)
1719        if len(changes) <= 0:
1720            return False
1721        firstChange = changes[0]
1722        #print "first change in branch: %s" % firstChange
1723        sourceBranch = self.knownBranches[branch]
1724        sourceDepotPath = self.depotPaths[0] + sourceBranch
1725        sourceRef = self.gitRefForBranch(sourceBranch)
1726        #print "source " + sourceBranch
1727
1728        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1729        #print "branch parent: %s" % branchParentChange
1730        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1731        if len(gitParent) > 0:
1732            self.initialParents[self.gitRefForBranch(branch)] = gitParent
1733            #print "parent git commit: %s" % gitParent
1734
1735        self.importChanges(changes)
1736        return True
1737
1738    def importChanges(self, changes):
1739        cnt = 1
1740        for change in changes:
1741            description = p4Cmd("describe %s" % change)
1742            self.updateOptionDict(description)
1743
1744            if not self.silent:
1745                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1746                sys.stdout.flush()
1747            cnt = cnt + 1
1748
1749            try:
1750                if self.detectBranches:
1751                    branches = self.splitFilesIntoBranches(description)
1752                    for branch in branches.keys():
1753                        ## HACK  --hwn
1754                        branchPrefix = self.depotPaths[0] + branch + "/"
1755
1756                        parent = ""
1757
1758                        filesForCommit = branches[branch]
1759
1760                        if self.verbose:
1761                            print "branch is %s" % branch
1762
1763                        self.updatedBranches.add(branch)
1764
1765                        if branch not in self.createdBranches:
1766                            self.createdBranches.add(branch)
1767                            parent = self.knownBranches[branch]
1768                            if parent == branch:
1769                                parent = ""
1770                            else:
1771                                fullBranch = self.projectName + branch
1772                                if fullBranch not in self.p4BranchesInGit:
1773                                    if not self.silent:
1774                                        print("\n    Importing new branch %s" % fullBranch);
1775                                    if self.importNewBranch(branch, change - 1):
1776                                        parent = ""
1777                                        self.p4BranchesInGit.append(fullBranch)
1778                                    if not self.silent:
1779                                        print("\n    Resuming with change %s" % change);
1780
1781                                if self.verbose:
1782                                    print "parent determined through known branches: %s" % parent
1783
1784                        branch = self.gitRefForBranch(branch)
1785                        parent = self.gitRefForBranch(parent)
1786
1787                        if self.verbose:
1788                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1789
1790                        if len(parent) == 0 and branch in self.initialParents:
1791                            parent = self.initialParents[branch]
1792                            del self.initialParents[branch]
1793
1794                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1795                else:
1796                    files = self.extractFilesFromCommit(description)
1797                    self.commit(description, files, self.branch, self.depotPaths,
1798                                self.initialParent)
1799                    self.initialParent = ""
1800            except IOError:
1801                print self.gitError.read()
1802                sys.exit(1)
1803
1804    def importHeadRevision(self, revision):
1805        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1806
1807        details = {}
1808        details["user"] = "git perforce import user"
1809        details["desc"] = ("Initial import of %s from the state at revision %s\n"
1810                           % (' '.join(self.depotPaths), revision))
1811        details["change"] = revision
1812        newestRevision = 0
1813
1814        fileCnt = 0
1815        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
1816
1817        for info in p4CmdList(["files"] + fileArgs):
1818
1819            if 'code' in info and info['code'] == 'error':
1820                sys.stderr.write("p4 returned an error: %s\n"
1821                                 % info['data'])
1822                if info['data'].find("must refer to client") >= 0:
1823                    sys.stderr.write("This particular p4 error is misleading.\n")
1824                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
1825                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
1826                sys.exit(1)
1827            if 'p4ExitCode' in info:
1828                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
1829                sys.exit(1)
1830
1831
1832            change = int(info["change"])
1833            if change > newestRevision:
1834                newestRevision = change
1835
1836            if info["action"] in self.delete_actions:
1837                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1838                #fileCnt = fileCnt + 1
1839                continue
1840
1841            for prop in ["depotFile", "rev", "action", "type" ]:
1842                details["%s%s" % (prop, fileCnt)] = info[prop]
1843
1844            fileCnt = fileCnt + 1
1845
1846        details["change"] = newestRevision
1847
1848        # Use time from top-most change so that all git-p4 clones of
1849        # the same p4 repo have the same commit SHA1s.
1850        res = p4CmdList("describe -s %d" % newestRevision)
1851        newestTime = None
1852        for r in res:
1853            if r.has_key('time'):
1854                newestTime = int(r['time'])
1855        if newestTime is None:
1856            die("\"describe -s\" on newest change %d did not give a time")
1857        details["time"] = newestTime
1858
1859        self.updateOptionDict(details)
1860        try:
1861            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1862        except IOError:
1863            print "IO error with git fast-import. Is your git version recent enough?"
1864            print self.gitError.read()
1865
1866
1867    def getClientSpec(self):
1868        specList = p4CmdList( "client -o" )
1869        temp = {}
1870        for entry in specList:
1871            for k,v in entry.iteritems():
1872                if k.startswith("View"):
1873
1874                    # p4 has these %%1 to %%9 arguments in specs to
1875                    # reorder paths; which we can't handle (yet :)
1876                    if re.match('%%\d', v) != None:
1877                        print "Sorry, can't handle %%n arguments in client specs"
1878                        sys.exit(1)
1879
1880                    if v.startswith('"'):
1881                        start = 1
1882                    else:
1883                        start = 0
1884                    index = v.find("...")
1885
1886                    # save the "client view"; i.e the RHS of the view
1887                    # line that tells the client where to put the
1888                    # files for this view.
1889                    cv = v[index+3:].strip() # +3 to remove previous '...'
1890
1891                    # if the client view doesn't end with a
1892                    # ... wildcard, then we're going to mess up the
1893                    # output directory, so fail gracefully.
1894                    if not cv.endswith('...'):
1895                        print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1896                        sys.exit(1)
1897                    cv=cv[:-3]
1898
1899                    # now save the view; +index means included, -index
1900                    # means it should be filtered out.
1901                    v = v[start:index]
1902                    if v.startswith("-"):
1903                        v = v[1:]
1904                        include = -len(v)
1905                    else:
1906                        include = len(v)
1907
1908                    temp[v] = (include, cv)
1909
1910        self.clientSpecDirs = temp.items()
1911        self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1912
1913    def run(self, args):
1914        self.depotPaths = []
1915        self.changeRange = ""
1916        self.initialParent = ""
1917        self.previousDepotPaths = []
1918
1919        # map from branch depot path to parent branch
1920        self.knownBranches = {}
1921        self.initialParents = {}
1922        self.hasOrigin = originP4BranchesExist()
1923        if not self.syncWithOrigin:
1924            self.hasOrigin = False
1925
1926        if self.importIntoRemotes:
1927            self.refPrefix = "refs/remotes/p4/"
1928        else:
1929            self.refPrefix = "refs/heads/p4/"
1930
1931        if self.syncWithOrigin and self.hasOrigin:
1932            if not self.silent:
1933                print "Syncing with origin first by calling git fetch origin"
1934            system("git fetch origin")
1935
1936        if len(self.branch) == 0:
1937            self.branch = self.refPrefix + "master"
1938            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1939                system("git update-ref %s refs/heads/p4" % self.branch)
1940                system("git branch -D p4");
1941            # create it /after/ importing, when master exists
1942            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1943                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1944
1945        if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1946            self.getClientSpec()
1947
1948        # TODO: should always look at previous commits,
1949        # merge with previous imports, if possible.
1950        if args == []:
1951            if self.hasOrigin:
1952                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1953            self.listExistingP4GitBranches()
1954
1955            if len(self.p4BranchesInGit) > 1:
1956                if not self.silent:
1957                    print "Importing from/into multiple branches"
1958                self.detectBranches = True
1959
1960            if self.verbose:
1961                print "branches: %s" % self.p4BranchesInGit
1962
1963            p4Change = 0
1964            for branch in self.p4BranchesInGit:
1965                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1966
1967                settings = extractSettingsGitLog(logMsg)
1968
1969                self.readOptions(settings)
1970                if (settings.has_key('depot-paths')
1971                    and settings.has_key ('change')):
1972                    change = int(settings['change']) + 1
1973                    p4Change = max(p4Change, change)
1974
1975                    depotPaths = sorted(settings['depot-paths'])
1976                    if self.previousDepotPaths == []:
1977                        self.previousDepotPaths = depotPaths
1978                    else:
1979                        paths = []
1980                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1981                            prev_list = prev.split("/")
1982                            cur_list = cur.split("/")
1983                            for i in range(0, min(len(cur_list), len(prev_list))):
1984                                if cur_list[i] <> prev_list[i]:
1985                                    i = i - 1
1986                                    break
1987
1988                            paths.append ("/".join(cur_list[:i + 1]))
1989
1990                        self.previousDepotPaths = paths
1991
1992            if p4Change > 0:
1993                self.depotPaths = sorted(self.previousDepotPaths)
1994                self.changeRange = "@%s,#head" % p4Change
1995                if not self.detectBranches:
1996                    self.initialParent = parseRevision(self.branch)
1997                if not self.silent and not self.detectBranches:
1998                    print "Performing incremental import into %s git branch" % self.branch
1999
2000        if not self.branch.startswith("refs/"):
2001            self.branch = "refs/heads/" + self.branch
2002
2003        if len(args) == 0 and self.depotPaths:
2004            if not self.silent:
2005                print "Depot paths: %s" % ' '.join(self.depotPaths)
2006        else:
2007            if self.depotPaths and self.depotPaths != args:
2008                print ("previous import used depot path %s and now %s was specified. "
2009                       "This doesn't work!" % (' '.join (self.depotPaths),
2010                                               ' '.join (args)))
2011                sys.exit(1)
2012
2013            self.depotPaths = sorted(args)
2014
2015        revision = ""
2016        self.users = {}
2017
2018        newPaths = []
2019        for p in self.depotPaths:
2020            if p.find("@") != -1:
2021                atIdx = p.index("@")
2022                self.changeRange = p[atIdx:]
2023                if self.changeRange == "@all":
2024                    self.changeRange = ""
2025                elif ',' not in self.changeRange:
2026                    revision = self.changeRange
2027                    self.changeRange = ""
2028                p = p[:atIdx]
2029            elif p.find("#") != -1:
2030                hashIdx = p.index("#")
2031                revision = p[hashIdx:]
2032                p = p[:hashIdx]
2033            elif self.previousDepotPaths == []:
2034                revision = "#head"
2035
2036            p = re.sub ("\.\.\.$", "", p)
2037            if not p.endswith("/"):
2038                p += "/"
2039
2040            newPaths.append(p)
2041
2042        self.depotPaths = newPaths
2043
2044
2045        self.loadUserMapFromCache()
2046        self.labels = {}
2047        if self.detectLabels:
2048            self.getLabels();
2049
2050        if self.detectBranches:
2051            ## FIXME - what's a P4 projectName ?
2052            self.projectName = self.guessProjectName()
2053
2054            if self.hasOrigin:
2055                self.getBranchMappingFromGitBranches()
2056            else:
2057                self.getBranchMapping()
2058            if self.verbose:
2059                print "p4-git branches: %s" % self.p4BranchesInGit
2060                print "initial parents: %s" % self.initialParents
2061            for b in self.p4BranchesInGit:
2062                if b != "master":
2063
2064                    ## FIXME
2065                    b = b[len(self.projectName):]
2066                self.createdBranches.add(b)
2067
2068        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2069
2070        importProcess = subprocess.Popen(["git", "fast-import"],
2071                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2072                                         stderr=subprocess.PIPE);
2073        self.gitOutput = importProcess.stdout
2074        self.gitStream = importProcess.stdin
2075        self.gitError = importProcess.stderr
2076
2077        if revision:
2078            self.importHeadRevision(revision)
2079        else:
2080            changes = []
2081
2082            if len(self.changesFile) > 0:
2083                output = open(self.changesFile).readlines()
2084                changeSet = set()
2085                for line in output:
2086                    changeSet.add(int(line))
2087
2088                for change in changeSet:
2089                    changes.append(change)
2090
2091                changes.sort()
2092            else:
2093                # catch "git-p4 sync" with no new branches, in a repo that
2094                # does not have any existing git-p4 branches
2095                if len(args) == 0 and not self.p4BranchesInGit:
2096                    die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
2097                if self.verbose:
2098                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2099                                                              self.changeRange)
2100                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2101
2102                if len(self.maxChanges) > 0:
2103                    changes = changes[:min(int(self.maxChanges), len(changes))]
2104
2105            if len(changes) == 0:
2106                if not self.silent:
2107                    print "No changes to import!"
2108                return True
2109
2110            if not self.silent and not self.detectBranches:
2111                print "Import destination: %s" % self.branch
2112
2113            self.updatedBranches = set()
2114
2115            self.importChanges(changes)
2116
2117            if not self.silent:
2118                print ""
2119                if len(self.updatedBranches) > 0:
2120                    sys.stdout.write("Updated branches: ")
2121                    for b in self.updatedBranches:
2122                        sys.stdout.write("%s " % b)
2123                    sys.stdout.write("\n")
2124
2125        self.gitStream.close()
2126        if importProcess.wait() != 0:
2127            die("fast-import failed: %s" % self.gitError.read())
2128        self.gitOutput.close()
2129        self.gitError.close()
2130
2131        return True
2132
2133class P4Rebase(Command):
2134    def __init__(self):
2135        Command.__init__(self)
2136        self.options = [ ]
2137        self.description = ("Fetches the latest revision from perforce and "
2138                            + "rebases the current work (branch) against it")
2139        self.verbose = False
2140
2141    def run(self, args):
2142        sync = P4Sync()
2143        sync.run([])
2144
2145        return self.rebase()
2146
2147    def rebase(self):
2148        if os.system("git update-index --refresh") != 0:
2149            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.");
2150        if len(read_pipe("git diff-index HEAD --")) > 0:
2151            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2152
2153        [upstream, settings] = findUpstreamBranchPoint()
2154        if len(upstream) == 0:
2155            die("Cannot find upstream branchpoint for rebase")
2156
2157        # the branchpoint may be p4/foo~3, so strip off the parent
2158        upstream = re.sub("~[0-9]+$", "", upstream)
2159
2160        print "Rebasing the current branch onto %s" % upstream
2161        oldHead = read_pipe("git rev-parse HEAD").strip()
2162        system("git rebase %s" % upstream)
2163        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
2164        return True
2165
2166class P4Clone(P4Sync):
2167    def __init__(self):
2168        P4Sync.__init__(self)
2169        self.description = "Creates a new git repository and imports from Perforce into it"
2170        self.usage = "usage: %prog [options] //depot/path[@revRange]"
2171        self.options += [
2172            optparse.make_option("--destination", dest="cloneDestination",
2173                                 action='store', default=None,
2174                                 help="where to leave result of the clone"),
2175            optparse.make_option("-/", dest="cloneExclude",
2176                                 action="append", type="string",
2177                                 help="exclude depot path"),
2178            optparse.make_option("--bare", dest="cloneBare",
2179                                 action="store_true", default=False),
2180        ]
2181        self.cloneDestination = None
2182        self.needsGit = False
2183        self.cloneBare = False
2184
2185    # This is required for the "append" cloneExclude action
2186    def ensure_value(self, attr, value):
2187        if not hasattr(self, attr) or getattr(self, attr) is None:
2188            setattr(self, attr, value)
2189        return getattr(self, attr)
2190
2191    def defaultDestination(self, args):
2192        ## TODO: use common prefix of args?
2193        depotPath = args[0]
2194        depotDir = re.sub("(@[^@]*)$", "", depotPath)
2195        depotDir = re.sub("(#[^#]*)$", "", depotDir)
2196        depotDir = re.sub(r"\.\.\.$", "", depotDir)
2197        depotDir = re.sub(r"/$", "", depotDir)
2198        return os.path.split(depotDir)[1]
2199
2200    def run(self, args):
2201        if len(args) < 1:
2202            return False
2203
2204        if self.keepRepoPath and not self.cloneDestination:
2205            sys.stderr.write("Must specify destination for --keep-path\n")
2206            sys.exit(1)
2207
2208        depotPaths = args
2209
2210        if not self.cloneDestination and len(depotPaths) > 1:
2211            self.cloneDestination = depotPaths[-1]
2212            depotPaths = depotPaths[:-1]
2213
2214        self.cloneExclude = ["/"+p for p in self.cloneExclude]
2215        for p in depotPaths:
2216            if not p.startswith("//"):
2217                return False
2218
2219        if not self.cloneDestination:
2220            self.cloneDestination = self.defaultDestination(args)
2221
2222        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2223
2224        if not os.path.exists(self.cloneDestination):
2225            os.makedirs(self.cloneDestination)
2226        chdir(self.cloneDestination)
2227
2228        init_cmd = [ "git", "init" ]
2229        if self.cloneBare:
2230            init_cmd.append("--bare")
2231        subprocess.check_call(init_cmd)
2232
2233        if not P4Sync.run(self, depotPaths):
2234            return False
2235        if self.branch != "master":
2236            if self.importIntoRemotes:
2237                masterbranch = "refs/remotes/p4/master"
2238            else:
2239                masterbranch = "refs/heads/p4/master"
2240            if gitBranchExists(masterbranch):
2241                system("git branch master %s" % masterbranch)
2242                if not self.cloneBare:
2243                    system("git checkout -f")
2244            else:
2245                print "Could not detect main branch. No checkout/master branch created."
2246
2247        return True
2248
2249class P4Branches(Command):
2250    def __init__(self):
2251        Command.__init__(self)
2252        self.options = [ ]
2253        self.description = ("Shows the git branches that hold imports and their "
2254                            + "corresponding perforce depot paths")
2255        self.verbose = False
2256
2257    def run(self, args):
2258        if originP4BranchesExist():
2259            createOrUpdateBranchesFromOrigin()
2260
2261        cmdline = "git rev-parse --symbolic "
2262        cmdline += " --remotes"
2263
2264        for line in read_pipe_lines(cmdline):
2265            line = line.strip()
2266
2267            if not line.startswith('p4/') or line == "p4/HEAD":
2268                continue
2269            branch = line
2270
2271            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2272            settings = extractSettingsGitLog(log)
2273
2274            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2275        return True
2276
2277class HelpFormatter(optparse.IndentedHelpFormatter):
2278    def __init__(self):
2279        optparse.IndentedHelpFormatter.__init__(self)
2280
2281    def format_description(self, description):
2282        if description:
2283            return description + "\n"
2284        else:
2285            return ""
2286
2287def printUsage(commands):
2288    print "usage: %s <command> [options]" % sys.argv[0]
2289    print ""
2290    print "valid commands: %s" % ", ".join(commands)
2291    print ""
2292    print "Try %s <command> --help for command specific help." % sys.argv[0]
2293    print ""
2294
2295commands = {
2296    "debug" : P4Debug,
2297    "submit" : P4Submit,
2298    "commit" : P4Submit,
2299    "sync" : P4Sync,
2300    "rebase" : P4Rebase,
2301    "clone" : P4Clone,
2302    "rollback" : P4RollBack,
2303    "branches" : P4Branches
2304}
2305
2306
2307def main():
2308    if len(sys.argv[1:]) == 0:
2309        printUsage(commands.keys())
2310        sys.exit(2)
2311
2312    cmd = ""
2313    cmdName = sys.argv[1]
2314    try:
2315        klass = commands[cmdName]
2316        cmd = klass()
2317    except KeyError:
2318        print "unknown command %s" % cmdName
2319        print ""
2320        printUsage(commands.keys())
2321        sys.exit(2)
2322
2323    options = cmd.options
2324    cmd.gitdir = os.environ.get("GIT_DIR", None)
2325
2326    args = sys.argv[2:]
2327
2328    if len(options) > 0:
2329        options.append(optparse.make_option("--git-dir", dest="gitdir"))
2330
2331        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2332                                       options,
2333                                       description = cmd.description,
2334                                       formatter = HelpFormatter())
2335
2336        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2337    global verbose
2338    verbose = cmd.verbose
2339    if cmd.needsGit:
2340        if cmd.gitdir == None:
2341            cmd.gitdir = os.path.abspath(".git")
2342            if not isValidGitDir(cmd.gitdir):
2343                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2344                if os.path.exists(cmd.gitdir):
2345                    cdup = read_pipe("git rev-parse --show-cdup").strip()
2346                    if len(cdup) > 0:
2347                        chdir(cdup);
2348
2349        if not isValidGitDir(cmd.gitdir):
2350            if isValidGitDir(cmd.gitdir + "/.git"):
2351                cmd.gitdir += "/.git"
2352            else:
2353                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2354
2355        os.environ["GIT_DIR"] = cmd.gitdir
2356
2357    if not cmd.run(args):
2358        parser.print_help()
2359
2360
2361if __name__ == '__main__':
2362    main()