git-p4 65.8 KB
Newer Older
1 2 3 4
#!/usr/bin/env python
#
# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
#
5 6
# Author: Simon Hausmann <simon@lst.de>
# Copyright: 2007 Simon Hausmann <simon@lst.de>
7
#            2007 Trolltech ASA
8 9 10
# License: MIT <http://www.opensource.org/licenses/mit-license.php>
#

11
import optparse, sys, os, marshal, popen2, subprocess, shelve
12
import tempfile, getopt, sha, os.path, time, platform
H
Han-Wen Nienhuys 已提交
13
import re
14

15
from sets import Set;
16

17
verbose = False
18

19 20 21 22 23 24 25 26

def p4_build_cmd(cmd):
    """Build a suitable p4 command line.

    This consolidates building and returning a p4 command line into one
    location. It means that hooking into the environment, or other configuration
    can be done more easily.
    """
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
    real_cmd = "%s " % "p4"

    user = gitConfig("git-p4.user")
    if len(user) > 0:
        real_cmd += "-u %s " % user

    password = gitConfig("git-p4.password")
    if len(password) > 0:
        real_cmd += "-P %s " % password

    port = gitConfig("git-p4.port")
    if len(port) > 0:
        real_cmd += "-p %s " % port

    host = gitConfig("git-p4.host")
    if len(host) > 0:
        real_cmd += "-h %s " % host

    client = gitConfig("git-p4.client")
    if len(client) > 0:
        real_cmd += "-c %s " % client

    real_cmd += "%s" % (cmd)
50 51
    if verbose:
        print real_cmd
52 53
    return real_cmd

54 55 56 57 58 59 60
def die(msg):
    if verbose:
        raise Exception(msg)
    else:
        sys.stderr.write(msg + "\n")
        sys.exit(1)

H
cleanup  
Han-Wen Nienhuys 已提交
61
def write_pipe(c, str):
62
    if verbose:
63
        sys.stderr.write('Writing pipe: %s\n' % c)
H
Han-Wen Nienhuys 已提交
64

H
cleanup  
Han-Wen Nienhuys 已提交
65
    pipe = os.popen(c, 'w')
H
Han-Wen Nienhuys 已提交
66
    val = pipe.write(str)
H
cleanup  
Han-Wen Nienhuys 已提交
67
    if pipe.close():
68
        die('Command failed: %s' % c)
H
Han-Wen Nienhuys 已提交
69 70 71

    return val

72 73 74 75
def p4_write_pipe(c, str):
    real_cmd = p4_build_cmd(c)
    return write_pipe(c, str)

76 77
def read_pipe(c, ignore_error=False):
    if verbose:
78
        sys.stderr.write('Reading pipe: %s\n' % c)
79

H
cleanup  
Han-Wen Nienhuys 已提交
80
    pipe = os.popen(c, 'rb')
H
Han-Wen Nienhuys 已提交
81
    val = pipe.read()
82
    if pipe.close() and not ignore_error:
83
        die('Command failed: %s' % c)
H
Han-Wen Nienhuys 已提交
84 85 86

    return val

87 88 89
def p4_read_pipe(c, ignore_error=False):
    real_cmd = p4_build_cmd(c)
    return read_pipe(real_cmd, ignore_error)
H
Han-Wen Nienhuys 已提交
90

H
cleanup  
Han-Wen Nienhuys 已提交
91
def read_pipe_lines(c):
92
    if verbose:
93
        sys.stderr.write('Reading pipe: %s\n' % c)
H
Han-Wen Nienhuys 已提交
94
    ## todo: check return status
H
cleanup  
Han-Wen Nienhuys 已提交
95
    pipe = os.popen(c, 'rb')
H
Han-Wen Nienhuys 已提交
96
    val = pipe.readlines()
H
cleanup  
Han-Wen Nienhuys 已提交
97
    if pipe.close():
98
        die('Command failed: %s' % c)
H
Han-Wen Nienhuys 已提交
99 100

    return val
101

102 103
def p4_read_pipe_lines(c):
    """Specifically invoke p4 on the command supplied. """
A
Anand Kumria 已提交
104
    real_cmd = p4_build_cmd(c)
105 106
    return read_pipe_lines(real_cmd)

H
Han-Wen Nienhuys 已提交
107
def system(cmd):
108
    if verbose:
H
Han-Wen Nienhuys 已提交
109
        sys.stderr.write("executing %s\n" % cmd)
H
Han-Wen Nienhuys 已提交
110 111 112
    if os.system(cmd) != 0:
        die("command failed: %s" % cmd)

113 114
def p4_system(cmd):
    """Specifically invoke p4 as the system command. """
A
Anand Kumria 已提交
115
    real_cmd = p4_build_cmd(cmd)
116 117
    return system(real_cmd)

D
David Brown 已提交
118 119 120 121 122 123 124 125
def isP4Exec(kind):
    """Determine if a Perforce 'kind' should have execute permission

    'p4 help filetypes' gives a list of the types.  If it starts with 'x',
    or x follows one of a few letters.  Otherwise, if there is an 'x' after
    a plus sign, it is also executable"""
    return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)

126 127 128 129 130 131 132 133 134 135 136 137 138
def setP4ExecBit(file, mode):
    # Reopens an already open file and changes the execute bit to match
    # the execute bit setting in the passed in mode.

    p4Type = "+x"

    if not isModeExec(mode):
        p4Type = getP4OpenedType(file)
        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
        if p4Type[-1] == "+":
            p4Type = p4Type[0:-1]

139
    p4_system("reopen -t %s %s" % (p4Type, file))
140 141 142 143

def getP4OpenedType(file):
    # Returns the perforce file type for the given file.

144
    result = p4_read_pipe("opened %s" % file)
145
    match = re.match(".*\((.+)\)\r?$", result)
146 147 148
    if match:
        return match.group(1)
    else:
149
        die("Could not determine file type for %s (result: '%s')" % (file, result))
150

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
def diffTreePattern():
    # This is a simple generator for the diff tree regex pattern. This could be
    # a class variable if this and parseDiffTreeEntry were a part of a class.
    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
    while True:
        yield pattern

def parseDiffTreeEntry(entry):
    """Parses a single diff tree entry into its component elements.

    See git-diff-tree(1) manpage for details about the format of the diff
    output. This method returns a dictionary with the following elements:

    src_mode - The mode of the source file
    dst_mode - The mode of the destination file
    src_sha1 - The sha1 for the source file
    dst_sha1 - The sha1 fr the destination file
    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
    status_score - The score for the status (applicable for 'C' and 'R'
                   statuses). This is None if there is no score.
    src - The path for the source file.
    dst - The path for the destination file. This is only present for
          copy or renames. If it is not present, this is None.

    If the pattern is not matched, None is returned."""

    match = diffTreePattern().next().match(entry)
    if match:
        return {
            'src_mode': match.group(1),
            'dst_mode': match.group(2),
            'src_sha1': match.group(3),
            'dst_sha1': match.group(4),
            'status': match.group(5),
            'status_score': match.group(6),
            'src': match.group(7),
            'dst': match.group(10)
        }
    return None

191 192 193 194 195 196 197 198
def isModeExec(mode):
    # Returns True if the given git mode represents an executable file,
    # otherwise False.
    return mode[-3:] == "755"

def isModeExecChanged(src_mode, dst_mode):
    return isModeExec(src_mode) != isModeExec(dst_mode)

S
Scott Lamb 已提交
199
def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
A
Anand Kumria 已提交
200
    cmd = p4_build_cmd("-G %s" % (cmd))
H
Han-Wen Nienhuys 已提交
201 202
    if verbose:
        sys.stderr.write("Opening pipe: %s\n" % cmd)
S
Scott Lamb 已提交
203 204 205 206 207 208 209 210 211 212 213 214 215 216

    # Use a temporary file to avoid deadlocks without
    # subprocess.communicate(), which would put another copy
    # of stdout into memory.
    stdin_file = None
    if stdin is not None:
        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
        stdin_file.write(stdin)
        stdin_file.flush()
        stdin_file.seek(0)

    p4 = subprocess.Popen(cmd, shell=True,
                          stdin=stdin_file,
                          stdout=subprocess.PIPE)
217 218 219 220

    result = []
    try:
        while True:
S
Scott Lamb 已提交
221
            entry = marshal.load(p4.stdout)
222 223 224
            result.append(entry)
    except EOFError:
        pass
S
Scott Lamb 已提交
225 226
    exitCode = p4.wait()
    if exitCode != 0:
227 228 229
        entry = {}
        entry["p4ExitCode"] = exitCode
        result.append(entry)
230 231 232 233 234 235 236 237 238 239

    return result

def p4Cmd(cmd):
    list = p4CmdList(cmd)
    result = {}
    for entry in list:
        result.update(entry)
    return result;

240 241 242 243
def p4Where(depotPath):
    if not depotPath.endswith("/"):
        depotPath += "/"
    output = p4Cmd("where %s..." % depotPath)
244 245
    if output["code"] == "error":
        return ""
246 247 248 249 250 251 252 253 254 255 256 257
    clientPath = ""
    if "path" in output:
        clientPath = output.get("path")
    elif "data" in output:
        data = output.get("data")
        lastSpace = data.rfind(" ")
        clientPath = data[lastSpace + 1:]

    if clientPath.endswith("..."):
        clientPath = clientPath[:-3]
    return clientPath

258
def currentGitBranch():
259
    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
260

261
def isValidGitDir(path):
H
Han-Wen Nienhuys 已提交
262 263
    if (os.path.exists(path + "/HEAD")
        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
264 265 266
        return True;
    return False

267
def parseRevision(ref):
268
    return read_pipe("git rev-parse %s" % ref).strip()
269

270 271
def extractLogMessageFromGitCommit(commit):
    logMessage = ""
H
Han-Wen Nienhuys 已提交
272 273

    ## fixme: title is first line of commit, not 1st paragraph.
274
    foundTitle = False
H
Han-Wen Nienhuys 已提交
275
    for log in read_pipe_lines("git cat-file commit %s" % commit):
276 277
       if not foundTitle:
           if len(log) == 1:
S
Simon Hausmann 已提交
278
               foundTitle = True
279 280 281 282 283
           continue

       logMessage += log
    return logMessage

H
Han-Wen Nienhuys 已提交
284
def extractSettingsGitLog(log):
285 286 287
    values = {}
    for line in log.split("\n"):
        line = line.strip()
288 289 290 291 292 293 294 295 296 297 298 299 300 301
        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
        if not m:
            continue

        assignments = m.group(1).split (':')
        for a in assignments:
            vals = a.split ('=')
            key = vals[0].strip()
            val = ('='.join (vals[1:])).strip()
            if val.endswith ('\"') and val.startswith('"'):
                val = val[1:-1]

            values[key] = val

302 303 304
    paths = values.get("depot-paths")
    if not paths:
        paths = values.get("depot-path")
305 306
    if paths:
        values['depot-paths'] = paths.split(',')
H
Han-Wen Nienhuys 已提交
307
    return values
308

309
def gitBranchExists(branch):
H
Han-Wen Nienhuys 已提交
310 311
    proc = subprocess.Popen(["git", "rev-parse", branch],
                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
312
    return proc.wait() == 0;
313

314
def gitConfig(key):
315
    return read_pipe("git config %s" % key, ignore_error=True).strip()
316

317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
def p4BranchesInGit(branchesAreInRemotes = True):
    branches = {}

    cmdline = "git rev-parse --symbolic "
    if branchesAreInRemotes:
        cmdline += " --remotes"
    else:
        cmdline += " --branches"

    for line in read_pipe_lines(cmdline):
        line = line.strip()

        ## only import to p4/
        if not line.startswith('p4/') or line == "p4/HEAD":
            continue
        branch = line

        # strip off p4
        branch = re.sub ("^p4/", "", line)

        branches[branch] = parseRevision(line)
    return branches

340
def findUpstreamBranchPoint(head = "HEAD"):
341 342 343 344 345 346 347 348 349 350 351
    branches = p4BranchesInGit()
    # map from depot-path to branch name
    branchByDepotPath = {}
    for branch in branches.keys():
        tip = branches[branch]
        log = extractLogMessageFromGitCommit(tip)
        settings = extractSettingsGitLog(log)
        if settings.has_key("depot-paths"):
            paths = ",".join(settings["depot-paths"])
            branchByDepotPath[paths] = "remotes/p4/" + branch

352 353 354
    settings = None
    parent = 0
    while parent < 65535:
355
        commit = head + "~%s" % parent
356 357
        log = extractLogMessageFromGitCommit(commit)
        settings = extractSettingsGitLog(log)
358 359 360 361
        if settings.has_key("depot-paths"):
            paths = ",".join(settings["depot-paths"])
            if branchByDepotPath.has_key(paths):
                return [branchByDepotPath[paths], settings]
362

363
        parent = parent + 1
364

365
    return ["", settings]
366

367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
    if not silent:
        print ("Creating/updating branch(es) in %s based on origin branch(es)"
               % localRefPrefix)

    originPrefix = "origin/p4/"

    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
        line = line.strip()
        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
            continue

        headName = line[len(originPrefix):]
        remoteHead = localRefPrefix + headName
        originHead = line

        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
        if (not original.has_key('depot-paths')
            or not original.has_key('change')):
            continue

        update = False
        if not gitBranchExists(remoteHead):
            if verbose:
                print "creating %s" % remoteHead
            update = True
        else:
            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
            if settings.has_key('change') > 0:
                if settings['depot-paths'] == original['depot-paths']:
                    originP4Change = int(original['change'])
                    p4Change = int(settings['change'])
                    if originP4Change > p4Change:
                        print ("%s (%s) is newer than %s (%s). "
                               "Updating p4 branch from origin."
                               % (originHead, originP4Change,
                                  remoteHead, p4Change))
                        update = True
                else:
                    print ("Ignoring: %s was imported from %s while "
                           "%s was imported from %s"
                           % (originHead, ','.join(original['depot-paths']),
                              remoteHead, ','.join(settings['depot-paths'])))

        if update:
            system("git update-ref %s %s" % (remoteHead, originHead))

def originP4BranchesExist():
        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")

417 418
def p4ChangesForPaths(depotPaths, changeRange):
    assert depotPaths
419
    output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
420 421 422 423 424 425 426 427 428 429
                                                        for p in depotPaths]))

    changes = []
    for line in output:
        changeNum = line.split(" ")[1]
        changes.append(int(changeNum))

    changes.sort()
    return changes

430 431 432
class Command:
    def __init__(self):
        self.usage = "usage: %prog [options]"
433
        self.needsGit = True
434 435

class P4Debug(Command):
436
    def __init__(self):
437
        Command.__init__(self)
438
        self.options = [
439 440
            optparse.make_option("--verbose", dest="verbose", action="store_true",
                                 default=False),
441
            ]
442
        self.description = "A tool to debug the output of p4 -G."
443
        self.needsGit = False
444
        self.verbose = False
445 446

    def run(self, args):
447
        j = 0
448
        for output in p4CmdList(" ".join(args)):
449 450
            print 'Element: %d' % j
            j += 1
451
            print output
452
        return True
453

454 455 456 457
class P4RollBack(Command):
    def __init__(self):
        Command.__init__(self)
        self.options = [
458 459
            optparse.make_option("--verbose", dest="verbose", action="store_true"),
            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
460 461
        ]
        self.description = "A tool to debug the multi-branch import. Don't use :)"
462
        self.verbose = False
463
        self.rollbackLocalBranches = False
464 465 466 467 468

    def run(self, args):
        if len(args) != 1:
            return False
        maxChange = int(args[0])
469

470
        if "p4ExitCode" in p4Cmd("changes -m 1"):
471 472
            die("Problems executing p4");

473 474
        if self.rollbackLocalBranches:
            refPrefix = "refs/heads/"
H
Han-Wen Nienhuys 已提交
475
            lines = read_pipe_lines("git rev-parse --symbolic --branches")
476 477
        else:
            refPrefix = "refs/remotes/"
H
Han-Wen Nienhuys 已提交
478
            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
479 480 481

        for line in lines:
            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
482 483
                line = line.strip()
                ref = refPrefix + line
484
                log = extractLogMessageFromGitCommit(ref)
H
Han-Wen Nienhuys 已提交
485 486 487 488 489
                settings = extractSettingsGitLog(log)

                depotPaths = settings['depot-paths']
                change = settings['change']

490
                changed = False
491

492 493
                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
                                                           for p in depotPaths]))) == 0:
494 495 496 497
                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
                    continue

H
Han-Wen Nienhuys 已提交
498
                while change and int(change) > maxChange:
499
                    changed = True
500 501
                    if self.verbose:
                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
502 503
                    system("git update-ref %s \"%s^\"" % (ref, ref))
                    log = extractLogMessageFromGitCommit(ref)
H
Han-Wen Nienhuys 已提交
504 505 506 507 508
                    settings =  extractSettingsGitLog(log)


                    depotPaths = settings['depot-paths']
                    change = settings['change']
509 510

                if changed:
511
                    print "%s rewound to %s" % (ref, change)
512 513 514

        return True

S
Simon Hausmann 已提交
515
class P4Submit(Command):
516
    def __init__(self):
517
        Command.__init__(self)
518
        self.options = [
519
                optparse.make_option("--verbose", dest="verbose", action="store_true"),
520
                optparse.make_option("--origin", dest="origin"),
521
                optparse.make_option("-M", dest="detectRename", action="store_true"),
522 523
        ]
        self.description = "Submit changes from git to the perforce depot."
524
        self.usage += " [name of git branch to submit into perforce depot]"
525
        self.interactive = True
526
        self.origin = ""
527
        self.detectRename = False
S
Simon Hausmann 已提交
528
        self.verbose = False
529
        self.isWindows = (platform.system() == "Windows")
530 531 532 533 534

    def check(self):
        if len(p4CmdList("opened ...")) > 0:
            die("You have files opened with perforce! Close them before starting the sync.")

535 536
    # replaces everything between 'Description:' and the next P4 submit template field with the
    # commit message
537 538 539
    def prepareLogMessage(self, template, message):
        result = ""

540 541
        inDescriptionSection = False

542 543 544 545 546
        for line in template.split("\n"):
            if line.startswith("#"):
                result += line + "\n"
                continue

547 548 549 550 551 552 553 554 555 556 557 558 559
            if inDescriptionSection:
                if line.startswith("Files:"):
                    inDescriptionSection = False
                else:
                    continue
            else:
                if line.startswith("Description:"):
                    inDescriptionSection = True
                    line += "\n"
                    for messageLine in message.split("\n"):
                        line += "\t" + messageLine + "\n"

            result += line + "\n"
560 561 562

        return result

563 564 565 566
    def prepareSubmitTemplate(self):
        # remove lines in the Files section that show changes to files outside the depot path we're committing into
        template = ""
        inFilesSection = False
567
        for line in p4_read_pipe_lines("change -o"):
568 569
            if line.endswith("\r\n"):
                line = line[:-2] + "\n"
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588
            if inFilesSection:
                if line.startswith("\t"):
                    # path starts and ends with a tab
                    path = line[1:]
                    lastTab = path.rfind("\t")
                    if lastTab != -1:
                        path = path[:lastTab]
                        if not path.startswith(self.depotPath):
                            continue
                else:
                    inFilesSection = False
            else:
                if line.startswith("Files:"):
                    inFilesSection = True

            template += line

        return template

589
    def applyCommit(self, id):
590 591 592
        print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
        diffOpts = ("", "-M")[self.detectRename]
        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
593 594
        filesToAdd = set()
        filesToDelete = set()
595
        editedFiles = set()
596
        filesToChangeExecBit = {}
597
        for line in diff:
598 599 600
            diff = parseDiffTreeEntry(line)
            modifier = diff['status']
            path = diff['src']
601
            if modifier == "M":
602
                p4_system("edit \"%s\"" % path)
603 604
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
                    filesToChangeExecBit[path] = diff['dst_mode']
605
                editedFiles.add(path)
606 607
            elif modifier == "A":
                filesToAdd.add(path)
608
                filesToChangeExecBit[path] = diff['dst_mode']
609 610 611 612 613 614
                if path in filesToDelete:
                    filesToDelete.remove(path)
            elif modifier == "D":
                filesToDelete.add(path)
                if path in filesToAdd:
                    filesToAdd.remove(path)
615
            elif modifier == "R":
616
                src, dest = diff['src'], diff['dst']
617 618
                p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
                p4_system("edit \"%s\"" % (dest))
619 620
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
                    filesToChangeExecBit[dest] = diff['dst_mode']
621 622 623
                os.unlink(dest)
                editedFiles.add(dest)
                filesToDelete.add(src)
624 625 626
            else:
                die("unknown modifier %s for %s" % (modifier, path))

627
        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
628
        patchcmd = diffcmd + " | git apply "
629 630
        tryPatchCmd = patchcmd + "--check -"
        applyPatchCmd = patchcmd + "--check --apply -"
631

632
        if os.system(tryPatchCmd) != 0:
633 634 635 636
            print "Unfortunately applying the change failed!"
            print "What do you want to do?"
            response = "x"
            while response != "s" and response != "a" and response != "w":
637 638
                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
639 640
            if response == "s":
                print "Skipping! Good luck with the next patches..."
641
                for f in editedFiles:
642
                    p4_system("revert \"%s\"" % f);
643 644
                for f in filesToAdd:
                    system("rm %s" %f)
645 646
                return
            elif response == "a":
647
                os.system(applyPatchCmd)
648 649 650 651 652 653
                if len(filesToAdd) > 0:
                    print "You may also want to call p4 add on the following files:"
                    print " ".join(filesToAdd)
                if len(filesToDelete):
                    print "The following files should be scheduled for deletion with p4 delete:"
                    print " ".join(filesToDelete)
654 655
                die("Please resolve and submit the conflict manually and "
                    + "continue afterwards with git-p4 submit --continue")
656 657 658
            elif response == "w":
                system(diffcmd + " > patch.txt")
                print "Patch saved to patch.txt in %s !" % self.clientPath
659 660
                die("Please resolve and submit the conflict manually and "
                    "continue afterwards with git-p4 submit --continue")
661

662
        system(applyPatchCmd)
663 664

        for f in filesToAdd:
665
            p4_system("add \"%s\"" % f)
666
        for f in filesToDelete:
667 668
            p4_system("revert \"%s\"" % f)
            p4_system("delete \"%s\"" % f)
669

670 671 672 673 674
        # Set/clear executable bits
        for f in filesToChangeExecBit.keys():
            mode = filesToChangeExecBit[f]
            setP4ExecBit(f, mode)

675 676
        logMessage = extractLogMessageFromGitCommit(id)
        logMessage = logMessage.strip()
677

678
        template = self.prepareSubmitTemplate()
679 680 681

        if self.interactive:
            submitTemplate = self.prepareLogMessage(template, logMessage)
682 683
            if os.environ.has_key("P4DIFF"):
                del(os.environ["P4DIFF"])
684
            diff = p4_read_pipe("diff -du ...")
685

686
            newdiff = ""
687
            for newFile in filesToAdd:
688 689 690
                newdiff += "==== new file ====\n"
                newdiff += "--- /dev/null\n"
                newdiff += "+++ %s\n" % newFile
691 692
                f = open(newFile, "r")
                for line in f.readlines():
693
                    newdiff += "+" + line
694 695
                f.close()

696
            separatorLine = "######## everything below this line is just the diff #######\n"
697

698 699
            [handle, fileName] = tempfile.mkstemp()
            tmpFile = os.fdopen(handle, "w+")
700 701 702 703 704
            if self.isWindows:
                submitTemplate = submitTemplate.replace("\n", "\r\n")
                separatorLine = separatorLine.replace("\n", "\r\n")
                newdiff = newdiff.replace("\n", "\r\n")
            tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
705 706 707 708
            tmpFile.close()
            defaultEditor = "vi"
            if platform.system() == "Windows":
                defaultEditor = "notepad"
709 710 711 712
            if os.environ.has_key("P4EDITOR"):
                editor = os.environ.get("P4EDITOR")
            else:
                editor = os.environ.get("EDITOR", defaultEditor);
713 714 715 716 717 718 719 720 721
            system(editor + " " + fileName)
            tmpFile = open(fileName, "rb")
            message = tmpFile.read()
            tmpFile.close()
            os.remove(fileName)
            submitTemplate = message[:message.index(separatorLine)]
            if self.isWindows:
                submitTemplate = submitTemplate.replace("\r\n", "\n")

722
            p4_write_pipe("submit -i", submitTemplate)
723 724 725 726 727
        else:
            fileName = "submit.txt"
            file = open(fileName, "w+")
            file.write(self.prepareLogMessage(template, logMessage))
            file.close()
728 729 730
            print ("Perforce submit template written as %s. "
                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
                   % (fileName, fileName))
731 732

    def run(self, args):
733 734
        if len(args) == 0:
            self.master = currentGitBranch()
735
            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
736 737 738 739 740 741
                die("Detecting current git branch failed!")
        elif len(args) == 1:
            self.master = args[0]
        else:
            return False

J
Jing Xue 已提交
742 743 744 745
        allowSubmit = gitConfig("git-p4.allowSubmit")
        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
            die("%s is not in git-p4.allowSubmit" % self.master)

746
        [upstream, settings] = findUpstreamBranchPoint()
747
        self.depotPath = settings['depot-paths'][0]
748 749
        if len(self.origin) == 0:
            self.origin = upstream
750 751 752

        if self.verbose:
            print "Origin branch is " + self.origin
753

754
        if len(self.depotPath) == 0:
755 756 757
            print "Internal error: cannot locate perforce depot path from existing branches"
            sys.exit(128)

758
        self.clientPath = p4Where(self.depotPath)
759

760
        if len(self.clientPath) == 0:
761
            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
762 763
            sys.exit(128)

764
        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
765
        self.oldWorkingDirectory = os.getcwd()
766

767
        os.chdir(self.clientPath)
768
        print "Syncronizing p4 checkout..."
769
        p4_system("sync ...")
770

771 772
        self.check()

S
Simon Hausmann 已提交
773 774 775 776
        commits = []
        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
            commits.append(line.strip())
        commits.reverse()
777 778 779 780

        while len(commits) > 0:
            commit = commits[0]
            commits = commits[1:]
781
            self.applyCommit(commit)
782 783 784 785
            if not self.interactive:
                break

        if len(commits) == 0:
S
Simon Hausmann 已提交
786 787
            print "All changes applied!"
            os.chdir(self.oldWorkingDirectory)
788

S
Simon Hausmann 已提交
789 790
            sync = P4Sync()
            sync.run([])
791

S
Simon Hausmann 已提交
792 793
            rebase = P4Rebase()
            rebase.rebase()
794

795 796
        return True

S
Simon Hausmann 已提交
797
class P4Sync(Command):
798 799 800 801 802 803 804
    def __init__(self):
        Command.__init__(self)
        self.options = [
                optparse.make_option("--branch", dest="branch"),
                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
                optparse.make_option("--changesfile", dest="changesFile"),
                optparse.make_option("--silent", dest="silent", action="store_true"),
805
                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
806
                optparse.make_option("--verbose", dest="verbose", action="store_true"),
807 808
                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
                                     help="Import into refs/heads/ , not refs/remotes"),
809
                optparse.make_option("--max-changes", dest="maxChanges"),
810
                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
811 812 813
                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
                                     help="Only sync files that are included in the Perforce Client Spec")
814 815 816 817 818 819 820 821 822 823 824 825 826
        ]
        self.description = """Imports from Perforce into a git repository.\n
    example:
    //depot/my/project/ -- to import the current head
    //depot/my/project/@all -- to import everything
    //depot/my/project/@1,6 -- to import only from revision 1 to 6

    (a ... is not needed in the path p4 specification, it's added implicitly)"""

        self.usage += " //depot/path[@revRange]"
        self.silent = False
        self.createdBranches = Set()
        self.committedChanges = Set()
827
        self.branch = ""
828
        self.detectBranches = False
829
        self.detectLabels = False
830
        self.changesFile = ""
831
        self.syncWithOrigin = True
832
        self.verbose = False
833
        self.importIntoRemotes = True
834
        self.maxChanges = ""
835
        self.isWindows = (platform.system() == "Windows")
836
        self.keepRepoPath = False
837
        self.depotPaths = None
838
        self.p4BranchesInGit = []
T
Tommy Thorn 已提交
839
        self.cloneExclude = []
840 841
        self.useClientSpec = False
        self.clientSpecDirs = []
842

843 844 845
        if gitConfig("git-p4.syncFromOrigin") == "false":
            self.syncWithOrigin = False

846
    def extractFilesFromCommit(self, commit):
T
Tommy Thorn 已提交
847 848
        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
                             for path in self.cloneExclude]
849 850 851 852
        files = []
        fnum = 0
        while commit.has_key("depotFile%s" % fnum):
            path =  commit["depotFile%s" % fnum]
853

T
Tommy Thorn 已提交
854 855 856 857 858 859
            if [p for p in self.cloneExclude
                if path.startswith (p)]:
                found = False
            else:
                found = [p for p in self.depotPaths
                         if path.startswith (p)]
860
            if not found:
861 862 863 864 865 866 867 868 869 870 871 872
                fnum = fnum + 1
                continue

            file = {}
            file["path"] = path
            file["rev"] = commit["rev%s" % fnum]
            file["action"] = commit["action%s" % fnum]
            file["type"] = commit["type%s" % fnum]
            files.append(file)
            fnum = fnum + 1
        return files

873
    def stripRepoPath(self, path, prefixes):
874
        if self.keepRepoPath:
875 876 877 878 879
            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]

        for p in prefixes:
            if path.startswith(p):
                path = path[len(p):]
880

881
        return path
H
Han-Wen Nienhuys 已提交
882

883
    def splitFilesIntoBranches(self, commit):
884
        branches = {}
885 886 887
        fnum = 0
        while commit.has_key("depotFile%s" % fnum):
            path =  commit["depotFile%s" % fnum]
888 889 890
            found = [p for p in self.depotPaths
                     if path.startswith (p)]
            if not found:
891 892 893 894 895 896 897 898 899 900
                fnum = fnum + 1
                continue

            file = {}
            file["path"] = path
            file["rev"] = commit["rev%s" % fnum]
            file["action"] = commit["action%s" % fnum]
            file["type"] = commit["type%s" % fnum]
            fnum = fnum + 1

901
            relPath = self.stripRepoPath(path, self.depotPaths)
902

903
            for branch in self.knownBranches.keys():
H
Han-Wen Nienhuys 已提交
904 905 906

                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
                if relPath.startswith(branch + "/"):
907 908
                    if branch not in branches:
                        branches[branch] = []
909
                    branches[branch].append(file)
910
                    break
911 912 913

        return branches

H
Han-Wen Nienhuys 已提交
914 915
    ## Should move this out, doesn't use SELF.
    def readP4Files(self, files):
916 917 918
        filesForCommit = []
        filesToRead = []

919
        for f in files:
920
            includeFile = True
921 922
            for val in self.clientSpecDirs:
                if f['path'].startswith(val[0]):
923 924
                    if val[1] <= 0:
                        includeFile = False
925 926
                    break

927 928 929 930
            if includeFile:
                filesForCommit.append(f)
                if f['action'] != 'delete':
                    filesToRead.append(f)
H
Han-Wen Nienhuys 已提交
931

932 933 934 935 936 937
        filedata = []
        if len(filesToRead) > 0:
            filedata = p4CmdList('-x - print',
                                 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
                                                  for f in filesToRead]),
                                 stdin_mode='w+')
938

939 940 941
            if "p4ExitCode" in filedata[0]:
                die("Problems executing p4. Error: [%d]."
                    % (filedata[0]['p4ExitCode']));
H
Han-Wen Nienhuys 已提交
942

943 944
        j = 0;
        contents = {}
945
        while j < len(filedata):
946
            stat = filedata[j]
947
            j += 1
948
            text = [];
949
            while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
950
                text.append(filedata[j]['data'])
951
                j += 1
952
            text = ''.join(text)
953 954 955 956 957

            if not stat.has_key('depotFile'):
                sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
                continue

958 959 960
            if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
                text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
            elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
961
                text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', text)
962

963
            contents[stat['depotFile']] = text
H
Han-Wen Nienhuys 已提交
964

965 966 967 968 969 970
        for f in filesForCommit:
            path = f['path']
            if contents.has_key(path):
                f['data'] = contents[path]

        return filesForCommit
H
Han-Wen Nienhuys 已提交
971

972
    def commit(self, details, files, branch, branchPrefixes, parent = ""):
973 974 975
        epoch = details["time"]
        author = details["user"]

976 977 978
        if self.verbose:
            print "commit into %s" % branch

979 980 981 982 983 984 985 986
        # start with reading files; if that fails, we should not
        # create a commit.
        new_files = []
        for f in files:
            if [p for p in branchPrefixes if f['path'].startswith(p)]:
                new_files.append (f)
            else:
                sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
987
        files = self.readP4Files(new_files)
988

989
        self.gitStream.write("commit %s\n" % branch)
H
Han-Wen Nienhuys 已提交
990
#        gitStream.write("mark :%s\n" % details["change"])
991 992
        self.committedChanges.add(int(details["change"]))
        committer = ""
993 994
        if author not in self.users:
            self.getUserMapFromPerforceServer()
995
        if author in self.users:
996
            committer = "%s %s %s" % (self.users[author], epoch, self.tz)
997
        else:
998
            committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
999 1000 1001 1002 1003

        self.gitStream.write("committer %s\n" % committer)

        self.gitStream.write("data <<EOT\n")
        self.gitStream.write(details["desc"])
1004 1005 1006 1007 1008
        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
                             % (','.join (branchPrefixes), details["change"]))
        if len(details['options']) > 0:
            self.gitStream.write(": options = %s" % details['options'])
        self.gitStream.write("]\nEOT\n\n")
1009 1010

        if len(parent) > 0:
1011 1012
            if self.verbose:
                print "parent %s" % parent
1013 1014
            self.gitStream.write("from %s\n" % parent)

H
Han-Wen Nienhuys 已提交
1015
        for file in files:
1016
            if file["type"] == "apple":
H
Han-Wen Nienhuys 已提交
1017
                print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1018 1019
                continue

H
Han-Wen Nienhuys 已提交
1020 1021
            relPath = self.stripRepoPath(file['path'], branchPrefixes)
            if file["action"] == "delete":
1022 1023
                self.gitStream.write("D %s\n" % relPath)
            else:
H
Han-Wen Nienhuys 已提交
1024
                data = file['data']
1025

1026
                mode = "644"
D
David Brown 已提交
1027
                if isP4Exec(file["type"]):
1028 1029 1030 1031 1032 1033
                    mode = "755"
                elif file["type"] == "symlink":
                    mode = "120000"
                    # p4 print on a symlink contains "target\n", so strip it off
                    data = data[:-1]

1034 1035 1036
                if self.isWindows and file["type"].endswith("text"):
                    data = data.replace("\r\n", "\n")

1037
                self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1038 1039 1040 1041 1042 1043
                self.gitStream.write("data %s\n" % len(data))
                self.gitStream.write(data)
                self.gitStream.write("\n")

        self.gitStream.write("\n")

1044 1045
        change = int(details["change"])

1046
        if self.labels.has_key(change):
1047 1048 1049
            label = self.labels[change]
            labelDetails = label[0]
            labelRevisions = label[1]
1050 1051
            if self.verbose:
                print "Change %s is labelled %s" % (change, labelDetails)
1052

1053 1054
            files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
                                                    for p in branchPrefixes]))
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079

            if len(files) == len(labelRevisions):

                cleanedFiles = {}
                for info in files:
                    if info["action"] == "delete":
                        continue
                    cleanedFiles[info["depotFile"]] = info["rev"]

                if cleanedFiles == labelRevisions:
                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
                    self.gitStream.write("from %s\n" % branch)

                    owner = labelDetails["Owner"]
                    tagger = ""
                    if author in self.users:
                        tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
                    else:
                        tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
                    self.gitStream.write("tagger %s\n" % tagger)
                    self.gitStream.write("data <<EOT\n")
                    self.gitStream.write(labelDetails["Description"])
                    self.gitStream.write("EOT\n\n")

                else:
1080
                    if not self.silent:
1081 1082
                        print ("Tag %s does not match with change %s: files do not match."
                               % (labelDetails["label"], change))
1083 1084

            else:
1085
                if not self.silent:
1086 1087
                    print ("Tag %s does not match with change %s: file count is different."
                           % (labelDetails["label"], change))
1088

1089
    def getUserCacheFilename(self):
1090 1091
        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
        return home + "/.gitp4-usercache.txt"
1092

1093
    def getUserMapFromPerforceServer(self):
1094 1095
        if self.userMapFromPerforceServer:
            return
1096 1097 1098 1099 1100 1101 1102
        self.users = {}

        for output in p4CmdList("users"):
            if not output.has_key("User"):
                continue
            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"

1103 1104 1105 1106 1107 1108

        s = ''
        for (key, val) in self.users.items():
            s += "%s\t%s\n" % (key, val)

        open(self.getUserCacheFilename(), "wb").write(s)
1109
        self.userMapFromPerforceServer = True
1110 1111 1112

    def loadUserMapFromCache(self):
        self.users = {}
1113
        self.userMapFromPerforceServer = False
1114
        try:
1115
            cache = open(self.getUserCacheFilename(), "rb")
1116 1117 1118
            lines = cache.readlines()
            cache.close()
            for line in lines:
1119
                entry = line.strip().split("\t")
1120 1121 1122 1123
                self.users[entry[0]] = entry[1]
        except IOError:
            self.getUserMapFromPerforceServer()

1124 1125 1126
    def getLabels(self):
        self.labels = {}

1127
        l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
S
Simon Hausmann 已提交
1128
        if len(l) > 0 and not self.silent:
1129
            print "Finding files belonging to labels in %s" % `self.depotPaths`
S
Simon Hausmann 已提交
1130 1131

        for output in l:
1132 1133 1134
            label = output["label"]
            revisions = {}
            newestChange = 0
1135 1136
            if self.verbose:
                print "Querying files for label %s" % label
1137 1138 1139
            for file in p4CmdList("files "
                                  +  ' '.join (["%s...@%s" % (p, label)
                                                for p in self.depotPaths])):
1140 1141 1142 1143 1144
                revisions[file["depotFile"]] = file["rev"]
                change = int(file["change"])
                if change > newestChange:
                    newestChange = change

1145 1146 1147 1148
            self.labels[newestChange] = [output, revisions]

        if self.verbose:
            print "Label changes: %s" % self.labels.keys()
1149

1150 1151
    def guessProjectName(self):
        for p in self.depotPaths:
S
Simon Hausmann 已提交
1152 1153 1154 1155 1156 1157
            if p.endswith("/"):
                p = p[:-1]
            p = p[p.strip().rfind("/") + 1:]
            if not p.endswith("/"):
               p += "/"
            return p
1158

1159
    def getBranchMapping(self):
1160 1161
        lostAndFoundBranches = set()

1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172
        for info in p4CmdList("branches"):
            details = p4Cmd("branch -o %s" % info["branch"])
            viewIdx = 0
            while details.has_key("View%s" % viewIdx):
                paths = details["View%s" % viewIdx].split(" ")
                viewIdx = viewIdx + 1
                # require standard //depot/foo/... //depot/bar/... mapping
                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
                    continue
                source = paths[0]
                destination = paths[1]
1173 1174 1175 1176
                ## HACK
                if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
                    source = source[len(self.depotPaths[0]):-4]
                    destination = destination[len(self.depotPaths[0]):-4]
1177

1178 1179 1180 1181 1182 1183
                    if destination in self.knownBranches:
                        if not self.silent:
                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
                        continue

1184 1185 1186 1187
                    self.knownBranches[destination] = source

                    lostAndFoundBranches.discard(destination)

1188
                    if source not in self.knownBranches:
1189 1190 1191 1192 1193
                        lostAndFoundBranches.add(source)


        for branch in lostAndFoundBranches:
            self.knownBranches[branch] = branch
1194

1195 1196 1197 1198 1199 1200 1201 1202 1203
    def getBranchMappingFromGitBranches(self):
        branches = p4BranchesInGit(self.importIntoRemotes)
        for branch in branches.keys():
            if branch == "master":
                branch = "main"
            else:
                branch = branch[len(self.projectName):]
            self.knownBranches[branch] = branch

1204
    def listExistingP4GitBranches(self):
1205 1206 1207 1208 1209
        # branches holds mapping from name to commit
        branches = p4BranchesInGit(self.importIntoRemotes)
        self.p4BranchesInGit = branches.keys()
        for branch in branches.keys():
            self.initialParents[self.refPrefix + branch] = branches[branch]
1210

H
Han-Wen Nienhuys 已提交
1211 1212 1213 1214 1215 1216 1217 1218 1219 1220
    def updateOptionDict(self, d):
        option_keys = {}
        if self.keepRepoPath:
            option_keys['keepRepoPath'] = 1

        d["options"] = ' '.join(sorted(option_keys.keys()))

    def readOptions(self, d):
        self.keepRepoPath = (d.has_key('options')
                             and ('keepRepoPath' in d['options']))
1221

1222 1223 1224 1225 1226 1227 1228 1229 1230
    def gitRefForBranch(self, branch):
        if branch == "main":
            return self.refPrefix + "master"

        if len(branch) <= 0:
            return branch

        return self.refPrefix + self.projectName + branch

1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291
    def gitCommitByP4Change(self, ref, change):
        if self.verbose:
            print "looking in ref " + ref + " for change %s using bisect..." % change

        earliestCommit = ""
        latestCommit = parseRevision(ref)

        while True:
            if self.verbose:
                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
            if len(next) == 0:
                if self.verbose:
                    print "argh"
                return ""
            log = extractLogMessageFromGitCommit(next)
            settings = extractSettingsGitLog(log)
            currentChange = int(settings['change'])
            if self.verbose:
                print "current change %s" % currentChange

            if currentChange == change:
                if self.verbose:
                    print "found %s" % next
                return next

            if currentChange < change:
                earliestCommit = "^%s" % next
            else:
                latestCommit = "%s" % next

        return ""

    def importNewBranch(self, branch, maxChange):
        # make fast-import flush all changes to disk and update the refs using the checkpoint
        # command so that we can try to find the branch parent in the git history
        self.gitStream.write("checkpoint\n\n");
        self.gitStream.flush();
        branchPrefix = self.depotPaths[0] + branch + "/"
        range = "@1,%s" % maxChange
        #print "prefix" + branchPrefix
        changes = p4ChangesForPaths([branchPrefix], range)
        if len(changes) <= 0:
            return False
        firstChange = changes[0]
        #print "first change in branch: %s" % firstChange
        sourceBranch = self.knownBranches[branch]
        sourceDepotPath = self.depotPaths[0] + sourceBranch
        sourceRef = self.gitRefForBranch(sourceBranch)
        #print "source " + sourceBranch

        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
        #print "branch parent: %s" % branchParentChange
        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
        if len(gitParent) > 0:
            self.initialParents[self.gitRefForBranch(branch)] = gitParent
            #print "parent git commit: %s" % gitParent

        self.importChanges(changes)
        return True

1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323
    def importChanges(self, changes):
        cnt = 1
        for change in changes:
            description = p4Cmd("describe %s" % change)
            self.updateOptionDict(description)

            if not self.silent:
                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
                sys.stdout.flush()
            cnt = cnt + 1

            try:
                if self.detectBranches:
                    branches = self.splitFilesIntoBranches(description)
                    for branch in branches.keys():
                        ## HACK  --hwn
                        branchPrefix = self.depotPaths[0] + branch + "/"

                        parent = ""

                        filesForCommit = branches[branch]

                        if self.verbose:
                            print "branch is %s" % branch

                        self.updatedBranches.add(branch)

                        if branch not in self.createdBranches:
                            self.createdBranches.add(branch)
                            parent = self.knownBranches[branch]
                            if parent == branch:
                                parent = ""
1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336
                            else:
                                fullBranch = self.projectName + branch
                                if fullBranch not in self.p4BranchesInGit:
                                    if not self.silent:
                                        print("\n    Importing new branch %s" % fullBranch);
                                    if self.importNewBranch(branch, change - 1):
                                        parent = ""
                                        self.p4BranchesInGit.append(fullBranch)
                                    if not self.silent:
                                        print("\n    Resuming with change %s" % change);

                                if self.verbose:
                                    print "parent determined through known branches: %s" % parent
1337

1338 1339
                        branch = self.gitRefForBranch(branch)
                        parent = self.gitRefForBranch(parent)
1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357

                        if self.verbose:
                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)

                        if len(parent) == 0 and branch in self.initialParents:
                            parent = self.initialParents[branch]
                            del self.initialParents[branch]

                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
                else:
                    files = self.extractFilesFromCommit(description)
                    self.commit(description, files, self.branch, self.depotPaths,
                                self.initialParent)
                    self.initialParent = ""
            except IOError:
                print self.gitError.read()
                sys.exit(1)

1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401
    def importHeadRevision(self, revision):
        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)

        details = { "user" : "git perforce import user", "time" : int(time.time()) }
        details["desc"] = ("Initial import of %s from the state at revision %s"
                           % (' '.join(self.depotPaths), revision))
        details["change"] = revision
        newestRevision = 0

        fileCnt = 0
        for info in p4CmdList("files "
                              +  ' '.join(["%s...%s"
                                           % (p, revision)
                                           for p in self.depotPaths])):

            if info['code'] == 'error':
                sys.stderr.write("p4 returned an error: %s\n"
                                 % info['data'])
                sys.exit(1)


            change = int(info["change"])
            if change > newestRevision:
                newestRevision = change

            if info["action"] == "delete":
                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
                #fileCnt = fileCnt + 1
                continue

            for prop in ["depotFile", "rev", "action", "type" ]:
                details["%s%s" % (prop, fileCnt)] = info[prop]

            fileCnt = fileCnt + 1

        details["change"] = newestRevision
        self.updateOptionDict(details)
        try:
            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
        except IOError:
            print "IO error with git fast-import. Is your git version recent enough?"
            print self.gitError.read()


1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421
    def getClientSpec(self):
        specList = p4CmdList( "client -o" )
        temp = {}
        for entry in specList:
            for k,v in entry.iteritems():
                if k.startswith("View"):
                    if v.startswith('"'):
                        start = 1
                    else:
                        start = 0
                    index = v.find("...")
                    v = v[start:index]
                    if v.startswith("-"):
                        v = v[1:]
                        temp[v] = -len(v)
                    else:
                        temp[v] = len(v)
        self.clientSpecDirs = temp.items()
        self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )

1422
    def run(self, args):
1423
        self.depotPaths = []
1424 1425
        self.changeRange = ""
        self.initialParent = ""
1426
        self.previousDepotPaths = []
H
Han-Wen Nienhuys 已提交
1427

1428 1429 1430
        # map from branch depot path to parent branch
        self.knownBranches = {}
        self.initialParents = {}
1431
        self.hasOrigin = originP4BranchesExist()
1432 1433
        if not self.syncWithOrigin:
            self.hasOrigin = False
1434

1435 1436 1437
        if self.importIntoRemotes:
            self.refPrefix = "refs/remotes/p4/"
        else:
1438
            self.refPrefix = "refs/heads/p4/"
1439

1440 1441 1442 1443
        if self.syncWithOrigin and self.hasOrigin:
            if not self.silent:
                print "Syncing with origin first by calling git fetch origin"
            system("git fetch origin")
1444

1445
        if len(self.branch) == 0:
1446
            self.branch = self.refPrefix + "master"
1447
            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1448 1449
                system("git update-ref %s refs/heads/p4" % self.branch)
                system("git branch -D p4");
1450
            # create it /after/ importing, when master exists
1451
            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1452
                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1453

1454
        if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1455 1456
            self.getClientSpec()

H
Han-Wen Nienhuys 已提交
1457 1458 1459
        # TODO: should always look at previous commits,
        # merge with previous imports, if possible.
        if args == []:
1460
            if self.hasOrigin:
1461
                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1462 1463 1464 1465 1466 1467
            self.listExistingP4GitBranches()

            if len(self.p4BranchesInGit) > 1:
                if not self.silent:
                    print "Importing from/into multiple branches"
                self.detectBranches = True
1468

1469 1470 1471 1472 1473
            if self.verbose:
                print "branches: %s" % self.p4BranchesInGit

            p4Change = 0
            for branch in self.p4BranchesInGit:
1474
                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
H
Han-Wen Nienhuys 已提交
1475 1476

                settings = extractSettingsGitLog(logMsg)
1477

H
Han-Wen Nienhuys 已提交
1478 1479 1480 1481
                self.readOptions(settings)
                if (settings.has_key('depot-paths')
                    and settings.has_key ('change')):
                    change = int(settings['change']) + 1
1482 1483
                    p4Change = max(p4Change, change)

H
Han-Wen Nienhuys 已提交
1484 1485
                    depotPaths = sorted(settings['depot-paths'])
                    if self.previousDepotPaths == []:
1486
                        self.previousDepotPaths = depotPaths
1487
                    else:
1488 1489
                        paths = []
                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1490
                            for i in range(0, min(len(cur), len(prev))):
1491
                                if cur[i] <> prev[i]:
1492
                                    i = i - 1
1493 1494
                                    break

1495
                            paths.append (cur[:i + 1])
1496 1497

                        self.previousDepotPaths = paths
1498 1499

            if p4Change > 0:
H
Han-Wen Nienhuys 已提交
1500
                self.depotPaths = sorted(self.previousDepotPaths)
1501
                self.changeRange = "@%s,#head" % p4Change
1502 1503
                if not self.detectBranches:
                    self.initialParent = parseRevision(self.branch)
1504
                if not self.silent and not self.detectBranches:
1505
                    print "Performing incremental import into %s git branch" % self.branch
1506

1507 1508
        if not self.branch.startswith("refs/"):
            self.branch = "refs/heads/" + self.branch
1509

1510
        if len(args) == 0 and self.depotPaths:
1511
            if not self.silent:
1512
                print "Depot paths: %s" % ' '.join(self.depotPaths)
1513
        else:
1514
            if self.depotPaths and self.depotPaths != args:
1515
                print ("previous import used depot path %s and now %s was specified. "
1516 1517
                       "This doesn't work!" % (' '.join (self.depotPaths),
                                               ' '.join (args)))
1518
                sys.exit(1)
1519

H
Han-Wen Nienhuys 已提交
1520
            self.depotPaths = sorted(args)
1521

1522
        revision = ""
1523 1524
        self.users = {}

1525 1526 1527 1528 1529 1530 1531
        newPaths = []
        for p in self.depotPaths:
            if p.find("@") != -1:
                atIdx = p.index("@")
                self.changeRange = p[atIdx:]
                if self.changeRange == "@all":
                    self.changeRange = ""
H
Han-Wen Nienhuys 已提交
1532
                elif ',' not in self.changeRange:
1533
                    revision = self.changeRange
1534
                    self.changeRange = ""
1535
                p = p[:atIdx]
1536 1537
            elif p.find("#") != -1:
                hashIdx = p.index("#")
1538
                revision = p[hashIdx:]
1539
                p = p[:hashIdx]
1540
            elif self.previousDepotPaths == []:
1541
                revision = "#head"
1542 1543 1544 1545 1546 1547 1548 1549 1550

            p = re.sub ("\.\.\.$", "", p)
            if not p.endswith("/"):
                p += "/"

            newPaths.append(p)

        self.depotPaths = newPaths

1551

1552
        self.loadUserMapFromCache()
1553 1554 1555
        self.labels = {}
        if self.detectLabels:
            self.getLabels();
1556

1557
        if self.detectBranches:
1558 1559 1560
            ## FIXME - what's a P4 projectName ?
            self.projectName = self.guessProjectName()

1561 1562 1563 1564
            if self.hasOrigin:
                self.getBranchMappingFromGitBranches()
            else:
                self.getBranchMapping()
1565 1566 1567 1568 1569
            if self.verbose:
                print "p4-git branches: %s" % self.p4BranchesInGit
                print "initial parents: %s" % self.initialParents
            for b in self.p4BranchesInGit:
                if b != "master":
1570 1571

                    ## FIXME
1572 1573
                    b = b[len(self.projectName):]
                self.createdBranches.add(b)
1574

1575
        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1576

1577
        importProcess = subprocess.Popen(["git", "fast-import"],
1578 1579
                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                                         stderr=subprocess.PIPE);
1580 1581 1582
        self.gitOutput = importProcess.stdout
        self.gitStream = importProcess.stdin
        self.gitError = importProcess.stderr
1583

1584
        if revision:
1585
            self.importHeadRevision(revision)
1586 1587 1588
        else:
            changes = []

1589
            if len(self.changesFile) > 0:
1590 1591 1592 1593 1594 1595 1596 1597 1598 1599
                output = open(self.changesFile).readlines()
                changeSet = Set()
                for line in output:
                    changeSet.add(int(line))

                for change in changeSet:
                    changes.append(change)

                changes.sort()
            else:
1600
                if self.verbose:
1601
                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1602
                                                              self.changeRange)
1603
                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1604

1605
                if len(self.maxChanges) > 0:
1606
                    changes = changes[:min(int(self.maxChanges), len(changes))]
1607

1608
            if len(changes) == 0:
1609
                if not self.silent:
1610
                    print "No changes to import!"
1611
                return True
1612

1613 1614 1615
            if not self.silent and not self.detectBranches:
                print "Import destination: %s" % self.branch

1616 1617
            self.updatedBranches = set()

1618
            self.importChanges(changes)
1619

1620 1621 1622 1623 1624 1625 1626
            if not self.silent:
                print ""
                if len(self.updatedBranches) > 0:
                    sys.stdout.write("Updated branches: ")
                    for b in self.updatedBranches:
                        sys.stdout.write("%s " % b)
                    sys.stdout.write("\n")
1627 1628

        self.gitStream.close()
1629 1630
        if importProcess.wait() != 0:
            die("fast-import failed: %s" % self.gitError.read())
1631 1632 1633 1634 1635
        self.gitOutput.close()
        self.gitError.close()

        return True

S
Simon Hausmann 已提交
1636 1637 1638
class P4Rebase(Command):
    def __init__(self):
        Command.__init__(self)
1639
        self.options = [ ]
1640 1641
        self.description = ("Fetches the latest revision from perforce and "
                            + "rebases the current work (branch) against it")
S
Simon Hausmann 已提交
1642
        self.verbose = False
S
Simon Hausmann 已提交
1643 1644 1645 1646

    def run(self, args):
        sync = P4Sync()
        sync.run([])
1647

1648 1649 1650
        return self.rebase()

    def rebase(self):
1651 1652 1653 1654 1655
        if os.system("git update-index --refresh") != 0:
            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.");
        if len(read_pipe("git diff-index HEAD --")) > 0:
            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");

1656 1657 1658 1659 1660 1661 1662 1663
        [upstream, settings] = findUpstreamBranchPoint()
        if len(upstream) == 0:
            die("Cannot find upstream branchpoint for rebase")

        # the branchpoint may be p4/foo~3, so strip off the parent
        upstream = re.sub("~[0-9]+$", "", upstream)

        print "Rebasing the current branch onto %s" % upstream
1664
        oldHead = read_pipe("git rev-parse HEAD").strip()
1665
        system("git rebase %s" % upstream)
1666
        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
S
Simon Hausmann 已提交
1667 1668
        return True

1669 1670 1671 1672
class P4Clone(P4Sync):
    def __init__(self):
        P4Sync.__init__(self)
        self.description = "Creates a new git repository and imports from Perforce into it"
H
Han-Wen Nienhuys 已提交
1673
        self.usage = "usage: %prog [options] //depot/path[@revRange]"
T
Tommy Thorn 已提交
1674
        self.options += [
H
Han-Wen Nienhuys 已提交
1675 1676
            optparse.make_option("--destination", dest="cloneDestination",
                                 action='store', default=None,
T
Tommy Thorn 已提交
1677 1678 1679 1680 1681
                                 help="where to leave result of the clone"),
            optparse.make_option("-/", dest="cloneExclude",
                                 action="append", type="string",
                                 help="exclude depot path")
        ]
H
Han-Wen Nienhuys 已提交
1682
        self.cloneDestination = None
1683 1684
        self.needsGit = False

T
Tommy Thorn 已提交
1685 1686 1687 1688 1689 1690
    # This is required for the "append" cloneExclude action
    def ensure_value(self, attr, value):
        if not hasattr(self, attr) or getattr(self, attr) is None:
            setattr(self, attr, value)
        return getattr(self, attr)

H
Han-Wen Nienhuys 已提交
1691 1692 1693 1694 1695
    def defaultDestination(self, args):
        ## TODO: use common prefix of args?
        depotPath = args[0]
        depotDir = re.sub("(@[^@]*)$", "", depotPath)
        depotDir = re.sub("(#[^#]*)$", "", depotDir)
1696
        depotDir = re.sub(r"\.\.\.$", "", depotDir)
H
Han-Wen Nienhuys 已提交
1697 1698 1699
        depotDir = re.sub(r"/$", "", depotDir)
        return os.path.split(depotDir)[1]

1700 1701 1702
    def run(self, args):
        if len(args) < 1:
            return False
H
Han-Wen Nienhuys 已提交
1703 1704 1705 1706

        if self.keepRepoPath and not self.cloneDestination:
            sys.stderr.write("Must specify destination for --keep-path\n")
            sys.exit(1)
1707

1708
        depotPaths = args
1709 1710 1711 1712 1713

        if not self.cloneDestination and len(depotPaths) > 1:
            self.cloneDestination = depotPaths[-1]
            depotPaths = depotPaths[:-1]

T
Tommy Thorn 已提交
1714
        self.cloneExclude = ["/"+p for p in self.cloneExclude]
1715 1716 1717
        for p in depotPaths:
            if not p.startswith("//"):
                return False
1718

H
Han-Wen Nienhuys 已提交
1719
        if not self.cloneDestination:
1720
            self.cloneDestination = self.defaultDestination(args)
1721

1722
        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1723 1724
        if not os.path.exists(self.cloneDestination):
            os.makedirs(self.cloneDestination)
H
Han-Wen Nienhuys 已提交
1725
        os.chdir(self.cloneDestination)
1726
        system("git init")
H
Han-Wen Nienhuys 已提交
1727
        self.gitdir = os.getcwd() + "/.git"
1728
        if not P4Sync.run(self, depotPaths):
1729 1730
            return False
        if self.branch != "master":
1731 1732 1733 1734 1735
            if gitBranchExists("refs/remotes/p4/master"):
                system("git branch master refs/remotes/p4/master")
                system("git checkout -f")
            else:
                print "Could not detect main branch. No checkout/master branch created."
1736

1737 1738
        return True

1739 1740 1741 1742 1743 1744 1745 1746 1747
class P4Branches(Command):
    def __init__(self):
        Command.__init__(self)
        self.options = [ ]
        self.description = ("Shows the git branches that hold imports and their "
                            + "corresponding perforce depot paths")
        self.verbose = False

    def run(self, args):
1748 1749 1750
        if originP4BranchesExist():
            createOrUpdateBranchesFromOrigin()

1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766
        cmdline = "git rev-parse --symbolic "
        cmdline += " --remotes"

        for line in read_pipe_lines(cmdline):
            line = line.strip()

            if not line.startswith('p4/') or line == "p4/HEAD":
                continue
            branch = line

            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
            settings = extractSettingsGitLog(log)

            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
        return True

1767 1768 1769 1770 1771 1772 1773 1774 1775
class HelpFormatter(optparse.IndentedHelpFormatter):
    def __init__(self):
        optparse.IndentedHelpFormatter.__init__(self)

    def format_description(self, description):
        if description:
            return description + "\n"
        else:
            return ""
1776

1777 1778 1779 1780 1781 1782 1783 1784 1785
def printUsage(commands):
    print "usage: %s <command> [options]" % sys.argv[0]
    print ""
    print "valid commands: %s" % ", ".join(commands)
    print ""
    print "Try %s <command> --help for command specific help." % sys.argv[0]
    print ""

commands = {
H
Han-Wen Nienhuys 已提交
1786 1787
    "debug" : P4Debug,
    "submit" : P4Submit,
1788
    "commit" : P4Submit,
H
Han-Wen Nienhuys 已提交
1789 1790 1791
    "sync" : P4Sync,
    "rebase" : P4Rebase,
    "clone" : P4Clone,
1792 1793
    "rollback" : P4RollBack,
    "branches" : P4Branches
1794 1795 1796
}


H
Han-Wen Nienhuys 已提交
1797 1798 1799 1800
def main():
    if len(sys.argv[1:]) == 0:
        printUsage(commands.keys())
        sys.exit(2)
1801

H
Han-Wen Nienhuys 已提交
1802 1803 1804
    cmd = ""
    cmdName = sys.argv[1]
    try:
H
Han-Wen Nienhuys 已提交
1805 1806
        klass = commands[cmdName]
        cmd = klass()
H
Han-Wen Nienhuys 已提交
1807 1808 1809 1810 1811 1812 1813
    except KeyError:
        print "unknown command %s" % cmdName
        print ""
        printUsage(commands.keys())
        sys.exit(2)

    options = cmd.options
H
Han-Wen Nienhuys 已提交
1814
    cmd.gitdir = os.environ.get("GIT_DIR", None)
H
Han-Wen Nienhuys 已提交
1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829

    args = sys.argv[2:]

    if len(options) > 0:
        options.append(optparse.make_option("--git-dir", dest="gitdir"))

        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
                                       options,
                                       description = cmd.description,
                                       formatter = HelpFormatter())

        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
    global verbose
    verbose = cmd.verbose
    if cmd.needsGit:
H
Han-Wen Nienhuys 已提交
1830 1831 1832 1833 1834
        if cmd.gitdir == None:
            cmd.gitdir = os.path.abspath(".git")
            if not isValidGitDir(cmd.gitdir):
                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
                if os.path.exists(cmd.gitdir):
H
Han-Wen Nienhuys 已提交
1835 1836 1837
                    cdup = read_pipe("git rev-parse --show-cdup").strip()
                    if len(cdup) > 0:
                        os.chdir(cdup);
1838

H
Han-Wen Nienhuys 已提交
1839 1840 1841
        if not isValidGitDir(cmd.gitdir):
            if isValidGitDir(cmd.gitdir + "/.git"):
                cmd.gitdir += "/.git"
H
Han-Wen Nienhuys 已提交
1842
            else:
H
Han-Wen Nienhuys 已提交
1843
                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1844

H
Han-Wen Nienhuys 已提交
1845
        os.environ["GIT_DIR"] = cmd.gitdir
1846

H
Han-Wen Nienhuys 已提交
1847 1848
    if not cmd.run(args):
        parser.print_help()
1849 1850


H
Han-Wen Nienhuys 已提交
1851 1852
if __name__ == '__main__':
    main()