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