git-p4.py 122.9 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
# Grab changes in blocks of this many revisions, unless otherwise requested
defaultBlockSize = 512

49 50 51 52 53 54 55
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.
    """
56
    real_cmd = ["p4"]
57 58 59

    user = gitConfig("git-p4.user")
    if len(user) > 0:
60
        real_cmd += ["-u",user]
61 62 63

    password = gitConfig("git-p4.password")
    if len(password) > 0:
64
        real_cmd += ["-P", password]
65 66 67

    port = gitConfig("git-p4.port")
    if len(port) > 0:
68
        real_cmd += ["-p", port]
69 70 71

    host = gitConfig("git-p4.host")
    if len(host) > 0:
72
        real_cmd += ["-H", host]
73 74 75

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

78 79 80 81 82

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

85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
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
106

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

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

118 119 120 121 122 123 124
    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 已提交
125 126 127

    return val

128
def p4_write_pipe(c, stdin):
129
    real_cmd = p4_build_cmd(c)
130
    return write_pipe(real_cmd, stdin)
131

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

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

    return val

145 146 147
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 已提交
148

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

    return val
161

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

167 168 169 170 171 172 173 174 175
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

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

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

211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
_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

227
def p4_integrate(src, dest):
228
    p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
229

230
def p4_sync(f, *options):
231
    p4_system(["sync"] + list(options) + [wildcard_encode(f)])
232 233

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

def p4_delete(f):
241
    p4_system(["delete", wildcard_encode(f)])
242 243

def p4_edit(f):
244
    p4_system(["edit", wildcard_encode(f)])
245 246

def p4_revert(f):
247
    p4_system(["revert", wildcard_encode(f)])
248

249 250
def p4_reopen(type, f):
    p4_system(["reopen", "-t", type, wildcard_encode(f)])
251

252 253 254
def p4_move(src, dest):
    p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])

255 256 257 258
def p4_last_change():
    results = p4CmdList(["changes", "-m", "1"])
    return int(results[0]['change'])

P
Pete Wyckoff 已提交
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
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 已提交
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 309 310 311 312 313 314 315
#
# 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 已提交
316

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

359 360 361 362 363 364 365 366 367 368 369 370 371
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]

372
    p4_reopen(p4Type, file)
373 374 375 376

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

377
    result = p4_read_pipe(["opened", wildcard_encode(file)])
378
    match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
379 380 381
    if match:
        return match.group(1)
    else:
382
        die("Could not determine file type for %s (result: '%s')" % (file, result))
383

384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
# 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

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

444 445 446 447 448 449 450 451
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)

452
def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
453 454 455 456 457 458 459 460 461

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

    cmd = p4_build_cmd(cmd)
H
Han-Wen Nienhuys 已提交
462
    if verbose:
463
        sys.stderr.write("Opening pipe: %s\n" % str(cmd))
S
Scott Lamb 已提交
464 465 466 467 468 469 470

    # 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)
471 472 473 474 475
        if isinstance(stdin,basestring):
            stdin_file.write(stdin)
        else:
            for i in stdin:
                stdin_file.write(i + '\n')
S
Scott Lamb 已提交
476 477 478
        stdin_file.flush()
        stdin_file.seek(0)

479 480
    p4 = subprocess.Popen(cmd,
                          shell=expand,
S
Scott Lamb 已提交
481 482
                          stdin=stdin_file,
                          stdout=subprocess.PIPE)
483 484 485 486

    result = []
    try:
        while True:
S
Scott Lamb 已提交
487
            entry = marshal.load(p4.stdout)
488 489 490 491
            if cb is not None:
                cb(entry)
            else:
                result.append(entry)
492 493
    except EOFError:
        pass
S
Scott Lamb 已提交
494 495
    exitCode = p4.wait()
    if exitCode != 0:
496 497 498
        entry = {}
        entry["p4ExitCode"] = exitCode
        result.append(entry)
499 500 501 502 503 504 505 506 507 508

    return result

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

509 510 511
def p4Where(depotPath):
    if not depotPath.endswith("/"):
        depotPath += "/"
512 513
    depotPathLong = depotPath + "..."
    outputList = p4CmdList(["where", depotPathLong])
514 515
    output = None
    for entry in outputList:
516
        if "depotFile" in entry:
517 518 519
            # Search for the base client side depot path, as long as it starts with the branch's P4 path.
            # The base path always ends with "/...".
            if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
520 521 522 523 524 525 526 527
                output = entry
                break
        elif "data" in entry:
            data = entry.get("data")
            space = data.find(" ")
            if data[:space] == depotPath:
                output = entry
                break
528 529
    if output == None:
        return ""
530 531
    if output["code"] == "error":
        return ""
532 533 534 535 536 537 538 539 540 541 542 543
    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

544
def currentGitBranch():
545
    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
546

547
def isValidGitDir(path):
H
Han-Wen Nienhuys 已提交
548 549
    if (os.path.exists(path + "/HEAD")
        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
550 551 552
        return True;
    return False

553
def parseRevision(ref):
554
    return read_pipe("git rev-parse %s" % ref).strip()
555

556 557 558 559 560
def branchExists(ref):
    rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
                     ignore_error=True)
    return len(rev) > 0

561 562
def extractLogMessageFromGitCommit(commit):
    logMessage = ""
H
Han-Wen Nienhuys 已提交
563 564

    ## fixme: title is first line of commit, not 1st paragraph.
565
    foundTitle = False
H
Han-Wen Nienhuys 已提交
566
    for log in read_pipe_lines("git cat-file commit %s" % commit):
567 568
       if not foundTitle:
           if len(log) == 1:
S
Simon Hausmann 已提交
569
               foundTitle = True
570 571 572 573 574
           continue

       logMessage += log
    return logMessage

H
Han-Wen Nienhuys 已提交
575
def extractSettingsGitLog(log):
576 577 578
    values = {}
    for line in log.split("\n"):
        line = line.strip()
579 580 581 582 583 584 585 586 587 588 589 590 591 592
        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

593 594 595
    paths = values.get("depot-paths")
    if not paths:
        paths = values.get("depot-path")
596 597
    if paths:
        values['depot-paths'] = paths.split(',')
H
Han-Wen Nienhuys 已提交
598
    return values
599

600
def gitBranchExists(branch):
H
Han-Wen Nienhuys 已提交
601 602
    proc = subprocess.Popen(["git", "rev-parse", branch],
                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
603
    return proc.wait() == 0;
604

605
_gitConfig = {}
606

P
Pete Wyckoff 已提交
607
def gitConfig(key):
608
    if not _gitConfig.has_key(key):
P
Pete Wyckoff 已提交
609
        cmd = [ "git", "config", key ]
610 611
        s = read_pipe(cmd, ignore_error=True)
        _gitConfig[key] = s.strip()
612
    return _gitConfig[key]
613

P
Pete Wyckoff 已提交
614 615 616 617 618
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."""

619
    if not _gitConfig.has_key(key):
P
Pete Wyckoff 已提交
620 621 622 623
        cmd = [ "git", "config", "--bool", key ]
        s = read_pipe(cmd, ignore_error=True)
        v = s.strip()
        _gitConfig[key] = v == "true"
624
    return _gitConfig[key]
625

626 627
def gitConfigList(key):
    if not _gitConfig.has_key(key):
628 629
        s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
        _gitConfig[key] = s.strip().split(os.linesep)
630 631
    return _gitConfig[key]

632 633 634 635 636 637 638
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."""

639 640 641 642
    branches = {}

    cmdline = "git rev-parse --symbolic "
    if branchesAreInRemotes:
643
        cmdline += "--remotes"
644
    else:
645
        cmdline += "--branches"
646 647 648 649

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

650 651 652 653 654
        # only import to p4/
        if not line.startswith('p4/'):
            continue
        # special symbolic ref to p4/master
        if line == "p4/HEAD":
655 656
            continue

657 658
        # strip off p4/ prefix
        branch = line[len("p4/"):]
659 660

        branches[branch] = parseRevision(line)
661

662 663
    return branches

664 665 666 667 668 669 670 671 672 673 674
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

675
def findUpstreamBranchPoint(head = "HEAD"):
676 677 678 679 680 681 682 683 684 685 686
    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

687 688 689
    settings = None
    parent = 0
    while parent < 65535:
690
        commit = head + "~%s" % parent
691 692
        log = extractLogMessageFromGitCommit(commit)
        settings = extractSettingsGitLog(log)
693 694 695 696
        if settings.has_key("depot-paths"):
            paths = ",".join(settings["depot-paths"])
            if branchByDepotPath.has_key(paths):
                return [branchByDepotPath[paths], settings]
697

698
        parent = parent + 1
699

700
    return ["", settings]
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 743 744 745 746 747 748 749 750 751
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")

752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768

def p4ParseNumericChangeRange(parts):
    changeStart = int(parts[0][1:])
    if parts[1] == '#head':
        changeEnd = p4_last_change()
    else:
        changeEnd = int(parts[1])

    return (changeStart, changeEnd)

def chooseBlockSize(blockSize):
    if blockSize:
        return blockSize
    else:
        return defaultBlockSize

def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
769
    assert depotPaths
770

771 772 773 774 775 776
    # Parse the change range into start and end. Try to find integer
    # revision ranges as these can be broken up into blocks to avoid
    # hitting server-side limits (maxrows, maxscanresults). But if
    # that doesn't work, fall back to using the raw revision specifier
    # strings, without using block mode.

777
    if changeRange is None or changeRange == '':
778 779 780
        changeStart = 1
        changeEnd = p4_last_change()
        block_size = chooseBlockSize(requestedBlockSize)
781 782 783
    else:
        parts = changeRange.split(',')
        assert len(parts) == 2
784 785 786 787 788 789 790 791 792
        try:
            (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
            block_size = chooseBlockSize(requestedBlockSize)
        except:
            changeStart = parts[0][1:]
            changeEnd = parts[1]
            if requestedBlockSize:
                die("cannot use --changes-block-size with non-numeric revisions")
            block_size = None
793

794
    # Accumulate change numbers in a dictionary to avoid duplicates
795
    changes = {}
796 797 798

    for p in depotPaths:
        # Retrieve changes a block at a time, to prevent running
799 800 801
        # into a MaxResults/MaxScanRows error from the server.

        while True:
802
            cmd = ['changes']
803 804 805 806 807 808 809 810 811

            if block_size:
                end = min(changeEnd, changeStart + block_size)
                revisionRange = "%d,%d" % (changeStart, end)
            else:
                revisionRange = "%s,%s" % (changeStart, changeEnd)

            cmd += ["%s...@%s" % (p, revisionRange)]

812 813 814
            for line in p4_read_pipe_lines(cmd):
                changeNum = int(line.split(" ")[1])
                changes[changeNum] = True
815 816 817 818 819 820 821 822

            if not block_size:
                break

            if end >= changeEnd:
                break

            changeStart = end + 1
823

824 825 826
    changelist = changes.keys()
    changelist.sort()
    return changelist
827

828 829 830 831 832 833 834 835
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 已提交
836
    if gitConfigBool("core.ignorecase"):
837 838 839
        return path.lower().startswith(prefix.lower())
    return path.startswith(prefix)

840 841 842 843 844 845 846 847 848 849 850 851
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]

852 853 854
    # the //client/ name
    client_name = entry["Client"]

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

    # hold this new View
859
    view = View(client_name)
860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882

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

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
#
# 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 已提交
909 910
    m = re.search("[*#@%]", path)
    return m is not None
911

912 913 914
class Command:
    def __init__(self):
        self.usage = "usage: %prog [options]"
915
        self.needsGit = True
916
        self.verbose = False
917

918 919 920
class P4UserMap:
    def __init__(self):
        self.userMapFromPerforceServer = False
921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940
        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
941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978

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

979
class P4Debug(Command):
980
    def __init__(self):
981
        Command.__init__(self)
982
        self.options = []
983
        self.description = "A tool to debug the output of p4 -G."
984
        self.needsGit = False
985 986

    def run(self, args):
987
        j = 0
988
        for output in p4CmdList(args):
989 990
            print 'Element: %d' % j
            j += 1
991
            print output
992
        return True
993

994 995 996 997
class P4RollBack(Command):
    def __init__(self):
        Command.__init__(self)
        self.options = [
998
            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
999 1000
        ]
        self.description = "A tool to debug the multi-branch import. Don't use :)"
1001
        self.rollbackLocalBranches = False
1002 1003 1004 1005 1006

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

1008
        if "p4ExitCode" in p4Cmd("changes -m 1"):
1009 1010
            die("Problems executing p4");

1011 1012
        if self.rollbackLocalBranches:
            refPrefix = "refs/heads/"
H
Han-Wen Nienhuys 已提交
1013
            lines = read_pipe_lines("git rev-parse --symbolic --branches")
1014 1015
        else:
            refPrefix = "refs/remotes/"
H
Han-Wen Nienhuys 已提交
1016
            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1017 1018 1019

        for line in lines:
            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1020 1021
                line = line.strip()
                ref = refPrefix + line
1022
                log = extractLogMessageFromGitCommit(ref)
H
Han-Wen Nienhuys 已提交
1023 1024 1025 1026 1027
                settings = extractSettingsGitLog(log)

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

1028
                changed = False
1029

1030 1031
                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
                                                           for p in depotPaths]))) == 0:
1032 1033 1034 1035
                    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 已提交
1036
                while change and int(change) > maxChange:
1037
                    changed = True
1038 1039
                    if self.verbose:
                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
1040 1041
                    system("git update-ref %s \"%s^\"" % (ref, ref))
                    log = extractLogMessageFromGitCommit(ref)
H
Han-Wen Nienhuys 已提交
1042 1043 1044 1045 1046
                    settings =  extractSettingsGitLog(log)


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

                if changed:
1049
                    print "%s rewound to %s" % (ref, change)
1050 1051 1052

        return True

1053
class P4Submit(Command, P4UserMap):
1054 1055 1056

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

1057
    def __init__(self):
1058
        Command.__init__(self)
1059
        P4UserMap.__init__(self)
1060 1061
        self.options = [
                optparse.make_option("--origin", dest="origin"),
1062
                optparse.make_option("-M", dest="detectRenames", action="store_true"),
1063 1064
                # preserve the user, requires relevant p4 permissions
                optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1065
                optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1066
                optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1067
                optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1068
                optparse.make_option("--conflict", dest="conflict_behavior",
1069 1070
                                     choices=self.conflict_behavior_choices),
                optparse.make_option("--branch", dest="branch"),
1071 1072
        ]
        self.description = "Submit changes from git to the perforce depot."
1073
        self.usage += " [name of git branch to submit into perforce depot]"
1074
        self.origin = ""
1075
        self.detectRenames = False
P
Pete Wyckoff 已提交
1076
        self.preserveUser = gitConfigBool("git-p4.preserveUser")
1077
        self.dry_run = False
1078
        self.prepare_p4_only = False
1079
        self.conflict_behavior = None
1080
        self.isWindows = (platform.system() == "Windows")
1081
        self.exportLabels = False
1082
        self.p4HasMoveCommand = p4_has_move_command()
1083
        self.branch = None
1084 1085 1086 1087 1088

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

1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116
    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."""
1117 1118
        result = ""

1119 1120
        inDescriptionSection = False

1121 1122 1123 1124 1125
        for line in template.split("\n"):
            if line.startswith("#"):
                result += line + "\n"
                continue

1126
            if inDescriptionSection:
1127
                if line.startswith("Files:") or line.startswith("Jobs:"):
1128
                    inDescriptionSection = False
1129 1130 1131
                    # insert Jobs section
                    if jobs:
                        result += jobs + "\n"
1132 1133 1134 1135 1136 1137 1138 1139 1140 1141
                else:
                    continue
            else:
                if line.startswith("Description:"):
                    inDescriptionSection = True
                    line += "\n"
                    for messageLine in message.split("\n"):
                        line += "\t" + messageLine + "\n"

            result += line + "\n"
1142 1143 1144

        return result

1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167
    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

1168 1169 1170
    def p4UserForCommit(self,id):
        # Return the tuple (perforce user,git email) for a given git commit id
        self.getUserMapFromPerforceServer()
1171 1172
        gitEmail = read_pipe(["git", "log", "--max-count=1",
                              "--format=%ae", id])
1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184
        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 已提交
1185
                if gitConfigBool("git-p4.allowMissingP4Users"):
1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202
                    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")
1203
        results = p4CmdList(["changes", "-c", client, "-m", "1"])
1204 1205 1206 1207 1208 1209 1210 1211
        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)
1212 1213 1214 1215 1216 1217 1218 1219 1220
        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)

1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233
        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.
1234
        results = p4CmdList(["protects", self.depotPath])
1235 1236 1237 1238 1239 1240 1241 1242
        for r in results:
            if r.has_key('perm'):
                if r['perm'] == 'admin':
                    return 1
                if r['perm'] == 'super':
                    return 1
        return 0

1243
    def prepareSubmitTemplate(self):
1244 1245 1246 1247 1248 1249 1250
        """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."""

1251 1252
        template = ""
        inFilesSection = False
1253
        for line in p4_read_pipe_lines(['change', '-o']):
1254 1255
            if line.endswith("\r\n"):
                line = line[:-2] + "\n"
1256 1257 1258 1259 1260 1261 1262
            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]
1263
                        if not p4PathStartsWith(path, self.depotPath):
1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274
                            continue
                else:
                    inFilesSection = False
            else:
                if line.startswith("Files:"):
                    inFilesSection = True

            template += line

        return template

P
Pete Wyckoff 已提交
1275 1276 1277 1278 1279
    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 已提交
1280
        if gitConfigBool("git-p4.skipSubmitEdit"):
P
Pete Wyckoff 已提交
1281 1282 1283 1284 1285 1286 1287
            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
1288
        if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
P
Pete Wyckoff 已提交
1289 1290 1291
            editor = os.environ.get("P4EDITOR")
        else:
            editor = read_pipe("git var GIT_EDITOR").strip()
1292
        system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
P
Pete Wyckoff 已提交
1293 1294 1295

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

1299 1300 1301 1302 1303 1304 1305 1306 1307 1308
        # 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 已提交
1309

1310
    def get_diff_description(self, editedFiles, filesToAdd):
1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329
        # 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()

1330
        return (diff + newdiff).replace('\r\n', '\n')
1331

1332
    def applyCommit(self, id):
1333 1334 1335 1336
        """Apply one commit, return True if it succeeded."""

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

1338
        (p4User, gitEmail) = self.p4UserForCommit(id)
1339

1340
        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1341 1342
        filesToAdd = set()
        filesToDelete = set()
1343
        editedFiles = set()
1344
        pureRenameCopy = set()
1345
        filesToChangeExecBit = {}
1346

1347
        for line in diff:
1348 1349 1350
            diff = parseDiffTreeEntry(line)
            modifier = diff['status']
            path = diff['src']
1351
            if modifier == "M":
1352
                p4_edit(path)
1353 1354
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
                    filesToChangeExecBit[path] = diff['dst_mode']
1355
                editedFiles.add(path)
1356 1357
            elif modifier == "A":
                filesToAdd.add(path)
1358
                filesToChangeExecBit[path] = diff['dst_mode']
1359 1360 1361 1362 1363 1364
                if path in filesToDelete:
                    filesToDelete.remove(path)
            elif modifier == "D":
                filesToDelete.add(path)
                if path in filesToAdd:
                    filesToAdd.remove(path)
1365 1366
            elif modifier == "C":
                src, dest = diff['src'], diff['dst']
1367
                p4_integrate(src, dest)
1368
                pureRenameCopy.add(dest)
1369
                if diff['src_sha1'] != diff['dst_sha1']:
1370
                    p4_edit(dest)
1371
                    pureRenameCopy.discard(dest)
1372
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1373
                    p4_edit(dest)
1374
                    pureRenameCopy.discard(dest)
1375
                    filesToChangeExecBit[dest] = diff['dst_mode']
1376 1377 1378
                if self.isWindows:
                    # turn off read-only attribute
                    os.chmod(dest, stat.S_IWRITE)
1379 1380
                os.unlink(dest)
                editedFiles.add(dest)
1381
            elif modifier == "R":
1382
                src, dest = diff['src'], diff['dst']
1383 1384 1385
                if self.p4HasMoveCommand:
                    p4_edit(src)        # src must be open before move
                    p4_move(src, dest)  # opens for (move/delete, move/add)
1386
                else:
1387 1388 1389 1390 1391
                    p4_integrate(src, dest)
                    if diff['src_sha1'] != diff['dst_sha1']:
                        p4_edit(dest)
                    else:
                        pureRenameCopy.add(dest)
1392
                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1393 1394
                    if not self.p4HasMoveCommand:
                        p4_edit(dest)   # with move: already open, writable
1395
                    filesToChangeExecBit[dest] = diff['dst_mode']
1396
                if not self.p4HasMoveCommand:
1397 1398
                    if self.isWindows:
                        os.chmod(dest, stat.S_IWRITE)
1399 1400
                    os.unlink(dest)
                    filesToDelete.add(src)
1401
                editedFiles.add(dest)
1402 1403 1404
            else:
                die("unknown modifier %s for %s" % (modifier, path))

1405
        diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1406
        patchcmd = diffcmd + " | git apply "
1407 1408
        tryPatchCmd = patchcmd + "--check -"
        applyPatchCmd = patchcmd + "--check --apply -"
1409
        patch_succeeded = True
1410

1411
        if os.system(tryPatchCmd) != 0:
1412 1413
            fixed_rcs_keywords = False
            patch_succeeded = False
1414
            print "Unfortunately applying the change failed!"
1415 1416 1417

            # Patch failed, maybe it's just RCS keyword woes. Look through
            # the patch to see if that's possible.
P
Pete Wyckoff 已提交
1418
            if gitConfigBool("git-p4.attemptRCSCleanup"):
1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438
                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)
1439 1440 1441 1442
                    # 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)
1443 1444 1445 1446 1447 1448 1449 1450 1451
                    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:
1452 1453 1454
            for f in editedFiles:
                p4_revert(f)
            return False
1455

1456 1457 1458
        #
        # Apply the patch for real, and do add/delete/+x handling.
        #
1459
        system(applyPatchCmd)
1460 1461

        for f in filesToAdd:
1462
            p4_add(f)
1463
        for f in filesToDelete:
1464 1465
            p4_revert(f)
            p4_delete(f)
1466

1467 1468 1469 1470 1471
        # Set/clear executable bits
        for f in filesToChangeExecBit.keys():
            mode = filesToChangeExecBit[f]
            setP4ExecBit(f, mode)

1472 1473 1474 1475
        #
        # Build p4 change description, starting with the contents
        # of the git commit message.
        #
1476 1477
        logMessage = extractLogMessageFromGitCommit(id)
        logMessage = logMessage.strip()
1478
        (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1479

1480
        template = self.prepareSubmitTemplate()
1481
        submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1482

1483
        if self.preserveUser:
1484
           submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1485

1486 1487 1488 1489
        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"
1490

1491
        separatorLine = "######## everything below this line is just the diff #######\n"
1492 1493
        if not self.prepare_p4_only:
            submitTemplate += separatorLine
1494
            submitTemplate += self.get_diff_description(editedFiles, filesToAdd)
1495

1496
        (handle, fileName) = tempfile.mkstemp()
1497
        tmpFile = os.fdopen(handle, "w+b")
1498 1499
        if self.isWindows:
            submitTemplate = submitTemplate.replace("\n", "\r\n")
1500
        tmpFile.write(submitTemplate)
1501 1502
        tmpFile.close()

1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513
        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,"
1514
            print "or \"p4 submit -i <%s\" to use the one prepared by" \
1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537
                  " \"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

1538 1539 1540
        #
        # Let the user edit the change description, then submit it.
        #
1541 1542
        if self.edit_template(fileName):
            # read the edited message and submit
1543
            ret = True
1544 1545
            tmpFile = open(fileName, "rb")
            message = tmpFile.read()
1546
            tmpFile.close()
1547
            if self.isWindows:
1548 1549
                message = message.replace("\r\n", "\n")
            submitTemplate = message[:message.index(separatorLine)]
1550
            p4_write_pipe(['submit', '-i'], submitTemplate)
1551

1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563
            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")
1564

1565
        else:
1566
            # skip this patch
1567
            ret = False
1568 1569 1570 1571 1572 1573
            print "Submission cancelled, undoing p4 changes."
            for f in editedFiles:
                p4_revert(f)
            for f in filesToAdd:
                p4_revert(f)
                os.remove(f)
1574 1575
            for f in filesToDelete:
                p4_revert(f)
1576 1577

        os.remove(fileName)
1578
        return ret
1579

1580 1581 1582
    # Export git tags as p4 labels. Create a p4 label and then tag
    # with that.
    def exportGitTags(self, gitTags):
1583 1584 1585 1586
        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
        if len(validLabelRegexp) == 0:
            validLabelRegexp = defaultLabelRegexp
        m = re.compile(validLabelRegexp)
1587 1588 1589 1590 1591

        for name in gitTags:

            if not m.match(name):
                if verbose:
1592
                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1593 1594 1595
                continue

            # Get the p4 commit this corresponds to
1596 1597
            logMessage = extractLogMessageFromGitCommit(name)
            values = extractSettingsGitLog(logMessage)
1598

1599
            if not values.has_key('change'):
1600 1601 1602 1603
                # a tag pointing to something not sent to p4; ignore
                if verbose:
                    print "git tag %s does not give a p4 commit" % name
                continue
1604 1605
            else:
                changelist = values['change']
1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632

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

1636 1637
            if self.dry_run:
                print "Would create p4 label %s for tag" % name
1638 1639 1640
            elif self.prepare_p4_only:
                print "Not creating p4 label %s for tag due to option" \
                      " --prepare-p4-only" % name
1641 1642
            else:
                p4_write_pipe(["label", "-i"], labelTemplate)
1643

1644 1645
                # Use the label
                p4_system(["tag", "-l", name] +
1646
                          ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1647

1648 1649
                if verbose:
                    print "created p4 label for tag %s" % name
1650

1651
    def run(self, args):
1652 1653
        if len(args) == 0:
            self.master = currentGitBranch()
1654
            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1655 1656 1657
                die("Detecting current git branch failed!")
        elif len(args) == 1:
            self.master = args[0]
1658 1659
            if not branchExists(self.master):
                die("Branch %s does not exist" % self.master)
1660 1661 1662
        else:
            return False

J
Jing Xue 已提交
1663 1664 1665 1666
        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)

1667
        [upstream, settings] = findUpstreamBranchPoint()
1668
        self.depotPath = settings['depot-paths'][0]
1669 1670
        if len(self.origin) == 0:
            self.origin = upstream
1671

1672 1673 1674 1675
        if self.preserveUser:
            if not self.canChangeChangelists():
                die("Cannot preserve user names without p4 super-user or admin permissions")

1676 1677 1678 1679 1680 1681 1682 1683 1684 1685
        # 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

1686 1687
        if self.verbose:
            print "Origin branch is " + self.origin
1688

1689
        if len(self.depotPath) == 0:
1690 1691 1692
            print "Internal error: cannot locate perforce depot path from existing branches"
            sys.exit(128)

1693
        self.useClientSpec = False
P
Pete Wyckoff 已提交
1694
        if gitConfigBool("git-p4.useclientspec"):
1695 1696 1697
            self.useClientSpec = True
        if self.useClientSpec:
            self.clientSpecDirs = getClientSpec()
1698

1699 1700 1701 1702
        # Check for the existance of P4 branches
        branchesDetected = (len(p4BranchesInGit().keys()) > 1)

        if self.useClientSpec and not branchesDetected:
1703 1704 1705 1706
            # all files are relative to the client spec
            self.clientPath = getClientRoot()
        else:
            self.clientPath = p4Where(self.depotPath)
1707

1708 1709
        if self.clientPath == "":
            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1710

1711
        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1712
        self.oldWorkingDirectory = os.getcwd()
1713

1714
        # ensure the clientPath exists
1715
        new_client_dir = False
1716
        if not os.path.exists(self.clientPath):
1717
            new_client_dir = True
1718 1719
            os.makedirs(self.clientPath)

1720
        chdir(self.clientPath, is_client_path=True)
1721 1722
        if self.dry_run:
            print "Would synchronize p4 checkout in %s" % self.clientPath
1723
        else:
1724 1725 1726 1727 1728 1729
            print "Synchronizing p4 checkout..."
            if new_client_dir:
                # old one was destroyed, and maybe nobody told p4
                p4_sync("...", "-f")
            else:
                p4_sync("...")
1730 1731
        self.check()

S
Simon Hausmann 已提交
1732
        commits = []
1733
        for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, self.master)]):
S
Simon Hausmann 已提交
1734 1735
            commits.append(line.strip())
        commits.reverse()
1736

P
Pete Wyckoff 已提交
1737
        if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1738 1739 1740 1741
            self.checkAuthorship = False
        else:
            self.checkAuthorship = True

1742 1743 1744
        if self.preserveUser:
            self.checkValidP4Users(commits)

1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772
        #
        # 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 已提交
1773
        if gitConfigBool("git-p4.detectCopiesHarder"):
1774 1775
            self.diffOpts += " --find-copies-harder"

1776 1777 1778 1779
        #
        # Apply the commits, one at a time.  On failure, ask if should
        # continue to try the rest of the patches, or quit.
        #
1780 1781
        if self.dry_run:
            print "Would apply"
1782
        applied = []
1783 1784
        last = len(commits) - 1
        for i, commit in enumerate(commits):
1785 1786 1787 1788 1789 1790
            if self.dry_run:
                print " ", read_pipe(["git", "show", "-s",
                                      "--format=format:%h %s", commit])
                ok = True
            else:
                ok = self.applyCommit(commit)
1791 1792
            if ok:
                applied.append(commit)
1793
            else:
1794 1795 1796 1797
                if self.prepare_p4_only and i < last:
                    print "Processing only the first commit due to option" \
                          " --prepare-p4-only"
                    break
1798 1799 1800
                if i < last:
                    quit = False
                    while True:
1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815
                        # 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)

1816 1817 1818 1819 1820 1821 1822 1823 1824
                        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
1825

1826
        chdir(self.oldWorkingDirectory)
1827

1828 1829
        if self.dry_run:
            pass
1830 1831
        elif self.prepare_p4_only:
            pass
1832
        elif len(commits) == len(applied):
1833
            print "All commits applied!"
1834

S
Simon Hausmann 已提交
1835
            sync = P4Sync()
1836 1837
            if self.branch:
                sync.branch = self.branch
S
Simon Hausmann 已提交
1838
            sync.run([])
1839

S
Simon Hausmann 已提交
1840 1841
            rebase = P4Rebase()
            rebase.rebase()
1842

1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856
        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 已提交
1857
        if gitConfigBool("git-p4.exportLabels"):
1858
            self.exportLabels = True
1859 1860 1861 1862 1863 1864 1865 1866

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

            missingGitTags = gitTags - p4Labels
            self.exportGitTags(missingGitTags)

O
Ondřej Bílka 已提交
1867
        # exit with error unless everything applied perfectly
1868 1869 1870
        if len(commits) != len(applied):
                sys.exit(1)

1871 1872
        return True

P
Pete Wyckoff 已提交
1873 1874 1875 1876
class View(object):
    """Represent a p4 view ("p4 help views"), and map files in a
       repo according to the view."""

1877
    def __init__(self, client_name):
P
Pete Wyckoff 已提交
1878
        self.mappings = []
1879 1880 1881
        self.client_prefix = "//%s/" % client_name
        # cache results of "p4 where" to lookup client file locations
        self.client_spec_path_cache = {}
P
Pete Wyckoff 已提交
1882 1883 1884

    def append(self, view_line):
        """Parse a view line, splitting it into depot and client
1885 1886
           sides.  Append to self.mappings, preserving order.  This
           is only needed for tag creation."""
P
Pete Wyckoff 已提交
1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917

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

1918
        # prefix - means exclude this path, leave out of mappings
P
Pete Wyckoff 已提交
1919 1920 1921 1922 1923
        exclude = False
        if depot_side.startswith("-"):
            exclude = True
            depot_side = depot_side[1:]

1924 1925
        if not exclude:
            self.mappings.append(depot_side)
P
Pete Wyckoff 已提交
1926

1927 1928 1929 1930 1931 1932
    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 已提交
1933

1934 1935
    def update_client_spec_path_cache(self, files):
        """ Caching file paths by "p4 where" batch query """
P
Pete Wyckoff 已提交
1936

1937 1938
        # 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 已提交
1939

1940 1941
        if len(fileArgs) == 0:
            return  # All files in cache
P
Pete Wyckoff 已提交
1942

1943 1944 1945 1946 1947 1948
        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:
1949
                die("No clientFile in 'p4 where' output")
1950 1951 1952
            if "unmap" in res:
                # it will list all of them, but only one not unmap-ped
                continue
1953 1954
            if gitConfigBool("core.ignorecase"):
                res['depotFile'] = res['depotFile'].lower()
1955
            self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
P
Pete Wyckoff 已提交
1956

1957 1958
        # not found files or unmap files set to ""
        for depotFile in fileArgs:
1959 1960
            if gitConfigBool("core.ignorecase"):
                depotFile = depotFile.lower()
1961 1962
            if depotFile not in self.client_spec_path_cache:
                self.client_spec_path_cache[depotFile] = ""
P
Pete Wyckoff 已提交
1963

1964 1965 1966 1967
    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 已提交
1968

1969 1970 1971
        if gitConfigBool("core.ignorecase"):
            depot_path = depot_path.lower()

1972 1973 1974 1975 1976
        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 已提交
1977

1978
class P4Sync(Command, P4UserMap):
1979 1980
    delete_actions = ( "delete", "move/delete", "purge" )

1981 1982
    def __init__(self):
        Command.__init__(self)
1983
        P4UserMap.__init__(self)
1984 1985 1986 1987 1988
        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"),
1989
                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1990
                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1991 1992
                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
                                     help="Import into refs/heads/ , not refs/remotes"),
1993 1994 1995 1996
                optparse.make_option("--max-changes", dest="maxChanges",
                                     help="Maximum number of changes to import"),
                optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
                                     help="Internal block size to use when iteratively calling p4 changes"),
1997
                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1998 1999
                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2000 2001 2002 2003
                                     help="Only sync files that are included in the Perforce Client Spec"),
                optparse.make_option("-/", dest="cloneExclude",
                                     action="append", type="string",
                                     help="exclude depot path"),
2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014
        ]
        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
2015 2016
        self.createdBranches = set()
        self.committedChanges = set()
2017
        self.branch = ""
2018
        self.detectBranches = False
2019
        self.detectLabels = False
2020
        self.importLabels = False
2021
        self.changesFile = ""
2022
        self.syncWithOrigin = True
2023
        self.importIntoRemotes = True
2024
        self.maxChanges = ""
2025
        self.changes_block_size = None
2026
        self.keepRepoPath = False
2027
        self.depotPaths = None
2028
        self.p4BranchesInGit = []
T
Tommy Thorn 已提交
2029
        self.cloneExclude = []
2030
        self.useClientSpec = False
2031
        self.useClientSpec_from_options = False
P
Pete Wyckoff 已提交
2032
        self.clientSpecDirs = None
2033 2034
        self.tempBranches = []
        self.tempBranchLocation = "git-p4-tmp"
2035

2036 2037 2038
        if gitConfig("git-p4.syncFromOrigin") == "false":
            self.syncWithOrigin = False

2039 2040 2041 2042 2043 2044
    # 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)

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

2053
    def extractFilesFromCommit(self, commit):
T
Tommy Thorn 已提交
2054 2055
        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
                             for path in self.cloneExclude]
2056 2057 2058 2059
        files = []
        fnum = 0
        while commit.has_key("depotFile%s" % fnum):
            path =  commit["depotFile%s" % fnum]
2060

T
Tommy Thorn 已提交
2061
            if [p for p in self.cloneExclude
2062
                if p4PathStartsWith(path, p)]:
T
Tommy Thorn 已提交
2063 2064 2065
                found = False
            else:
                found = [p for p in self.depotPaths
2066
                         if p4PathStartsWith(path, p)]
2067
            if not found:
2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079
                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

2080
    def stripRepoPath(self, path, prefixes):
2081 2082 2083 2084 2085
        """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."""

2086
        if self.useClientSpec:
2087 2088
            # branch detection moves files up a level (the branch name)
            # from what client spec interpretation gives
2089
            path = self.clientSpecDirs.map_in_client(path)
2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101
            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):]
2102

2103 2104 2105 2106
        else:
            for p in prefixes:
                if p4PathStartsWith(path, p):
                    path = path[len(p):]
2107
                    break
2108

2109
        path = wildcard_decode(path)
2110
        return path
H
Han-Wen Nienhuys 已提交
2111

2112
    def splitFilesIntoBranches(self, commit):
2113 2114 2115
        """Look at each depotFile in the commit to figure out to what
           branch it belongs."""

2116 2117 2118 2119
        if self.clientSpecDirs:
            files = self.extractFilesFromCommit(commit)
            self.clientSpecDirs.update_client_spec_path_cache(files)

2120
        branches = {}
2121 2122 2123
        fnum = 0
        while commit.has_key("depotFile%s" % fnum):
            path =  commit["depotFile%s" % fnum]
2124
            found = [p for p in self.depotPaths
2125
                     if p4PathStartsWith(path, p)]
2126
            if not found:
2127 2128 2129 2130 2131 2132 2133 2134 2135 2136
                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

2137 2138 2139 2140 2141 2142
            # 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)
2143

2144
            for branch in self.knownBranches.keys():
2145 2146
                # 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 已提交
2147
                if relPath.startswith(branch + "/"):
2148 2149
                    if branch not in branches:
                        branches[branch] = []
2150
                    branches[branch].append(file)
2151
                    break
2152 2153 2154

        return branches

2155 2156 2157 2158 2159 2160 2161 2162
    # 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 已提交
2163 2164 2165 2166 2167 2168 2169
        (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"
2170 2171
            # p4 print on a symlink sometimes contains "target\n";
            # if it does, remove the newline
2172
            data = ''.join(contents)
2173 2174 2175 2176 2177 2178 2179 2180
            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':
2181 2182 2183
                contents = [data[:-1]]
            else:
                contents = [data]
2184

P
Pete Wyckoff 已提交
2185
        if type_base == "utf16":
2186 2187 2188 2189 2190
            # 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.
2191 2192 2193 2194 2195
            #
            # 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.
            #
2196
            text = p4_read_pipe(['print', '-q', '-o', '-', "%s@%s" % (file['depotFile'], file['change']) ])
2197 2198
            if p4_version_string().find("/NT") >= 0:
                text = text.replace("\r\n", "\n")
2199 2200
            contents = [ text ]

P
Pete Wyckoff 已提交
2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213
        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

2214 2215
        # Note that we do not try to de-mangle keywords on utf16 files,
        # even though in theory somebody may want that.
2216 2217 2218 2219 2220 2221
        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 ]
2222

P
Pete Wyckoff 已提交
2223
        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243

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

2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266
        # 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)

2267 2268 2269 2270 2271 2272
        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
2273

2274 2275 2276 2277 2278 2279 2280
        # 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]
2281

2282
        self.stream_have_file_info = True
2283 2284 2285

    # Stream directly from "p4 files" into "git fast-import"
    def streamP4Files(self, files):
2286 2287
        filesForCommit = []
        filesToRead = []
2288
        filesToDelete = []
2289

2290
        for f in files:
P
Pete Wyckoff 已提交
2291 2292 2293 2294 2295
            # 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
2296

P
Pete Wyckoff 已提交
2297 2298 2299 2300 2301
            filesForCommit.append(f)
            if f['action'] in self.delete_actions:
                filesToDelete.append(f)
            else:
                filesToRead.append(f)
H
Han-Wen Nienhuys 已提交
2302

2303 2304 2305
        # deleted files...
        for f in filesToDelete:
            self.streamOneP4Deletion(f)
2306

2307 2308 2309 2310
        if len(filesToRead) > 0:
            self.stream_file = {}
            self.stream_contents = []
            self.stream_have_file_info = False
2311

2312 2313 2314
            # curry self argument
            def streamP4FilesCbSelf(entry):
                self.streamP4FilesCb(entry)
H
Han-Wen Nienhuys 已提交
2315

2316 2317 2318 2319 2320
            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]

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

2322 2323 2324
            # do the last chunk
            if self.stream_file.has_key('depotFile'):
                self.streamOneP4File(self.stream_file, self.stream_contents)
H
Han-Wen Nienhuys 已提交
2325

2326 2327 2328 2329 2330 2331
    def make_email(self, userid):
        if userid in self.users:
            return self.users[userid]
        else:
            return "%s <a@b>" % userid

2332
    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2333 2334 2335 2336
        """ Stream a p4 tag.
        commit is either a git commit, or a fast-import mark, ":<p4commit>"
        """

2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366
        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")

2367
    def commit(self, details, files, branch, parent = ""):
2368 2369 2370
        epoch = details["time"]
        author = details["user"]

2371 2372 2373
        if self.verbose:
            print "commit into %s" % branch

2374 2375 2376 2377
        # start with reading files; if that fails, we should not
        # create a commit.
        new_files = []
        for f in files:
2378
            if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2379 2380
                new_files.append (f)
            else:
2381
                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2382

2383 2384 2385
        if self.clientSpecDirs:
            self.clientSpecDirs.update_client_spec_path_cache(files)

2386
        self.gitStream.write("commit %s\n" % branch)
2387
        self.gitStream.write("mark :%s\n" % details["change"])
2388 2389
        self.committedChanges.add(int(details["change"]))
        committer = ""
2390 2391
        if author not in self.users:
            self.getUserMapFromPerforceServer()
2392
        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2393 2394 2395 2396 2397

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

        self.gitStream.write("data <<EOT\n")
        self.gitStream.write(details["desc"])
2398 2399
        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
                             (','.join(self.branchPrefixes), details["change"]))
2400 2401 2402
        if len(details['options']) > 0:
            self.gitStream.write(": options = %s" % details['options'])
        self.gitStream.write("]\nEOT\n\n")
2403 2404

        if len(parent) > 0:
2405 2406
            if self.verbose:
                print "parent %s" % parent
2407 2408
            self.gitStream.write("from %s\n" % parent)

2409
        self.streamP4Files(new_files)
2410 2411
        self.gitStream.write("\n")

2412 2413
        change = int(details["change"])

2414
        if self.labels.has_key(change):
2415 2416 2417
            label = self.labels[change]
            labelDetails = label[0]
            labelRevisions = label[1]
2418 2419
            if self.verbose:
                print "Change %s is labelled %s" % (change, labelDetails)
2420

2421
            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2422
                                                for p in self.branchPrefixes])
2423 2424 2425 2426 2427

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

                cleanedFiles = {}
                for info in files:
2428
                    if info["action"] in self.delete_actions:
2429 2430 2431 2432
                        continue
                    cleanedFiles[info["depotFile"]] = info["rev"]

                if cleanedFiles == labelRevisions:
2433
                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2434 2435

                else:
2436
                    if not self.silent:
2437 2438
                        print ("Tag %s does not match with change %s: files do not match."
                               % (labelDetails["label"], change))
2439 2440

            else:
2441
                if not self.silent:
2442 2443
                    print ("Tag %s does not match with change %s: file count is different."
                           % (labelDetails["label"], change))
2444

2445
    # Build a dictionary of changelists and labels, for "detect-labels" option.
2446 2447 2448
    def getLabels(self):
        self.labels = {}

2449
        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
S
Simon Hausmann 已提交
2450
        if len(l) > 0 and not self.silent:
2451
            print "Finding files belonging to labels in %s" % `self.depotPaths`
S
Simon Hausmann 已提交
2452 2453

        for output in l:
2454 2455 2456
            label = output["label"]
            revisions = {}
            newestChange = 0
2457 2458
            if self.verbose:
                print "Querying files for label %s" % label
2459 2460 2461
            for file in p4CmdList(["files"] +
                                      ["%s...@%s" % (p, label)
                                          for p in self.depotPaths]):
2462 2463 2464 2465 2466
                revisions[file["depotFile"]] = file["rev"]
                change = int(file["change"])
                if change > newestChange:
                    newestChange = change

2467 2468 2469 2470
            self.labels[newestChange] = [output, revisions]

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

2472 2473 2474 2475 2476 2477 2478 2479 2480
    # 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")
2481
        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505
        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'])
2506 2507
                if changelist in self.committedChanges:
                    gitCommit = ":%d" % changelist       # use a fast-import mark
2508
                    commitFound = True
2509 2510 2511 2512 2513 2514 2515 2516 2517 2518
                else:
                    gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
                        "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
                    if len(gitCommit) == 0:
                        print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
                    else:
                        commitFound = True
                        gitCommit = gitCommit.strip()

                if commitFound:
2519 2520 2521 2522
                    # Convert from p4 time format
                    try:
                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
                    except ValueError:
2523
                        print "Could not convert label time %s" % labelDetails['Update']
2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540
                        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])

2541 2542
    def guessProjectName(self):
        for p in self.depotPaths:
S
Simon Hausmann 已提交
2543 2544 2545 2546 2547 2548
            if p.endswith("/"):
                p = p[:-1]
            p = p[p.strip().rfind("/") + 1:]
            if not p.endswith("/"):
               p += "/"
            return p
2549

2550
    def getBranchMapping(self):
2551 2552
        lostAndFoundBranches = set()

2553 2554 2555 2556 2557 2558 2559
        user = gitConfig("git-p4.branchUser")
        if len(user) > 0:
            command = "branches -u %s" % user
        else:
            command = "branches"

        for info in p4CmdList(command):
2560
            details = p4Cmd(["branch", "-o", info["branch"]])
2561 2562 2563 2564 2565 2566 2567 2568 2569
            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]
2570
                ## HACK
2571
                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2572 2573
                    source = source[len(self.depotPaths[0]):-4]
                    destination = destination[len(self.depotPaths[0]):-4]
2574

2575 2576 2577 2578 2579 2580
                    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

2581 2582 2583 2584
                    self.knownBranches[destination] = source

                    lostAndFoundBranches.discard(destination)

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

2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606
        # 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)

2607 2608 2609

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

2611 2612 2613 2614 2615 2616 2617 2618 2619
    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 已提交
2620 2621 2622 2623 2624 2625 2626 2627 2628 2629
    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']))
2630

2631 2632 2633 2634 2635 2636 2637 2638 2639
    def gitRefForBranch(self, branch):
        if branch == "main":
            return self.refPrefix + "master"

        if len(branch) <= 0:
            return branch

        return self.refPrefix + self.projectName + branch

2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680
    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
2681
        changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
2682 2683 2684 2685 2686 2687 2688 2689 2690
        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

2691
        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2692 2693 2694 2695 2696 2697 2698 2699 2700
        #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

2701 2702
    def searchParent(self, parent, branch, target):
        parentFound = False
2703 2704
        for blob in read_pipe_lines(["git", "rev-list", "--reverse",
                                     "--no-merges", parent]):
2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715
            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

2716 2717 2718
    def importChanges(self, changes):
        cnt = 1
        for change in changes:
P
Pete Wyckoff 已提交
2719
            description = p4_describe(change)
2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732
            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 + "/"
2733
                        self.branchPrefixes = [ branchPrefix ]
2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748

                        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 = ""
2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761
                            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
2762

2763 2764
                        branch = self.gitRefForBranch(branch)
                        parent = self.gitRefForBranch(parent)
2765 2766 2767 2768 2769 2770 2771 2772

                        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]

2773 2774
                        blob = None
                        if len(parent) > 0:
2775
                            tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2776 2777
                            if self.verbose:
                                print "Creating temporary branch: " + tempBranch
2778
                            self.commit(description, filesForCommit, tempBranch)
2779 2780 2781 2782
                            self.tempBranches.append(tempBranch)
                            self.checkpoint()
                            blob = self.searchParent(parent, branch, tempBranch)
                        if blob:
2783
                            self.commit(description, filesForCommit, branch, blob)
2784 2785 2786
                        else:
                            if self.verbose:
                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2787
                            self.commit(description, filesForCommit, branch, parent)
2788 2789
                else:
                    files = self.extractFilesFromCommit(description)
2790
                    self.commit(description, files, self.branch,
2791
                                self.initialParent)
2792
                    # only needed once, to connect to the previous commit
2793 2794 2795 2796 2797
                    self.initialParent = ""
            except IOError:
                print self.gitError.read()
                sys.exit(1)

2798 2799 2800
    def importHeadRevision(self, revision):
        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)

2801 2802
        details = {}
        details["user"] = "git perforce import user"
2803
        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2804 2805 2806 2807 2808
                           % (' '.join(self.depotPaths), revision))
        details["change"] = revision
        newestRevision = 0

        fileCnt = 0
2809 2810 2811
        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]

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

2813
            if 'code' in info and info['code'] == 'error':
2814 2815
                sys.stderr.write("p4 returned an error: %s\n"
                                 % info['data'])
2816 2817 2818 2819
                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))
2820
                sys.exit(1)
2821 2822
            if 'p4ExitCode' in info:
                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2823 2824 2825 2826 2827 2828 2829
                sys.exit(1)


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

2830
            if info["action"] in self.delete_actions:
2831 2832 2833 2834 2835 2836 2837 2838 2839 2840
                # 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
2841

P
Pete Wyckoff 已提交
2842
        # Use time from top-most change so that all git p4 clones of
2843
        # the same p4 repo have the same commit SHA1s.
P
Pete Wyckoff 已提交
2844 2845
        res = p4_describe(newestRevision)
        details["time"] = res["time"]
2846

2847 2848
        self.updateOptionDict(details)
        try:
2849
            self.commit(details, self.extractFilesFromCommit(details), self.branch)
2850 2851 2852 2853 2854
        except IOError:
            print "IO error with git fast-import. Is your git version recent enough?"
            print self.gitError.read()


2855
    def run(self, args):
2856
        self.depotPaths = []
2857
        self.changeRange = ""
2858
        self.previousDepotPaths = []
2859
        self.hasOrigin = False
H
Han-Wen Nienhuys 已提交
2860

2861 2862 2863 2864
        # map from branch depot path to parent branch
        self.knownBranches = {}
        self.initialParents = {}

2865 2866 2867
        if self.importIntoRemotes:
            self.refPrefix = "refs/remotes/p4/"
        else:
2868
            self.refPrefix = "refs/heads/p4/"
2869

2870 2871 2872 2873 2874 2875
        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")
2876

2877
        branch_arg_given = bool(self.branch)
2878
        if len(self.branch) == 0:
2879
            self.branch = self.refPrefix + "master"
2880
            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2881
                system("git update-ref %s refs/heads/p4" % self.branch)
2882
                system("git branch -D p4")
2883

2884 2885 2886 2887 2888
        # 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 已提交
2889
            if gitConfigBool("git-p4.useclientspec"):
2890 2891
                self.useClientSpec = True
        if self.useClientSpec:
2892
            self.clientSpecDirs = getClientSpec()
2893

H
Han-Wen Nienhuys 已提交
2894 2895 2896
        # TODO: should always look at previous commits,
        # merge with previous imports, if possible.
        if args == []:
2897
            if self.hasOrigin:
2898
                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2899 2900 2901

            # branches holds mapping from branch name to sha1
            branches = p4BranchesInGit(self.importIntoRemotes)
2902 2903 2904 2905 2906 2907 2908 2909

            # 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()
2910 2911 2912 2913 2914

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

2919 2920 2921 2922 2923
            if self.verbose:
                print "branches: %s" % self.p4BranchesInGit

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

                settings = extractSettingsGitLog(logMsg)
2927

H
Han-Wen Nienhuys 已提交
2928 2929 2930 2931
                self.readOptions(settings)
                if (settings.has_key('depot-paths')
                    and settings.has_key ('change')):
                    change = int(settings['change']) + 1
2932 2933
                    p4Change = max(p4Change, change)

H
Han-Wen Nienhuys 已提交
2934 2935
                    depotPaths = sorted(settings['depot-paths'])
                    if self.previousDepotPaths == []:
2936
                        self.previousDepotPaths = depotPaths
2937
                    else:
2938 2939
                        paths = []
                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2940 2941 2942 2943
                            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]:
2944
                                    i = i - 1
2945 2946
                                    break

2947
                            paths.append ("/".join(cur_list[:i + 1]))
2948 2949

                        self.previousDepotPaths = paths
2950 2951

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

2957 2958 2959 2960
        # 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/
2961
        if not self.branch.startswith("refs/"):
2962 2963 2964 2965 2966 2967 2968
            if self.importIntoRemotes:
                prepend = "refs/remotes/"
            else:
                prepend = "refs/heads/"
            if not self.branch.startswith("p4/"):
                prepend += "p4/"
            self.branch = prepend + self.branch
2969

2970
        if len(args) == 0 and self.depotPaths:
2971
            if not self.silent:
2972
                print "Depot paths: %s" % ' '.join(self.depotPaths)
2973
        else:
2974
            if self.depotPaths and self.depotPaths != args:
2975
                print ("previous import used depot path %s and now %s was specified. "
2976 2977
                       "This doesn't work!" % (' '.join (self.depotPaths),
                                               ' '.join (args)))
2978
                sys.exit(1)
2979

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

2982
        revision = ""
2983 2984
        self.users = {}

2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995
        # 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")

2996 2997 2998 2999 3000 3001 3002
        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 已提交
3003
                elif ',' not in self.changeRange:
3004
                    revision = self.changeRange
3005
                    self.changeRange = ""
3006
                p = p[:atIdx]
3007 3008
            elif p.find("#") != -1:
                hashIdx = p.index("#")
3009
                revision = p[hashIdx:]
3010
                p = p[:hashIdx]
3011
            elif self.previousDepotPaths == []:
3012 3013 3014 3015
                # pay attention to changesfile, if given, else import
                # the entire p4 tree at the head revision
                if len(self.changesFile) == 0:
                    revision = "#head"
3016 3017 3018 3019 3020 3021 3022 3023 3024

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

            newPaths.append(p)

        self.depotPaths = newPaths

3025 3026 3027
        # --detect-branches may change this for each branch
        self.branchPrefixes = self.depotPaths

3028
        self.loadUserMapFromCache()
3029 3030 3031
        self.labels = {}
        if self.detectLabels:
            self.getLabels();
3032

3033
        if self.detectBranches:
3034 3035 3036
            ## FIXME - what's a P4 projectName ?
            self.projectName = self.guessProjectName()

3037 3038 3039 3040
            if self.hasOrigin:
                self.getBranchMappingFromGitBranches()
            else:
                self.getBranchMapping()
3041 3042 3043 3044 3045
            if self.verbose:
                print "p4-git branches: %s" % self.p4BranchesInGit
                print "initial parents: %s" % self.initialParents
            for b in self.p4BranchesInGit:
                if b != "master":
3046 3047

                    ## FIXME
3048 3049
                    b = b[len(self.projectName):]
                self.createdBranches.add(b)
3050

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

3053 3054 3055 3056 3057 3058 3059
        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
3060

3061
        if revision:
3062
            self.importHeadRevision(revision)
3063 3064 3065
        else:
            changes = []

3066
            if len(self.changesFile) > 0:
3067
                output = open(self.changesFile).readlines()
3068
                changeSet = set()
3069 3070 3071 3072 3073 3074 3075 3076
                for line in output:
                    changeSet.add(int(line))

                for change in changeSet:
                    changes.append(change)

                changes.sort()
            else:
P
Pete Wyckoff 已提交
3077 3078
                # catch "git p4 sync" with no new branches, in a repo that
                # does not have any existing p4 branches
3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093
                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)

3094
                if self.verbose:
3095
                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3096
                                                              self.changeRange)
3097
                changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3098

3099
                if len(self.maxChanges) > 0:
3100
                    changes = changes[:min(int(self.maxChanges), len(changes))]
3101

3102
            if len(changes) == 0:
3103
                if not self.silent:
3104
                    print "No changes to import!"
3105 3106 3107 3108 3109
            else:
                if not self.silent and not self.detectBranches:
                    print "Import destination: %s" % self.branch

                self.updatedBranches = set()
3110

3111 3112 3113 3114 3115 3116 3117 3118
                if not self.detectBranches:
                    if args:
                        # start a new branch
                        self.initialParent = ""
                    else:
                        # build on a previous revision
                        self.initialParent = parseRevision(self.branch)

3119
                self.importChanges(changes)
3120

3121 3122 3123 3124 3125 3126 3127
                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")
3128

P
Pete Wyckoff 已提交
3129
        if gitConfigBool("git-p4.importLabels"):
3130
            self.importLabels = True
3131

3132 3133 3134 3135 3136 3137
        if self.importLabels:
            p4Labels = getP4Labels(self.depotPaths)
            gitTags = getGitTags()

            missingP4Labels = p4Labels - gitTags
            self.importP4Labels(self.gitStream, missingP4Labels)
3138 3139

        self.gitStream.close()
3140
        if self.importProcess.wait() != 0:
3141
            die("fast-import failed: %s" % self.gitError.read())
3142 3143 3144
        self.gitOutput.close()
        self.gitError.close()

3145 3146 3147 3148 3149 3150
        # 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))

3151 3152 3153 3154 3155 3156 3157
        # 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])

3158 3159
        return True

S
Simon Hausmann 已提交
3160 3161 3162
class P4Rebase(Command):
    def __init__(self):
        Command.__init__(self)
3163 3164 3165 3166
        self.options = [
                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
        ]
        self.importLabels = False
3167 3168
        self.description = ("Fetches the latest revision from perforce and "
                            + "rebases the current work (branch) against it")
S
Simon Hausmann 已提交
3169 3170 3171

    def run(self, args):
        sync = P4Sync()
3172
        sync.importLabels = self.importLabels
S
Simon Hausmann 已提交
3173
        sync.run([])
3174

3175 3176 3177
        return self.rebase()

    def rebase(self):
3178 3179 3180
        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:
3181
            die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3182

3183 3184 3185 3186 3187 3188 3189 3190
        [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
3191
        oldHead = read_pipe("git rev-parse HEAD").strip()
3192
        system("git rebase %s" % upstream)
3193
        system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
S
Simon Hausmann 已提交
3194 3195
        return True

3196 3197 3198 3199
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 已提交
3200
        self.usage = "usage: %prog [options] //depot/path[@revRange]"
T
Tommy Thorn 已提交
3201
        self.options += [
H
Han-Wen Nienhuys 已提交
3202 3203
            optparse.make_option("--destination", dest="cloneDestination",
                                 action='store', default=None,
T
Tommy Thorn 已提交
3204
                                 help="where to leave result of the clone"),
P
Pete Wyckoff 已提交
3205 3206
            optparse.make_option("--bare", dest="cloneBare",
                                 action="store_true", default=False),
T
Tommy Thorn 已提交
3207
        ]
H
Han-Wen Nienhuys 已提交
3208
        self.cloneDestination = None
3209
        self.needsGit = False
P
Pete Wyckoff 已提交
3210
        self.cloneBare = False
3211

H
Han-Wen Nienhuys 已提交
3212 3213 3214 3215 3216
    def defaultDestination(self, args):
        ## TODO: use common prefix of args?
        depotPath = args[0]
        depotDir = re.sub("(@[^@]*)$", "", depotPath)
        depotDir = re.sub("(#[^#]*)$", "", depotDir)
3217
        depotDir = re.sub(r"\.\.\.$", "", depotDir)
H
Han-Wen Nienhuys 已提交
3218 3219 3220
        depotDir = re.sub(r"/$", "", depotDir)
        return os.path.split(depotDir)[1]

3221 3222 3223
    def run(self, args):
        if len(args) < 1:
            return False
H
Han-Wen Nienhuys 已提交
3224 3225 3226 3227

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

3229
        depotPaths = args
3230 3231 3232 3233 3234

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

T
Tommy Thorn 已提交
3235
        self.cloneExclude = ["/"+p for p in self.cloneExclude]
3236 3237
        for p in depotPaths:
            if not p.startswith("//"):
3238
                sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3239
                return False
3240

H
Han-Wen Nienhuys 已提交
3241
        if not self.cloneDestination:
3242
            self.cloneDestination = self.defaultDestination(args)
3243

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

3246 3247
        if not os.path.exists(self.cloneDestination):
            os.makedirs(self.cloneDestination)
3248
        chdir(self.cloneDestination)
P
Pete Wyckoff 已提交
3249 3250 3251 3252

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

3257
        if not P4Sync.run(self, depotPaths):
3258
            return False
3259 3260 3261 3262 3263 3264 3265 3266 3267

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

3269 3270 3271 3272
        # auto-set this variable if invoked with --use-client-spec
        if self.useClientSpec_from_options:
            system("git config --bool git-p4.useclientspec true")

3273 3274
        return True

3275 3276 3277 3278 3279 3280 3281 3282 3283
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):
3284 3285 3286
        if originP4BranchesExist():
            createOrUpdateBranchesFromOrigin()

3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302
        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

3303 3304 3305 3306 3307 3308 3309 3310 3311
class HelpFormatter(optparse.IndentedHelpFormatter):
    def __init__(self):
        optparse.IndentedHelpFormatter.__init__(self)

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

3313 3314 3315 3316 3317 3318 3319 3320 3321
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 已提交
3322 3323
    "debug" : P4Debug,
    "submit" : P4Submit,
3324
    "commit" : P4Submit,
H
Han-Wen Nienhuys 已提交
3325 3326 3327
    "sync" : P4Sync,
    "rebase" : P4Rebase,
    "clone" : P4Clone,
3328 3329
    "rollback" : P4RollBack,
    "branches" : P4Branches
3330 3331 3332
}


H
Han-Wen Nienhuys 已提交
3333 3334 3335 3336
def main():
    if len(sys.argv[1:]) == 0:
        printUsage(commands.keys())
        sys.exit(2)
3337

H
Han-Wen Nienhuys 已提交
3338 3339
    cmdName = sys.argv[1]
    try:
H
Han-Wen Nienhuys 已提交
3340 3341
        klass = commands[cmdName]
        cmd = klass()
H
Han-Wen Nienhuys 已提交
3342 3343 3344 3345 3346 3347 3348
    except KeyError:
        print "unknown command %s" % cmdName
        print ""
        printUsage(commands.keys())
        sys.exit(2)

    options = cmd.options
H
Han-Wen Nienhuys 已提交
3349
    cmd.gitdir = os.environ.get("GIT_DIR", None)
H
Han-Wen Nienhuys 已提交
3350 3351 3352

    args = sys.argv[2:]

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

3357 3358 3359 3360
    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
                                   options,
                                   description = cmd.description,
                                   formatter = HelpFormatter())
H
Han-Wen Nienhuys 已提交
3361

3362
    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
H
Han-Wen Nienhuys 已提交
3363 3364 3365
    global verbose
    verbose = cmd.verbose
    if cmd.needsGit:
H
Han-Wen Nienhuys 已提交
3366 3367 3368 3369 3370
        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 已提交
3371 3372
                    cdup = read_pipe("git rev-parse --show-cdup").strip()
                    if len(cdup) > 0:
3373
                        chdir(cdup);
3374

H
Han-Wen Nienhuys 已提交
3375 3376 3377
        if not isValidGitDir(cmd.gitdir):
            if isValidGitDir(cmd.gitdir + "/.git"):
                cmd.gitdir += "/.git"
H
Han-Wen Nienhuys 已提交
3378
            else:
H
Han-Wen Nienhuys 已提交
3379
                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3380

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

H
Han-Wen Nienhuys 已提交
3383 3384
    if not cmd.run(args):
        parser.print_help()
3385
        sys.exit(2)
3386 3387


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