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