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