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