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