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