Merge branch 'maint'
[gitweb.git] / contrib / fast-import / git-p4
index 3cbb2da221618a67fda6991317352e330c6db8ba..557649a14ad0e3a7ab4d0f1b9b128d1ba64d0757 100755 (executable)
@@ -63,6 +63,14 @@ def system(cmd):
     if os.system(cmd) != 0:
         die("command failed: %s" % cmd)
 
+def isP4Exec(kind):
+    """Determine if a Perforce 'kind' should have execute permission
+
+    'p4 help filetypes' gives a list of the types.  If it starts with 'x',
+    or x follows one of a few letters.  Otherwise, if there is an 'x' after
+    a plus sign, it is also executable"""
+    return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
+
 def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
     cmd = "p4 -G %s" % cmd
     if verbose:
@@ -231,6 +239,69 @@ def findUpstreamBranchPoint(head = "HEAD"):
 
     return ["", settings]
 
+def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
+    if not silent:
+        print ("Creating/updating branch(es) in %s based on origin branch(es)"
+               % localRefPrefix)
+
+    originPrefix = "origin/p4/"
+
+    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
+        line = line.strip()
+        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
+            continue
+
+        headName = line[len(originPrefix):]
+        remoteHead = localRefPrefix + headName
+        originHead = line
+
+        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
+        if (not original.has_key('depot-paths')
+            or not original.has_key('change')):
+            continue
+
+        update = False
+        if not gitBranchExists(remoteHead):
+            if verbose:
+                print "creating %s" % remoteHead
+            update = True
+        else:
+            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
+            if settings.has_key('change') > 0:
+                if settings['depot-paths'] == original['depot-paths']:
+                    originP4Change = int(original['change'])
+                    p4Change = int(settings['change'])
+                    if originP4Change > p4Change:
+                        print ("%s (%s) is newer than %s (%s). "
+                               "Updating p4 branch from origin."
+                               % (originHead, originP4Change,
+                                  remoteHead, p4Change))
+                        update = True
+                else:
+                    print ("Ignoring: %s was imported from %s while "
+                           "%s was imported from %s"
+                           % (originHead, ','.join(original['depot-paths']),
+                              remoteHead, ','.join(settings['depot-paths'])))
+
+        if update:
+            system("git update-ref %s %s" % (remoteHead, originHead))
+
+def originP4BranchesExist():
+        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
+
+def p4ChangesForPaths(depotPaths, changeRange):
+    assert depotPaths
+    output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, changeRange)
+                                                        for p in depotPaths]))
+
+    changes = []
+    for line in output:
+        changeNum = line.split(" ")[1]
+        changes.append(int(changeNum))
+
+    changes.sort()
+    return changes
+
 class Command:
     def __init__(self):
         self.usage = "usage: %prog [options]"
@@ -390,6 +461,30 @@ class P4Submit(Command):
 
         return result
 
+    def prepareSubmitTemplate(self):
+        # remove lines in the Files section that show changes to files outside the depot path we're committing into
+        template = ""
+        inFilesSection = False
+        for line in read_pipe_lines("p4 change -o"):
+            if inFilesSection:
+                if line.startswith("\t"):
+                    # path starts and ends with a tab
+                    path = line[1:]
+                    lastTab = path.rfind("\t")
+                    if lastTab != -1:
+                        path = path[:lastTab]
+                        if not path.startswith(self.depotPath):
+                            continue
+                else:
+                    inFilesSection = False
+            else:
+                if line.startswith("Files:"):
+                    inFilesSection = True
+
+            template += line
+
+        return template
+
     def applyCommit(self, id):
         if self.directSubmit:
             print "Applying local change in working directory/index"
@@ -467,7 +562,7 @@ class P4Submit(Command):
                 logMessage = logMessage.replace("\n", "\r\n")
             logMessage = logMessage.strip()
 
-        template = read_pipe("p4 change -o")
+        template = self.prepareSubmitTemplate()
 
         if self.interactive:
             submitTemplate = self.prepareLogMessage(template, logMessage)
@@ -558,24 +653,24 @@ class P4Submit(Command):
             return False
 
         [upstream, settings] = findUpstreamBranchPoint()
-        depotPath = settings['depot-paths'][0]
+        self.depotPath = settings['depot-paths'][0]
         if len(self.origin) == 0:
             self.origin = upstream
 
         if self.verbose:
             print "Origin branch is " + self.origin
 
-        if len(depotPath) == 0:
+        if len(self.depotPath) == 0:
             print "Internal error: cannot locate perforce depot path from existing branches"
             sys.exit(128)
 
-        self.clientPath = p4Where(depotPath)
+        self.clientPath = p4Where(self.depotPath)
 
         if len(self.clientPath) == 0:
-            print "Error: Cannot locate perforce checkout of %s in client view" % depotPath
+            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
             sys.exit(128)
 
-        print "Perforce checkout for depot path %s located at %s" % (depotPath, self.clientPath)
+        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
         self.oldWorkingDirectory = os.getcwd()
 
         if self.directSubmit:
@@ -590,9 +685,8 @@ class P4Submit(Command):
             f.close();
 
         os.chdir(self.clientPath)
-        response = raw_input("Do you want to sync %s with p4 sync? [y]es/[n]o " % self.clientPath)
-        if response == "y" or response == "yes":
-            system("p4 sync ...")
+        print "Syncronizing p4 checkout..."
+        system("p4 sync ...")
 
         if self.reset:
             self.firstTime = True
@@ -631,10 +725,14 @@ class P4Submit(Command):
             else:
                 print "All changes applied!"
                 os.chdir(self.oldWorkingDirectory)
-                response = raw_input("Do you want to sync from Perforce now using git-p4 rebase? [y]es/[n]o ")
+
+                sync = P4Sync()
+                sync.run([])
+
+                response = raw_input("Do you want to rebase current HEAD from Perforce now using git-p4 rebase? [y]es/[n]o ")
                 if response == "y" or response == "yes":
                     rebase = P4Rebase()
-                    rebase.run([])
+                    rebase.rebase()
             os.remove(self.configFile)
 
         return True
@@ -842,7 +940,7 @@ class P4Sync(Command):
                 data = file['data']
 
                 mode = "644"
-                if file["type"].startswith("x"):
+                if isP4Exec(file["type"]):
                     mode = "755"
                 elif file["type"] == "symlink":
                     mode = "120000"
@@ -1017,63 +1115,196 @@ class P4Sync(Command):
         for branch in branches.keys():
             self.initialParents[self.refPrefix + branch] = branches[branch]
 
-    def createOrUpdateBranchesFromOrigin(self):
-        if not self.silent:
-            print ("Creating/updating branch(es) in %s based on origin branch(es)"
-                   % self.refPrefix)
+    def updateOptionDict(self, d):
+        option_keys = {}
+        if self.keepRepoPath:
+            option_keys['keepRepoPath'] = 1
 
-        originPrefix = "origin/p4/"
+        d["options"] = ' '.join(sorted(option_keys.keys()))
 
-        for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
-            line = line.strip()
-            if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
-                continue
+    def readOptions(self, d):
+        self.keepRepoPath = (d.has_key('options')
+                             and ('keepRepoPath' in d['options']))
 
-            headName = line[len(originPrefix):]
-            remoteHead = self.refPrefix + headName
-            originHead = line
+    def gitRefForBranch(self, branch):
+        if branch == "main":
+            return self.refPrefix + "master"
 
-            original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
-            if (not original.has_key('depot-paths')
-                or not original.has_key('change')):
-                continue
+        if len(branch) <= 0:
+            return branch
 
-            update = False
-            if not gitBranchExists(remoteHead):
+        return self.refPrefix + self.projectName + branch
+
+    def gitCommitByP4Change(self, ref, change):
+        if self.verbose:
+            print "looking in ref " + ref + " for change %s using bisect..." % change
+
+        earliestCommit = ""
+        latestCommit = parseRevision(ref)
+
+        while True:
+            if self.verbose:
+                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
+            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
+            if len(next) == 0:
                 if self.verbose:
-                    print "creating %s" % remoteHead
-                update = True
+                    print "argh"
+                return ""
+            log = extractLogMessageFromGitCommit(next)
+            settings = extractSettingsGitLog(log)
+            currentChange = int(settings['change'])
+            if self.verbose:
+                print "current change %s" % currentChange
+
+            if currentChange == change:
+                if self.verbose:
+                    print "found %s" % next
+                return next
+
+            if currentChange < change:
+                earliestCommit = "^%s" % next
             else:
-                settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
-                if settings.has_key('change') > 0:
-                    if settings['depot-paths'] == original['depot-paths']:
-                        originP4Change = int(original['change'])
-                        p4Change = int(settings['change'])
-                        if originP4Change > p4Change:
-                            print ("%s (%s) is newer than %s (%s). "
-                                   "Updating p4 branch from origin."
-                                   % (originHead, originP4Change,
-                                      remoteHead, p4Change))
-                            update = True
-                    else:
-                        print ("Ignoring: %s was imported from %s while "
-                               "%s was imported from %s"
-                               % (originHead, ','.join(original['depot-paths']),
-                                  remoteHead, ','.join(settings['depot-paths'])))
+                latestCommit = "%s" % next
 
-            if update:
-                system("git update-ref %s %s" % (remoteHead, originHead))
+        return ""
 
-    def updateOptionDict(self, d):
-        option_keys = {}
-        if self.keepRepoPath:
-            option_keys['keepRepoPath'] = 1
+    def importNewBranch(self, branch, maxChange):
+        # make fast-import flush all changes to disk and update the refs using the checkpoint
+        # command so that we can try to find the branch parent in the git history
+        self.gitStream.write("checkpoint\n\n");
+        self.gitStream.flush();
+        branchPrefix = self.depotPaths[0] + branch + "/"
+        range = "@1,%s" % maxChange
+        #print "prefix" + branchPrefix
+        changes = p4ChangesForPaths([branchPrefix], range)
+        if len(changes) <= 0:
+            return False
+        firstChange = changes[0]
+        #print "first change in branch: %s" % firstChange
+        sourceBranch = self.knownBranches[branch]
+        sourceDepotPath = self.depotPaths[0] + sourceBranch
+        sourceRef = self.gitRefForBranch(sourceBranch)
+        #print "source " + sourceBranch
+
+        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
+        #print "branch parent: %s" % branchParentChange
+        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
+        if len(gitParent) > 0:
+            self.initialParents[self.gitRefForBranch(branch)] = gitParent
+            #print "parent git commit: %s" % gitParent
+
+        self.importChanges(changes)
+        return True
 
-        d["options"] = ' '.join(sorted(option_keys.keys()))
+    def importChanges(self, changes):
+        cnt = 1
+        for change in changes:
+            description = p4Cmd("describe %s" % change)
+            self.updateOptionDict(description)
+
+            if not self.silent:
+                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
+                sys.stdout.flush()
+            cnt = cnt + 1
+
+            try:
+                if self.detectBranches:
+                    branches = self.splitFilesIntoBranches(description)
+                    for branch in branches.keys():
+                        ## HACK  --hwn
+                        branchPrefix = self.depotPaths[0] + branch + "/"
+
+                        parent = ""
+
+                        filesForCommit = branches[branch]
+
+                        if self.verbose:
+                            print "branch is %s" % branch
+
+                        self.updatedBranches.add(branch)
+
+                        if branch not in self.createdBranches:
+                            self.createdBranches.add(branch)
+                            parent = self.knownBranches[branch]
+                            if parent == branch:
+                                parent = ""
+                            else:
+                                fullBranch = self.projectName + branch
+                                if fullBranch not in self.p4BranchesInGit:
+                                    if not self.silent:
+                                        print("\n    Importing new branch %s" % fullBranch);
+                                    if self.importNewBranch(branch, change - 1):
+                                        parent = ""
+                                        self.p4BranchesInGit.append(fullBranch)
+                                    if not self.silent:
+                                        print("\n    Resuming with change %s" % change);
+
+                                if self.verbose:
+                                    print "parent determined through known branches: %s" % parent
+
+                        branch = self.gitRefForBranch(branch)
+                        parent = self.gitRefForBranch(parent)
+
+                        if self.verbose:
+                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
+
+                        if len(parent) == 0 and branch in self.initialParents:
+                            parent = self.initialParents[branch]
+                            del self.initialParents[branch]
+
+                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
+                else:
+                    files = self.extractFilesFromCommit(description)
+                    self.commit(description, files, self.branch, self.depotPaths,
+                                self.initialParent)
+                    self.initialParent = ""
+            except IOError:
+                print self.gitError.read()
+                sys.exit(1)
+
+    def importHeadRevision(self, revision):
+        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
+
+        details = { "user" : "git perforce import user", "time" : int(time.time()) }
+        details["desc"] = ("Initial import of %s from the state at revision %s"
+                           % (' '.join(self.depotPaths), revision))
+        details["change"] = revision
+        newestRevision = 0
+
+        fileCnt = 0
+        for info in p4CmdList("files "
+                              +  ' '.join(["%s...%s"
+                                           % (p, revision)
+                                           for p in self.depotPaths])):
+
+            if info['code'] == 'error':
+                sys.stderr.write("p4 returned an error: %s\n"
+                                 % info['data'])
+                sys.exit(1)
+
+
+            change = int(info["change"])
+            if change > newestRevision:
+                newestRevision = change
+
+            if info["action"] == "delete":
+                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
+                #fileCnt = fileCnt + 1
+                continue
+
+            for prop in ["depotFile", "rev", "action", "type" ]:
+                details["%s%s" % (prop, fileCnt)] = info[prop]
+
+            fileCnt = fileCnt + 1
+
+        details["change"] = newestRevision
+        self.updateOptionDict(details)
+        try:
+            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
+        except IOError:
+            print "IO error with git fast-import. Is your git version recent enough?"
+            print self.gitError.read()
 
-    def readOptions(self, d):
-        self.keepRepoPath = (d.has_key('options')
-                             and ('keepRepoPath' in d['options']))
 
     def run(self, args):
         self.depotPaths = []
@@ -1084,7 +1315,7 @@ class P4Sync(Command):
         # map from branch depot path to parent branch
         self.knownBranches = {}
         self.initialParents = {}
-        self.hasOrigin = gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
+        self.hasOrigin = originP4BranchesExist()
         if not self.syncWithOrigin:
             self.hasOrigin = False
 
@@ -1104,14 +1335,14 @@ class P4Sync(Command):
                 system("git update-ref %s refs/heads/p4" % self.branch)
                 system("git branch -D p4");
             # create it /after/ importing, when master exists
-            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes:
+            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
                 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
 
         # TODO: should always look at previous commits,
         # merge with previous imports, if possible.
         if args == []:
             if self.hasOrigin:
-                self.createOrUpdateBranchesFromOrigin()
+                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
             self.listExistingP4GitBranches()
 
             if len(self.p4BranchesInGit) > 1:
@@ -1172,7 +1403,7 @@ class P4Sync(Command):
 
             self.depotPaths = sorted(args)
 
-        self.revision = ""
+        revision = ""
         self.users = {}
 
         newPaths = []
@@ -1183,15 +1414,15 @@ class P4Sync(Command):
                 if self.changeRange == "@all":
                     self.changeRange = ""
                 elif ',' not in self.changeRange:
-                    self.revision = self.changeRange
+                    revision = self.changeRange
                     self.changeRange = ""
                 p = p[:atIdx]
             elif p.find("#") != -1:
                 hashIdx = p.index("#")
-                self.revision = p[hashIdx:]
+                revision = p[hashIdx:]
                 p = p[:hashIdx]
             elif self.previousDepotPaths == []:
-                self.revision = "#head"
+                revision = "#head"
 
             p = re.sub ("\.\.\.$", "", p)
             if not p.endswith("/"):
@@ -1232,49 +1463,8 @@ class P4Sync(Command):
         self.gitStream = importProcess.stdin
         self.gitError = importProcess.stderr
 
-        if self.revision:
-            print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), self.revision, self.branch)
-
-            details = { "user" : "git perforce import user", "time" : int(time.time()) }
-            details["desc"] = ("Initial import of %s from the state at revision %s"
-                               % (' '.join(self.depotPaths), self.revision))
-            details["change"] = self.revision
-            newestRevision = 0
-
-            fileCnt = 0
-            for info in p4CmdList("files "
-                                  +  ' '.join(["%s...%s"
-                                               % (p, self.revision)
-                                               for p in self.depotPaths])):
-
-                if info['code'] == 'error':
-                    sys.stderr.write("p4 returned an error: %s\n"
-                                     % info['data'])
-                    sys.exit(1)
-
-
-                change = int(info["change"])
-                if change > newestRevision:
-                    newestRevision = change
-
-                if info["action"] == "delete":
-                    # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
-                    #fileCnt = fileCnt + 1
-                    continue
-
-                for prop in ["depotFile", "rev", "action", "type" ]:
-                    details["%s%s" % (prop, fileCnt)] = info[prop]
-
-                fileCnt = fileCnt + 1
-
-            details["change"] = newestRevision
-            self.updateOptionDict(details)
-            try:
-                self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
-            except IOError:
-                print "IO error with git fast-import. Is your git version recent enough?"
-                print self.gitError.read()
-
+        if revision:
+            self.importHeadRevision(revision)
         else:
             changes = []
 
@@ -1292,15 +1482,7 @@ class P4Sync(Command):
                 if self.verbose:
                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
                                                               self.changeRange)
-                assert self.depotPaths
-                output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, self.changeRange)
-                                                                    for p in self.depotPaths]))
-
-                for line in output:
-                    changeNum = line.split(" ")[1]
-                    changes.append(changeNum)
-
-                changes.sort()
+                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
 
                 if len(self.maxChanges) > 0:
                     changes = changes[:min(int(self.maxChanges), len(changes))]
@@ -1315,74 +1497,7 @@ class P4Sync(Command):
 
             self.updatedBranches = set()
 
-            cnt = 1
-            for change in changes:
-                description = p4Cmd("describe %s" % change)
-                self.updateOptionDict(description)
-
-                if not self.silent:
-                    sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
-                    sys.stdout.flush()
-                cnt = cnt + 1
-
-                try:
-                    if self.detectBranches:
-                        branches = self.splitFilesIntoBranches(description)
-                        for branch in branches.keys():
-                            ## HACK  --hwn
-                            branchPrefix = self.depotPaths[0] + branch + "/"
-
-                            parent = ""
-
-                            filesForCommit = branches[branch]
-
-                            if self.verbose:
-                                print "branch is %s" % branch
-
-                            self.updatedBranches.add(branch)
-
-                            if branch not in self.createdBranches:
-                                self.createdBranches.add(branch)
-                                parent = self.knownBranches[branch]
-                                if parent == branch:
-                                    parent = ""
-                                elif self.verbose:
-                                    print "parent determined through known branches: %s" % parent
-
-                            # main branch? use master
-                            if branch == "main":
-                                branch = "master"
-                            else:
-
-                                ## FIXME
-                                branch = self.projectName + branch
-
-                            if parent == "main":
-                                parent = "master"
-                            elif len(parent) > 0:
-                                ## FIXME
-                                parent = self.projectName + parent
-
-                            branch = self.refPrefix + branch
-                            if len(parent) > 0:
-                                parent = self.refPrefix + parent
-
-                            if self.verbose:
-                                print "looking for initial parent for %s; current parent is %s" % (branch, parent)
-
-                            if len(parent) == 0 and branch in self.initialParents:
-                                parent = self.initialParents[branch]
-                                del self.initialParents[branch]
-
-                            self.commit(description, filesForCommit, branch, [branchPrefix], parent)
-                    else:
-                        files = self.extractFilesFromCommit(description)
-                        self.commit(description, files, self.branch, self.depotPaths,
-                                    self.initialParent)
-                        self.initialParent = ""
-                except IOError:
-                    print self.gitError.read()
-                    sys.exit(1)
+            self.importChanges(changes)
 
             if not self.silent:
                 print ""
@@ -1392,7 +1507,6 @@ class P4Sync(Command):
                         sys.stdout.write("%s " % b)
                     sys.stdout.write("\n")
 
-
         self.gitStream.close()
         if importProcess.wait() != 0:
             die("fast-import failed: %s" % self.gitError.read())
@@ -1413,6 +1527,9 @@ class P4Rebase(Command):
         sync = P4Sync()
         sync.run([])
 
+        return self.rebase()
+
+    def rebase(self):
         [upstream, settings] = findUpstreamBranchPoint()
         if len(upstream) == 0:
             die("Cannot find upstream branchpoint for rebase")
@@ -1494,6 +1611,9 @@ class P4Branches(Command):
         self.verbose = False
 
     def run(self, args):
+        if originP4BranchesExist():
+            createOrUpdateBranchesFromOrigin()
+
         cmdline = "git rev-parse --symbolic "
         cmdline += " --remotes"