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