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