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