git-p4.pyon commit Merge branch 'ep/shell-command-substitution' (0e6e1a5)
   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 applyCommit(self, id):
1242        """Apply one commit, return True if it succeeded."""
1243
1244        print "Applying", read_pipe(["git", "show", "-s",
1245                                     "--format=format:%h %s", id])
1246
1247        (p4User, gitEmail) = self.p4UserForCommit(id)
1248
1249        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1250        filesToAdd = set()
1251        filesToDelete = set()
1252        editedFiles = set()
1253        pureRenameCopy = set()
1254        filesToChangeExecBit = {}
1255
1256        for line in diff:
1257            diff = parseDiffTreeEntry(line)
1258            modifier = diff['status']
1259            path = diff['src']
1260            if modifier == "M":
1261                p4_edit(path)
1262                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1263                    filesToChangeExecBit[path] = diff['dst_mode']
1264                editedFiles.add(path)
1265            elif modifier == "A":
1266                filesToAdd.add(path)
1267                filesToChangeExecBit[path] = diff['dst_mode']
1268                if path in filesToDelete:
1269                    filesToDelete.remove(path)
1270            elif modifier == "D":
1271                filesToDelete.add(path)
1272                if path in filesToAdd:
1273                    filesToAdd.remove(path)
1274            elif modifier == "C":
1275                src, dest = diff['src'], diff['dst']
1276                p4_integrate(src, dest)
1277                pureRenameCopy.add(dest)
1278                if diff['src_sha1'] != diff['dst_sha1']:
1279                    p4_edit(dest)
1280                    pureRenameCopy.discard(dest)
1281                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1282                    p4_edit(dest)
1283                    pureRenameCopy.discard(dest)
1284                    filesToChangeExecBit[dest] = diff['dst_mode']
1285                if self.isWindows:
1286                    # turn off read-only attribute
1287                    os.chmod(dest, stat.S_IWRITE)
1288                os.unlink(dest)
1289                editedFiles.add(dest)
1290            elif modifier == "R":
1291                src, dest = diff['src'], diff['dst']
1292                if self.p4HasMoveCommand:
1293                    p4_edit(src)        # src must be open before move
1294                    p4_move(src, dest)  # opens for (move/delete, move/add)
1295                else:
1296                    p4_integrate(src, dest)
1297                    if diff['src_sha1'] != diff['dst_sha1']:
1298                        p4_edit(dest)
1299                    else:
1300                        pureRenameCopy.add(dest)
1301                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1302                    if not self.p4HasMoveCommand:
1303                        p4_edit(dest)   # with move: already open, writable
1304                    filesToChangeExecBit[dest] = diff['dst_mode']
1305                if not self.p4HasMoveCommand:
1306                    if self.isWindows:
1307                        os.chmod(dest, stat.S_IWRITE)
1308                    os.unlink(dest)
1309                    filesToDelete.add(src)
1310                editedFiles.add(dest)
1311            else:
1312                die("unknown modifier %s for %s" % (modifier, path))
1313
1314        diffcmd = "git diff-tree -p \"%s\"" % (id)
1315        patchcmd = diffcmd + " | git apply "
1316        tryPatchCmd = patchcmd + "--check -"
1317        applyPatchCmd = patchcmd + "--check --apply -"
1318        patch_succeeded = True
1319
1320        if os.system(tryPatchCmd) != 0:
1321            fixed_rcs_keywords = False
1322            patch_succeeded = False
1323            print "Unfortunately applying the change failed!"
1324
1325            # Patch failed, maybe it's just RCS keyword woes. Look through
1326            # the patch to see if that's possible.
1327            if gitConfigBool("git-p4.attemptRCSCleanup"):
1328                file = None
1329                pattern = None
1330                kwfiles = {}
1331                for file in editedFiles | filesToDelete:
1332                    # did this file's delta contain RCS keywords?
1333                    pattern = p4_keywords_regexp_for_file(file)
1334
1335                    if pattern:
1336                        # this file is a possibility...look for RCS keywords.
1337                        regexp = re.compile(pattern, re.VERBOSE)
1338                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1339                            if regexp.search(line):
1340                                if verbose:
1341                                    print "got keyword match on %s in %s in %s" % (pattern, line, file)
1342                                kwfiles[file] = pattern
1343                                break
1344
1345                for file in kwfiles:
1346                    if verbose:
1347                        print "zapping %s with %s" % (line,pattern)
1348                    # File is being deleted, so not open in p4.  Must
1349                    # disable the read-only bit on windows.
1350                    if self.isWindows and file not in editedFiles:
1351                        os.chmod(file, stat.S_IWRITE)
1352                    self.patchRCSKeywords(file, kwfiles[file])
1353                    fixed_rcs_keywords = True
1354
1355            if fixed_rcs_keywords:
1356                print "Retrying the patch with RCS keywords cleaned up"
1357                if os.system(tryPatchCmd) == 0:
1358                    patch_succeeded = True
1359
1360        if not patch_succeeded:
1361            for f in editedFiles:
1362                p4_revert(f)
1363            return False
1364
1365        #
1366        # Apply the patch for real, and do add/delete/+x handling.
1367        #
1368        system(applyPatchCmd)
1369
1370        for f in filesToAdd:
1371            p4_add(f)
1372        for f in filesToDelete:
1373            p4_revert(f)
1374            p4_delete(f)
1375
1376        # Set/clear executable bits
1377        for f in filesToChangeExecBit.keys():
1378            mode = filesToChangeExecBit[f]
1379            setP4ExecBit(f, mode)
1380
1381        #
1382        # Build p4 change description, starting with the contents
1383        # of the git commit message.
1384        #
1385        logMessage = extractLogMessageFromGitCommit(id)
1386        logMessage = logMessage.strip()
1387        (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1388
1389        template = self.prepareSubmitTemplate()
1390        submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1391
1392        if self.preserveUser:
1393           submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1394
1395        if self.checkAuthorship and not self.p4UserIsMe(p4User):
1396            submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1397            submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1398            submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1399
1400        separatorLine = "######## everything below this line is just the diff #######\n"
1401
1402        # diff
1403        if os.environ.has_key("P4DIFF"):
1404            del(os.environ["P4DIFF"])
1405        diff = ""
1406        for editedFile in editedFiles:
1407            diff += p4_read_pipe(['diff', '-du',
1408                                  wildcard_encode(editedFile)])
1409
1410        # new file diff
1411        newdiff = ""
1412        for newFile in filesToAdd:
1413            newdiff += "==== new file ====\n"
1414            newdiff += "--- /dev/null\n"
1415            newdiff += "+++ %s\n" % newFile
1416            f = open(newFile, "r")
1417            for line in f.readlines():
1418                newdiff += "+" + line
1419            f.close()
1420
1421        # change description file: submitTemplate, separatorLine, diff, newdiff
1422        (handle, fileName) = tempfile.mkstemp()
1423        tmpFile = os.fdopen(handle, "w+")
1424        if self.isWindows:
1425            submitTemplate = submitTemplate.replace("\n", "\r\n")
1426            separatorLine = separatorLine.replace("\n", "\r\n")
1427            newdiff = newdiff.replace("\n", "\r\n")
1428        tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
1429        tmpFile.close()
1430
1431        if self.prepare_p4_only:
1432            #
1433            # Leave the p4 tree prepared, and the submit template around
1434            # and let the user decide what to do next
1435            #
1436            print
1437            print "P4 workspace prepared for submission."
1438            print "To submit or revert, go to client workspace"
1439            print "  " + self.clientPath
1440            print
1441            print "To submit, use \"p4 submit\" to write a new description,"
1442            print "or \"p4 submit -i %s\" to use the one prepared by" \
1443                  " \"git p4\"." % fileName
1444            print "You can delete the file \"%s\" when finished." % fileName
1445
1446            if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1447                print "To preserve change ownership by user %s, you must\n" \
1448                      "do \"p4 change -f <change>\" after submitting and\n" \
1449                      "edit the User field."
1450            if pureRenameCopy:
1451                print "After submitting, renamed files must be re-synced."
1452                print "Invoke \"p4 sync -f\" on each of these files:"
1453                for f in pureRenameCopy:
1454                    print "  " + f
1455
1456            print
1457            print "To revert the changes, use \"p4 revert ...\", and delete"
1458            print "the submit template file \"%s\"" % fileName
1459            if filesToAdd:
1460                print "Since the commit adds new files, they must be deleted:"
1461                for f in filesToAdd:
1462                    print "  " + f
1463            print
1464            return True
1465
1466        #
1467        # Let the user edit the change description, then submit it.
1468        #
1469        if self.edit_template(fileName):
1470            # read the edited message and submit
1471            ret = True
1472            tmpFile = open(fileName, "rb")
1473            message = tmpFile.read()
1474            tmpFile.close()
1475            submitTemplate = message[:message.index(separatorLine)]
1476            if self.isWindows:
1477                submitTemplate = submitTemplate.replace("\r\n", "\n")
1478            p4_write_pipe(['submit', '-i'], submitTemplate)
1479
1480            if self.preserveUser:
1481                if p4User:
1482                    # Get last changelist number. Cannot easily get it from
1483                    # the submit command output as the output is
1484                    # unmarshalled.
1485                    changelist = self.lastP4Changelist()
1486                    self.modifyChangelistUser(changelist, p4User)
1487
1488            # The rename/copy happened by applying a patch that created a
1489            # new file.  This leaves it writable, which confuses p4.
1490            for f in pureRenameCopy:
1491                p4_sync(f, "-f")
1492
1493        else:
1494            # skip this patch
1495            ret = False
1496            print "Submission cancelled, undoing p4 changes."
1497            for f in editedFiles:
1498                p4_revert(f)
1499            for f in filesToAdd:
1500                p4_revert(f)
1501                os.remove(f)
1502            for f in filesToDelete:
1503                p4_revert(f)
1504
1505        os.remove(fileName)
1506        return ret
1507
1508    # Export git tags as p4 labels. Create a p4 label and then tag
1509    # with that.
1510    def exportGitTags(self, gitTags):
1511        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1512        if len(validLabelRegexp) == 0:
1513            validLabelRegexp = defaultLabelRegexp
1514        m = re.compile(validLabelRegexp)
1515
1516        for name in gitTags:
1517
1518            if not m.match(name):
1519                if verbose:
1520                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1521                continue
1522
1523            # Get the p4 commit this corresponds to
1524            logMessage = extractLogMessageFromGitCommit(name)
1525            values = extractSettingsGitLog(logMessage)
1526
1527            if not values.has_key('change'):
1528                # a tag pointing to something not sent to p4; ignore
1529                if verbose:
1530                    print "git tag %s does not give a p4 commit" % name
1531                continue
1532            else:
1533                changelist = values['change']
1534
1535            # Get the tag details.
1536            inHeader = True
1537            isAnnotated = False
1538            body = []
1539            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1540                l = l.strip()
1541                if inHeader:
1542                    if re.match(r'tag\s+', l):
1543                        isAnnotated = True
1544                    elif re.match(r'\s*$', l):
1545                        inHeader = False
1546                        continue
1547                else:
1548                    body.append(l)
1549
1550            if not isAnnotated:
1551                body = ["lightweight tag imported by git p4\n"]
1552
1553            # Create the label - use the same view as the client spec we are using
1554            clientSpec = getClientSpec()
1555
1556            labelTemplate  = "Label: %s\n" % name
1557            labelTemplate += "Description:\n"
1558            for b in body:
1559                labelTemplate += "\t" + b + "\n"
1560            labelTemplate += "View:\n"
1561            for depot_side in clientSpec.mappings:
1562                labelTemplate += "\t%s\n" % depot_side
1563
1564            if self.dry_run:
1565                print "Would create p4 label %s for tag" % name
1566            elif self.prepare_p4_only:
1567                print "Not creating p4 label %s for tag due to option" \
1568                      " --prepare-p4-only" % name
1569            else:
1570                p4_write_pipe(["label", "-i"], labelTemplate)
1571
1572                # Use the label
1573                p4_system(["tag", "-l", name] +
1574                          ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1575
1576                if verbose:
1577                    print "created p4 label for tag %s" % name
1578
1579    def run(self, args):
1580        if len(args) == 0:
1581            self.master = currentGitBranch()
1582            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1583                die("Detecting current git branch failed!")
1584        elif len(args) == 1:
1585            self.master = args[0]
1586            if not branchExists(self.master):
1587                die("Branch %s does not exist" % self.master)
1588        else:
1589            return False
1590
1591        allowSubmit = gitConfig("git-p4.allowSubmit")
1592        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1593            die("%s is not in git-p4.allowSubmit" % self.master)
1594
1595        [upstream, settings] = findUpstreamBranchPoint()
1596        self.depotPath = settings['depot-paths'][0]
1597        if len(self.origin) == 0:
1598            self.origin = upstream
1599
1600        if self.preserveUser:
1601            if not self.canChangeChangelists():
1602                die("Cannot preserve user names without p4 super-user or admin permissions")
1603
1604        # if not set from the command line, try the config file
1605        if self.conflict_behavior is None:
1606            val = gitConfig("git-p4.conflict")
1607            if val:
1608                if val not in self.conflict_behavior_choices:
1609                    die("Invalid value '%s' for config git-p4.conflict" % val)
1610            else:
1611                val = "ask"
1612            self.conflict_behavior = val
1613
1614        if self.verbose:
1615            print "Origin branch is " + self.origin
1616
1617        if len(self.depotPath) == 0:
1618            print "Internal error: cannot locate perforce depot path from existing branches"
1619            sys.exit(128)
1620
1621        self.useClientSpec = False
1622        if gitConfigBool("git-p4.useclientspec"):
1623            self.useClientSpec = True
1624        if self.useClientSpec:
1625            self.clientSpecDirs = getClientSpec()
1626
1627        if self.useClientSpec:
1628            # all files are relative to the client spec
1629            self.clientPath = getClientRoot()
1630        else:
1631            self.clientPath = p4Where(self.depotPath)
1632
1633        if self.clientPath == "":
1634            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1635
1636        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1637        self.oldWorkingDirectory = os.getcwd()
1638
1639        # ensure the clientPath exists
1640        new_client_dir = False
1641        if not os.path.exists(self.clientPath):
1642            new_client_dir = True
1643            os.makedirs(self.clientPath)
1644
1645        chdir(self.clientPath, is_client_path=True)
1646        if self.dry_run:
1647            print "Would synchronize p4 checkout in %s" % self.clientPath
1648        else:
1649            print "Synchronizing p4 checkout..."
1650            if new_client_dir:
1651                # old one was destroyed, and maybe nobody told p4
1652                p4_sync("...", "-f")
1653            else:
1654                p4_sync("...")
1655        self.check()
1656
1657        commits = []
1658        for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, self.master)]):
1659            commits.append(line.strip())
1660        commits.reverse()
1661
1662        if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1663            self.checkAuthorship = False
1664        else:
1665            self.checkAuthorship = True
1666
1667        if self.preserveUser:
1668            self.checkValidP4Users(commits)
1669
1670        #
1671        # Build up a set of options to be passed to diff when
1672        # submitting each commit to p4.
1673        #
1674        if self.detectRenames:
1675            # command-line -M arg
1676            self.diffOpts = "-M"
1677        else:
1678            # If not explicitly set check the config variable
1679            detectRenames = gitConfig("git-p4.detectRenames")
1680
1681            if detectRenames.lower() == "false" or detectRenames == "":
1682                self.diffOpts = ""
1683            elif detectRenames.lower() == "true":
1684                self.diffOpts = "-M"
1685            else:
1686                self.diffOpts = "-M%s" % detectRenames
1687
1688        # no command-line arg for -C or --find-copies-harder, just
1689        # config variables
1690        detectCopies = gitConfig("git-p4.detectCopies")
1691        if detectCopies.lower() == "false" or detectCopies == "":
1692            pass
1693        elif detectCopies.lower() == "true":
1694            self.diffOpts += " -C"
1695        else:
1696            self.diffOpts += " -C%s" % detectCopies
1697
1698        if gitConfigBool("git-p4.detectCopiesHarder"):
1699            self.diffOpts += " --find-copies-harder"
1700
1701        #
1702        # Apply the commits, one at a time.  On failure, ask if should
1703        # continue to try the rest of the patches, or quit.
1704        #
1705        if self.dry_run:
1706            print "Would apply"
1707        applied = []
1708        last = len(commits) - 1
1709        for i, commit in enumerate(commits):
1710            if self.dry_run:
1711                print " ", read_pipe(["git", "show", "-s",
1712                                      "--format=format:%h %s", commit])
1713                ok = True
1714            else:
1715                ok = self.applyCommit(commit)
1716            if ok:
1717                applied.append(commit)
1718            else:
1719                if self.prepare_p4_only and i < last:
1720                    print "Processing only the first commit due to option" \
1721                          " --prepare-p4-only"
1722                    break
1723                if i < last:
1724                    quit = False
1725                    while True:
1726                        # prompt for what to do, or use the option/variable
1727                        if self.conflict_behavior == "ask":
1728                            print "What do you want to do?"
1729                            response = raw_input("[s]kip this commit but apply"
1730                                                 " the rest, or [q]uit? ")
1731                            if not response:
1732                                continue
1733                        elif self.conflict_behavior == "skip":
1734                            response = "s"
1735                        elif self.conflict_behavior == "quit":
1736                            response = "q"
1737                        else:
1738                            die("Unknown conflict_behavior '%s'" %
1739                                self.conflict_behavior)
1740
1741                        if response[0] == "s":
1742                            print "Skipping this commit, but applying the rest"
1743                            break
1744                        if response[0] == "q":
1745                            print "Quitting"
1746                            quit = True
1747                            break
1748                    if quit:
1749                        break
1750
1751        chdir(self.oldWorkingDirectory)
1752
1753        if self.dry_run:
1754            pass
1755        elif self.prepare_p4_only:
1756            pass
1757        elif len(commits) == len(applied):
1758            print "All commits applied!"
1759
1760            sync = P4Sync()
1761            if self.branch:
1762                sync.branch = self.branch
1763            sync.run([])
1764
1765            rebase = P4Rebase()
1766            rebase.rebase()
1767
1768        else:
1769            if len(applied) == 0:
1770                print "No commits applied."
1771            else:
1772                print "Applied only the commits marked with '*':"
1773                for c in commits:
1774                    if c in applied:
1775                        star = "*"
1776                    else:
1777                        star = " "
1778                    print star, read_pipe(["git", "show", "-s",
1779                                           "--format=format:%h %s",  c])
1780                print "You will have to do 'git p4 sync' and rebase."
1781
1782        if gitConfigBool("git-p4.exportLabels"):
1783            self.exportLabels = True
1784
1785        if self.exportLabels:
1786            p4Labels = getP4Labels(self.depotPath)
1787            gitTags = getGitTags()
1788
1789            missingGitTags = gitTags - p4Labels
1790            self.exportGitTags(missingGitTags)
1791
1792        # exit with error unless everything applied perfectly
1793        if len(commits) != len(applied):
1794                sys.exit(1)
1795
1796        return True
1797
1798class View(object):
1799    """Represent a p4 view ("p4 help views"), and map files in a
1800       repo according to the view."""
1801
1802    def __init__(self, client_name):
1803        self.mappings = []
1804        self.client_prefix = "//%s/" % client_name
1805        # cache results of "p4 where" to lookup client file locations
1806        self.client_spec_path_cache = {}
1807
1808    def append(self, view_line):
1809        """Parse a view line, splitting it into depot and client
1810           sides.  Append to self.mappings, preserving order.  This
1811           is only needed for tag creation."""
1812
1813        # Split the view line into exactly two words.  P4 enforces
1814        # structure on these lines that simplifies this quite a bit.
1815        #
1816        # Either or both words may be double-quoted.
1817        # Single quotes do not matter.
1818        # Double-quote marks cannot occur inside the words.
1819        # A + or - prefix is also inside the quotes.
1820        # There are no quotes unless they contain a space.
1821        # The line is already white-space stripped.
1822        # The two words are separated by a single space.
1823        #
1824        if view_line[0] == '"':
1825            # First word is double quoted.  Find its end.
1826            close_quote_index = view_line.find('"', 1)
1827            if close_quote_index <= 0:
1828                die("No first-word closing quote found: %s" % view_line)
1829            depot_side = view_line[1:close_quote_index]
1830            # skip closing quote and space
1831            rhs_index = close_quote_index + 1 + 1
1832        else:
1833            space_index = view_line.find(" ")
1834            if space_index <= 0:
1835                die("No word-splitting space found: %s" % view_line)
1836            depot_side = view_line[0:space_index]
1837            rhs_index = space_index + 1
1838
1839        # prefix + means overlay on previous mapping
1840        if depot_side.startswith("+"):
1841            depot_side = depot_side[1:]
1842
1843        # prefix - means exclude this path, leave out of mappings
1844        exclude = False
1845        if depot_side.startswith("-"):
1846            exclude = True
1847            depot_side = depot_side[1:]
1848
1849        if not exclude:
1850            self.mappings.append(depot_side)
1851
1852    def convert_client_path(self, clientFile):
1853        # chop off //client/ part to make it relative
1854        if not clientFile.startswith(self.client_prefix):
1855            die("No prefix '%s' on clientFile '%s'" %
1856                (self.client_prefix, clientFile))
1857        return clientFile[len(self.client_prefix):]
1858
1859    def update_client_spec_path_cache(self, files):
1860        """ Caching file paths by "p4 where" batch query """
1861
1862        # List depot file paths exclude that already cached
1863        fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
1864
1865        if len(fileArgs) == 0:
1866            return  # All files in cache
1867
1868        where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
1869        for res in where_result:
1870            if "code" in res and res["code"] == "error":
1871                # assume error is "... file(s) not in client view"
1872                continue
1873            if "clientFile" not in res:
1874                die("No clientFile in 'p4 where' output")
1875            if "unmap" in res:
1876                # it will list all of them, but only one not unmap-ped
1877                continue
1878            self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
1879
1880        # not found files or unmap files set to ""
1881        for depotFile in fileArgs:
1882            if depotFile not in self.client_spec_path_cache:
1883                self.client_spec_path_cache[depotFile] = ""
1884
1885    def map_in_client(self, depot_path):
1886        """Return the relative location in the client where this
1887           depot file should live.  Returns "" if the file should
1888           not be mapped in the client."""
1889
1890        if depot_path in self.client_spec_path_cache:
1891            return self.client_spec_path_cache[depot_path]
1892
1893        die( "Error: %s is not found in client spec path" % depot_path )
1894        return ""
1895
1896class P4Sync(Command, P4UserMap):
1897    delete_actions = ( "delete", "move/delete", "purge" )
1898
1899    def __init__(self):
1900        Command.__init__(self)
1901        P4UserMap.__init__(self)
1902        self.options = [
1903                optparse.make_option("--branch", dest="branch"),
1904                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1905                optparse.make_option("--changesfile", dest="changesFile"),
1906                optparse.make_option("--silent", dest="silent", action="store_true"),
1907                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1908                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1909                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1910                                     help="Import into refs/heads/ , not refs/remotes"),
1911                optparse.make_option("--max-changes", dest="maxChanges"),
1912                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1913                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1914                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1915                                     help="Only sync files that are included in the Perforce Client Spec")
1916        ]
1917        self.description = """Imports from Perforce into a git repository.\n
1918    example:
1919    //depot/my/project/ -- to import the current head
1920    //depot/my/project/@all -- to import everything
1921    //depot/my/project/@1,6 -- to import only from revision 1 to 6
1922
1923    (a ... is not needed in the path p4 specification, it's added implicitly)"""
1924
1925        self.usage += " //depot/path[@revRange]"
1926        self.silent = False
1927        self.createdBranches = set()
1928        self.committedChanges = set()
1929        self.branch = ""
1930        self.detectBranches = False
1931        self.detectLabels = False
1932        self.importLabels = False
1933        self.changesFile = ""
1934        self.syncWithOrigin = True
1935        self.importIntoRemotes = True
1936        self.maxChanges = ""
1937        self.keepRepoPath = False
1938        self.depotPaths = None
1939        self.p4BranchesInGit = []
1940        self.cloneExclude = []
1941        self.useClientSpec = False
1942        self.useClientSpec_from_options = False
1943        self.clientSpecDirs = None
1944        self.tempBranches = []
1945        self.tempBranchLocation = "git-p4-tmp"
1946
1947        if gitConfig("git-p4.syncFromOrigin") == "false":
1948            self.syncWithOrigin = False
1949
1950    # Force a checkpoint in fast-import and wait for it to finish
1951    def checkpoint(self):
1952        self.gitStream.write("checkpoint\n\n")
1953        self.gitStream.write("progress checkpoint\n\n")
1954        out = self.gitOutput.readline()
1955        if self.verbose:
1956            print "checkpoint finished: " + out
1957
1958    def extractFilesFromCommit(self, commit):
1959        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1960                             for path in self.cloneExclude]
1961        files = []
1962        fnum = 0
1963        while commit.has_key("depotFile%s" % fnum):
1964            path =  commit["depotFile%s" % fnum]
1965
1966            if [p for p in self.cloneExclude
1967                if p4PathStartsWith(path, p)]:
1968                found = False
1969            else:
1970                found = [p for p in self.depotPaths
1971                         if p4PathStartsWith(path, p)]
1972            if not found:
1973                fnum = fnum + 1
1974                continue
1975
1976            file = {}
1977            file["path"] = path
1978            file["rev"] = commit["rev%s" % fnum]
1979            file["action"] = commit["action%s" % fnum]
1980            file["type"] = commit["type%s" % fnum]
1981            files.append(file)
1982            fnum = fnum + 1
1983        return files
1984
1985    def stripRepoPath(self, path, prefixes):
1986        """When streaming files, this is called to map a p4 depot path
1987           to where it should go in git.  The prefixes are either
1988           self.depotPaths, or self.branchPrefixes in the case of
1989           branch detection."""
1990
1991        if self.useClientSpec:
1992            # branch detection moves files up a level (the branch name)
1993            # from what client spec interpretation gives
1994            path = self.clientSpecDirs.map_in_client(path)
1995            if self.detectBranches:
1996                for b in self.knownBranches:
1997                    if path.startswith(b + "/"):
1998                        path = path[len(b)+1:]
1999
2000        elif self.keepRepoPath:
2001            # Preserve everything in relative path name except leading
2002            # //depot/; just look at first prefix as they all should
2003            # be in the same depot.
2004            depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2005            if p4PathStartsWith(path, depot):
2006                path = path[len(depot):]
2007
2008        else:
2009            for p in prefixes:
2010                if p4PathStartsWith(path, p):
2011                    path = path[len(p):]
2012                    break
2013
2014        path = wildcard_decode(path)
2015        return path
2016
2017    def splitFilesIntoBranches(self, commit):
2018        """Look at each depotFile in the commit to figure out to what
2019           branch it belongs."""
2020
2021        if self.clientSpecDirs:
2022            files = self.extractFilesFromCommit(commit)
2023            self.clientSpecDirs.update_client_spec_path_cache(files)
2024
2025        branches = {}
2026        fnum = 0
2027        while commit.has_key("depotFile%s" % fnum):
2028            path =  commit["depotFile%s" % fnum]
2029            found = [p for p in self.depotPaths
2030                     if p4PathStartsWith(path, p)]
2031            if not found:
2032                fnum = fnum + 1
2033                continue
2034
2035            file = {}
2036            file["path"] = path
2037            file["rev"] = commit["rev%s" % fnum]
2038            file["action"] = commit["action%s" % fnum]
2039            file["type"] = commit["type%s" % fnum]
2040            fnum = fnum + 1
2041
2042            # start with the full relative path where this file would
2043            # go in a p4 client
2044            if self.useClientSpec:
2045                relPath = self.clientSpecDirs.map_in_client(path)
2046            else:
2047                relPath = self.stripRepoPath(path, self.depotPaths)
2048
2049            for branch in self.knownBranches.keys():
2050                # add a trailing slash so that a commit into qt/4.2foo
2051                # doesn't end up in qt/4.2, e.g.
2052                if relPath.startswith(branch + "/"):
2053                    if branch not in branches:
2054                        branches[branch] = []
2055                    branches[branch].append(file)
2056                    break
2057
2058        return branches
2059
2060    # output one file from the P4 stream
2061    # - helper for streamP4Files
2062
2063    def streamOneP4File(self, file, contents):
2064        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2065        if verbose:
2066            sys.stderr.write("%s\n" % relPath)
2067
2068        (type_base, type_mods) = split_p4_type(file["type"])
2069
2070        git_mode = "100644"
2071        if "x" in type_mods:
2072            git_mode = "100755"
2073        if type_base == "symlink":
2074            git_mode = "120000"
2075            # p4 print on a symlink sometimes contains "target\n";
2076            # if it does, remove the newline
2077            data = ''.join(contents)
2078            if not data:
2079                # Some version of p4 allowed creating a symlink that pointed
2080                # to nothing.  This causes p4 errors when checking out such
2081                # a change, and errors here too.  Work around it by ignoring
2082                # the bad symlink; hopefully a future change fixes it.
2083                print "\nIgnoring empty symlink in %s" % file['depotFile']
2084                return
2085            elif data[-1] == '\n':
2086                contents = [data[:-1]]
2087            else:
2088                contents = [data]
2089
2090        if type_base == "utf16":
2091            # p4 delivers different text in the python output to -G
2092            # than it does when using "print -o", or normal p4 client
2093            # operations.  utf16 is converted to ascii or utf8, perhaps.
2094            # But ascii text saved as -t utf16 is completely mangled.
2095            # Invoke print -o to get the real contents.
2096            #
2097            # On windows, the newlines will always be mangled by print, so put
2098            # them back too.  This is not needed to the cygwin windows version,
2099            # just the native "NT" type.
2100            #
2101            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
2102            if p4_version_string().find("/NT") >= 0:
2103                text = text.replace("\r\n", "\n")
2104            contents = [ text ]
2105
2106        if type_base == "apple":
2107            # Apple filetype files will be streamed as a concatenation of
2108            # its appledouble header and the contents.  This is useless
2109            # on both macs and non-macs.  If using "print -q -o xx", it
2110            # will create "xx" with the data, and "%xx" with the header.
2111            # This is also not very useful.
2112            #
2113            # Ideally, someday, this script can learn how to generate
2114            # appledouble files directly and import those to git, but
2115            # non-mac machines can never find a use for apple filetype.
2116            print "\nIgnoring apple filetype file %s" % file['depotFile']
2117            return
2118
2119        # Note that we do not try to de-mangle keywords on utf16 files,
2120        # even though in theory somebody may want that.
2121        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2122        if pattern:
2123            regexp = re.compile(pattern, re.VERBOSE)
2124            text = ''.join(contents)
2125            text = regexp.sub(r'$\1$', text)
2126            contents = [ text ]
2127
2128        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2129
2130        # total length...
2131        length = 0
2132        for d in contents:
2133            length = length + len(d)
2134
2135        self.gitStream.write("data %d\n" % length)
2136        for d in contents:
2137            self.gitStream.write(d)
2138        self.gitStream.write("\n")
2139
2140    def streamOneP4Deletion(self, file):
2141        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2142        if verbose:
2143            sys.stderr.write("delete %s\n" % relPath)
2144        self.gitStream.write("D %s\n" % relPath)
2145
2146    # handle another chunk of streaming data
2147    def streamP4FilesCb(self, marshalled):
2148
2149        # catch p4 errors and complain
2150        err = None
2151        if "code" in marshalled:
2152            if marshalled["code"] == "error":
2153                if "data" in marshalled:
2154                    err = marshalled["data"].rstrip()
2155        if err:
2156            f = None
2157            if self.stream_have_file_info:
2158                if "depotFile" in self.stream_file:
2159                    f = self.stream_file["depotFile"]
2160            # force a failure in fast-import, else an empty
2161            # commit will be made
2162            self.gitStream.write("\n")
2163            self.gitStream.write("die-now\n")
2164            self.gitStream.close()
2165            # ignore errors, but make sure it exits first
2166            self.importProcess.wait()
2167            if f:
2168                die("Error from p4 print for %s: %s" % (f, err))
2169            else:
2170                die("Error from p4 print: %s" % err)
2171
2172        if marshalled.has_key('depotFile') and self.stream_have_file_info:
2173            # start of a new file - output the old one first
2174            self.streamOneP4File(self.stream_file, self.stream_contents)
2175            self.stream_file = {}
2176            self.stream_contents = []
2177            self.stream_have_file_info = False
2178
2179        # pick up the new file information... for the
2180        # 'data' field we need to append to our array
2181        for k in marshalled.keys():
2182            if k == 'data':
2183                self.stream_contents.append(marshalled['data'])
2184            else:
2185                self.stream_file[k] = marshalled[k]
2186
2187        self.stream_have_file_info = True
2188
2189    # Stream directly from "p4 files" into "git fast-import"
2190    def streamP4Files(self, files):
2191        filesForCommit = []
2192        filesToRead = []
2193        filesToDelete = []
2194
2195        for f in files:
2196            # if using a client spec, only add the files that have
2197            # a path in the client
2198            if self.clientSpecDirs:
2199                if self.clientSpecDirs.map_in_client(f['path']) == "":
2200                    continue
2201
2202            filesForCommit.append(f)
2203            if f['action'] in self.delete_actions:
2204                filesToDelete.append(f)
2205            else:
2206                filesToRead.append(f)
2207
2208        # deleted files...
2209        for f in filesToDelete:
2210            self.streamOneP4Deletion(f)
2211
2212        if len(filesToRead) > 0:
2213            self.stream_file = {}
2214            self.stream_contents = []
2215            self.stream_have_file_info = False
2216
2217            # curry self argument
2218            def streamP4FilesCbSelf(entry):
2219                self.streamP4FilesCb(entry)
2220
2221            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2222
2223            p4CmdList(["-x", "-", "print"],
2224                      stdin=fileArgs,
2225                      cb=streamP4FilesCbSelf)
2226
2227            # do the last chunk
2228            if self.stream_file.has_key('depotFile'):
2229                self.streamOneP4File(self.stream_file, self.stream_contents)
2230
2231    def make_email(self, userid):
2232        if userid in self.users:
2233            return self.users[userid]
2234        else:
2235            return "%s <a@b>" % userid
2236
2237    # Stream a p4 tag
2238    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2239        if verbose:
2240            print "writing tag %s for commit %s" % (labelName, commit)
2241        gitStream.write("tag %s\n" % labelName)
2242        gitStream.write("from %s\n" % commit)
2243
2244        if labelDetails.has_key('Owner'):
2245            owner = labelDetails["Owner"]
2246        else:
2247            owner = None
2248
2249        # Try to use the owner of the p4 label, or failing that,
2250        # the current p4 user id.
2251        if owner:
2252            email = self.make_email(owner)
2253        else:
2254            email = self.make_email(self.p4UserId())
2255        tagger = "%s %s %s" % (email, epoch, self.tz)
2256
2257        gitStream.write("tagger %s\n" % tagger)
2258
2259        print "labelDetails=",labelDetails
2260        if labelDetails.has_key('Description'):
2261            description = labelDetails['Description']
2262        else:
2263            description = 'Label from git p4'
2264
2265        gitStream.write("data %d\n" % len(description))
2266        gitStream.write(description)
2267        gitStream.write("\n")
2268
2269    def commit(self, details, files, branch, parent = ""):
2270        epoch = details["time"]
2271        author = details["user"]
2272
2273        if self.verbose:
2274            print "commit into %s" % branch
2275
2276        # start with reading files; if that fails, we should not
2277        # create a commit.
2278        new_files = []
2279        for f in files:
2280            if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2281                new_files.append (f)
2282            else:
2283                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2284
2285        if self.clientSpecDirs:
2286            self.clientSpecDirs.update_client_spec_path_cache(files)
2287
2288        self.gitStream.write("commit %s\n" % branch)
2289#        gitStream.write("mark :%s\n" % details["change"])
2290        self.committedChanges.add(int(details["change"]))
2291        committer = ""
2292        if author not in self.users:
2293            self.getUserMapFromPerforceServer()
2294        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2295
2296        self.gitStream.write("committer %s\n" % committer)
2297
2298        self.gitStream.write("data <<EOT\n")
2299        self.gitStream.write(details["desc"])
2300        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2301                             (','.join(self.branchPrefixes), details["change"]))
2302        if len(details['options']) > 0:
2303            self.gitStream.write(": options = %s" % details['options'])
2304        self.gitStream.write("]\nEOT\n\n")
2305
2306        if len(parent) > 0:
2307            if self.verbose:
2308                print "parent %s" % parent
2309            self.gitStream.write("from %s\n" % parent)
2310
2311        self.streamP4Files(new_files)
2312        self.gitStream.write("\n")
2313
2314        change = int(details["change"])
2315
2316        if self.labels.has_key(change):
2317            label = self.labels[change]
2318            labelDetails = label[0]
2319            labelRevisions = label[1]
2320            if self.verbose:
2321                print "Change %s is labelled %s" % (change, labelDetails)
2322
2323            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2324                                                for p in self.branchPrefixes])
2325
2326            if len(files) == len(labelRevisions):
2327
2328                cleanedFiles = {}
2329                for info in files:
2330                    if info["action"] in self.delete_actions:
2331                        continue
2332                    cleanedFiles[info["depotFile"]] = info["rev"]
2333
2334                if cleanedFiles == labelRevisions:
2335                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2336
2337                else:
2338                    if not self.silent:
2339                        print ("Tag %s does not match with change %s: files do not match."
2340                               % (labelDetails["label"], change))
2341
2342            else:
2343                if not self.silent:
2344                    print ("Tag %s does not match with change %s: file count is different."
2345                           % (labelDetails["label"], change))
2346
2347    # Build a dictionary of changelists and labels, for "detect-labels" option.
2348    def getLabels(self):
2349        self.labels = {}
2350
2351        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2352        if len(l) > 0 and not self.silent:
2353            print "Finding files belonging to labels in %s" % `self.depotPaths`
2354
2355        for output in l:
2356            label = output["label"]
2357            revisions = {}
2358            newestChange = 0
2359            if self.verbose:
2360                print "Querying files for label %s" % label
2361            for file in p4CmdList(["files"] +
2362                                      ["%s...@%s" % (p, label)
2363                                          for p in self.depotPaths]):
2364                revisions[file["depotFile"]] = file["rev"]
2365                change = int(file["change"])
2366                if change > newestChange:
2367                    newestChange = change
2368
2369            self.labels[newestChange] = [output, revisions]
2370
2371        if self.verbose:
2372            print "Label changes: %s" % self.labels.keys()
2373
2374    # Import p4 labels as git tags. A direct mapping does not
2375    # exist, so assume that if all the files are at the same revision
2376    # then we can use that, or it's something more complicated we should
2377    # just ignore.
2378    def importP4Labels(self, stream, p4Labels):
2379        if verbose:
2380            print "import p4 labels: " + ' '.join(p4Labels)
2381
2382        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2383        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2384        if len(validLabelRegexp) == 0:
2385            validLabelRegexp = defaultLabelRegexp
2386        m = re.compile(validLabelRegexp)
2387
2388        for name in p4Labels:
2389            commitFound = False
2390
2391            if not m.match(name):
2392                if verbose:
2393                    print "label %s does not match regexp %s" % (name,validLabelRegexp)
2394                continue
2395
2396            if name in ignoredP4Labels:
2397                continue
2398
2399            labelDetails = p4CmdList(['label', "-o", name])[0]
2400
2401            # get the most recent changelist for each file in this label
2402            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2403                                for p in self.depotPaths])
2404
2405            if change.has_key('change'):
2406                # find the corresponding git commit; take the oldest commit
2407                changelist = int(change['change'])
2408                gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2409                     "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2410                if len(gitCommit) == 0:
2411                    print "could not find git commit for changelist %d" % changelist
2412                else:
2413                    gitCommit = gitCommit.strip()
2414                    commitFound = True
2415                    # Convert from p4 time format
2416                    try:
2417                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2418                    except ValueError:
2419                        print "Could not convert label time %s" % labelDetails['Update']
2420                        tmwhen = 1
2421
2422                    when = int(time.mktime(tmwhen))
2423                    self.streamTag(stream, name, labelDetails, gitCommit, when)
2424                    if verbose:
2425                        print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2426            else:
2427                if verbose:
2428                    print "Label %s has no changelists - possibly deleted?" % name
2429
2430            if not commitFound:
2431                # We can't import this label; don't try again as it will get very
2432                # expensive repeatedly fetching all the files for labels that will
2433                # never be imported. If the label is moved in the future, the
2434                # ignore will need to be removed manually.
2435                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2436
2437    def guessProjectName(self):
2438        for p in self.depotPaths:
2439            if p.endswith("/"):
2440                p = p[:-1]
2441            p = p[p.strip().rfind("/") + 1:]
2442            if not p.endswith("/"):
2443               p += "/"
2444            return p
2445
2446    def getBranchMapping(self):
2447        lostAndFoundBranches = set()
2448
2449        user = gitConfig("git-p4.branchUser")
2450        if len(user) > 0:
2451            command = "branches -u %s" % user
2452        else:
2453            command = "branches"
2454
2455        for info in p4CmdList(command):
2456            details = p4Cmd(["branch", "-o", info["branch"]])
2457            viewIdx = 0
2458            while details.has_key("View%s" % viewIdx):
2459                paths = details["View%s" % viewIdx].split(" ")
2460                viewIdx = viewIdx + 1
2461                # require standard //depot/foo/... //depot/bar/... mapping
2462                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2463                    continue
2464                source = paths[0]
2465                destination = paths[1]
2466                ## HACK
2467                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2468                    source = source[len(self.depotPaths[0]):-4]
2469                    destination = destination[len(self.depotPaths[0]):-4]
2470
2471                    if destination in self.knownBranches:
2472                        if not self.silent:
2473                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2474                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2475                        continue
2476
2477                    self.knownBranches[destination] = source
2478
2479                    lostAndFoundBranches.discard(destination)
2480
2481                    if source not in self.knownBranches:
2482                        lostAndFoundBranches.add(source)
2483
2484        # Perforce does not strictly require branches to be defined, so we also
2485        # check git config for a branch list.
2486        #
2487        # Example of branch definition in git config file:
2488        # [git-p4]
2489        #   branchList=main:branchA
2490        #   branchList=main:branchB
2491        #   branchList=branchA:branchC
2492        configBranches = gitConfigList("git-p4.branchList")
2493        for branch in configBranches:
2494            if branch:
2495                (source, destination) = branch.split(":")
2496                self.knownBranches[destination] = source
2497
2498                lostAndFoundBranches.discard(destination)
2499
2500                if source not in self.knownBranches:
2501                    lostAndFoundBranches.add(source)
2502
2503
2504        for branch in lostAndFoundBranches:
2505            self.knownBranches[branch] = branch
2506
2507    def getBranchMappingFromGitBranches(self):
2508        branches = p4BranchesInGit(self.importIntoRemotes)
2509        for branch in branches.keys():
2510            if branch == "master":
2511                branch = "main"
2512            else:
2513                branch = branch[len(self.projectName):]
2514            self.knownBranches[branch] = branch
2515
2516    def updateOptionDict(self, d):
2517        option_keys = {}
2518        if self.keepRepoPath:
2519            option_keys['keepRepoPath'] = 1
2520
2521        d["options"] = ' '.join(sorted(option_keys.keys()))
2522
2523    def readOptions(self, d):
2524        self.keepRepoPath = (d.has_key('options')
2525                             and ('keepRepoPath' in d['options']))
2526
2527    def gitRefForBranch(self, branch):
2528        if branch == "main":
2529            return self.refPrefix + "master"
2530
2531        if len(branch) <= 0:
2532            return branch
2533
2534        return self.refPrefix + self.projectName + branch
2535
2536    def gitCommitByP4Change(self, ref, change):
2537        if self.verbose:
2538            print "looking in ref " + ref + " for change %s using bisect..." % change
2539
2540        earliestCommit = ""
2541        latestCommit = parseRevision(ref)
2542
2543        while True:
2544            if self.verbose:
2545                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2546            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2547            if len(next) == 0:
2548                if self.verbose:
2549                    print "argh"
2550                return ""
2551            log = extractLogMessageFromGitCommit(next)
2552            settings = extractSettingsGitLog(log)
2553            currentChange = int(settings['change'])
2554            if self.verbose:
2555                print "current change %s" % currentChange
2556
2557            if currentChange == change:
2558                if self.verbose:
2559                    print "found %s" % next
2560                return next
2561
2562            if currentChange < change:
2563                earliestCommit = "^%s" % next
2564            else:
2565                latestCommit = "%s" % next
2566
2567        return ""
2568
2569    def importNewBranch(self, branch, maxChange):
2570        # make fast-import flush all changes to disk and update the refs using the checkpoint
2571        # command so that we can try to find the branch parent in the git history
2572        self.gitStream.write("checkpoint\n\n");
2573        self.gitStream.flush();
2574        branchPrefix = self.depotPaths[0] + branch + "/"
2575        range = "@1,%s" % maxChange
2576        #print "prefix" + branchPrefix
2577        changes = p4ChangesForPaths([branchPrefix], range)
2578        if len(changes) <= 0:
2579            return False
2580        firstChange = changes[0]
2581        #print "first change in branch: %s" % firstChange
2582        sourceBranch = self.knownBranches[branch]
2583        sourceDepotPath = self.depotPaths[0] + sourceBranch
2584        sourceRef = self.gitRefForBranch(sourceBranch)
2585        #print "source " + sourceBranch
2586
2587        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2588        #print "branch parent: %s" % branchParentChange
2589        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2590        if len(gitParent) > 0:
2591            self.initialParents[self.gitRefForBranch(branch)] = gitParent
2592            #print "parent git commit: %s" % gitParent
2593
2594        self.importChanges(changes)
2595        return True
2596
2597    def searchParent(self, parent, branch, target):
2598        parentFound = False
2599        for blob in read_pipe_lines(["git", "rev-list", "--reverse",
2600                                     "--no-merges", parent]):
2601            blob = blob.strip()
2602            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2603                parentFound = True
2604                if self.verbose:
2605                    print "Found parent of %s in commit %s" % (branch, blob)
2606                break
2607        if parentFound:
2608            return blob
2609        else:
2610            return None
2611
2612    def importChanges(self, changes):
2613        cnt = 1
2614        for change in changes:
2615            description = p4_describe(change)
2616            self.updateOptionDict(description)
2617
2618            if not self.silent:
2619                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2620                sys.stdout.flush()
2621            cnt = cnt + 1
2622
2623            try:
2624                if self.detectBranches:
2625                    branches = self.splitFilesIntoBranches(description)
2626                    for branch in branches.keys():
2627                        ## HACK  --hwn
2628                        branchPrefix = self.depotPaths[0] + branch + "/"
2629                        self.branchPrefixes = [ branchPrefix ]
2630
2631                        parent = ""
2632
2633                        filesForCommit = branches[branch]
2634
2635                        if self.verbose:
2636                            print "branch is %s" % branch
2637
2638                        self.updatedBranches.add(branch)
2639
2640                        if branch not in self.createdBranches:
2641                            self.createdBranches.add(branch)
2642                            parent = self.knownBranches[branch]
2643                            if parent == branch:
2644                                parent = ""
2645                            else:
2646                                fullBranch = self.projectName + branch
2647                                if fullBranch not in self.p4BranchesInGit:
2648                                    if not self.silent:
2649                                        print("\n    Importing new branch %s" % fullBranch);
2650                                    if self.importNewBranch(branch, change - 1):
2651                                        parent = ""
2652                                        self.p4BranchesInGit.append(fullBranch)
2653                                    if not self.silent:
2654                                        print("\n    Resuming with change %s" % change);
2655
2656                                if self.verbose:
2657                                    print "parent determined through known branches: %s" % parent
2658
2659                        branch = self.gitRefForBranch(branch)
2660                        parent = self.gitRefForBranch(parent)
2661
2662                        if self.verbose:
2663                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2664
2665                        if len(parent) == 0 and branch in self.initialParents:
2666                            parent = self.initialParents[branch]
2667                            del self.initialParents[branch]
2668
2669                        blob = None
2670                        if len(parent) > 0:
2671                            tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2672                            if self.verbose:
2673                                print "Creating temporary branch: " + tempBranch
2674                            self.commit(description, filesForCommit, tempBranch)
2675                            self.tempBranches.append(tempBranch)
2676                            self.checkpoint()
2677                            blob = self.searchParent(parent, branch, tempBranch)
2678                        if blob:
2679                            self.commit(description, filesForCommit, branch, blob)
2680                        else:
2681                            if self.verbose:
2682                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2683                            self.commit(description, filesForCommit, branch, parent)
2684                else:
2685                    files = self.extractFilesFromCommit(description)
2686                    self.commit(description, files, self.branch,
2687                                self.initialParent)
2688                    # only needed once, to connect to the previous commit
2689                    self.initialParent = ""
2690            except IOError:
2691                print self.gitError.read()
2692                sys.exit(1)
2693
2694    def importHeadRevision(self, revision):
2695        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2696
2697        details = {}
2698        details["user"] = "git perforce import user"
2699        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2700                           % (' '.join(self.depotPaths), revision))
2701        details["change"] = revision
2702        newestRevision = 0
2703
2704        fileCnt = 0
2705        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2706
2707        for info in p4CmdList(["files"] + fileArgs):
2708
2709            if 'code' in info and info['code'] == 'error':
2710                sys.stderr.write("p4 returned an error: %s\n"
2711                                 % info['data'])
2712                if info['data'].find("must refer to client") >= 0:
2713                    sys.stderr.write("This particular p4 error is misleading.\n")
2714                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
2715                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2716                sys.exit(1)
2717            if 'p4ExitCode' in info:
2718                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2719                sys.exit(1)
2720
2721
2722            change = int(info["change"])
2723            if change > newestRevision:
2724                newestRevision = change
2725
2726            if info["action"] in self.delete_actions:
2727                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2728                #fileCnt = fileCnt + 1
2729                continue
2730
2731            for prop in ["depotFile", "rev", "action", "type" ]:
2732                details["%s%s" % (prop, fileCnt)] = info[prop]
2733
2734            fileCnt = fileCnt + 1
2735
2736        details["change"] = newestRevision
2737
2738        # Use time from top-most change so that all git p4 clones of
2739        # the same p4 repo have the same commit SHA1s.
2740        res = p4_describe(newestRevision)
2741        details["time"] = res["time"]
2742
2743        self.updateOptionDict(details)
2744        try:
2745            self.commit(details, self.extractFilesFromCommit(details), self.branch)
2746        except IOError:
2747            print "IO error with git fast-import. Is your git version recent enough?"
2748            print self.gitError.read()
2749
2750
2751    def run(self, args):
2752        self.depotPaths = []
2753        self.changeRange = ""
2754        self.previousDepotPaths = []
2755        self.hasOrigin = False
2756
2757        # map from branch depot path to parent branch
2758        self.knownBranches = {}
2759        self.initialParents = {}
2760
2761        if self.importIntoRemotes:
2762            self.refPrefix = "refs/remotes/p4/"
2763        else:
2764            self.refPrefix = "refs/heads/p4/"
2765
2766        if self.syncWithOrigin:
2767            self.hasOrigin = originP4BranchesExist()
2768            if self.hasOrigin:
2769                if not self.silent:
2770                    print 'Syncing with origin first, using "git fetch origin"'
2771                system("git fetch origin")
2772
2773        branch_arg_given = bool(self.branch)
2774        if len(self.branch) == 0:
2775            self.branch = self.refPrefix + "master"
2776            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2777                system("git update-ref %s refs/heads/p4" % self.branch)
2778                system("git branch -D p4")
2779
2780        # accept either the command-line option, or the configuration variable
2781        if self.useClientSpec:
2782            # will use this after clone to set the variable
2783            self.useClientSpec_from_options = True
2784        else:
2785            if gitConfigBool("git-p4.useclientspec"):
2786                self.useClientSpec = True
2787        if self.useClientSpec:
2788            self.clientSpecDirs = getClientSpec()
2789
2790        # TODO: should always look at previous commits,
2791        # merge with previous imports, if possible.
2792        if args == []:
2793            if self.hasOrigin:
2794                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2795
2796            # branches holds mapping from branch name to sha1
2797            branches = p4BranchesInGit(self.importIntoRemotes)
2798
2799            # restrict to just this one, disabling detect-branches
2800            if branch_arg_given:
2801                short = self.branch.split("/")[-1]
2802                if short in branches:
2803                    self.p4BranchesInGit = [ short ]
2804            else:
2805                self.p4BranchesInGit = branches.keys()
2806
2807            if len(self.p4BranchesInGit) > 1:
2808                if not self.silent:
2809                    print "Importing from/into multiple branches"
2810                self.detectBranches = True
2811                for branch in branches.keys():
2812                    self.initialParents[self.refPrefix + branch] = \
2813                        branches[branch]
2814
2815            if self.verbose:
2816                print "branches: %s" % self.p4BranchesInGit
2817
2818            p4Change = 0
2819            for branch in self.p4BranchesInGit:
2820                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2821
2822                settings = extractSettingsGitLog(logMsg)
2823
2824                self.readOptions(settings)
2825                if (settings.has_key('depot-paths')
2826                    and settings.has_key ('change')):
2827                    change = int(settings['change']) + 1
2828                    p4Change = max(p4Change, change)
2829
2830                    depotPaths = sorted(settings['depot-paths'])
2831                    if self.previousDepotPaths == []:
2832                        self.previousDepotPaths = depotPaths
2833                    else:
2834                        paths = []
2835                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2836                            prev_list = prev.split("/")
2837                            cur_list = cur.split("/")
2838                            for i in range(0, min(len(cur_list), len(prev_list))):
2839                                if cur_list[i] <> prev_list[i]:
2840                                    i = i - 1
2841                                    break
2842
2843                            paths.append ("/".join(cur_list[:i + 1]))
2844
2845                        self.previousDepotPaths = paths
2846
2847            if p4Change > 0:
2848                self.depotPaths = sorted(self.previousDepotPaths)
2849                self.changeRange = "@%s,#head" % p4Change
2850                if not self.silent and not self.detectBranches:
2851                    print "Performing incremental import into %s git branch" % self.branch
2852
2853        # accept multiple ref name abbreviations:
2854        #    refs/foo/bar/branch -> use it exactly
2855        #    p4/branch -> prepend refs/remotes/ or refs/heads/
2856        #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2857        if not self.branch.startswith("refs/"):
2858            if self.importIntoRemotes:
2859                prepend = "refs/remotes/"
2860            else:
2861                prepend = "refs/heads/"
2862            if not self.branch.startswith("p4/"):
2863                prepend += "p4/"
2864            self.branch = prepend + self.branch
2865
2866        if len(args) == 0 and self.depotPaths:
2867            if not self.silent:
2868                print "Depot paths: %s" % ' '.join(self.depotPaths)
2869        else:
2870            if self.depotPaths and self.depotPaths != args:
2871                print ("previous import used depot path %s and now %s was specified. "
2872                       "This doesn't work!" % (' '.join (self.depotPaths),
2873                                               ' '.join (args)))
2874                sys.exit(1)
2875
2876            self.depotPaths = sorted(args)
2877
2878        revision = ""
2879        self.users = {}
2880
2881        # Make sure no revision specifiers are used when --changesfile
2882        # is specified.
2883        bad_changesfile = False
2884        if len(self.changesFile) > 0:
2885            for p in self.depotPaths:
2886                if p.find("@") >= 0 or p.find("#") >= 0:
2887                    bad_changesfile = True
2888                    break
2889        if bad_changesfile:
2890            die("Option --changesfile is incompatible with revision specifiers")
2891
2892        newPaths = []
2893        for p in self.depotPaths:
2894            if p.find("@") != -1:
2895                atIdx = p.index("@")
2896                self.changeRange = p[atIdx:]
2897                if self.changeRange == "@all":
2898                    self.changeRange = ""
2899                elif ',' not in self.changeRange:
2900                    revision = self.changeRange
2901                    self.changeRange = ""
2902                p = p[:atIdx]
2903            elif p.find("#") != -1:
2904                hashIdx = p.index("#")
2905                revision = p[hashIdx:]
2906                p = p[:hashIdx]
2907            elif self.previousDepotPaths == []:
2908                # pay attention to changesfile, if given, else import
2909                # the entire p4 tree at the head revision
2910                if len(self.changesFile) == 0:
2911                    revision = "#head"
2912
2913            p = re.sub ("\.\.\.$", "", p)
2914            if not p.endswith("/"):
2915                p += "/"
2916
2917            newPaths.append(p)
2918
2919        self.depotPaths = newPaths
2920
2921        # --detect-branches may change this for each branch
2922        self.branchPrefixes = self.depotPaths
2923
2924        self.loadUserMapFromCache()
2925        self.labels = {}
2926        if self.detectLabels:
2927            self.getLabels();
2928
2929        if self.detectBranches:
2930            ## FIXME - what's a P4 projectName ?
2931            self.projectName = self.guessProjectName()
2932
2933            if self.hasOrigin:
2934                self.getBranchMappingFromGitBranches()
2935            else:
2936                self.getBranchMapping()
2937            if self.verbose:
2938                print "p4-git branches: %s" % self.p4BranchesInGit
2939                print "initial parents: %s" % self.initialParents
2940            for b in self.p4BranchesInGit:
2941                if b != "master":
2942
2943                    ## FIXME
2944                    b = b[len(self.projectName):]
2945                self.createdBranches.add(b)
2946
2947        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2948
2949        self.importProcess = subprocess.Popen(["git", "fast-import"],
2950                                              stdin=subprocess.PIPE,
2951                                              stdout=subprocess.PIPE,
2952                                              stderr=subprocess.PIPE);
2953        self.gitOutput = self.importProcess.stdout
2954        self.gitStream = self.importProcess.stdin
2955        self.gitError = self.importProcess.stderr
2956
2957        if revision:
2958            self.importHeadRevision(revision)
2959        else:
2960            changes = []
2961
2962            if len(self.changesFile) > 0:
2963                output = open(self.changesFile).readlines()
2964                changeSet = set()
2965                for line in output:
2966                    changeSet.add(int(line))
2967
2968                for change in changeSet:
2969                    changes.append(change)
2970
2971                changes.sort()
2972            else:
2973                # catch "git p4 sync" with no new branches, in a repo that
2974                # does not have any existing p4 branches
2975                if len(args) == 0:
2976                    if not self.p4BranchesInGit:
2977                        die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
2978
2979                    # The default branch is master, unless --branch is used to
2980                    # specify something else.  Make sure it exists, or complain
2981                    # nicely about how to use --branch.
2982                    if not self.detectBranches:
2983                        if not branch_exists(self.branch):
2984                            if branch_arg_given:
2985                                die("Error: branch %s does not exist." % self.branch)
2986                            else:
2987                                die("Error: no branch %s; perhaps specify one with --branch." %
2988                                    self.branch)
2989
2990                if self.verbose:
2991                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2992                                                              self.changeRange)
2993                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2994
2995                if len(self.maxChanges) > 0:
2996                    changes = changes[:min(int(self.maxChanges), len(changes))]
2997
2998            if len(changes) == 0:
2999                if not self.silent:
3000                    print "No changes to import!"
3001            else:
3002                if not self.silent and not self.detectBranches:
3003                    print "Import destination: %s" % self.branch
3004
3005                self.updatedBranches = set()
3006
3007                if not self.detectBranches:
3008                    if args:
3009                        # start a new branch
3010                        self.initialParent = ""
3011                    else:
3012                        # build on a previous revision
3013                        self.initialParent = parseRevision(self.branch)
3014
3015                self.importChanges(changes)
3016
3017                if not self.silent:
3018                    print ""
3019                    if len(self.updatedBranches) > 0:
3020                        sys.stdout.write("Updated branches: ")
3021                        for b in self.updatedBranches:
3022                            sys.stdout.write("%s " % b)
3023                        sys.stdout.write("\n")
3024
3025        if gitConfigBool("git-p4.importLabels"):
3026            self.importLabels = True
3027
3028        if self.importLabels:
3029            p4Labels = getP4Labels(self.depotPaths)
3030            gitTags = getGitTags()
3031
3032            missingP4Labels = p4Labels - gitTags
3033            self.importP4Labels(self.gitStream, missingP4Labels)
3034
3035        self.gitStream.close()
3036        if self.importProcess.wait() != 0:
3037            die("fast-import failed: %s" % self.gitError.read())
3038        self.gitOutput.close()
3039        self.gitError.close()
3040
3041        # Cleanup temporary branches created during import
3042        if self.tempBranches != []:
3043            for branch in self.tempBranches:
3044                read_pipe("git update-ref -d %s" % branch)
3045            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3046
3047        # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3048        # a convenient shortcut refname "p4".
3049        if self.importIntoRemotes:
3050            head_ref = self.refPrefix + "HEAD"
3051            if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3052                system(["git", "symbolic-ref", head_ref, self.branch])
3053
3054        return True
3055
3056class P4Rebase(Command):
3057    def __init__(self):
3058        Command.__init__(self)
3059        self.options = [
3060                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3061        ]
3062        self.importLabels = False
3063        self.description = ("Fetches the latest revision from perforce and "
3064                            + "rebases the current work (branch) against it")
3065
3066    def run(self, args):
3067        sync = P4Sync()
3068        sync.importLabels = self.importLabels
3069        sync.run([])
3070
3071        return self.rebase()
3072
3073    def rebase(self):
3074        if os.system("git update-index --refresh") != 0:
3075            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.");
3076        if len(read_pipe("git diff-index HEAD --")) > 0:
3077            die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3078
3079        [upstream, settings] = findUpstreamBranchPoint()
3080        if len(upstream) == 0:
3081            die("Cannot find upstream branchpoint for rebase")
3082
3083        # the branchpoint may be p4/foo~3, so strip off the parent
3084        upstream = re.sub("~[0-9]+$", "", upstream)
3085
3086        print "Rebasing the current branch onto %s" % upstream
3087        oldHead = read_pipe("git rev-parse HEAD").strip()
3088        system("git rebase %s" % upstream)
3089        system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3090        return True
3091
3092class P4Clone(P4Sync):
3093    def __init__(self):
3094        P4Sync.__init__(self)
3095        self.description = "Creates a new git repository and imports from Perforce into it"
3096        self.usage = "usage: %prog [options] //depot/path[@revRange]"
3097        self.options += [
3098            optparse.make_option("--destination", dest="cloneDestination",
3099                                 action='store', default=None,
3100                                 help="where to leave result of the clone"),
3101            optparse.make_option("-/", dest="cloneExclude",
3102                                 action="append", type="string",
3103                                 help="exclude depot path"),
3104            optparse.make_option("--bare", dest="cloneBare",
3105                                 action="store_true", default=False),
3106        ]
3107        self.cloneDestination = None
3108        self.needsGit = False
3109        self.cloneBare = False
3110
3111    # This is required for the "append" cloneExclude action
3112    def ensure_value(self, attr, value):
3113        if not hasattr(self, attr) or getattr(self, attr) is None:
3114            setattr(self, attr, value)
3115        return getattr(self, attr)
3116
3117    def defaultDestination(self, args):
3118        ## TODO: use common prefix of args?
3119        depotPath = args[0]
3120        depotDir = re.sub("(@[^@]*)$", "", depotPath)
3121        depotDir = re.sub("(#[^#]*)$", "", depotDir)
3122        depotDir = re.sub(r"\.\.\.$", "", depotDir)
3123        depotDir = re.sub(r"/$", "", depotDir)
3124        return os.path.split(depotDir)[1]
3125
3126    def run(self, args):
3127        if len(args) < 1:
3128            return False
3129
3130        if self.keepRepoPath and not self.cloneDestination:
3131            sys.stderr.write("Must specify destination for --keep-path\n")
3132            sys.exit(1)
3133
3134        depotPaths = args
3135
3136        if not self.cloneDestination and len(depotPaths) > 1:
3137            self.cloneDestination = depotPaths[-1]
3138            depotPaths = depotPaths[:-1]
3139
3140        self.cloneExclude = ["/"+p for p in self.cloneExclude]
3141        for p in depotPaths:
3142            if not p.startswith("//"):
3143                sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3144                return False
3145
3146        if not self.cloneDestination:
3147            self.cloneDestination = self.defaultDestination(args)
3148
3149        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3150
3151        if not os.path.exists(self.cloneDestination):
3152            os.makedirs(self.cloneDestination)
3153        chdir(self.cloneDestination)
3154
3155        init_cmd = [ "git", "init" ]
3156        if self.cloneBare:
3157            init_cmd.append("--bare")
3158        retcode = subprocess.call(init_cmd)
3159        if retcode:
3160            raise CalledProcessError(retcode, init_cmd)
3161
3162        if not P4Sync.run(self, depotPaths):
3163            return False
3164
3165        # create a master branch and check out a work tree
3166        if gitBranchExists(self.branch):
3167            system([ "git", "branch", "master", self.branch ])
3168            if not self.cloneBare:
3169                system([ "git", "checkout", "-f" ])
3170        else:
3171            print 'Not checking out any branch, use ' \
3172                  '"git checkout -q -b master <branch>"'
3173
3174        # auto-set this variable if invoked with --use-client-spec
3175        if self.useClientSpec_from_options:
3176            system("git config --bool git-p4.useclientspec true")
3177
3178        return True
3179
3180class P4Branches(Command):
3181    def __init__(self):
3182        Command.__init__(self)
3183        self.options = [ ]
3184        self.description = ("Shows the git branches that hold imports and their "
3185                            + "corresponding perforce depot paths")
3186        self.verbose = False
3187
3188    def run(self, args):
3189        if originP4BranchesExist():
3190            createOrUpdateBranchesFromOrigin()
3191
3192        cmdline = "git rev-parse --symbolic "
3193        cmdline += " --remotes"
3194
3195        for line in read_pipe_lines(cmdline):
3196            line = line.strip()
3197
3198            if not line.startswith('p4/') or line == "p4/HEAD":
3199                continue
3200            branch = line
3201
3202            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3203            settings = extractSettingsGitLog(log)
3204
3205            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3206        return True
3207
3208class HelpFormatter(optparse.IndentedHelpFormatter):
3209    def __init__(self):
3210        optparse.IndentedHelpFormatter.__init__(self)
3211
3212    def format_description(self, description):
3213        if description:
3214            return description + "\n"
3215        else:
3216            return ""
3217
3218def printUsage(commands):
3219    print "usage: %s <command> [options]" % sys.argv[0]
3220    print ""
3221    print "valid commands: %s" % ", ".join(commands)
3222    print ""
3223    print "Try %s <command> --help for command specific help." % sys.argv[0]
3224    print ""
3225
3226commands = {
3227    "debug" : P4Debug,
3228    "submit" : P4Submit,
3229    "commit" : P4Submit,
3230    "sync" : P4Sync,
3231    "rebase" : P4Rebase,
3232    "clone" : P4Clone,
3233    "rollback" : P4RollBack,
3234    "branches" : P4Branches
3235}
3236
3237
3238def main():
3239    if len(sys.argv[1:]) == 0:
3240        printUsage(commands.keys())
3241        sys.exit(2)
3242
3243    cmdName = sys.argv[1]
3244    try:
3245        klass = commands[cmdName]
3246        cmd = klass()
3247    except KeyError:
3248        print "unknown command %s" % cmdName
3249        print ""
3250        printUsage(commands.keys())
3251        sys.exit(2)
3252
3253    options = cmd.options
3254    cmd.gitdir = os.environ.get("GIT_DIR", None)
3255
3256    args = sys.argv[2:]
3257
3258    options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3259    if cmd.needsGit:
3260        options.append(optparse.make_option("--git-dir", dest="gitdir"))
3261
3262    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3263                                   options,
3264                                   description = cmd.description,
3265                                   formatter = HelpFormatter())
3266
3267    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3268    global verbose
3269    verbose = cmd.verbose
3270    if cmd.needsGit:
3271        if cmd.gitdir == None:
3272            cmd.gitdir = os.path.abspath(".git")
3273            if not isValidGitDir(cmd.gitdir):
3274                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3275                if os.path.exists(cmd.gitdir):
3276                    cdup = read_pipe("git rev-parse --show-cdup").strip()
3277                    if len(cdup) > 0:
3278                        chdir(cdup);
3279
3280        if not isValidGitDir(cmd.gitdir):
3281            if isValidGitDir(cmd.gitdir + "/.git"):
3282                cmd.gitdir += "/.git"
3283            else:
3284                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3285
3286        os.environ["GIT_DIR"] = cmd.gitdir
3287
3288    if not cmd.run(args):
3289        parser.print_help()
3290        sys.exit(2)
3291
3292
3293if __name__ == '__main__':
3294    main()