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