contrib / fast-import / git-p4on commit Merge branch 'pb/send-email-cccmd-fix' (36e74ab)
   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, popen2, subprocess, shelve
  12import tempfile, getopt, sha, os.path, time, platform
  13import re
  14
  15from sets import Set;
  16
  17verbose = False
  18
  19
  20def p4_build_cmd(cmd):
  21    """Build a suitable p4 command line.
  22
  23    This consolidates building and returning a p4 command line into one
  24    location. It means that hooking into the environment, or other configuration
  25    can be done more easily.
  26    """
  27    real_cmd = "%s " % "p4"
  28
  29    user = gitConfig("git-p4.user")
  30    if len(user) > 0:
  31        real_cmd += "-u %s " % user
  32
  33    password = gitConfig("git-p4.password")
  34    if len(password) > 0:
  35        real_cmd += "-P %s " % password
  36
  37    port = gitConfig("git-p4.port")
  38    if len(port) > 0:
  39        real_cmd += "-p %s " % port
  40
  41    host = gitConfig("git-p4.host")
  42    if len(host) > 0:
  43        real_cmd += "-h %s " % host
  44
  45    client = gitConfig("git-p4.client")
  46    if len(client) > 0:
  47        real_cmd += "-c %s " % client
  48
  49    real_cmd += "%s" % (cmd)
  50    if verbose:
  51        print real_cmd
  52    return real_cmd
  53
  54def chdir(dir):
  55    if os.name == 'nt':
  56        os.environ['PWD']=dir
  57    os.chdir(dir)
  58
  59def die(msg):
  60    if verbose:
  61        raise Exception(msg)
  62    else:
  63        sys.stderr.write(msg + "\n")
  64        sys.exit(1)
  65
  66def write_pipe(c, str):
  67    if verbose:
  68        sys.stderr.write('Writing pipe: %s\n' % c)
  69
  70    pipe = os.popen(c, 'w')
  71    val = pipe.write(str)
  72    if pipe.close():
  73        die('Command failed: %s' % c)
  74
  75    return val
  76
  77def p4_write_pipe(c, str):
  78    real_cmd = p4_build_cmd(c)
  79    return write_pipe(real_cmd, str)
  80
  81def read_pipe(c, ignore_error=False):
  82    if verbose:
  83        sys.stderr.write('Reading pipe: %s\n' % c)
  84
  85    pipe = os.popen(c, 'rb')
  86    val = pipe.read()
  87    if pipe.close() and not ignore_error:
  88        die('Command failed: %s' % c)
  89
  90    return val
  91
  92def p4_read_pipe(c, ignore_error=False):
  93    real_cmd = p4_build_cmd(c)
  94    return read_pipe(real_cmd, ignore_error)
  95
  96def read_pipe_lines(c):
  97    if verbose:
  98        sys.stderr.write('Reading pipe: %s\n' % c)
  99    ## todo: check return status
 100    pipe = os.popen(c, 'rb')
 101    val = pipe.readlines()
 102    if pipe.close():
 103        die('Command failed: %s' % c)
 104
 105    return val
 106
 107def p4_read_pipe_lines(c):
 108    """Specifically invoke p4 on the command supplied. """
 109    real_cmd = p4_build_cmd(c)
 110    return read_pipe_lines(real_cmd)
 111
 112def system(cmd):
 113    if verbose:
 114        sys.stderr.write("executing %s\n" % cmd)
 115    if os.system(cmd) != 0:
 116        die("command failed: %s" % cmd)
 117
 118def p4_system(cmd):
 119    """Specifically invoke p4 as the system command. """
 120    real_cmd = p4_build_cmd(cmd)
 121    return system(real_cmd)
 122
 123def isP4Exec(kind):
 124    """Determine if a Perforce 'kind' should have execute permission
 125
 126    'p4 help filetypes' gives a list of the types.  If it starts with 'x',
 127    or x follows one of a few letters.  Otherwise, if there is an 'x' after
 128    a plus sign, it is also executable"""
 129    return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
 130
 131def setP4ExecBit(file, mode):
 132    # Reopens an already open file and changes the execute bit to match
 133    # the execute bit setting in the passed in mode.
 134
 135    p4Type = "+x"
 136
 137    if not isModeExec(mode):
 138        p4Type = getP4OpenedType(file)
 139        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
 140        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
 141        if p4Type[-1] == "+":
 142            p4Type = p4Type[0:-1]
 143
 144    p4_system("reopen -t %s %s" % (p4Type, file))
 145
 146def getP4OpenedType(file):
 147    # Returns the perforce file type for the given file.
 148
 149    result = p4_read_pipe("opened %s" % file)
 150    match = re.match(".*\((.+)\)\r?$", result)
 151    if match:
 152        return match.group(1)
 153    else:
 154        die("Could not determine file type for %s (result: '%s')" % (file, result))
 155
 156def diffTreePattern():
 157    # This is a simple generator for the diff tree regex pattern. This could be
 158    # a class variable if this and parseDiffTreeEntry were a part of a class.
 159    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
 160    while True:
 161        yield pattern
 162
 163def parseDiffTreeEntry(entry):
 164    """Parses a single diff tree entry into its component elements.
 165
 166    See git-diff-tree(1) manpage for details about the format of the diff
 167    output. This method returns a dictionary with the following elements:
 168
 169    src_mode - The mode of the source file
 170    dst_mode - The mode of the destination file
 171    src_sha1 - The sha1 for the source file
 172    dst_sha1 - The sha1 fr the destination file
 173    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
 174    status_score - The score for the status (applicable for 'C' and 'R'
 175                   statuses). This is None if there is no score.
 176    src - The path for the source file.
 177    dst - The path for the destination file. This is only present for
 178          copy or renames. If it is not present, this is None.
 179
 180    If the pattern is not matched, None is returned."""
 181
 182    match = diffTreePattern().next().match(entry)
 183    if match:
 184        return {
 185            'src_mode': match.group(1),
 186            'dst_mode': match.group(2),
 187            'src_sha1': match.group(3),
 188            'dst_sha1': match.group(4),
 189            'status': match.group(5),
 190            'status_score': match.group(6),
 191            'src': match.group(7),
 192            'dst': match.group(10)
 193        }
 194    return None
 195
 196def isModeExec(mode):
 197    # Returns True if the given git mode represents an executable file,
 198    # otherwise False.
 199    return mode[-3:] == "755"
 200
 201def isModeExecChanged(src_mode, dst_mode):
 202    return isModeExec(src_mode) != isModeExec(dst_mode)
 203
 204def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
 205    cmd = p4_build_cmd("-G %s" % (cmd))
 206    if verbose:
 207        sys.stderr.write("Opening pipe: %s\n" % cmd)
 208
 209    # Use a temporary file to avoid deadlocks without
 210    # subprocess.communicate(), which would put another copy
 211    # of stdout into memory.
 212    stdin_file = None
 213    if stdin is not None:
 214        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
 215        stdin_file.write(stdin)
 216        stdin_file.flush()
 217        stdin_file.seek(0)
 218
 219    p4 = subprocess.Popen(cmd, shell=True,
 220                          stdin=stdin_file,
 221                          stdout=subprocess.PIPE)
 222
 223    result = []
 224    try:
 225        while True:
 226            entry = marshal.load(p4.stdout)
 227            result.append(entry)
 228    except EOFError:
 229        pass
 230    exitCode = p4.wait()
 231    if exitCode != 0:
 232        entry = {}
 233        entry["p4ExitCode"] = exitCode
 234        result.append(entry)
 235
 236    return result
 237
 238def p4Cmd(cmd):
 239    list = p4CmdList(cmd)
 240    result = {}
 241    for entry in list:
 242        result.update(entry)
 243    return result;
 244
 245def p4Where(depotPath):
 246    if not depotPath.endswith("/"):
 247        depotPath += "/"
 248    depotPath = depotPath + "..."
 249    outputList = p4CmdList("where %s" % depotPath)
 250    output = None
 251    for entry in outputList:
 252        if "depotFile" in entry:
 253            if entry["depotFile"] == depotPath:
 254                output = entry
 255                break
 256        elif "data" in entry:
 257            data = entry.get("data")
 258            space = data.find(" ")
 259            if data[:space] == depotPath:
 260                output = entry
 261                break
 262    if output == None:
 263        return ""
 264    if output["code"] == "error":
 265        return ""
 266    clientPath = ""
 267    if "path" in output:
 268        clientPath = output.get("path")
 269    elif "data" in output:
 270        data = output.get("data")
 271        lastSpace = data.rfind(" ")
 272        clientPath = data[lastSpace + 1:]
 273
 274    if clientPath.endswith("..."):
 275        clientPath = clientPath[:-3]
 276    return clientPath
 277
 278def currentGitBranch():
 279    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
 280
 281def isValidGitDir(path):
 282    if (os.path.exists(path + "/HEAD")
 283        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
 284        return True;
 285    return False
 286
 287def parseRevision(ref):
 288    return read_pipe("git rev-parse %s" % ref).strip()
 289
 290def extractLogMessageFromGitCommit(commit):
 291    logMessage = ""
 292
 293    ## fixme: title is first line of commit, not 1st paragraph.
 294    foundTitle = False
 295    for log in read_pipe_lines("git cat-file commit %s" % commit):
 296       if not foundTitle:
 297           if len(log) == 1:
 298               foundTitle = True
 299           continue
 300
 301       logMessage += log
 302    return logMessage
 303
 304def extractSettingsGitLog(log):
 305    values = {}
 306    for line in log.split("\n"):
 307        line = line.strip()
 308        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
 309        if not m:
 310            continue
 311
 312        assignments = m.group(1).split (':')
 313        for a in assignments:
 314            vals = a.split ('=')
 315            key = vals[0].strip()
 316            val = ('='.join (vals[1:])).strip()
 317            if val.endswith ('\"') and val.startswith('"'):
 318                val = val[1:-1]
 319
 320            values[key] = val
 321
 322    paths = values.get("depot-paths")
 323    if not paths:
 324        paths = values.get("depot-path")
 325    if paths:
 326        values['depot-paths'] = paths.split(',')
 327    return values
 328
 329def gitBranchExists(branch):
 330    proc = subprocess.Popen(["git", "rev-parse", branch],
 331                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
 332    return proc.wait() == 0;
 333
 334_gitConfig = {}
 335def gitConfig(key):
 336    if not _gitConfig.has_key(key):
 337        _gitConfig[key] = read_pipe("git config %s" % key, ignore_error=True).strip()
 338    return _gitConfig[key]
 339
 340def p4BranchesInGit(branchesAreInRemotes = True):
 341    branches = {}
 342
 343    cmdline = "git rev-parse --symbolic "
 344    if branchesAreInRemotes:
 345        cmdline += " --remotes"
 346    else:
 347        cmdline += " --branches"
 348
 349    for line in read_pipe_lines(cmdline):
 350        line = line.strip()
 351
 352        ## only import to p4/
 353        if not line.startswith('p4/') or line == "p4/HEAD":
 354            continue
 355        branch = line
 356
 357        # strip off p4
 358        branch = re.sub ("^p4/", "", line)
 359
 360        branches[branch] = parseRevision(line)
 361    return branches
 362
 363def findUpstreamBranchPoint(head = "HEAD"):
 364    branches = p4BranchesInGit()
 365    # map from depot-path to branch name
 366    branchByDepotPath = {}
 367    for branch in branches.keys():
 368        tip = branches[branch]
 369        log = extractLogMessageFromGitCommit(tip)
 370        settings = extractSettingsGitLog(log)
 371        if settings.has_key("depot-paths"):
 372            paths = ",".join(settings["depot-paths"])
 373            branchByDepotPath[paths] = "remotes/p4/" + branch
 374
 375    settings = None
 376    parent = 0
 377    while parent < 65535:
 378        commit = head + "~%s" % parent
 379        log = extractLogMessageFromGitCommit(commit)
 380        settings = extractSettingsGitLog(log)
 381        if settings.has_key("depot-paths"):
 382            paths = ",".join(settings["depot-paths"])
 383            if branchByDepotPath.has_key(paths):
 384                return [branchByDepotPath[paths], settings]
 385
 386        parent = parent + 1
 387
 388    return ["", settings]
 389
 390def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
 391    if not silent:
 392        print ("Creating/updating branch(es) in %s based on origin branch(es)"
 393               % localRefPrefix)
 394
 395    originPrefix = "origin/p4/"
 396
 397    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 398        line = line.strip()
 399        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
 400            continue
 401
 402        headName = line[len(originPrefix):]
 403        remoteHead = localRefPrefix + headName
 404        originHead = line
 405
 406        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
 407        if (not original.has_key('depot-paths')
 408            or not original.has_key('change')):
 409            continue
 410
 411        update = False
 412        if not gitBranchExists(remoteHead):
 413            if verbose:
 414                print "creating %s" % remoteHead
 415            update = True
 416        else:
 417            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
 418            if settings.has_key('change') > 0:
 419                if settings['depot-paths'] == original['depot-paths']:
 420                    originP4Change = int(original['change'])
 421                    p4Change = int(settings['change'])
 422                    if originP4Change > p4Change:
 423                        print ("%s (%s) is newer than %s (%s). "
 424                               "Updating p4 branch from origin."
 425                               % (originHead, originP4Change,
 426                                  remoteHead, p4Change))
 427                        update = True
 428                else:
 429                    print ("Ignoring: %s was imported from %s while "
 430                           "%s was imported from %s"
 431                           % (originHead, ','.join(original['depot-paths']),
 432                              remoteHead, ','.join(settings['depot-paths'])))
 433
 434        if update:
 435            system("git update-ref %s %s" % (remoteHead, originHead))
 436
 437def originP4BranchesExist():
 438        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
 439
 440def p4ChangesForPaths(depotPaths, changeRange):
 441    assert depotPaths
 442    output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
 443                                                        for p in depotPaths]))
 444
 445    changes = {}
 446    for line in output:
 447        changeNum = int(line.split(" ")[1])
 448        changes[changeNum] = True
 449
 450    changelist = changes.keys()
 451    changelist.sort()
 452    return changelist
 453
 454class Command:
 455    def __init__(self):
 456        self.usage = "usage: %prog [options]"
 457        self.needsGit = True
 458
 459class P4Debug(Command):
 460    def __init__(self):
 461        Command.__init__(self)
 462        self.options = [
 463            optparse.make_option("--verbose", dest="verbose", action="store_true",
 464                                 default=False),
 465            ]
 466        self.description = "A tool to debug the output of p4 -G."
 467        self.needsGit = False
 468        self.verbose = False
 469
 470    def run(self, args):
 471        j = 0
 472        for output in p4CmdList(" ".join(args)):
 473            print 'Element: %d' % j
 474            j += 1
 475            print output
 476        return True
 477
 478class P4RollBack(Command):
 479    def __init__(self):
 480        Command.__init__(self)
 481        self.options = [
 482            optparse.make_option("--verbose", dest="verbose", action="store_true"),
 483            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
 484        ]
 485        self.description = "A tool to debug the multi-branch import. Don't use :)"
 486        self.verbose = False
 487        self.rollbackLocalBranches = False
 488
 489    def run(self, args):
 490        if len(args) != 1:
 491            return False
 492        maxChange = int(args[0])
 493
 494        if "p4ExitCode" in p4Cmd("changes -m 1"):
 495            die("Problems executing p4");
 496
 497        if self.rollbackLocalBranches:
 498            refPrefix = "refs/heads/"
 499            lines = read_pipe_lines("git rev-parse --symbolic --branches")
 500        else:
 501            refPrefix = "refs/remotes/"
 502            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
 503
 504        for line in lines:
 505            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
 506                line = line.strip()
 507                ref = refPrefix + line
 508                log = extractLogMessageFromGitCommit(ref)
 509                settings = extractSettingsGitLog(log)
 510
 511                depotPaths = settings['depot-paths']
 512                change = settings['change']
 513
 514                changed = False
 515
 516                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
 517                                                           for p in depotPaths]))) == 0:
 518                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
 519                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
 520                    continue
 521
 522                while change and int(change) > maxChange:
 523                    changed = True
 524                    if self.verbose:
 525                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
 526                    system("git update-ref %s \"%s^\"" % (ref, ref))
 527                    log = extractLogMessageFromGitCommit(ref)
 528                    settings =  extractSettingsGitLog(log)
 529
 530
 531                    depotPaths = settings['depot-paths']
 532                    change = settings['change']
 533
 534                if changed:
 535                    print "%s rewound to %s" % (ref, change)
 536
 537        return True
 538
 539class P4Submit(Command):
 540    def __init__(self):
 541        Command.__init__(self)
 542        self.options = [
 543                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 544                optparse.make_option("--origin", dest="origin"),
 545                optparse.make_option("-M", dest="detectRename", action="store_true"),
 546        ]
 547        self.description = "Submit changes from git to the perforce depot."
 548        self.usage += " [name of git branch to submit into perforce depot]"
 549        self.interactive = True
 550        self.origin = ""
 551        self.detectRename = False
 552        self.verbose = False
 553        self.isWindows = (platform.system() == "Windows")
 554
 555    def check(self):
 556        if len(p4CmdList("opened ...")) > 0:
 557            die("You have files opened with perforce! Close them before starting the sync.")
 558
 559    # replaces everything between 'Description:' and the next P4 submit template field with the
 560    # commit message
 561    def prepareLogMessage(self, template, message):
 562        result = ""
 563
 564        inDescriptionSection = False
 565
 566        for line in template.split("\n"):
 567            if line.startswith("#"):
 568                result += line + "\n"
 569                continue
 570
 571            if inDescriptionSection:
 572                if line.startswith("Files:"):
 573                    inDescriptionSection = False
 574                else:
 575                    continue
 576            else:
 577                if line.startswith("Description:"):
 578                    inDescriptionSection = True
 579                    line += "\n"
 580                    for messageLine in message.split("\n"):
 581                        line += "\t" + messageLine + "\n"
 582
 583            result += line + "\n"
 584
 585        return result
 586
 587    def prepareSubmitTemplate(self):
 588        # remove lines in the Files section that show changes to files outside the depot path we're committing into
 589        template = ""
 590        inFilesSection = False
 591        for line in p4_read_pipe_lines("change -o"):
 592            if line.endswith("\r\n"):
 593                line = line[:-2] + "\n"
 594            if inFilesSection:
 595                if line.startswith("\t"):
 596                    # path starts and ends with a tab
 597                    path = line[1:]
 598                    lastTab = path.rfind("\t")
 599                    if lastTab != -1:
 600                        path = path[:lastTab]
 601                        if not path.startswith(self.depotPath):
 602                            continue
 603                else:
 604                    inFilesSection = False
 605            else:
 606                if line.startswith("Files:"):
 607                    inFilesSection = True
 608
 609            template += line
 610
 611        return template
 612
 613    def applyCommit(self, id):
 614        print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
 615        diffOpts = ("", "-M")[self.detectRename]
 616        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
 617        filesToAdd = set()
 618        filesToDelete = set()
 619        editedFiles = set()
 620        filesToChangeExecBit = {}
 621        for line in diff:
 622            diff = parseDiffTreeEntry(line)
 623            modifier = diff['status']
 624            path = diff['src']
 625            if modifier == "M":
 626                p4_system("edit \"%s\"" % path)
 627                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 628                    filesToChangeExecBit[path] = diff['dst_mode']
 629                editedFiles.add(path)
 630            elif modifier == "A":
 631                filesToAdd.add(path)
 632                filesToChangeExecBit[path] = diff['dst_mode']
 633                if path in filesToDelete:
 634                    filesToDelete.remove(path)
 635            elif modifier == "D":
 636                filesToDelete.add(path)
 637                if path in filesToAdd:
 638                    filesToAdd.remove(path)
 639            elif modifier == "R":
 640                src, dest = diff['src'], diff['dst']
 641                p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
 642                p4_system("edit \"%s\"" % (dest))
 643                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 644                    filesToChangeExecBit[dest] = diff['dst_mode']
 645                os.unlink(dest)
 646                editedFiles.add(dest)
 647                filesToDelete.add(src)
 648            else:
 649                die("unknown modifier %s for %s" % (modifier, path))
 650
 651        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
 652        patchcmd = diffcmd + " | git apply "
 653        tryPatchCmd = patchcmd + "--check -"
 654        applyPatchCmd = patchcmd + "--check --apply -"
 655
 656        if os.system(tryPatchCmd) != 0:
 657            print "Unfortunately applying the change failed!"
 658            print "What do you want to do?"
 659            response = "x"
 660            while response != "s" and response != "a" and response != "w":
 661                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
 662                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
 663            if response == "s":
 664                print "Skipping! Good luck with the next patches..."
 665                for f in editedFiles:
 666                    p4_system("revert \"%s\"" % f);
 667                for f in filesToAdd:
 668                    system("rm %s" %f)
 669                return
 670            elif response == "a":
 671                os.system(applyPatchCmd)
 672                if len(filesToAdd) > 0:
 673                    print "You may also want to call p4 add on the following files:"
 674                    print " ".join(filesToAdd)
 675                if len(filesToDelete):
 676                    print "The following files should be scheduled for deletion with p4 delete:"
 677                    print " ".join(filesToDelete)
 678                die("Please resolve and submit the conflict manually and "
 679                    + "continue afterwards with git-p4 submit --continue")
 680            elif response == "w":
 681                system(diffcmd + " > patch.txt")
 682                print "Patch saved to patch.txt in %s !" % self.clientPath
 683                die("Please resolve and submit the conflict manually and "
 684                    "continue afterwards with git-p4 submit --continue")
 685
 686        system(applyPatchCmd)
 687
 688        for f in filesToAdd:
 689            p4_system("add \"%s\"" % f)
 690        for f in filesToDelete:
 691            p4_system("revert \"%s\"" % f)
 692            p4_system("delete \"%s\"" % f)
 693
 694        # Set/clear executable bits
 695        for f in filesToChangeExecBit.keys():
 696            mode = filesToChangeExecBit[f]
 697            setP4ExecBit(f, mode)
 698
 699        logMessage = extractLogMessageFromGitCommit(id)
 700        logMessage = logMessage.strip()
 701
 702        template = self.prepareSubmitTemplate()
 703
 704        if self.interactive:
 705            submitTemplate = self.prepareLogMessage(template, logMessage)
 706            if os.environ.has_key("P4DIFF"):
 707                del(os.environ["P4DIFF"])
 708            diff = p4_read_pipe("diff -du ...")
 709
 710            newdiff = ""
 711            for newFile in filesToAdd:
 712                newdiff += "==== new file ====\n"
 713                newdiff += "--- /dev/null\n"
 714                newdiff += "+++ %s\n" % newFile
 715                f = open(newFile, "r")
 716                for line in f.readlines():
 717                    newdiff += "+" + line
 718                f.close()
 719
 720            separatorLine = "######## everything below this line is just the diff #######\n"
 721
 722            [handle, fileName] = tempfile.mkstemp()
 723            tmpFile = os.fdopen(handle, "w+")
 724            if self.isWindows:
 725                submitTemplate = submitTemplate.replace("\n", "\r\n")
 726                separatorLine = separatorLine.replace("\n", "\r\n")
 727                newdiff = newdiff.replace("\n", "\r\n")
 728            tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
 729            tmpFile.close()
 730            mtime = os.stat(fileName).st_mtime
 731            defaultEditor = "vi"
 732            if platform.system() == "Windows":
 733                defaultEditor = "notepad"
 734            if os.environ.has_key("P4EDITOR"):
 735                editor = os.environ.get("P4EDITOR")
 736            else:
 737                editor = os.environ.get("EDITOR", defaultEditor);
 738            system(editor + " " + fileName)
 739
 740            response = "y"
 741            if os.stat(fileName).st_mtime <= mtime:
 742                response = "x"
 743                while response != "y" and response != "n":
 744                    response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
 745
 746            if response == "y":
 747                tmpFile = open(fileName, "rb")
 748                message = tmpFile.read()
 749                tmpFile.close()
 750                submitTemplate = message[:message.index(separatorLine)]
 751                if self.isWindows:
 752                    submitTemplate = submitTemplate.replace("\r\n", "\n")
 753                p4_write_pipe("submit -i", submitTemplate)
 754            else:
 755                for f in editedFiles:
 756                    p4_system("revert \"%s\"" % f);
 757                for f in filesToAdd:
 758                    p4_system("revert \"%s\"" % f);
 759                    system("rm %s" %f)
 760
 761            os.remove(fileName)
 762        else:
 763            fileName = "submit.txt"
 764            file = open(fileName, "w+")
 765            file.write(self.prepareLogMessage(template, logMessage))
 766            file.close()
 767            print ("Perforce submit template written as %s. "
 768                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
 769                   % (fileName, fileName))
 770
 771    def run(self, args):
 772        if len(args) == 0:
 773            self.master = currentGitBranch()
 774            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
 775                die("Detecting current git branch failed!")
 776        elif len(args) == 1:
 777            self.master = args[0]
 778        else:
 779            return False
 780
 781        allowSubmit = gitConfig("git-p4.allowSubmit")
 782        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
 783            die("%s is not in git-p4.allowSubmit" % self.master)
 784
 785        [upstream, settings] = findUpstreamBranchPoint()
 786        self.depotPath = settings['depot-paths'][0]
 787        if len(self.origin) == 0:
 788            self.origin = upstream
 789
 790        if self.verbose:
 791            print "Origin branch is " + self.origin
 792
 793        if len(self.depotPath) == 0:
 794            print "Internal error: cannot locate perforce depot path from existing branches"
 795            sys.exit(128)
 796
 797        self.clientPath = p4Where(self.depotPath)
 798
 799        if len(self.clientPath) == 0:
 800            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
 801            sys.exit(128)
 802
 803        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
 804        self.oldWorkingDirectory = os.getcwd()
 805
 806        chdir(self.clientPath)
 807        print "Syncronizing p4 checkout..."
 808        p4_system("sync ...")
 809
 810        self.check()
 811
 812        commits = []
 813        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
 814            commits.append(line.strip())
 815        commits.reverse()
 816
 817        while len(commits) > 0:
 818            commit = commits[0]
 819            commits = commits[1:]
 820            self.applyCommit(commit)
 821            if not self.interactive:
 822                break
 823
 824        if len(commits) == 0:
 825            print "All changes applied!"
 826            chdir(self.oldWorkingDirectory)
 827
 828            sync = P4Sync()
 829            sync.run([])
 830
 831            rebase = P4Rebase()
 832            rebase.rebase()
 833
 834        return True
 835
 836class P4Sync(Command):
 837    def __init__(self):
 838        Command.__init__(self)
 839        self.options = [
 840                optparse.make_option("--branch", dest="branch"),
 841                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
 842                optparse.make_option("--changesfile", dest="changesFile"),
 843                optparse.make_option("--silent", dest="silent", action="store_true"),
 844                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
 845                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 846                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
 847                                     help="Import into refs/heads/ , not refs/remotes"),
 848                optparse.make_option("--max-changes", dest="maxChanges"),
 849                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
 850                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
 851                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
 852                                     help="Only sync files that are included in the Perforce Client Spec")
 853        ]
 854        self.description = """Imports from Perforce into a git repository.\n
 855    example:
 856    //depot/my/project/ -- to import the current head
 857    //depot/my/project/@all -- to import everything
 858    //depot/my/project/@1,6 -- to import only from revision 1 to 6
 859
 860    (a ... is not needed in the path p4 specification, it's added implicitly)"""
 861
 862        self.usage += " //depot/path[@revRange]"
 863        self.silent = False
 864        self.createdBranches = Set()
 865        self.committedChanges = Set()
 866        self.branch = ""
 867        self.detectBranches = False
 868        self.detectLabels = False
 869        self.changesFile = ""
 870        self.syncWithOrigin = True
 871        self.verbose = False
 872        self.importIntoRemotes = True
 873        self.maxChanges = ""
 874        self.isWindows = (platform.system() == "Windows")
 875        self.keepRepoPath = False
 876        self.depotPaths = None
 877        self.p4BranchesInGit = []
 878        self.cloneExclude = []
 879        self.useClientSpec = False
 880        self.clientSpecDirs = []
 881
 882        if gitConfig("git-p4.syncFromOrigin") == "false":
 883            self.syncWithOrigin = False
 884
 885    def extractFilesFromCommit(self, commit):
 886        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
 887                             for path in self.cloneExclude]
 888        files = []
 889        fnum = 0
 890        while commit.has_key("depotFile%s" % fnum):
 891            path =  commit["depotFile%s" % fnum]
 892
 893            if [p for p in self.cloneExclude
 894                if path.startswith (p)]:
 895                found = False
 896            else:
 897                found = [p for p in self.depotPaths
 898                         if path.startswith (p)]
 899            if not found:
 900                fnum = fnum + 1
 901                continue
 902
 903            file = {}
 904            file["path"] = path
 905            file["rev"] = commit["rev%s" % fnum]
 906            file["action"] = commit["action%s" % fnum]
 907            file["type"] = commit["type%s" % fnum]
 908            files.append(file)
 909            fnum = fnum + 1
 910        return files
 911
 912    def stripRepoPath(self, path, prefixes):
 913        if self.keepRepoPath:
 914            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
 915
 916        for p in prefixes:
 917            if path.startswith(p):
 918                path = path[len(p):]
 919
 920        return path
 921
 922    def splitFilesIntoBranches(self, commit):
 923        branches = {}
 924        fnum = 0
 925        while commit.has_key("depotFile%s" % fnum):
 926            path =  commit["depotFile%s" % fnum]
 927            found = [p for p in self.depotPaths
 928                     if path.startswith (p)]
 929            if not found:
 930                fnum = fnum + 1
 931                continue
 932
 933            file = {}
 934            file["path"] = path
 935            file["rev"] = commit["rev%s" % fnum]
 936            file["action"] = commit["action%s" % fnum]
 937            file["type"] = commit["type%s" % fnum]
 938            fnum = fnum + 1
 939
 940            relPath = self.stripRepoPath(path, self.depotPaths)
 941
 942            for branch in self.knownBranches.keys():
 943
 944                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
 945                if relPath.startswith(branch + "/"):
 946                    if branch not in branches:
 947                        branches[branch] = []
 948                    branches[branch].append(file)
 949                    break
 950
 951        return branches
 952
 953    ## Should move this out, doesn't use SELF.
 954    def readP4Files(self, files):
 955        filesForCommit = []
 956        filesToRead = []
 957
 958        for f in files:
 959            includeFile = True
 960            for val in self.clientSpecDirs:
 961                if f['path'].startswith(val[0]):
 962                    if val[1] <= 0:
 963                        includeFile = False
 964                    break
 965
 966            if includeFile:
 967                filesForCommit.append(f)
 968                if f['action'] not in ('delete', 'purge'):
 969                    filesToRead.append(f)
 970
 971        filedata = []
 972        if len(filesToRead) > 0:
 973            filedata = p4CmdList('-x - print',
 974                                 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
 975                                                  for f in filesToRead]),
 976                                 stdin_mode='w+')
 977
 978            if "p4ExitCode" in filedata[0]:
 979                die("Problems executing p4. Error: [%d]."
 980                    % (filedata[0]['p4ExitCode']));
 981
 982        j = 0;
 983        contents = {}
 984        while j < len(filedata):
 985            stat = filedata[j]
 986            j += 1
 987            text = ''
 988            while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
 989                text += filedata[j]['data']
 990                del filedata[j]['data']
 991                j += 1
 992
 993            if not stat.has_key('depotFile'):
 994                sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
 995                continue
 996
 997            if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
 998                text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
 999            elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1000                text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text)
1001
1002            contents[stat['depotFile']] = text
1003
1004        for f in filesForCommit:
1005            path = f['path']
1006            if contents.has_key(path):
1007                f['data'] = contents[path]
1008
1009        return filesForCommit
1010
1011    def commit(self, details, files, branch, branchPrefixes, parent = ""):
1012        epoch = details["time"]
1013        author = details["user"]
1014
1015        if self.verbose:
1016            print "commit into %s" % branch
1017
1018        # start with reading files; if that fails, we should not
1019        # create a commit.
1020        new_files = []
1021        for f in files:
1022            if [p for p in branchPrefixes if f['path'].startswith(p)]:
1023                new_files.append (f)
1024            else:
1025                sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
1026        files = self.readP4Files(new_files)
1027
1028        self.gitStream.write("commit %s\n" % branch)
1029#        gitStream.write("mark :%s\n" % details["change"])
1030        self.committedChanges.add(int(details["change"]))
1031        committer = ""
1032        if author not in self.users:
1033            self.getUserMapFromPerforceServer()
1034        if author in self.users:
1035            committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1036        else:
1037            committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1038
1039        self.gitStream.write("committer %s\n" % committer)
1040
1041        self.gitStream.write("data <<EOT\n")
1042        self.gitStream.write(details["desc"])
1043        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1044                             % (','.join (branchPrefixes), details["change"]))
1045        if len(details['options']) > 0:
1046            self.gitStream.write(": options = %s" % details['options'])
1047        self.gitStream.write("]\nEOT\n\n")
1048
1049        if len(parent) > 0:
1050            if self.verbose:
1051                print "parent %s" % parent
1052            self.gitStream.write("from %s\n" % parent)
1053
1054        for file in files:
1055            if file["type"] == "apple":
1056                print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1057                continue
1058
1059            relPath = self.stripRepoPath(file['path'], branchPrefixes)
1060            if file["action"] in ("delete", "purge"):
1061                self.gitStream.write("D %s\n" % relPath)
1062            else:
1063                data = file['data']
1064
1065                mode = "644"
1066                if isP4Exec(file["type"]):
1067                    mode = "755"
1068                elif file["type"] == "symlink":
1069                    mode = "120000"
1070                    # p4 print on a symlink contains "target\n", so strip it off
1071                    data = data[:-1]
1072
1073                if self.isWindows and file["type"].endswith("text"):
1074                    data = data.replace("\r\n", "\n")
1075
1076                self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1077                self.gitStream.write("data %s\n" % len(data))
1078                self.gitStream.write(data)
1079                self.gitStream.write("\n")
1080
1081        self.gitStream.write("\n")
1082
1083        change = int(details["change"])
1084
1085        if self.labels.has_key(change):
1086            label = self.labels[change]
1087            labelDetails = label[0]
1088            labelRevisions = label[1]
1089            if self.verbose:
1090                print "Change %s is labelled %s" % (change, labelDetails)
1091
1092            files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1093                                                    for p in branchPrefixes]))
1094
1095            if len(files) == len(labelRevisions):
1096
1097                cleanedFiles = {}
1098                for info in files:
1099                    if info["action"] in ("delete", "purge"):
1100                        continue
1101                    cleanedFiles[info["depotFile"]] = info["rev"]
1102
1103                if cleanedFiles == labelRevisions:
1104                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1105                    self.gitStream.write("from %s\n" % branch)
1106
1107                    owner = labelDetails["Owner"]
1108                    tagger = ""
1109                    if author in self.users:
1110                        tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1111                    else:
1112                        tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1113                    self.gitStream.write("tagger %s\n" % tagger)
1114                    self.gitStream.write("data <<EOT\n")
1115                    self.gitStream.write(labelDetails["Description"])
1116                    self.gitStream.write("EOT\n\n")
1117
1118                else:
1119                    if not self.silent:
1120                        print ("Tag %s does not match with change %s: files do not match."
1121                               % (labelDetails["label"], change))
1122
1123            else:
1124                if not self.silent:
1125                    print ("Tag %s does not match with change %s: file count is different."
1126                           % (labelDetails["label"], change))
1127
1128    def getUserCacheFilename(self):
1129        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1130        return home + "/.gitp4-usercache.txt"
1131
1132    def getUserMapFromPerforceServer(self):
1133        if self.userMapFromPerforceServer:
1134            return
1135        self.users = {}
1136
1137        for output in p4CmdList("users"):
1138            if not output.has_key("User"):
1139                continue
1140            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1141
1142
1143        s = ''
1144        for (key, val) in self.users.items():
1145            s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1146
1147        open(self.getUserCacheFilename(), "wb").write(s)
1148        self.userMapFromPerforceServer = True
1149
1150    def loadUserMapFromCache(self):
1151        self.users = {}
1152        self.userMapFromPerforceServer = False
1153        try:
1154            cache = open(self.getUserCacheFilename(), "rb")
1155            lines = cache.readlines()
1156            cache.close()
1157            for line in lines:
1158                entry = line.strip().split("\t")
1159                self.users[entry[0]] = entry[1]
1160        except IOError:
1161            self.getUserMapFromPerforceServer()
1162
1163    def getLabels(self):
1164        self.labels = {}
1165
1166        l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1167        if len(l) > 0 and not self.silent:
1168            print "Finding files belonging to labels in %s" % `self.depotPaths`
1169
1170        for output in l:
1171            label = output["label"]
1172            revisions = {}
1173            newestChange = 0
1174            if self.verbose:
1175                print "Querying files for label %s" % label
1176            for file in p4CmdList("files "
1177                                  +  ' '.join (["%s...@%s" % (p, label)
1178                                                for p in self.depotPaths])):
1179                revisions[file["depotFile"]] = file["rev"]
1180                change = int(file["change"])
1181                if change > newestChange:
1182                    newestChange = change
1183
1184            self.labels[newestChange] = [output, revisions]
1185
1186        if self.verbose:
1187            print "Label changes: %s" % self.labels.keys()
1188
1189    def guessProjectName(self):
1190        for p in self.depotPaths:
1191            if p.endswith("/"):
1192                p = p[:-1]
1193            p = p[p.strip().rfind("/") + 1:]
1194            if not p.endswith("/"):
1195               p += "/"
1196            return p
1197
1198    def getBranchMapping(self):
1199        lostAndFoundBranches = set()
1200
1201        for info in p4CmdList("branches"):
1202            details = p4Cmd("branch -o %s" % info["branch"])
1203            viewIdx = 0
1204            while details.has_key("View%s" % viewIdx):
1205                paths = details["View%s" % viewIdx].split(" ")
1206                viewIdx = viewIdx + 1
1207                # require standard //depot/foo/... //depot/bar/... mapping
1208                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1209                    continue
1210                source = paths[0]
1211                destination = paths[1]
1212                ## HACK
1213                if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1214                    source = source[len(self.depotPaths[0]):-4]
1215                    destination = destination[len(self.depotPaths[0]):-4]
1216
1217                    if destination in self.knownBranches:
1218                        if not self.silent:
1219                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1220                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1221                        continue
1222
1223                    self.knownBranches[destination] = source
1224
1225                    lostAndFoundBranches.discard(destination)
1226
1227                    if source not in self.knownBranches:
1228                        lostAndFoundBranches.add(source)
1229
1230
1231        for branch in lostAndFoundBranches:
1232            self.knownBranches[branch] = branch
1233
1234    def getBranchMappingFromGitBranches(self):
1235        branches = p4BranchesInGit(self.importIntoRemotes)
1236        for branch in branches.keys():
1237            if branch == "master":
1238                branch = "main"
1239            else:
1240                branch = branch[len(self.projectName):]
1241            self.knownBranches[branch] = branch
1242
1243    def listExistingP4GitBranches(self):
1244        # branches holds mapping from name to commit
1245        branches = p4BranchesInGit(self.importIntoRemotes)
1246        self.p4BranchesInGit = branches.keys()
1247        for branch in branches.keys():
1248            self.initialParents[self.refPrefix + branch] = branches[branch]
1249
1250    def updateOptionDict(self, d):
1251        option_keys = {}
1252        if self.keepRepoPath:
1253            option_keys['keepRepoPath'] = 1
1254
1255        d["options"] = ' '.join(sorted(option_keys.keys()))
1256
1257    def readOptions(self, d):
1258        self.keepRepoPath = (d.has_key('options')
1259                             and ('keepRepoPath' in d['options']))
1260
1261    def gitRefForBranch(self, branch):
1262        if branch == "main":
1263            return self.refPrefix + "master"
1264
1265        if len(branch) <= 0:
1266            return branch
1267
1268        return self.refPrefix + self.projectName + branch
1269
1270    def gitCommitByP4Change(self, ref, change):
1271        if self.verbose:
1272            print "looking in ref " + ref + " for change %s using bisect..." % change
1273
1274        earliestCommit = ""
1275        latestCommit = parseRevision(ref)
1276
1277        while True:
1278            if self.verbose:
1279                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1280            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1281            if len(next) == 0:
1282                if self.verbose:
1283                    print "argh"
1284                return ""
1285            log = extractLogMessageFromGitCommit(next)
1286            settings = extractSettingsGitLog(log)
1287            currentChange = int(settings['change'])
1288            if self.verbose:
1289                print "current change %s" % currentChange
1290
1291            if currentChange == change:
1292                if self.verbose:
1293                    print "found %s" % next
1294                return next
1295
1296            if currentChange < change:
1297                earliestCommit = "^%s" % next
1298            else:
1299                latestCommit = "%s" % next
1300
1301        return ""
1302
1303    def importNewBranch(self, branch, maxChange):
1304        # make fast-import flush all changes to disk and update the refs using the checkpoint
1305        # command so that we can try to find the branch parent in the git history
1306        self.gitStream.write("checkpoint\n\n");
1307        self.gitStream.flush();
1308        branchPrefix = self.depotPaths[0] + branch + "/"
1309        range = "@1,%s" % maxChange
1310        #print "prefix" + branchPrefix
1311        changes = p4ChangesForPaths([branchPrefix], range)
1312        if len(changes) <= 0:
1313            return False
1314        firstChange = changes[0]
1315        #print "first change in branch: %s" % firstChange
1316        sourceBranch = self.knownBranches[branch]
1317        sourceDepotPath = self.depotPaths[0] + sourceBranch
1318        sourceRef = self.gitRefForBranch(sourceBranch)
1319        #print "source " + sourceBranch
1320
1321        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1322        #print "branch parent: %s" % branchParentChange
1323        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1324        if len(gitParent) > 0:
1325            self.initialParents[self.gitRefForBranch(branch)] = gitParent
1326            #print "parent git commit: %s" % gitParent
1327
1328        self.importChanges(changes)
1329        return True
1330
1331    def importChanges(self, changes):
1332        cnt = 1
1333        for change in changes:
1334            description = p4Cmd("describe %s" % change)
1335            self.updateOptionDict(description)
1336
1337            if not self.silent:
1338                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1339                sys.stdout.flush()
1340            cnt = cnt + 1
1341
1342            try:
1343                if self.detectBranches:
1344                    branches = self.splitFilesIntoBranches(description)
1345                    for branch in branches.keys():
1346                        ## HACK  --hwn
1347                        branchPrefix = self.depotPaths[0] + branch + "/"
1348
1349                        parent = ""
1350
1351                        filesForCommit = branches[branch]
1352
1353                        if self.verbose:
1354                            print "branch is %s" % branch
1355
1356                        self.updatedBranches.add(branch)
1357
1358                        if branch not in self.createdBranches:
1359                            self.createdBranches.add(branch)
1360                            parent = self.knownBranches[branch]
1361                            if parent == branch:
1362                                parent = ""
1363                            else:
1364                                fullBranch = self.projectName + branch
1365                                if fullBranch not in self.p4BranchesInGit:
1366                                    if not self.silent:
1367                                        print("\n    Importing new branch %s" % fullBranch);
1368                                    if self.importNewBranch(branch, change - 1):
1369                                        parent = ""
1370                                        self.p4BranchesInGit.append(fullBranch)
1371                                    if not self.silent:
1372                                        print("\n    Resuming with change %s" % change);
1373
1374                                if self.verbose:
1375                                    print "parent determined through known branches: %s" % parent
1376
1377                        branch = self.gitRefForBranch(branch)
1378                        parent = self.gitRefForBranch(parent)
1379
1380                        if self.verbose:
1381                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1382
1383                        if len(parent) == 0 and branch in self.initialParents:
1384                            parent = self.initialParents[branch]
1385                            del self.initialParents[branch]
1386
1387                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1388                else:
1389                    files = self.extractFilesFromCommit(description)
1390                    self.commit(description, files, self.branch, self.depotPaths,
1391                                self.initialParent)
1392                    self.initialParent = ""
1393            except IOError:
1394                print self.gitError.read()
1395                sys.exit(1)
1396
1397    def importHeadRevision(self, revision):
1398        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1399
1400        details = { "user" : "git perforce import user", "time" : int(time.time()) }
1401        details["desc"] = ("Initial import of %s from the state at revision %s"
1402                           % (' '.join(self.depotPaths), revision))
1403        details["change"] = revision
1404        newestRevision = 0
1405
1406        fileCnt = 0
1407        for info in p4CmdList("files "
1408                              +  ' '.join(["%s...%s"
1409                                           % (p, revision)
1410                                           for p in self.depotPaths])):
1411
1412            if info['code'] == 'error':
1413                sys.stderr.write("p4 returned an error: %s\n"
1414                                 % info['data'])
1415                sys.exit(1)
1416
1417
1418            change = int(info["change"])
1419            if change > newestRevision:
1420                newestRevision = change
1421
1422            if info["action"] in ("delete", "purge"):
1423                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1424                #fileCnt = fileCnt + 1
1425                continue
1426
1427            for prop in ["depotFile", "rev", "action", "type" ]:
1428                details["%s%s" % (prop, fileCnt)] = info[prop]
1429
1430            fileCnt = fileCnt + 1
1431
1432        details["change"] = newestRevision
1433        self.updateOptionDict(details)
1434        try:
1435            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1436        except IOError:
1437            print "IO error with git fast-import. Is your git version recent enough?"
1438            print self.gitError.read()
1439
1440
1441    def getClientSpec(self):
1442        specList = p4CmdList( "client -o" )
1443        temp = {}
1444        for entry in specList:
1445            for k,v in entry.iteritems():
1446                if k.startswith("View"):
1447                    if v.startswith('"'):
1448                        start = 1
1449                    else:
1450                        start = 0
1451                    index = v.find("...")
1452                    v = v[start:index]
1453                    if v.startswith("-"):
1454                        v = v[1:]
1455                        temp[v] = -len(v)
1456                    else:
1457                        temp[v] = len(v)
1458        self.clientSpecDirs = temp.items()
1459        self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1460
1461    def run(self, args):
1462        self.depotPaths = []
1463        self.changeRange = ""
1464        self.initialParent = ""
1465        self.previousDepotPaths = []
1466
1467        # map from branch depot path to parent branch
1468        self.knownBranches = {}
1469        self.initialParents = {}
1470        self.hasOrigin = originP4BranchesExist()
1471        if not self.syncWithOrigin:
1472            self.hasOrigin = False
1473
1474        if self.importIntoRemotes:
1475            self.refPrefix = "refs/remotes/p4/"
1476        else:
1477            self.refPrefix = "refs/heads/p4/"
1478
1479        if self.syncWithOrigin and self.hasOrigin:
1480            if not self.silent:
1481                print "Syncing with origin first by calling git fetch origin"
1482            system("git fetch origin")
1483
1484        if len(self.branch) == 0:
1485            self.branch = self.refPrefix + "master"
1486            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1487                system("git update-ref %s refs/heads/p4" % self.branch)
1488                system("git branch -D p4");
1489            # create it /after/ importing, when master exists
1490            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1491                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1492
1493        if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1494            self.getClientSpec()
1495
1496        # TODO: should always look at previous commits,
1497        # merge with previous imports, if possible.
1498        if args == []:
1499            if self.hasOrigin:
1500                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1501            self.listExistingP4GitBranches()
1502
1503            if len(self.p4BranchesInGit) > 1:
1504                if not self.silent:
1505                    print "Importing from/into multiple branches"
1506                self.detectBranches = True
1507
1508            if self.verbose:
1509                print "branches: %s" % self.p4BranchesInGit
1510
1511            p4Change = 0
1512            for branch in self.p4BranchesInGit:
1513                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1514
1515                settings = extractSettingsGitLog(logMsg)
1516
1517                self.readOptions(settings)
1518                if (settings.has_key('depot-paths')
1519                    and settings.has_key ('change')):
1520                    change = int(settings['change']) + 1
1521                    p4Change = max(p4Change, change)
1522
1523                    depotPaths = sorted(settings['depot-paths'])
1524                    if self.previousDepotPaths == []:
1525                        self.previousDepotPaths = depotPaths
1526                    else:
1527                        paths = []
1528                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1529                            for i in range(0, min(len(cur), len(prev))):
1530                                if cur[i] <> prev[i]:
1531                                    i = i - 1
1532                                    break
1533
1534                            paths.append (cur[:i + 1])
1535
1536                        self.previousDepotPaths = paths
1537
1538            if p4Change > 0:
1539                self.depotPaths = sorted(self.previousDepotPaths)
1540                self.changeRange = "@%s,#head" % p4Change
1541                if not self.detectBranches:
1542                    self.initialParent = parseRevision(self.branch)
1543                if not self.silent and not self.detectBranches:
1544                    print "Performing incremental import into %s git branch" % self.branch
1545
1546        if not self.branch.startswith("refs/"):
1547            self.branch = "refs/heads/" + self.branch
1548
1549        if len(args) == 0 and self.depotPaths:
1550            if not self.silent:
1551                print "Depot paths: %s" % ' '.join(self.depotPaths)
1552        else:
1553            if self.depotPaths and self.depotPaths != args:
1554                print ("previous import used depot path %s and now %s was specified. "
1555                       "This doesn't work!" % (' '.join (self.depotPaths),
1556                                               ' '.join (args)))
1557                sys.exit(1)
1558
1559            self.depotPaths = sorted(args)
1560
1561        revision = ""
1562        self.users = {}
1563
1564        newPaths = []
1565        for p in self.depotPaths:
1566            if p.find("@") != -1:
1567                atIdx = p.index("@")
1568                self.changeRange = p[atIdx:]
1569                if self.changeRange == "@all":
1570                    self.changeRange = ""
1571                elif ',' not in self.changeRange:
1572                    revision = self.changeRange
1573                    self.changeRange = ""
1574                p = p[:atIdx]
1575            elif p.find("#") != -1:
1576                hashIdx = p.index("#")
1577                revision = p[hashIdx:]
1578                p = p[:hashIdx]
1579            elif self.previousDepotPaths == []:
1580                revision = "#head"
1581
1582            p = re.sub ("\.\.\.$", "", p)
1583            if not p.endswith("/"):
1584                p += "/"
1585
1586            newPaths.append(p)
1587
1588        self.depotPaths = newPaths
1589
1590
1591        self.loadUserMapFromCache()
1592        self.labels = {}
1593        if self.detectLabels:
1594            self.getLabels();
1595
1596        if self.detectBranches:
1597            ## FIXME - what's a P4 projectName ?
1598            self.projectName = self.guessProjectName()
1599
1600            if self.hasOrigin:
1601                self.getBranchMappingFromGitBranches()
1602            else:
1603                self.getBranchMapping()
1604            if self.verbose:
1605                print "p4-git branches: %s" % self.p4BranchesInGit
1606                print "initial parents: %s" % self.initialParents
1607            for b in self.p4BranchesInGit:
1608                if b != "master":
1609
1610                    ## FIXME
1611                    b = b[len(self.projectName):]
1612                self.createdBranches.add(b)
1613
1614        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1615
1616        importProcess = subprocess.Popen(["git", "fast-import"],
1617                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1618                                         stderr=subprocess.PIPE);
1619        self.gitOutput = importProcess.stdout
1620        self.gitStream = importProcess.stdin
1621        self.gitError = importProcess.stderr
1622
1623        if revision:
1624            self.importHeadRevision(revision)
1625        else:
1626            changes = []
1627
1628            if len(self.changesFile) > 0:
1629                output = open(self.changesFile).readlines()
1630                changeSet = Set()
1631                for line in output:
1632                    changeSet.add(int(line))
1633
1634                for change in changeSet:
1635                    changes.append(change)
1636
1637                changes.sort()
1638            else:
1639                if self.verbose:
1640                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1641                                                              self.changeRange)
1642                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1643
1644                if len(self.maxChanges) > 0:
1645                    changes = changes[:min(int(self.maxChanges), len(changes))]
1646
1647            if len(changes) == 0:
1648                if not self.silent:
1649                    print "No changes to import!"
1650                return True
1651
1652            if not self.silent and not self.detectBranches:
1653                print "Import destination: %s" % self.branch
1654
1655            self.updatedBranches = set()
1656
1657            self.importChanges(changes)
1658
1659            if not self.silent:
1660                print ""
1661                if len(self.updatedBranches) > 0:
1662                    sys.stdout.write("Updated branches: ")
1663                    for b in self.updatedBranches:
1664                        sys.stdout.write("%s " % b)
1665                    sys.stdout.write("\n")
1666
1667        self.gitStream.close()
1668        if importProcess.wait() != 0:
1669            die("fast-import failed: %s" % self.gitError.read())
1670        self.gitOutput.close()
1671        self.gitError.close()
1672
1673        return True
1674
1675class P4Rebase(Command):
1676    def __init__(self):
1677        Command.__init__(self)
1678        self.options = [ ]
1679        self.description = ("Fetches the latest revision from perforce and "
1680                            + "rebases the current work (branch) against it")
1681        self.verbose = False
1682
1683    def run(self, args):
1684        sync = P4Sync()
1685        sync.run([])
1686
1687        return self.rebase()
1688
1689    def rebase(self):
1690        if os.system("git update-index --refresh") != 0:
1691            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.");
1692        if len(read_pipe("git diff-index HEAD --")) > 0:
1693            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1694
1695        [upstream, settings] = findUpstreamBranchPoint()
1696        if len(upstream) == 0:
1697            die("Cannot find upstream branchpoint for rebase")
1698
1699        # the branchpoint may be p4/foo~3, so strip off the parent
1700        upstream = re.sub("~[0-9]+$", "", upstream)
1701
1702        print "Rebasing the current branch onto %s" % upstream
1703        oldHead = read_pipe("git rev-parse HEAD").strip()
1704        system("git rebase %s" % upstream)
1705        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1706        return True
1707
1708class P4Clone(P4Sync):
1709    def __init__(self):
1710        P4Sync.__init__(self)
1711        self.description = "Creates a new git repository and imports from Perforce into it"
1712        self.usage = "usage: %prog [options] //depot/path[@revRange]"
1713        self.options += [
1714            optparse.make_option("--destination", dest="cloneDestination",
1715                                 action='store', default=None,
1716                                 help="where to leave result of the clone"),
1717            optparse.make_option("-/", dest="cloneExclude",
1718                                 action="append", type="string",
1719                                 help="exclude depot path")
1720        ]
1721        self.cloneDestination = None
1722        self.needsGit = False
1723
1724    # This is required for the "append" cloneExclude action
1725    def ensure_value(self, attr, value):
1726        if not hasattr(self, attr) or getattr(self, attr) is None:
1727            setattr(self, attr, value)
1728        return getattr(self, attr)
1729
1730    def defaultDestination(self, args):
1731        ## TODO: use common prefix of args?
1732        depotPath = args[0]
1733        depotDir = re.sub("(@[^@]*)$", "", depotPath)
1734        depotDir = re.sub("(#[^#]*)$", "", depotDir)
1735        depotDir = re.sub(r"\.\.\.$", "", depotDir)
1736        depotDir = re.sub(r"/$", "", depotDir)
1737        return os.path.split(depotDir)[1]
1738
1739    def run(self, args):
1740        if len(args) < 1:
1741            return False
1742
1743        if self.keepRepoPath and not self.cloneDestination:
1744            sys.stderr.write("Must specify destination for --keep-path\n")
1745            sys.exit(1)
1746
1747        depotPaths = args
1748
1749        if not self.cloneDestination and len(depotPaths) > 1:
1750            self.cloneDestination = depotPaths[-1]
1751            depotPaths = depotPaths[:-1]
1752
1753        self.cloneExclude = ["/"+p for p in self.cloneExclude]
1754        for p in depotPaths:
1755            if not p.startswith("//"):
1756                return False
1757
1758        if not self.cloneDestination:
1759            self.cloneDestination = self.defaultDestination(args)
1760
1761        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1762        if not os.path.exists(self.cloneDestination):
1763            os.makedirs(self.cloneDestination)
1764        chdir(self.cloneDestination)
1765        system("git init")
1766        self.gitdir = os.getcwd() + "/.git"
1767        if not P4Sync.run(self, depotPaths):
1768            return False
1769        if self.branch != "master":
1770            if self.importIntoRemotes:
1771                masterbranch = "refs/remotes/p4/master"
1772            else:
1773                masterbranch = "refs/heads/p4/master"
1774            if gitBranchExists(masterbranch):
1775                system("git branch master %s" % masterbranch)
1776                system("git checkout -f")
1777            else:
1778                print "Could not detect main branch. No checkout/master branch created."
1779
1780        return True
1781
1782class P4Branches(Command):
1783    def __init__(self):
1784        Command.__init__(self)
1785        self.options = [ ]
1786        self.description = ("Shows the git branches that hold imports and their "
1787                            + "corresponding perforce depot paths")
1788        self.verbose = False
1789
1790    def run(self, args):
1791        if originP4BranchesExist():
1792            createOrUpdateBranchesFromOrigin()
1793
1794        cmdline = "git rev-parse --symbolic "
1795        cmdline += " --remotes"
1796
1797        for line in read_pipe_lines(cmdline):
1798            line = line.strip()
1799
1800            if not line.startswith('p4/') or line == "p4/HEAD":
1801                continue
1802            branch = line
1803
1804            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1805            settings = extractSettingsGitLog(log)
1806
1807            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1808        return True
1809
1810class HelpFormatter(optparse.IndentedHelpFormatter):
1811    def __init__(self):
1812        optparse.IndentedHelpFormatter.__init__(self)
1813
1814    def format_description(self, description):
1815        if description:
1816            return description + "\n"
1817        else:
1818            return ""
1819
1820def printUsage(commands):
1821    print "usage: %s <command> [options]" % sys.argv[0]
1822    print ""
1823    print "valid commands: %s" % ", ".join(commands)
1824    print ""
1825    print "Try %s <command> --help for command specific help." % sys.argv[0]
1826    print ""
1827
1828commands = {
1829    "debug" : P4Debug,
1830    "submit" : P4Submit,
1831    "commit" : P4Submit,
1832    "sync" : P4Sync,
1833    "rebase" : P4Rebase,
1834    "clone" : P4Clone,
1835    "rollback" : P4RollBack,
1836    "branches" : P4Branches
1837}
1838
1839
1840def main():
1841    if len(sys.argv[1:]) == 0:
1842        printUsage(commands.keys())
1843        sys.exit(2)
1844
1845    cmd = ""
1846    cmdName = sys.argv[1]
1847    try:
1848        klass = commands[cmdName]
1849        cmd = klass()
1850    except KeyError:
1851        print "unknown command %s" % cmdName
1852        print ""
1853        printUsage(commands.keys())
1854        sys.exit(2)
1855
1856    options = cmd.options
1857    cmd.gitdir = os.environ.get("GIT_DIR", None)
1858
1859    args = sys.argv[2:]
1860
1861    if len(options) > 0:
1862        options.append(optparse.make_option("--git-dir", dest="gitdir"))
1863
1864        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1865                                       options,
1866                                       description = cmd.description,
1867                                       formatter = HelpFormatter())
1868
1869        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1870    global verbose
1871    verbose = cmd.verbose
1872    if cmd.needsGit:
1873        if cmd.gitdir == None:
1874            cmd.gitdir = os.path.abspath(".git")
1875            if not isValidGitDir(cmd.gitdir):
1876                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1877                if os.path.exists(cmd.gitdir):
1878                    cdup = read_pipe("git rev-parse --show-cdup").strip()
1879                    if len(cdup) > 0:
1880                        chdir(cdup);
1881
1882        if not isValidGitDir(cmd.gitdir):
1883            if isValidGitDir(cmd.gitdir + "/.git"):
1884                cmd.gitdir += "/.git"
1885            else:
1886                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1887
1888        os.environ["GIT_DIR"] = cmd.gitdir
1889
1890    if not cmd.run(args):
1891        parser.print_help()
1892
1893
1894if __name__ == '__main__':
1895    main()