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