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