git-p4.py 121.7 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
# License: MIT <http://www.opensource.org/licenses/mit-license.php>
#
10 11 12 13 14
import sys
if sys.hexversion < 0x02040000:
    # The limiter is the subprocess module
    sys.stderr.write("git-p4: requires Python 2.4 or later.\n")
    sys.exit(1)
P
Pete Wyckoff 已提交
15 16 17 18 19 20 21 22 23
import os
import optparse
import marshal
import subprocess
import tempfile
import time
import platform
import re
import shutil
24
import stat
25

26
verbose = False
27

28
# Only labels/tags matching this will be imported/exported
29
defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
30 31 32 33 34 35 36 37

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.
    """
38
    real_cmd = ["p4"]
39 40 41

    user = gitConfig("git-p4.user")
    if len(user) > 0:
42
        real_cmd += ["-u",user]
43 44 45

    password = gitConfig("git-p4.password")
    if len(password) > 0:
46
        real_cmd += ["-P", password]
47 48 49

    port = gitConfig("git-p4.port")
    if len(port) > 0:
50
        real_cmd += ["-p", port]
51 52 53

    host = gitConfig("git-p4.host")
    if len(host) > 0:
54
        real_cmd += ["-H", host]
55 56 57

    client = gitConfig("git-p4.client")
    if len(client) > 0:
58
        real_cmd += ["-c", client]
59

60 61 62 63 64

    if isinstance(cmd,basestring):
        real_cmd = ' '.join(real_cmd) + ' ' + cmd
    else:
        real_cmd += cmd
65 66
    return real_cmd

67
def chdir(dir):
68
    # P4 uses the PWD environment variable rather than getcwd(). Since we're
69 70
    # not using the shell, we have to set it ourselves.  This path could
    # be relative, so go there first, then figure out where we ended up.
71
    os.chdir(dir)
72
    os.environ['PWD'] = os.getcwd()
73

74 75 76 77 78 79 80
def die(msg):
    if verbose:
        raise Exception(msg)
    else:
        sys.stderr.write(msg + "\n")
        sys.exit(1)

81
def write_pipe(c, stdin):
82
    if verbose:
83
        sys.stderr.write('Writing pipe: %s\n' % str(c))
H
Han-Wen Nienhuys 已提交
84

85 86 87 88 89 90 91
    expand = isinstance(c,basestring)
    p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
    pipe = p.stdin
    val = pipe.write(stdin)
    pipe.close()
    if p.wait():
        die('Command failed: %s' % str(c))
H
Han-Wen Nienhuys 已提交
92 93 94

    return val

95
def p4_write_pipe(c, stdin):
96
    real_cmd = p4_build_cmd(c)
97
    return write_pipe(real_cmd, stdin)
98

99 100
def read_pipe(c, ignore_error=False):
    if verbose:
101
        sys.stderr.write('Reading pipe: %s\n' % str(c))
102

103 104 105
    expand = isinstance(c,basestring)
    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
    pipe = p.stdout
H
Han-Wen Nienhuys 已提交
106
    val = pipe.read()
107 108
    if p.wait() and not ignore_error:
        die('Command failed: %s' % str(c))
H
Han-Wen Nienhuys 已提交
109 110 111

    return val

112 113 114
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 已提交
115

H
cleanup  
Han-Wen Nienhuys 已提交
116
def read_pipe_lines(c):
117
    if verbose:
118 119 120 121 122
        sys.stderr.write('Reading pipe: %s\n' % str(c))

    expand = isinstance(c, basestring)
    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
    pipe = p.stdout
H
Han-Wen Nienhuys 已提交
123
    val = pipe.readlines()
124 125
    if pipe.close() or p.wait():
        die('Command failed: %s' % str(c))
H
Han-Wen Nienhuys 已提交
126 127

    return val
128

129 130
def p4_read_pipe_lines(c):
    """Specifically invoke p4 on the command supplied. """
A
Anand Kumria 已提交
131
    real_cmd = p4_build_cmd(c)
132 133
    return read_pipe_lines(real_cmd)

134 135 136 137 138 139 140 141 142
def p4_has_command(cmd):
    """Ask p4 for help on this command.  If it returns an error, the
       command does not exist in this version of p4."""
    real_cmd = p4_build_cmd(["help", cmd])
    p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
    p.communicate()
    return p.returncode == 0

143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
def p4_has_move_command():
    """See if the move command exists, that it supports -k, and that
       it has not been administratively disabled.  The arguments
       must be correct, but the filenames do not have to exist.  Use
       ones with wildcards so even if they exist, it will fail."""

    if not p4_has_command("move"):
        return False
    cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = p.communicate()
    # return code will be 1 in either case
    if err.find("Invalid option") >= 0:
        return False
    if err.find("disabled") >= 0:
        return False
    # assume it failed because @... was invalid changelist
    return True

H
Han-Wen Nienhuys 已提交
162
def system(cmd):
163
    expand = isinstance(cmd,basestring)
164
    if verbose:
165 166
        sys.stderr.write("executing %s\n" % str(cmd))
    subprocess.check_call(cmd, shell=expand)
H
Han-Wen Nienhuys 已提交
167

168 169
def p4_system(cmd):
    """Specifically invoke p4 as the system command. """
A
Anand Kumria 已提交
170
    real_cmd = p4_build_cmd(cmd)
171 172 173
    expand = isinstance(real_cmd, basestring)
    subprocess.check_call(real_cmd, shell=expand)

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
_p4_version_string = None
def p4_version_string():
    """Read the version string, showing just the last line, which
       hopefully is the interesting version bit.

       $ p4 -V
       Perforce - The Fast Software Configuration Management System.
       Copyright 1995-2011 Perforce Software.  All rights reserved.
       Rev. P4/NTX86/2011.1/393975 (2011/12/16).
    """
    global _p4_version_string
    if not _p4_version_string:
        a = p4_read_pipe_lines(["-V"])
        _p4_version_string = a[-1].rstrip()
    return _p4_version_string

190
def p4_integrate(src, dest):
191
    p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
192

193
def p4_sync(f, *options):
194
    p4_system(["sync"] + list(options) + [wildcard_encode(f)])
195 196

def p4_add(f):
197 198 199 200 201
    # forcibly add file names with wildcards
    if wildcard_present(f):
        p4_system(["add", "-f", f])
    else:
        p4_system(["add", f])
202 203

def p4_delete(f):
204
    p4_system(["delete", wildcard_encode(f)])
205 206

def p4_edit(f):
207
    p4_system(["edit", wildcard_encode(f)])
208 209

def p4_revert(f):
210
    p4_system(["revert", wildcard_encode(f)])
211

212 213
def p4_reopen(type, f):
    p4_system(["reopen", "-t", type, wildcard_encode(f)])
214

215 216 217
def p4_move(src, dest):
    p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])

P
Pete Wyckoff 已提交
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
def p4_describe(change):
    """Make sure it returns a valid result by checking for
       the presence of field "time".  Return a dict of the
       results."""

    ds = p4CmdList(["describe", "-s", str(change)])
    if len(ds) != 1:
        die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))

    d = ds[0]

    if "p4ExitCode" in d:
        die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
                                                      str(d)))
    if "code" in d:
        if d["code"] == "error":
            die("p4 describe -s %d returned error code: %s" % (change, str(d)))

    if "time" not in d:
        die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))

    return d

P
Pete Wyckoff 已提交
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
#
# Canonicalize the p4 type and return a tuple of the
# base type, plus any modifiers.  See "p4 help filetypes"
# for a list and explanation.
#
def split_p4_type(p4type):

    p4_filetypes_historical = {
        "ctempobj": "binary+Sw",
        "ctext": "text+C",
        "cxtext": "text+Cx",
        "ktext": "text+k",
        "kxtext": "text+kx",
        "ltext": "text+F",
        "tempobj": "binary+FSw",
        "ubinary": "binary+F",
        "uresource": "resource+F",
        "uxbinary": "binary+Fx",
        "xbinary": "binary+x",
        "xltext": "text+Fx",
        "xtempobj": "binary+Swx",
        "xtext": "text+x",
        "xunicode": "unicode+x",
        "xutf16": "utf16+x",
    }
    if p4type in p4_filetypes_historical:
        p4type = p4_filetypes_historical[p4type]
    mods = ""
    s = p4type.split("+")
    base = s[0]
    mods = ""
    if len(s) > 1:
        mods = s[1]
    return (base, mods)
D
David Brown 已提交
275

276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
#
# return the raw p4 type of a file (text, text+ko, etc)
#
def p4_type(file):
    results = p4CmdList(["fstat", "-T", "headType", file])
    return results[0]['headType']

#
# Given a type base and modifier, return a regexp matching
# the keywords that can be expanded in the file
#
def p4_keywords_regexp_for_type(base, type_mods):
    if base in ("text", "unicode", "binary"):
        kwords = None
        if "ko" in type_mods:
            kwords = 'Id|Header'
        elif "k" in type_mods:
            kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
        else:
            return None
        pattern = r"""
            \$              # Starts with a dollar, followed by...
            (%s)            # one of the keywords, followed by...
299
            (:[^$\n]+)?     # possibly an old expansion, followed by...
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
            \$              # another dollar
            """ % kwords
        return pattern
    else:
        return None

#
# Given a file, return a regexp matching the possible
# RCS keywords that will be expanded, or None for files
# with kw expansion turned off.
#
def p4_keywords_regexp_for_file(file):
    if not os.path.exists(file):
        return None
    else:
        (type_base, type_mods) = split_p4_type(p4_type(file))
        return p4_keywords_regexp_for_type(type_base, type_mods)
D
David Brown 已提交
317

318 319 320 321 322 323 324 325 326 327 328 329 330
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]

331
    p4_reopen(p4Type, file)
332 333 334 335

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

336
    result = p4_read_pipe(["opened", wildcard_encode(file)])
337
    match = re.match(".*\((.+)\)\r?$", result)
338 339 340
    if match:
        return match.group(1)
    else:
341
        die("Could not determine file type for %s (result: '%s')" % (file, result))
342

343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
# Return the set of all p4 labels
def getP4Labels(depotPaths):
    labels = set()
    if isinstance(depotPaths,basestring):
        depotPaths = [depotPaths]

    for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
        label = l['label']
        labels.add(label)

    return labels

# Return the set of all git tags
def getGitTags():
    gitTags = set()
    for line in read_pipe_lines(["git", "tag"]):
        tag = line.strip()
        gitTags.add(tag)
    return gitTags

363 364 365 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
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

403 404 405 406 407 408 409 410
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)

411
def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
412 413 414 415 416 417 418 419 420

    if isinstance(cmd,basestring):
        cmd = "-G " + cmd
        expand = True
    else:
        cmd = ["-G"] + cmd
        expand = False

    cmd = p4_build_cmd(cmd)
H
Han-Wen Nienhuys 已提交
421
    if verbose:
422
        sys.stderr.write("Opening pipe: %s\n" % str(cmd))
S
Scott Lamb 已提交
423 424 425 426 427 428 429

    # 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)
430 431 432 433 434
        if isinstance(stdin,basestring):
            stdin_file.write(stdin)
        else:
            for i in stdin:
                stdin_file.write(i + '\n')
S
Scott Lamb 已提交
435 436 437
        stdin_file.flush()
        stdin_file.seek(0)

438 439
    p4 = subprocess.Popen(cmd,
                          shell=expand,
S
Scott Lamb 已提交
440 441
                          stdin=stdin_file,
                          stdout=subprocess.PIPE)
442 443 444 445

    result = []
    try:
        while True:
S
Scott Lamb 已提交
446
            entry = marshal.load(p4.stdout)
447 448 449 450
            if cb is not None:
                cb(entry)
            else:
                result.append(entry)
451 452
    except EOFError:
        pass
S
Scott Lamb 已提交
453 454
    exitCode = p4.wait()
    if exitCode != 0:
455 456 457
        entry = {}
        entry["p4ExitCode"] = exitCode
        result.append(entry)
458 459 460 461 462 463 464 465 466 467

    return result

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

468 469 470
def p4Where(depotPath):
    if not depotPath.endswith("/"):
        depotPath += "/"
471
    depotPath = depotPath + "..."
472
    outputList = p4CmdList(["where", depotPath])
473 474
    output = None
    for entry in outputList:
475 476 477 478 479 480 481 482 483 484
        if "depotFile" in entry:
            if entry["depotFile"] == depotPath:
                output = entry
                break
        elif "data" in entry:
            data = entry.get("data")
            space = data.find(" ")
            if data[:space] == depotPath:
                output = entry
                break
485 486
    if output == None:
        return ""
487 488
    if output["code"] == "error":
        return ""
489 490 491 492 493 494 495 496 497 498 499 500
    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

501
def currentGitBranch():
502
    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
503

504
def isValidGitDir(path):
H
Han-Wen Nienhuys 已提交
505 506
    if (os.path.exists(path + "/HEAD")
        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
507 508 509
        return True;
    return False

510
def parseRevision(ref):
511
    return read_pipe("git rev-parse %s" % ref).strip()
512

513 514 515 516 517
def branchExists(ref):
    rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
                     ignore_error=True)
    return len(rev) > 0

518 519
def extractLogMessageFromGitCommit(commit):
    logMessage = ""
H
Han-Wen Nienhuys 已提交
520 521

    ## fixme: title is first line of commit, not 1st paragraph.
522
    foundTitle = False
H
Han-Wen Nienhuys 已提交
523
    for log in read_pipe_lines("git cat-file commit %s" % commit):
524 525
       if not foundTitle:
           if len(log) == 1:
S
Simon Hausmann 已提交
526
               foundTitle = True
527 528 529 530 531
           continue

       logMessage += log
    return logMessage

H
Han-Wen Nienhuys 已提交
532
def extractSettingsGitLog(log):
533 534 535
    values = {}
    for line in log.split("\n"):
        line = line.strip()
536 537 538 539 540 541 542 543 544 545 546 547 548 549
        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

550 551 552
    paths = values.get("depot-paths")
    if not paths:
        paths = values.get("depot-path")
553 554
    if paths:
        values['depot-paths'] = paths.split(',')
H
Han-Wen Nienhuys 已提交
555
    return values
556

557
def gitBranchExists(branch):
H
Han-Wen Nienhuys 已提交
558 559
    proc = subprocess.Popen(["git", "rev-parse", branch],
                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
560
    return proc.wait() == 0;
561

562
_gitConfig = {}
563

P
Pete Wyckoff 已提交
564
def gitConfig(key):
565
    if not _gitConfig.has_key(key):
P
Pete Wyckoff 已提交
566
        cmd = [ "git", "config", key ]
567 568
        s = read_pipe(cmd, ignore_error=True)
        _gitConfig[key] = s.strip()
569
    return _gitConfig[key]
570

P
Pete Wyckoff 已提交
571 572 573 574 575 576 577 578 579 580 581 582
def gitConfigBool(key):
    """Return a bool, using git config --bool.  It is True only if the
       variable is set to true, and False if set to false or not present
       in the config."""

    if not _gitConfig.has_key(key):
        cmd = [ "git", "config", "--bool", key ]
        s = read_pipe(cmd, ignore_error=True)
        v = s.strip()
        _gitConfig[key] = v == "true"
    return _gitConfig[key]

583 584
def gitConfigList(key):
    if not _gitConfig.has_key(key):
585 586
        s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
        _gitConfig[key] = s.strip().split(os.linesep)
587 588
    return _gitConfig[key]

589 590 591 592 593 594 595
def p4BranchesInGit(branchesAreInRemotes=True):
    """Find all the branches whose names start with "p4/", looking
       in remotes or heads as specified by the argument.  Return
       a dictionary of { branch: revision } for each one found.
       The branch names are the short names, without any
       "p4/" prefix."""

596 597 598 599
    branches = {}

    cmdline = "git rev-parse --symbolic "
    if branchesAreInRemotes:
600
        cmdline += "--remotes"
601
    else:
602
        cmdline += "--branches"
603 604 605 606

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

607 608 609 610 611
        # only import to p4/
        if not line.startswith('p4/'):
            continue
        # special symbolic ref to p4/master
        if line == "p4/HEAD":
612 613
            continue

614 615
        # strip off p4/ prefix
        branch = line[len("p4/"):]
616 617

        branches[branch] = parseRevision(line)
618

619 620
    return branches

621 622 623 624 625 626 627 628 629 630 631
def branch_exists(branch):
    """Make sure that the given ref name really exists."""

    cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, _ = p.communicate()
    if p.returncode:
        return False
    # expect exactly one line of output: the branch name
    return out.rstrip() == branch

632
def findUpstreamBranchPoint(head = "HEAD"):
633 634 635 636 637 638 639 640 641 642 643
    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

644 645 646
    settings = None
    parent = 0
    while parent < 65535:
647
        commit = head + "~%s" % parent
648 649
        log = extractLogMessageFromGitCommit(commit)
        settings = extractSettingsGitLog(log)
650 651 652 653
        if settings.has_key("depot-paths"):
            paths = ",".join(settings["depot-paths"])
            if branchByDepotPath.has_key(paths):
                return [branchByDepotPath[paths], settings]
654

655
        parent = parent + 1
656

657
    return ["", settings]
658

659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
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")

709 710
def p4ChangesForPaths(depotPaths, changeRange):
    assert depotPaths
711 712 713 714
    cmd = ['changes']
    for p in depotPaths:
        cmd += ["%s...%s" % (p, changeRange)]
    output = p4_read_pipe_lines(cmd)
715

716
    changes = {}
717
    for line in output:
718 719
        changeNum = int(line.split(" ")[1])
        changes[changeNum] = True
720

721 722 723
    changelist = changes.keys()
    changelist.sort()
    return changelist
724

725 726 727 728 729 730 731 732
def p4PathStartsWith(path, prefix):
    # This method tries to remedy a potential mixed-case issue:
    #
    # If UserA adds  //depot/DirA/file1
    # and UserB adds //depot/dira/file2
    #
    # we may or may not have a problem. If you have core.ignorecase=true,
    # we treat DirA and dira as the same directory
P
Pete Wyckoff 已提交
733
    if gitConfigBool("core.ignorecase"):
734 735 736
        return path.lower().startswith(prefix.lower())
    return path.startswith(prefix)

737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
def getClientSpec():
    """Look at the p4 client spec, create a View() object that contains
       all the mappings, and return it."""

    specList = p4CmdList("client -o")
    if len(specList) != 1:
        die('Output from "client -o" is %d lines, expecting 1' %
            len(specList))

    # dictionary of all client parameters
    entry = specList[0]

    # just the keys that start with "View"
    view_keys = [ k for k in entry.keys() if k.startswith("View") ]

    # hold this new View
    view = View()

    # append the lines, in order, to the view
    for view_num in range(len(view_keys)):
        k = "View%d" % view_num
        if k not in view_keys:
            die("Expected view key %s missing" % k)
        view.append(entry[k])

    return view

def getClientRoot():
    """Grab the client directory."""

    output = p4CmdList("client -o")
    if len(output) != 1:
        die('Output from "client -o" is %d lines, expecting 1' % len(output))

    entry = output[0]
    if "Root" not in entry:
        die('Client has no "Root"')

    return entry["Root"]

777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804
#
# P4 wildcards are not allowed in filenames.  P4 complains
# if you simply add them, but you can force it with "-f", in
# which case it translates them into %xx encoding internally.
#
def wildcard_decode(path):
    # Search for and fix just these four characters.  Do % last so
    # that fixing it does not inadvertently create new %-escapes.
    # Cannot have * in a filename in windows; untested as to
    # what p4 would do in such a case.
    if not platform.system() == "Windows":
        path = path.replace("%2A", "*")
    path = path.replace("%23", "#") \
               .replace("%40", "@") \
               .replace("%25", "%")
    return path

def wildcard_encode(path):
    # do % first to avoid double-encoding the %s introduced here
    path = path.replace("%", "%25") \
               .replace("*", "%2A") \
               .replace("#", "%23") \
               .replace("@", "%40")
    return path

def wildcard_present(path):
    return path.translate(None, "*#@%") != path

805 806 807
class Command:
    def __init__(self):
        self.usage = "usage: %prog [options]"
808
        self.needsGit = True
809
        self.verbose = False
810

811 812 813
class P4UserMap:
    def __init__(self):
        self.userMapFromPerforceServer = False
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833
        self.myP4UserId = None

    def p4UserId(self):
        if self.myP4UserId:
            return self.myP4UserId

        results = p4CmdList("user -o")
        for r in results:
            if r.has_key('User'):
                self.myP4UserId = r['User']
                return r['User']
        die("Could not find your p4 user id")

    def p4UserIsMe(self, p4User):
        # return True if the given p4 user is actually me
        me = self.p4UserId()
        if not p4User or p4User != me:
            return False
        else:
            return True
834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871

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

    def getUserMapFromPerforceServer(self):
        if self.userMapFromPerforceServer:
            return
        self.users = {}
        self.emails = {}

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


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

        open(self.getUserCacheFilename(), "wb").write(s)
        self.userMapFromPerforceServer = True

    def loadUserMapFromCache(self):
        self.users = {}
        self.userMapFromPerforceServer = False
        try:
            cache = open(self.getUserCacheFilename(), "rb")
            lines = cache.readlines()
            cache.close()
            for line in lines:
                entry = line.strip().split("\t")
                self.users[entry[0]] = entry[1]
        except IOError:
            self.getUserMapFromPerforceServer()

872
class P4Debug(Command):
873
    def __init__(self):
874
        Command.__init__(self)
875
        self.options = []
876
        self.description = "A tool to debug the output of p4 -G."
877
        self.needsGit = False
878 879

    def run(self, args):
880
        j = 0
881
        for output in p4CmdList(args):
882 883
            print 'Element: %d' % j
            j += 1
884
            print output
885
        return True
886

887 888 889 890
class P4RollBack(Command):
    def __init__(self):
        Command.__init__(self)
        self.options = [
891
            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
892 893
        ]
        self.description = "A tool to debug the multi-branch import. Don't use :)"
894
        self.rollbackLocalBranches = False
895 896 897 898 899

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

901
        if "p4ExitCode" in p4Cmd("changes -m 1"):
902 903
            die("Problems executing p4");

904 905
        if self.rollbackLocalBranches:
            refPrefix = "refs/heads/"
H
Han-Wen Nienhuys 已提交
906
            lines = read_pipe_lines("git rev-parse --symbolic --branches")
907 908
        else:
            refPrefix = "refs/remotes/"
H
Han-Wen Nienhuys 已提交
909
            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
910 911 912

        for line in lines:
            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
913 914
                line = line.strip()
                ref = refPrefix + line
915
                log = extractLogMessageFromGitCommit(ref)
H
Han-Wen Nienhuys 已提交
916 917 918 919 920
                settings = extractSettingsGitLog(log)

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

921
                changed = False
922

923 924
                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
                                                           for p in depotPaths]))) == 0:
925 926 927 928
                    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 已提交
929
                while change and int(change) > maxChange:
930
                    changed = True
931 932
                    if self.verbose:
                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
933 934
                    system("git update-ref %s \"%s^\"" % (ref, ref))
                    log = extractLogMessageFromGitCommit(ref)
H
Han-Wen Nienhuys 已提交
935 936 937 938 939
                    settings =  extractSettingsGitLog(log)


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

                if changed:
942
                    print "%s rewound to %s" % (ref, change)
943 944 945

        return True

946
class P4Submit(Command, P4UserMap):
947 948 949

    conflict_behavior_choices = ("ask", "skip", "quit")

950
    def __init__(self):
951
        Command.__init__(self)
952
        P4UserMap.__init__(self)
953 954
        self.options = [
                optparse.make_option("--origin", dest="origin"),
955
                optparse.make_option("-M", dest="detectRenames", action="store_true"),
956 957
                # preserve the user, requires relevant p4 permissions
                optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
958
                optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
959
                optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
960
                optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
961
                optparse.make_option("--conflict", dest="conflict_behavior",
962 963
                                     choices=self.conflict_behavior_choices),
                optparse.make_option("--branch", dest="branch"),
964 965
        ]
        self.description = "Submit changes from git to the perforce depot."
966
        self.usage += " [name of git branch to submit into perforce depot]"
967
        self.origin = ""
968
        self.detectRenames = False
P
Pete Wyckoff 已提交
969
        self.preserveUser = gitConfigBool("git-p4.preserveUser")
970
        self.dry_run = False
971
        self.prepare_p4_only = False
972
        self.conflict_behavior = None
973
        self.isWindows = (platform.system() == "Windows")
974
        self.exportLabels = False
975
        self.p4HasMoveCommand = p4_has_move_command()
976
        self.branch = None
977 978 979 980 981

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

982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009
    def separate_jobs_from_description(self, message):
        """Extract and return a possible Jobs field in the commit
           message.  It goes into a separate section in the p4 change
           specification.

           A jobs line starts with "Jobs:" and looks like a new field
           in a form.  Values are white-space separated on the same
           line or on following lines that start with a tab.

           This does not parse and extract the full git commit message
           like a p4 form.  It just sees the Jobs: line as a marker
           to pass everything from then on directly into the p4 form,
           but outside the description section.

           Return a tuple (stripped log message, jobs string)."""

        m = re.search(r'^Jobs:', message, re.MULTILINE)
        if m is None:
            return (message, None)

        jobtext = message[m.start():]
        stripped_message = message[:m.start()].rstrip()
        return (stripped_message, jobtext)

    def prepareLogMessage(self, template, message, jobs):
        """Edits the template returned from "p4 change -o" to insert
           the message in the Description field, and the jobs text in
           the Jobs field."""
1010 1011
        result = ""

1012 1013
        inDescriptionSection = False

1014 1015 1016 1017 1018
        for line in template.split("\n"):
            if line.startswith("#"):
                result += line + "\n"
                continue

1019
            if inDescriptionSection:
1020
                if line.startswith("Files:") or line.startswith("Jobs:"):
1021
                    inDescriptionSection = False
1022 1023 1024
                    # insert Jobs section
                    if jobs:
                        result += jobs + "\n"
1025 1026 1027 1028 1029 1030 1031 1032 1033 1034
                else:
                    continue
            else:
                if line.startswith("Description:"):
                    inDescriptionSection = True
                    line += "\n"
                    for messageLine in message.split("\n"):
                        line += "\t" + messageLine + "\n"

            result += line + "\n"
1035 1036 1037

        return result

1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060
    def patchRCSKeywords(self, file, pattern):
        # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
        (handle, outFileName) = tempfile.mkstemp(dir='.')
        try:
            outFile = os.fdopen(handle, "w+")
            inFile = open(file, "r")
            regexp = re.compile(pattern, re.VERBOSE)
            for line in inFile.readlines():
                line = regexp.sub(r'$\1$', line)
                outFile.write(line)
            inFile.close()
            outFile.close()
            # Forcibly overwrite the original file
            os.unlink(file)
            shutil.move(outFileName, file)
        except:
            # cleanup our temporary file
            os.unlink(outFileName)
            print "Failed to strip RCS keywords in %s" % file
            raise

        print "Patched up RCS keywords in %s" % file

1061 1062 1063
    def p4UserForCommit(self,id):
        # Return the tuple (perforce user,git email) for a given git commit id
        self.getUserMapFromPerforceServer()
1064 1065
        gitEmail = read_pipe(["git", "log", "--max-count=1",
                              "--format=%ae", id])
1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077
        gitEmail = gitEmail.strip()
        if not self.emails.has_key(gitEmail):
            return (None,gitEmail)
        else:
            return (self.emails[gitEmail],gitEmail)

    def checkValidP4Users(self,commits):
        # check if any git authors cannot be mapped to p4 users
        for id in commits:
            (user,email) = self.p4UserForCommit(id)
            if not user:
                msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
P
Pete Wyckoff 已提交
1078
                if gitConfigBool("git-p4.allowMissingP4Users"):
1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095
                    print "%s" % msg
                else:
                    die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)

    def lastP4Changelist(self):
        # Get back the last changelist number submitted in this client spec. This
        # then gets used to patch up the username in the change. If the same
        # client spec is being used by multiple processes then this might go
        # wrong.
        results = p4CmdList("client -o")        # find the current client
        client = None
        for r in results:
            if r.has_key('Client'):
                client = r['Client']
                break
        if not client:
            die("could not get client spec")
1096
        results = p4CmdList(["changes", "-c", client, "-m", "1"])
1097 1098 1099 1100 1101 1102 1103 1104
        for r in results:
            if r.has_key('change'):
                return r['change']
        die("Could not get changelist number for last submit - cannot patch up user details")

    def modifyChangelistUser(self, changelist, newUser):
        # fixup the user field of a changelist after it has been submitted.
        changes = p4CmdList("change -o %s" % changelist)
1105 1106 1107 1108 1109 1110 1111 1112 1113
        if len(changes) != 1:
            die("Bad output from p4 change modifying %s to user %s" %
                (changelist, newUser))

        c = changes[0]
        if c['User'] == newUser: return   # nothing to do
        c['User'] = newUser
        input = marshal.dumps(c)

1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126
        result = p4CmdList("change -f -i", stdin=input)
        for r in result:
            if r.has_key('code'):
                if r['code'] == 'error':
                    die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
            if r.has_key('data'):
                print("Updated user field for changelist %s to %s" % (changelist, newUser))
                return
        die("Could not modify user field of changelist %s to %s" % (changelist, newUser))

    def canChangeChangelists(self):
        # check to see if we have p4 admin or super-user permissions, either of
        # which are required to modify changelists.
1127
        results = p4CmdList(["protects", self.depotPath])
1128 1129 1130 1131 1132 1133 1134 1135
        for r in results:
            if r.has_key('perm'):
                if r['perm'] == 'admin':
                    return 1
                if r['perm'] == 'super':
                    return 1
        return 0

1136
    def prepareSubmitTemplate(self):
1137 1138 1139 1140 1141 1142 1143
        """Run "p4 change -o" to grab a change specification template.
           This does not use "p4 -G", as it is nice to keep the submission
           template in original order, since a human might edit it.

           Remove lines in the Files section that show changes to files
           outside the depot path we're committing into."""

1144 1145
        template = ""
        inFilesSection = False
1146
        for line in p4_read_pipe_lines(['change', '-o']):
1147 1148
            if line.endswith("\r\n"):
                line = line[:-2] + "\n"
1149 1150 1151 1152 1153 1154 1155
            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]
1156
                        if not p4PathStartsWith(path, self.depotPath):
1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167
                            continue
                else:
                    inFilesSection = False
            else:
                if line.startswith("Files:"):
                    inFilesSection = True

            template += line

        return template

P
Pete Wyckoff 已提交
1168 1169 1170 1171 1172
    def edit_template(self, template_file):
        """Invoke the editor to let the user change the submission
           message.  Return true if okay to continue with the submit."""

        # if configured to skip the editing part, just submit
P
Pete Wyckoff 已提交
1173
        if gitConfigBool("git-p4.skipSubmitEdit"):
P
Pete Wyckoff 已提交
1174 1175 1176 1177 1178 1179 1180
            return True

        # look at the modification time, to check later if the user saved
        # the file
        mtime = os.stat(template_file).st_mtime

        # invoke the editor
1181
        if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
P
Pete Wyckoff 已提交
1182 1183 1184 1185 1186 1187 1188
            editor = os.environ.get("P4EDITOR")
        else:
            editor = read_pipe("git var GIT_EDITOR").strip()
        system(editor + " " + template_file)

        # If the file was not saved, prompt to see if this patch should
        # be skipped.  But skip this verification step if configured so.
P
Pete Wyckoff 已提交
1189
        if gitConfigBool("git-p4.skipSubmitEditCheck"):
P
Pete Wyckoff 已提交
1190 1191
            return True

1192 1193 1194 1195 1196 1197 1198 1199 1200 1201
        # modification time updated means user saved the file
        if os.stat(template_file).st_mtime > mtime:
            return True

        while True:
            response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
            if response == 'y':
                return True
            if response == 'n':
                return False
P
Pete Wyckoff 已提交
1202

1203
    def applyCommit(self, id):
1204 1205 1206 1207
        """Apply one commit, return True if it succeeded."""

        print "Applying", read_pipe(["git", "show", "-s",
                                     "--format=format:%h %s", id])
1208

1209
        (p4User, gitEmail) = self.p4UserForCommit(id)
1210

1211
        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1212 1213
        filesToAdd = set()
        filesToDelete = set()
1214
        editedFiles = set()
1215
        pureRenameCopy = set()
1216
        filesToChangeExecBit = {}
1217

1218
        for line in diff:
1219 1220 1221
            diff = parseDiffTreeEntry(line)
            modifier = diff['status']
            path = diff['src']
1222
            if modifier == "M":
1223
                p4_edit(path)
1224 1225
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
                    filesToChangeExecBit[path] = diff['dst_mode']
1226
                editedFiles.add(path)
1227 1228
            elif modifier == "A":
                filesToAdd.add(path)
1229
                filesToChangeExecBit[path] = diff['dst_mode']
1230 1231 1232 1233 1234 1235
                if path in filesToDelete:
                    filesToDelete.remove(path)
            elif modifier == "D":
                filesToDelete.add(path)
                if path in filesToAdd:
                    filesToAdd.remove(path)
1236 1237
            elif modifier == "C":
                src, dest = diff['src'], diff['dst']
1238
                p4_integrate(src, dest)
1239
                pureRenameCopy.add(dest)
1240
                if diff['src_sha1'] != diff['dst_sha1']:
1241
                    p4_edit(dest)
1242
                    pureRenameCopy.discard(dest)
1243
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1244
                    p4_edit(dest)
1245
                    pureRenameCopy.discard(dest)
1246
                    filesToChangeExecBit[dest] = diff['dst_mode']
1247 1248 1249
                if self.isWindows:
                    # turn off read-only attribute
                    os.chmod(dest, stat.S_IWRITE)
1250 1251
                os.unlink(dest)
                editedFiles.add(dest)
1252
            elif modifier == "R":
1253
                src, dest = diff['src'], diff['dst']
1254 1255 1256
                if self.p4HasMoveCommand:
                    p4_edit(src)        # src must be open before move
                    p4_move(src, dest)  # opens for (move/delete, move/add)
1257
                else:
1258 1259 1260 1261 1262
                    p4_integrate(src, dest)
                    if diff['src_sha1'] != diff['dst_sha1']:
                        p4_edit(dest)
                    else:
                        pureRenameCopy.add(dest)
1263
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1264 1265
                    if not self.p4HasMoveCommand:
                        p4_edit(dest)   # with move: already open, writable
1266
                    filesToChangeExecBit[dest] = diff['dst_mode']
1267
                if not self.p4HasMoveCommand:
1268 1269
                    if self.isWindows:
                        os.chmod(dest, stat.S_IWRITE)
1270 1271
                    os.unlink(dest)
                    filesToDelete.add(src)
1272
                editedFiles.add(dest)
1273 1274 1275
            else:
                die("unknown modifier %s for %s" % (modifier, path))

1276
        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
1277
        patchcmd = diffcmd + " | git apply "
1278 1279
        tryPatchCmd = patchcmd + "--check -"
        applyPatchCmd = patchcmd + "--check --apply -"
1280
        patch_succeeded = True
1281

1282
        if os.system(tryPatchCmd) != 0:
1283 1284
            fixed_rcs_keywords = False
            patch_succeeded = False
1285
            print "Unfortunately applying the change failed!"
1286 1287 1288

            # Patch failed, maybe it's just RCS keyword woes. Look through
            # the patch to see if that's possible.
P
Pete Wyckoff 已提交
1289
            if gitConfigBool("git-p4.attemptRCSCleanup"):
1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309
                file = None
                pattern = None
                kwfiles = {}
                for file in editedFiles | filesToDelete:
                    # did this file's delta contain RCS keywords?
                    pattern = p4_keywords_regexp_for_file(file)

                    if pattern:
                        # this file is a possibility...look for RCS keywords.
                        regexp = re.compile(pattern, re.VERBOSE)
                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
                            if regexp.search(line):
                                if verbose:
                                    print "got keyword match on %s in %s in %s" % (pattern, line, file)
                                kwfiles[file] = pattern
                                break

                for file in kwfiles:
                    if verbose:
                        print "zapping %s with %s" % (line,pattern)
1310 1311 1312 1313
                    # File is being deleted, so not open in p4.  Must
                    # disable the read-only bit on windows.
                    if self.isWindows and file not in editedFiles:
                        os.chmod(file, stat.S_IWRITE)
1314 1315 1316 1317 1318 1319 1320 1321 1322
                    self.patchRCSKeywords(file, kwfiles[file])
                    fixed_rcs_keywords = True

            if fixed_rcs_keywords:
                print "Retrying the patch with RCS keywords cleaned up"
                if os.system(tryPatchCmd) == 0:
                    patch_succeeded = True

        if not patch_succeeded:
1323 1324 1325
            for f in editedFiles:
                p4_revert(f)
            return False
1326

1327 1328 1329
        #
        # Apply the patch for real, and do add/delete/+x handling.
        #
1330
        system(applyPatchCmd)
1331 1332

        for f in filesToAdd:
1333
            p4_add(f)
1334
        for f in filesToDelete:
1335 1336
            p4_revert(f)
            p4_delete(f)
1337

1338 1339 1340 1341 1342
        # Set/clear executable bits
        for f in filesToChangeExecBit.keys():
            mode = filesToChangeExecBit[f]
            setP4ExecBit(f, mode)

1343 1344 1345 1346
        #
        # Build p4 change description, starting with the contents
        # of the git commit message.
        #
1347 1348
        logMessage = extractLogMessageFromGitCommit(id)
        logMessage = logMessage.strip()
1349
        (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1350

1351
        template = self.prepareSubmitTemplate()
1352
        submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1353

1354
        if self.preserveUser:
1355
           submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1356

1357 1358 1359 1360
        if self.checkAuthorship and not self.p4UserIsMe(p4User):
            submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
            submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
            submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1361

1362 1363 1364
        separatorLine = "######## everything below this line is just the diff #######\n"

        # diff
1365 1366 1367 1368 1369 1370 1371
        if os.environ.has_key("P4DIFF"):
            del(os.environ["P4DIFF"])
        diff = ""
        for editedFile in editedFiles:
            diff += p4_read_pipe(['diff', '-du',
                                  wildcard_encode(editedFile)])

1372
        # new file diff
1373 1374 1375 1376 1377 1378 1379 1380 1381 1382
        newdiff = ""
        for newFile in filesToAdd:
            newdiff += "==== new file ====\n"
            newdiff += "--- /dev/null\n"
            newdiff += "+++ %s\n" % newFile
            f = open(newFile, "r")
            for line in f.readlines():
                newdiff += "+" + line
            f.close()

1383
        # change description file: submitTemplate, separatorLine, diff, newdiff
1384 1385 1386 1387 1388 1389 1390 1391 1392
        (handle, fileName) = tempfile.mkstemp()
        tmpFile = os.fdopen(handle, "w+")
        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)
        tmpFile.close()

1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427
        if self.prepare_p4_only:
            #
            # Leave the p4 tree prepared, and the submit template around
            # and let the user decide what to do next
            #
            print
            print "P4 workspace prepared for submission."
            print "To submit or revert, go to client workspace"
            print "  " + self.clientPath
            print
            print "To submit, use \"p4 submit\" to write a new description,"
            print "or \"p4 submit -i %s\" to use the one prepared by" \
                  " \"git p4\"." % fileName
            print "You can delete the file \"%s\" when finished." % fileName

            if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
                print "To preserve change ownership by user %s, you must\n" \
                      "do \"p4 change -f <change>\" after submitting and\n" \
                      "edit the User field."
            if pureRenameCopy:
                print "After submitting, renamed files must be re-synced."
                print "Invoke \"p4 sync -f\" on each of these files:"
                for f in pureRenameCopy:
                    print "  " + f

            print
            print "To revert the changes, use \"p4 revert ...\", and delete"
            print "the submit template file \"%s\"" % fileName
            if filesToAdd:
                print "Since the commit adds new files, they must be deleted:"
                for f in filesToAdd:
                    print "  " + f
            print
            return True

1428 1429 1430
        #
        # Let the user edit the change description, then submit it.
        #
1431 1432
        if self.edit_template(fileName):
            # read the edited message and submit
1433
            ret = True
1434 1435
            tmpFile = open(fileName, "rb")
            message = tmpFile.read()
1436
            tmpFile.close()
1437 1438 1439 1440
            submitTemplate = message[:message.index(separatorLine)]
            if self.isWindows:
                submitTemplate = submitTemplate.replace("\r\n", "\n")
            p4_write_pipe(['submit', '-i'], submitTemplate)
1441

1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453
            if self.preserveUser:
                if p4User:
                    # Get last changelist number. Cannot easily get it from
                    # the submit command output as the output is
                    # unmarshalled.
                    changelist = self.lastP4Changelist()
                    self.modifyChangelistUser(changelist, p4User)

            # The rename/copy happened by applying a patch that created a
            # new file.  This leaves it writable, which confuses p4.
            for f in pureRenameCopy:
                p4_sync(f, "-f")
1454

1455
        else:
1456
            # skip this patch
1457
            ret = False
1458 1459 1460 1461 1462 1463
            print "Submission cancelled, undoing p4 changes."
            for f in editedFiles:
                p4_revert(f)
            for f in filesToAdd:
                p4_revert(f)
                os.remove(f)
1464 1465
            for f in filesToDelete:
                p4_revert(f)
1466 1467

        os.remove(fileName)
1468
        return ret
1469

1470 1471 1472
    # Export git tags as p4 labels. Create a p4 label and then tag
    # with that.
    def exportGitTags(self, gitTags):
1473 1474 1475 1476
        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
        if len(validLabelRegexp) == 0:
            validLabelRegexp = defaultLabelRegexp
        m = re.compile(validLabelRegexp)
1477 1478 1479 1480 1481

        for name in gitTags:

            if not m.match(name):
                if verbose:
1482
                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1483 1484 1485
                continue

            # Get the p4 commit this corresponds to
1486 1487
            logMessage = extractLogMessageFromGitCommit(name)
            values = extractSettingsGitLog(logMessage)
1488

1489
            if not values.has_key('change'):
1490 1491 1492 1493
                # a tag pointing to something not sent to p4; ignore
                if verbose:
                    print "git tag %s does not give a p4 commit" % name
                continue
1494 1495
            else:
                changelist = values['change']
1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525

            # Get the tag details.
            inHeader = True
            isAnnotated = False
            body = []
            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
                l = l.strip()
                if inHeader:
                    if re.match(r'tag\s+', l):
                        isAnnotated = True
                    elif re.match(r'\s*$', l):
                        inHeader = False
                        continue
                else:
                    body.append(l)

            if not isAnnotated:
                body = ["lightweight tag imported by git p4\n"]

            # Create the label - use the same view as the client spec we are using
            clientSpec = getClientSpec()

            labelTemplate  = "Label: %s\n" % name
            labelTemplate += "Description:\n"
            for b in body:
                labelTemplate += "\t" + b + "\n"
            labelTemplate += "View:\n"
            for mapping in clientSpec.mappings:
                labelTemplate += "\t%s\n" % mapping.depot_side.path

1526 1527
            if self.dry_run:
                print "Would create p4 label %s for tag" % name
1528 1529 1530
            elif self.prepare_p4_only:
                print "Not creating p4 label %s for tag due to option" \
                      " --prepare-p4-only" % name
1531 1532
            else:
                p4_write_pipe(["label", "-i"], labelTemplate)
1533

1534 1535 1536
                # Use the label
                p4_system(["tag", "-l", name] +
                          ["%s@%s" % (mapping.depot_side.path, changelist) for mapping in clientSpec.mappings])
1537

1538 1539
                if verbose:
                    print "created p4 label for tag %s" % name
1540

1541
    def run(self, args):
1542 1543
        if len(args) == 0:
            self.master = currentGitBranch()
1544
            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1545 1546 1547
                die("Detecting current git branch failed!")
        elif len(args) == 1:
            self.master = args[0]
1548 1549
            if not branchExists(self.master):
                die("Branch %s does not exist" % self.master)
1550 1551 1552
        else:
            return False

J
Jing Xue 已提交
1553 1554 1555 1556
        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)

1557
        [upstream, settings] = findUpstreamBranchPoint()
1558
        self.depotPath = settings['depot-paths'][0]
1559 1560
        if len(self.origin) == 0:
            self.origin = upstream
1561

1562 1563 1564 1565
        if self.preserveUser:
            if not self.canChangeChangelists():
                die("Cannot preserve user names without p4 super-user or admin permissions")

1566 1567 1568 1569 1570 1571 1572 1573 1574 1575
        # if not set from the command line, try the config file
        if self.conflict_behavior is None:
            val = gitConfig("git-p4.conflict")
            if val:
                if val not in self.conflict_behavior_choices:
                    die("Invalid value '%s' for config git-p4.conflict" % val)
            else:
                val = "ask"
            self.conflict_behavior = val

1576 1577
        if self.verbose:
            print "Origin branch is " + self.origin
1578

1579
        if len(self.depotPath) == 0:
1580 1581 1582
            print "Internal error: cannot locate perforce depot path from existing branches"
            sys.exit(128)

1583
        self.useClientSpec = False
P
Pete Wyckoff 已提交
1584
        if gitConfigBool("git-p4.useclientspec"):
1585 1586 1587
            self.useClientSpec = True
        if self.useClientSpec:
            self.clientSpecDirs = getClientSpec()
1588

1589 1590 1591 1592 1593
        if self.useClientSpec:
            # all files are relative to the client spec
            self.clientPath = getClientRoot()
        else:
            self.clientPath = p4Where(self.depotPath)
1594

1595 1596
        if self.clientPath == "":
            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1597

1598
        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1599
        self.oldWorkingDirectory = os.getcwd()
1600

1601
        # ensure the clientPath exists
1602
        new_client_dir = False
1603
        if not os.path.exists(self.clientPath):
1604
            new_client_dir = True
1605 1606
            os.makedirs(self.clientPath)

1607
        chdir(self.clientPath)
1608 1609
        if self.dry_run:
            print "Would synchronize p4 checkout in %s" % self.clientPath
1610
        else:
1611 1612 1613 1614 1615 1616
            print "Synchronizing p4 checkout..."
            if new_client_dir:
                # old one was destroyed, and maybe nobody told p4
                p4_sync("...", "-f")
            else:
                p4_sync("...")
1617 1618
        self.check()

S
Simon Hausmann 已提交
1619
        commits = []
1620
        for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, self.master)]):
S
Simon Hausmann 已提交
1621 1622
            commits.append(line.strip())
        commits.reverse()
1623

P
Pete Wyckoff 已提交
1624
        if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1625 1626 1627 1628
            self.checkAuthorship = False
        else:
            self.checkAuthorship = True

1629 1630 1631
        if self.preserveUser:
            self.checkValidP4Users(commits)

1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659
        #
        # Build up a set of options to be passed to diff when
        # submitting each commit to p4.
        #
        if self.detectRenames:
            # command-line -M arg
            self.diffOpts = "-M"
        else:
            # If not explicitly set check the config variable
            detectRenames = gitConfig("git-p4.detectRenames")

            if detectRenames.lower() == "false" or detectRenames == "":
                self.diffOpts = ""
            elif detectRenames.lower() == "true":
                self.diffOpts = "-M"
            else:
                self.diffOpts = "-M%s" % detectRenames

        # no command-line arg for -C or --find-copies-harder, just
        # config variables
        detectCopies = gitConfig("git-p4.detectCopies")
        if detectCopies.lower() == "false" or detectCopies == "":
            pass
        elif detectCopies.lower() == "true":
            self.diffOpts += " -C"
        else:
            self.diffOpts += " -C%s" % detectCopies

P
Pete Wyckoff 已提交
1660
        if gitConfigBool("git-p4.detectCopiesHarder"):
1661 1662
            self.diffOpts += " --find-copies-harder"

1663 1664 1665 1666
        #
        # Apply the commits, one at a time.  On failure, ask if should
        # continue to try the rest of the patches, or quit.
        #
1667 1668
        if self.dry_run:
            print "Would apply"
1669
        applied = []
1670 1671
        last = len(commits) - 1
        for i, commit in enumerate(commits):
1672 1673 1674 1675 1676 1677
            if self.dry_run:
                print " ", read_pipe(["git", "show", "-s",
                                      "--format=format:%h %s", commit])
                ok = True
            else:
                ok = self.applyCommit(commit)
1678 1679
            if ok:
                applied.append(commit)
1680
            else:
1681 1682 1683 1684
                if self.prepare_p4_only and i < last:
                    print "Processing only the first commit due to option" \
                          " --prepare-p4-only"
                    break
1685 1686 1687
                if i < last:
                    quit = False
                    while True:
1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702
                        # prompt for what to do, or use the option/variable
                        if self.conflict_behavior == "ask":
                            print "What do you want to do?"
                            response = raw_input("[s]kip this commit but apply"
                                                 " the rest, or [q]uit? ")
                            if not response:
                                continue
                        elif self.conflict_behavior == "skip":
                            response = "s"
                        elif self.conflict_behavior == "quit":
                            response = "q"
                        else:
                            die("Unknown conflict_behavior '%s'" %
                                self.conflict_behavior)

1703 1704 1705 1706 1707 1708 1709 1710 1711
                        if response[0] == "s":
                            print "Skipping this commit, but applying the rest"
                            break
                        if response[0] == "q":
                            print "Quitting"
                            quit = True
                            break
                    if quit:
                        break
1712

1713
        chdir(self.oldWorkingDirectory)
1714

1715 1716
        if self.dry_run:
            pass
1717 1718
        elif self.prepare_p4_only:
            pass
1719
        elif len(commits) == len(applied):
1720
            print "All commits applied!"
1721

S
Simon Hausmann 已提交
1722
            sync = P4Sync()
1723 1724
            if self.branch:
                sync.branch = self.branch
S
Simon Hausmann 已提交
1725
            sync.run([])
1726

S
Simon Hausmann 已提交
1727 1728
            rebase = P4Rebase()
            rebase.rebase()
1729

1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743
        else:
            if len(applied) == 0:
                print "No commits applied."
            else:
                print "Applied only the commits marked with '*':"
                for c in commits:
                    if c in applied:
                        star = "*"
                    else:
                        star = " "
                    print star, read_pipe(["git", "show", "-s",
                                           "--format=format:%h %s",  c])
                print "You will have to do 'git p4 sync' and rebase."

P
Pete Wyckoff 已提交
1744
        if gitConfigBool("git-p4.exportLabels"):
1745
            self.exportLabels = True
1746 1747 1748 1749 1750 1751 1752 1753

        if self.exportLabels:
            p4Labels = getP4Labels(self.depotPath)
            gitTags = getGitTags()

            missingGitTags = gitTags - p4Labels
            self.exportGitTags(missingGitTags)

1754 1755 1756 1757
        # exit with error unless everything applied perfecly
        if len(commits) != len(applied):
                sys.exit(1)

1758 1759
        return True

P
Pete Wyckoff 已提交
1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797
class View(object):
    """Represent a p4 view ("p4 help views"), and map files in a
       repo according to the view."""

    class Path(object):
        """A depot or client path, possibly containing wildcards.
           The only one supported is ... at the end, currently.
           Initialize with the full path, with //depot or //client."""

        def __init__(self, path, is_depot):
            self.path = path
            self.is_depot = is_depot
            self.find_wildcards()
            # remember the prefix bit, useful for relative mappings
            m = re.match("(//[^/]+/)", self.path)
            if not m:
                die("Path %s does not start with //prefix/" % self.path)
            prefix = m.group(1)
            if not self.is_depot:
                # strip //client/ on client paths
                self.path = self.path[len(prefix):]

        def find_wildcards(self):
            """Make sure wildcards are valid, and set up internal
               variables."""

            self.ends_triple_dot = False
            # There are three wildcards allowed in p4 views
            # (see "p4 help views").  This code knows how to
            # handle "..." (only at the end), but cannot deal with
            # "%%n" or "*".  Only check the depot_side, as p4 should
            # validate that the client_side matches too.
            if re.search(r'%%[1-9]', self.path):
                die("Can't handle %%n wildcards in view: %s" % self.path)
            if self.path.find("*") >= 0:
                die("Can't handle * wildcards in view: %s" % self.path)
            triple_dot_index = self.path.find("...")
            if triple_dot_index >= 0:
1798 1799
                if triple_dot_index != len(self.path) - 3:
                    die("Can handle only single ... wildcard, at end: %s" %
P
Pete Wyckoff 已提交
1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853
                        self.path)
                self.ends_triple_dot = True

        def ensure_compatible(self, other_path):
            """Make sure the wildcards agree."""
            if self.ends_triple_dot != other_path.ends_triple_dot:
                 die("Both paths must end with ... if either does;\n" +
                     "paths: %s %s" % (self.path, other_path.path))

        def match_wildcards(self, test_path):
            """See if this test_path matches us, and fill in the value
               of the wildcards if so.  Returns a tuple of
               (True|False, wildcards[]).  For now, only the ... at end
               is supported, so at most one wildcard."""
            if self.ends_triple_dot:
                dotless = self.path[:-3]
                if test_path.startswith(dotless):
                    wildcard = test_path[len(dotless):]
                    return (True, [ wildcard ])
            else:
                if test_path == self.path:
                    return (True, [])
            return (False, [])

        def match(self, test_path):
            """Just return if it matches; don't bother with the wildcards."""
            b, _ = self.match_wildcards(test_path)
            return b

        def fill_in_wildcards(self, wildcards):
            """Return the relative path, with the wildcards filled in
               if there are any."""
            if self.ends_triple_dot:
                return self.path[:-3] + wildcards[0]
            else:
                return self.path

    class Mapping(object):
        def __init__(self, depot_side, client_side, overlay, exclude):
            # depot_side is without the trailing /... if it had one
            self.depot_side = View.Path(depot_side, is_depot=True)
            self.client_side = View.Path(client_side, is_depot=False)
            self.overlay = overlay  # started with "+"
            self.exclude = exclude  # started with "-"
            assert not (self.overlay and self.exclude)
            self.depot_side.ensure_compatible(self.client_side)

        def __str__(self):
            c = " "
            if self.overlay:
                c = "+"
            if self.exclude:
                c = "-"
            return "View.Mapping: %s%s -> %s" % \
P
Pete Wyckoff 已提交
1854
                   (c, self.depot_side.path, self.client_side.path)
P
Pete Wyckoff 已提交
1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953

        def map_depot_to_client(self, depot_path):
            """Calculate the client path if using this mapping on the
               given depot path; does not consider the effect of other
               mappings in a view.  Even excluded mappings are returned."""
            matches, wildcards = self.depot_side.match_wildcards(depot_path)
            if not matches:
                return ""
            client_path = self.client_side.fill_in_wildcards(wildcards)
            return client_path

    #
    # View methods
    #
    def __init__(self):
        self.mappings = []

    def append(self, view_line):
        """Parse a view line, splitting it into depot and client
           sides.  Append to self.mappings, preserving order."""

        # Split the view line into exactly two words.  P4 enforces
        # structure on these lines that simplifies this quite a bit.
        #
        # Either or both words may be double-quoted.
        # Single quotes do not matter.
        # Double-quote marks cannot occur inside the words.
        # A + or - prefix is also inside the quotes.
        # There are no quotes unless they contain a space.
        # The line is already white-space stripped.
        # The two words are separated by a single space.
        #
        if view_line[0] == '"':
            # First word is double quoted.  Find its end.
            close_quote_index = view_line.find('"', 1)
            if close_quote_index <= 0:
                die("No first-word closing quote found: %s" % view_line)
            depot_side = view_line[1:close_quote_index]
            # skip closing quote and space
            rhs_index = close_quote_index + 1 + 1
        else:
            space_index = view_line.find(" ")
            if space_index <= 0:
                die("No word-splitting space found: %s" % view_line)
            depot_side = view_line[0:space_index]
            rhs_index = space_index + 1

        if view_line[rhs_index] == '"':
            # Second word is double quoted.  Make sure there is a
            # double quote at the end too.
            if not view_line.endswith('"'):
                die("View line with rhs quote should end with one: %s" %
                    view_line)
            # skip the quotes
            client_side = view_line[rhs_index+1:-1]
        else:
            client_side = view_line[rhs_index:]

        # prefix + means overlay on previous mapping
        overlay = False
        if depot_side.startswith("+"):
            overlay = True
            depot_side = depot_side[1:]

        # prefix - means exclude this path
        exclude = False
        if depot_side.startswith("-"):
            exclude = True
            depot_side = depot_side[1:]

        m = View.Mapping(depot_side, client_side, overlay, exclude)
        self.mappings.append(m)

    def map_in_client(self, depot_path):
        """Return the relative location in the client where this
           depot file should live.  Returns "" if the file should
           not be mapped in the client."""

        paths_filled = []
        client_path = ""

        # look at later entries first
        for m in self.mappings[::-1]:

            # see where will this path end up in the client
            p = m.map_depot_to_client(depot_path)

            if p == "":
                # Depot path does not belong in client.  Must remember
                # this, as previous items should not cause files to
                # exist in this path either.  Remember that the list is
                # being walked from the end, which has higher precedence.
                # Overlap mappings do not exclude previous mappings.
                if not m.overlay:
                    paths_filled.append(m.client_side)

            else:
                # This mapping matched; no need to search any further.
                # But, the mapping could be rejected if the client path
P
Pete Wyckoff 已提交
1954 1955
                # has already been claimed by an earlier mapping (i.e.
                # one later in the list, which we are walking backwards).
P
Pete Wyckoff 已提交
1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972
                already_mapped_in_client = False
                for f in paths_filled:
                    # this is View.Path.match
                    if f.match(p):
                        already_mapped_in_client = True
                        break
                if not already_mapped_in_client:
                    # Include this file, unless it is from a line that
                    # explicitly said to exclude it.
                    if not m.exclude:
                        client_path = p

                # a match, even if rejected, always stops the search
                break

        return client_path

1973
class P4Sync(Command, P4UserMap):
1974 1975
    delete_actions = ( "delete", "move/delete", "purge" )

1976 1977
    def __init__(self):
        Command.__init__(self)
1978
        P4UserMap.__init__(self)
1979 1980 1981 1982 1983
        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"),
1984
                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1985
                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1986 1987
                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
                                     help="Import into refs/heads/ , not refs/remotes"),
1988
                optparse.make_option("--max-changes", dest="maxChanges"),
1989
                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1990 1991 1992
                                     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")
1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003
        ]
        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
2004 2005
        self.createdBranches = set()
        self.committedChanges = set()
2006
        self.branch = ""
2007
        self.detectBranches = False
2008
        self.detectLabels = False
2009
        self.importLabels = False
2010
        self.changesFile = ""
2011
        self.syncWithOrigin = True
2012
        self.importIntoRemotes = True
2013
        self.maxChanges = ""
2014
        self.keepRepoPath = False
2015
        self.depotPaths = None
2016
        self.p4BranchesInGit = []
T
Tommy Thorn 已提交
2017
        self.cloneExclude = []
2018
        self.useClientSpec = False
2019
        self.useClientSpec_from_options = False
P
Pete Wyckoff 已提交
2020
        self.clientSpecDirs = None
2021 2022
        self.tempBranches = []
        self.tempBranchLocation = "git-p4-tmp"
2023

2024 2025 2026
        if gitConfig("git-p4.syncFromOrigin") == "false":
            self.syncWithOrigin = False

2027 2028 2029 2030 2031 2032 2033 2034
    # Force a checkpoint in fast-import and wait for it to finish
    def checkpoint(self):
        self.gitStream.write("checkpoint\n\n")
        self.gitStream.write("progress checkpoint\n\n")
        out = self.gitOutput.readline()
        if self.verbose:
            print "checkpoint finished: " + out

2035
    def extractFilesFromCommit(self, commit):
T
Tommy Thorn 已提交
2036 2037
        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
                             for path in self.cloneExclude]
2038 2039 2040 2041
        files = []
        fnum = 0
        while commit.has_key("depotFile%s" % fnum):
            path =  commit["depotFile%s" % fnum]
2042

T
Tommy Thorn 已提交
2043
            if [p for p in self.cloneExclude
2044
                if p4PathStartsWith(path, p)]:
T
Tommy Thorn 已提交
2045 2046 2047
                found = False
            else:
                found = [p for p in self.depotPaths
2048
                         if p4PathStartsWith(path, p)]
2049
            if not found:
2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061
                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

2062
    def stripRepoPath(self, path, prefixes):
2063 2064 2065 2066 2067
        """When streaming files, this is called to map a p4 depot path
           to where it should go in git.  The prefixes are either
           self.depotPaths, or self.branchPrefixes in the case of
           branch detection."""

2068
        if self.useClientSpec:
2069 2070
            # branch detection moves files up a level (the branch name)
            # from what client spec interpretation gives
2071
            path = self.clientSpecDirs.map_in_client(path)
2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083
            if self.detectBranches:
                for b in self.knownBranches:
                    if path.startswith(b + "/"):
                        path = path[len(b)+1:]

        elif self.keepRepoPath:
            # Preserve everything in relative path name except leading
            # //depot/; just look at first prefix as they all should
            # be in the same depot.
            depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
            if p4PathStartsWith(path, depot):
                path = path[len(depot):]
2084

2085 2086 2087 2088
        else:
            for p in prefixes:
                if p4PathStartsWith(path, p):
                    path = path[len(p):]
2089
                    break
2090

2091
        path = wildcard_decode(path)
2092
        return path
H
Han-Wen Nienhuys 已提交
2093

2094
    def splitFilesIntoBranches(self, commit):
2095 2096 2097
        """Look at each depotFile in the commit to figure out to what
           branch it belongs."""

2098
        branches = {}
2099 2100 2101
        fnum = 0
        while commit.has_key("depotFile%s" % fnum):
            path =  commit["depotFile%s" % fnum]
2102
            found = [p for p in self.depotPaths
2103
                     if p4PathStartsWith(path, p)]
2104
            if not found:
2105 2106 2107 2108 2109 2110 2111 2112 2113 2114
                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

2115 2116 2117 2118 2119 2120
            # start with the full relative path where this file would
            # go in a p4 client
            if self.useClientSpec:
                relPath = self.clientSpecDirs.map_in_client(path)
            else:
                relPath = self.stripRepoPath(path, self.depotPaths)
2121

2122
            for branch in self.knownBranches.keys():
2123 2124
                # add a trailing slash so that a commit into qt/4.2foo
                # doesn't end up in qt/4.2, e.g.
H
Han-Wen Nienhuys 已提交
2125
                if relPath.startswith(branch + "/"):
2126 2127
                    if branch not in branches:
                        branches[branch] = []
2128
                    branches[branch].append(file)
2129
                    break
2130 2131 2132

        return branches

2133 2134 2135 2136 2137 2138 2139 2140
    # output one file from the P4 stream
    # - helper for streamP4Files

    def streamOneP4File(self, file, contents):
        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
        if verbose:
            sys.stderr.write("%s\n" % relPath)

P
Pete Wyckoff 已提交
2141 2142 2143 2144 2145 2146 2147 2148
        (type_base, type_mods) = split_p4_type(file["type"])

        git_mode = "100644"
        if "x" in type_mods:
            git_mode = "100755"
        if type_base == "symlink":
            git_mode = "120000"
            # p4 print on a symlink contains "target\n"; remove the newline
2149 2150
            data = ''.join(contents)
            contents = [data[:-1]]
2151

P
Pete Wyckoff 已提交
2152
        if type_base == "utf16":
2153 2154 2155 2156 2157
            # p4 delivers different text in the python output to -G
            # than it does when using "print -o", or normal p4 client
            # operations.  utf16 is converted to ascii or utf8, perhaps.
            # But ascii text saved as -t utf16 is completely mangled.
            # Invoke print -o to get the real contents.
2158 2159 2160 2161 2162
            #
            # On windows, the newlines will always be mangled by print, so put
            # them back too.  This is not needed to the cygwin windows version,
            # just the native "NT" type.
            #
2163
            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
2164 2165
            if p4_version_string().find("/NT") >= 0:
                text = text.replace("\r\n", "\n")
2166 2167
            contents = [ text ]

P
Pete Wyckoff 已提交
2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180
        if type_base == "apple":
            # Apple filetype files will be streamed as a concatenation of
            # its appledouble header and the contents.  This is useless
            # on both macs and non-macs.  If using "print -q -o xx", it
            # will create "xx" with the data, and "%xx" with the header.
            # This is also not very useful.
            #
            # Ideally, someday, this script can learn how to generate
            # appledouble files directly and import those to git, but
            # non-mac machines can never find a use for apple filetype.
            print "\nIgnoring apple filetype file %s" % file['depotFile']
            return

2181 2182
        # Note that we do not try to de-mangle keywords on utf16 files,
        # even though in theory somebody may want that.
2183 2184 2185 2186 2187 2188
        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
        if pattern:
            regexp = re.compile(pattern, re.VERBOSE)
            text = ''.join(contents)
            text = regexp.sub(r'$\1$', text)
            contents = [ text ]
2189

P
Pete Wyckoff 已提交
2190
        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210

        # total length...
        length = 0
        for d in contents:
            length = length + len(d)

        self.gitStream.write("data %d\n" % length)
        for d in contents:
            self.gitStream.write(d)
        self.gitStream.write("\n")

    def streamOneP4Deletion(self, file):
        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
        if verbose:
            sys.stderr.write("delete %s\n" % relPath)
        self.gitStream.write("D %s\n" % relPath)

    # handle another chunk of streaming data
    def streamP4FilesCb(self, marshalled):

2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233
        # catch p4 errors and complain
        err = None
        if "code" in marshalled:
            if marshalled["code"] == "error":
                if "data" in marshalled:
                    err = marshalled["data"].rstrip()
        if err:
            f = None
            if self.stream_have_file_info:
                if "depotFile" in self.stream_file:
                    f = self.stream_file["depotFile"]
            # force a failure in fast-import, else an empty
            # commit will be made
            self.gitStream.write("\n")
            self.gitStream.write("die-now\n")
            self.gitStream.close()
            # ignore errors, but make sure it exits first
            self.importProcess.wait()
            if f:
                die("Error from p4 print for %s: %s" % (f, err))
            else:
                die("Error from p4 print: %s" % err)

2234 2235 2236 2237 2238 2239
        if marshalled.has_key('depotFile') and self.stream_have_file_info:
            # start of a new file - output the old one first
            self.streamOneP4File(self.stream_file, self.stream_contents)
            self.stream_file = {}
            self.stream_contents = []
            self.stream_have_file_info = False
2240

2241 2242 2243 2244 2245 2246 2247
        # pick up the new file information... for the
        # 'data' field we need to append to our array
        for k in marshalled.keys():
            if k == 'data':
                self.stream_contents.append(marshalled['data'])
            else:
                self.stream_file[k] = marshalled[k]
2248

2249
        self.stream_have_file_info = True
2250 2251 2252

    # Stream directly from "p4 files" into "git fast-import"
    def streamP4Files(self, files):
2253 2254
        filesForCommit = []
        filesToRead = []
2255
        filesToDelete = []
2256

2257
        for f in files:
P
Pete Wyckoff 已提交
2258 2259 2260 2261 2262
            # if using a client spec, only add the files that have
            # a path in the client
            if self.clientSpecDirs:
                if self.clientSpecDirs.map_in_client(f['path']) == "":
                    continue
2263

P
Pete Wyckoff 已提交
2264 2265 2266 2267 2268
            filesForCommit.append(f)
            if f['action'] in self.delete_actions:
                filesToDelete.append(f)
            else:
                filesToRead.append(f)
H
Han-Wen Nienhuys 已提交
2269

2270 2271 2272
        # deleted files...
        for f in filesToDelete:
            self.streamOneP4Deletion(f)
2273

2274 2275 2276 2277
        if len(filesToRead) > 0:
            self.stream_file = {}
            self.stream_contents = []
            self.stream_have_file_info = False
2278

2279 2280 2281
            # curry self argument
            def streamP4FilesCbSelf(entry):
                self.streamP4FilesCb(entry)
H
Han-Wen Nienhuys 已提交
2282

2283 2284 2285 2286 2287
            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]

            p4CmdList(["-x", "-", "print"],
                      stdin=fileArgs,
                      cb=streamP4FilesCbSelf)
2288

2289 2290 2291
            # do the last chunk
            if self.stream_file.has_key('depotFile'):
                self.streamOneP4File(self.stream_file, self.stream_contents)
H
Han-Wen Nienhuys 已提交
2292

2293 2294 2295 2296 2297 2298
    def make_email(self, userid):
        if userid in self.users:
            return self.users[userid]
        else:
            return "%s <a@b>" % userid

2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330
    # Stream a p4 tag
    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
        if verbose:
            print "writing tag %s for commit %s" % (labelName, commit)
        gitStream.write("tag %s\n" % labelName)
        gitStream.write("from %s\n" % commit)

        if labelDetails.has_key('Owner'):
            owner = labelDetails["Owner"]
        else:
            owner = None

        # Try to use the owner of the p4 label, or failing that,
        # the current p4 user id.
        if owner:
            email = self.make_email(owner)
        else:
            email = self.make_email(self.p4UserId())
        tagger = "%s %s %s" % (email, epoch, self.tz)

        gitStream.write("tagger %s\n" % tagger)

        print "labelDetails=",labelDetails
        if labelDetails.has_key('Description'):
            description = labelDetails['Description']
        else:
            description = 'Label from git p4'

        gitStream.write("data %d\n" % len(description))
        gitStream.write(description)
        gitStream.write("\n")

2331
    def commit(self, details, files, branch, parent = ""):
2332 2333 2334
        epoch = details["time"]
        author = details["user"]

2335 2336 2337
        if self.verbose:
            print "commit into %s" % branch

2338 2339 2340 2341
        # start with reading files; if that fails, we should not
        # create a commit.
        new_files = []
        for f in files:
2342
            if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2343 2344
                new_files.append (f)
            else:
2345
                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2346

2347
        self.gitStream.write("commit %s\n" % branch)
H
Han-Wen Nienhuys 已提交
2348
#        gitStream.write("mark :%s\n" % details["change"])
2349 2350
        self.committedChanges.add(int(details["change"]))
        committer = ""
2351 2352
        if author not in self.users:
            self.getUserMapFromPerforceServer()
2353
        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2354 2355 2356 2357 2358

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

        self.gitStream.write("data <<EOT\n")
        self.gitStream.write(details["desc"])
2359 2360
        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
                             (','.join(self.branchPrefixes), details["change"]))
2361 2362 2363
        if len(details['options']) > 0:
            self.gitStream.write(": options = %s" % details['options'])
        self.gitStream.write("]\nEOT\n\n")
2364 2365

        if len(parent) > 0:
2366 2367
            if self.verbose:
                print "parent %s" % parent
2368 2369
            self.gitStream.write("from %s\n" % parent)

2370
        self.streamP4Files(new_files)
2371 2372
        self.gitStream.write("\n")

2373 2374
        change = int(details["change"])

2375
        if self.labels.has_key(change):
2376 2377 2378
            label = self.labels[change]
            labelDetails = label[0]
            labelRevisions = label[1]
2379 2380
            if self.verbose:
                print "Change %s is labelled %s" % (change, labelDetails)
2381

2382
            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2383
                                                for p in self.branchPrefixes])
2384 2385 2386 2387 2388

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

                cleanedFiles = {}
                for info in files:
2389
                    if info["action"] in self.delete_actions:
2390 2391 2392 2393
                        continue
                    cleanedFiles[info["depotFile"]] = info["rev"]

                if cleanedFiles == labelRevisions:
2394
                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2395 2396

                else:
2397
                    if not self.silent:
2398 2399
                        print ("Tag %s does not match with change %s: files do not match."
                               % (labelDetails["label"], change))
2400 2401

            else:
2402
                if not self.silent:
2403 2404
                    print ("Tag %s does not match with change %s: file count is different."
                           % (labelDetails["label"], change))
2405

2406
    # Build a dictionary of changelists and labels, for "detect-labels" option.
2407 2408 2409
    def getLabels(self):
        self.labels = {}

2410
        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
S
Simon Hausmann 已提交
2411
        if len(l) > 0 and not self.silent:
2412
            print "Finding files belonging to labels in %s" % `self.depotPaths`
S
Simon Hausmann 已提交
2413 2414

        for output in l:
2415 2416 2417
            label = output["label"]
            revisions = {}
            newestChange = 0
2418 2419
            if self.verbose:
                print "Querying files for label %s" % label
2420 2421 2422
            for file in p4CmdList(["files"] +
                                      ["%s...@%s" % (p, label)
                                          for p in self.depotPaths]):
2423 2424 2425 2426 2427
                revisions[file["depotFile"]] = file["rev"]
                change = int(file["change"])
                if change > newestChange:
                    newestChange = change

2428 2429 2430 2431
            self.labels[newestChange] = [output, revisions]

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

2433 2434 2435 2436 2437 2438 2439 2440 2441
    # Import p4 labels as git tags. A direct mapping does not
    # exist, so assume that if all the files are at the same revision
    # then we can use that, or it's something more complicated we should
    # just ignore.
    def importP4Labels(self, stream, p4Labels):
        if verbose:
            print "import p4 labels: " + ' '.join(p4Labels)

        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2442
        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477
        if len(validLabelRegexp) == 0:
            validLabelRegexp = defaultLabelRegexp
        m = re.compile(validLabelRegexp)

        for name in p4Labels:
            commitFound = False

            if not m.match(name):
                if verbose:
                    print "label %s does not match regexp %s" % (name,validLabelRegexp)
                continue

            if name in ignoredP4Labels:
                continue

            labelDetails = p4CmdList(['label', "-o", name])[0]

            # get the most recent changelist for each file in this label
            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
                                for p in self.depotPaths])

            if change.has_key('change'):
                # find the corresponding git commit; take the oldest commit
                changelist = int(change['change'])
                gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
                     "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
                if len(gitCommit) == 0:
                    print "could not find git commit for changelist %d" % changelist
                else:
                    gitCommit = gitCommit.strip()
                    commitFound = True
                    # Convert from p4 time format
                    try:
                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
                    except ValueError:
2478
                        print "Could not convert label time %s" % labelDetails['Update']
2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495
                        tmwhen = 1

                    when = int(time.mktime(tmwhen))
                    self.streamTag(stream, name, labelDetails, gitCommit, when)
                    if verbose:
                        print "p4 label %s mapped to git commit %s" % (name, gitCommit)
            else:
                if verbose:
                    print "Label %s has no changelists - possibly deleted?" % name

            if not commitFound:
                # We can't import this label; don't try again as it will get very
                # expensive repeatedly fetching all the files for labels that will
                # never be imported. If the label is moved in the future, the
                # ignore will need to be removed manually.
                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])

2496 2497
    def guessProjectName(self):
        for p in self.depotPaths:
S
Simon Hausmann 已提交
2498 2499 2500 2501 2502 2503
            if p.endswith("/"):
                p = p[:-1]
            p = p[p.strip().rfind("/") + 1:]
            if not p.endswith("/"):
               p += "/"
            return p
2504

2505
    def getBranchMapping(self):
2506 2507
        lostAndFoundBranches = set()

2508 2509 2510 2511 2512 2513 2514
        user = gitConfig("git-p4.branchUser")
        if len(user) > 0:
            command = "branches -u %s" % user
        else:
            command = "branches"

        for info in p4CmdList(command):
2515
            details = p4Cmd(["branch", "-o", info["branch"]])
2516 2517 2518 2519 2520 2521 2522 2523 2524
            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]
2525
                ## HACK
2526
                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2527 2528
                    source = source[len(self.depotPaths[0]):-4]
                    destination = destination[len(self.depotPaths[0]):-4]
2529

2530 2531 2532 2533 2534 2535
                    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

2536 2537 2538 2539
                    self.knownBranches[destination] = source

                    lostAndFoundBranches.discard(destination)

2540
                    if source not in self.knownBranches:
2541 2542
                        lostAndFoundBranches.add(source)

2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561
        # Perforce does not strictly require branches to be defined, so we also
        # check git config for a branch list.
        #
        # Example of branch definition in git config file:
        # [git-p4]
        #   branchList=main:branchA
        #   branchList=main:branchB
        #   branchList=branchA:branchC
        configBranches = gitConfigList("git-p4.branchList")
        for branch in configBranches:
            if branch:
                (source, destination) = branch.split(":")
                self.knownBranches[destination] = source

                lostAndFoundBranches.discard(destination)

                if source not in self.knownBranches:
                    lostAndFoundBranches.add(source)

2562 2563 2564

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

2566 2567 2568 2569 2570 2571 2572 2573 2574
    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

H
Han-Wen Nienhuys 已提交
2575 2576 2577 2578 2579 2580 2581 2582 2583 2584
    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']))
2585

2586 2587 2588 2589 2590 2591 2592 2593 2594
    def gitRefForBranch(self, branch):
        if branch == "main":
            return self.refPrefix + "master"

        if len(branch) <= 0:
            return branch

        return self.refPrefix + self.projectName + branch

2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645
    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

2646
        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2647 2648 2649 2650 2651 2652 2653 2654 2655
        #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

2656 2657
    def searchParent(self, parent, branch, target):
        parentFound = False
2658 2659
        for blob in read_pipe_lines(["git", "rev-list", "--reverse",
                                     "--no-merges", parent]):
2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670
            blob = blob.strip()
            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
                parentFound = True
                if self.verbose:
                    print "Found parent of %s in commit %s" % (branch, blob)
                break
        if parentFound:
            return blob
        else:
            return None

2671 2672 2673
    def importChanges(self, changes):
        cnt = 1
        for change in changes:
P
Pete Wyckoff 已提交
2674
            description = p4_describe(change)
2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687
            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 + "/"
2688
                        self.branchPrefixes = [ branchPrefix ]
2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703

                        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 = ""
2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716
                            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
2717

2718 2719
                        branch = self.gitRefForBranch(branch)
                        parent = self.gitRefForBranch(parent)
2720 2721 2722 2723 2724 2725 2726 2727

                        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]

2728 2729
                        blob = None
                        if len(parent) > 0:
2730
                            tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2731 2732
                            if self.verbose:
                                print "Creating temporary branch: " + tempBranch
2733
                            self.commit(description, filesForCommit, tempBranch)
2734 2735 2736 2737
                            self.tempBranches.append(tempBranch)
                            self.checkpoint()
                            blob = self.searchParent(parent, branch, tempBranch)
                        if blob:
2738
                            self.commit(description, filesForCommit, branch, blob)
2739 2740 2741
                        else:
                            if self.verbose:
                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2742
                            self.commit(description, filesForCommit, branch, parent)
2743 2744
                else:
                    files = self.extractFilesFromCommit(description)
2745
                    self.commit(description, files, self.branch,
2746
                                self.initialParent)
2747
                    # only needed once, to connect to the previous commit
2748 2749 2750 2751 2752
                    self.initialParent = ""
            except IOError:
                print self.gitError.read()
                sys.exit(1)

2753 2754 2755
    def importHeadRevision(self, revision):
        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)

2756 2757
        details = {}
        details["user"] = "git perforce import user"
2758
        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2759 2760 2761 2762 2763
                           % (' '.join(self.depotPaths), revision))
        details["change"] = revision
        newestRevision = 0

        fileCnt = 0
2764 2765 2766
        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]

        for info in p4CmdList(["files"] + fileArgs):
2767

2768
            if 'code' in info and info['code'] == 'error':
2769 2770
                sys.stderr.write("p4 returned an error: %s\n"
                                 % info['data'])
2771 2772 2773 2774
                if info['data'].find("must refer to client") >= 0:
                    sys.stderr.write("This particular p4 error is misleading.\n")
                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2775
                sys.exit(1)
2776 2777
            if 'p4ExitCode' in info:
                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2778 2779 2780 2781 2782 2783 2784
                sys.exit(1)


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

2785
            if info["action"] in self.delete_actions:
2786 2787 2788 2789 2790 2791 2792 2793 2794 2795
                # 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
2796

P
Pete Wyckoff 已提交
2797
        # Use time from top-most change so that all git p4 clones of
2798
        # the same p4 repo have the same commit SHA1s.
P
Pete Wyckoff 已提交
2799 2800
        res = p4_describe(newestRevision)
        details["time"] = res["time"]
2801

2802 2803
        self.updateOptionDict(details)
        try:
2804
            self.commit(details, self.extractFilesFromCommit(details), self.branch)
2805 2806 2807 2808 2809
        except IOError:
            print "IO error with git fast-import. Is your git version recent enough?"
            print self.gitError.read()


2810
    def run(self, args):
2811
        self.depotPaths = []
2812
        self.changeRange = ""
2813
        self.previousDepotPaths = []
2814
        self.hasOrigin = False
H
Han-Wen Nienhuys 已提交
2815

2816 2817 2818 2819
        # map from branch depot path to parent branch
        self.knownBranches = {}
        self.initialParents = {}

2820 2821 2822
        if self.importIntoRemotes:
            self.refPrefix = "refs/remotes/p4/"
        else:
2823
            self.refPrefix = "refs/heads/p4/"
2824

2825 2826 2827 2828 2829 2830
        if self.syncWithOrigin:
            self.hasOrigin = originP4BranchesExist()
            if self.hasOrigin:
                if not self.silent:
                    print 'Syncing with origin first, using "git fetch origin"'
                system("git fetch origin")
2831

2832
        branch_arg_given = bool(self.branch)
2833
        if len(self.branch) == 0:
2834
            self.branch = self.refPrefix + "master"
2835
            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2836
                system("git update-ref %s refs/heads/p4" % self.branch)
2837
                system("git branch -D p4")
2838

2839 2840 2841 2842 2843
        # accept either the command-line option, or the configuration variable
        if self.useClientSpec:
            # will use this after clone to set the variable
            self.useClientSpec_from_options = True
        else:
P
Pete Wyckoff 已提交
2844
            if gitConfigBool("git-p4.useclientspec"):
2845 2846
                self.useClientSpec = True
        if self.useClientSpec:
2847
            self.clientSpecDirs = getClientSpec()
2848

H
Han-Wen Nienhuys 已提交
2849 2850 2851
        # TODO: should always look at previous commits,
        # merge with previous imports, if possible.
        if args == []:
2852
            if self.hasOrigin:
2853
                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2854 2855 2856

            # branches holds mapping from branch name to sha1
            branches = p4BranchesInGit(self.importIntoRemotes)
2857 2858 2859 2860 2861 2862 2863 2864

            # restrict to just this one, disabling detect-branches
            if branch_arg_given:
                short = self.branch.split("/")[-1]
                if short in branches:
                    self.p4BranchesInGit = [ short ]
            else:
                self.p4BranchesInGit = branches.keys()
2865 2866 2867 2868 2869

            if len(self.p4BranchesInGit) > 1:
                if not self.silent:
                    print "Importing from/into multiple branches"
                self.detectBranches = True
2870 2871 2872
                for branch in branches.keys():
                    self.initialParents[self.refPrefix + branch] = \
                        branches[branch]
2873

2874 2875 2876 2877 2878
            if self.verbose:
                print "branches: %s" % self.p4BranchesInGit

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

                settings = extractSettingsGitLog(logMsg)
2882

H
Han-Wen Nienhuys 已提交
2883 2884 2885 2886
                self.readOptions(settings)
                if (settings.has_key('depot-paths')
                    and settings.has_key ('change')):
                    change = int(settings['change']) + 1
2887 2888
                    p4Change = max(p4Change, change)

H
Han-Wen Nienhuys 已提交
2889 2890
                    depotPaths = sorted(settings['depot-paths'])
                    if self.previousDepotPaths == []:
2891
                        self.previousDepotPaths = depotPaths
2892
                    else:
2893 2894
                        paths = []
                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2895 2896 2897 2898
                            prev_list = prev.split("/")
                            cur_list = cur.split("/")
                            for i in range(0, min(len(cur_list), len(prev_list))):
                                if cur_list[i] <> prev_list[i]:
2899
                                    i = i - 1
2900 2901
                                    break

2902
                            paths.append ("/".join(cur_list[:i + 1]))
2903 2904

                        self.previousDepotPaths = paths
2905 2906

            if p4Change > 0:
H
Han-Wen Nienhuys 已提交
2907
                self.depotPaths = sorted(self.previousDepotPaths)
2908
                self.changeRange = "@%s,#head" % p4Change
2909
                if not self.silent and not self.detectBranches:
2910
                    print "Performing incremental import into %s git branch" % self.branch
2911

2912 2913 2914 2915
        # accept multiple ref name abbreviations:
        #    refs/foo/bar/branch -> use it exactly
        #    p4/branch -> prepend refs/remotes/ or refs/heads/
        #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2916
        if not self.branch.startswith("refs/"):
2917 2918 2919 2920 2921 2922 2923
            if self.importIntoRemotes:
                prepend = "refs/remotes/"
            else:
                prepend = "refs/heads/"
            if not self.branch.startswith("p4/"):
                prepend += "p4/"
            self.branch = prepend + self.branch
2924

2925
        if len(args) == 0 and self.depotPaths:
2926
            if not self.silent:
2927
                print "Depot paths: %s" % ' '.join(self.depotPaths)
2928
        else:
2929
            if self.depotPaths and self.depotPaths != args:
2930
                print ("previous import used depot path %s and now %s was specified. "
2931 2932
                       "This doesn't work!" % (' '.join (self.depotPaths),
                                               ' '.join (args)))
2933
                sys.exit(1)
2934

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

2937
        revision = ""
2938 2939
        self.users = {}

2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950
        # Make sure no revision specifiers are used when --changesfile
        # is specified.
        bad_changesfile = False
        if len(self.changesFile) > 0:
            for p in self.depotPaths:
                if p.find("@") >= 0 or p.find("#") >= 0:
                    bad_changesfile = True
                    break
        if bad_changesfile:
            die("Option --changesfile is incompatible with revision specifiers")

2951 2952 2953 2954 2955 2956 2957
        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 已提交
2958
                elif ',' not in self.changeRange:
2959
                    revision = self.changeRange
2960
                    self.changeRange = ""
2961
                p = p[:atIdx]
2962 2963
            elif p.find("#") != -1:
                hashIdx = p.index("#")
2964
                revision = p[hashIdx:]
2965
                p = p[:hashIdx]
2966
            elif self.previousDepotPaths == []:
2967 2968 2969 2970
                # pay attention to changesfile, if given, else import
                # the entire p4 tree at the head revision
                if len(self.changesFile) == 0:
                    revision = "#head"
2971 2972 2973 2974 2975 2976 2977 2978 2979

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

            newPaths.append(p)

        self.depotPaths = newPaths

2980 2981 2982
        # --detect-branches may change this for each branch
        self.branchPrefixes = self.depotPaths

2983
        self.loadUserMapFromCache()
2984 2985 2986
        self.labels = {}
        if self.detectLabels:
            self.getLabels();
2987

2988
        if self.detectBranches:
2989 2990 2991
            ## FIXME - what's a P4 projectName ?
            self.projectName = self.guessProjectName()

2992 2993 2994 2995
            if self.hasOrigin:
                self.getBranchMappingFromGitBranches()
            else:
                self.getBranchMapping()
2996 2997 2998 2999 3000
            if self.verbose:
                print "p4-git branches: %s" % self.p4BranchesInGit
                print "initial parents: %s" % self.initialParents
            for b in self.p4BranchesInGit:
                if b != "master":
3001 3002

                    ## FIXME
3003 3004
                    b = b[len(self.projectName):]
                self.createdBranches.add(b)
3005

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

3008 3009 3010 3011 3012 3013 3014
        self.importProcess = subprocess.Popen(["git", "fast-import"],
                                              stdin=subprocess.PIPE,
                                              stdout=subprocess.PIPE,
                                              stderr=subprocess.PIPE);
        self.gitOutput = self.importProcess.stdout
        self.gitStream = self.importProcess.stdin
        self.gitError = self.importProcess.stderr
3015

3016
        if revision:
3017
            self.importHeadRevision(revision)
3018 3019 3020
        else:
            changes = []

3021
            if len(self.changesFile) > 0:
3022
                output = open(self.changesFile).readlines()
3023
                changeSet = set()
3024 3025 3026 3027 3028 3029 3030 3031
                for line in output:
                    changeSet.add(int(line))

                for change in changeSet:
                    changes.append(change)

                changes.sort()
            else:
P
Pete Wyckoff 已提交
3032 3033
                # catch "git p4 sync" with no new branches, in a repo that
                # does not have any existing p4 branches
3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048
                if len(args) == 0:
                    if not self.p4BranchesInGit:
                        die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")

                    # The default branch is master, unless --branch is used to
                    # specify something else.  Make sure it exists, or complain
                    # nicely about how to use --branch.
                    if not self.detectBranches:
                        if not branch_exists(self.branch):
                            if branch_arg_given:
                                die("Error: branch %s does not exist." % self.branch)
                            else:
                                die("Error: no branch %s; perhaps specify one with --branch." %
                                    self.branch)

3049
                if self.verbose:
3050
                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3051
                                                              self.changeRange)
3052
                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
3053

3054
                if len(self.maxChanges) > 0:
3055
                    changes = changes[:min(int(self.maxChanges), len(changes))]
3056

3057
            if len(changes) == 0:
3058
                if not self.silent:
3059
                    print "No changes to import!"
3060 3061 3062 3063 3064
            else:
                if not self.silent and not self.detectBranches:
                    print "Import destination: %s" % self.branch

                self.updatedBranches = set()
3065

3066 3067 3068 3069 3070 3071 3072 3073
                if not self.detectBranches:
                    if args:
                        # start a new branch
                        self.initialParent = ""
                    else:
                        # build on a previous revision
                        self.initialParent = parseRevision(self.branch)

3074
                self.importChanges(changes)
3075

3076 3077 3078 3079 3080 3081 3082
                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")
3083

P
Pete Wyckoff 已提交
3084
        if gitConfigBool("git-p4.importLabels"):
3085
            self.importLabels = True
3086

3087 3088 3089 3090 3091 3092
        if self.importLabels:
            p4Labels = getP4Labels(self.depotPaths)
            gitTags = getGitTags()

            missingP4Labels = p4Labels - gitTags
            self.importP4Labels(self.gitStream, missingP4Labels)
3093 3094

        self.gitStream.close()
3095
        if self.importProcess.wait() != 0:
3096
            die("fast-import failed: %s" % self.gitError.read())
3097 3098 3099
        self.gitOutput.close()
        self.gitError.close()

3100 3101 3102 3103 3104 3105
        # Cleanup temporary branches created during import
        if self.tempBranches != []:
            for branch in self.tempBranches:
                read_pipe("git update-ref -d %s" % branch)
            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))

3106 3107 3108 3109 3110 3111 3112
        # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
        # a convenient shortcut refname "p4".
        if self.importIntoRemotes:
            head_ref = self.refPrefix + "HEAD"
            if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
                system(["git", "symbolic-ref", head_ref, self.branch])

3113 3114
        return True

S
Simon Hausmann 已提交
3115 3116 3117
class P4Rebase(Command):
    def __init__(self):
        Command.__init__(self)
3118 3119 3120 3121
        self.options = [
                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
        ]
        self.importLabels = False
3122 3123
        self.description = ("Fetches the latest revision from perforce and "
                            + "rebases the current work (branch) against it")
S
Simon Hausmann 已提交
3124 3125 3126

    def run(self, args):
        sync = P4Sync()
3127
        sync.importLabels = self.importLabels
S
Simon Hausmann 已提交
3128
        sync.run([])
3129

3130 3131 3132
        return self.rebase()

    def rebase(self):
3133 3134 3135 3136 3137
        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.");

3138 3139 3140 3141 3142 3143 3144 3145
        [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
3146
        oldHead = read_pipe("git rev-parse HEAD").strip()
3147
        system("git rebase %s" % upstream)
3148
        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
S
Simon Hausmann 已提交
3149 3150
        return True

3151 3152 3153 3154
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 已提交
3155
        self.usage = "usage: %prog [options] //depot/path[@revRange]"
T
Tommy Thorn 已提交
3156
        self.options += [
H
Han-Wen Nienhuys 已提交
3157 3158
            optparse.make_option("--destination", dest="cloneDestination",
                                 action='store', default=None,
T
Tommy Thorn 已提交
3159 3160 3161
                                 help="where to leave result of the clone"),
            optparse.make_option("-/", dest="cloneExclude",
                                 action="append", type="string",
P
Pete Wyckoff 已提交
3162 3163 3164
                                 help="exclude depot path"),
            optparse.make_option("--bare", dest="cloneBare",
                                 action="store_true", default=False),
T
Tommy Thorn 已提交
3165
        ]
H
Han-Wen Nienhuys 已提交
3166
        self.cloneDestination = None
3167
        self.needsGit = False
P
Pete Wyckoff 已提交
3168
        self.cloneBare = False
3169

T
Tommy Thorn 已提交
3170 3171 3172 3173 3174 3175
    # 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 已提交
3176 3177 3178 3179 3180
    def defaultDestination(self, args):
        ## TODO: use common prefix of args?
        depotPath = args[0]
        depotDir = re.sub("(@[^@]*)$", "", depotPath)
        depotDir = re.sub("(#[^#]*)$", "", depotDir)
3181
        depotDir = re.sub(r"\.\.\.$", "", depotDir)
H
Han-Wen Nienhuys 已提交
3182 3183 3184
        depotDir = re.sub(r"/$", "", depotDir)
        return os.path.split(depotDir)[1]

3185 3186 3187
    def run(self, args):
        if len(args) < 1:
            return False
H
Han-Wen Nienhuys 已提交
3188 3189 3190 3191

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

3193
        depotPaths = args
3194 3195 3196 3197 3198

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

T
Tommy Thorn 已提交
3199
        self.cloneExclude = ["/"+p for p in self.cloneExclude]
3200 3201
        for p in depotPaths:
            if not p.startswith("//"):
3202
                sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3203
                return False
3204

H
Han-Wen Nienhuys 已提交
3205
        if not self.cloneDestination:
3206
            self.cloneDestination = self.defaultDestination(args)
3207

3208
        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
P
Pete Wyckoff 已提交
3209

3210 3211
        if not os.path.exists(self.cloneDestination):
            os.makedirs(self.cloneDestination)
3212
        chdir(self.cloneDestination)
P
Pete Wyckoff 已提交
3213 3214 3215 3216 3217 3218

        init_cmd = [ "git", "init" ]
        if self.cloneBare:
            init_cmd.append("--bare")
        subprocess.check_call(init_cmd)

3219
        if not P4Sync.run(self, depotPaths):
3220
            return False
3221 3222 3223 3224 3225 3226 3227 3228 3229

        # create a master branch and check out a work tree
        if gitBranchExists(self.branch):
            system([ "git", "branch", "master", self.branch ])
            if not self.cloneBare:
                system([ "git", "checkout", "-f" ])
        else:
            print 'Not checking out any branch, use ' \
                  '"git checkout -q -b master <branch>"'
3230

3231 3232 3233 3234
        # auto-set this variable if invoked with --use-client-spec
        if self.useClientSpec_from_options:
            system("git config --bool git-p4.useclientspec true")

3235 3236
        return True

3237 3238 3239 3240 3241 3242 3243 3244 3245
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):
3246 3247 3248
        if originP4BranchesExist():
            createOrUpdateBranchesFromOrigin()

3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264
        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

3265 3266 3267 3268 3269 3270 3271 3272 3273
class HelpFormatter(optparse.IndentedHelpFormatter):
    def __init__(self):
        optparse.IndentedHelpFormatter.__init__(self)

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

3275 3276 3277 3278 3279 3280 3281 3282 3283
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 已提交
3284 3285
    "debug" : P4Debug,
    "submit" : P4Submit,
3286
    "commit" : P4Submit,
H
Han-Wen Nienhuys 已提交
3287 3288 3289
    "sync" : P4Sync,
    "rebase" : P4Rebase,
    "clone" : P4Clone,
3290 3291
    "rollback" : P4RollBack,
    "branches" : P4Branches
3292 3293 3294
}


H
Han-Wen Nienhuys 已提交
3295 3296 3297 3298
def main():
    if len(sys.argv[1:]) == 0:
        printUsage(commands.keys())
        sys.exit(2)
3299

H
Han-Wen Nienhuys 已提交
3300 3301
    cmdName = sys.argv[1]
    try:
H
Han-Wen Nienhuys 已提交
3302 3303
        klass = commands[cmdName]
        cmd = klass()
H
Han-Wen Nienhuys 已提交
3304 3305 3306 3307 3308 3309 3310
    except KeyError:
        print "unknown command %s" % cmdName
        print ""
        printUsage(commands.keys())
        sys.exit(2)

    options = cmd.options
H
Han-Wen Nienhuys 已提交
3311
    cmd.gitdir = os.environ.get("GIT_DIR", None)
H
Han-Wen Nienhuys 已提交
3312 3313 3314

    args = sys.argv[2:]

P
Pete Wyckoff 已提交
3315
    options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3316 3317
    if cmd.needsGit:
        options.append(optparse.make_option("--git-dir", dest="gitdir"))
H
Han-Wen Nienhuys 已提交
3318

3319 3320 3321 3322
    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
                                   options,
                                   description = cmd.description,
                                   formatter = HelpFormatter())
H
Han-Wen Nienhuys 已提交
3323

3324
    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
H
Han-Wen Nienhuys 已提交
3325 3326 3327
    global verbose
    verbose = cmd.verbose
    if cmd.needsGit:
H
Han-Wen Nienhuys 已提交
3328 3329 3330 3331 3332
        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 已提交
3333 3334
                    cdup = read_pipe("git rev-parse --show-cdup").strip()
                    if len(cdup) > 0:
3335
                        chdir(cdup);
3336

H
Han-Wen Nienhuys 已提交
3337 3338 3339
        if not isValidGitDir(cmd.gitdir):
            if isValidGitDir(cmd.gitdir + "/.git"):
                cmd.gitdir += "/.git"
H
Han-Wen Nienhuys 已提交
3340
            else:
H
Han-Wen Nienhuys 已提交
3341
                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3342

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

H
Han-Wen Nienhuys 已提交
3345 3346
    if not cmd.run(args):
        parser.print_help()
3347
        sys.exit(2)
3348 3349


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