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