git-p4.py 119.3 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

B
Brandon Casey 已提交
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
try:
    from subprocess import CalledProcessError
except ImportError:
    # from python2.7:subprocess.py
    # Exception classes used by this module.
    class CalledProcessError(Exception):
        """This exception is raised when a process run by check_call() returns
        a non-zero exit status.  The exit status will be stored in the
        returncode attribute."""
        def __init__(self, returncode, cmd):
            self.returncode = returncode
            self.cmd = cmd
        def __str__(self):
            return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)

41
verbose = False
42

43
# Only labels/tags matching this will be imported/exported
44
defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
45 46 47 48 49 50 51 52

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.
    """
53
    real_cmd = ["p4"]
54 55 56

    user = gitConfig("git-p4.user")
    if len(user) > 0:
57
        real_cmd += ["-u",user]
58 59 60

    password = gitConfig("git-p4.password")
    if len(password) > 0:
61
        real_cmd += ["-P", password]
62 63 64

    port = gitConfig("git-p4.port")
    if len(port) > 0:
65
        real_cmd += ["-p", port]
66 67 68

    host = gitConfig("git-p4.host")
    if len(host) > 0:
69
        real_cmd += ["-H", host]
70 71 72

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

75 76 77 78 79

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

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
def chdir(path, is_client_path=False):
    """Do chdir to the given path, and set the PWD environment
       variable for use by P4.  It does not look at getcwd() output.
       Since we're not using the shell, it is necessary to set the
       PWD environment variable explicitly.

       Normally, expand the path to force it to be absolute.  This
       addresses the use of relative path names inside P4 settings,
       e.g. P4CONFIG=.p4config.  P4 does not simply open the filename
       as given; it looks for .p4config using PWD.

       If is_client_path, the path was handed to us directly by p4,
       and may be a symbolic link.  Do not call os.getcwd() in this
       case, because it will cause p4 to think that PWD is not inside
       the client path.
       """

    os.chdir(path)
    if not is_client_path:
        path = os.getcwd()
    os.environ['PWD'] = path
103

104 105 106 107 108 109 110
def die(msg):
    if verbose:
        raise Exception(msg)
    else:
        sys.stderr.write(msg + "\n")
        sys.exit(1)

111
def write_pipe(c, stdin):
112
    if verbose:
113
        sys.stderr.write('Writing pipe: %s\n' % str(c))
H
Han-Wen Nienhuys 已提交
114

115 116 117 118 119 120 121
    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 已提交
122 123 124

    return val

125
def p4_write_pipe(c, stdin):
126
    real_cmd = p4_build_cmd(c)
127
    return write_pipe(real_cmd, stdin)
128

129 130
def read_pipe(c, ignore_error=False):
    if verbose:
131
        sys.stderr.write('Reading pipe: %s\n' % str(c))
132

133 134 135
    expand = isinstance(c,basestring)
    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
    pipe = p.stdout
H
Han-Wen Nienhuys 已提交
136
    val = pipe.read()
137 138
    if p.wait() and not ignore_error:
        die('Command failed: %s' % str(c))
H
Han-Wen Nienhuys 已提交
139 140 141

    return val

142 143 144
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 已提交
145

H
cleanup  
Han-Wen Nienhuys 已提交
146
def read_pipe_lines(c):
147
    if verbose:
148 149 150 151 152
        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 已提交
153
    val = pipe.readlines()
154 155
    if pipe.close() or p.wait():
        die('Command failed: %s' % str(c))
H
Han-Wen Nienhuys 已提交
156 157

    return val
158

159 160
def p4_read_pipe_lines(c):
    """Specifically invoke p4 on the command supplied. """
A
Anand Kumria 已提交
161
    real_cmd = p4_build_cmd(c)
162 163
    return read_pipe_lines(real_cmd)

164 165 166 167 168 169 170 171 172
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

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
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 已提交
192
def system(cmd):
193
    expand = isinstance(cmd,basestring)
194
    if verbose:
195
        sys.stderr.write("executing %s\n" % str(cmd))
B
Brandon Casey 已提交
196 197 198
    retcode = subprocess.call(cmd, shell=expand)
    if retcode:
        raise CalledProcessError(retcode, cmd)
H
Han-Wen Nienhuys 已提交
199

200 201
def p4_system(cmd):
    """Specifically invoke p4 as the system command. """
A
Anand Kumria 已提交
202
    real_cmd = p4_build_cmd(cmd)
203
    expand = isinstance(real_cmd, basestring)
B
Brandon Casey 已提交
204 205 206
    retcode = subprocess.call(real_cmd, shell=expand)
    if retcode:
        raise CalledProcessError(retcode, real_cmd)
207

208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
_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

224
def p4_integrate(src, dest):
225
    p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
226

227
def p4_sync(f, *options):
228
    p4_system(["sync"] + list(options) + [wildcard_encode(f)])
229 230

def p4_add(f):
231 232 233 234 235
    # forcibly add file names with wildcards
    if wildcard_present(f):
        p4_system(["add", "-f", f])
    else:
        p4_system(["add", f])
236 237

def p4_delete(f):
238
    p4_system(["delete", wildcard_encode(f)])
239 240

def p4_edit(f):
241
    p4_system(["edit", wildcard_encode(f)])
242 243

def p4_revert(f):
244
    p4_system(["revert", wildcard_encode(f)])
245

246 247
def p4_reopen(type, f):
    p4_system(["reopen", "-t", type, wildcard_encode(f)])
248

249 250 251
def p4_move(src, dest):
    p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])

P
Pete Wyckoff 已提交
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
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 已提交
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
#
# 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 已提交
309

310 311 312
#
# return the raw p4 type of a file (text, text+ko, etc)
#
313 314
def p4_type(f):
    results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
    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...
333
            (:[^$\n]+)?     # possibly an old expansion, followed by...
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
            \$              # 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 已提交
351

352 353 354 355 356 357 358 359 360 361 362 363 364
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]

365
    p4_reopen(p4Type, file)
366 367 368 369

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

370
    result = p4_read_pipe(["opened", wildcard_encode(file)])
371
    match = re.match(".*\((.+)\)\r?$", result)
372 373 374
    if match:
        return match.group(1)
    else:
375
        die("Could not determine file type for %s (result: '%s')" % (file, result))
376

377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
# 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

397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
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

437 438 439 440 441 442 443 444
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)

445
def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
446 447 448 449 450 451 452 453 454

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

    cmd = p4_build_cmd(cmd)
H
Han-Wen Nienhuys 已提交
455
    if verbose:
456
        sys.stderr.write("Opening pipe: %s\n" % str(cmd))
S
Scott Lamb 已提交
457 458 459 460 461 462 463

    # 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)
464 465 466 467 468
        if isinstance(stdin,basestring):
            stdin_file.write(stdin)
        else:
            for i in stdin:
                stdin_file.write(i + '\n')
S
Scott Lamb 已提交
469 470 471
        stdin_file.flush()
        stdin_file.seek(0)

472 473
    p4 = subprocess.Popen(cmd,
                          shell=expand,
S
Scott Lamb 已提交
474 475
                          stdin=stdin_file,
                          stdout=subprocess.PIPE)
476 477 478 479

    result = []
    try:
        while True:
S
Scott Lamb 已提交
480
            entry = marshal.load(p4.stdout)
481 482 483 484
            if cb is not None:
                cb(entry)
            else:
                result.append(entry)
485 486
    except EOFError:
        pass
S
Scott Lamb 已提交
487 488
    exitCode = p4.wait()
    if exitCode != 0:
489 490 491
        entry = {}
        entry["p4ExitCode"] = exitCode
        result.append(entry)
492 493 494 495 496 497 498 499 500 501

    return result

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

502 503 504
def p4Where(depotPath):
    if not depotPath.endswith("/"):
        depotPath += "/"
505
    depotPath = depotPath + "..."
506
    outputList = p4CmdList(["where", depotPath])
507 508
    output = None
    for entry in outputList:
509 510 511 512 513 514 515 516 517 518
        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
519 520
    if output == None:
        return ""
521 522
    if output["code"] == "error":
        return ""
523 524 525 526 527 528 529 530 531 532 533 534
    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

535
def currentGitBranch():
536
    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
537

538
def isValidGitDir(path):
H
Han-Wen Nienhuys 已提交
539 540
    if (os.path.exists(path + "/HEAD")
        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
541 542 543
        return True;
    return False

544
def parseRevision(ref):
545
    return read_pipe("git rev-parse %s" % ref).strip()
546

547 548 549 550 551
def branchExists(ref):
    rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
                     ignore_error=True)
    return len(rev) > 0

552 553
def extractLogMessageFromGitCommit(commit):
    logMessage = ""
H
Han-Wen Nienhuys 已提交
554 555

    ## fixme: title is first line of commit, not 1st paragraph.
556
    foundTitle = False
H
Han-Wen Nienhuys 已提交
557
    for log in read_pipe_lines("git cat-file commit %s" % commit):
558 559
       if not foundTitle:
           if len(log) == 1:
S
Simon Hausmann 已提交
560
               foundTitle = True
561 562 563 564 565
           continue

       logMessage += log
    return logMessage

H
Han-Wen Nienhuys 已提交
566
def extractSettingsGitLog(log):
567 568 569
    values = {}
    for line in log.split("\n"):
        line = line.strip()
570 571 572 573 574 575 576 577 578 579 580 581 582 583
        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

584 585 586
    paths = values.get("depot-paths")
    if not paths:
        paths = values.get("depot-path")
587 588
    if paths:
        values['depot-paths'] = paths.split(',')
H
Han-Wen Nienhuys 已提交
589
    return values
590

591
def gitBranchExists(branch):
H
Han-Wen Nienhuys 已提交
592 593
    proc = subprocess.Popen(["git", "rev-parse", branch],
                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
594
    return proc.wait() == 0;
595

596
_gitConfig = {}
597

P
Pete Wyckoff 已提交
598
def gitConfig(key):
599
    if not _gitConfig.has_key(key):
P
Pete Wyckoff 已提交
600
        cmd = [ "git", "config", key ]
601 602
        s = read_pipe(cmd, ignore_error=True)
        _gitConfig[key] = s.strip()
603
    return _gitConfig[key]
604

P
Pete Wyckoff 已提交
605 606 607 608 609
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."""

610
    if not _gitConfig.has_key(key):
P
Pete Wyckoff 已提交
611 612 613 614
        cmd = [ "git", "config", "--bool", key ]
        s = read_pipe(cmd, ignore_error=True)
        v = s.strip()
        _gitConfig[key] = v == "true"
615
    return _gitConfig[key]
616

617 618
def gitConfigList(key):
    if not _gitConfig.has_key(key):
619 620
        s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
        _gitConfig[key] = s.strip().split(os.linesep)
621 622
    return _gitConfig[key]

623 624 625 626 627 628 629
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."""

630 631 632 633
    branches = {}

    cmdline = "git rev-parse --symbolic "
    if branchesAreInRemotes:
634
        cmdline += "--remotes"
635
    else:
636
        cmdline += "--branches"
637 638 639 640

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

641 642 643 644 645
        # only import to p4/
        if not line.startswith('p4/'):
            continue
        # special symbolic ref to p4/master
        if line == "p4/HEAD":
646 647
            continue

648 649
        # strip off p4/ prefix
        branch = line[len("p4/"):]
650 651

        branches[branch] = parseRevision(line)
652

653 654
    return branches

655 656 657 658 659 660 661 662 663 664 665
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

666
def findUpstreamBranchPoint(head = "HEAD"):
667 668 669 670 671 672 673 674 675 676 677
    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

678 679 680
    settings = None
    parent = 0
    while parent < 65535:
681
        commit = head + "~%s" % parent
682 683
        log = extractLogMessageFromGitCommit(commit)
        settings = extractSettingsGitLog(log)
684 685 686 687
        if settings.has_key("depot-paths"):
            paths = ",".join(settings["depot-paths"])
            if branchByDepotPath.has_key(paths):
                return [branchByDepotPath[paths], settings]
688

689
        parent = parent + 1
690

691
    return ["", settings]
692

693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742
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")

743 744
def p4ChangesForPaths(depotPaths, changeRange):
    assert depotPaths
745 746 747 748
    cmd = ['changes']
    for p in depotPaths:
        cmd += ["%s...%s" % (p, changeRange)]
    output = p4_read_pipe_lines(cmd)
749

750
    changes = {}
751
    for line in output:
752 753
        changeNum = int(line.split(" ")[1])
        changes[changeNum] = True
754

755 756 757
    changelist = changes.keys()
    changelist.sort()
    return changelist
758

759 760 761 762 763 764 765 766
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 已提交
767
    if gitConfigBool("core.ignorecase"):
768 769 770
        return path.lower().startswith(prefix.lower())
    return path.startswith(prefix)

771 772 773 774 775 776 777 778 779 780 781 782
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]

783 784 785
    # the //client/ name
    client_name = entry["Client"]

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

    # hold this new View
790
    view = View(client_name)
791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813

    # 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"]

814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839
#
# 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):
B
Brandon Casey 已提交
840 841
    m = re.search("[*#@%]", path)
    return m is not None
842

843 844 845
class Command:
    def __init__(self):
        self.usage = "usage: %prog [options]"
846
        self.needsGit = True
847
        self.verbose = False
848

849 850 851
class P4UserMap:
    def __init__(self):
        self.userMapFromPerforceServer = False
852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871
        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
872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909

    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()

910
class P4Debug(Command):
911
    def __init__(self):
912
        Command.__init__(self)
913
        self.options = []
914
        self.description = "A tool to debug the output of p4 -G."
915
        self.needsGit = False
916 917

    def run(self, args):
918
        j = 0
919
        for output in p4CmdList(args):
920 921
            print 'Element: %d' % j
            j += 1
922
            print output
923
        return True
924

925 926 927 928
class P4RollBack(Command):
    def __init__(self):
        Command.__init__(self)
        self.options = [
929
            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
930 931
        ]
        self.description = "A tool to debug the multi-branch import. Don't use :)"
932
        self.rollbackLocalBranches = False
933 934 935 936 937

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

939
        if "p4ExitCode" in p4Cmd("changes -m 1"):
940 941
            die("Problems executing p4");

942 943
        if self.rollbackLocalBranches:
            refPrefix = "refs/heads/"
H
Han-Wen Nienhuys 已提交
944
            lines = read_pipe_lines("git rev-parse --symbolic --branches")
945 946
        else:
            refPrefix = "refs/remotes/"
H
Han-Wen Nienhuys 已提交
947
            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
948 949 950

        for line in lines:
            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
951 952
                line = line.strip()
                ref = refPrefix + line
953
                log = extractLogMessageFromGitCommit(ref)
H
Han-Wen Nienhuys 已提交
954 955 956 957 958
                settings = extractSettingsGitLog(log)

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

959
                changed = False
960

961 962
                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
                                                           for p in depotPaths]))) == 0:
963 964 965 966
                    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 已提交
967
                while change and int(change) > maxChange:
968
                    changed = True
969 970
                    if self.verbose:
                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
971 972
                    system("git update-ref %s \"%s^\"" % (ref, ref))
                    log = extractLogMessageFromGitCommit(ref)
H
Han-Wen Nienhuys 已提交
973 974 975 976 977
                    settings =  extractSettingsGitLog(log)


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

                if changed:
980
                    print "%s rewound to %s" % (ref, change)
981 982 983

        return True

984
class P4Submit(Command, P4UserMap):
985 986 987

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

988
    def __init__(self):
989
        Command.__init__(self)
990
        P4UserMap.__init__(self)
991 992
        self.options = [
                optparse.make_option("--origin", dest="origin"),
993
                optparse.make_option("-M", dest="detectRenames", action="store_true"),
994 995
                # preserve the user, requires relevant p4 permissions
                optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
996
                optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
997
                optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
998
                optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
999
                optparse.make_option("--conflict", dest="conflict_behavior",
1000 1001
                                     choices=self.conflict_behavior_choices),
                optparse.make_option("--branch", dest="branch"),
1002 1003
        ]
        self.description = "Submit changes from git to the perforce depot."
1004
        self.usage += " [name of git branch to submit into perforce depot]"
1005
        self.origin = ""
1006
        self.detectRenames = False
P
Pete Wyckoff 已提交
1007
        self.preserveUser = gitConfigBool("git-p4.preserveUser")
1008
        self.dry_run = False
1009
        self.prepare_p4_only = False
1010
        self.conflict_behavior = None
1011
        self.isWindows = (platform.system() == "Windows")
1012
        self.exportLabels = False
1013
        self.p4HasMoveCommand = p4_has_move_command()
1014
        self.branch = None
1015 1016 1017 1018 1019

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

1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047
    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."""
1048 1049
        result = ""

1050 1051
        inDescriptionSection = False

1052 1053 1054 1055 1056
        for line in template.split("\n"):
            if line.startswith("#"):
                result += line + "\n"
                continue

1057
            if inDescriptionSection:
1058
                if line.startswith("Files:") or line.startswith("Jobs:"):
1059
                    inDescriptionSection = False
1060 1061 1062
                    # insert Jobs section
                    if jobs:
                        result += jobs + "\n"
1063 1064 1065 1066 1067 1068 1069 1070 1071 1072
                else:
                    continue
            else:
                if line.startswith("Description:"):
                    inDescriptionSection = True
                    line += "\n"
                    for messageLine in message.split("\n"):
                        line += "\t" + messageLine + "\n"

            result += line + "\n"
1073 1074 1075

        return result

1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098
    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

1099 1100 1101
    def p4UserForCommit(self,id):
        # Return the tuple (perforce user,git email) for a given git commit id
        self.getUserMapFromPerforceServer()
1102 1103
        gitEmail = read_pipe(["git", "log", "--max-count=1",
                              "--format=%ae", id])
1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115
        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 已提交
1116
                if gitConfigBool("git-p4.allowMissingP4Users"):
1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133
                    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")
1134
        results = p4CmdList(["changes", "-c", client, "-m", "1"])
1135 1136 1137 1138 1139 1140 1141 1142
        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)
1143 1144 1145 1146 1147 1148 1149 1150 1151
        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)

1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164
        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.
1165
        results = p4CmdList(["protects", self.depotPath])
1166 1167 1168 1169 1170 1171 1172 1173
        for r in results:
            if r.has_key('perm'):
                if r['perm'] == 'admin':
                    return 1
                if r['perm'] == 'super':
                    return 1
        return 0

1174
    def prepareSubmitTemplate(self):
1175 1176 1177 1178 1179 1180 1181
        """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."""

1182 1183
        template = ""
        inFilesSection = False
1184
        for line in p4_read_pipe_lines(['change', '-o']):
1185 1186
            if line.endswith("\r\n"):
                line = line[:-2] + "\n"
1187 1188 1189 1190 1191 1192 1193
            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]
1194
                        if not p4PathStartsWith(path, self.depotPath):
1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205
                            continue
                else:
                    inFilesSection = False
            else:
                if line.startswith("Files:"):
                    inFilesSection = True

            template += line

        return template

P
Pete Wyckoff 已提交
1206 1207 1208 1209 1210
    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 已提交
1211
        if gitConfigBool("git-p4.skipSubmitEdit"):
P
Pete Wyckoff 已提交
1212 1213 1214 1215 1216 1217 1218
            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
1219
        if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
P
Pete Wyckoff 已提交
1220 1221 1222
            editor = os.environ.get("P4EDITOR")
        else:
            editor = read_pipe("git var GIT_EDITOR").strip()
P
Pete Wyckoff 已提交
1223
        system([editor, template_file])
P
Pete Wyckoff 已提交
1224 1225 1226

        # 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 已提交
1227
        if gitConfigBool("git-p4.skipSubmitEditCheck"):
P
Pete Wyckoff 已提交
1228 1229
            return True

1230 1231 1232 1233 1234 1235 1236 1237 1238 1239
        # 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 已提交
1240

1241
    def get_diff_description(self, editedFiles, filesToAdd):
1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260
        # diff
        if os.environ.has_key("P4DIFF"):
            del(os.environ["P4DIFF"])
        diff = ""
        for editedFile in editedFiles:
            diff += p4_read_pipe(['diff', '-du',
                                  wildcard_encode(editedFile)])

        # new file diff
        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()

1261
        return (diff + newdiff).replace('\r\n', '\n')
1262

1263
    def applyCommit(self, id):
1264 1265 1266 1267
        """Apply one commit, return True if it succeeded."""

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

1269
        (p4User, gitEmail) = self.p4UserForCommit(id)
1270

1271
        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1272 1273
        filesToAdd = set()
        filesToDelete = set()
1274
        editedFiles = set()
1275
        pureRenameCopy = set()
1276
        filesToChangeExecBit = {}
1277

1278
        for line in diff:
1279 1280 1281
            diff = parseDiffTreeEntry(line)
            modifier = diff['status']
            path = diff['src']
1282
            if modifier == "M":
1283
                p4_edit(path)
1284 1285
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
                    filesToChangeExecBit[path] = diff['dst_mode']
1286
                editedFiles.add(path)
1287 1288
            elif modifier == "A":
                filesToAdd.add(path)
1289
                filesToChangeExecBit[path] = diff['dst_mode']
1290 1291 1292 1293 1294 1295
                if path in filesToDelete:
                    filesToDelete.remove(path)
            elif modifier == "D":
                filesToDelete.add(path)
                if path in filesToAdd:
                    filesToAdd.remove(path)
1296 1297
            elif modifier == "C":
                src, dest = diff['src'], diff['dst']
1298
                p4_integrate(src, dest)
1299
                pureRenameCopy.add(dest)
1300
                if diff['src_sha1'] != diff['dst_sha1']:
1301
                    p4_edit(dest)
1302
                    pureRenameCopy.discard(dest)
1303
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1304
                    p4_edit(dest)
1305
                    pureRenameCopy.discard(dest)
1306
                    filesToChangeExecBit[dest] = diff['dst_mode']
1307 1308 1309
                if self.isWindows:
                    # turn off read-only attribute
                    os.chmod(dest, stat.S_IWRITE)
1310 1311
                os.unlink(dest)
                editedFiles.add(dest)
1312
            elif modifier == "R":
1313
                src, dest = diff['src'], diff['dst']
1314 1315 1316
                if self.p4HasMoveCommand:
                    p4_edit(src)        # src must be open before move
                    p4_move(src, dest)  # opens for (move/delete, move/add)
1317
                else:
1318 1319 1320 1321 1322
                    p4_integrate(src, dest)
                    if diff['src_sha1'] != diff['dst_sha1']:
                        p4_edit(dest)
                    else:
                        pureRenameCopy.add(dest)
1323
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1324 1325
                    if not self.p4HasMoveCommand:
                        p4_edit(dest)   # with move: already open, writable
1326
                    filesToChangeExecBit[dest] = diff['dst_mode']
1327
                if not self.p4HasMoveCommand:
1328 1329
                    if self.isWindows:
                        os.chmod(dest, stat.S_IWRITE)
1330 1331
                    os.unlink(dest)
                    filesToDelete.add(src)
1332
                editedFiles.add(dest)
1333 1334 1335
            else:
                die("unknown modifier %s for %s" % (modifier, path))

1336
        diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1337
        patchcmd = diffcmd + " | git apply "
1338 1339
        tryPatchCmd = patchcmd + "--check -"
        applyPatchCmd = patchcmd + "--check --apply -"
1340
        patch_succeeded = True
1341

1342
        if os.system(tryPatchCmd) != 0:
1343 1344
            fixed_rcs_keywords = False
            patch_succeeded = False
1345
            print "Unfortunately applying the change failed!"
1346 1347 1348

            # Patch failed, maybe it's just RCS keyword woes. Look through
            # the patch to see if that's possible.
P
Pete Wyckoff 已提交
1349
            if gitConfigBool("git-p4.attemptRCSCleanup"):
1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369
                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)
1370 1371 1372 1373
                    # 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)
1374 1375 1376 1377 1378 1379 1380 1381 1382
                    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:
1383 1384 1385
            for f in editedFiles:
                p4_revert(f)
            return False
1386

1387 1388 1389
        #
        # Apply the patch for real, and do add/delete/+x handling.
        #
1390
        system(applyPatchCmd)
1391 1392

        for f in filesToAdd:
1393
            p4_add(f)
1394
        for f in filesToDelete:
1395 1396
            p4_revert(f)
            p4_delete(f)
1397

1398 1399 1400 1401 1402
        # Set/clear executable bits
        for f in filesToChangeExecBit.keys():
            mode = filesToChangeExecBit[f]
            setP4ExecBit(f, mode)

1403 1404 1405 1406
        #
        # Build p4 change description, starting with the contents
        # of the git commit message.
        #
1407 1408
        logMessage = extractLogMessageFromGitCommit(id)
        logMessage = logMessage.strip()
1409
        (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1410

1411
        template = self.prepareSubmitTemplate()
1412
        submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1413

1414
        if self.preserveUser:
1415
           submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1416

1417 1418 1419 1420
        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"
1421

1422
        separatorLine = "######## everything below this line is just the diff #######\n"
1423 1424
        if not self.prepare_p4_only:
            submitTemplate += separatorLine
1425
            submitTemplate += self.get_diff_description(editedFiles, filesToAdd)
1426

1427
        (handle, fileName) = tempfile.mkstemp()
1428
        tmpFile = os.fdopen(handle, "w+b")
1429 1430
        if self.isWindows:
            submitTemplate = submitTemplate.replace("\n", "\r\n")
1431
        tmpFile.write(submitTemplate)
1432 1433
        tmpFile.close()

1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444
        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,"
1445
            print "or \"p4 submit -i <%s\" to use the one prepared by" \
1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468
                  " \"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

1469 1470 1471
        #
        # Let the user edit the change description, then submit it.
        #
1472 1473
        if self.edit_template(fileName):
            # read the edited message and submit
1474
            ret = True
1475 1476
            tmpFile = open(fileName, "rb")
            message = tmpFile.read()
1477
            tmpFile.close()
1478
            if self.isWindows:
1479 1480
                message = message.replace("\r\n", "\n")
            submitTemplate = message[:message.index(separatorLine)]
1481
            p4_write_pipe(['submit', '-i'], submitTemplate)
1482

1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494
            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")
1495

1496
        else:
1497
            # skip this patch
1498
            ret = False
1499 1500 1501 1502 1503 1504
            print "Submission cancelled, undoing p4 changes."
            for f in editedFiles:
                p4_revert(f)
            for f in filesToAdd:
                p4_revert(f)
                os.remove(f)
1505 1506
            for f in filesToDelete:
                p4_revert(f)
1507 1508

        os.remove(fileName)
1509
        return ret
1510

1511 1512 1513
    # Export git tags as p4 labels. Create a p4 label and then tag
    # with that.
    def exportGitTags(self, gitTags):
1514 1515 1516 1517
        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
        if len(validLabelRegexp) == 0:
            validLabelRegexp = defaultLabelRegexp
        m = re.compile(validLabelRegexp)
1518 1519 1520 1521 1522

        for name in gitTags:

            if not m.match(name):
                if verbose:
1523
                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1524 1525 1526
                continue

            # Get the p4 commit this corresponds to
1527 1528
            logMessage = extractLogMessageFromGitCommit(name)
            values = extractSettingsGitLog(logMessage)
1529

1530
            if not values.has_key('change'):
1531 1532 1533 1534
                # a tag pointing to something not sent to p4; ignore
                if verbose:
                    print "git tag %s does not give a p4 commit" % name
                continue
1535 1536
            else:
                changelist = values['change']
1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563

            # 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"
1564 1565
            for depot_side in clientSpec.mappings:
                labelTemplate += "\t%s\n" % depot_side
1566

1567 1568
            if self.dry_run:
                print "Would create p4 label %s for tag" % name
1569 1570 1571
            elif self.prepare_p4_only:
                print "Not creating p4 label %s for tag due to option" \
                      " --prepare-p4-only" % name
1572 1573
            else:
                p4_write_pipe(["label", "-i"], labelTemplate)
1574

1575 1576
                # Use the label
                p4_system(["tag", "-l", name] +
1577
                          ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1578

1579 1580
                if verbose:
                    print "created p4 label for tag %s" % name
1581

1582
    def run(self, args):
1583 1584
        if len(args) == 0:
            self.master = currentGitBranch()
1585
            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1586 1587 1588
                die("Detecting current git branch failed!")
        elif len(args) == 1:
            self.master = args[0]
1589 1590
            if not branchExists(self.master):
                die("Branch %s does not exist" % self.master)
1591 1592 1593
        else:
            return False

J
Jing Xue 已提交
1594 1595 1596 1597
        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)

1598
        [upstream, settings] = findUpstreamBranchPoint()
1599
        self.depotPath = settings['depot-paths'][0]
1600 1601
        if len(self.origin) == 0:
            self.origin = upstream
1602

1603 1604 1605 1606
        if self.preserveUser:
            if not self.canChangeChangelists():
                die("Cannot preserve user names without p4 super-user or admin permissions")

1607 1608 1609 1610 1611 1612 1613 1614 1615 1616
        # 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

1617 1618
        if self.verbose:
            print "Origin branch is " + self.origin
1619

1620
        if len(self.depotPath) == 0:
1621 1622 1623
            print "Internal error: cannot locate perforce depot path from existing branches"
            sys.exit(128)

1624
        self.useClientSpec = False
P
Pete Wyckoff 已提交
1625
        if gitConfigBool("git-p4.useclientspec"):
1626 1627 1628
            self.useClientSpec = True
        if self.useClientSpec:
            self.clientSpecDirs = getClientSpec()
1629

1630 1631 1632 1633 1634
        if self.useClientSpec:
            # all files are relative to the client spec
            self.clientPath = getClientRoot()
        else:
            self.clientPath = p4Where(self.depotPath)
1635

1636 1637
        if self.clientPath == "":
            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1638

1639
        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1640
        self.oldWorkingDirectory = os.getcwd()
1641

1642
        # ensure the clientPath exists
1643
        new_client_dir = False
1644
        if not os.path.exists(self.clientPath):
1645
            new_client_dir = True
1646 1647
            os.makedirs(self.clientPath)

1648
        chdir(self.clientPath, is_client_path=True)
1649 1650
        if self.dry_run:
            print "Would synchronize p4 checkout in %s" % self.clientPath
1651
        else:
1652 1653 1654 1655 1656 1657
            print "Synchronizing p4 checkout..."
            if new_client_dir:
                # old one was destroyed, and maybe nobody told p4
                p4_sync("...", "-f")
            else:
                p4_sync("...")
1658 1659
        self.check()

S
Simon Hausmann 已提交
1660
        commits = []
1661
        for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, self.master)]):
S
Simon Hausmann 已提交
1662 1663
            commits.append(line.strip())
        commits.reverse()
1664

P
Pete Wyckoff 已提交
1665
        if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1666 1667 1668 1669
            self.checkAuthorship = False
        else:
            self.checkAuthorship = True

1670 1671 1672
        if self.preserveUser:
            self.checkValidP4Users(commits)

1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700
        #
        # 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 已提交
1701
        if gitConfigBool("git-p4.detectCopiesHarder"):
1702 1703
            self.diffOpts += " --find-copies-harder"

1704 1705 1706 1707
        #
        # Apply the commits, one at a time.  On failure, ask if should
        # continue to try the rest of the patches, or quit.
        #
1708 1709
        if self.dry_run:
            print "Would apply"
1710
        applied = []
1711 1712
        last = len(commits) - 1
        for i, commit in enumerate(commits):
1713 1714 1715 1716 1717 1718
            if self.dry_run:
                print " ", read_pipe(["git", "show", "-s",
                                      "--format=format:%h %s", commit])
                ok = True
            else:
                ok = self.applyCommit(commit)
1719 1720
            if ok:
                applied.append(commit)
1721
            else:
1722 1723 1724 1725
                if self.prepare_p4_only and i < last:
                    print "Processing only the first commit due to option" \
                          " --prepare-p4-only"
                    break
1726 1727 1728
                if i < last:
                    quit = False
                    while True:
1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743
                        # 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)

1744 1745 1746 1747 1748 1749 1750 1751 1752
                        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
1753

1754
        chdir(self.oldWorkingDirectory)
1755

1756 1757
        if self.dry_run:
            pass
1758 1759
        elif self.prepare_p4_only:
            pass
1760
        elif len(commits) == len(applied):
1761
            print "All commits applied!"
1762

S
Simon Hausmann 已提交
1763
            sync = P4Sync()
1764 1765
            if self.branch:
                sync.branch = self.branch
S
Simon Hausmann 已提交
1766
            sync.run([])
1767

S
Simon Hausmann 已提交
1768 1769
            rebase = P4Rebase()
            rebase.rebase()
1770

1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784
        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 已提交
1785
        if gitConfigBool("git-p4.exportLabels"):
1786
            self.exportLabels = True
1787 1788 1789 1790 1791 1792 1793 1794

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

            missingGitTags = gitTags - p4Labels
            self.exportGitTags(missingGitTags)

O
Ondřej Bílka 已提交
1795
        # exit with error unless everything applied perfectly
1796 1797 1798
        if len(commits) != len(applied):
                sys.exit(1)

1799 1800
        return True

P
Pete Wyckoff 已提交
1801 1802 1803 1804
class View(object):
    """Represent a p4 view ("p4 help views"), and map files in a
       repo according to the view."""

1805
    def __init__(self, client_name):
P
Pete Wyckoff 已提交
1806
        self.mappings = []
1807 1808 1809
        self.client_prefix = "//%s/" % client_name
        # cache results of "p4 where" to lookup client file locations
        self.client_spec_path_cache = {}
P
Pete Wyckoff 已提交
1810 1811 1812

    def append(self, view_line):
        """Parse a view line, splitting it into depot and client
1813 1814
           sides.  Append to self.mappings, preserving order.  This
           is only needed for tag creation."""
P
Pete Wyckoff 已提交
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

        # 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

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

1846
        # prefix - means exclude this path, leave out of mappings
P
Pete Wyckoff 已提交
1847 1848 1849 1850 1851
        exclude = False
        if depot_side.startswith("-"):
            exclude = True
            depot_side = depot_side[1:]

1852 1853
        if not exclude:
            self.mappings.append(depot_side)
P
Pete Wyckoff 已提交
1854

1855 1856 1857 1858 1859 1860
    def convert_client_path(self, clientFile):
        # chop off //client/ part to make it relative
        if not clientFile.startswith(self.client_prefix):
            die("No prefix '%s' on clientFile '%s'" %
                (self.client_prefix, clientFile))
        return clientFile[len(self.client_prefix):]
P
Pete Wyckoff 已提交
1861

1862 1863
    def update_client_spec_path_cache(self, files):
        """ Caching file paths by "p4 where" batch query """
P
Pete Wyckoff 已提交
1864

1865 1866
        # List depot file paths exclude that already cached
        fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
P
Pete Wyckoff 已提交
1867

1868 1869
        if len(fileArgs) == 0:
            return  # All files in cache
P
Pete Wyckoff 已提交
1870

1871 1872 1873 1874 1875 1876
        where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
        for res in where_result:
            if "code" in res and res["code"] == "error":
                # assume error is "... file(s) not in client view"
                continue
            if "clientFile" not in res:
1877
                die("No clientFile in 'p4 where' output")
1878 1879 1880 1881
            if "unmap" in res:
                # it will list all of them, but only one not unmap-ped
                continue
            self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
P
Pete Wyckoff 已提交
1882

1883 1884 1885 1886
        # not found files or unmap files set to ""
        for depotFile in fileArgs:
            if depotFile not in self.client_spec_path_cache:
                self.client_spec_path_cache[depotFile] = ""
P
Pete Wyckoff 已提交
1887

1888 1889 1890 1891
    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."""
P
Pete Wyckoff 已提交
1892

1893 1894 1895 1896 1897
        if depot_path in self.client_spec_path_cache:
            return self.client_spec_path_cache[depot_path]

        die( "Error: %s is not found in client spec path" % depot_path )
        return ""
P
Pete Wyckoff 已提交
1898

1899
class P4Sync(Command, P4UserMap):
1900 1901
    delete_actions = ( "delete", "move/delete", "purge" )

1902 1903
    def __init__(self):
        Command.__init__(self)
1904
        P4UserMap.__init__(self)
1905 1906 1907 1908 1909
        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"),
1910
                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1911
                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1912 1913
                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
                                     help="Import into refs/heads/ , not refs/remotes"),
1914
                optparse.make_option("--max-changes", dest="maxChanges"),
1915
                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1916 1917 1918
                                     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")
1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929
        ]
        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
1930 1931
        self.createdBranches = set()
        self.committedChanges = set()
1932
        self.branch = ""
1933
        self.detectBranches = False
1934
        self.detectLabels = False
1935
        self.importLabels = False
1936
        self.changesFile = ""
1937
        self.syncWithOrigin = True
1938
        self.importIntoRemotes = True
1939
        self.maxChanges = ""
1940
        self.keepRepoPath = False
1941
        self.depotPaths = None
1942
        self.p4BranchesInGit = []
T
Tommy Thorn 已提交
1943
        self.cloneExclude = []
1944
        self.useClientSpec = False
1945
        self.useClientSpec_from_options = False
P
Pete Wyckoff 已提交
1946
        self.clientSpecDirs = None
1947 1948
        self.tempBranches = []
        self.tempBranchLocation = "git-p4-tmp"
1949

1950 1951 1952
        if gitConfig("git-p4.syncFromOrigin") == "false":
            self.syncWithOrigin = False

1953 1954 1955 1956 1957 1958 1959 1960
    # 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

1961
    def extractFilesFromCommit(self, commit):
T
Tommy Thorn 已提交
1962 1963
        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
                             for path in self.cloneExclude]
1964 1965 1966 1967
        files = []
        fnum = 0
        while commit.has_key("depotFile%s" % fnum):
            path =  commit["depotFile%s" % fnum]
1968

T
Tommy Thorn 已提交
1969
            if [p for p in self.cloneExclude
1970
                if p4PathStartsWith(path, p)]:
T
Tommy Thorn 已提交
1971 1972 1973
                found = False
            else:
                found = [p for p in self.depotPaths
1974
                         if p4PathStartsWith(path, p)]
1975
            if not found:
1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987
                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

1988
    def stripRepoPath(self, path, prefixes):
1989 1990 1991 1992 1993
        """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."""

1994
        if self.useClientSpec:
1995 1996
            # branch detection moves files up a level (the branch name)
            # from what client spec interpretation gives
1997
            path = self.clientSpecDirs.map_in_client(path)
1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009
            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):]
2010

2011 2012 2013 2014
        else:
            for p in prefixes:
                if p4PathStartsWith(path, p):
                    path = path[len(p):]
2015
                    break
2016

2017
        path = wildcard_decode(path)
2018
        return path
H
Han-Wen Nienhuys 已提交
2019

2020
    def splitFilesIntoBranches(self, commit):
2021 2022 2023
        """Look at each depotFile in the commit to figure out to what
           branch it belongs."""

2024 2025 2026 2027
        if self.clientSpecDirs:
            files = self.extractFilesFromCommit(commit)
            self.clientSpecDirs.update_client_spec_path_cache(files)

2028
        branches = {}
2029 2030 2031
        fnum = 0
        while commit.has_key("depotFile%s" % fnum):
            path =  commit["depotFile%s" % fnum]
2032
            found = [p for p in self.depotPaths
2033
                     if p4PathStartsWith(path, p)]
2034
            if not found:
2035 2036 2037 2038 2039 2040 2041 2042 2043 2044
                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

2045 2046 2047 2048 2049 2050
            # 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)
2051

2052
            for branch in self.knownBranches.keys():
2053 2054
                # 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 已提交
2055
                if relPath.startswith(branch + "/"):
2056 2057
                    if branch not in branches:
                        branches[branch] = []
2058
                    branches[branch].append(file)
2059
                    break
2060 2061 2062

        return branches

2063 2064 2065 2066 2067 2068 2069 2070
    # 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 已提交
2071 2072 2073 2074 2075 2076 2077
        (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"
2078 2079
            # p4 print on a symlink sometimes contains "target\n";
            # if it does, remove the newline
2080
            data = ''.join(contents)
2081 2082 2083 2084 2085 2086 2087 2088
            if not data:
                # Some version of p4 allowed creating a symlink that pointed
                # to nothing.  This causes p4 errors when checking out such
                # a change, and errors here too.  Work around it by ignoring
                # the bad symlink; hopefully a future change fixes it.
                print "\nIgnoring empty symlink in %s" % file['depotFile']
                return
            elif data[-1] == '\n':
2089 2090 2091
                contents = [data[:-1]]
            else:
                contents = [data]
2092

P
Pete Wyckoff 已提交
2093
        if type_base == "utf16":
2094 2095 2096 2097 2098
            # 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.
2099 2100 2101 2102 2103
            #
            # 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.
            #
2104
            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
2105 2106
            if p4_version_string().find("/NT") >= 0:
                text = text.replace("\r\n", "\n")
2107 2108
            contents = [ text ]

P
Pete Wyckoff 已提交
2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121
        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

2122 2123
        # Note that we do not try to de-mangle keywords on utf16 files,
        # even though in theory somebody may want that.
2124 2125 2126 2127 2128 2129
        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 ]
2130

P
Pete Wyckoff 已提交
2131
        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151

        # 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):

2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174
        # 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)

2175 2176 2177 2178 2179 2180
        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
2181

2182 2183 2184 2185 2186 2187 2188
        # 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]
2189

2190
        self.stream_have_file_info = True
2191 2192 2193

    # Stream directly from "p4 files" into "git fast-import"
    def streamP4Files(self, files):
2194 2195
        filesForCommit = []
        filesToRead = []
2196
        filesToDelete = []
2197

2198
        for f in files:
P
Pete Wyckoff 已提交
2199 2200 2201 2202 2203
            # 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
2204

P
Pete Wyckoff 已提交
2205 2206 2207 2208 2209
            filesForCommit.append(f)
            if f['action'] in self.delete_actions:
                filesToDelete.append(f)
            else:
                filesToRead.append(f)
H
Han-Wen Nienhuys 已提交
2210

2211 2212 2213
        # deleted files...
        for f in filesToDelete:
            self.streamOneP4Deletion(f)
2214

2215 2216 2217 2218
        if len(filesToRead) > 0:
            self.stream_file = {}
            self.stream_contents = []
            self.stream_have_file_info = False
2219

2220 2221 2222
            # curry self argument
            def streamP4FilesCbSelf(entry):
                self.streamP4FilesCb(entry)
H
Han-Wen Nienhuys 已提交
2223

2224 2225 2226 2227 2228
            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]

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

2230 2231 2232
            # do the last chunk
            if self.stream_file.has_key('depotFile'):
                self.streamOneP4File(self.stream_file, self.stream_contents)
H
Han-Wen Nienhuys 已提交
2233

2234 2235 2236 2237 2238 2239
    def make_email(self, userid):
        if userid in self.users:
            return self.users[userid]
        else:
            return "%s <a@b>" % userid

2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271
    # 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")

2272
    def commit(self, details, files, branch, parent = ""):
2273 2274 2275
        epoch = details["time"]
        author = details["user"]

2276 2277 2278
        if self.verbose:
            print "commit into %s" % branch

2279 2280 2281 2282
        # start with reading files; if that fails, we should not
        # create a commit.
        new_files = []
        for f in files:
2283
            if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2284 2285
                new_files.append (f)
            else:
2286
                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2287

2288 2289 2290
        if self.clientSpecDirs:
            self.clientSpecDirs.update_client_spec_path_cache(files)

2291
        self.gitStream.write("commit %s\n" % branch)
H
Han-Wen Nienhuys 已提交
2292
#        gitStream.write("mark :%s\n" % details["change"])
2293 2294
        self.committedChanges.add(int(details["change"]))
        committer = ""
2295 2296
        if author not in self.users:
            self.getUserMapFromPerforceServer()
2297
        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2298 2299 2300 2301 2302

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

        self.gitStream.write("data <<EOT\n")
        self.gitStream.write(details["desc"])
2303 2304
        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
                             (','.join(self.branchPrefixes), details["change"]))
2305 2306 2307
        if len(details['options']) > 0:
            self.gitStream.write(": options = %s" % details['options'])
        self.gitStream.write("]\nEOT\n\n")
2308 2309

        if len(parent) > 0:
2310 2311
            if self.verbose:
                print "parent %s" % parent
2312 2313
            self.gitStream.write("from %s\n" % parent)

2314
        self.streamP4Files(new_files)
2315 2316
        self.gitStream.write("\n")

2317 2318
        change = int(details["change"])

2319
        if self.labels.has_key(change):
2320 2321 2322
            label = self.labels[change]
            labelDetails = label[0]
            labelRevisions = label[1]
2323 2324
            if self.verbose:
                print "Change %s is labelled %s" % (change, labelDetails)
2325

2326
            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2327
                                                for p in self.branchPrefixes])
2328 2329 2330 2331 2332

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

                cleanedFiles = {}
                for info in files:
2333
                    if info["action"] in self.delete_actions:
2334 2335 2336 2337
                        continue
                    cleanedFiles[info["depotFile"]] = info["rev"]

                if cleanedFiles == labelRevisions:
2338
                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2339 2340

                else:
2341
                    if not self.silent:
2342 2343
                        print ("Tag %s does not match with change %s: files do not match."
                               % (labelDetails["label"], change))
2344 2345

            else:
2346
                if not self.silent:
2347 2348
                    print ("Tag %s does not match with change %s: file count is different."
                           % (labelDetails["label"], change))
2349

2350
    # Build a dictionary of changelists and labels, for "detect-labels" option.
2351 2352 2353
    def getLabels(self):
        self.labels = {}

2354
        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
S
Simon Hausmann 已提交
2355
        if len(l) > 0 and not self.silent:
2356
            print "Finding files belonging to labels in %s" % `self.depotPaths`
S
Simon Hausmann 已提交
2357 2358

        for output in l:
2359 2360 2361
            label = output["label"]
            revisions = {}
            newestChange = 0
2362 2363
            if self.verbose:
                print "Querying files for label %s" % label
2364 2365 2366
            for file in p4CmdList(["files"] +
                                      ["%s...@%s" % (p, label)
                                          for p in self.depotPaths]):
2367 2368 2369 2370 2371
                revisions[file["depotFile"]] = file["rev"]
                change = int(file["change"])
                if change > newestChange:
                    newestChange = change

2372 2373 2374 2375
            self.labels[newestChange] = [output, revisions]

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

2377 2378 2379 2380 2381 2382 2383 2384 2385
    # 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")
2386
        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421
        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:
2422
                        print "Could not convert label time %s" % labelDetails['Update']
2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439
                        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])

2440 2441
    def guessProjectName(self):
        for p in self.depotPaths:
S
Simon Hausmann 已提交
2442 2443 2444 2445 2446 2447
            if p.endswith("/"):
                p = p[:-1]
            p = p[p.strip().rfind("/") + 1:]
            if not p.endswith("/"):
               p += "/"
            return p
2448

2449
    def getBranchMapping(self):
2450 2451
        lostAndFoundBranches = set()

2452 2453 2454 2455 2456 2457 2458
        user = gitConfig("git-p4.branchUser")
        if len(user) > 0:
            command = "branches -u %s" % user
        else:
            command = "branches"

        for info in p4CmdList(command):
2459
            details = p4Cmd(["branch", "-o", info["branch"]])
2460 2461 2462 2463 2464 2465 2466 2467 2468
            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]
2469
                ## HACK
2470
                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2471 2472
                    source = source[len(self.depotPaths[0]):-4]
                    destination = destination[len(self.depotPaths[0]):-4]
2473

2474 2475 2476 2477 2478 2479
                    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

2480 2481 2482 2483
                    self.knownBranches[destination] = source

                    lostAndFoundBranches.discard(destination)

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

2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505
        # 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)

2506 2507 2508

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

2510 2511 2512 2513 2514 2515 2516 2517 2518
    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 已提交
2519 2520 2521 2522 2523 2524 2525 2526 2527 2528
    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']))
2529

2530 2531 2532 2533 2534 2535 2536 2537 2538
    def gitRefForBranch(self, branch):
        if branch == "main":
            return self.refPrefix + "master"

        if len(branch) <= 0:
            return branch

        return self.refPrefix + self.projectName + branch

2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589
    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

2590
        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2591 2592 2593 2594 2595 2596 2597 2598 2599
        #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

2600 2601
    def searchParent(self, parent, branch, target):
        parentFound = False
2602 2603
        for blob in read_pipe_lines(["git", "rev-list", "--reverse",
                                     "--no-merges", parent]):
2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614
            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

2615 2616 2617
    def importChanges(self, changes):
        cnt = 1
        for change in changes:
P
Pete Wyckoff 已提交
2618
            description = p4_describe(change)
2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631
            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 + "/"
2632
                        self.branchPrefixes = [ branchPrefix ]
2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647

                        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 = ""
2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660
                            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
2661

2662 2663
                        branch = self.gitRefForBranch(branch)
                        parent = self.gitRefForBranch(parent)
2664 2665 2666 2667 2668 2669 2670 2671

                        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]

2672 2673
                        blob = None
                        if len(parent) > 0:
2674
                            tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2675 2676
                            if self.verbose:
                                print "Creating temporary branch: " + tempBranch
2677
                            self.commit(description, filesForCommit, tempBranch)
2678 2679 2680 2681
                            self.tempBranches.append(tempBranch)
                            self.checkpoint()
                            blob = self.searchParent(parent, branch, tempBranch)
                        if blob:
2682
                            self.commit(description, filesForCommit, branch, blob)
2683 2684 2685
                        else:
                            if self.verbose:
                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2686
                            self.commit(description, filesForCommit, branch, parent)
2687 2688
                else:
                    files = self.extractFilesFromCommit(description)
2689
                    self.commit(description, files, self.branch,
2690
                                self.initialParent)
2691
                    # only needed once, to connect to the previous commit
2692 2693 2694 2695 2696
                    self.initialParent = ""
            except IOError:
                print self.gitError.read()
                sys.exit(1)

2697 2698 2699
    def importHeadRevision(self, revision):
        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)

2700 2701
        details = {}
        details["user"] = "git perforce import user"
2702
        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2703 2704 2705 2706 2707
                           % (' '.join(self.depotPaths), revision))
        details["change"] = revision
        newestRevision = 0

        fileCnt = 0
2708 2709 2710
        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]

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

2712
            if 'code' in info and info['code'] == 'error':
2713 2714
                sys.stderr.write("p4 returned an error: %s\n"
                                 % info['data'])
2715 2716 2717 2718
                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))
2719
                sys.exit(1)
2720 2721
            if 'p4ExitCode' in info:
                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2722 2723 2724 2725 2726 2727 2728
                sys.exit(1)


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

2729
            if info["action"] in self.delete_actions:
2730 2731 2732 2733 2734 2735 2736 2737 2738 2739
                # 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
2740

P
Pete Wyckoff 已提交
2741
        # Use time from top-most change so that all git p4 clones of
2742
        # the same p4 repo have the same commit SHA1s.
P
Pete Wyckoff 已提交
2743 2744
        res = p4_describe(newestRevision)
        details["time"] = res["time"]
2745

2746 2747
        self.updateOptionDict(details)
        try:
2748
            self.commit(details, self.extractFilesFromCommit(details), self.branch)
2749 2750 2751 2752 2753
        except IOError:
            print "IO error with git fast-import. Is your git version recent enough?"
            print self.gitError.read()


2754
    def run(self, args):
2755
        self.depotPaths = []
2756
        self.changeRange = ""
2757
        self.previousDepotPaths = []
2758
        self.hasOrigin = False
H
Han-Wen Nienhuys 已提交
2759

2760 2761 2762 2763
        # map from branch depot path to parent branch
        self.knownBranches = {}
        self.initialParents = {}

2764 2765 2766
        if self.importIntoRemotes:
            self.refPrefix = "refs/remotes/p4/"
        else:
2767
            self.refPrefix = "refs/heads/p4/"
2768

2769 2770 2771 2772 2773 2774
        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")
2775

2776
        branch_arg_given = bool(self.branch)
2777
        if len(self.branch) == 0:
2778
            self.branch = self.refPrefix + "master"
2779
            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2780
                system("git update-ref %s refs/heads/p4" % self.branch)
2781
                system("git branch -D p4")
2782

2783 2784 2785 2786 2787
        # 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 已提交
2788
            if gitConfigBool("git-p4.useclientspec"):
2789 2790
                self.useClientSpec = True
        if self.useClientSpec:
2791
            self.clientSpecDirs = getClientSpec()
2792

H
Han-Wen Nienhuys 已提交
2793 2794 2795
        # TODO: should always look at previous commits,
        # merge with previous imports, if possible.
        if args == []:
2796
            if self.hasOrigin:
2797
                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2798 2799 2800

            # branches holds mapping from branch name to sha1
            branches = p4BranchesInGit(self.importIntoRemotes)
2801 2802 2803 2804 2805 2806 2807 2808

            # 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()
2809 2810 2811 2812 2813

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

2818 2819 2820 2821 2822
            if self.verbose:
                print "branches: %s" % self.p4BranchesInGit

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

                settings = extractSettingsGitLog(logMsg)
2826

H
Han-Wen Nienhuys 已提交
2827 2828 2829 2830
                self.readOptions(settings)
                if (settings.has_key('depot-paths')
                    and settings.has_key ('change')):
                    change = int(settings['change']) + 1
2831 2832
                    p4Change = max(p4Change, change)

H
Han-Wen Nienhuys 已提交
2833 2834
                    depotPaths = sorted(settings['depot-paths'])
                    if self.previousDepotPaths == []:
2835
                        self.previousDepotPaths = depotPaths
2836
                    else:
2837 2838
                        paths = []
                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2839 2840 2841 2842
                            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]:
2843
                                    i = i - 1
2844 2845
                                    break

2846
                            paths.append ("/".join(cur_list[:i + 1]))
2847 2848

                        self.previousDepotPaths = paths
2849 2850

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

2856 2857 2858 2859
        # 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/
2860
        if not self.branch.startswith("refs/"):
2861 2862 2863 2864 2865 2866 2867
            if self.importIntoRemotes:
                prepend = "refs/remotes/"
            else:
                prepend = "refs/heads/"
            if not self.branch.startswith("p4/"):
                prepend += "p4/"
            self.branch = prepend + self.branch
2868

2869
        if len(args) == 0 and self.depotPaths:
2870
            if not self.silent:
2871
                print "Depot paths: %s" % ' '.join(self.depotPaths)
2872
        else:
2873
            if self.depotPaths and self.depotPaths != args:
2874
                print ("previous import used depot path %s and now %s was specified. "
2875 2876
                       "This doesn't work!" % (' '.join (self.depotPaths),
                                               ' '.join (args)))
2877
                sys.exit(1)
2878

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

2881
        revision = ""
2882 2883
        self.users = {}

2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894
        # 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")

2895 2896 2897 2898 2899 2900 2901
        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 已提交
2902
                elif ',' not in self.changeRange:
2903
                    revision = self.changeRange
2904
                    self.changeRange = ""
2905
                p = p[:atIdx]
2906 2907
            elif p.find("#") != -1:
                hashIdx = p.index("#")
2908
                revision = p[hashIdx:]
2909
                p = p[:hashIdx]
2910
            elif self.previousDepotPaths == []:
2911 2912 2913 2914
                # pay attention to changesfile, if given, else import
                # the entire p4 tree at the head revision
                if len(self.changesFile) == 0:
                    revision = "#head"
2915 2916 2917 2918 2919 2920 2921 2922 2923

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

            newPaths.append(p)

        self.depotPaths = newPaths

2924 2925 2926
        # --detect-branches may change this for each branch
        self.branchPrefixes = self.depotPaths

2927
        self.loadUserMapFromCache()
2928 2929 2930
        self.labels = {}
        if self.detectLabels:
            self.getLabels();
2931

2932
        if self.detectBranches:
2933 2934 2935
            ## FIXME - what's a P4 projectName ?
            self.projectName = self.guessProjectName()

2936 2937 2938 2939
            if self.hasOrigin:
                self.getBranchMappingFromGitBranches()
            else:
                self.getBranchMapping()
2940 2941 2942 2943 2944
            if self.verbose:
                print "p4-git branches: %s" % self.p4BranchesInGit
                print "initial parents: %s" % self.initialParents
            for b in self.p4BranchesInGit:
                if b != "master":
2945 2946

                    ## FIXME
2947 2948
                    b = b[len(self.projectName):]
                self.createdBranches.add(b)
2949

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

2952 2953 2954 2955 2956 2957 2958
        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
2959

2960
        if revision:
2961
            self.importHeadRevision(revision)
2962 2963 2964
        else:
            changes = []

2965
            if len(self.changesFile) > 0:
2966
                output = open(self.changesFile).readlines()
2967
                changeSet = set()
2968 2969 2970 2971 2972 2973 2974 2975
                for line in output:
                    changeSet.add(int(line))

                for change in changeSet:
                    changes.append(change)

                changes.sort()
            else:
P
Pete Wyckoff 已提交
2976 2977
                # catch "git p4 sync" with no new branches, in a repo that
                # does not have any existing p4 branches
2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992
                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)

2993
                if self.verbose:
2994
                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2995
                                                              self.changeRange)
2996
                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2997

2998
                if len(self.maxChanges) > 0:
2999
                    changes = changes[:min(int(self.maxChanges), len(changes))]
3000

3001
            if len(changes) == 0:
3002
                if not self.silent:
3003
                    print "No changes to import!"
3004 3005 3006 3007 3008
            else:
                if not self.silent and not self.detectBranches:
                    print "Import destination: %s" % self.branch

                self.updatedBranches = set()
3009

3010 3011 3012 3013 3014 3015 3016 3017
                if not self.detectBranches:
                    if args:
                        # start a new branch
                        self.initialParent = ""
                    else:
                        # build on a previous revision
                        self.initialParent = parseRevision(self.branch)

3018
                self.importChanges(changes)
3019

3020 3021 3022 3023 3024 3025 3026
                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")
3027

P
Pete Wyckoff 已提交
3028
        if gitConfigBool("git-p4.importLabels"):
3029
            self.importLabels = True
3030

3031 3032 3033 3034 3035 3036
        if self.importLabels:
            p4Labels = getP4Labels(self.depotPaths)
            gitTags = getGitTags()

            missingP4Labels = p4Labels - gitTags
            self.importP4Labels(self.gitStream, missingP4Labels)
3037 3038

        self.gitStream.close()
3039
        if self.importProcess.wait() != 0:
3040
            die("fast-import failed: %s" % self.gitError.read())
3041 3042 3043
        self.gitOutput.close()
        self.gitError.close()

3044 3045 3046 3047 3048 3049
        # 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))

3050 3051 3052 3053 3054 3055 3056
        # 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])

3057 3058
        return True

S
Simon Hausmann 已提交
3059 3060 3061
class P4Rebase(Command):
    def __init__(self):
        Command.__init__(self)
3062 3063 3064 3065
        self.options = [
                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
        ]
        self.importLabels = False
3066 3067
        self.description = ("Fetches the latest revision from perforce and "
                            + "rebases the current work (branch) against it")
S
Simon Hausmann 已提交
3068 3069 3070

    def run(self, args):
        sync = P4Sync()
3071
        sync.importLabels = self.importLabels
S
Simon Hausmann 已提交
3072
        sync.run([])
3073

3074 3075 3076
        return self.rebase()

    def rebase(self):
3077 3078 3079
        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:
3080
            die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3081

3082 3083 3084 3085 3086 3087 3088 3089
        [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
3090
        oldHead = read_pipe("git rev-parse HEAD").strip()
3091
        system("git rebase %s" % upstream)
3092
        system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
S
Simon Hausmann 已提交
3093 3094
        return True

3095 3096 3097 3098
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 已提交
3099
        self.usage = "usage: %prog [options] //depot/path[@revRange]"
T
Tommy Thorn 已提交
3100
        self.options += [
H
Han-Wen Nienhuys 已提交
3101 3102
            optparse.make_option("--destination", dest="cloneDestination",
                                 action='store', default=None,
T
Tommy Thorn 已提交
3103 3104 3105
                                 help="where to leave result of the clone"),
            optparse.make_option("-/", dest="cloneExclude",
                                 action="append", type="string",
P
Pete Wyckoff 已提交
3106 3107 3108
                                 help="exclude depot path"),
            optparse.make_option("--bare", dest="cloneBare",
                                 action="store_true", default=False),
T
Tommy Thorn 已提交
3109
        ]
H
Han-Wen Nienhuys 已提交
3110
        self.cloneDestination = None
3111
        self.needsGit = False
P
Pete Wyckoff 已提交
3112
        self.cloneBare = False
3113

T
Tommy Thorn 已提交
3114 3115 3116 3117 3118 3119
    # 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 已提交
3120 3121 3122 3123 3124
    def defaultDestination(self, args):
        ## TODO: use common prefix of args?
        depotPath = args[0]
        depotDir = re.sub("(@[^@]*)$", "", depotPath)
        depotDir = re.sub("(#[^#]*)$", "", depotDir)
3125
        depotDir = re.sub(r"\.\.\.$", "", depotDir)
H
Han-Wen Nienhuys 已提交
3126 3127 3128
        depotDir = re.sub(r"/$", "", depotDir)
        return os.path.split(depotDir)[1]

3129 3130 3131
    def run(self, args):
        if len(args) < 1:
            return False
H
Han-Wen Nienhuys 已提交
3132 3133 3134 3135

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

3137
        depotPaths = args
3138 3139 3140 3141 3142

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

T
Tommy Thorn 已提交
3143
        self.cloneExclude = ["/"+p for p in self.cloneExclude]
3144 3145
        for p in depotPaths:
            if not p.startswith("//"):
3146
                sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3147
                return False
3148

H
Han-Wen Nienhuys 已提交
3149
        if not self.cloneDestination:
3150
            self.cloneDestination = self.defaultDestination(args)
3151

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

3154 3155
        if not os.path.exists(self.cloneDestination):
            os.makedirs(self.cloneDestination)
3156
        chdir(self.cloneDestination)
P
Pete Wyckoff 已提交
3157 3158 3159 3160

        init_cmd = [ "git", "init" ]
        if self.cloneBare:
            init_cmd.append("--bare")
B
Brandon Casey 已提交
3161 3162 3163
        retcode = subprocess.call(init_cmd)
        if retcode:
            raise CalledProcessError(retcode, init_cmd)
P
Pete Wyckoff 已提交
3164

3165
        if not P4Sync.run(self, depotPaths):
3166
            return False
3167 3168 3169 3170 3171 3172 3173 3174 3175

        # 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>"'
3176

3177 3178 3179 3180
        # auto-set this variable if invoked with --use-client-spec
        if self.useClientSpec_from_options:
            system("git config --bool git-p4.useclientspec true")

3181 3182
        return True

3183 3184 3185 3186 3187 3188 3189 3190 3191
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):
3192 3193 3194
        if originP4BranchesExist():
            createOrUpdateBranchesFromOrigin()

3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210
        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

3211 3212 3213 3214 3215 3216 3217 3218 3219
class HelpFormatter(optparse.IndentedHelpFormatter):
    def __init__(self):
        optparse.IndentedHelpFormatter.__init__(self)

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

3221 3222 3223 3224 3225 3226 3227 3228 3229
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 已提交
3230 3231
    "debug" : P4Debug,
    "submit" : P4Submit,
3232
    "commit" : P4Submit,
H
Han-Wen Nienhuys 已提交
3233 3234 3235
    "sync" : P4Sync,
    "rebase" : P4Rebase,
    "clone" : P4Clone,
3236 3237
    "rollback" : P4RollBack,
    "branches" : P4Branches
3238 3239 3240
}


H
Han-Wen Nienhuys 已提交
3241 3242 3243 3244
def main():
    if len(sys.argv[1:]) == 0:
        printUsage(commands.keys())
        sys.exit(2)
3245

H
Han-Wen Nienhuys 已提交
3246 3247
    cmdName = sys.argv[1]
    try:
H
Han-Wen Nienhuys 已提交
3248 3249
        klass = commands[cmdName]
        cmd = klass()
H
Han-Wen Nienhuys 已提交
3250 3251 3252 3253 3254 3255 3256
    except KeyError:
        print "unknown command %s" % cmdName
        print ""
        printUsage(commands.keys())
        sys.exit(2)

    options = cmd.options
H
Han-Wen Nienhuys 已提交
3257
    cmd.gitdir = os.environ.get("GIT_DIR", None)
H
Han-Wen Nienhuys 已提交
3258 3259 3260

    args = sys.argv[2:]

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

3265 3266 3267 3268
    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
                                   options,
                                   description = cmd.description,
                                   formatter = HelpFormatter())
H
Han-Wen Nienhuys 已提交
3269

3270
    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
H
Han-Wen Nienhuys 已提交
3271 3272 3273
    global verbose
    verbose = cmd.verbose
    if cmd.needsGit:
H
Han-Wen Nienhuys 已提交
3274 3275 3276 3277 3278
        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 已提交
3279 3280
                    cdup = read_pipe("git rev-parse --show-cdup").strip()
                    if len(cdup) > 0:
3281
                        chdir(cdup);
3282

H
Han-Wen Nienhuys 已提交
3283 3284 3285
        if not isValidGitDir(cmd.gitdir):
            if isValidGitDir(cmd.gitdir + "/.git"):
                cmd.gitdir += "/.git"
H
Han-Wen Nienhuys 已提交
3286
            else:
H
Han-Wen Nienhuys 已提交
3287
                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3288

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

H
Han-Wen Nienhuys 已提交
3291 3292
    if not cmd.run(args):
        parser.print_help()
3293
        sys.exit(2)
3294 3295


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