Code

Make git-p4 submit --direct safer by also creating a git commit
[git.git] / contrib / fast-import / git-p4
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 <hausmann@kde.org>
6 # Copyright: 2007 Simon Hausmann <hausmann@kde.org>
7 #            2007 Trolltech ASA
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
9 #
10 # TODO: * implement git-p4 rollback <perforce change number> for debugging
11 #         to roll back all p4 remote branches to a commit older or equal to
12 #         the specified change.
13 #       * Consider making --with-origin the default, assuming that the git
14 #         protocol is always more efficient. (needs manual testing first :)
15 #
17 import optparse, sys, os, marshal, popen2, subprocess, shelve
18 import tempfile, getopt, sha, os.path, time, platform
19 from sets import Set;
21 gitdir = os.environ.get("GIT_DIR", "")
23 def mypopen(command):
24     return os.popen(command, "rb");
26 def p4CmdList(cmd):
27     cmd = "p4 -G %s" % cmd
28     pipe = os.popen(cmd, "rb")
30     result = []
31     try:
32         while True:
33             entry = marshal.load(pipe)
34             result.append(entry)
35     except EOFError:
36         pass
37     pipe.close()
39     return result
41 def p4Cmd(cmd):
42     list = p4CmdList(cmd)
43     result = {}
44     for entry in list:
45         result.update(entry)
46     return result;
48 def p4Where(depotPath):
49     if not depotPath.endswith("/"):
50         depotPath += "/"
51     output = p4Cmd("where %s..." % depotPath)
52     if output["code"] == "error":
53         return ""
54     clientPath = ""
55     if "path" in output:
56         clientPath = output.get("path")
57     elif "data" in output:
58         data = output.get("data")
59         lastSpace = data.rfind(" ")
60         clientPath = data[lastSpace + 1:]
62     if clientPath.endswith("..."):
63         clientPath = clientPath[:-3]
64     return clientPath
66 def die(msg):
67     sys.stderr.write(msg + "\n")
68     sys.exit(1)
70 def currentGitBranch():
71     return mypopen("git name-rev HEAD").read().split(" ")[1][:-1]
73 def isValidGitDir(path):
74     if os.path.exists(path + "/HEAD") and os.path.exists(path + "/refs") and os.path.exists(path + "/objects"):
75         return True;
76     return False
78 def parseRevision(ref):
79     return mypopen("git rev-parse %s" % ref).read()[:-1]
81 def system(cmd):
82     if os.system(cmd) != 0:
83         die("command failed: %s" % cmd)
85 def extractLogMessageFromGitCommit(commit):
86     logMessage = ""
87     foundTitle = False
88     for log in mypopen("git cat-file commit %s" % commit).readlines():
89        if not foundTitle:
90            if len(log) == 1:
91                foundTitle = True
92            continue
94        logMessage += log
95     return logMessage
97 def extractDepotPathAndChangeFromGitLog(log):
98     values = {}
99     for line in log.split("\n"):
100         line = line.strip()
101         if line.startswith("[git-p4:") and line.endswith("]"):
102             line = line[8:-1].strip()
103             for assignment in line.split(":"):
104                 variable = assignment.strip()
105                 value = ""
106                 equalPos = assignment.find("=")
107                 if equalPos != -1:
108                     variable = assignment[:equalPos].strip()
109                     value = assignment[equalPos + 1:].strip()
110                     if value.startswith("\"") and value.endswith("\""):
111                         value = value[1:-1]
112                 values[variable] = value
114     return values.get("depot-path"), values.get("change")
116 def gitBranchExists(branch):
117     proc = subprocess.Popen(["git", "rev-parse", branch], stderr=subprocess.PIPE, stdout=subprocess.PIPE);
118     return proc.wait() == 0;
120 class Command:
121     def __init__(self):
122         self.usage = "usage: %prog [options]"
123         self.needsGit = True
125 class P4Debug(Command):
126     def __init__(self):
127         Command.__init__(self)
128         self.options = [
129         ]
130         self.description = "A tool to debug the output of p4 -G."
131         self.needsGit = False
133     def run(self, args):
134         for output in p4CmdList(" ".join(args)):
135             print output
136         return True
138 class P4Submit(Command):
139     def __init__(self):
140         Command.__init__(self)
141         self.options = [
142                 optparse.make_option("--continue", action="store_false", dest="firstTime"),
143                 optparse.make_option("--origin", dest="origin"),
144                 optparse.make_option("--reset", action="store_true", dest="reset"),
145                 optparse.make_option("--log-substitutions", dest="substFile"),
146                 optparse.make_option("--noninteractive", action="store_false"),
147                 optparse.make_option("--dry-run", action="store_true"),
148                 optparse.make_option("--direct", dest="directSubmit", action="store_true"),
149         ]
150         self.description = "Submit changes from git to the perforce depot."
151         self.usage += " [name of git branch to submit into perforce depot]"
152         self.firstTime = True
153         self.reset = False
154         self.interactive = True
155         self.dryRun = False
156         self.substFile = ""
157         self.firstTime = True
158         self.origin = ""
159         self.directSubmit = False
161         self.logSubstitutions = {}
162         self.logSubstitutions["<enter description here>"] = "%log%"
163         self.logSubstitutions["\tDetails:"] = "\tDetails:  %log%"
165     def check(self):
166         if len(p4CmdList("opened ...")) > 0:
167             die("You have files opened with perforce! Close them before starting the sync.")
169     def start(self):
170         if len(self.config) > 0 and not self.reset:
171             die("Cannot start sync. Previous sync config found at %s\nIf you want to start submitting again from scratch maybe you want to call git-p4 submit --reset" % self.configFile)
173         commits = []
174         if self.directSubmit:
175             commits.append("0")
176         else:
177             for line in mypopen("git rev-list --no-merges %s..%s" % (self.origin, self.master)).readlines():
178                 commits.append(line[:-1])
179             commits.reverse()
181         self.config["commits"] = commits
183     def prepareLogMessage(self, template, message):
184         result = ""
186         for line in template.split("\n"):
187             if line.startswith("#"):
188                 result += line + "\n"
189                 continue
191             substituted = False
192             for key in self.logSubstitutions.keys():
193                 if line.find(key) != -1:
194                     value = self.logSubstitutions[key]
195                     value = value.replace("%log%", message)
196                     if value != "@remove@":
197                         result += line.replace(key, value) + "\n"
198                     substituted = True
199                     break
201             if not substituted:
202                 result += line + "\n"
204         return result
206     def apply(self, id):
207         if self.directSubmit:
208             print "Applying local change in working directory/index"
209             diff = self.diffStatus
210         else:
211             print "Applying %s" % (mypopen("git log --max-count=1 --pretty=oneline %s" % id).read())
212             diff = mypopen("git diff-tree -r --name-status \"%s^\" \"%s\"" % (id, id)).readlines()
213         filesToAdd = set()
214         filesToDelete = set()
215         editedFiles = set()
216         for line in diff:
217             modifier = line[0]
218             path = line[1:].strip()
219             if modifier == "M":
220                 system("p4 edit \"%s\"" % path)
221                 editedFiles.add(path)
222             elif modifier == "A":
223                 filesToAdd.add(path)
224                 if path in filesToDelete:
225                     filesToDelete.remove(path)
226             elif modifier == "D":
227                 filesToDelete.add(path)
228                 if path in filesToAdd:
229                     filesToAdd.remove(path)
230             else:
231                 die("unknown modifier %s for %s" % (modifier, path))
233         if self.directSubmit:
234             diffcmd = "cat \"%s\"" % self.diffFile
235         else:
236             diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
237         patchcmd = diffcmd + " | git apply "
238         tryPatchCmd = patchcmd + "--check -"
239         applyPatchCmd = patchcmd + "--check --apply -"
241         if os.system(tryPatchCmd) != 0:
242             print "Unfortunately applying the change failed!"
243             print "What do you want to do?"
244             response = "x"
245             while response != "s" and response != "a" and response != "w":
246                 response = raw_input("[s]kip this patch / [a]pply the patch forcibly and with .rej files / [w]rite the patch to a file (patch.txt) ")
247             if response == "s":
248                 print "Skipping! Good luck with the next patches..."
249                 return
250             elif response == "a":
251                 os.system(applyPatchCmd)
252                 if len(filesToAdd) > 0:
253                     print "You may also want to call p4 add on the following files:"
254                     print " ".join(filesToAdd)
255                 if len(filesToDelete):
256                     print "The following files should be scheduled for deletion with p4 delete:"
257                     print " ".join(filesToDelete)
258                 die("Please resolve and submit the conflict manually and continue afterwards with git-p4 submit --continue")
259             elif response == "w":
260                 system(diffcmd + " > patch.txt")
261                 print "Patch saved to patch.txt in %s !" % self.clientPath
262                 die("Please resolve and submit the conflict manually and continue afterwards with git-p4 submit --continue")
264         system(applyPatchCmd)
266         for f in filesToAdd:
267             system("p4 add %s" % f)
268         for f in filesToDelete:
269             system("p4 revert %s" % f)
270             system("p4 delete %s" % f)
272         logMessage = ""
273         if not self.directSubmit:
274             logMessage = extractLogMessageFromGitCommit(id)
275             logMessage = logMessage.replace("\n", "\n\t")
276             logMessage = logMessage[:-1]
278         template = mypopen("p4 change -o").read()
280         if self.interactive:
281             submitTemplate = self.prepareLogMessage(template, logMessage)
282             diff = mypopen("p4 diff -du ...").read()
284             for newFile in filesToAdd:
285                 diff += "==== new file ====\n"
286                 diff += "--- /dev/null\n"
287                 diff += "+++ %s\n" % newFile
288                 f = open(newFile, "r")
289                 for line in f.readlines():
290                     diff += "+" + line
291                 f.close()
293             separatorLine = "######## everything below this line is just the diff #######"
294             if platform.system() == "Windows":
295                 separatorLine += "\r"
296             separatorLine += "\n"
298             response = "e"
299             firstIteration = True
300             while response == "e":
301                 if not firstIteration:
302                     response = raw_input("Do you want to submit this change? [y]es/[e]dit/[n]o/[s]kip ")
303                 firstIteration = False
304                 if response == "e":
305                     [handle, fileName] = tempfile.mkstemp()
306                     tmpFile = os.fdopen(handle, "w+")
307                     tmpFile.write(submitTemplate + separatorLine + diff)
308                     tmpFile.close()
309                     defaultEditor = "vi"
310                     if platform.system() == "Windows":
311                         defaultEditor = "notepad"
312                     editor = os.environ.get("EDITOR", defaultEditor);
313                     system(editor + " " + fileName)
314                     tmpFile = open(fileName, "rb")
315                     message = tmpFile.read()
316                     tmpFile.close()
317                     os.remove(fileName)
318                     submitTemplate = message[:message.index(separatorLine)]
320             if response == "y" or response == "yes":
321                if self.dryRun:
322                    print submitTemplate
323                    raw_input("Press return to continue...")
324                else:
325                    if self.directSubmit:
326                        print "Submitting to git first"
327                        os.chdir(self.oldWorkingDirectory)
328                        pipe = os.popen("git commit -a -F -", "wb")
329                        pipe.write(submitTemplate)
330                        pipe.close()
331                        os.chdir(self.clientPath)
333                    pipe = os.popen("p4 submit -i", "wb")
334                    pipe.write(submitTemplate)
335                    pipe.close()
336             elif response == "s":
337                 for f in editedFiles:
338                     system("p4 revert \"%s\"" % f);
339                 for f in filesToAdd:
340                     system("p4 revert \"%s\"" % f);
341                     system("rm %s" %f)
342                 for f in filesToDelete:
343                     system("p4 delete \"%s\"" % f);
344                 return
345             else:
346                 print "Not submitting!"
347                 self.interactive = False
348         else:
349             fileName = "submit.txt"
350             file = open(fileName, "w+")
351             file.write(self.prepareLogMessage(template, logMessage))
352             file.close()
353             print "Perforce submit template written as %s. Please review/edit and then use p4 submit -i < %s to submit directly!" % (fileName, fileName)
355     def run(self, args):
356         global gitdir
357         # make gitdir absolute so we can cd out into the perforce checkout
358         gitdir = os.path.abspath(gitdir)
359         os.environ["GIT_DIR"] = gitdir
361         if len(args) == 0:
362             self.master = currentGitBranch()
363             if len(self.master) == 0 or not os.path.exists("%s/refs/heads/%s" % (gitdir, self.master)):
364                 die("Detecting current git branch failed!")
365         elif len(args) == 1:
366             self.master = args[0]
367         else:
368             return False
370         depotPath = ""
371         if gitBranchExists("p4"):
372             [depotPath, dummy] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("p4"))
373         if len(depotPath) == 0 and gitBranchExists("origin"):
374             [depotPath, dummy] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("origin"))
376         if len(depotPath) == 0:
377             print "Internal error: cannot locate perforce depot path from existing branches"
378             sys.exit(128)
380         self.clientPath = p4Where(depotPath)
382         if len(self.clientPath) == 0:
383             print "Error: Cannot locate perforce checkout of %s in client view" % depotPath
384             sys.exit(128)
386         print "Perforce checkout for depot path %s located at %s" % (depotPath, self.clientPath)
387         self.oldWorkingDirectory = os.getcwd()
389         if self.directSubmit:
390             self.diffStatus = mypopen("git diff -r --name-status HEAD").readlines()
391             if len(self.diffStatus) == 0:
392                 print "No changes in working directory to submit."
393                 return True
394             patch = mypopen("git diff -p --binary --diff-filter=ACMRTUXB HEAD").read()
395             self.diffFile = gitdir + "/p4-git-diff"
396             f = open(self.diffFile, "wb")
397             f.write(patch)
398             f.close();
400         os.chdir(self.clientPath)
401         response = raw_input("Do you want to sync %s with p4 sync? [y]es/[n]o " % self.clientPath)
402         if response == "y" or response == "yes":
403             system("p4 sync ...")
405         if len(self.origin) == 0:
406             if gitBranchExists("p4"):
407                 self.origin = "p4"
408             else:
409                 self.origin = "origin"
411         if self.reset:
412             self.firstTime = True
414         if len(self.substFile) > 0:
415             for line in open(self.substFile, "r").readlines():
416                 tokens = line[:-1].split("=")
417                 self.logSubstitutions[tokens[0]] = tokens[1]
419         self.check()
420         self.configFile = gitdir + "/p4-git-sync.cfg"
421         self.config = shelve.open(self.configFile, writeback=True)
423         if self.firstTime:
424             self.start()
426         commits = self.config.get("commits", [])
428         while len(commits) > 0:
429             self.firstTime = False
430             commit = commits[0]
431             commits = commits[1:]
432             self.config["commits"] = commits
433             self.apply(commit)
434             if not self.interactive:
435                 break
437         self.config.close()
439         if self.directSubmit:
440             os.remove(self.diffFile)
442         if len(commits) == 0:
443             if self.firstTime:
444                 print "No changes found to apply between %s and current HEAD" % self.origin
445             else:
446                 print "All changes applied!"
447                 os.chdir(self.oldWorkingDirectory)
448                 response = raw_input("Do you want to sync from Perforce now using git-p4 rebase? [y]es/[n]o ")
449                 if response == "y" or response == "yes":
450                     rebase = P4Rebase()
451                     rebase.run([])
452             os.remove(self.configFile)
454         return True
456 class P4Sync(Command):
457     def __init__(self):
458         Command.__init__(self)
459         self.options = [
460                 optparse.make_option("--branch", dest="branch"),
461                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
462                 optparse.make_option("--changesfile", dest="changesFile"),
463                 optparse.make_option("--silent", dest="silent", action="store_true"),
464                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
465                 optparse.make_option("--with-origin", dest="syncWithOrigin", action="store_true"),
466                 optparse.make_option("--verbose", dest="verbose", action="store_true")
467         ]
468         self.description = """Imports from Perforce into a git repository.\n
469     example:
470     //depot/my/project/ -- to import the current head
471     //depot/my/project/@all -- to import everything
472     //depot/my/project/@1,6 -- to import only from revision 1 to 6
474     (a ... is not needed in the path p4 specification, it's added implicitly)"""
476         self.usage += " //depot/path[@revRange]"
478         self.silent = False
479         self.createdBranches = Set()
480         self.committedChanges = Set()
481         self.branch = ""
482         self.detectBranches = False
483         self.detectLabels = False
484         self.changesFile = ""
485         self.syncWithOrigin = False
486         self.verbose = False
488     def p4File(self, depotPath):
489         return os.popen("p4 print -q \"%s\"" % depotPath, "rb").read()
491     def extractFilesFromCommit(self, commit):
492         files = []
493         fnum = 0
494         while commit.has_key("depotFile%s" % fnum):
495             path =  commit["depotFile%s" % fnum]
496             if not path.startswith(self.depotPath):
497     #            if not self.silent:
498     #                print "\nchanged files: ignoring path %s outside of %s in change %s" % (path, self.depotPath, change)
499                 fnum = fnum + 1
500                 continue
502             file = {}
503             file["path"] = path
504             file["rev"] = commit["rev%s" % fnum]
505             file["action"] = commit["action%s" % fnum]
506             file["type"] = commit["type%s" % fnum]
507             files.append(file)
508             fnum = fnum + 1
509         return files
511     def splitFilesIntoBranches(self, commit):
512         branches = {}
514         fnum = 0
515         while commit.has_key("depotFile%s" % fnum):
516             path =  commit["depotFile%s" % fnum]
517             if not path.startswith(self.depotPath):
518     #            if not self.silent:
519     #                print "\nchanged files: ignoring path %s outside of %s in change %s" % (path, self.depotPath, change)
520                 fnum = fnum + 1
521                 continue
523             file = {}
524             file["path"] = path
525             file["rev"] = commit["rev%s" % fnum]
526             file["action"] = commit["action%s" % fnum]
527             file["type"] = commit["type%s" % fnum]
528             fnum = fnum + 1
530             relPath = path[len(self.depotPath):]
532             for branch in self.knownBranches.keys():
533                 if relPath.startswith(branch):
534                     if branch not in branches:
535                         branches[branch] = []
536                     branches[branch].append(file)
538         return branches
540     def commit(self, details, files, branch, branchPrefix, parent = ""):
541         epoch = details["time"]
542         author = details["user"]
544         if self.verbose:
545             print "commit into %s" % branch
547         self.gitStream.write("commit %s\n" % branch)
548     #    gitStream.write("mark :%s\n" % details["change"])
549         self.committedChanges.add(int(details["change"]))
550         committer = ""
551         if author not in self.users:
552             self.getUserMapFromPerforceServer()
553         if author in self.users:
554             committer = "%s %s %s" % (self.users[author], epoch, self.tz)
555         else:
556             committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
558         self.gitStream.write("committer %s\n" % committer)
560         self.gitStream.write("data <<EOT\n")
561         self.gitStream.write(details["desc"])
562         self.gitStream.write("\n[git-p4: depot-path = \"%s\": change = %s]\n" % (branchPrefix, details["change"]))
563         self.gitStream.write("EOT\n\n")
565         if len(parent) > 0:
566             if self.verbose:
567                 print "parent %s" % parent
568             self.gitStream.write("from %s\n" % parent)
570         for file in files:
571             path = file["path"]
572             if not path.startswith(branchPrefix):
573     #            if not silent:
574     #                print "\nchanged files: ignoring path %s outside of branch prefix %s in change %s" % (path, branchPrefix, details["change"])
575                 continue
576             rev = file["rev"]
577             depotPath = path + "#" + rev
578             relPath = path[len(branchPrefix):]
579             action = file["action"]
581             if file["type"] == "apple":
582                 print "\nfile %s is a strange apple file that forks. Ignoring!" % path
583                 continue
585             if action == "delete":
586                 self.gitStream.write("D %s\n" % relPath)
587             else:
588                 mode = 644
589                 if file["type"].startswith("x"):
590                     mode = 755
592                 data = self.p4File(depotPath)
594                 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
595                 self.gitStream.write("data %s\n" % len(data))
596                 self.gitStream.write(data)
597                 self.gitStream.write("\n")
599         self.gitStream.write("\n")
601         change = int(details["change"])
603         if self.labels.has_key(change):
604             label = self.labels[change]
605             labelDetails = label[0]
606             labelRevisions = label[1]
607             if self.verbose:
608                 print "Change %s is labelled %s" % (change, labelDetails)
610             files = p4CmdList("files %s...@%s" % (branchPrefix, change))
612             if len(files) == len(labelRevisions):
614                 cleanedFiles = {}
615                 for info in files:
616                     if info["action"] == "delete":
617                         continue
618                     cleanedFiles[info["depotFile"]] = info["rev"]
620                 if cleanedFiles == labelRevisions:
621                     self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
622                     self.gitStream.write("from %s\n" % branch)
624                     owner = labelDetails["Owner"]
625                     tagger = ""
626                     if author in self.users:
627                         tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
628                     else:
629                         tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
630                     self.gitStream.write("tagger %s\n" % tagger)
631                     self.gitStream.write("data <<EOT\n")
632                     self.gitStream.write(labelDetails["Description"])
633                     self.gitStream.write("EOT\n\n")
635                 else:
636                     if not self.silent:
637                         print "Tag %s does not match with change %s: files do not match." % (labelDetails["label"], change)
639             else:
640                 if not self.silent:
641                     print "Tag %s does not match with change %s: file count is different." % (labelDetails["label"], change)
643     def getUserMapFromPerforceServer(self):
644         self.users = {}
646         for output in p4CmdList("users"):
647             if not output.has_key("User"):
648                 continue
649             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
651         cache = open(gitdir + "/p4-usercache.txt", "wb")
652         for user in self.users.keys():
653             cache.write("%s\t%s\n" % (user, self.users[user]))
654         cache.close();
656     def loadUserMapFromCache(self):
657         self.users = {}
658         try:
659             cache = open(gitdir + "/p4-usercache.txt", "rb")
660             lines = cache.readlines()
661             cache.close()
662             for line in lines:
663                 entry = line[:-1].split("\t")
664                 self.users[entry[0]] = entry[1]
665         except IOError:
666             self.getUserMapFromPerforceServer()
668     def getLabels(self):
669         self.labels = {}
671         l = p4CmdList("labels %s..." % self.depotPath)
672         if len(l) > 0 and not self.silent:
673             print "Finding files belonging to labels in %s" % self.depotPath
675         for output in l:
676             label = output["label"]
677             revisions = {}
678             newestChange = 0
679             if self.verbose:
680                 print "Querying files for label %s" % label
681             for file in p4CmdList("files %s...@%s" % (self.depotPath, label)):
682                 revisions[file["depotFile"]] = file["rev"]
683                 change = int(file["change"])
684                 if change > newestChange:
685                     newestChange = change
687             self.labels[newestChange] = [output, revisions]
689         if self.verbose:
690             print "Label changes: %s" % self.labels.keys()
692     def getBranchMapping(self):
693         self.projectName = self.depotPath[self.depotPath[:-1].rfind("/") + 1:]
695         for info in p4CmdList("branches"):
696             details = p4Cmd("branch -o %s" % info["branch"])
697             viewIdx = 0
698             while details.has_key("View%s" % viewIdx):
699                 paths = details["View%s" % viewIdx].split(" ")
700                 viewIdx = viewIdx + 1
701                 # require standard //depot/foo/... //depot/bar/... mapping
702                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
703                     continue
704                 source = paths[0]
705                 destination = paths[1]
706                 if source.startswith(self.depotPath) and destination.startswith(self.depotPath):
707                     source = source[len(self.depotPath):-4]
708                     destination = destination[len(self.depotPath):-4]
709                     if destination not in self.knownBranches:
710                         self.knownBranches[destination] = source
711                     if source not in self.knownBranches:
712                         self.knownBranches[source] = source
714     def listExistingP4GitBranches(self):
715         self.p4BranchesInGit = []
717         for line in mypopen("git rev-parse --symbolic --remotes").readlines():
718             if line.startswith("p4/") and line != "p4/HEAD\n":
719                 branch = line[3:-1]
720                 self.p4BranchesInGit.append(branch)
721                 self.initialParents["refs/remotes/p4/" + branch] = parseRevision(line[:-1])
723     def run(self, args):
724         self.depotPath = ""
725         self.changeRange = ""
726         self.initialParent = ""
727         self.previousDepotPath = ""
728         # map from branch depot path to parent branch
729         self.knownBranches = {}
730         self.initialParents = {}
732         createP4HeadRef = False;
734         if self.syncWithOrigin and gitBranchExists("origin") and gitBranchExists("refs/remotes/p4/master") and not self.detectBranches:
735             ### needs to be ported to multi branch import
737             print "Syncing with origin first as requested by calling git fetch origin"
738             system("git fetch origin")
739             [originPreviousDepotPath, originP4Change] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("origin"))
740             [p4PreviousDepotPath, p4Change] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("p4"))
741             if len(originPreviousDepotPath) > 0 and len(originP4Change) > 0 and len(p4Change) > 0:
742                 if originPreviousDepotPath == p4PreviousDepotPath:
743                     originP4Change = int(originP4Change)
744                     p4Change = int(p4Change)
745                     if originP4Change > p4Change:
746                         print "origin (%s) is newer than p4 (%s). Updating p4 branch from origin." % (originP4Change, p4Change)
747                         system("git update-ref refs/remotes/p4/master origin");
748                 else:
749                     print "Cannot sync with origin. It was imported from %s while remotes/p4 was imported from %s" % (originPreviousDepotPath, p4PreviousDepotPath)
751         if len(self.branch) == 0:
752             self.branch = "refs/remotes/p4/master"
753             if gitBranchExists("refs/heads/p4"):
754                 system("git update-ref %s refs/heads/p4" % self.branch)
755                 system("git branch -D p4");
756             # create it /after/ importing, when master exists
757             if not gitBranchExists("refs/remotes/p4/HEAD"):
758                 createP4HeadRef = True
760         # this needs to be called after the conversion from heads/p4 to remotes/p4/master
761         self.listExistingP4GitBranches()
762         if len(self.p4BranchesInGit) > 1 and not self.silent:
763             print "Importing from/into multiple branches"
764             self.detectBranches = True
766         if len(args) == 0:
767             if not gitBranchExists(self.branch) and gitBranchExists("origin") and not self.detectBranches:
768                 ### needs to be ported to multi branch import
769                 if not self.silent:
770                     print "Creating %s branch in git repository based on origin" % self.branch
771                 branch = self.branch
772                 if not branch.startswith("refs"):
773                     branch = "refs/heads/" + branch
774                 system("git update-ref %s origin" % branch)
776             if self.verbose:
777                 print "branches: %s" % self.p4BranchesInGit
779             p4Change = 0
780             for branch in self.p4BranchesInGit:
781                 depotPath, change = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("refs/remotes/p4/" + branch))
783                 if self.verbose:
784                     print "path %s change %s" % (depotPath, change)
786                 if len(depotPath) > 0 and len(change) > 0:
787                     change = int(change) + 1
788                     p4Change = max(p4Change, change)
790                     if len(self.previousDepotPath) == 0:
791                         self.previousDepotPath = depotPath
792                     else:
793                         i = 0
794                         l = min(len(self.previousDepotPath), len(depotPath))
795                         while i < l and self.previousDepotPath[i] == depotPath[i]:
796                             i = i + 1
797                         self.previousDepotPath = self.previousDepotPath[:i]
799             if p4Change > 0:
800                 self.depotPath = self.previousDepotPath
801                 self.changeRange = "@%s,#head" % p4Change
802                 self.initialParent = parseRevision(self.branch)
803                 if not self.silent and not self.detectBranches:
804                     print "Performing incremental import into %s git branch" % self.branch
806         if not self.branch.startswith("refs/"):
807             self.branch = "refs/heads/" + self.branch
809         if len(self.depotPath) != 0:
810             self.depotPath = self.depotPath[:-1]
812         if len(args) == 0 and len(self.depotPath) != 0:
813             if not self.silent:
814                 print "Depot path: %s" % self.depotPath
815         elif len(args) != 1:
816             return False
817         else:
818             if len(self.depotPath) != 0 and self.depotPath != args[0]:
819                 print "previous import used depot path %s and now %s was specified. this doesn't work!" % (self.depotPath, args[0])
820                 sys.exit(1)
821             self.depotPath = args[0]
823         self.revision = ""
824         self.users = {}
826         if self.depotPath.find("@") != -1:
827             atIdx = self.depotPath.index("@")
828             self.changeRange = self.depotPath[atIdx:]
829             if self.changeRange == "@all":
830                 self.changeRange = ""
831             elif self.changeRange.find(",") == -1:
832                 self.revision = self.changeRange
833                 self.changeRange = ""
834             self.depotPath = self.depotPath[0:atIdx]
835         elif self.depotPath.find("#") != -1:
836             hashIdx = self.depotPath.index("#")
837             self.revision = self.depotPath[hashIdx:]
838             self.depotPath = self.depotPath[0:hashIdx]
839         elif len(self.previousDepotPath) == 0:
840             self.revision = "#head"
842         if self.depotPath.endswith("..."):
843             self.depotPath = self.depotPath[:-3]
845         if not self.depotPath.endswith("/"):
846             self.depotPath += "/"
848         self.loadUserMapFromCache()
849         self.labels = {}
850         if self.detectLabels:
851             self.getLabels();
853         if self.detectBranches:
854             self.getBranchMapping();
855             if self.verbose:
856                 print "p4-git branches: %s" % self.p4BranchesInGit
857                 print "initial parents: %s" % self.initialParents
858             for b in self.p4BranchesInGit:
859                 if b != "master":
860                     b = b[len(self.projectName):]
861                 self.createdBranches.add(b)
863         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
865         importProcess = subprocess.Popen(["git", "fast-import"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE);
866         self.gitOutput = importProcess.stdout
867         self.gitStream = importProcess.stdin
868         self.gitError = importProcess.stderr
870         if len(self.revision) > 0:
871             print "Doing initial import of %s from revision %s" % (self.depotPath, self.revision)
873             details = { "user" : "git perforce import user", "time" : int(time.time()) }
874             details["desc"] = "Initial import of %s from the state at revision %s" % (self.depotPath, self.revision)
875             details["change"] = self.revision
876             newestRevision = 0
878             fileCnt = 0
879             for info in p4CmdList("files %s...%s" % (self.depotPath, self.revision)):
880                 change = int(info["change"])
881                 if change > newestRevision:
882                     newestRevision = change
884                 if info["action"] == "delete":
885                     # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
886                     #fileCnt = fileCnt + 1
887                     continue
889                 for prop in [ "depotFile", "rev", "action", "type" ]:
890                     details["%s%s" % (prop, fileCnt)] = info[prop]
892                 fileCnt = fileCnt + 1
894             details["change"] = newestRevision
896             try:
897                 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPath)
898             except IOError:
899                 print "IO error with git fast-import. Is your git version recent enough?"
900                 print self.gitError.read()
902         else:
903             changes = []
905             if len(self.changesFile) > 0:
906                 output = open(self.changesFile).readlines()
907                 changeSet = Set()
908                 for line in output:
909                     changeSet.add(int(line))
911                 for change in changeSet:
912                     changes.append(change)
914                 changes.sort()
915             else:
916                 if self.verbose:
917                     print "Getting p4 changes for %s...%s" % (self.depotPath, self.changeRange)
918                 output = mypopen("p4 changes %s...%s" % (self.depotPath, self.changeRange)).readlines()
920                 for line in output:
921                     changeNum = line.split(" ")[1]
922                     changes.append(changeNum)
924                 changes.reverse()
926             if len(changes) == 0:
927                 if not self.silent:
928                     print "No changes to import!"
929                 return True
931             self.updatedBranches = set()
933             cnt = 1
934             for change in changes:
935                 description = p4Cmd("describe %s" % change)
937                 if not self.silent:
938                     sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
939                     sys.stdout.flush()
940                 cnt = cnt + 1
942                 try:
943                     if self.detectBranches:
944                         branches = self.splitFilesIntoBranches(description)
945                         for branch in branches.keys():
946                             branchPrefix = self.depotPath + branch + "/"
948                             parent = ""
950                             filesForCommit = branches[branch]
952                             if self.verbose:
953                                 print "branch is %s" % branch
955                             self.updatedBranches.add(branch)
957                             if branch not in self.createdBranches:
958                                 self.createdBranches.add(branch)
959                                 parent = self.knownBranches[branch]
960                                 if parent == branch:
961                                     parent = ""
962                                 elif self.verbose:
963                                     print "parent determined through known branches: %s" % parent
965                             # main branch? use master
966                             if branch == "main":
967                                 branch = "master"
968                             else:
969                                 branch = self.projectName + branch
971                             if parent == "main":
972                                 parent = "master"
973                             elif len(parent) > 0:
974                                 parent = self.projectName + parent
976                             branch = "refs/remotes/p4/" + branch
977                             if len(parent) > 0:
978                                 parent = "refs/remotes/p4/" + parent
980                             if self.verbose:
981                                 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
983                             if len(parent) == 0 and branch in self.initialParents:
984                                 parent = self.initialParents[branch]
985                                 del self.initialParents[branch]
987                             self.commit(description, filesForCommit, branch, branchPrefix, parent)
988                     else:
989                         files = self.extractFilesFromCommit(description)
990                         self.commit(description, files, self.branch, self.depotPath, self.initialParent)
991                         self.initialParent = ""
992                 except IOError:
993                     print self.gitError.read()
994                     sys.exit(1)
996             if not self.silent:
997                 print ""
998                 if len(self.updatedBranches) > 0:
999                     sys.stdout.write("Updated branches: ")
1000                     for b in self.updatedBranches:
1001                         sys.stdout.write("%s " % b)
1002                     sys.stdout.write("\n")
1005         self.gitStream.close()
1006         if importProcess.wait() != 0:
1007             die("fast-import failed: %s" % self.gitError.read())
1008         self.gitOutput.close()
1009         self.gitError.close()
1011         if createP4HeadRef:
1012             system("git symbolic-ref refs/remotes/p4/HEAD %s" % self.branch)
1014         return True
1016 class P4Rebase(Command):
1017     def __init__(self):
1018         Command.__init__(self)
1019         self.options = [ optparse.make_option("--with-origin", dest="syncWithOrigin", action="store_true") ]
1020         self.description = "Fetches the latest revision from perforce and rebases the current work (branch) against it"
1021         self.syncWithOrigin = False
1023     def run(self, args):
1024         sync = P4Sync()
1025         sync.syncWithOrigin = self.syncWithOrigin
1026         sync.run([])
1027         print "Rebasing the current branch"
1028         oldHead = mypopen("git rev-parse HEAD").read()[:-1]
1029         system("git rebase p4")
1030         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1031         return True
1033 class P4Clone(P4Sync):
1034     def __init__(self):
1035         P4Sync.__init__(self)
1036         self.description = "Creates a new git repository and imports from Perforce into it"
1037         self.usage = "usage: %prog [options] //depot/path[@revRange] [directory]"
1038         self.needsGit = False
1040     def run(self, args):
1041         global gitdir
1043         if len(args) < 1:
1044             return False
1045         depotPath = args[0]
1046         dir = ""
1047         if len(args) == 2:
1048             dir = args[1]
1049         elif len(args) > 2:
1050             return False
1052         if not depotPath.startswith("//"):
1053             return False
1055         if len(dir) == 0:
1056             dir = depotPath
1057             atPos = dir.rfind("@")
1058             if atPos != -1:
1059                 dir = dir[0:atPos]
1060             hashPos = dir.rfind("#")
1061             if hashPos != -1:
1062                 dir = dir[0:hashPos]
1064             if dir.endswith("..."):
1065                 dir = dir[:-3]
1067             if dir.endswith("/"):
1068                dir = dir[:-1]
1070             slashPos = dir.rfind("/")
1071             if slashPos != -1:
1072                 dir = dir[slashPos + 1:]
1074         print "Importing from %s into %s" % (depotPath, dir)
1075         os.makedirs(dir)
1076         os.chdir(dir)
1077         system("git init")
1078         gitdir = os.getcwd() + "/.git"
1079         if not P4Sync.run(self, [depotPath]):
1080             return False
1081         if self.branch != "master":
1082             if gitBranchExists("refs/remotes/p4/master"):
1083                 system("git branch master refs/remotes/p4/master")
1084                 system("git checkout -f")
1085             else:
1086                 print "Could not detect main branch. No checkout/master branch created."
1087         return True
1089 class HelpFormatter(optparse.IndentedHelpFormatter):
1090     def __init__(self):
1091         optparse.IndentedHelpFormatter.__init__(self)
1093     def format_description(self, description):
1094         if description:
1095             return description + "\n"
1096         else:
1097             return ""
1099 def printUsage(commands):
1100     print "usage: %s <command> [options]" % sys.argv[0]
1101     print ""
1102     print "valid commands: %s" % ", ".join(commands)
1103     print ""
1104     print "Try %s <command> --help for command specific help." % sys.argv[0]
1105     print ""
1107 commands = {
1108     "debug" : P4Debug(),
1109     "submit" : P4Submit(),
1110     "sync" : P4Sync(),
1111     "rebase" : P4Rebase(),
1112     "clone" : P4Clone()
1115 if len(sys.argv[1:]) == 0:
1116     printUsage(commands.keys())
1117     sys.exit(2)
1119 cmd = ""
1120 cmdName = sys.argv[1]
1121 try:
1122     cmd = commands[cmdName]
1123 except KeyError:
1124     print "unknown command %s" % cmdName
1125     print ""
1126     printUsage(commands.keys())
1127     sys.exit(2)
1129 options = cmd.options
1130 cmd.gitdir = gitdir
1132 args = sys.argv[2:]
1134 if len(options) > 0:
1135     options.append(optparse.make_option("--git-dir", dest="gitdir"))
1137     parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1138                                    options,
1139                                    description = cmd.description,
1140                                    formatter = HelpFormatter())
1142     (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1144 if cmd.needsGit:
1145     gitdir = cmd.gitdir
1146     if len(gitdir) == 0:
1147         gitdir = ".git"
1148         if not isValidGitDir(gitdir):
1149             gitdir = mypopen("git rev-parse --git-dir").read()[:-1]
1150             if os.path.exists(gitdir):
1151                 cdup = mypopen("git rev-parse --show-cdup").read()[:-1];
1152                 if len(cdup) > 0:
1153                     os.chdir(cdup);
1155     if not isValidGitDir(gitdir):
1156         if isValidGitDir(gitdir + "/.git"):
1157             gitdir += "/.git"
1158         else:
1159             die("fatal: cannot locate git repository at %s" % gitdir)
1161     os.environ["GIT_DIR"] = gitdir
1163 if not cmd.run(args):
1164     parser.print_help()