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