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