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