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