git-p4.pyon commit Documentation: replace: add --graft option (78024c4)
   1#!/usr/bin/env python
   2#
   3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
   4#
   5# Author: Simon Hausmann <simon@lst.de>
   6# Copyright: 2007 Simon Hausmann <simon@lst.de>
   7#            2007 Trolltech ASA
   8# License: MIT <http://www.opensource.org/licenses/mit-license.php>
   9#
  10import sys
  11if sys.hexversion < 0x02040000:
  12    # The limiter is the subprocess module
  13    sys.stderr.write("git-p4: requires Python 2.4 or later.\n")
  14    sys.exit(1)
  15import os
  16import optparse
  17import marshal
  18import subprocess
  19import tempfile
  20import time
  21import platform
  22import re
  23import shutil
  24import stat
  25
  26try:
  27    from subprocess import CalledProcessError
  28except ImportError:
  29    # from python2.7:subprocess.py
  30    # Exception classes used by this module.
  31    class CalledProcessError(Exception):
  32        """This exception is raised when a process run by check_call() returns
  33        a non-zero exit status.  The exit status will be stored in the
  34        returncode attribute."""
  35        def __init__(self, returncode, cmd):
  36            self.returncode = returncode
  37            self.cmd = cmd
  38        def __str__(self):
  39            return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
  40
  41verbose = False
  42
  43# Only labels/tags matching this will be imported/exported
  44defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
  45
  46def p4_build_cmd(cmd):
  47    """Build a suitable p4 command line.
  48
  49    This consolidates building and returning a p4 command line into one
  50    location. It means that hooking into the environment, or other configuration
  51    can be done more easily.
  52    """
  53    real_cmd = ["p4"]
  54
  55    user = gitConfig("git-p4.user")
  56    if len(user) > 0:
  57        real_cmd += ["-u",user]
  58
  59    password = gitConfig("git-p4.password")
  60    if len(password) > 0:
  61        real_cmd += ["-P", password]
  62
  63    port = gitConfig("git-p4.port")
  64    if len(port) > 0:
  65        real_cmd += ["-p", port]
  66
  67    host = gitConfig("git-p4.host")
  68    if len(host) > 0:
  69        real_cmd += ["-H", host]
  70
  71    client = gitConfig("git-p4.client")
  72    if len(client) > 0:
  73        real_cmd += ["-c", client]
  74
  75
  76    if isinstance(cmd,basestring):
  77        real_cmd = ' '.join(real_cmd) + ' ' + cmd
  78    else:
  79        real_cmd += cmd
  80    return real_cmd
  81
  82def chdir(path, is_client_path=False):
  83    """Do chdir to the given path, and set the PWD environment
  84       variable for use by P4.  It does not look at getcwd() output.
  85       Since we're not using the shell, it is necessary to set the
  86       PWD environment variable explicitly.
  87
  88       Normally, expand the path to force it to be absolute.  This
  89       addresses the use of relative path names inside P4 settings,
  90       e.g. P4CONFIG=.p4config.  P4 does not simply open the filename
  91       as given; it looks for .p4config using PWD.
  92
  93       If is_client_path, the path was handed to us directly by p4,
  94       and may be a symbolic link.  Do not call os.getcwd() in this
  95       case, because it will cause p4 to think that PWD is not inside
  96       the client path.
  97       """
  98
  99    os.chdir(path)
 100    if not is_client_path:
 101        path = os.getcwd()
 102    os.environ['PWD'] = path
 103
 104def die(msg):
 105    if verbose:
 106        raise Exception(msg)
 107    else:
 108        sys.stderr.write(msg + "\n")
 109        sys.exit(1)
 110
 111def write_pipe(c, stdin):
 112    if verbose:
 113        sys.stderr.write('Writing pipe: %s\n' % str(c))
 114
 115    expand = isinstance(c,basestring)
 116    p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
 117    pipe = p.stdin
 118    val = pipe.write(stdin)
 119    pipe.close()
 120    if p.wait():
 121        die('Command failed: %s' % str(c))
 122
 123    return val
 124
 125def p4_write_pipe(c, stdin):
 126    real_cmd = p4_build_cmd(c)
 127    return write_pipe(real_cmd, stdin)
 128
 129def read_pipe(c, ignore_error=False):
 130    if verbose:
 131        sys.stderr.write('Reading pipe: %s\n' % str(c))
 132
 133    expand = isinstance(c,basestring)
 134    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
 135    pipe = p.stdout
 136    val = pipe.read()
 137    if p.wait() and not ignore_error:
 138        die('Command failed: %s' % str(c))
 139
 140    return val
 141
 142def p4_read_pipe(c, ignore_error=False):
 143    real_cmd = p4_build_cmd(c)
 144    return read_pipe(real_cmd, ignore_error)
 145
 146def read_pipe_lines(c):
 147    if verbose:
 148        sys.stderr.write('Reading pipe: %s\n' % str(c))
 149
 150    expand = isinstance(c, basestring)
 151    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
 152    pipe = p.stdout
 153    val = pipe.readlines()
 154    if pipe.close() or p.wait():
 155        die('Command failed: %s' % str(c))
 156
 157    return val
 158
 159def p4_read_pipe_lines(c):
 160    """Specifically invoke p4 on the command supplied. """
 161    real_cmd = p4_build_cmd(c)
 162    return read_pipe_lines(real_cmd)
 163
 164def p4_has_command(cmd):
 165    """Ask p4 for help on this command.  If it returns an error, the
 166       command does not exist in this version of p4."""
 167    real_cmd = p4_build_cmd(["help", cmd])
 168    p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
 169                                   stderr=subprocess.PIPE)
 170    p.communicate()
 171    return p.returncode == 0
 172
 173def p4_has_move_command():
 174    """See if the move command exists, that it supports -k, and that
 175       it has not been administratively disabled.  The arguments
 176       must be correct, but the filenames do not have to exist.  Use
 177       ones with wildcards so even if they exist, it will fail."""
 178
 179    if not p4_has_command("move"):
 180        return False
 181    cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
 182    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 183    (out, err) = p.communicate()
 184    # return code will be 1 in either case
 185    if err.find("Invalid option") >= 0:
 186        return False
 187    if err.find("disabled") >= 0:
 188        return False
 189    # assume it failed because @... was invalid changelist
 190    return True
 191
 192def system(cmd):
 193    expand = isinstance(cmd,basestring)
 194    if verbose:
 195        sys.stderr.write("executing %s\n" % str(cmd))
 196    retcode = subprocess.call(cmd, shell=expand)
 197    if retcode:
 198        raise CalledProcessError(retcode, cmd)
 199
 200def p4_system(cmd):
 201    """Specifically invoke p4 as the system command. """
 202    real_cmd = p4_build_cmd(cmd)
 203    expand = isinstance(real_cmd, basestring)
 204    retcode = subprocess.call(real_cmd, shell=expand)
 205    if retcode:
 206        raise CalledProcessError(retcode, real_cmd)
 207
 208_p4_version_string = None
 209def p4_version_string():
 210    """Read the version string, showing just the last line, which
 211       hopefully is the interesting version bit.
 212
 213       $ p4 -V
 214       Perforce - The Fast Software Configuration Management System.
 215       Copyright 1995-2011 Perforce Software.  All rights reserved.
 216       Rev. P4/NTX86/2011.1/393975 (2011/12/16).
 217    """
 218    global _p4_version_string
 219    if not _p4_version_string:
 220        a = p4_read_pipe_lines(["-V"])
 221        _p4_version_string = a[-1].rstrip()
 222    return _p4_version_string
 223
 224def p4_integrate(src, dest):
 225    p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
 226
 227def p4_sync(f, *options):
 228    p4_system(["sync"] + list(options) + [wildcard_encode(f)])
 229
 230def p4_add(f):
 231    # forcibly add file names with wildcards
 232    if wildcard_present(f):
 233        p4_system(["add", "-f", f])
 234    else:
 235        p4_system(["add", f])
 236
 237def p4_delete(f):
 238    p4_system(["delete", wildcard_encode(f)])
 239
 240def p4_edit(f):
 241    p4_system(["edit", wildcard_encode(f)])
 242
 243def p4_revert(f):
 244    p4_system(["revert", wildcard_encode(f)])
 245
 246def p4_reopen(type, f):
 247    p4_system(["reopen", "-t", type, wildcard_encode(f)])
 248
 249def p4_move(src, dest):
 250    p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
 251
 252def p4_describe(change):
 253    """Make sure it returns a valid result by checking for
 254       the presence of field "time".  Return a dict of the
 255       results."""
 256
 257    ds = p4CmdList(["describe", "-s", str(change)])
 258    if len(ds) != 1:
 259        die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
 260
 261    d = ds[0]
 262
 263    if "p4ExitCode" in d:
 264        die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
 265                                                      str(d)))
 266    if "code" in d:
 267        if d["code"] == "error":
 268            die("p4 describe -s %d returned error code: %s" % (change, str(d)))
 269
 270    if "time" not in d:
 271        die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
 272
 273    return d
 274
 275#
 276# Canonicalize the p4 type and return a tuple of the
 277# base type, plus any modifiers.  See "p4 help filetypes"
 278# for a list and explanation.
 279#
 280def split_p4_type(p4type):
 281
 282    p4_filetypes_historical = {
 283        "ctempobj": "binary+Sw",
 284        "ctext": "text+C",
 285        "cxtext": "text+Cx",
 286        "ktext": "text+k",
 287        "kxtext": "text+kx",
 288        "ltext": "text+F",
 289        "tempobj": "binary+FSw",
 290        "ubinary": "binary+F",
 291        "uresource": "resource+F",
 292        "uxbinary": "binary+Fx",
 293        "xbinary": "binary+x",
 294        "xltext": "text+Fx",
 295        "xtempobj": "binary+Swx",
 296        "xtext": "text+x",
 297        "xunicode": "unicode+x",
 298        "xutf16": "utf16+x",
 299    }
 300    if p4type in p4_filetypes_historical:
 301        p4type = p4_filetypes_historical[p4type]
 302    mods = ""
 303    s = p4type.split("+")
 304    base = s[0]
 305    mods = ""
 306    if len(s) > 1:
 307        mods = s[1]
 308    return (base, mods)
 309
 310#
 311# return the raw p4 type of a file (text, text+ko, etc)
 312#
 313def p4_type(f):
 314    results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
 315    return results[0]['headType']
 316
 317#
 318# Given a type base and modifier, return a regexp matching
 319# the keywords that can be expanded in the file
 320#
 321def p4_keywords_regexp_for_type(base, type_mods):
 322    if base in ("text", "unicode", "binary"):
 323        kwords = None
 324        if "ko" in type_mods:
 325            kwords = 'Id|Header'
 326        elif "k" in type_mods:
 327            kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
 328        else:
 329            return None
 330        pattern = r"""
 331            \$              # Starts with a dollar, followed by...
 332            (%s)            # one of the keywords, followed by...
 333            (:[^$\n]+)?     # possibly an old expansion, followed by...
 334            \$              # another dollar
 335            """ % kwords
 336        return pattern
 337    else:
 338        return None
 339
 340#
 341# Given a file, return a regexp matching the possible
 342# RCS keywords that will be expanded, or None for files
 343# with kw expansion turned off.
 344#
 345def p4_keywords_regexp_for_file(file):
 346    if not os.path.exists(file):
 347        return None
 348    else:
 349        (type_base, type_mods) = split_p4_type(p4_type(file))
 350        return p4_keywords_regexp_for_type(type_base, type_mods)
 351
 352def setP4ExecBit(file, mode):
 353    # Reopens an already open file and changes the execute bit to match
 354    # the execute bit setting in the passed in mode.
 355
 356    p4Type = "+x"
 357
 358    if not isModeExec(mode):
 359        p4Type = getP4OpenedType(file)
 360        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
 361        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
 362        if p4Type[-1] == "+":
 363            p4Type = p4Type[0:-1]
 364
 365    p4_reopen(p4Type, file)
 366
 367def getP4OpenedType(file):
 368    # Returns the perforce file type for the given file.
 369
 370    result = p4_read_pipe(["opened", wildcard_encode(file)])
 371    match = re.match(".*\((.+)\)\r?$", result)
 372    if match:
 373        return match.group(1)
 374    else:
 375        die("Could not determine file type for %s (result: '%s')" % (file, result))
 376
 377# Return the set of all p4 labels
 378def getP4Labels(depotPaths):
 379    labels = set()
 380    if isinstance(depotPaths,basestring):
 381        depotPaths = [depotPaths]
 382
 383    for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
 384        label = l['label']
 385        labels.add(label)
 386
 387    return labels
 388
 389# Return the set of all git tags
 390def getGitTags():
 391    gitTags = set()
 392    for line in read_pipe_lines(["git", "tag"]):
 393        tag = line.strip()
 394        gitTags.add(tag)
 395    return gitTags
 396
 397def diffTreePattern():
 398    # This is a simple generator for the diff tree regex pattern. This could be
 399    # a class variable if this and parseDiffTreeEntry were a part of a class.
 400    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
 401    while True:
 402        yield pattern
 403
 404def parseDiffTreeEntry(entry):
 405    """Parses a single diff tree entry into its component elements.
 406
 407    See git-diff-tree(1) manpage for details about the format of the diff
 408    output. This method returns a dictionary with the following elements:
 409
 410    src_mode - The mode of the source file
 411    dst_mode - The mode of the destination file
 412    src_sha1 - The sha1 for the source file
 413    dst_sha1 - The sha1 fr the destination file
 414    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
 415    status_score - The score for the status (applicable for 'C' and 'R'
 416                   statuses). This is None if there is no score.
 417    src - The path for the source file.
 418    dst - The path for the destination file. This is only present for
 419          copy or renames. If it is not present, this is None.
 420
 421    If the pattern is not matched, None is returned."""
 422
 423    match = diffTreePattern().next().match(entry)
 424    if match:
 425        return {
 426            'src_mode': match.group(1),
 427            'dst_mode': match.group(2),
 428            'src_sha1': match.group(3),
 429            'dst_sha1': match.group(4),
 430            'status': match.group(5),
 431            'status_score': match.group(6),
 432            'src': match.group(7),
 433            'dst': match.group(10)
 434        }
 435    return None
 436
 437def isModeExec(mode):
 438    # Returns True if the given git mode represents an executable file,
 439    # otherwise False.
 440    return mode[-3:] == "755"
 441
 442def isModeExecChanged(src_mode, dst_mode):
 443    return isModeExec(src_mode) != isModeExec(dst_mode)
 444
 445def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
 446
 447    if isinstance(cmd,basestring):
 448        cmd = "-G " + cmd
 449        expand = True
 450    else:
 451        cmd = ["-G"] + cmd
 452        expand = False
 453
 454    cmd = p4_build_cmd(cmd)
 455    if verbose:
 456        sys.stderr.write("Opening pipe: %s\n" % str(cmd))
 457
 458    # Use a temporary file to avoid deadlocks without
 459    # subprocess.communicate(), which would put another copy
 460    # of stdout into memory.
 461    stdin_file = None
 462    if stdin is not None:
 463        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
 464        if isinstance(stdin,basestring):
 465            stdin_file.write(stdin)
 466        else:
 467            for i in stdin:
 468                stdin_file.write(i + '\n')
 469        stdin_file.flush()
 470        stdin_file.seek(0)
 471
 472    p4 = subprocess.Popen(cmd,
 473                          shell=expand,
 474                          stdin=stdin_file,
 475                          stdout=subprocess.PIPE)
 476
 477    result = []
 478    try:
 479        while True:
 480            entry = marshal.load(p4.stdout)
 481            if cb is not None:
 482                cb(entry)
 483            else:
 484                result.append(entry)
 485    except EOFError:
 486        pass
 487    exitCode = p4.wait()
 488    if exitCode != 0:
 489        entry = {}
 490        entry["p4ExitCode"] = exitCode
 491        result.append(entry)
 492
 493    return result
 494
 495def p4Cmd(cmd):
 496    list = p4CmdList(cmd)
 497    result = {}
 498    for entry in list:
 499        result.update(entry)
 500    return result;
 501
 502def p4Where(depotPath):
 503    if not depotPath.endswith("/"):
 504        depotPath += "/"
 505    depotPath = depotPath + "..."
 506    outputList = p4CmdList(["where", depotPath])
 507    output = None
 508    for entry in outputList:
 509        if "depotFile" in entry:
 510            if entry["depotFile"] == depotPath:
 511                output = entry
 512                break
 513        elif "data" in entry:
 514            data = entry.get("data")
 515            space = data.find(" ")
 516            if data[:space] == depotPath:
 517                output = entry
 518                break
 519    if output == None:
 520        return ""
 521    if output["code"] == "error":
 522        return ""
 523    clientPath = ""
 524    if "path" in output:
 525        clientPath = output.get("path")
 526    elif "data" in output:
 527        data = output.get("data")
 528        lastSpace = data.rfind(" ")
 529        clientPath = data[lastSpace + 1:]
 530
 531    if clientPath.endswith("..."):
 532        clientPath = clientPath[:-3]
 533    return clientPath
 534
 535def currentGitBranch():
 536    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
 537
 538def isValidGitDir(path):
 539    if (os.path.exists(path + "/HEAD")
 540        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
 541        return True;
 542    return False
 543
 544def parseRevision(ref):
 545    return read_pipe("git rev-parse %s" % ref).strip()
 546
 547def branchExists(ref):
 548    rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
 549                     ignore_error=True)
 550    return len(rev) > 0
 551
 552def extractLogMessageFromGitCommit(commit):
 553    logMessage = ""
 554
 555    ## fixme: title is first line of commit, not 1st paragraph.
 556    foundTitle = False
 557    for log in read_pipe_lines("git cat-file commit %s" % commit):
 558       if not foundTitle:
 559           if len(log) == 1:
 560               foundTitle = True
 561           continue
 562
 563       logMessage += log
 564    return logMessage
 565
 566def extractSettingsGitLog(log):
 567    values = {}
 568    for line in log.split("\n"):
 569        line = line.strip()
 570        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
 571        if not m:
 572            continue
 573
 574        assignments = m.group(1).split (':')
 575        for a in assignments:
 576            vals = a.split ('=')
 577            key = vals[0].strip()
 578            val = ('='.join (vals[1:])).strip()
 579            if val.endswith ('\"') and val.startswith('"'):
 580                val = val[1:-1]
 581
 582            values[key] = val
 583
 584    paths = values.get("depot-paths")
 585    if not paths:
 586        paths = values.get("depot-path")
 587    if paths:
 588        values['depot-paths'] = paths.split(',')
 589    return values
 590
 591def gitBranchExists(branch):
 592    proc = subprocess.Popen(["git", "rev-parse", branch],
 593                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
 594    return proc.wait() == 0;
 595
 596_gitConfig = {}
 597
 598def gitConfig(key):
 599    if not _gitConfig.has_key(key):
 600        cmd = [ "git", "config", key ]
 601        s = read_pipe(cmd, ignore_error=True)
 602        _gitConfig[key] = s.strip()
 603    return _gitConfig[key]
 604
 605def gitConfigBool(key):
 606    """Return a bool, using git config --bool.  It is True only if the
 607       variable is set to true, and False if set to false or not present
 608       in the config."""
 609
 610    if not _gitConfig.has_key(key):
 611        cmd = [ "git", "config", "--bool", key ]
 612        s = read_pipe(cmd, ignore_error=True)
 613        v = s.strip()
 614        _gitConfig[key] = v == "true"
 615    return _gitConfig[key]
 616
 617def gitConfigList(key):
 618    if not _gitConfig.has_key(key):
 619        s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
 620        _gitConfig[key] = s.strip().split(os.linesep)
 621    return _gitConfig[key]
 622
 623def p4BranchesInGit(branchesAreInRemotes=True):
 624    """Find all the branches whose names start with "p4/", looking
 625       in remotes or heads as specified by the argument.  Return
 626       a dictionary of { branch: revision } for each one found.
 627       The branch names are the short names, without any
 628       "p4/" prefix."""
 629
 630    branches = {}
 631
 632    cmdline = "git rev-parse --symbolic "
 633    if branchesAreInRemotes:
 634        cmdline += "--remotes"
 635    else:
 636        cmdline += "--branches"
 637
 638    for line in read_pipe_lines(cmdline):
 639        line = line.strip()
 640
 641        # only import to p4/
 642        if not line.startswith('p4/'):
 643            continue
 644        # special symbolic ref to p4/master
 645        if line == "p4/HEAD":
 646            continue
 647
 648        # strip off p4/ prefix
 649        branch = line[len("p4/"):]
 650
 651        branches[branch] = parseRevision(line)
 652
 653    return branches
 654
 655def branch_exists(branch):
 656    """Make sure that the given ref name really exists."""
 657
 658    cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
 659    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 660    out, _ = p.communicate()
 661    if p.returncode:
 662        return False
 663    # expect exactly one line of output: the branch name
 664    return out.rstrip() == branch
 665
 666def findUpstreamBranchPoint(head = "HEAD"):
 667    branches = p4BranchesInGit()
 668    # map from depot-path to branch name
 669    branchByDepotPath = {}
 670    for branch in branches.keys():
 671        tip = branches[branch]
 672        log = extractLogMessageFromGitCommit(tip)
 673        settings = extractSettingsGitLog(log)
 674        if settings.has_key("depot-paths"):
 675            paths = ",".join(settings["depot-paths"])
 676            branchByDepotPath[paths] = "remotes/p4/" + branch
 677
 678    settings = None
 679    parent = 0
 680    while parent < 65535:
 681        commit = head + "~%s" % parent
 682        log = extractLogMessageFromGitCommit(commit)
 683        settings = extractSettingsGitLog(log)
 684        if settings.has_key("depot-paths"):
 685            paths = ",".join(settings["depot-paths"])
 686            if branchByDepotPath.has_key(paths):
 687                return [branchByDepotPath[paths], settings]
 688
 689        parent = parent + 1
 690
 691    return ["", settings]
 692
 693def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
 694    if not silent:
 695        print ("Creating/updating branch(es) in %s based on origin branch(es)"
 696               % localRefPrefix)
 697
 698    originPrefix = "origin/p4/"
 699
 700    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 701        line = line.strip()
 702        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
 703            continue
 704
 705        headName = line[len(originPrefix):]
 706        remoteHead = localRefPrefix + headName
 707        originHead = line
 708
 709        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
 710        if (not original.has_key('depot-paths')
 711            or not original.has_key('change')):
 712            continue
 713
 714        update = False
 715        if not gitBranchExists(remoteHead):
 716            if verbose:
 717                print "creating %s" % remoteHead
 718            update = True
 719        else:
 720            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
 721            if settings.has_key('change') > 0:
 722                if settings['depot-paths'] == original['depot-paths']:
 723                    originP4Change = int(original['change'])
 724                    p4Change = int(settings['change'])
 725                    if originP4Change > p4Change:
 726                        print ("%s (%s) is newer than %s (%s). "
 727                               "Updating p4 branch from origin."
 728                               % (originHead, originP4Change,
 729                                  remoteHead, p4Change))
 730                        update = True
 731                else:
 732                    print ("Ignoring: %s was imported from %s while "
 733                           "%s was imported from %s"
 734                           % (originHead, ','.join(original['depot-paths']),
 735                              remoteHead, ','.join(settings['depot-paths'])))
 736
 737        if update:
 738            system("git update-ref %s %s" % (remoteHead, originHead))
 739
 740def originP4BranchesExist():
 741        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
 742
 743def p4ChangesForPaths(depotPaths, changeRange):
 744    assert depotPaths
 745    cmd = ['changes']
 746    for p in depotPaths:
 747        cmd += ["%s...%s" % (p, changeRange)]
 748    output = p4_read_pipe_lines(cmd)
 749
 750    changes = {}
 751    for line in output:
 752        changeNum = int(line.split(" ")[1])
 753        changes[changeNum] = True
 754
 755    changelist = changes.keys()
 756    changelist.sort()
 757    return changelist
 758
 759def p4PathStartsWith(path, prefix):
 760    # This method tries to remedy a potential mixed-case issue:
 761    #
 762    # If UserA adds  //depot/DirA/file1
 763    # and UserB adds //depot/dira/file2
 764    #
 765    # we may or may not have a problem. If you have core.ignorecase=true,
 766    # we treat DirA and dira as the same directory
 767    if gitConfigBool("core.ignorecase"):
 768        return path.lower().startswith(prefix.lower())
 769    return path.startswith(prefix)
 770
 771def getClientSpec():
 772    """Look at the p4 client spec, create a View() object that contains
 773       all the mappings, and return it."""
 774
 775    specList = p4CmdList("client -o")
 776    if len(specList) != 1:
 777        die('Output from "client -o" is %d lines, expecting 1' %
 778            len(specList))
 779
 780    # dictionary of all client parameters
 781    entry = specList[0]
 782
 783    # the //client/ name
 784    client_name = entry["Client"]
 785
 786    # just the keys that start with "View"
 787    view_keys = [ k for k in entry.keys() if k.startswith("View") ]
 788
 789    # hold this new View
 790    view = View(client_name)
 791
 792    # append the lines, in order, to the view
 793    for view_num in range(len(view_keys)):
 794        k = "View%d" % view_num
 795        if k not in view_keys:
 796            die("Expected view key %s missing" % k)
 797        view.append(entry[k])
 798
 799    return view
 800
 801def getClientRoot():
 802    """Grab the client directory."""
 803
 804    output = p4CmdList("client -o")
 805    if len(output) != 1:
 806        die('Output from "client -o" is %d lines, expecting 1' % len(output))
 807
 808    entry = output[0]
 809    if "Root" not in entry:
 810        die('Client has no "Root"')
 811
 812    return entry["Root"]
 813
 814#
 815# P4 wildcards are not allowed in filenames.  P4 complains
 816# if you simply add them, but you can force it with "-f", in
 817# which case it translates them into %xx encoding internally.
 818#
 819def wildcard_decode(path):
 820    # Search for and fix just these four characters.  Do % last so
 821    # that fixing it does not inadvertently create new %-escapes.
 822    # Cannot have * in a filename in windows; untested as to
 823    # what p4 would do in such a case.
 824    if not platform.system() == "Windows":
 825        path = path.replace("%2A", "*")
 826    path = path.replace("%23", "#") \
 827               .replace("%40", "@") \
 828               .replace("%25", "%")
 829    return path
 830
 831def wildcard_encode(path):
 832    # do % first to avoid double-encoding the %s introduced here
 833    path = path.replace("%", "%25") \
 834               .replace("*", "%2A") \
 835               .replace("#", "%23") \
 836               .replace("@", "%40")
 837    return path
 838
 839def wildcard_present(path):
 840    m = re.search("[*#@%]", path)
 841    return m is not None
 842
 843class Command:
 844    def __init__(self):
 845        self.usage = "usage: %prog [options]"
 846        self.needsGit = True
 847        self.verbose = False
 848
 849class P4UserMap:
 850    def __init__(self):
 851        self.userMapFromPerforceServer = False
 852        self.myP4UserId = None
 853
 854    def p4UserId(self):
 855        if self.myP4UserId:
 856            return self.myP4UserId
 857
 858        results = p4CmdList("user -o")
 859        for r in results:
 860            if r.has_key('User'):
 861                self.myP4UserId = r['User']
 862                return r['User']
 863        die("Could not find your p4 user id")
 864
 865    def p4UserIsMe(self, p4User):
 866        # return True if the given p4 user is actually me
 867        me = self.p4UserId()
 868        if not p4User or p4User != me:
 869            return False
 870        else:
 871            return True
 872
 873    def getUserCacheFilename(self):
 874        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
 875        return home + "/.gitp4-usercache.txt"
 876
 877    def getUserMapFromPerforceServer(self):
 878        if self.userMapFromPerforceServer:
 879            return
 880        self.users = {}
 881        self.emails = {}
 882
 883        for output in p4CmdList("users"):
 884            if not output.has_key("User"):
 885                continue
 886            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
 887            self.emails[output["Email"]] = output["User"]
 888
 889
 890        s = ''
 891        for (key, val) in self.users.items():
 892            s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
 893
 894        open(self.getUserCacheFilename(), "wb").write(s)
 895        self.userMapFromPerforceServer = True
 896
 897    def loadUserMapFromCache(self):
 898        self.users = {}
 899        self.userMapFromPerforceServer = False
 900        try:
 901            cache = open(self.getUserCacheFilename(), "rb")
 902            lines = cache.readlines()
 903            cache.close()
 904            for line in lines:
 905                entry = line.strip().split("\t")
 906                self.users[entry[0]] = entry[1]
 907        except IOError:
 908            self.getUserMapFromPerforceServer()
 909
 910class P4Debug(Command):
 911    def __init__(self):
 912        Command.__init__(self)
 913        self.options = []
 914        self.description = "A tool to debug the output of p4 -G."
 915        self.needsGit = False
 916
 917    def run(self, args):
 918        j = 0
 919        for output in p4CmdList(args):
 920            print 'Element: %d' % j
 921            j += 1
 922            print output
 923        return True
 924
 925class P4RollBack(Command):
 926    def __init__(self):
 927        Command.__init__(self)
 928        self.options = [
 929            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
 930        ]
 931        self.description = "A tool to debug the multi-branch import. Don't use :)"
 932        self.rollbackLocalBranches = False
 933
 934    def run(self, args):
 935        if len(args) != 1:
 936            return False
 937        maxChange = int(args[0])
 938
 939        if "p4ExitCode" in p4Cmd("changes -m 1"):
 940            die("Problems executing p4");
 941
 942        if self.rollbackLocalBranches:
 943            refPrefix = "refs/heads/"
 944            lines = read_pipe_lines("git rev-parse --symbolic --branches")
 945        else:
 946            refPrefix = "refs/remotes/"
 947            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
 948
 949        for line in lines:
 950            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
 951                line = line.strip()
 952                ref = refPrefix + line
 953                log = extractLogMessageFromGitCommit(ref)
 954                settings = extractSettingsGitLog(log)
 955
 956                depotPaths = settings['depot-paths']
 957                change = settings['change']
 958
 959                changed = False
 960
 961                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
 962                                                           for p in depotPaths]))) == 0:
 963                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
 964                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
 965                    continue
 966
 967                while change and int(change) > maxChange:
 968                    changed = True
 969                    if self.verbose:
 970                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
 971                    system("git update-ref %s \"%s^\"" % (ref, ref))
 972                    log = extractLogMessageFromGitCommit(ref)
 973                    settings =  extractSettingsGitLog(log)
 974
 975
 976                    depotPaths = settings['depot-paths']
 977                    change = settings['change']
 978
 979                if changed:
 980                    print "%s rewound to %s" % (ref, change)
 981
 982        return True
 983
 984class P4Submit(Command, P4UserMap):
 985
 986    conflict_behavior_choices = ("ask", "skip", "quit")
 987
 988    def __init__(self):
 989        Command.__init__(self)
 990        P4UserMap.__init__(self)
 991        self.options = [
 992                optparse.make_option("--origin", dest="origin"),
 993                optparse.make_option("-M", dest="detectRenames", action="store_true"),
 994                # preserve the user, requires relevant p4 permissions
 995                optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
 996                optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
 997                optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
 998                optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
 999                optparse.make_option("--conflict", dest="conflict_behavior",
1000                                     choices=self.conflict_behavior_choices),
1001                optparse.make_option("--branch", dest="branch"),
1002        ]
1003        self.description = "Submit changes from git to the perforce depot."
1004        self.usage += " [name of git branch to submit into perforce depot]"
1005        self.origin = ""
1006        self.detectRenames = False
1007        self.preserveUser = gitConfigBool("git-p4.preserveUser")
1008        self.dry_run = False
1009        self.prepare_p4_only = False
1010        self.conflict_behavior = None
1011        self.isWindows = (platform.system() == "Windows")
1012        self.exportLabels = False
1013        self.p4HasMoveCommand = p4_has_move_command()
1014        self.branch = None
1015
1016    def check(self):
1017        if len(p4CmdList("opened ...")) > 0:
1018            die("You have files opened with perforce! Close them before starting the sync.")
1019
1020    def separate_jobs_from_description(self, message):
1021        """Extract and return a possible Jobs field in the commit
1022           message.  It goes into a separate section in the p4 change
1023           specification.
1024
1025           A jobs line starts with "Jobs:" and looks like a new field
1026           in a form.  Values are white-space separated on the same
1027           line or on following lines that start with a tab.
1028
1029           This does not parse and extract the full git commit message
1030           like a p4 form.  It just sees the Jobs: line as a marker
1031           to pass everything from then on directly into the p4 form,
1032           but outside the description section.
1033
1034           Return a tuple (stripped log message, jobs string)."""
1035
1036        m = re.search(r'^Jobs:', message, re.MULTILINE)
1037        if m is None:
1038            return (message, None)
1039
1040        jobtext = message[m.start():]
1041        stripped_message = message[:m.start()].rstrip()
1042        return (stripped_message, jobtext)
1043
1044    def prepareLogMessage(self, template, message, jobs):
1045        """Edits the template returned from "p4 change -o" to insert
1046           the message in the Description field, and the jobs text in
1047           the Jobs field."""
1048        result = ""
1049
1050        inDescriptionSection = False
1051
1052        for line in template.split("\n"):
1053            if line.startswith("#"):
1054                result += line + "\n"
1055                continue
1056
1057            if inDescriptionSection:
1058                if line.startswith("Files:") or line.startswith("Jobs:"):
1059                    inDescriptionSection = False
1060                    # insert Jobs section
1061                    if jobs:
1062                        result += jobs + "\n"
1063                else:
1064                    continue
1065            else:
1066                if line.startswith("Description:"):
1067                    inDescriptionSection = True
1068                    line += "\n"
1069                    for messageLine in message.split("\n"):
1070                        line += "\t" + messageLine + "\n"
1071
1072            result += line + "\n"
1073
1074        return result
1075
1076    def patchRCSKeywords(self, file, pattern):
1077        # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1078        (handle, outFileName) = tempfile.mkstemp(dir='.')
1079        try:
1080            outFile = os.fdopen(handle, "w+")
1081            inFile = open(file, "r")
1082            regexp = re.compile(pattern, re.VERBOSE)
1083            for line in inFile.readlines():
1084                line = regexp.sub(r'$\1$', line)
1085                outFile.write(line)
1086            inFile.close()
1087            outFile.close()
1088            # Forcibly overwrite the original file
1089            os.unlink(file)
1090            shutil.move(outFileName, file)
1091        except:
1092            # cleanup our temporary file
1093            os.unlink(outFileName)
1094            print "Failed to strip RCS keywords in %s" % file
1095            raise
1096
1097        print "Patched up RCS keywords in %s" % file
1098
1099    def p4UserForCommit(self,id):
1100        # Return the tuple (perforce user,git email) for a given git commit id
1101        self.getUserMapFromPerforceServer()
1102        gitEmail = read_pipe(["git", "log", "--max-count=1",
1103                              "--format=%ae", id])
1104        gitEmail = gitEmail.strip()
1105        if not self.emails.has_key(gitEmail):
1106            return (None,gitEmail)
1107        else:
1108            return (self.emails[gitEmail],gitEmail)
1109
1110    def checkValidP4Users(self,commits):
1111        # check if any git authors cannot be mapped to p4 users
1112        for id in commits:
1113            (user,email) = self.p4UserForCommit(id)
1114            if not user:
1115                msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1116                if gitConfigBool("git-p4.allowMissingP4Users"):
1117                    print "%s" % msg
1118                else:
1119                    die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1120
1121    def lastP4Changelist(self):
1122        # Get back the last changelist number submitted in this client spec. This
1123        # then gets used to patch up the username in the change. If the same
1124        # client spec is being used by multiple processes then this might go
1125        # wrong.
1126        results = p4CmdList("client -o")        # find the current client
1127        client = None
1128        for r in results:
1129            if r.has_key('Client'):
1130                client = r['Client']
1131                break
1132        if not client:
1133            die("could not get client spec")
1134        results = p4CmdList(["changes", "-c", client, "-m", "1"])
1135        for r in results:
1136            if r.has_key('change'):
1137                return r['change']
1138        die("Could not get changelist number for last submit - cannot patch up user details")
1139
1140    def modifyChangelistUser(self, changelist, newUser):
1141        # fixup the user field of a changelist after it has been submitted.
1142        changes = p4CmdList("change -o %s" % changelist)
1143        if len(changes) != 1:
1144            die("Bad output from p4 change modifying %s to user %s" %
1145                (changelist, newUser))
1146
1147        c = changes[0]
1148        if c['User'] == newUser: return   # nothing to do
1149        c['User'] = newUser
1150        input = marshal.dumps(c)
1151
1152        result = p4CmdList("change -f -i", stdin=input)
1153        for r in result:
1154            if r.has_key('code'):
1155                if r['code'] == 'error':
1156                    die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1157            if r.has_key('data'):
1158                print("Updated user field for changelist %s to %s" % (changelist, newUser))
1159                return
1160        die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1161
1162    def canChangeChangelists(self):
1163        # check to see if we have p4 admin or super-user permissions, either of
1164        # which are required to modify changelists.
1165        results = p4CmdList(["protects", self.depotPath])
1166        for r in results:
1167            if r.has_key('perm'):
1168                if r['perm'] == 'admin':
1169                    return 1
1170                if r['perm'] == 'super':
1171                    return 1
1172        return 0
1173
1174    def prepareSubmitTemplate(self):
1175        """Run "p4 change -o" to grab a change specification template.
1176           This does not use "p4 -G", as it is nice to keep the submission
1177           template in original order, since a human might edit it.
1178
1179           Remove lines in the Files section that show changes to files
1180           outside the depot path we're committing into."""
1181
1182        template = ""
1183        inFilesSection = False
1184        for line in p4_read_pipe_lines(['change', '-o']):
1185            if line.endswith("\r\n"):
1186                line = line[:-2] + "\n"
1187            if inFilesSection:
1188                if line.startswith("\t"):
1189                    # path starts and ends with a tab
1190                    path = line[1:]
1191                    lastTab = path.rfind("\t")
1192                    if lastTab != -1:
1193                        path = path[:lastTab]
1194                        if not p4PathStartsWith(path, self.depotPath):
1195                            continue
1196                else:
1197                    inFilesSection = False
1198            else:
1199                if line.startswith("Files:"):
1200                    inFilesSection = True
1201
1202            template += line
1203
1204        return template
1205
1206    def edit_template(self, template_file):
1207        """Invoke the editor to let the user change the submission
1208           message.  Return true if okay to continue with the submit."""
1209
1210        # if configured to skip the editing part, just submit
1211        if gitConfigBool("git-p4.skipSubmitEdit"):
1212            return True
1213
1214        # look at the modification time, to check later if the user saved
1215        # the file
1216        mtime = os.stat(template_file).st_mtime
1217
1218        # invoke the editor
1219        if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1220            editor = os.environ.get("P4EDITOR")
1221        else:
1222            editor = read_pipe("git var GIT_EDITOR").strip()
1223        system([editor, template_file])
1224
1225        # If the file was not saved, prompt to see if this patch should
1226        # be skipped.  But skip this verification step if configured so.
1227        if gitConfigBool("git-p4.skipSubmitEditCheck"):
1228            return True
1229
1230        # modification time updated means user saved the file
1231        if os.stat(template_file).st_mtime > mtime:
1232            return True
1233
1234        while True:
1235            response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1236            if response == 'y':
1237                return True
1238            if response == 'n':
1239                return False
1240
1241    def get_diff_description(self, editedFiles, filesToAdd):
1242        # diff
1243        if os.environ.has_key("P4DIFF"):
1244            del(os.environ["P4DIFF"])
1245        diff = ""
1246        for editedFile in editedFiles:
1247            diff += p4_read_pipe(['diff', '-du',
1248                                  wildcard_encode(editedFile)])
1249
1250        # new file diff
1251        newdiff = ""
1252        for newFile in filesToAdd:
1253            newdiff += "==== new file ====\n"
1254            newdiff += "--- /dev/null\n"
1255            newdiff += "+++ %s\n" % newFile
1256            f = open(newFile, "r")
1257            for line in f.readlines():
1258                newdiff += "+" + line
1259            f.close()
1260
1261        return (diff + newdiff).replace('\r\n', '\n')
1262
1263    def applyCommit(self, id):
1264        """Apply one commit, return True if it succeeded."""
1265
1266        print "Applying", read_pipe(["git", "show", "-s",
1267                                     "--format=format:%h %s", id])
1268
1269        (p4User, gitEmail) = self.p4UserForCommit(id)
1270
1271        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1272        filesToAdd = set()
1273        filesToDelete = set()
1274        editedFiles = set()
1275        pureRenameCopy = set()
1276        filesToChangeExecBit = {}
1277
1278        for line in diff:
1279            diff = parseDiffTreeEntry(line)
1280            modifier = diff['status']
1281            path = diff['src']
1282            if modifier == "M":
1283                p4_edit(path)
1284                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1285                    filesToChangeExecBit[path] = diff['dst_mode']
1286                editedFiles.add(path)
1287            elif modifier == "A":
1288                filesToAdd.add(path)
1289                filesToChangeExecBit[path] = diff['dst_mode']
1290                if path in filesToDelete:
1291                    filesToDelete.remove(path)
1292            elif modifier == "D":
1293                filesToDelete.add(path)
1294                if path in filesToAdd:
1295                    filesToAdd.remove(path)
1296            elif modifier == "C":
1297                src, dest = diff['src'], diff['dst']
1298                p4_integrate(src, dest)
1299                pureRenameCopy.add(dest)
1300                if diff['src_sha1'] != diff['dst_sha1']:
1301                    p4_edit(dest)
1302                    pureRenameCopy.discard(dest)
1303                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1304                    p4_edit(dest)
1305                    pureRenameCopy.discard(dest)
1306                    filesToChangeExecBit[dest] = diff['dst_mode']
1307                if self.isWindows:
1308                    # turn off read-only attribute
1309                    os.chmod(dest, stat.S_IWRITE)
1310                os.unlink(dest)
1311                editedFiles.add(dest)
1312            elif modifier == "R":
1313                src, dest = diff['src'], diff['dst']
1314                if self.p4HasMoveCommand:
1315                    p4_edit(src)        # src must be open before move
1316                    p4_move(src, dest)  # opens for (move/delete, move/add)
1317                else:
1318                    p4_integrate(src, dest)
1319                    if diff['src_sha1'] != diff['dst_sha1']:
1320                        p4_edit(dest)
1321                    else:
1322                        pureRenameCopy.add(dest)
1323                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1324                    if not self.p4HasMoveCommand:
1325                        p4_edit(dest)   # with move: already open, writable
1326                    filesToChangeExecBit[dest] = diff['dst_mode']
1327                if not self.p4HasMoveCommand:
1328                    if self.isWindows:
1329                        os.chmod(dest, stat.S_IWRITE)
1330                    os.unlink(dest)
1331                    filesToDelete.add(src)
1332                editedFiles.add(dest)
1333            else:
1334                die("unknown modifier %s for %s" % (modifier, path))
1335
1336        diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1337        patchcmd = diffcmd + " | git apply "
1338        tryPatchCmd = patchcmd + "--check -"
1339        applyPatchCmd = patchcmd + "--check --apply -"
1340        patch_succeeded = True
1341
1342        if os.system(tryPatchCmd) != 0:
1343            fixed_rcs_keywords = False
1344            patch_succeeded = False
1345            print "Unfortunately applying the change failed!"
1346
1347            # Patch failed, maybe it's just RCS keyword woes. Look through
1348            # the patch to see if that's possible.
1349            if gitConfigBool("git-p4.attemptRCSCleanup"):
1350                file = None
1351                pattern = None
1352                kwfiles = {}
1353                for file in editedFiles | filesToDelete:
1354                    # did this file's delta contain RCS keywords?
1355                    pattern = p4_keywords_regexp_for_file(file)
1356
1357                    if pattern:
1358                        # this file is a possibility...look for RCS keywords.
1359                        regexp = re.compile(pattern, re.VERBOSE)
1360                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1361                            if regexp.search(line):
1362                                if verbose:
1363                                    print "got keyword match on %s in %s in %s" % (pattern, line, file)
1364                                kwfiles[file] = pattern
1365                                break
1366
1367                for file in kwfiles:
1368                    if verbose:
1369                        print "zapping %s with %s" % (line,pattern)
1370                    # File is being deleted, so not open in p4.  Must
1371                    # disable the read-only bit on windows.
1372                    if self.isWindows and file not in editedFiles:
1373                        os.chmod(file, stat.S_IWRITE)
1374                    self.patchRCSKeywords(file, kwfiles[file])
1375                    fixed_rcs_keywords = True
1376
1377            if fixed_rcs_keywords:
1378                print "Retrying the patch with RCS keywords cleaned up"
1379                if os.system(tryPatchCmd) == 0:
1380                    patch_succeeded = True
1381
1382        if not patch_succeeded:
1383            for f in editedFiles:
1384                p4_revert(f)
1385            return False
1386
1387        #
1388        # Apply the patch for real, and do add/delete/+x handling.
1389        #
1390        system(applyPatchCmd)
1391
1392        for f in filesToAdd:
1393            p4_add(f)
1394        for f in filesToDelete:
1395            p4_revert(f)
1396            p4_delete(f)
1397
1398        # Set/clear executable bits
1399        for f in filesToChangeExecBit.keys():
1400            mode = filesToChangeExecBit[f]
1401            setP4ExecBit(f, mode)
1402
1403        #
1404        # Build p4 change description, starting with the contents
1405        # of the git commit message.
1406        #
1407        logMessage = extractLogMessageFromGitCommit(id)
1408        logMessage = logMessage.strip()
1409        (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1410
1411        template = self.prepareSubmitTemplate()
1412        submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1413
1414        if self.preserveUser:
1415           submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1416
1417        if self.checkAuthorship and not self.p4UserIsMe(p4User):
1418            submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1419            submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1420            submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1421
1422        separatorLine = "######## everything below this line is just the diff #######\n"
1423        if not self.prepare_p4_only:
1424            submitTemplate += separatorLine
1425            submitTemplate += self.get_diff_description(editedFiles, filesToAdd)
1426
1427        (handle, fileName) = tempfile.mkstemp()
1428        tmpFile = os.fdopen(handle, "w+b")
1429        if self.isWindows:
1430            submitTemplate = submitTemplate.replace("\n", "\r\n")
1431        tmpFile.write(submitTemplate)
1432        tmpFile.close()
1433
1434        if self.prepare_p4_only:
1435            #
1436            # Leave the p4 tree prepared, and the submit template around
1437            # and let the user decide what to do next
1438            #
1439            print
1440            print "P4 workspace prepared for submission."
1441            print "To submit or revert, go to client workspace"
1442            print "  " + self.clientPath
1443            print
1444            print "To submit, use \"p4 submit\" to write a new description,"
1445            print "or \"p4 submit -i %s\" to use the one prepared by" \
1446                  " \"git p4\"." % fileName
1447            print "You can delete the file \"%s\" when finished." % fileName
1448
1449            if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1450                print "To preserve change ownership by user %s, you must\n" \
1451                      "do \"p4 change -f <change>\" after submitting and\n" \
1452                      "edit the User field."
1453            if pureRenameCopy:
1454                print "After submitting, renamed files must be re-synced."
1455                print "Invoke \"p4 sync -f\" on each of these files:"
1456                for f in pureRenameCopy:
1457                    print "  " + f
1458
1459            print
1460            print "To revert the changes, use \"p4 revert ...\", and delete"
1461            print "the submit template file \"%s\"" % fileName
1462            if filesToAdd:
1463                print "Since the commit adds new files, they must be deleted:"
1464                for f in filesToAdd:
1465                    print "  " + f
1466            print
1467            return True
1468
1469        #
1470        # Let the user edit the change description, then submit it.
1471        #
1472        if self.edit_template(fileName):
1473            # read the edited message and submit
1474            ret = True
1475            tmpFile = open(fileName, "rb")
1476            message = tmpFile.read()
1477            tmpFile.close()
1478            if self.isWindows:
1479                message = message.replace("\r\n", "\n")
1480            submitTemplate = message[:message.index(separatorLine)]
1481            p4_write_pipe(['submit', '-i'], submitTemplate)
1482
1483            if self.preserveUser:
1484                if p4User:
1485                    # Get last changelist number. Cannot easily get it from
1486                    # the submit command output as the output is
1487                    # unmarshalled.
1488                    changelist = self.lastP4Changelist()
1489                    self.modifyChangelistUser(changelist, p4User)
1490
1491            # The rename/copy happened by applying a patch that created a
1492            # new file.  This leaves it writable, which confuses p4.
1493            for f in pureRenameCopy:
1494                p4_sync(f, "-f")
1495
1496        else:
1497            # skip this patch
1498            ret = False
1499            print "Submission cancelled, undoing p4 changes."
1500            for f in editedFiles:
1501                p4_revert(f)
1502            for f in filesToAdd:
1503                p4_revert(f)
1504                os.remove(f)
1505            for f in filesToDelete:
1506                p4_revert(f)
1507
1508        os.remove(fileName)
1509        return ret
1510
1511    # Export git tags as p4 labels. Create a p4 label and then tag
1512    # with that.
1513    def exportGitTags(self, gitTags):
1514        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1515        if len(validLabelRegexp) == 0:
1516            validLabelRegexp = defaultLabelRegexp
1517        m = re.compile(validLabelRegexp)
1518
1519        for name in gitTags:
1520
1521            if not m.match(name):
1522                if verbose:
1523                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1524                continue
1525
1526            # Get the p4 commit this corresponds to
1527            logMessage = extractLogMessageFromGitCommit(name)
1528            values = extractSettingsGitLog(logMessage)
1529
1530            if not values.has_key('change'):
1531                # a tag pointing to something not sent to p4; ignore
1532                if verbose:
1533                    print "git tag %s does not give a p4 commit" % name
1534                continue
1535            else:
1536                changelist = values['change']
1537
1538            # Get the tag details.
1539            inHeader = True
1540            isAnnotated = False
1541            body = []
1542            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1543                l = l.strip()
1544                if inHeader:
1545                    if re.match(r'tag\s+', l):
1546                        isAnnotated = True
1547                    elif re.match(r'\s*$', l):
1548                        inHeader = False
1549                        continue
1550                else:
1551                    body.append(l)
1552
1553            if not isAnnotated:
1554                body = ["lightweight tag imported by git p4\n"]
1555
1556            # Create the label - use the same view as the client spec we are using
1557            clientSpec = getClientSpec()
1558
1559            labelTemplate  = "Label: %s\n" % name
1560            labelTemplate += "Description:\n"
1561            for b in body:
1562                labelTemplate += "\t" + b + "\n"
1563            labelTemplate += "View:\n"
1564            for depot_side in clientSpec.mappings:
1565                labelTemplate += "\t%s\n" % depot_side
1566
1567            if self.dry_run:
1568                print "Would create p4 label %s for tag" % name
1569            elif self.prepare_p4_only:
1570                print "Not creating p4 label %s for tag due to option" \
1571                      " --prepare-p4-only" % name
1572            else:
1573                p4_write_pipe(["label", "-i"], labelTemplate)
1574
1575                # Use the label
1576                p4_system(["tag", "-l", name] +
1577                          ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1578
1579                if verbose:
1580                    print "created p4 label for tag %s" % name
1581
1582    def run(self, args):
1583        if len(args) == 0:
1584            self.master = currentGitBranch()
1585            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1586                die("Detecting current git branch failed!")
1587        elif len(args) == 1:
1588            self.master = args[0]
1589            if not branchExists(self.master):
1590                die("Branch %s does not exist" % self.master)
1591        else:
1592            return False
1593
1594        allowSubmit = gitConfig("git-p4.allowSubmit")
1595        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1596            die("%s is not in git-p4.allowSubmit" % self.master)
1597
1598        [upstream, settings] = findUpstreamBranchPoint()
1599        self.depotPath = settings['depot-paths'][0]
1600        if len(self.origin) == 0:
1601            self.origin = upstream
1602
1603        if self.preserveUser:
1604            if not self.canChangeChangelists():
1605                die("Cannot preserve user names without p4 super-user or admin permissions")
1606
1607        # if not set from the command line, try the config file
1608        if self.conflict_behavior is None:
1609            val = gitConfig("git-p4.conflict")
1610            if val:
1611                if val not in self.conflict_behavior_choices:
1612                    die("Invalid value '%s' for config git-p4.conflict" % val)
1613            else:
1614                val = "ask"
1615            self.conflict_behavior = val
1616
1617        if self.verbose:
1618            print "Origin branch is " + self.origin
1619
1620        if len(self.depotPath) == 0:
1621            print "Internal error: cannot locate perforce depot path from existing branches"
1622            sys.exit(128)
1623
1624        self.useClientSpec = False
1625        if gitConfigBool("git-p4.useclientspec"):
1626            self.useClientSpec = True
1627        if self.useClientSpec:
1628            self.clientSpecDirs = getClientSpec()
1629
1630        if self.useClientSpec:
1631            # all files are relative to the client spec
1632            self.clientPath = getClientRoot()
1633        else:
1634            self.clientPath = p4Where(self.depotPath)
1635
1636        if self.clientPath == "":
1637            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1638
1639        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1640        self.oldWorkingDirectory = os.getcwd()
1641
1642        # ensure the clientPath exists
1643        new_client_dir = False
1644        if not os.path.exists(self.clientPath):
1645            new_client_dir = True
1646            os.makedirs(self.clientPath)
1647
1648        chdir(self.clientPath, is_client_path=True)
1649        if self.dry_run:
1650            print "Would synchronize p4 checkout in %s" % self.clientPath
1651        else:
1652            print "Synchronizing p4 checkout..."
1653            if new_client_dir:
1654                # old one was destroyed, and maybe nobody told p4
1655                p4_sync("...", "-f")
1656            else:
1657                p4_sync("...")
1658        self.check()
1659
1660        commits = []
1661        for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, self.master)]):
1662            commits.append(line.strip())
1663        commits.reverse()
1664
1665        if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1666            self.checkAuthorship = False
1667        else:
1668            self.checkAuthorship = True
1669
1670        if self.preserveUser:
1671            self.checkValidP4Users(commits)
1672
1673        #
1674        # Build up a set of options to be passed to diff when
1675        # submitting each commit to p4.
1676        #
1677        if self.detectRenames:
1678            # command-line -M arg
1679            self.diffOpts = "-M"
1680        else:
1681            # If not explicitly set check the config variable
1682            detectRenames = gitConfig("git-p4.detectRenames")
1683
1684            if detectRenames.lower() == "false" or detectRenames == "":
1685                self.diffOpts = ""
1686            elif detectRenames.lower() == "true":
1687                self.diffOpts = "-M"
1688            else:
1689                self.diffOpts = "-M%s" % detectRenames
1690
1691        # no command-line arg for -C or --find-copies-harder, just
1692        # config variables
1693        detectCopies = gitConfig("git-p4.detectCopies")
1694        if detectCopies.lower() == "false" or detectCopies == "":
1695            pass
1696        elif detectCopies.lower() == "true":
1697            self.diffOpts += " -C"
1698        else:
1699            self.diffOpts += " -C%s" % detectCopies
1700
1701        if gitConfigBool("git-p4.detectCopiesHarder"):
1702            self.diffOpts += " --find-copies-harder"
1703
1704        #
1705        # Apply the commits, one at a time.  On failure, ask if should
1706        # continue to try the rest of the patches, or quit.
1707        #
1708        if self.dry_run:
1709            print "Would apply"
1710        applied = []
1711        last = len(commits) - 1
1712        for i, commit in enumerate(commits):
1713            if self.dry_run:
1714                print " ", read_pipe(["git", "show", "-s",
1715                                      "--format=format:%h %s", commit])
1716                ok = True
1717            else:
1718                ok = self.applyCommit(commit)
1719            if ok:
1720                applied.append(commit)
1721            else:
1722                if self.prepare_p4_only and i < last:
1723                    print "Processing only the first commit due to option" \
1724                          " --prepare-p4-only"
1725                    break
1726                if i < last:
1727                    quit = False
1728                    while True:
1729                        # prompt for what to do, or use the option/variable
1730                        if self.conflict_behavior == "ask":
1731                            print "What do you want to do?"
1732                            response = raw_input("[s]kip this commit but apply"
1733                                                 " the rest, or [q]uit? ")
1734                            if not response:
1735                                continue
1736                        elif self.conflict_behavior == "skip":
1737                            response = "s"
1738                        elif self.conflict_behavior == "quit":
1739                            response = "q"
1740                        else:
1741                            die("Unknown conflict_behavior '%s'" %
1742                                self.conflict_behavior)
1743
1744                        if response[0] == "s":
1745                            print "Skipping this commit, but applying the rest"
1746                            break
1747                        if response[0] == "q":
1748                            print "Quitting"
1749                            quit = True
1750                            break
1751                    if quit:
1752                        break
1753
1754        chdir(self.oldWorkingDirectory)
1755
1756        if self.dry_run:
1757            pass
1758        elif self.prepare_p4_only:
1759            pass
1760        elif len(commits) == len(applied):
1761            print "All commits applied!"
1762
1763            sync = P4Sync()
1764            if self.branch:
1765                sync.branch = self.branch
1766            sync.run([])
1767
1768            rebase = P4Rebase()
1769            rebase.rebase()
1770
1771        else:
1772            if len(applied) == 0:
1773                print "No commits applied."
1774            else:
1775                print "Applied only the commits marked with '*':"
1776                for c in commits:
1777                    if c in applied:
1778                        star = "*"
1779                    else:
1780                        star = " "
1781                    print star, read_pipe(["git", "show", "-s",
1782                                           "--format=format:%h %s",  c])
1783                print "You will have to do 'git p4 sync' and rebase."
1784
1785        if gitConfigBool("git-p4.exportLabels"):
1786            self.exportLabels = True
1787
1788        if self.exportLabels:
1789            p4Labels = getP4Labels(self.depotPath)
1790            gitTags = getGitTags()
1791
1792            missingGitTags = gitTags - p4Labels
1793            self.exportGitTags(missingGitTags)
1794
1795        # exit with error unless everything applied perfectly
1796        if len(commits) != len(applied):
1797                sys.exit(1)
1798
1799        return True
1800
1801class View(object):
1802    """Represent a p4 view ("p4 help views"), and map files in a
1803       repo according to the view."""
1804
1805    def __init__(self, client_name):
1806        self.mappings = []
1807        self.client_prefix = "//%s/" % client_name
1808        # cache results of "p4 where" to lookup client file locations
1809        self.client_spec_path_cache = {}
1810
1811    def append(self, view_line):
1812        """Parse a view line, splitting it into depot and client
1813           sides.  Append to self.mappings, preserving order.  This
1814           is only needed for tag creation."""
1815
1816        # Split the view line into exactly two words.  P4 enforces
1817        # structure on these lines that simplifies this quite a bit.
1818        #
1819        # Either or both words may be double-quoted.
1820        # Single quotes do not matter.
1821        # Double-quote marks cannot occur inside the words.
1822        # A + or - prefix is also inside the quotes.
1823        # There are no quotes unless they contain a space.
1824        # The line is already white-space stripped.
1825        # The two words are separated by a single space.
1826        #
1827        if view_line[0] == '"':
1828            # First word is double quoted.  Find its end.
1829            close_quote_index = view_line.find('"', 1)
1830            if close_quote_index <= 0:
1831                die("No first-word closing quote found: %s" % view_line)
1832            depot_side = view_line[1:close_quote_index]
1833            # skip closing quote and space
1834            rhs_index = close_quote_index + 1 + 1
1835        else:
1836            space_index = view_line.find(" ")
1837            if space_index <= 0:
1838                die("No word-splitting space found: %s" % view_line)
1839            depot_side = view_line[0:space_index]
1840            rhs_index = space_index + 1
1841
1842        # prefix + means overlay on previous mapping
1843        if depot_side.startswith("+"):
1844            depot_side = depot_side[1:]
1845
1846        # prefix - means exclude this path, leave out of mappings
1847        exclude = False
1848        if depot_side.startswith("-"):
1849            exclude = True
1850            depot_side = depot_side[1:]
1851
1852        if not exclude:
1853            self.mappings.append(depot_side)
1854
1855    def convert_client_path(self, clientFile):
1856        # chop off //client/ part to make it relative
1857        if not clientFile.startswith(self.client_prefix):
1858            die("No prefix '%s' on clientFile '%s'" %
1859                (self.client_prefix, clientFile))
1860        return clientFile[len(self.client_prefix):]
1861
1862    def update_client_spec_path_cache(self, files):
1863        """ Caching file paths by "p4 where" batch query """
1864
1865        # List depot file paths exclude that already cached
1866        fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
1867
1868        if len(fileArgs) == 0:
1869            return  # All files in cache
1870
1871        where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
1872        for res in where_result:
1873            if "code" in res and res["code"] == "error":
1874                # assume error is "... file(s) not in client view"
1875                continue
1876            if "clientFile" not in res:
1877                die("No clientFile in 'p4 where' output")
1878            if "unmap" in res:
1879                # it will list all of them, but only one not unmap-ped
1880                continue
1881            self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
1882
1883        # not found files or unmap files set to ""
1884        for depotFile in fileArgs:
1885            if depotFile not in self.client_spec_path_cache:
1886                self.client_spec_path_cache[depotFile] = ""
1887
1888    def map_in_client(self, depot_path):
1889        """Return the relative location in the client where this
1890           depot file should live.  Returns "" if the file should
1891           not be mapped in the client."""
1892
1893        if depot_path in self.client_spec_path_cache:
1894            return self.client_spec_path_cache[depot_path]
1895
1896        die( "Error: %s is not found in client spec path" % depot_path )
1897        return ""
1898
1899class P4Sync(Command, P4UserMap):
1900    delete_actions = ( "delete", "move/delete", "purge" )
1901
1902    def __init__(self):
1903        Command.__init__(self)
1904        P4UserMap.__init__(self)
1905        self.options = [
1906                optparse.make_option("--branch", dest="branch"),
1907                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1908                optparse.make_option("--changesfile", dest="changesFile"),
1909                optparse.make_option("--silent", dest="silent", action="store_true"),
1910                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1911                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1912                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1913                                     help="Import into refs/heads/ , not refs/remotes"),
1914                optparse.make_option("--max-changes", dest="maxChanges"),
1915                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1916                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1917                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1918                                     help="Only sync files that are included in the Perforce Client Spec")
1919        ]
1920        self.description = """Imports from Perforce into a git repository.\n
1921    example:
1922    //depot/my/project/ -- to import the current head
1923    //depot/my/project/@all -- to import everything
1924    //depot/my/project/@1,6 -- to import only from revision 1 to 6
1925
1926    (a ... is not needed in the path p4 specification, it's added implicitly)"""
1927
1928        self.usage += " //depot/path[@revRange]"
1929        self.silent = False
1930        self.createdBranches = set()
1931        self.committedChanges = set()
1932        self.branch = ""
1933        self.detectBranches = False
1934        self.detectLabels = False
1935        self.importLabels = False
1936        self.changesFile = ""
1937        self.syncWithOrigin = True
1938        self.importIntoRemotes = True
1939        self.maxChanges = ""
1940        self.keepRepoPath = False
1941        self.depotPaths = None
1942        self.p4BranchesInGit = []
1943        self.cloneExclude = []
1944        self.useClientSpec = False
1945        self.useClientSpec_from_options = False
1946        self.clientSpecDirs = None
1947        self.tempBranches = []
1948        self.tempBranchLocation = "git-p4-tmp"
1949
1950        if gitConfig("git-p4.syncFromOrigin") == "false":
1951            self.syncWithOrigin = False
1952
1953    # Force a checkpoint in fast-import and wait for it to finish
1954    def checkpoint(self):
1955        self.gitStream.write("checkpoint\n\n")
1956        self.gitStream.write("progress checkpoint\n\n")
1957        out = self.gitOutput.readline()
1958        if self.verbose:
1959            print "checkpoint finished: " + out
1960
1961    def extractFilesFromCommit(self, commit):
1962        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1963                             for path in self.cloneExclude]
1964        files = []
1965        fnum = 0
1966        while commit.has_key("depotFile%s" % fnum):
1967            path =  commit["depotFile%s" % fnum]
1968
1969            if [p for p in self.cloneExclude
1970                if p4PathStartsWith(path, p)]:
1971                found = False
1972            else:
1973                found = [p for p in self.depotPaths
1974                         if p4PathStartsWith(path, p)]
1975            if not found:
1976                fnum = fnum + 1
1977                continue
1978
1979            file = {}
1980            file["path"] = path
1981            file["rev"] = commit["rev%s" % fnum]
1982            file["action"] = commit["action%s" % fnum]
1983            file["type"] = commit["type%s" % fnum]
1984            files.append(file)
1985            fnum = fnum + 1
1986        return files
1987
1988    def stripRepoPath(self, path, prefixes):
1989        """When streaming files, this is called to map a p4 depot path
1990           to where it should go in git.  The prefixes are either
1991           self.depotPaths, or self.branchPrefixes in the case of
1992           branch detection."""
1993
1994        if self.useClientSpec:
1995            # branch detection moves files up a level (the branch name)
1996            # from what client spec interpretation gives
1997            path = self.clientSpecDirs.map_in_client(path)
1998            if self.detectBranches:
1999                for b in self.knownBranches:
2000                    if path.startswith(b + "/"):
2001                        path = path[len(b)+1:]
2002
2003        elif self.keepRepoPath:
2004            # Preserve everything in relative path name except leading
2005            # //depot/; just look at first prefix as they all should
2006            # be in the same depot.
2007            depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2008            if p4PathStartsWith(path, depot):
2009                path = path[len(depot):]
2010
2011        else:
2012            for p in prefixes:
2013                if p4PathStartsWith(path, p):
2014                    path = path[len(p):]
2015                    break
2016
2017        path = wildcard_decode(path)
2018        return path
2019
2020    def splitFilesIntoBranches(self, commit):
2021        """Look at each depotFile in the commit to figure out to what
2022           branch it belongs."""
2023
2024        if self.clientSpecDirs:
2025            files = self.extractFilesFromCommit(commit)
2026            self.clientSpecDirs.update_client_spec_path_cache(files)
2027
2028        branches = {}
2029        fnum = 0
2030        while commit.has_key("depotFile%s" % fnum):
2031            path =  commit["depotFile%s" % fnum]
2032            found = [p for p in self.depotPaths
2033                     if p4PathStartsWith(path, p)]
2034            if not found:
2035                fnum = fnum + 1
2036                continue
2037
2038            file = {}
2039            file["path"] = path
2040            file["rev"] = commit["rev%s" % fnum]
2041            file["action"] = commit["action%s" % fnum]
2042            file["type"] = commit["type%s" % fnum]
2043            fnum = fnum + 1
2044
2045            # start with the full relative path where this file would
2046            # go in a p4 client
2047            if self.useClientSpec:
2048                relPath = self.clientSpecDirs.map_in_client(path)
2049            else:
2050                relPath = self.stripRepoPath(path, self.depotPaths)
2051
2052            for branch in self.knownBranches.keys():
2053                # add a trailing slash so that a commit into qt/4.2foo
2054                # doesn't end up in qt/4.2, e.g.
2055                if relPath.startswith(branch + "/"):
2056                    if branch not in branches:
2057                        branches[branch] = []
2058                    branches[branch].append(file)
2059                    break
2060
2061        return branches
2062
2063    # output one file from the P4 stream
2064    # - helper for streamP4Files
2065
2066    def streamOneP4File(self, file, contents):
2067        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2068        if verbose:
2069            sys.stderr.write("%s\n" % relPath)
2070
2071        (type_base, type_mods) = split_p4_type(file["type"])
2072
2073        git_mode = "100644"
2074        if "x" in type_mods:
2075            git_mode = "100755"
2076        if type_base == "symlink":
2077            git_mode = "120000"
2078            # p4 print on a symlink sometimes contains "target\n";
2079            # if it does, remove the newline
2080            data = ''.join(contents)
2081            if not data:
2082                # Some version of p4 allowed creating a symlink that pointed
2083                # to nothing.  This causes p4 errors when checking out such
2084                # a change, and errors here too.  Work around it by ignoring
2085                # the bad symlink; hopefully a future change fixes it.
2086                print "\nIgnoring empty symlink in %s" % file['depotFile']
2087                return
2088            elif data[-1] == '\n':
2089                contents = [data[:-1]]
2090            else:
2091                contents = [data]
2092
2093        if type_base == "utf16":
2094            # p4 delivers different text in the python output to -G
2095            # than it does when using "print -o", or normal p4 client
2096            # operations.  utf16 is converted to ascii or utf8, perhaps.
2097            # But ascii text saved as -t utf16 is completely mangled.
2098            # Invoke print -o to get the real contents.
2099            #
2100            # On windows, the newlines will always be mangled by print, so put
2101            # them back too.  This is not needed to the cygwin windows version,
2102            # just the native "NT" type.
2103            #
2104            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
2105            if p4_version_string().find("/NT") >= 0:
2106                text = text.replace("\r\n", "\n")
2107            contents = [ text ]
2108
2109        if type_base == "apple":
2110            # Apple filetype files will be streamed as a concatenation of
2111            # its appledouble header and the contents.  This is useless
2112            # on both macs and non-macs.  If using "print -q -o xx", it
2113            # will create "xx" with the data, and "%xx" with the header.
2114            # This is also not very useful.
2115            #
2116            # Ideally, someday, this script can learn how to generate
2117            # appledouble files directly and import those to git, but
2118            # non-mac machines can never find a use for apple filetype.
2119            print "\nIgnoring apple filetype file %s" % file['depotFile']
2120            return
2121
2122        # Note that we do not try to de-mangle keywords on utf16 files,
2123        # even though in theory somebody may want that.
2124        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2125        if pattern:
2126            regexp = re.compile(pattern, re.VERBOSE)
2127            text = ''.join(contents)
2128            text = regexp.sub(r'$\1$', text)
2129            contents = [ text ]
2130
2131        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2132
2133        # total length...
2134        length = 0
2135        for d in contents:
2136            length = length + len(d)
2137
2138        self.gitStream.write("data %d\n" % length)
2139        for d in contents:
2140            self.gitStream.write(d)
2141        self.gitStream.write("\n")
2142
2143    def streamOneP4Deletion(self, file):
2144        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2145        if verbose:
2146            sys.stderr.write("delete %s\n" % relPath)
2147        self.gitStream.write("D %s\n" % relPath)
2148
2149    # handle another chunk of streaming data
2150    def streamP4FilesCb(self, marshalled):
2151
2152        # catch p4 errors and complain
2153        err = None
2154        if "code" in marshalled:
2155            if marshalled["code"] == "error":
2156                if "data" in marshalled:
2157                    err = marshalled["data"].rstrip()
2158        if err:
2159            f = None
2160            if self.stream_have_file_info:
2161                if "depotFile" in self.stream_file:
2162                    f = self.stream_file["depotFile"]
2163            # force a failure in fast-import, else an empty
2164            # commit will be made
2165            self.gitStream.write("\n")
2166            self.gitStream.write("die-now\n")
2167            self.gitStream.close()
2168            # ignore errors, but make sure it exits first
2169            self.importProcess.wait()
2170            if f:
2171                die("Error from p4 print for %s: %s" % (f, err))
2172            else:
2173                die("Error from p4 print: %s" % err)
2174
2175        if marshalled.has_key('depotFile') and self.stream_have_file_info:
2176            # start of a new file - output the old one first
2177            self.streamOneP4File(self.stream_file, self.stream_contents)
2178            self.stream_file = {}
2179            self.stream_contents = []
2180            self.stream_have_file_info = False
2181
2182        # pick up the new file information... for the
2183        # 'data' field we need to append to our array
2184        for k in marshalled.keys():
2185            if k == 'data':
2186                self.stream_contents.append(marshalled['data'])
2187            else:
2188                self.stream_file[k] = marshalled[k]
2189
2190        self.stream_have_file_info = True
2191
2192    # Stream directly from "p4 files" into "git fast-import"
2193    def streamP4Files(self, files):
2194        filesForCommit = []
2195        filesToRead = []
2196        filesToDelete = []
2197
2198        for f in files:
2199            # if using a client spec, only add the files that have
2200            # a path in the client
2201            if self.clientSpecDirs:
2202                if self.clientSpecDirs.map_in_client(f['path']) == "":
2203                    continue
2204
2205            filesForCommit.append(f)
2206            if f['action'] in self.delete_actions:
2207                filesToDelete.append(f)
2208            else:
2209                filesToRead.append(f)
2210
2211        # deleted files...
2212        for f in filesToDelete:
2213            self.streamOneP4Deletion(f)
2214
2215        if len(filesToRead) > 0:
2216            self.stream_file = {}
2217            self.stream_contents = []
2218            self.stream_have_file_info = False
2219
2220            # curry self argument
2221            def streamP4FilesCbSelf(entry):
2222                self.streamP4FilesCb(entry)
2223
2224            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2225
2226            p4CmdList(["-x", "-", "print"],
2227                      stdin=fileArgs,
2228                      cb=streamP4FilesCbSelf)
2229
2230            # do the last chunk
2231            if self.stream_file.has_key('depotFile'):
2232                self.streamOneP4File(self.stream_file, self.stream_contents)
2233
2234    def make_email(self, userid):
2235        if userid in self.users:
2236            return self.users[userid]
2237        else:
2238            return "%s <a@b>" % userid
2239
2240    # Stream a p4 tag
2241    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2242        if verbose:
2243            print "writing tag %s for commit %s" % (labelName, commit)
2244        gitStream.write("tag %s\n" % labelName)
2245        gitStream.write("from %s\n" % commit)
2246
2247        if labelDetails.has_key('Owner'):
2248            owner = labelDetails["Owner"]
2249        else:
2250            owner = None
2251
2252        # Try to use the owner of the p4 label, or failing that,
2253        # the current p4 user id.
2254        if owner:
2255            email = self.make_email(owner)
2256        else:
2257            email = self.make_email(self.p4UserId())
2258        tagger = "%s %s %s" % (email, epoch, self.tz)
2259
2260        gitStream.write("tagger %s\n" % tagger)
2261
2262        print "labelDetails=",labelDetails
2263        if labelDetails.has_key('Description'):
2264            description = labelDetails['Description']
2265        else:
2266            description = 'Label from git p4'
2267
2268        gitStream.write("data %d\n" % len(description))
2269        gitStream.write(description)
2270        gitStream.write("\n")
2271
2272    def commit(self, details, files, branch, parent = ""):
2273        epoch = details["time"]
2274        author = details["user"]
2275
2276        if self.verbose:
2277            print "commit into %s" % branch
2278
2279        # start with reading files; if that fails, we should not
2280        # create a commit.
2281        new_files = []
2282        for f in files:
2283            if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2284                new_files.append (f)
2285            else:
2286                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2287
2288        if self.clientSpecDirs:
2289            self.clientSpecDirs.update_client_spec_path_cache(files)
2290
2291        self.gitStream.write("commit %s\n" % branch)
2292#        gitStream.write("mark :%s\n" % details["change"])
2293        self.committedChanges.add(int(details["change"]))
2294        committer = ""
2295        if author not in self.users:
2296            self.getUserMapFromPerforceServer()
2297        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2298
2299        self.gitStream.write("committer %s\n" % committer)
2300
2301        self.gitStream.write("data <<EOT\n")
2302        self.gitStream.write(details["desc"])
2303        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2304                             (','.join(self.branchPrefixes), details["change"]))
2305        if len(details['options']) > 0:
2306            self.gitStream.write(": options = %s" % details['options'])
2307        self.gitStream.write("]\nEOT\n\n")
2308
2309        if len(parent) > 0:
2310            if self.verbose:
2311                print "parent %s" % parent
2312            self.gitStream.write("from %s\n" % parent)
2313
2314        self.streamP4Files(new_files)
2315        self.gitStream.write("\n")
2316
2317        change = int(details["change"])
2318
2319        if self.labels.has_key(change):
2320            label = self.labels[change]
2321            labelDetails = label[0]
2322            labelRevisions = label[1]
2323            if self.verbose:
2324                print "Change %s is labelled %s" % (change, labelDetails)
2325
2326            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2327                                                for p in self.branchPrefixes])
2328
2329            if len(files) == len(labelRevisions):
2330
2331                cleanedFiles = {}
2332                for info in files:
2333                    if info["action"] in self.delete_actions:
2334                        continue
2335                    cleanedFiles[info["depotFile"]] = info["rev"]
2336
2337                if cleanedFiles == labelRevisions:
2338                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2339
2340                else:
2341                    if not self.silent:
2342                        print ("Tag %s does not match with change %s: files do not match."
2343                               % (labelDetails["label"], change))
2344
2345            else:
2346                if not self.silent:
2347                    print ("Tag %s does not match with change %s: file count is different."
2348                           % (labelDetails["label"], change))
2349
2350    # Build a dictionary of changelists and labels, for "detect-labels" option.
2351    def getLabels(self):
2352        self.labels = {}
2353
2354        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2355        if len(l) > 0 and not self.silent:
2356            print "Finding files belonging to labels in %s" % `self.depotPaths`
2357
2358        for output in l:
2359            label = output["label"]
2360            revisions = {}
2361            newestChange = 0
2362            if self.verbose:
2363                print "Querying files for label %s" % label
2364            for file in p4CmdList(["files"] +
2365                                      ["%s...@%s" % (p, label)
2366                                          for p in self.depotPaths]):
2367                revisions[file["depotFile"]] = file["rev"]
2368                change = int(file["change"])
2369                if change > newestChange:
2370                    newestChange = change
2371
2372            self.labels[newestChange] = [output, revisions]
2373
2374        if self.verbose:
2375            print "Label changes: %s" % self.labels.keys()
2376
2377    # Import p4 labels as git tags. A direct mapping does not
2378    # exist, so assume that if all the files are at the same revision
2379    # then we can use that, or it's something more complicated we should
2380    # just ignore.
2381    def importP4Labels(self, stream, p4Labels):
2382        if verbose:
2383            print "import p4 labels: " + ' '.join(p4Labels)
2384
2385        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2386        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2387        if len(validLabelRegexp) == 0:
2388            validLabelRegexp = defaultLabelRegexp
2389        m = re.compile(validLabelRegexp)
2390
2391        for name in p4Labels:
2392            commitFound = False
2393
2394            if not m.match(name):
2395                if verbose:
2396                    print "label %s does not match regexp %s" % (name,validLabelRegexp)
2397                continue
2398
2399            if name in ignoredP4Labels:
2400                continue
2401
2402            labelDetails = p4CmdList(['label', "-o", name])[0]
2403
2404            # get the most recent changelist for each file in this label
2405            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2406                                for p in self.depotPaths])
2407
2408            if change.has_key('change'):
2409                # find the corresponding git commit; take the oldest commit
2410                changelist = int(change['change'])
2411                gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2412                     "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2413                if len(gitCommit) == 0:
2414                    print "could not find git commit for changelist %d" % changelist
2415                else:
2416                    gitCommit = gitCommit.strip()
2417                    commitFound = True
2418                    # Convert from p4 time format
2419                    try:
2420                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2421                    except ValueError:
2422                        print "Could not convert label time %s" % labelDetails['Update']
2423                        tmwhen = 1
2424
2425                    when = int(time.mktime(tmwhen))
2426                    self.streamTag(stream, name, labelDetails, gitCommit, when)
2427                    if verbose:
2428                        print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2429            else:
2430                if verbose:
2431                    print "Label %s has no changelists - possibly deleted?" % name
2432
2433            if not commitFound:
2434                # We can't import this label; don't try again as it will get very
2435                # expensive repeatedly fetching all the files for labels that will
2436                # never be imported. If the label is moved in the future, the
2437                # ignore will need to be removed manually.
2438                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2439
2440    def guessProjectName(self):
2441        for p in self.depotPaths:
2442            if p.endswith("/"):
2443                p = p[:-1]
2444            p = p[p.strip().rfind("/") + 1:]
2445            if not p.endswith("/"):
2446               p += "/"
2447            return p
2448
2449    def getBranchMapping(self):
2450        lostAndFoundBranches = set()
2451
2452        user = gitConfig("git-p4.branchUser")
2453        if len(user) > 0:
2454            command = "branches -u %s" % user
2455        else:
2456            command = "branches"
2457
2458        for info in p4CmdList(command):
2459            details = p4Cmd(["branch", "-o", info["branch"]])
2460            viewIdx = 0
2461            while details.has_key("View%s" % viewIdx):
2462                paths = details["View%s" % viewIdx].split(" ")
2463                viewIdx = viewIdx + 1
2464                # require standard //depot/foo/... //depot/bar/... mapping
2465                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2466                    continue
2467                source = paths[0]
2468                destination = paths[1]
2469                ## HACK
2470                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2471                    source = source[len(self.depotPaths[0]):-4]
2472                    destination = destination[len(self.depotPaths[0]):-4]
2473
2474                    if destination in self.knownBranches:
2475                        if not self.silent:
2476                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2477                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2478                        continue
2479
2480                    self.knownBranches[destination] = source
2481
2482                    lostAndFoundBranches.discard(destination)
2483
2484                    if source not in self.knownBranches:
2485                        lostAndFoundBranches.add(source)
2486
2487        # Perforce does not strictly require branches to be defined, so we also
2488        # check git config for a branch list.
2489        #
2490        # Example of branch definition in git config file:
2491        # [git-p4]
2492        #   branchList=main:branchA
2493        #   branchList=main:branchB
2494        #   branchList=branchA:branchC
2495        configBranches = gitConfigList("git-p4.branchList")
2496        for branch in configBranches:
2497            if branch:
2498                (source, destination) = branch.split(":")
2499                self.knownBranches[destination] = source
2500
2501                lostAndFoundBranches.discard(destination)
2502
2503                if source not in self.knownBranches:
2504                    lostAndFoundBranches.add(source)
2505
2506
2507        for branch in lostAndFoundBranches:
2508            self.knownBranches[branch] = branch
2509
2510    def getBranchMappingFromGitBranches(self):
2511        branches = p4BranchesInGit(self.importIntoRemotes)
2512        for branch in branches.keys():
2513            if branch == "master":
2514                branch = "main"
2515            else:
2516                branch = branch[len(self.projectName):]
2517            self.knownBranches[branch] = branch
2518
2519    def updateOptionDict(self, d):
2520        option_keys = {}
2521        if self.keepRepoPath:
2522            option_keys['keepRepoPath'] = 1
2523
2524        d["options"] = ' '.join(sorted(option_keys.keys()))
2525
2526    def readOptions(self, d):
2527        self.keepRepoPath = (d.has_key('options')
2528                             and ('keepRepoPath' in d['options']))
2529
2530    def gitRefForBranch(self, branch):
2531        if branch == "main":
2532            return self.refPrefix + "master"
2533
2534        if len(branch) <= 0:
2535            return branch
2536
2537        return self.refPrefix + self.projectName + branch
2538
2539    def gitCommitByP4Change(self, ref, change):
2540        if self.verbose:
2541            print "looking in ref " + ref + " for change %s using bisect..." % change
2542
2543        earliestCommit = ""
2544        latestCommit = parseRevision(ref)
2545
2546        while True:
2547            if self.verbose:
2548                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2549            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2550            if len(next) == 0:
2551                if self.verbose:
2552                    print "argh"
2553                return ""
2554            log = extractLogMessageFromGitCommit(next)
2555            settings = extractSettingsGitLog(log)
2556            currentChange = int(settings['change'])
2557            if self.verbose:
2558                print "current change %s" % currentChange
2559
2560            if currentChange == change:
2561                if self.verbose:
2562                    print "found %s" % next
2563                return next
2564
2565            if currentChange < change:
2566                earliestCommit = "^%s" % next
2567            else:
2568                latestCommit = "%s" % next
2569
2570        return ""
2571
2572    def importNewBranch(self, branch, maxChange):
2573        # make fast-import flush all changes to disk and update the refs using the checkpoint
2574        # command so that we can try to find the branch parent in the git history
2575        self.gitStream.write("checkpoint\n\n");
2576        self.gitStream.flush();
2577        branchPrefix = self.depotPaths[0] + branch + "/"
2578        range = "@1,%s" % maxChange
2579        #print "prefix" + branchPrefix
2580        changes = p4ChangesForPaths([branchPrefix], range)
2581        if len(changes) <= 0:
2582            return False
2583        firstChange = changes[0]
2584        #print "first change in branch: %s" % firstChange
2585        sourceBranch = self.knownBranches[branch]
2586        sourceDepotPath = self.depotPaths[0] + sourceBranch
2587        sourceRef = self.gitRefForBranch(sourceBranch)
2588        #print "source " + sourceBranch
2589
2590        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2591        #print "branch parent: %s" % branchParentChange
2592        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2593        if len(gitParent) > 0:
2594            self.initialParents[self.gitRefForBranch(branch)] = gitParent
2595            #print "parent git commit: %s" % gitParent
2596
2597        self.importChanges(changes)
2598        return True
2599
2600    def searchParent(self, parent, branch, target):
2601        parentFound = False
2602        for blob in read_pipe_lines(["git", "rev-list", "--reverse",
2603                                     "--no-merges", parent]):
2604            blob = blob.strip()
2605            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2606                parentFound = True
2607                if self.verbose:
2608                    print "Found parent of %s in commit %s" % (branch, blob)
2609                break
2610        if parentFound:
2611            return blob
2612        else:
2613            return None
2614
2615    def importChanges(self, changes):
2616        cnt = 1
2617        for change in changes:
2618            description = p4_describe(change)
2619            self.updateOptionDict(description)
2620
2621            if not self.silent:
2622                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2623                sys.stdout.flush()
2624            cnt = cnt + 1
2625
2626            try:
2627                if self.detectBranches:
2628                    branches = self.splitFilesIntoBranches(description)
2629                    for branch in branches.keys():
2630                        ## HACK  --hwn
2631                        branchPrefix = self.depotPaths[0] + branch + "/"
2632                        self.branchPrefixes = [ branchPrefix ]
2633
2634                        parent = ""
2635
2636                        filesForCommit = branches[branch]
2637
2638                        if self.verbose:
2639                            print "branch is %s" % branch
2640
2641                        self.updatedBranches.add(branch)
2642
2643                        if branch not in self.createdBranches:
2644                            self.createdBranches.add(branch)
2645                            parent = self.knownBranches[branch]
2646                            if parent == branch:
2647                                parent = ""
2648                            else:
2649                                fullBranch = self.projectName + branch
2650                                if fullBranch not in self.p4BranchesInGit:
2651                                    if not self.silent:
2652                                        print("\n    Importing new branch %s" % fullBranch);
2653                                    if self.importNewBranch(branch, change - 1):
2654                                        parent = ""
2655                                        self.p4BranchesInGit.append(fullBranch)
2656                                    if not self.silent:
2657                                        print("\n    Resuming with change %s" % change);
2658
2659                                if self.verbose:
2660                                    print "parent determined through known branches: %s" % parent
2661
2662                        branch = self.gitRefForBranch(branch)
2663                        parent = self.gitRefForBranch(parent)
2664
2665                        if self.verbose:
2666                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2667
2668                        if len(parent) == 0 and branch in self.initialParents:
2669                            parent = self.initialParents[branch]
2670                            del self.initialParents[branch]
2671
2672                        blob = None
2673                        if len(parent) > 0:
2674                            tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2675                            if self.verbose:
2676                                print "Creating temporary branch: " + tempBranch
2677                            self.commit(description, filesForCommit, tempBranch)
2678                            self.tempBranches.append(tempBranch)
2679                            self.checkpoint()
2680                            blob = self.searchParent(parent, branch, tempBranch)
2681                        if blob:
2682                            self.commit(description, filesForCommit, branch, blob)
2683                        else:
2684                            if self.verbose:
2685                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2686                            self.commit(description, filesForCommit, branch, parent)
2687                else:
2688                    files = self.extractFilesFromCommit(description)
2689                    self.commit(description, files, self.branch,
2690                                self.initialParent)
2691                    # only needed once, to connect to the previous commit
2692                    self.initialParent = ""
2693            except IOError:
2694                print self.gitError.read()
2695                sys.exit(1)
2696
2697    def importHeadRevision(self, revision):
2698        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2699
2700        details = {}
2701        details["user"] = "git perforce import user"
2702        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2703                           % (' '.join(self.depotPaths), revision))
2704        details["change"] = revision
2705        newestRevision = 0
2706
2707        fileCnt = 0
2708        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2709
2710        for info in p4CmdList(["files"] + fileArgs):
2711
2712            if 'code' in info and info['code'] == 'error':
2713                sys.stderr.write("p4 returned an error: %s\n"
2714                                 % info['data'])
2715                if info['data'].find("must refer to client") >= 0:
2716                    sys.stderr.write("This particular p4 error is misleading.\n")
2717                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
2718                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2719                sys.exit(1)
2720            if 'p4ExitCode' in info:
2721                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2722                sys.exit(1)
2723
2724
2725            change = int(info["change"])
2726            if change > newestRevision:
2727                newestRevision = change
2728
2729            if info["action"] in self.delete_actions:
2730                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2731                #fileCnt = fileCnt + 1
2732                continue
2733
2734            for prop in ["depotFile", "rev", "action", "type" ]:
2735                details["%s%s" % (prop, fileCnt)] = info[prop]
2736
2737            fileCnt = fileCnt + 1
2738
2739        details["change"] = newestRevision
2740
2741        # Use time from top-most change so that all git p4 clones of
2742        # the same p4 repo have the same commit SHA1s.
2743        res = p4_describe(newestRevision)
2744        details["time"] = res["time"]
2745
2746        self.updateOptionDict(details)
2747        try:
2748            self.commit(details, self.extractFilesFromCommit(details), self.branch)
2749        except IOError:
2750            print "IO error with git fast-import. Is your git version recent enough?"
2751            print self.gitError.read()
2752
2753
2754    def run(self, args):
2755        self.depotPaths = []
2756        self.changeRange = ""
2757        self.previousDepotPaths = []
2758        self.hasOrigin = False
2759
2760        # map from branch depot path to parent branch
2761        self.knownBranches = {}
2762        self.initialParents = {}
2763
2764        if self.importIntoRemotes:
2765            self.refPrefix = "refs/remotes/p4/"
2766        else:
2767            self.refPrefix = "refs/heads/p4/"
2768
2769        if self.syncWithOrigin:
2770            self.hasOrigin = originP4BranchesExist()
2771            if self.hasOrigin:
2772                if not self.silent:
2773                    print 'Syncing with origin first, using "git fetch origin"'
2774                system("git fetch origin")
2775
2776        branch_arg_given = bool(self.branch)
2777        if len(self.branch) == 0:
2778            self.branch = self.refPrefix + "master"
2779            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2780                system("git update-ref %s refs/heads/p4" % self.branch)
2781                system("git branch -D p4")
2782
2783        # accept either the command-line option, or the configuration variable
2784        if self.useClientSpec:
2785            # will use this after clone to set the variable
2786            self.useClientSpec_from_options = True
2787        else:
2788            if gitConfigBool("git-p4.useclientspec"):
2789                self.useClientSpec = True
2790        if self.useClientSpec:
2791            self.clientSpecDirs = getClientSpec()
2792
2793        # TODO: should always look at previous commits,
2794        # merge with previous imports, if possible.
2795        if args == []:
2796            if self.hasOrigin:
2797                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2798
2799            # branches holds mapping from branch name to sha1
2800            branches = p4BranchesInGit(self.importIntoRemotes)
2801
2802            # restrict to just this one, disabling detect-branches
2803            if branch_arg_given:
2804                short = self.branch.split("/")[-1]
2805                if short in branches:
2806                    self.p4BranchesInGit = [ short ]
2807            else:
2808                self.p4BranchesInGit = branches.keys()
2809
2810            if len(self.p4BranchesInGit) > 1:
2811                if not self.silent:
2812                    print "Importing from/into multiple branches"
2813                self.detectBranches = True
2814                for branch in branches.keys():
2815                    self.initialParents[self.refPrefix + branch] = \
2816                        branches[branch]
2817
2818            if self.verbose:
2819                print "branches: %s" % self.p4BranchesInGit
2820
2821            p4Change = 0
2822            for branch in self.p4BranchesInGit:
2823                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2824
2825                settings = extractSettingsGitLog(logMsg)
2826
2827                self.readOptions(settings)
2828                if (settings.has_key('depot-paths')
2829                    and settings.has_key ('change')):
2830                    change = int(settings['change']) + 1
2831                    p4Change = max(p4Change, change)
2832
2833                    depotPaths = sorted(settings['depot-paths'])
2834                    if self.previousDepotPaths == []:
2835                        self.previousDepotPaths = depotPaths
2836                    else:
2837                        paths = []
2838                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2839                            prev_list = prev.split("/")
2840                            cur_list = cur.split("/")
2841                            for i in range(0, min(len(cur_list), len(prev_list))):
2842                                if cur_list[i] <> prev_list[i]:
2843                                    i = i - 1
2844                                    break
2845
2846                            paths.append ("/".join(cur_list[:i + 1]))
2847
2848                        self.previousDepotPaths = paths
2849
2850            if p4Change > 0:
2851                self.depotPaths = sorted(self.previousDepotPaths)
2852                self.changeRange = "@%s,#head" % p4Change
2853                if not self.silent and not self.detectBranches:
2854                    print "Performing incremental import into %s git branch" % self.branch
2855
2856        # accept multiple ref name abbreviations:
2857        #    refs/foo/bar/branch -> use it exactly
2858        #    p4/branch -> prepend refs/remotes/ or refs/heads/
2859        #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2860        if not self.branch.startswith("refs/"):
2861            if self.importIntoRemotes:
2862                prepend = "refs/remotes/"
2863            else:
2864                prepend = "refs/heads/"
2865            if not self.branch.startswith("p4/"):
2866                prepend += "p4/"
2867            self.branch = prepend + self.branch
2868
2869        if len(args) == 0 and self.depotPaths:
2870            if not self.silent:
2871                print "Depot paths: %s" % ' '.join(self.depotPaths)
2872        else:
2873            if self.depotPaths and self.depotPaths != args:
2874                print ("previous import used depot path %s and now %s was specified. "
2875                       "This doesn't work!" % (' '.join (self.depotPaths),
2876                                               ' '.join (args)))
2877                sys.exit(1)
2878
2879            self.depotPaths = sorted(args)
2880
2881        revision = ""
2882        self.users = {}
2883
2884        # Make sure no revision specifiers are used when --changesfile
2885        # is specified.
2886        bad_changesfile = False
2887        if len(self.changesFile) > 0:
2888            for p in self.depotPaths:
2889                if p.find("@") >= 0 or p.find("#") >= 0:
2890                    bad_changesfile = True
2891                    break
2892        if bad_changesfile:
2893            die("Option --changesfile is incompatible with revision specifiers")
2894
2895        newPaths = []
2896        for p in self.depotPaths:
2897            if p.find("@") != -1:
2898                atIdx = p.index("@")
2899                self.changeRange = p[atIdx:]
2900                if self.changeRange == "@all":
2901                    self.changeRange = ""
2902                elif ',' not in self.changeRange:
2903                    revision = self.changeRange
2904                    self.changeRange = ""
2905                p = p[:atIdx]
2906            elif p.find("#") != -1:
2907                hashIdx = p.index("#")
2908                revision = p[hashIdx:]
2909                p = p[:hashIdx]
2910            elif self.previousDepotPaths == []:
2911                # pay attention to changesfile, if given, else import
2912                # the entire p4 tree at the head revision
2913                if len(self.changesFile) == 0:
2914                    revision = "#head"
2915
2916            p = re.sub ("\.\.\.$", "", p)
2917            if not p.endswith("/"):
2918                p += "/"
2919
2920            newPaths.append(p)
2921
2922        self.depotPaths = newPaths
2923
2924        # --detect-branches may change this for each branch
2925        self.branchPrefixes = self.depotPaths
2926
2927        self.loadUserMapFromCache()
2928        self.labels = {}
2929        if self.detectLabels:
2930            self.getLabels();
2931
2932        if self.detectBranches:
2933            ## FIXME - what's a P4 projectName ?
2934            self.projectName = self.guessProjectName()
2935
2936            if self.hasOrigin:
2937                self.getBranchMappingFromGitBranches()
2938            else:
2939                self.getBranchMapping()
2940            if self.verbose:
2941                print "p4-git branches: %s" % self.p4BranchesInGit
2942                print "initial parents: %s" % self.initialParents
2943            for b in self.p4BranchesInGit:
2944                if b != "master":
2945
2946                    ## FIXME
2947                    b = b[len(self.projectName):]
2948                self.createdBranches.add(b)
2949
2950        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2951
2952        self.importProcess = subprocess.Popen(["git", "fast-import"],
2953                                              stdin=subprocess.PIPE,
2954                                              stdout=subprocess.PIPE,
2955                                              stderr=subprocess.PIPE);
2956        self.gitOutput = self.importProcess.stdout
2957        self.gitStream = self.importProcess.stdin
2958        self.gitError = self.importProcess.stderr
2959
2960        if revision:
2961            self.importHeadRevision(revision)
2962        else:
2963            changes = []
2964
2965            if len(self.changesFile) > 0:
2966                output = open(self.changesFile).readlines()
2967                changeSet = set()
2968                for line in output:
2969                    changeSet.add(int(line))
2970
2971                for change in changeSet:
2972                    changes.append(change)
2973
2974                changes.sort()
2975            else:
2976                # catch "git p4 sync" with no new branches, in a repo that
2977                # does not have any existing p4 branches
2978                if len(args) == 0:
2979                    if not self.p4BranchesInGit:
2980                        die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
2981
2982                    # The default branch is master, unless --branch is used to
2983                    # specify something else.  Make sure it exists, or complain
2984                    # nicely about how to use --branch.
2985                    if not self.detectBranches:
2986                        if not branch_exists(self.branch):
2987                            if branch_arg_given:
2988                                die("Error: branch %s does not exist." % self.branch)
2989                            else:
2990                                die("Error: no branch %s; perhaps specify one with --branch." %
2991                                    self.branch)
2992
2993                if self.verbose:
2994                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2995                                                              self.changeRange)
2996                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2997
2998                if len(self.maxChanges) > 0:
2999                    changes = changes[:min(int(self.maxChanges), len(changes))]
3000
3001            if len(changes) == 0:
3002                if not self.silent:
3003                    print "No changes to import!"
3004            else:
3005                if not self.silent and not self.detectBranches:
3006                    print "Import destination: %s" % self.branch
3007
3008                self.updatedBranches = set()
3009
3010                if not self.detectBranches:
3011                    if args:
3012                        # start a new branch
3013                        self.initialParent = ""
3014                    else:
3015                        # build on a previous revision
3016                        self.initialParent = parseRevision(self.branch)
3017
3018                self.importChanges(changes)
3019
3020                if not self.silent:
3021                    print ""
3022                    if len(self.updatedBranches) > 0:
3023                        sys.stdout.write("Updated branches: ")
3024                        for b in self.updatedBranches:
3025                            sys.stdout.write("%s " % b)
3026                        sys.stdout.write("\n")
3027
3028        if gitConfigBool("git-p4.importLabels"):
3029            self.importLabels = True
3030
3031        if self.importLabels:
3032            p4Labels = getP4Labels(self.depotPaths)
3033            gitTags = getGitTags()
3034
3035            missingP4Labels = p4Labels - gitTags
3036            self.importP4Labels(self.gitStream, missingP4Labels)
3037
3038        self.gitStream.close()
3039        if self.importProcess.wait() != 0:
3040            die("fast-import failed: %s" % self.gitError.read())
3041        self.gitOutput.close()
3042        self.gitError.close()
3043
3044        # Cleanup temporary branches created during import
3045        if self.tempBranches != []:
3046            for branch in self.tempBranches:
3047                read_pipe("git update-ref -d %s" % branch)
3048            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3049
3050        # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3051        # a convenient shortcut refname "p4".
3052        if self.importIntoRemotes:
3053            head_ref = self.refPrefix + "HEAD"
3054            if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3055                system(["git", "symbolic-ref", head_ref, self.branch])
3056
3057        return True
3058
3059class P4Rebase(Command):
3060    def __init__(self):
3061        Command.__init__(self)
3062        self.options = [
3063                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3064        ]
3065        self.importLabels = False
3066        self.description = ("Fetches the latest revision from perforce and "
3067                            + "rebases the current work (branch) against it")
3068
3069    def run(self, args):
3070        sync = P4Sync()
3071        sync.importLabels = self.importLabels
3072        sync.run([])
3073
3074        return self.rebase()
3075
3076    def rebase(self):
3077        if os.system("git update-index --refresh") != 0:
3078            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.");
3079        if len(read_pipe("git diff-index HEAD --")) > 0:
3080            die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3081
3082        [upstream, settings] = findUpstreamBranchPoint()
3083        if len(upstream) == 0:
3084            die("Cannot find upstream branchpoint for rebase")
3085
3086        # the branchpoint may be p4/foo~3, so strip off the parent
3087        upstream = re.sub("~[0-9]+$", "", upstream)
3088
3089        print "Rebasing the current branch onto %s" % upstream
3090        oldHead = read_pipe("git rev-parse HEAD").strip()
3091        system("git rebase %s" % upstream)
3092        system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3093        return True
3094
3095class P4Clone(P4Sync):
3096    def __init__(self):
3097        P4Sync.__init__(self)
3098        self.description = "Creates a new git repository and imports from Perforce into it"
3099        self.usage = "usage: %prog [options] //depot/path[@revRange]"
3100        self.options += [
3101            optparse.make_option("--destination", dest="cloneDestination",
3102                                 action='store', default=None,
3103                                 help="where to leave result of the clone"),
3104            optparse.make_option("-/", dest="cloneExclude",
3105                                 action="append", type="string",
3106                                 help="exclude depot path"),
3107            optparse.make_option("--bare", dest="cloneBare",
3108                                 action="store_true", default=False),
3109        ]
3110        self.cloneDestination = None
3111        self.needsGit = False
3112        self.cloneBare = False
3113
3114    # This is required for the "append" cloneExclude action
3115    def ensure_value(self, attr, value):
3116        if not hasattr(self, attr) or getattr(self, attr) is None:
3117            setattr(self, attr, value)
3118        return getattr(self, attr)
3119
3120    def defaultDestination(self, args):
3121        ## TODO: use common prefix of args?
3122        depotPath = args[0]
3123        depotDir = re.sub("(@[^@]*)$", "", depotPath)
3124        depotDir = re.sub("(#[^#]*)$", "", depotDir)
3125        depotDir = re.sub(r"\.\.\.$", "", depotDir)
3126        depotDir = re.sub(r"/$", "", depotDir)
3127        return os.path.split(depotDir)[1]
3128
3129    def run(self, args):
3130        if len(args) < 1:
3131            return False
3132
3133        if self.keepRepoPath and not self.cloneDestination:
3134            sys.stderr.write("Must specify destination for --keep-path\n")
3135            sys.exit(1)
3136
3137        depotPaths = args
3138
3139        if not self.cloneDestination and len(depotPaths) > 1:
3140            self.cloneDestination = depotPaths[-1]
3141            depotPaths = depotPaths[:-1]
3142
3143        self.cloneExclude = ["/"+p for p in self.cloneExclude]
3144        for p in depotPaths:
3145            if not p.startswith("//"):
3146                sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3147                return False
3148
3149        if not self.cloneDestination:
3150            self.cloneDestination = self.defaultDestination(args)
3151
3152        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3153
3154        if not os.path.exists(self.cloneDestination):
3155            os.makedirs(self.cloneDestination)
3156        chdir(self.cloneDestination)
3157
3158        init_cmd = [ "git", "init" ]
3159        if self.cloneBare:
3160            init_cmd.append("--bare")
3161        retcode = subprocess.call(init_cmd)
3162        if retcode:
3163            raise CalledProcessError(retcode, init_cmd)
3164
3165        if not P4Sync.run(self, depotPaths):
3166            return False
3167
3168        # create a master branch and check out a work tree
3169        if gitBranchExists(self.branch):
3170            system([ "git", "branch", "master", self.branch ])
3171            if not self.cloneBare:
3172                system([ "git", "checkout", "-f" ])
3173        else:
3174            print 'Not checking out any branch, use ' \
3175                  '"git checkout -q -b master <branch>"'
3176
3177        # auto-set this variable if invoked with --use-client-spec
3178        if self.useClientSpec_from_options:
3179            system("git config --bool git-p4.useclientspec true")
3180
3181        return True
3182
3183class P4Branches(Command):
3184    def __init__(self):
3185        Command.__init__(self)
3186        self.options = [ ]
3187        self.description = ("Shows the git branches that hold imports and their "
3188                            + "corresponding perforce depot paths")
3189        self.verbose = False
3190
3191    def run(self, args):
3192        if originP4BranchesExist():
3193            createOrUpdateBranchesFromOrigin()
3194
3195        cmdline = "git rev-parse --symbolic "
3196        cmdline += " --remotes"
3197
3198        for line in read_pipe_lines(cmdline):
3199            line = line.strip()
3200
3201            if not line.startswith('p4/') or line == "p4/HEAD":
3202                continue
3203            branch = line
3204
3205            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3206            settings = extractSettingsGitLog(log)
3207
3208            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3209        return True
3210
3211class HelpFormatter(optparse.IndentedHelpFormatter):
3212    def __init__(self):
3213        optparse.IndentedHelpFormatter.__init__(self)
3214
3215    def format_description(self, description):
3216        if description:
3217            return description + "\n"
3218        else:
3219            return ""
3220
3221def printUsage(commands):
3222    print "usage: %s <command> [options]" % sys.argv[0]
3223    print ""
3224    print "valid commands: %s" % ", ".join(commands)
3225    print ""
3226    print "Try %s <command> --help for command specific help." % sys.argv[0]
3227    print ""
3228
3229commands = {
3230    "debug" : P4Debug,
3231    "submit" : P4Submit,
3232    "commit" : P4Submit,
3233    "sync" : P4Sync,
3234    "rebase" : P4Rebase,
3235    "clone" : P4Clone,
3236    "rollback" : P4RollBack,
3237    "branches" : P4Branches
3238}
3239
3240
3241def main():
3242    if len(sys.argv[1:]) == 0:
3243        printUsage(commands.keys())
3244        sys.exit(2)
3245
3246    cmdName = sys.argv[1]
3247    try:
3248        klass = commands[cmdName]
3249        cmd = klass()
3250    except KeyError:
3251        print "unknown command %s" % cmdName
3252        print ""
3253        printUsage(commands.keys())
3254        sys.exit(2)
3255
3256    options = cmd.options
3257    cmd.gitdir = os.environ.get("GIT_DIR", None)
3258
3259    args = sys.argv[2:]
3260
3261    options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3262    if cmd.needsGit:
3263        options.append(optparse.make_option("--git-dir", dest="gitdir"))
3264
3265    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3266                                   options,
3267                                   description = cmd.description,
3268                                   formatter = HelpFormatter())
3269
3270    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3271    global verbose
3272    verbose = cmd.verbose
3273    if cmd.needsGit:
3274        if cmd.gitdir == None:
3275            cmd.gitdir = os.path.abspath(".git")
3276            if not isValidGitDir(cmd.gitdir):
3277                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3278                if os.path.exists(cmd.gitdir):
3279                    cdup = read_pipe("git rev-parse --show-cdup").strip()
3280                    if len(cdup) > 0:
3281                        chdir(cdup);
3282
3283        if not isValidGitDir(cmd.gitdir):
3284            if isValidGitDir(cmd.gitdir + "/.git"):
3285                cmd.gitdir += "/.git"
3286            else:
3287                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3288
3289        os.environ["GIT_DIR"] = cmd.gitdir
3290
3291    if not cmd.run(args):
3292        parser.print_help()
3293        sys.exit(2)
3294
3295
3296if __name__ == '__main__':
3297    main()