git-p4.pyon commit git p4: fix writable file after rename or copy (b6ad6dc)
   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(f, *options):
 139    p4_system(["sync"] + list(options) + [f])
 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        pureRenameCopy = set()
1042        filesToChangeExecBit = {}
1043
1044        for line in diff:
1045            diff = parseDiffTreeEntry(line)
1046            modifier = diff['status']
1047            path = diff['src']
1048            if modifier == "M":
1049                p4_edit(path)
1050                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1051                    filesToChangeExecBit[path] = diff['dst_mode']
1052                editedFiles.add(path)
1053            elif modifier == "A":
1054                filesToAdd.add(path)
1055                filesToChangeExecBit[path] = diff['dst_mode']
1056                if path in filesToDelete:
1057                    filesToDelete.remove(path)
1058            elif modifier == "D":
1059                filesToDelete.add(path)
1060                if path in filesToAdd:
1061                    filesToAdd.remove(path)
1062            elif modifier == "C":
1063                src, dest = diff['src'], diff['dst']
1064                p4_integrate(src, dest)
1065                pureRenameCopy.add(dest)
1066                if diff['src_sha1'] != diff['dst_sha1']:
1067                    p4_edit(dest)
1068                    pureRenameCopy.discard(dest)
1069                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1070                    p4_edit(dest)
1071                    pureRenameCopy.discard(dest)
1072                    filesToChangeExecBit[dest] = diff['dst_mode']
1073                os.unlink(dest)
1074                editedFiles.add(dest)
1075            elif modifier == "R":
1076                src, dest = diff['src'], diff['dst']
1077                p4_integrate(src, dest)
1078                if diff['src_sha1'] != diff['dst_sha1']:
1079                    p4_edit(dest)
1080                else:
1081                    pureRenameCopy.add(dest)
1082                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1083                    p4_edit(dest)
1084                    filesToChangeExecBit[dest] = diff['dst_mode']
1085                os.unlink(dest)
1086                editedFiles.add(dest)
1087                filesToDelete.add(src)
1088            else:
1089                die("unknown modifier %s for %s" % (modifier, path))
1090
1091        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
1092        patchcmd = diffcmd + " | git apply "
1093        tryPatchCmd = patchcmd + "--check -"
1094        applyPatchCmd = patchcmd + "--check --apply -"
1095        patch_succeeded = True
1096
1097        if os.system(tryPatchCmd) != 0:
1098            fixed_rcs_keywords = False
1099            patch_succeeded = False
1100            print "Unfortunately applying the change failed!"
1101
1102            # Patch failed, maybe it's just RCS keyword woes. Look through
1103            # the patch to see if that's possible.
1104            if gitConfig("git-p4.attemptRCSCleanup","--bool") == "true":
1105                file = None
1106                pattern = None
1107                kwfiles = {}
1108                for file in editedFiles | filesToDelete:
1109                    # did this file's delta contain RCS keywords?
1110                    pattern = p4_keywords_regexp_for_file(file)
1111
1112                    if pattern:
1113                        # this file is a possibility...look for RCS keywords.
1114                        regexp = re.compile(pattern, re.VERBOSE)
1115                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1116                            if regexp.search(line):
1117                                if verbose:
1118                                    print "got keyword match on %s in %s in %s" % (pattern, line, file)
1119                                kwfiles[file] = pattern
1120                                break
1121
1122                for file in kwfiles:
1123                    if verbose:
1124                        print "zapping %s with %s" % (line,pattern)
1125                    self.patchRCSKeywords(file, kwfiles[file])
1126                    fixed_rcs_keywords = True
1127
1128            if fixed_rcs_keywords:
1129                print "Retrying the patch with RCS keywords cleaned up"
1130                if os.system(tryPatchCmd) == 0:
1131                    patch_succeeded = True
1132
1133        if not patch_succeeded:
1134            print "What do you want to do?"
1135            response = "x"
1136            while response != "s" and response != "a" and response != "w":
1137                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
1138                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
1139            if response == "s":
1140                print "Skipping! Good luck with the next patches..."
1141                for f in editedFiles:
1142                    p4_revert(f)
1143                for f in filesToAdd:
1144                    os.remove(f)
1145                return
1146            elif response == "a":
1147                os.system(applyPatchCmd)
1148                if len(filesToAdd) > 0:
1149                    print "You may also want to call p4 add on the following files:"
1150                    print " ".join(filesToAdd)
1151                if len(filesToDelete):
1152                    print "The following files should be scheduled for deletion with p4 delete:"
1153                    print " ".join(filesToDelete)
1154                die("Please resolve and submit the conflict manually and "
1155                    + "continue afterwards with git p4 submit --continue")
1156            elif response == "w":
1157                system(diffcmd + " > patch.txt")
1158                print "Patch saved to patch.txt in %s !" % self.clientPath
1159                die("Please resolve and submit the conflict manually and "
1160                    "continue afterwards with git p4 submit --continue")
1161
1162        system(applyPatchCmd)
1163
1164        for f in filesToAdd:
1165            p4_add(f)
1166        for f in filesToDelete:
1167            p4_revert(f)
1168            p4_delete(f)
1169
1170        # Set/clear executable bits
1171        for f in filesToChangeExecBit.keys():
1172            mode = filesToChangeExecBit[f]
1173            setP4ExecBit(f, mode)
1174
1175        logMessage = extractLogMessageFromGitCommit(id)
1176        logMessage = logMessage.strip()
1177
1178        template = self.prepareSubmitTemplate()
1179
1180        if self.interactive:
1181            submitTemplate = self.prepareLogMessage(template, logMessage)
1182
1183            if self.preserveUser:
1184               submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
1185
1186            if os.environ.has_key("P4DIFF"):
1187                del(os.environ["P4DIFF"])
1188            diff = ""
1189            for editedFile in editedFiles:
1190                diff += p4_read_pipe(['diff', '-du', editedFile])
1191
1192            newdiff = ""
1193            for newFile in filesToAdd:
1194                newdiff += "==== new file ====\n"
1195                newdiff += "--- /dev/null\n"
1196                newdiff += "+++ %s\n" % newFile
1197                f = open(newFile, "r")
1198                for line in f.readlines():
1199                    newdiff += "+" + line
1200                f.close()
1201
1202            if self.checkAuthorship and not self.p4UserIsMe(p4User):
1203                submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1204                submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1205                submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1206
1207            separatorLine = "######## everything below this line is just the diff #######\n"
1208
1209            (handle, fileName) = tempfile.mkstemp()
1210            tmpFile = os.fdopen(handle, "w+")
1211            if self.isWindows:
1212                submitTemplate = submitTemplate.replace("\n", "\r\n")
1213                separatorLine = separatorLine.replace("\n", "\r\n")
1214                newdiff = newdiff.replace("\n", "\r\n")
1215            tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
1216            tmpFile.close()
1217
1218            if self.edit_template(fileName):
1219                # read the edited message and submit
1220                tmpFile = open(fileName, "rb")
1221                message = tmpFile.read()
1222                tmpFile.close()
1223                submitTemplate = message[:message.index(separatorLine)]
1224                if self.isWindows:
1225                    submitTemplate = submitTemplate.replace("\r\n", "\n")
1226                p4_write_pipe(['submit', '-i'], submitTemplate)
1227
1228                if self.preserveUser:
1229                    if p4User:
1230                        # Get last changelist number. Cannot easily get it from
1231                        # the submit command output as the output is
1232                        # unmarshalled.
1233                        changelist = self.lastP4Changelist()
1234                        self.modifyChangelistUser(changelist, p4User)
1235
1236                # The rename/copy happened by applying a patch that created a
1237                # new file.  This leaves it writable, which confuses p4.
1238                for f in pureRenameCopy:
1239                    p4_sync(f, "-f")
1240
1241            else:
1242                # skip this patch
1243                print "Submission cancelled, undoing p4 changes."
1244                for f in editedFiles:
1245                    p4_revert(f)
1246                for f in filesToAdd:
1247                    p4_revert(f)
1248                    os.remove(f)
1249
1250            os.remove(fileName)
1251        else:
1252            fileName = "submit.txt"
1253            file = open(fileName, "w+")
1254            file.write(self.prepareLogMessage(template, logMessage))
1255            file.close()
1256            print ("Perforce submit template written as %s. "
1257                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
1258                   % (fileName, fileName))
1259
1260    # Export git tags as p4 labels. Create a p4 label and then tag
1261    # with that.
1262    def exportGitTags(self, gitTags):
1263        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1264        if len(validLabelRegexp) == 0:
1265            validLabelRegexp = defaultLabelRegexp
1266        m = re.compile(validLabelRegexp)
1267
1268        for name in gitTags:
1269
1270            if not m.match(name):
1271                if verbose:
1272                    print "tag %s does not match regexp %s" % (name, validTagRegexp)
1273                continue
1274
1275            # Get the p4 commit this corresponds to
1276            logMessage = extractLogMessageFromGitCommit(name)
1277            values = extractSettingsGitLog(logMessage)
1278
1279            if not values.has_key('change'):
1280                # a tag pointing to something not sent to p4; ignore
1281                if verbose:
1282                    print "git tag %s does not give a p4 commit" % name
1283                continue
1284            else:
1285                changelist = values['change']
1286
1287            # Get the tag details.
1288            inHeader = True
1289            isAnnotated = False
1290            body = []
1291            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1292                l = l.strip()
1293                if inHeader:
1294                    if re.match(r'tag\s+', l):
1295                        isAnnotated = True
1296                    elif re.match(r'\s*$', l):
1297                        inHeader = False
1298                        continue
1299                else:
1300                    body.append(l)
1301
1302            if not isAnnotated:
1303                body = ["lightweight tag imported by git p4\n"]
1304
1305            # Create the label - use the same view as the client spec we are using
1306            clientSpec = getClientSpec()
1307
1308            labelTemplate  = "Label: %s\n" % name
1309            labelTemplate += "Description:\n"
1310            for b in body:
1311                labelTemplate += "\t" + b + "\n"
1312            labelTemplate += "View:\n"
1313            for mapping in clientSpec.mappings:
1314                labelTemplate += "\t%s\n" % mapping.depot_side.path
1315
1316            p4_write_pipe(["label", "-i"], labelTemplate)
1317
1318            # Use the label
1319            p4_system(["tag", "-l", name] +
1320                      ["%s@%s" % (mapping.depot_side.path, changelist) for mapping in clientSpec.mappings])
1321
1322            if verbose:
1323                print "created p4 label for tag %s" % name
1324
1325    def run(self, args):
1326        if len(args) == 0:
1327            self.master = currentGitBranch()
1328            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1329                die("Detecting current git branch failed!")
1330        elif len(args) == 1:
1331            self.master = args[0]
1332            if not branchExists(self.master):
1333                die("Branch %s does not exist" % self.master)
1334        else:
1335            return False
1336
1337        allowSubmit = gitConfig("git-p4.allowSubmit")
1338        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1339            die("%s is not in git-p4.allowSubmit" % self.master)
1340
1341        [upstream, settings] = findUpstreamBranchPoint()
1342        self.depotPath = settings['depot-paths'][0]
1343        if len(self.origin) == 0:
1344            self.origin = upstream
1345
1346        if self.preserveUser:
1347            if not self.canChangeChangelists():
1348                die("Cannot preserve user names without p4 super-user or admin permissions")
1349
1350        if self.verbose:
1351            print "Origin branch is " + self.origin
1352
1353        if len(self.depotPath) == 0:
1354            print "Internal error: cannot locate perforce depot path from existing branches"
1355            sys.exit(128)
1356
1357        self.useClientSpec = False
1358        if gitConfig("git-p4.useclientspec", "--bool") == "true":
1359            self.useClientSpec = True
1360        if self.useClientSpec:
1361            self.clientSpecDirs = getClientSpec()
1362
1363        if self.useClientSpec:
1364            # all files are relative to the client spec
1365            self.clientPath = getClientRoot()
1366        else:
1367            self.clientPath = p4Where(self.depotPath)
1368
1369        if self.clientPath == "":
1370            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1371
1372        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1373        self.oldWorkingDirectory = os.getcwd()
1374
1375        # ensure the clientPath exists
1376        new_client_dir = False
1377        if not os.path.exists(self.clientPath):
1378            new_client_dir = True
1379            os.makedirs(self.clientPath)
1380
1381        chdir(self.clientPath)
1382        print "Synchronizing p4 checkout..."
1383        if new_client_dir:
1384            # old one was destroyed, and maybe nobody told p4
1385            p4_sync("...", "-f")
1386        else:
1387            p4_sync("...")
1388        self.check()
1389
1390        commits = []
1391        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1392            commits.append(line.strip())
1393        commits.reverse()
1394
1395        if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1396            self.checkAuthorship = False
1397        else:
1398            self.checkAuthorship = True
1399
1400        if self.preserveUser:
1401            self.checkValidP4Users(commits)
1402
1403        while len(commits) > 0:
1404            commit = commits[0]
1405            commits = commits[1:]
1406            self.applyCommit(commit)
1407            if not self.interactive:
1408                break
1409
1410        if len(commits) == 0:
1411            print "All changes applied!"
1412            chdir(self.oldWorkingDirectory)
1413
1414            sync = P4Sync()
1415            sync.run([])
1416
1417            rebase = P4Rebase()
1418            rebase.rebase()
1419
1420        if gitConfig("git-p4.exportLabels", "--bool") == "true":
1421            self.exportLabels = true
1422
1423        if self.exportLabels:
1424            p4Labels = getP4Labels(self.depotPath)
1425            gitTags = getGitTags()
1426
1427            missingGitTags = gitTags - p4Labels
1428            self.exportGitTags(missingGitTags)
1429
1430        return True
1431
1432class View(object):
1433    """Represent a p4 view ("p4 help views"), and map files in a
1434       repo according to the view."""
1435
1436    class Path(object):
1437        """A depot or client path, possibly containing wildcards.
1438           The only one supported is ... at the end, currently.
1439           Initialize with the full path, with //depot or //client."""
1440
1441        def __init__(self, path, is_depot):
1442            self.path = path
1443            self.is_depot = is_depot
1444            self.find_wildcards()
1445            # remember the prefix bit, useful for relative mappings
1446            m = re.match("(//[^/]+/)", self.path)
1447            if not m:
1448                die("Path %s does not start with //prefix/" % self.path)
1449            prefix = m.group(1)
1450            if not self.is_depot:
1451                # strip //client/ on client paths
1452                self.path = self.path[len(prefix):]
1453
1454        def find_wildcards(self):
1455            """Make sure wildcards are valid, and set up internal
1456               variables."""
1457
1458            self.ends_triple_dot = False
1459            # There are three wildcards allowed in p4 views
1460            # (see "p4 help views").  This code knows how to
1461            # handle "..." (only at the end), but cannot deal with
1462            # "%%n" or "*".  Only check the depot_side, as p4 should
1463            # validate that the client_side matches too.
1464            if re.search(r'%%[1-9]', self.path):
1465                die("Can't handle %%n wildcards in view: %s" % self.path)
1466            if self.path.find("*") >= 0:
1467                die("Can't handle * wildcards in view: %s" % self.path)
1468            triple_dot_index = self.path.find("...")
1469            if triple_dot_index >= 0:
1470                if triple_dot_index != len(self.path) - 3:
1471                    die("Can handle only single ... wildcard, at end: %s" %
1472                        self.path)
1473                self.ends_triple_dot = True
1474
1475        def ensure_compatible(self, other_path):
1476            """Make sure the wildcards agree."""
1477            if self.ends_triple_dot != other_path.ends_triple_dot:
1478                 die("Both paths must end with ... if either does;\n" +
1479                     "paths: %s %s" % (self.path, other_path.path))
1480
1481        def match_wildcards(self, test_path):
1482            """See if this test_path matches us, and fill in the value
1483               of the wildcards if so.  Returns a tuple of
1484               (True|False, wildcards[]).  For now, only the ... at end
1485               is supported, so at most one wildcard."""
1486            if self.ends_triple_dot:
1487                dotless = self.path[:-3]
1488                if test_path.startswith(dotless):
1489                    wildcard = test_path[len(dotless):]
1490                    return (True, [ wildcard ])
1491            else:
1492                if test_path == self.path:
1493                    return (True, [])
1494            return (False, [])
1495
1496        def match(self, test_path):
1497            """Just return if it matches; don't bother with the wildcards."""
1498            b, _ = self.match_wildcards(test_path)
1499            return b
1500
1501        def fill_in_wildcards(self, wildcards):
1502            """Return the relative path, with the wildcards filled in
1503               if there are any."""
1504            if self.ends_triple_dot:
1505                return self.path[:-3] + wildcards[0]
1506            else:
1507                return self.path
1508
1509    class Mapping(object):
1510        def __init__(self, depot_side, client_side, overlay, exclude):
1511            # depot_side is without the trailing /... if it had one
1512            self.depot_side = View.Path(depot_side, is_depot=True)
1513            self.client_side = View.Path(client_side, is_depot=False)
1514            self.overlay = overlay  # started with "+"
1515            self.exclude = exclude  # started with "-"
1516            assert not (self.overlay and self.exclude)
1517            self.depot_side.ensure_compatible(self.client_side)
1518
1519        def __str__(self):
1520            c = " "
1521            if self.overlay:
1522                c = "+"
1523            if self.exclude:
1524                c = "-"
1525            return "View.Mapping: %s%s -> %s" % \
1526                   (c, self.depot_side.path, self.client_side.path)
1527
1528        def map_depot_to_client(self, depot_path):
1529            """Calculate the client path if using this mapping on the
1530               given depot path; does not consider the effect of other
1531               mappings in a view.  Even excluded mappings are returned."""
1532            matches, wildcards = self.depot_side.match_wildcards(depot_path)
1533            if not matches:
1534                return ""
1535            client_path = self.client_side.fill_in_wildcards(wildcards)
1536            return client_path
1537
1538    #
1539    # View methods
1540    #
1541    def __init__(self):
1542        self.mappings = []
1543
1544    def append(self, view_line):
1545        """Parse a view line, splitting it into depot and client
1546           sides.  Append to self.mappings, preserving order."""
1547
1548        # Split the view line into exactly two words.  P4 enforces
1549        # structure on these lines that simplifies this quite a bit.
1550        #
1551        # Either or both words may be double-quoted.
1552        # Single quotes do not matter.
1553        # Double-quote marks cannot occur inside the words.
1554        # A + or - prefix is also inside the quotes.
1555        # There are no quotes unless they contain a space.
1556        # The line is already white-space stripped.
1557        # The two words are separated by a single space.
1558        #
1559        if view_line[0] == '"':
1560            # First word is double quoted.  Find its end.
1561            close_quote_index = view_line.find('"', 1)
1562            if close_quote_index <= 0:
1563                die("No first-word closing quote found: %s" % view_line)
1564            depot_side = view_line[1:close_quote_index]
1565            # skip closing quote and space
1566            rhs_index = close_quote_index + 1 + 1
1567        else:
1568            space_index = view_line.find(" ")
1569            if space_index <= 0:
1570                die("No word-splitting space found: %s" % view_line)
1571            depot_side = view_line[0:space_index]
1572            rhs_index = space_index + 1
1573
1574        if view_line[rhs_index] == '"':
1575            # Second word is double quoted.  Make sure there is a
1576            # double quote at the end too.
1577            if not view_line.endswith('"'):
1578                die("View line with rhs quote should end with one: %s" %
1579                    view_line)
1580            # skip the quotes
1581            client_side = view_line[rhs_index+1:-1]
1582        else:
1583            client_side = view_line[rhs_index:]
1584
1585        # prefix + means overlay on previous mapping
1586        overlay = False
1587        if depot_side.startswith("+"):
1588            overlay = True
1589            depot_side = depot_side[1:]
1590
1591        # prefix - means exclude this path
1592        exclude = False
1593        if depot_side.startswith("-"):
1594            exclude = True
1595            depot_side = depot_side[1:]
1596
1597        m = View.Mapping(depot_side, client_side, overlay, exclude)
1598        self.mappings.append(m)
1599
1600    def map_in_client(self, depot_path):
1601        """Return the relative location in the client where this
1602           depot file should live.  Returns "" if the file should
1603           not be mapped in the client."""
1604
1605        paths_filled = []
1606        client_path = ""
1607
1608        # look at later entries first
1609        for m in self.mappings[::-1]:
1610
1611            # see where will this path end up in the client
1612            p = m.map_depot_to_client(depot_path)
1613
1614            if p == "":
1615                # Depot path does not belong in client.  Must remember
1616                # this, as previous items should not cause files to
1617                # exist in this path either.  Remember that the list is
1618                # being walked from the end, which has higher precedence.
1619                # Overlap mappings do not exclude previous mappings.
1620                if not m.overlay:
1621                    paths_filled.append(m.client_side)
1622
1623            else:
1624                # This mapping matched; no need to search any further.
1625                # But, the mapping could be rejected if the client path
1626                # has already been claimed by an earlier mapping (i.e.
1627                # one later in the list, which we are walking backwards).
1628                already_mapped_in_client = False
1629                for f in paths_filled:
1630                    # this is View.Path.match
1631                    if f.match(p):
1632                        already_mapped_in_client = True
1633                        break
1634                if not already_mapped_in_client:
1635                    # Include this file, unless it is from a line that
1636                    # explicitly said to exclude it.
1637                    if not m.exclude:
1638                        client_path = p
1639
1640                # a match, even if rejected, always stops the search
1641                break
1642
1643        return client_path
1644
1645class P4Sync(Command, P4UserMap):
1646    delete_actions = ( "delete", "move/delete", "purge" )
1647
1648    def __init__(self):
1649        Command.__init__(self)
1650        P4UserMap.__init__(self)
1651        self.options = [
1652                optparse.make_option("--branch", dest="branch"),
1653                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1654                optparse.make_option("--changesfile", dest="changesFile"),
1655                optparse.make_option("--silent", dest="silent", action="store_true"),
1656                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1657                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1658                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1659                                     help="Import into refs/heads/ , not refs/remotes"),
1660                optparse.make_option("--max-changes", dest="maxChanges"),
1661                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1662                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1663                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1664                                     help="Only sync files that are included in the Perforce Client Spec")
1665        ]
1666        self.description = """Imports from Perforce into a git repository.\n
1667    example:
1668    //depot/my/project/ -- to import the current head
1669    //depot/my/project/@all -- to import everything
1670    //depot/my/project/@1,6 -- to import only from revision 1 to 6
1671
1672    (a ... is not needed in the path p4 specification, it's added implicitly)"""
1673
1674        self.usage += " //depot/path[@revRange]"
1675        self.silent = False
1676        self.createdBranches = set()
1677        self.committedChanges = set()
1678        self.branch = ""
1679        self.detectBranches = False
1680        self.detectLabels = False
1681        self.importLabels = False
1682        self.changesFile = ""
1683        self.syncWithOrigin = True
1684        self.importIntoRemotes = True
1685        self.maxChanges = ""
1686        self.isWindows = (platform.system() == "Windows")
1687        self.keepRepoPath = False
1688        self.depotPaths = None
1689        self.p4BranchesInGit = []
1690        self.cloneExclude = []
1691        self.useClientSpec = False
1692        self.useClientSpec_from_options = False
1693        self.clientSpecDirs = None
1694        self.tempBranches = []
1695        self.tempBranchLocation = "git-p4-tmp"
1696
1697        if gitConfig("git-p4.syncFromOrigin") == "false":
1698            self.syncWithOrigin = False
1699
1700    #
1701    # P4 wildcards are not allowed in filenames.  P4 complains
1702    # if you simply add them, but you can force it with "-f", in
1703    # which case it translates them into %xx encoding internally.
1704    # Search for and fix just these four characters.  Do % last so
1705    # that fixing it does not inadvertently create new %-escapes.
1706    #
1707    def wildcard_decode(self, path):
1708        # Cannot have * in a filename in windows; untested as to
1709        # what p4 would do in such a case.
1710        if not self.isWindows:
1711            path = path.replace("%2A", "*")
1712        path = path.replace("%23", "#") \
1713                   .replace("%40", "@") \
1714                   .replace("%25", "%")
1715        return path
1716
1717    # Force a checkpoint in fast-import and wait for it to finish
1718    def checkpoint(self):
1719        self.gitStream.write("checkpoint\n\n")
1720        self.gitStream.write("progress checkpoint\n\n")
1721        out = self.gitOutput.readline()
1722        if self.verbose:
1723            print "checkpoint finished: " + out
1724
1725    def extractFilesFromCommit(self, commit):
1726        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1727                             for path in self.cloneExclude]
1728        files = []
1729        fnum = 0
1730        while commit.has_key("depotFile%s" % fnum):
1731            path =  commit["depotFile%s" % fnum]
1732
1733            if [p for p in self.cloneExclude
1734                if p4PathStartsWith(path, p)]:
1735                found = False
1736            else:
1737                found = [p for p in self.depotPaths
1738                         if p4PathStartsWith(path, p)]
1739            if not found:
1740                fnum = fnum + 1
1741                continue
1742
1743            file = {}
1744            file["path"] = path
1745            file["rev"] = commit["rev%s" % fnum]
1746            file["action"] = commit["action%s" % fnum]
1747            file["type"] = commit["type%s" % fnum]
1748            files.append(file)
1749            fnum = fnum + 1
1750        return files
1751
1752    def stripRepoPath(self, path, prefixes):
1753        if self.useClientSpec:
1754            return self.clientSpecDirs.map_in_client(path)
1755
1756        if self.keepRepoPath:
1757            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1758
1759        for p in prefixes:
1760            if p4PathStartsWith(path, p):
1761                path = path[len(p):]
1762
1763        return path
1764
1765    def splitFilesIntoBranches(self, commit):
1766        branches = {}
1767        fnum = 0
1768        while commit.has_key("depotFile%s" % fnum):
1769            path =  commit["depotFile%s" % fnum]
1770            found = [p for p in self.depotPaths
1771                     if p4PathStartsWith(path, p)]
1772            if not found:
1773                fnum = fnum + 1
1774                continue
1775
1776            file = {}
1777            file["path"] = path
1778            file["rev"] = commit["rev%s" % fnum]
1779            file["action"] = commit["action%s" % fnum]
1780            file["type"] = commit["type%s" % fnum]
1781            fnum = fnum + 1
1782
1783            relPath = self.stripRepoPath(path, self.depotPaths)
1784
1785            for branch in self.knownBranches.keys():
1786
1787                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1788                if relPath.startswith(branch + "/"):
1789                    if branch not in branches:
1790                        branches[branch] = []
1791                    branches[branch].append(file)
1792                    break
1793
1794        return branches
1795
1796    # output one file from the P4 stream
1797    # - helper for streamP4Files
1798
1799    def streamOneP4File(self, file, contents):
1800        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1801        relPath = self.wildcard_decode(relPath)
1802        if verbose:
1803            sys.stderr.write("%s\n" % relPath)
1804
1805        (type_base, type_mods) = split_p4_type(file["type"])
1806
1807        git_mode = "100644"
1808        if "x" in type_mods:
1809            git_mode = "100755"
1810        if type_base == "symlink":
1811            git_mode = "120000"
1812            # p4 print on a symlink contains "target\n"; remove the newline
1813            data = ''.join(contents)
1814            contents = [data[:-1]]
1815
1816        if type_base == "utf16":
1817            # p4 delivers different text in the python output to -G
1818            # than it does when using "print -o", or normal p4 client
1819            # operations.  utf16 is converted to ascii or utf8, perhaps.
1820            # But ascii text saved as -t utf16 is completely mangled.
1821            # Invoke print -o to get the real contents.
1822            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
1823            contents = [ text ]
1824
1825        if type_base == "apple":
1826            # Apple filetype files will be streamed as a concatenation of
1827            # its appledouble header and the contents.  This is useless
1828            # on both macs and non-macs.  If using "print -q -o xx", it
1829            # will create "xx" with the data, and "%xx" with the header.
1830            # This is also not very useful.
1831            #
1832            # Ideally, someday, this script can learn how to generate
1833            # appledouble files directly and import those to git, but
1834            # non-mac machines can never find a use for apple filetype.
1835            print "\nIgnoring apple filetype file %s" % file['depotFile']
1836            return
1837
1838        # Perhaps windows wants unicode, utf16 newlines translated too;
1839        # but this is not doing it.
1840        if self.isWindows and type_base == "text":
1841            mangled = []
1842            for data in contents:
1843                data = data.replace("\r\n", "\n")
1844                mangled.append(data)
1845            contents = mangled
1846
1847        # Note that we do not try to de-mangle keywords on utf16 files,
1848        # even though in theory somebody may want that.
1849        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
1850        if pattern:
1851            regexp = re.compile(pattern, re.VERBOSE)
1852            text = ''.join(contents)
1853            text = regexp.sub(r'$\1$', text)
1854            contents = [ text ]
1855
1856        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
1857
1858        # total length...
1859        length = 0
1860        for d in contents:
1861            length = length + len(d)
1862
1863        self.gitStream.write("data %d\n" % length)
1864        for d in contents:
1865            self.gitStream.write(d)
1866        self.gitStream.write("\n")
1867
1868    def streamOneP4Deletion(self, file):
1869        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1870        if verbose:
1871            sys.stderr.write("delete %s\n" % relPath)
1872        self.gitStream.write("D %s\n" % relPath)
1873
1874    # handle another chunk of streaming data
1875    def streamP4FilesCb(self, marshalled):
1876
1877        if marshalled.has_key('depotFile') and self.stream_have_file_info:
1878            # start of a new file - output the old one first
1879            self.streamOneP4File(self.stream_file, self.stream_contents)
1880            self.stream_file = {}
1881            self.stream_contents = []
1882            self.stream_have_file_info = False
1883
1884        # pick up the new file information... for the
1885        # 'data' field we need to append to our array
1886        for k in marshalled.keys():
1887            if k == 'data':
1888                self.stream_contents.append(marshalled['data'])
1889            else:
1890                self.stream_file[k] = marshalled[k]
1891
1892        self.stream_have_file_info = True
1893
1894    # Stream directly from "p4 files" into "git fast-import"
1895    def streamP4Files(self, files):
1896        filesForCommit = []
1897        filesToRead = []
1898        filesToDelete = []
1899
1900        for f in files:
1901            # if using a client spec, only add the files that have
1902            # a path in the client
1903            if self.clientSpecDirs:
1904                if self.clientSpecDirs.map_in_client(f['path']) == "":
1905                    continue
1906
1907            filesForCommit.append(f)
1908            if f['action'] in self.delete_actions:
1909                filesToDelete.append(f)
1910            else:
1911                filesToRead.append(f)
1912
1913        # deleted files...
1914        for f in filesToDelete:
1915            self.streamOneP4Deletion(f)
1916
1917        if len(filesToRead) > 0:
1918            self.stream_file = {}
1919            self.stream_contents = []
1920            self.stream_have_file_info = False
1921
1922            # curry self argument
1923            def streamP4FilesCbSelf(entry):
1924                self.streamP4FilesCb(entry)
1925
1926            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
1927
1928            p4CmdList(["-x", "-", "print"],
1929                      stdin=fileArgs,
1930                      cb=streamP4FilesCbSelf)
1931
1932            # do the last chunk
1933            if self.stream_file.has_key('depotFile'):
1934                self.streamOneP4File(self.stream_file, self.stream_contents)
1935
1936    def make_email(self, userid):
1937        if userid in self.users:
1938            return self.users[userid]
1939        else:
1940            return "%s <a@b>" % userid
1941
1942    # Stream a p4 tag
1943    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
1944        if verbose:
1945            print "writing tag %s for commit %s" % (labelName, commit)
1946        gitStream.write("tag %s\n" % labelName)
1947        gitStream.write("from %s\n" % commit)
1948
1949        if labelDetails.has_key('Owner'):
1950            owner = labelDetails["Owner"]
1951        else:
1952            owner = None
1953
1954        # Try to use the owner of the p4 label, or failing that,
1955        # the current p4 user id.
1956        if owner:
1957            email = self.make_email(owner)
1958        else:
1959            email = self.make_email(self.p4UserId())
1960        tagger = "%s %s %s" % (email, epoch, self.tz)
1961
1962        gitStream.write("tagger %s\n" % tagger)
1963
1964        print "labelDetails=",labelDetails
1965        if labelDetails.has_key('Description'):
1966            description = labelDetails['Description']
1967        else:
1968            description = 'Label from git p4'
1969
1970        gitStream.write("data %d\n" % len(description))
1971        gitStream.write(description)
1972        gitStream.write("\n")
1973
1974    def commit(self, details, files, branch, branchPrefixes, parent = ""):
1975        epoch = details["time"]
1976        author = details["user"]
1977        self.branchPrefixes = branchPrefixes
1978
1979        if self.verbose:
1980            print "commit into %s" % branch
1981
1982        # start with reading files; if that fails, we should not
1983        # create a commit.
1984        new_files = []
1985        for f in files:
1986            if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1987                new_files.append (f)
1988            else:
1989                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1990
1991        self.gitStream.write("commit %s\n" % branch)
1992#        gitStream.write("mark :%s\n" % details["change"])
1993        self.committedChanges.add(int(details["change"]))
1994        committer = ""
1995        if author not in self.users:
1996            self.getUserMapFromPerforceServer()
1997        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
1998
1999        self.gitStream.write("committer %s\n" % committer)
2000
2001        self.gitStream.write("data <<EOT\n")
2002        self.gitStream.write(details["desc"])
2003        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
2004                             % (','.join (branchPrefixes), details["change"]))
2005        if len(details['options']) > 0:
2006            self.gitStream.write(": options = %s" % details['options'])
2007        self.gitStream.write("]\nEOT\n\n")
2008
2009        if len(parent) > 0:
2010            if self.verbose:
2011                print "parent %s" % parent
2012            self.gitStream.write("from %s\n" % parent)
2013
2014        self.streamP4Files(new_files)
2015        self.gitStream.write("\n")
2016
2017        change = int(details["change"])
2018
2019        if self.labels.has_key(change):
2020            label = self.labels[change]
2021            labelDetails = label[0]
2022            labelRevisions = label[1]
2023            if self.verbose:
2024                print "Change %s is labelled %s" % (change, labelDetails)
2025
2026            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2027                                                    for p in branchPrefixes])
2028
2029            if len(files) == len(labelRevisions):
2030
2031                cleanedFiles = {}
2032                for info in files:
2033                    if info["action"] in self.delete_actions:
2034                        continue
2035                    cleanedFiles[info["depotFile"]] = info["rev"]
2036
2037                if cleanedFiles == labelRevisions:
2038                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2039
2040                else:
2041                    if not self.silent:
2042                        print ("Tag %s does not match with change %s: files do not match."
2043                               % (labelDetails["label"], change))
2044
2045            else:
2046                if not self.silent:
2047                    print ("Tag %s does not match with change %s: file count is different."
2048                           % (labelDetails["label"], change))
2049
2050    # Build a dictionary of changelists and labels, for "detect-labels" option.
2051    def getLabels(self):
2052        self.labels = {}
2053
2054        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2055        if len(l) > 0 and not self.silent:
2056            print "Finding files belonging to labels in %s" % `self.depotPaths`
2057
2058        for output in l:
2059            label = output["label"]
2060            revisions = {}
2061            newestChange = 0
2062            if self.verbose:
2063                print "Querying files for label %s" % label
2064            for file in p4CmdList(["files"] +
2065                                      ["%s...@%s" % (p, label)
2066                                          for p in self.depotPaths]):
2067                revisions[file["depotFile"]] = file["rev"]
2068                change = int(file["change"])
2069                if change > newestChange:
2070                    newestChange = change
2071
2072            self.labels[newestChange] = [output, revisions]
2073
2074        if self.verbose:
2075            print "Label changes: %s" % self.labels.keys()
2076
2077    # Import p4 labels as git tags. A direct mapping does not
2078    # exist, so assume that if all the files are at the same revision
2079    # then we can use that, or it's something more complicated we should
2080    # just ignore.
2081    def importP4Labels(self, stream, p4Labels):
2082        if verbose:
2083            print "import p4 labels: " + ' '.join(p4Labels)
2084
2085        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2086        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2087        if len(validLabelRegexp) == 0:
2088            validLabelRegexp = defaultLabelRegexp
2089        m = re.compile(validLabelRegexp)
2090
2091        for name in p4Labels:
2092            commitFound = False
2093
2094            if not m.match(name):
2095                if verbose:
2096                    print "label %s does not match regexp %s" % (name,validLabelRegexp)
2097                continue
2098
2099            if name in ignoredP4Labels:
2100                continue
2101
2102            labelDetails = p4CmdList(['label', "-o", name])[0]
2103
2104            # get the most recent changelist for each file in this label
2105            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2106                                for p in self.depotPaths])
2107
2108            if change.has_key('change'):
2109                # find the corresponding git commit; take the oldest commit
2110                changelist = int(change['change'])
2111                gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2112                     "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2113                if len(gitCommit) == 0:
2114                    print "could not find git commit for changelist %d" % changelist
2115                else:
2116                    gitCommit = gitCommit.strip()
2117                    commitFound = True
2118                    # Convert from p4 time format
2119                    try:
2120                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2121                    except ValueError:
2122                        print "Could not convert label time %s" % labelDetail['Update']
2123                        tmwhen = 1
2124
2125                    when = int(time.mktime(tmwhen))
2126                    self.streamTag(stream, name, labelDetails, gitCommit, when)
2127                    if verbose:
2128                        print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2129            else:
2130                if verbose:
2131                    print "Label %s has no changelists - possibly deleted?" % name
2132
2133            if not commitFound:
2134                # We can't import this label; don't try again as it will get very
2135                # expensive repeatedly fetching all the files for labels that will
2136                # never be imported. If the label is moved in the future, the
2137                # ignore will need to be removed manually.
2138                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2139
2140    def guessProjectName(self):
2141        for p in self.depotPaths:
2142            if p.endswith("/"):
2143                p = p[:-1]
2144            p = p[p.strip().rfind("/") + 1:]
2145            if not p.endswith("/"):
2146               p += "/"
2147            return p
2148
2149    def getBranchMapping(self):
2150        lostAndFoundBranches = set()
2151
2152        user = gitConfig("git-p4.branchUser")
2153        if len(user) > 0:
2154            command = "branches -u %s" % user
2155        else:
2156            command = "branches"
2157
2158        for info in p4CmdList(command):
2159            details = p4Cmd(["branch", "-o", info["branch"]])
2160            viewIdx = 0
2161            while details.has_key("View%s" % viewIdx):
2162                paths = details["View%s" % viewIdx].split(" ")
2163                viewIdx = viewIdx + 1
2164                # require standard //depot/foo/... //depot/bar/... mapping
2165                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2166                    continue
2167                source = paths[0]
2168                destination = paths[1]
2169                ## HACK
2170                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2171                    source = source[len(self.depotPaths[0]):-4]
2172                    destination = destination[len(self.depotPaths[0]):-4]
2173
2174                    if destination in self.knownBranches:
2175                        if not self.silent:
2176                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2177                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2178                        continue
2179
2180                    self.knownBranches[destination] = source
2181
2182                    lostAndFoundBranches.discard(destination)
2183
2184                    if source not in self.knownBranches:
2185                        lostAndFoundBranches.add(source)
2186
2187        # Perforce does not strictly require branches to be defined, so we also
2188        # check git config for a branch list.
2189        #
2190        # Example of branch definition in git config file:
2191        # [git-p4]
2192        #   branchList=main:branchA
2193        #   branchList=main:branchB
2194        #   branchList=branchA:branchC
2195        configBranches = gitConfigList("git-p4.branchList")
2196        for branch in configBranches:
2197            if branch:
2198                (source, destination) = branch.split(":")
2199                self.knownBranches[destination] = source
2200
2201                lostAndFoundBranches.discard(destination)
2202
2203                if source not in self.knownBranches:
2204                    lostAndFoundBranches.add(source)
2205
2206
2207        for branch in lostAndFoundBranches:
2208            self.knownBranches[branch] = branch
2209
2210    def getBranchMappingFromGitBranches(self):
2211        branches = p4BranchesInGit(self.importIntoRemotes)
2212        for branch in branches.keys():
2213            if branch == "master":
2214                branch = "main"
2215            else:
2216                branch = branch[len(self.projectName):]
2217            self.knownBranches[branch] = branch
2218
2219    def listExistingP4GitBranches(self):
2220        # branches holds mapping from name to commit
2221        branches = p4BranchesInGit(self.importIntoRemotes)
2222        self.p4BranchesInGit = branches.keys()
2223        for branch in branches.keys():
2224            self.initialParents[self.refPrefix + branch] = branches[branch]
2225
2226    def updateOptionDict(self, d):
2227        option_keys = {}
2228        if self.keepRepoPath:
2229            option_keys['keepRepoPath'] = 1
2230
2231        d["options"] = ' '.join(sorted(option_keys.keys()))
2232
2233    def readOptions(self, d):
2234        self.keepRepoPath = (d.has_key('options')
2235                             and ('keepRepoPath' in d['options']))
2236
2237    def gitRefForBranch(self, branch):
2238        if branch == "main":
2239            return self.refPrefix + "master"
2240
2241        if len(branch) <= 0:
2242            return branch
2243
2244        return self.refPrefix + self.projectName + branch
2245
2246    def gitCommitByP4Change(self, ref, change):
2247        if self.verbose:
2248            print "looking in ref " + ref + " for change %s using bisect..." % change
2249
2250        earliestCommit = ""
2251        latestCommit = parseRevision(ref)
2252
2253        while True:
2254            if self.verbose:
2255                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2256            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2257            if len(next) == 0:
2258                if self.verbose:
2259                    print "argh"
2260                return ""
2261            log = extractLogMessageFromGitCommit(next)
2262            settings = extractSettingsGitLog(log)
2263            currentChange = int(settings['change'])
2264            if self.verbose:
2265                print "current change %s" % currentChange
2266
2267            if currentChange == change:
2268                if self.verbose:
2269                    print "found %s" % next
2270                return next
2271
2272            if currentChange < change:
2273                earliestCommit = "^%s" % next
2274            else:
2275                latestCommit = "%s" % next
2276
2277        return ""
2278
2279    def importNewBranch(self, branch, maxChange):
2280        # make fast-import flush all changes to disk and update the refs using the checkpoint
2281        # command so that we can try to find the branch parent in the git history
2282        self.gitStream.write("checkpoint\n\n");
2283        self.gitStream.flush();
2284        branchPrefix = self.depotPaths[0] + branch + "/"
2285        range = "@1,%s" % maxChange
2286        #print "prefix" + branchPrefix
2287        changes = p4ChangesForPaths([branchPrefix], range)
2288        if len(changes) <= 0:
2289            return False
2290        firstChange = changes[0]
2291        #print "first change in branch: %s" % firstChange
2292        sourceBranch = self.knownBranches[branch]
2293        sourceDepotPath = self.depotPaths[0] + sourceBranch
2294        sourceRef = self.gitRefForBranch(sourceBranch)
2295        #print "source " + sourceBranch
2296
2297        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2298        #print "branch parent: %s" % branchParentChange
2299        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2300        if len(gitParent) > 0:
2301            self.initialParents[self.gitRefForBranch(branch)] = gitParent
2302            #print "parent git commit: %s" % gitParent
2303
2304        self.importChanges(changes)
2305        return True
2306
2307    def searchParent(self, parent, branch, target):
2308        parentFound = False
2309        for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]):
2310            blob = blob.strip()
2311            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2312                parentFound = True
2313                if self.verbose:
2314                    print "Found parent of %s in commit %s" % (branch, blob)
2315                break
2316        if parentFound:
2317            return blob
2318        else:
2319            return None
2320
2321    def importChanges(self, changes):
2322        cnt = 1
2323        for change in changes:
2324            description = p4Cmd(["describe", str(change)])
2325            self.updateOptionDict(description)
2326
2327            if not self.silent:
2328                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2329                sys.stdout.flush()
2330            cnt = cnt + 1
2331
2332            try:
2333                if self.detectBranches:
2334                    branches = self.splitFilesIntoBranches(description)
2335                    for branch in branches.keys():
2336                        ## HACK  --hwn
2337                        branchPrefix = self.depotPaths[0] + branch + "/"
2338
2339                        parent = ""
2340
2341                        filesForCommit = branches[branch]
2342
2343                        if self.verbose:
2344                            print "branch is %s" % branch
2345
2346                        self.updatedBranches.add(branch)
2347
2348                        if branch not in self.createdBranches:
2349                            self.createdBranches.add(branch)
2350                            parent = self.knownBranches[branch]
2351                            if parent == branch:
2352                                parent = ""
2353                            else:
2354                                fullBranch = self.projectName + branch
2355                                if fullBranch not in self.p4BranchesInGit:
2356                                    if not self.silent:
2357                                        print("\n    Importing new branch %s" % fullBranch);
2358                                    if self.importNewBranch(branch, change - 1):
2359                                        parent = ""
2360                                        self.p4BranchesInGit.append(fullBranch)
2361                                    if not self.silent:
2362                                        print("\n    Resuming with change %s" % change);
2363
2364                                if self.verbose:
2365                                    print "parent determined through known branches: %s" % parent
2366
2367                        branch = self.gitRefForBranch(branch)
2368                        parent = self.gitRefForBranch(parent)
2369
2370                        if self.verbose:
2371                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2372
2373                        if len(parent) == 0 and branch in self.initialParents:
2374                            parent = self.initialParents[branch]
2375                            del self.initialParents[branch]
2376
2377                        blob = None
2378                        if len(parent) > 0:
2379                            tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change))
2380                            if self.verbose:
2381                                print "Creating temporary branch: " + tempBranch
2382                            self.commit(description, filesForCommit, tempBranch, [branchPrefix])
2383                            self.tempBranches.append(tempBranch)
2384                            self.checkpoint()
2385                            blob = self.searchParent(parent, branch, tempBranch)
2386                        if blob:
2387                            self.commit(description, filesForCommit, branch, [branchPrefix], blob)
2388                        else:
2389                            if self.verbose:
2390                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2391                            self.commit(description, filesForCommit, branch, [branchPrefix], parent)
2392                else:
2393                    files = self.extractFilesFromCommit(description)
2394                    self.commit(description, files, self.branch, self.depotPaths,
2395                                self.initialParent)
2396                    self.initialParent = ""
2397            except IOError:
2398                print self.gitError.read()
2399                sys.exit(1)
2400
2401    def importHeadRevision(self, revision):
2402        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2403
2404        details = {}
2405        details["user"] = "git perforce import user"
2406        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2407                           % (' '.join(self.depotPaths), revision))
2408        details["change"] = revision
2409        newestRevision = 0
2410
2411        fileCnt = 0
2412        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2413
2414        for info in p4CmdList(["files"] + fileArgs):
2415
2416            if 'code' in info and info['code'] == 'error':
2417                sys.stderr.write("p4 returned an error: %s\n"
2418                                 % info['data'])
2419                if info['data'].find("must refer to client") >= 0:
2420                    sys.stderr.write("This particular p4 error is misleading.\n")
2421                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
2422                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2423                sys.exit(1)
2424            if 'p4ExitCode' in info:
2425                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2426                sys.exit(1)
2427
2428
2429            change = int(info["change"])
2430            if change > newestRevision:
2431                newestRevision = change
2432
2433            if info["action"] in self.delete_actions:
2434                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2435                #fileCnt = fileCnt + 1
2436                continue
2437
2438            for prop in ["depotFile", "rev", "action", "type" ]:
2439                details["%s%s" % (prop, fileCnt)] = info[prop]
2440
2441            fileCnt = fileCnt + 1
2442
2443        details["change"] = newestRevision
2444
2445        # Use time from top-most change so that all git p4 clones of
2446        # the same p4 repo have the same commit SHA1s.
2447        res = p4CmdList("describe -s %d" % newestRevision)
2448        newestTime = None
2449        for r in res:
2450            if r.has_key('time'):
2451                newestTime = int(r['time'])
2452        if newestTime is None:
2453            die("\"describe -s\" on newest change %d did not give a time")
2454        details["time"] = newestTime
2455
2456        self.updateOptionDict(details)
2457        try:
2458            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
2459        except IOError:
2460            print "IO error with git fast-import. Is your git version recent enough?"
2461            print self.gitError.read()
2462
2463
2464    def run(self, args):
2465        self.depotPaths = []
2466        self.changeRange = ""
2467        self.initialParent = ""
2468        self.previousDepotPaths = []
2469
2470        # map from branch depot path to parent branch
2471        self.knownBranches = {}
2472        self.initialParents = {}
2473        self.hasOrigin = originP4BranchesExist()
2474        if not self.syncWithOrigin:
2475            self.hasOrigin = False
2476
2477        if self.importIntoRemotes:
2478            self.refPrefix = "refs/remotes/p4/"
2479        else:
2480            self.refPrefix = "refs/heads/p4/"
2481
2482        if self.syncWithOrigin and self.hasOrigin:
2483            if not self.silent:
2484                print "Syncing with origin first by calling git fetch origin"
2485            system("git fetch origin")
2486
2487        if len(self.branch) == 0:
2488            self.branch = self.refPrefix + "master"
2489            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2490                system("git update-ref %s refs/heads/p4" % self.branch)
2491                system("git branch -D p4");
2492            # create it /after/ importing, when master exists
2493            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
2494                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
2495
2496        # accept either the command-line option, or the configuration variable
2497        if self.useClientSpec:
2498            # will use this after clone to set the variable
2499            self.useClientSpec_from_options = True
2500        else:
2501            if gitConfig("git-p4.useclientspec", "--bool") == "true":
2502                self.useClientSpec = True
2503        if self.useClientSpec:
2504            self.clientSpecDirs = getClientSpec()
2505
2506        # TODO: should always look at previous commits,
2507        # merge with previous imports, if possible.
2508        if args == []:
2509            if self.hasOrigin:
2510                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2511            self.listExistingP4GitBranches()
2512
2513            if len(self.p4BranchesInGit) > 1:
2514                if not self.silent:
2515                    print "Importing from/into multiple branches"
2516                self.detectBranches = True
2517
2518            if self.verbose:
2519                print "branches: %s" % self.p4BranchesInGit
2520
2521            p4Change = 0
2522            for branch in self.p4BranchesInGit:
2523                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2524
2525                settings = extractSettingsGitLog(logMsg)
2526
2527                self.readOptions(settings)
2528                if (settings.has_key('depot-paths')
2529                    and settings.has_key ('change')):
2530                    change = int(settings['change']) + 1
2531                    p4Change = max(p4Change, change)
2532
2533                    depotPaths = sorted(settings['depot-paths'])
2534                    if self.previousDepotPaths == []:
2535                        self.previousDepotPaths = depotPaths
2536                    else:
2537                        paths = []
2538                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2539                            prev_list = prev.split("/")
2540                            cur_list = cur.split("/")
2541                            for i in range(0, min(len(cur_list), len(prev_list))):
2542                                if cur_list[i] <> prev_list[i]:
2543                                    i = i - 1
2544                                    break
2545
2546                            paths.append ("/".join(cur_list[:i + 1]))
2547
2548                        self.previousDepotPaths = paths
2549
2550            if p4Change > 0:
2551                self.depotPaths = sorted(self.previousDepotPaths)
2552                self.changeRange = "@%s,#head" % p4Change
2553                if not self.detectBranches:
2554                    self.initialParent = parseRevision(self.branch)
2555                if not self.silent and not self.detectBranches:
2556                    print "Performing incremental import into %s git branch" % self.branch
2557
2558        if not self.branch.startswith("refs/"):
2559            self.branch = "refs/heads/" + self.branch
2560
2561        if len(args) == 0 and self.depotPaths:
2562            if not self.silent:
2563                print "Depot paths: %s" % ' '.join(self.depotPaths)
2564        else:
2565            if self.depotPaths and self.depotPaths != args:
2566                print ("previous import used depot path %s and now %s was specified. "
2567                       "This doesn't work!" % (' '.join (self.depotPaths),
2568                                               ' '.join (args)))
2569                sys.exit(1)
2570
2571            self.depotPaths = sorted(args)
2572
2573        revision = ""
2574        self.users = {}
2575
2576        # Make sure no revision specifiers are used when --changesfile
2577        # is specified.
2578        bad_changesfile = False
2579        if len(self.changesFile) > 0:
2580            for p in self.depotPaths:
2581                if p.find("@") >= 0 or p.find("#") >= 0:
2582                    bad_changesfile = True
2583                    break
2584        if bad_changesfile:
2585            die("Option --changesfile is incompatible with revision specifiers")
2586
2587        newPaths = []
2588        for p in self.depotPaths:
2589            if p.find("@") != -1:
2590                atIdx = p.index("@")
2591                self.changeRange = p[atIdx:]
2592                if self.changeRange == "@all":
2593                    self.changeRange = ""
2594                elif ',' not in self.changeRange:
2595                    revision = self.changeRange
2596                    self.changeRange = ""
2597                p = p[:atIdx]
2598            elif p.find("#") != -1:
2599                hashIdx = p.index("#")
2600                revision = p[hashIdx:]
2601                p = p[:hashIdx]
2602            elif self.previousDepotPaths == []:
2603                # pay attention to changesfile, if given, else import
2604                # the entire p4 tree at the head revision
2605                if len(self.changesFile) == 0:
2606                    revision = "#head"
2607
2608            p = re.sub ("\.\.\.$", "", p)
2609            if not p.endswith("/"):
2610                p += "/"
2611
2612            newPaths.append(p)
2613
2614        self.depotPaths = newPaths
2615
2616        self.loadUserMapFromCache()
2617        self.labels = {}
2618        if self.detectLabels:
2619            self.getLabels();
2620
2621        if self.detectBranches:
2622            ## FIXME - what's a P4 projectName ?
2623            self.projectName = self.guessProjectName()
2624
2625            if self.hasOrigin:
2626                self.getBranchMappingFromGitBranches()
2627            else:
2628                self.getBranchMapping()
2629            if self.verbose:
2630                print "p4-git branches: %s" % self.p4BranchesInGit
2631                print "initial parents: %s" % self.initialParents
2632            for b in self.p4BranchesInGit:
2633                if b != "master":
2634
2635                    ## FIXME
2636                    b = b[len(self.projectName):]
2637                self.createdBranches.add(b)
2638
2639        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2640
2641        importProcess = subprocess.Popen(["git", "fast-import"],
2642                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2643                                         stderr=subprocess.PIPE);
2644        self.gitOutput = importProcess.stdout
2645        self.gitStream = importProcess.stdin
2646        self.gitError = importProcess.stderr
2647
2648        if revision:
2649            self.importHeadRevision(revision)
2650        else:
2651            changes = []
2652
2653            if len(self.changesFile) > 0:
2654                output = open(self.changesFile).readlines()
2655                changeSet = set()
2656                for line in output:
2657                    changeSet.add(int(line))
2658
2659                for change in changeSet:
2660                    changes.append(change)
2661
2662                changes.sort()
2663            else:
2664                # catch "git p4 sync" with no new branches, in a repo that
2665                # does not have any existing p4 branches
2666                if len(args) == 0 and not self.p4BranchesInGit:
2667                    die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
2668                if self.verbose:
2669                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2670                                                              self.changeRange)
2671                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2672
2673                if len(self.maxChanges) > 0:
2674                    changes = changes[:min(int(self.maxChanges), len(changes))]
2675
2676            if len(changes) == 0:
2677                if not self.silent:
2678                    print "No changes to import!"
2679            else:
2680                if not self.silent and not self.detectBranches:
2681                    print "Import destination: %s" % self.branch
2682
2683                self.updatedBranches = set()
2684
2685                self.importChanges(changes)
2686
2687                if not self.silent:
2688                    print ""
2689                    if len(self.updatedBranches) > 0:
2690                        sys.stdout.write("Updated branches: ")
2691                        for b in self.updatedBranches:
2692                            sys.stdout.write("%s " % b)
2693                        sys.stdout.write("\n")
2694
2695        if gitConfig("git-p4.importLabels", "--bool") == "true":
2696            self.importLabels = true
2697
2698        if self.importLabels:
2699            p4Labels = getP4Labels(self.depotPaths)
2700            gitTags = getGitTags()
2701
2702            missingP4Labels = p4Labels - gitTags
2703            self.importP4Labels(self.gitStream, missingP4Labels)
2704
2705        self.gitStream.close()
2706        if importProcess.wait() != 0:
2707            die("fast-import failed: %s" % self.gitError.read())
2708        self.gitOutput.close()
2709        self.gitError.close()
2710
2711        # Cleanup temporary branches created during import
2712        if self.tempBranches != []:
2713            for branch in self.tempBranches:
2714                read_pipe("git update-ref -d %s" % branch)
2715            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
2716
2717        return True
2718
2719class P4Rebase(Command):
2720    def __init__(self):
2721        Command.__init__(self)
2722        self.options = [
2723                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2724        ]
2725        self.importLabels = False
2726        self.description = ("Fetches the latest revision from perforce and "
2727                            + "rebases the current work (branch) against it")
2728
2729    def run(self, args):
2730        sync = P4Sync()
2731        sync.importLabels = self.importLabels
2732        sync.run([])
2733
2734        return self.rebase()
2735
2736    def rebase(self):
2737        if os.system("git update-index --refresh") != 0:
2738            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.");
2739        if len(read_pipe("git diff-index HEAD --")) > 0:
2740            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2741
2742        [upstream, settings] = findUpstreamBranchPoint()
2743        if len(upstream) == 0:
2744            die("Cannot find upstream branchpoint for rebase")
2745
2746        # the branchpoint may be p4/foo~3, so strip off the parent
2747        upstream = re.sub("~[0-9]+$", "", upstream)
2748
2749        print "Rebasing the current branch onto %s" % upstream
2750        oldHead = read_pipe("git rev-parse HEAD").strip()
2751        system("git rebase %s" % upstream)
2752        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
2753        return True
2754
2755class P4Clone(P4Sync):
2756    def __init__(self):
2757        P4Sync.__init__(self)
2758        self.description = "Creates a new git repository and imports from Perforce into it"
2759        self.usage = "usage: %prog [options] //depot/path[@revRange]"
2760        self.options += [
2761            optparse.make_option("--destination", dest="cloneDestination",
2762                                 action='store', default=None,
2763                                 help="where to leave result of the clone"),
2764            optparse.make_option("-/", dest="cloneExclude",
2765                                 action="append", type="string",
2766                                 help="exclude depot path"),
2767            optparse.make_option("--bare", dest="cloneBare",
2768                                 action="store_true", default=False),
2769        ]
2770        self.cloneDestination = None
2771        self.needsGit = False
2772        self.cloneBare = False
2773
2774    # This is required for the "append" cloneExclude action
2775    def ensure_value(self, attr, value):
2776        if not hasattr(self, attr) or getattr(self, attr) is None:
2777            setattr(self, attr, value)
2778        return getattr(self, attr)
2779
2780    def defaultDestination(self, args):
2781        ## TODO: use common prefix of args?
2782        depotPath = args[0]
2783        depotDir = re.sub("(@[^@]*)$", "", depotPath)
2784        depotDir = re.sub("(#[^#]*)$", "", depotDir)
2785        depotDir = re.sub(r"\.\.\.$", "", depotDir)
2786        depotDir = re.sub(r"/$", "", depotDir)
2787        return os.path.split(depotDir)[1]
2788
2789    def run(self, args):
2790        if len(args) < 1:
2791            return False
2792
2793        if self.keepRepoPath and not self.cloneDestination:
2794            sys.stderr.write("Must specify destination for --keep-path\n")
2795            sys.exit(1)
2796
2797        depotPaths = args
2798
2799        if not self.cloneDestination and len(depotPaths) > 1:
2800            self.cloneDestination = depotPaths[-1]
2801            depotPaths = depotPaths[:-1]
2802
2803        self.cloneExclude = ["/"+p for p in self.cloneExclude]
2804        for p in depotPaths:
2805            if not p.startswith("//"):
2806                return False
2807
2808        if not self.cloneDestination:
2809            self.cloneDestination = self.defaultDestination(args)
2810
2811        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2812
2813        if not os.path.exists(self.cloneDestination):
2814            os.makedirs(self.cloneDestination)
2815        chdir(self.cloneDestination)
2816
2817        init_cmd = [ "git", "init" ]
2818        if self.cloneBare:
2819            init_cmd.append("--bare")
2820        subprocess.check_call(init_cmd)
2821
2822        if not P4Sync.run(self, depotPaths):
2823            return False
2824        if self.branch != "master":
2825            if self.importIntoRemotes:
2826                masterbranch = "refs/remotes/p4/master"
2827            else:
2828                masterbranch = "refs/heads/p4/master"
2829            if gitBranchExists(masterbranch):
2830                system("git branch master %s" % masterbranch)
2831                if not self.cloneBare:
2832                    system("git checkout -f")
2833            else:
2834                print "Could not detect main branch. No checkout/master branch created."
2835
2836        # auto-set this variable if invoked with --use-client-spec
2837        if self.useClientSpec_from_options:
2838            system("git config --bool git-p4.useclientspec true")
2839
2840        return True
2841
2842class P4Branches(Command):
2843    def __init__(self):
2844        Command.__init__(self)
2845        self.options = [ ]
2846        self.description = ("Shows the git branches that hold imports and their "
2847                            + "corresponding perforce depot paths")
2848        self.verbose = False
2849
2850    def run(self, args):
2851        if originP4BranchesExist():
2852            createOrUpdateBranchesFromOrigin()
2853
2854        cmdline = "git rev-parse --symbolic "
2855        cmdline += " --remotes"
2856
2857        for line in read_pipe_lines(cmdline):
2858            line = line.strip()
2859
2860            if not line.startswith('p4/') or line == "p4/HEAD":
2861                continue
2862            branch = line
2863
2864            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2865            settings = extractSettingsGitLog(log)
2866
2867            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2868        return True
2869
2870class HelpFormatter(optparse.IndentedHelpFormatter):
2871    def __init__(self):
2872        optparse.IndentedHelpFormatter.__init__(self)
2873
2874    def format_description(self, description):
2875        if description:
2876            return description + "\n"
2877        else:
2878            return ""
2879
2880def printUsage(commands):
2881    print "usage: %s <command> [options]" % sys.argv[0]
2882    print ""
2883    print "valid commands: %s" % ", ".join(commands)
2884    print ""
2885    print "Try %s <command> --help for command specific help." % sys.argv[0]
2886    print ""
2887
2888commands = {
2889    "debug" : P4Debug,
2890    "submit" : P4Submit,
2891    "commit" : P4Submit,
2892    "sync" : P4Sync,
2893    "rebase" : P4Rebase,
2894    "clone" : P4Clone,
2895    "rollback" : P4RollBack,
2896    "branches" : P4Branches
2897}
2898
2899
2900def main():
2901    if len(sys.argv[1:]) == 0:
2902        printUsage(commands.keys())
2903        sys.exit(2)
2904
2905    cmd = ""
2906    cmdName = sys.argv[1]
2907    try:
2908        klass = commands[cmdName]
2909        cmd = klass()
2910    except KeyError:
2911        print "unknown command %s" % cmdName
2912        print ""
2913        printUsage(commands.keys())
2914        sys.exit(2)
2915
2916    options = cmd.options
2917    cmd.gitdir = os.environ.get("GIT_DIR", None)
2918
2919    args = sys.argv[2:]
2920
2921    options.append(optparse.make_option("--verbose", dest="verbose", action="store_true"))
2922    if cmd.needsGit:
2923        options.append(optparse.make_option("--git-dir", dest="gitdir"))
2924
2925    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2926                                   options,
2927                                   description = cmd.description,
2928                                   formatter = HelpFormatter())
2929
2930    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2931    global verbose
2932    verbose = cmd.verbose
2933    if cmd.needsGit:
2934        if cmd.gitdir == None:
2935            cmd.gitdir = os.path.abspath(".git")
2936            if not isValidGitDir(cmd.gitdir):
2937                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2938                if os.path.exists(cmd.gitdir):
2939                    cdup = read_pipe("git rev-parse --show-cdup").strip()
2940                    if len(cdup) > 0:
2941                        chdir(cdup);
2942
2943        if not isValidGitDir(cmd.gitdir):
2944            if isValidGitDir(cmd.gitdir + "/.git"):
2945                cmd.gitdir += "/.git"
2946            else:
2947                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2948
2949        os.environ["GIT_DIR"] = cmd.gitdir
2950
2951    if not cmd.run(args):
2952        parser.print_help()
2953        sys.exit(2)
2954
2955
2956if __name__ == '__main__':
2957    main()