Code

1f549b5c626325f3ed79d856a673b75d5fda3d21
[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: Add an option to sync/rebase to fetch and rebase from origin first.
11 #
13 import optparse, sys, os, marshal, popen2, subprocess, shelve
14 import tempfile, getopt, sha, os.path, time, platform
15 from sets import Set;
17 gitdir = os.environ.get("GIT_DIR", "")
19 def mypopen(command):
20     return os.popen(command, "rb");
22 def p4CmdList(cmd):
23     cmd = "p4 -G %s" % cmd
24     pipe = os.popen(cmd, "rb")
26     result = []
27     try:
28         while True:
29             entry = marshal.load(pipe)
30             result.append(entry)
31     except EOFError:
32         pass
33     pipe.close()
35     return result
37 def p4Cmd(cmd):
38     list = p4CmdList(cmd)
39     result = {}
40     for entry in list:
41         result.update(entry)
42     return result;
44 def p4Where(depotPath):
45     if not depotPath.endswith("/"):
46         depotPath += "/"
47     output = p4Cmd("where %s..." % depotPath)
48     clientPath = ""
49     if "path" in output:
50         clientPath = output.get("path")
51     elif "data" in output:
52         data = output.get("data")
53         lastSpace = data.rfind(" ")
54         clientPath = data[lastSpace + 1:]
56     if clientPath.endswith("..."):
57         clientPath = clientPath[:-3]
58     return clientPath
60 def die(msg):
61     sys.stderr.write(msg + "\n")
62     sys.exit(1)
64 def currentGitBranch():
65     return mypopen("git name-rev HEAD").read().split(" ")[1][:-1]
67 def isValidGitDir(path):
68     if os.path.exists(path + "/HEAD") and os.path.exists(path + "/refs") and os.path.exists(path + "/objects"):
69         return True;
70     return False
72 def system(cmd):
73     if os.system(cmd) != 0:
74         die("command failed: %s" % cmd)
76 def extractLogMessageFromGitCommit(commit):
77     logMessage = ""
78     foundTitle = False
79     for log in mypopen("git cat-file commit %s" % commit).readlines():
80        if not foundTitle:
81            if len(log) == 1:
82                foundTitle = True
83            continue
85        logMessage += log
86     return logMessage
88 def extractDepotPathAndChangeFromGitLog(log):
89     values = {}
90     for line in log.split("\n"):
91         line = line.strip()
92         if line.startswith("[git-p4:") and line.endswith("]"):
93             line = line[8:-1].strip()
94             for assignment in line.split(":"):
95                 variable = assignment.strip()
96                 value = ""
97                 equalPos = assignment.find("=")
98                 if equalPos != -1:
99                     variable = assignment[:equalPos].strip()
100                     value = assignment[equalPos + 1:].strip()
101                     if value.startswith("\"") and value.endswith("\""):
102                         value = value[1:-1]
103                 values[variable] = value
105     return values.get("depot-path"), values.get("change")
107 def gitBranchExists(branch):
108     proc = subprocess.Popen(["git", "rev-parse", branch], stderr=subprocess.PIPE, stdout=subprocess.PIPE);
109     return proc.wait() == 0;
111 class Command:
112     def __init__(self):
113         self.usage = "usage: %prog [options]"
114         self.needsGit = True
116 class P4Debug(Command):
117     def __init__(self):
118         Command.__init__(self)
119         self.options = [
120         ]
121         self.description = "A tool to debug the output of p4 -G."
122         self.needsGit = False
124     def run(self, args):
125         for output in p4CmdList(" ".join(args)):
126             print output
127         return True
129 class P4CleanTags(Command):
130     def __init__(self):
131         Command.__init__(self)
132         self.options = [
133 #                optparse.make_option("--branch", dest="branch", default="refs/heads/master")
134         ]
135         self.description = "A tool to remove stale unused tags from incremental perforce imports."
136     def run(self, args):
137         branch = currentGitBranch()
138         print "Cleaning out stale p4 import tags..."
139         sout, sin, serr = popen2.popen3("git name-rev --tags `git rev-parse %s`" % branch)
140         output = sout.read()
141         try:
142             tagIdx = output.index(" tags/p4/")
143         except:
144             print "Cannot find any p4/* tag. Nothing to do."
145             sys.exit(0)
147         try:
148             caretIdx = output.index("^")
149         except:
150             caretIdx = len(output) - 1
151         rev = int(output[tagIdx + 9 : caretIdx])
153         allTags = mypopen("git tag -l p4/").readlines()
154         for i in range(len(allTags)):
155             allTags[i] = int(allTags[i][3:-1])
157         allTags.sort()
159         allTags.remove(rev)
161         for rev in allTags:
162             print mypopen("git tag -d p4/%s" % rev).read()
164         print "%s tags removed." % len(allTags)
165         return True
167 class P4Submit(Command):
168     def __init__(self):
169         Command.__init__(self)
170         self.options = [
171                 optparse.make_option("--continue", action="store_false", dest="firstTime"),
172                 optparse.make_option("--origin", dest="origin"),
173                 optparse.make_option("--reset", action="store_true", dest="reset"),
174                 optparse.make_option("--log-substitutions", dest="substFile"),
175                 optparse.make_option("--noninteractive", action="store_false"),
176                 optparse.make_option("--dry-run", action="store_true"),
177         ]
178         self.description = "Submit changes from git to the perforce depot."
179         self.usage += " [name of git branch to submit into perforce depot]"
180         self.firstTime = True
181         self.reset = False
182         self.interactive = True
183         self.dryRun = False
184         self.substFile = ""
185         self.firstTime = True
186         self.origin = ""
188         self.logSubstitutions = {}
189         self.logSubstitutions["<enter description here>"] = "%log%"
190         self.logSubstitutions["\tDetails:"] = "\tDetails:  %log%"
192     def check(self):
193         if len(p4CmdList("opened ...")) > 0:
194             die("You have files opened with perforce! Close them before starting the sync.")
196     def start(self):
197         if len(self.config) > 0 and not self.reset:
198             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)
200         commits = []
201         for line in mypopen("git rev-list --no-merges %s..%s" % (self.origin, self.master)).readlines():
202             commits.append(line[:-1])
203         commits.reverse()
205         self.config["commits"] = commits
207     def prepareLogMessage(self, template, message):
208         result = ""
210         for line in template.split("\n"):
211             if line.startswith("#"):
212                 result += line + "\n"
213                 continue
215             substituted = False
216             for key in self.logSubstitutions.keys():
217                 if line.find(key) != -1:
218                     value = self.logSubstitutions[key]
219                     value = value.replace("%log%", message)
220                     if value != "@remove@":
221                         result += line.replace(key, value) + "\n"
222                     substituted = True
223                     break
225             if not substituted:
226                 result += line + "\n"
228         return result
230     def apply(self, id):
231         print "Applying %s" % (mypopen("git log --max-count=1 --pretty=oneline %s" % id).read())
232         diff = mypopen("git diff-tree -r --name-status \"%s^\" \"%s\"" % (id, id)).readlines()
233         filesToAdd = set()
234         filesToDelete = set()
235         editedFiles = set()
236         for line in diff:
237             modifier = line[0]
238             path = line[1:].strip()
239             if modifier == "M":
240                 system("p4 edit \"%s\"" % path)
241                 editedFiles.add(path)
242             elif modifier == "A":
243                 filesToAdd.add(path)
244                 if path in filesToDelete:
245                     filesToDelete.remove(path)
246             elif modifier == "D":
247                 filesToDelete.add(path)
248                 if path in filesToAdd:
249                     filesToAdd.remove(path)
250             else:
251                 die("unknown modifier %s for %s" % (modifier, path))
253         diffcmd = "git diff-tree -p --diff-filter=ACMRTUXB \"%s^\" \"%s\"" % (id, id)
254         patchcmd = diffcmd + " | patch -p1"
256         if os.system(patchcmd + " --dry-run --silent") != 0:
257             print "Unfortunately applying the change failed!"
258             print "What do you want to do?"
259             response = "x"
260             while response != "s" and response != "a" and response != "w":
261                 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) ")
262             if response == "s":
263                 print "Skipping! Good luck with the next patches..."
264                 return
265             elif response == "a":
266                 os.system(patchcmd)
267                 if len(filesToAdd) > 0:
268                     print "You may also want to call p4 add on the following files:"
269                     print " ".join(filesToAdd)
270                 if len(filesToDelete):
271                     print "The following files should be scheduled for deletion with p4 delete:"
272                     print " ".join(filesToDelete)
273                 die("Please resolve and submit the conflict manually and continue afterwards with git-p4 submit --continue")
274             elif response == "w":
275                 system(diffcmd + " > patch.txt")
276                 print "Patch saved to patch.txt in %s !" % self.clientPath
277                 die("Please resolve and submit the conflict manually and continue afterwards with git-p4 submit --continue")
279         system(patchcmd)
281         for f in filesToAdd:
282             system("p4 add %s" % f)
283         for f in filesToDelete:
284             system("p4 revert %s" % f)
285             system("p4 delete %s" % f)
287         logMessage = extractLogMessageFromGitCommit(id)
288         logMessage = logMessage.replace("\n", "\n\t")
289         logMessage = logMessage[:-1]
291         template = mypopen("p4 change -o").read()
293         if self.interactive:
294             submitTemplate = self.prepareLogMessage(template, logMessage)
295             diff = mypopen("p4 diff -du ...").read()
297             for newFile in filesToAdd:
298                 diff += "==== new file ====\n"
299                 diff += "--- /dev/null\n"
300                 diff += "+++ %s\n" % newFile
301                 f = open(newFile, "r")
302                 for line in f.readlines():
303                     diff += "+" + line
304                 f.close()
306             separatorLine = "######## everything below this line is just the diff #######"
307             if platform.system() == "Windows":
308                 separatorLine += "\r"
309             separatorLine += "\n"
311             response = "e"
312             firstIteration = True
313             while response == "e":
314                 if not firstIteration:
315                     response = raw_input("Do you want to submit this change? [y]es/[e]dit/[n]o/[s]kip ")
316                 firstIteration = False
317                 if response == "e":
318                     [handle, fileName] = tempfile.mkstemp()
319                     tmpFile = os.fdopen(handle, "w+")
320                     tmpFile.write(submitTemplate + separatorLine + diff)
321                     tmpFile.close()
322                     defaultEditor = "vi"
323                     if platform.system() == "Windows":
324                         defaultEditor = "notepad"
325                     editor = os.environ.get("EDITOR", defaultEditor);
326                     system(editor + " " + fileName)
327                     tmpFile = open(fileName, "rb")
328                     message = tmpFile.read()
329                     tmpFile.close()
330                     os.remove(fileName)
331                     submitTemplate = message[:message.index(separatorLine)]
333             if response == "y" or response == "yes":
334                if self.dryRun:
335                    print submitTemplate
336                    raw_input("Press return to continue...")
337                else:
338                     pipe = os.popen("p4 submit -i", "wb")
339                     pipe.write(submitTemplate)
340                     pipe.close()
341             elif response == "s":
342                 for f in editedFiles:
343                     system("p4 revert \"%s\"" % f);
344                 for f in filesToAdd:
345                     system("p4 revert \"%s\"" % f);
346                     system("rm %s" %f)
347                 for f in filesToDelete:
348                     system("p4 delete \"%s\"" % f);
349                 return
350             else:
351                 print "Not submitting!"
352                 self.interactive = False
353         else:
354             fileName = "submit.txt"
355             file = open(fileName, "w+")
356             file.write(self.prepareLogMessage(template, logMessage))
357             file.close()
358             print "Perforce submit template written as %s. Please review/edit and then use p4 submit -i < %s to submit directly!" % (fileName, fileName)
360     def run(self, args):
361         global gitdir
362         # make gitdir absolute so we can cd out into the perforce checkout
363         gitdir = os.path.abspath(gitdir)
364         os.environ["GIT_DIR"] = gitdir
366         if len(args) == 0:
367             self.master = currentGitBranch()
368             if len(self.master) == 0 or not os.path.exists("%s/refs/heads/%s" % (gitdir, self.master)):
369                 die("Detecting current git branch failed!")
370         elif len(args) == 1:
371             self.master = args[0]
372         else:
373             return False
375         depotPath = ""
376         if gitBranchExists("p4"):
377             [depotPath, dummy] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("p4"))
378         if len(depotPath) == 0 and gitBranchExists("origin"):
379             [depotPath, dummy] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("origin"))
381         if len(depotPath) == 0:
382             print "Internal error: cannot locate perforce depot path from existing branches"
383             sys.exit(128)
385         self.clientPath = p4Where(depotPath)
387         if len(self.clientPath) == 0:
388             print "Error: Cannot locate perforce checkout of %s in client view" % depotPath
389             sys.exit(128)
391         print "Perforce checkout for depot path %s located at %s" % (depotPath, self.clientPath)
392         oldWorkingDirectory = os.getcwd()
393         os.chdir(self.clientPath)
394         response = raw_input("Do you want to sync %s with p4 sync? [y]es/[n]o " % self.clientPath)
395         if response == "y" or response == "yes":
396             system("p4 sync ...")
398         if len(self.origin) == 0:
399             if gitBranchExists("p4"):
400                 self.origin = "p4"
401             else:
402                 self.origin = "origin"
404         if self.reset:
405             self.firstTime = True
407         if len(self.substFile) > 0:
408             for line in open(self.substFile, "r").readlines():
409                 tokens = line[:-1].split("=")
410                 self.logSubstitutions[tokens[0]] = tokens[1]
412         self.check()
413         self.configFile = gitdir + "/p4-git-sync.cfg"
414         self.config = shelve.open(self.configFile, writeback=True)
416         if self.firstTime:
417             self.start()
419         commits = self.config.get("commits", [])
421         while len(commits) > 0:
422             self.firstTime = False
423             commit = commits[0]
424             commits = commits[1:]
425             self.config["commits"] = commits
426             self.apply(commit)
427             if not self.interactive:
428                 break
430         self.config.close()
432         if len(commits) == 0:
433             if self.firstTime:
434                 print "No changes found to apply between %s and current HEAD" % self.origin
435             else:
436                 print "All changes applied!"
437                 response = raw_input("Do you want to sync from Perforce now using git-p4 rebase? [y]es/[n]o ")
438                 if response == "y" or response == "yes":
439                     os.chdir(oldWorkingDirectory)
440                     rebase = P4Rebase()
441                     rebase.run([])
442             os.remove(self.configFile)
444         return True
446 class P4Sync(Command):
447     def __init__(self):
448         Command.__init__(self)
449         self.options = [
450                 optparse.make_option("--branch", dest="branch"),
451                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
452                 optparse.make_option("--changesfile", dest="changesFile"),
453                 optparse.make_option("--silent", dest="silent", action="store_true"),
454                 optparse.make_option("--known-branches", dest="knownBranches"),
455                 optparse.make_option("--data-cache", dest="dataCache", action="store_true"),
456                 optparse.make_option("--command-cache", dest="commandCache", action="store_true"),
457                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true")
458         ]
459         self.description = """Imports from Perforce into a git repository.\n
460     example:
461     //depot/my/project/ -- to import the current head
462     //depot/my/project/@all -- to import everything
463     //depot/my/project/@1,6 -- to import only from revision 1 to 6
465     (a ... is not needed in the path p4 specification, it's added implicitly)"""
467         self.usage += " //depot/path[@revRange]"
469         self.dataCache = False
470         self.commandCache = False
471         self.silent = False
472         self.knownBranches = Set()
473         self.createdBranches = Set()
474         self.committedChanges = Set()
475         self.branch = ""
476         self.detectBranches = False
477         self.detectLabels = False
478         self.changesFile = ""
480     def p4File(self, depotPath):
481         return os.popen("p4 print -q \"%s\"" % depotPath, "rb").read()
483     def extractFilesFromCommit(self, commit):
484         files = []
485         fnum = 0
486         while commit.has_key("depotFile%s" % fnum):
487             path =  commit["depotFile%s" % fnum]
488             if not path.startswith(self.depotPath):
489     #            if not self.silent:
490     #                print "\nchanged files: ignoring path %s outside of %s in change %s" % (path, self.depotPath, change)
491                 fnum = fnum + 1
492                 continue
494             file = {}
495             file["path"] = path
496             file["rev"] = commit["rev%s" % fnum]
497             file["action"] = commit["action%s" % fnum]
498             file["type"] = commit["type%s" % fnum]
499             files.append(file)
500             fnum = fnum + 1
501         return files
503     def isSubPathOf(self, first, second):
504         if not first.startswith(second):
505             return False
506         if first == second:
507             return True
508         return first[len(second)] == "/"
510     def branchesForCommit(self, files):
511         branches = Set()
513         for file in files:
514             relativePath = file["path"][len(self.depotPath):]
515             # strip off the filename
516             relativePath = relativePath[0:relativePath.rfind("/")]
518     #        if len(branches) == 0:
519     #            branches.add(relativePath)
520     #            knownBranches.add(relativePath)
521     #            continue
523             ###### this needs more testing :)
524             knownBranch = False
525             for branch in branches:
526                 if relativePath == branch:
527                     knownBranch = True
528                     break
529     #            if relativePath.startswith(branch):
530                 if self.isSubPathOf(relativePath, branch):
531                     knownBranch = True
532                     break
533     #            if branch.startswith(relativePath):
534                 if self.isSubPathOf(branch, relativePath):
535                     branches.remove(branch)
536                     break
538             if knownBranch:
539                 continue
541             for branch in self.knownBranches:
542                 #if relativePath.startswith(branch):
543                 if self.isSubPathOf(relativePath, branch):
544                     if len(branches) == 0:
545                         relativePath = branch
546                     else:
547                         knownBranch = True
548                     break
550             if knownBranch:
551                 continue
553             branches.add(relativePath)
554             self.knownBranches.add(relativePath)
556         return branches
558     def findBranchParent(self, branchPrefix, files):
559         for file in files:
560             path = file["path"]
561             if not path.startswith(branchPrefix):
562                 continue
563             action = file["action"]
564             if action != "integrate" and action != "branch":
565                 continue
566             rev = file["rev"]
567             depotPath = path + "#" + rev
569             log = p4CmdList("filelog \"%s\"" % depotPath)
570             if len(log) != 1:
571                 print "eek! I got confused by the filelog of %s" % depotPath
572                 sys.exit(1);
574             log = log[0]
575             if log["action0"] != action:
576                 print "eek! wrong action in filelog for %s : found %s, expected %s" % (depotPath, log["action0"], action)
577                 sys.exit(1);
579             branchAction = log["how0,0"]
580     #        if branchAction == "branch into" or branchAction == "ignored":
581     #            continue # ignore for branching
583             if not branchAction.endswith(" from"):
584                 continue # ignore for branching
585     #            print "eek! file %s was not branched from but instead: %s" % (depotPath, branchAction)
586     #            sys.exit(1);
588             source = log["file0,0"]
589             if source.startswith(branchPrefix):
590                 continue
592             lastSourceRev = log["erev0,0"]
594             sourceLog = p4CmdList("filelog -m 1 \"%s%s\"" % (source, lastSourceRev))
595             if len(sourceLog) != 1:
596                 print "eek! I got confused by the source filelog of %s%s" % (source, lastSourceRev)
597                 sys.exit(1);
598             sourceLog = sourceLog[0]
600             relPath = source[len(self.depotPath):]
601             # strip off the filename
602             relPath = relPath[0:relPath.rfind("/")]
604             for branch in self.knownBranches:
605                 if self.isSubPathOf(relPath, branch):
606     #                print "determined parent branch branch %s due to change in file %s" % (branch, source)
607                     return branch
608     #            else:
609     #                print "%s is not a subpath of branch %s" % (relPath, branch)
611         return ""
613     def commit(self, details, files, branch, branchPrefix, parent = "", merged = ""):
614         epoch = details["time"]
615         author = details["user"]
617         self.gitStream.write("commit %s\n" % branch)
618     #    gitStream.write("mark :%s\n" % details["change"])
619         self.committedChanges.add(int(details["change"]))
620         committer = ""
621         if author in self.users:
622             committer = "%s %s %s" % (self.users[author], epoch, self.tz)
623         else:
624             committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
626         self.gitStream.write("committer %s\n" % committer)
628         self.gitStream.write("data <<EOT\n")
629         self.gitStream.write(details["desc"])
630         self.gitStream.write("\n[git-p4: depot-path = \"%s\": change = %s]\n" % (branchPrefix, details["change"]))
631         self.gitStream.write("EOT\n\n")
633         if len(parent) > 0:
634             self.gitStream.write("from %s\n" % parent)
636         if len(merged) > 0:
637             self.gitStream.write("merge %s\n" % merged)
639         for file in files:
640             path = file["path"]
641             if not path.startswith(branchPrefix):
642     #            if not silent:
643     #                print "\nchanged files: ignoring path %s outside of branch prefix %s in change %s" % (path, branchPrefix, details["change"])
644                 continue
645             rev = file["rev"]
646             depotPath = path + "#" + rev
647             relPath = path[len(branchPrefix):]
648             action = file["action"]
650             if file["type"] == "apple":
651                 print "\nfile %s is a strange apple file that forks. Ignoring!" % path
652                 continue
654             if action == "delete":
655                 self.gitStream.write("D %s\n" % relPath)
656             else:
657                 mode = 644
658                 if file["type"].startswith("x"):
659                     mode = 755
661                 data = self.p4File(depotPath)
663                 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
664                 self.gitStream.write("data %s\n" % len(data))
665                 self.gitStream.write(data)
666                 self.gitStream.write("\n")
668         self.gitStream.write("\n")
670         change = int(details["change"])
672         self.lastChange = change
674         if change in self.labels:
675             label = self.labels[change]
676             labelDetails = label[0]
677             labelRevisions = label[1]
679             files = p4CmdList("files %s...@%s" % (branchPrefix, change))
681             if len(files) == len(labelRevisions):
683                 cleanedFiles = {}
684                 for info in files:
685                     if info["action"] == "delete":
686                         continue
687                     cleanedFiles[info["depotFile"]] = info["rev"]
689                 if cleanedFiles == labelRevisions:
690                     self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
691                     self.gitStream.write("from %s\n" % branch)
693                     owner = labelDetails["Owner"]
694                     tagger = ""
695                     if author in self.users:
696                         tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
697                     else:
698                         tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
699                     self.gitStream.write("tagger %s\n" % tagger)
700                     self.gitStream.write("data <<EOT\n")
701                     self.gitStream.write(labelDetails["Description"])
702                     self.gitStream.write("EOT\n\n")
704                 else:
705                     if not self.silent:
706                         print "Tag %s does not match with change %s: files do not match." % (labelDetails["label"], change)
708             else:
709                 if not self.silent:
710                     print "Tag %s does not match with change %s: file count is different." % (labelDetails["label"], change)
712     def extractFilesInCommitToBranch(self, files, branchPrefix):
713         newFiles = []
715         for file in files:
716             path = file["path"]
717             if path.startswith(branchPrefix):
718                 newFiles.append(file)
720         return newFiles
722     def findBranchSourceHeuristic(self, files, branch, branchPrefix):
723         for file in files:
724             action = file["action"]
725             if action != "integrate" and action != "branch":
726                 continue
727             path = file["path"]
728             rev = file["rev"]
729             depotPath = path + "#" + rev
731             log = p4CmdList("filelog \"%s\"" % depotPath)
732             if len(log) != 1:
733                 print "eek! I got confused by the filelog of %s" % depotPath
734                 sys.exit(1);
736             log = log[0]
737             if log["action0"] != action:
738                 print "eek! wrong action in filelog for %s : found %s, expected %s" % (depotPath, log["action0"], action)
739                 sys.exit(1);
741             branchAction = log["how0,0"]
743             if not branchAction.endswith(" from"):
744                 continue # ignore for branching
745     #            print "eek! file %s was not branched from but instead: %s" % (depotPath, branchAction)
746     #            sys.exit(1);
748             source = log["file0,0"]
749             if source.startswith(branchPrefix):
750                 continue
752             lastSourceRev = log["erev0,0"]
754             sourceLog = p4CmdList("filelog -m 1 \"%s%s\"" % (source, lastSourceRev))
755             if len(sourceLog) != 1:
756                 print "eek! I got confused by the source filelog of %s%s" % (source, lastSourceRev)
757                 sys.exit(1);
758             sourceLog = sourceLog[0]
760             relPath = source[len(self.depotPath):]
761             # strip off the filename
762             relPath = relPath[0:relPath.rfind("/")]
764             for candidate in self.knownBranches:
765                 if self.isSubPathOf(relPath, candidate) and candidate != branch:
766                     return candidate
768         return ""
770     def changeIsBranchMerge(self, sourceBranch, destinationBranch, change):
771         sourceFiles = {}
772         for file in p4CmdList("files %s...@%s" % (self.depotPath + sourceBranch + "/", change)):
773             if file["action"] == "delete":
774                 continue
775             sourceFiles[file["depotFile"]] = file
777         destinationFiles = {}
778         for file in p4CmdList("files %s...@%s" % (self.depotPath + destinationBranch + "/", change)):
779             destinationFiles[file["depotFile"]] = file
781         for fileName in sourceFiles.keys():
782             integrations = []
783             deleted = False
784             integrationCount = 0
785             for integration in p4CmdList("integrated \"%s\"" % fileName):
786                 toFile = integration["fromFile"] # yes, it's true, it's fromFile
787                 if not toFile in destinationFiles:
788                     continue
789                 destFile = destinationFiles[toFile]
790                 if destFile["action"] == "delete":
791     #                print "file %s has been deleted in %s" % (fileName, toFile)
792                     deleted = True
793                     break
794                 integrationCount += 1
795                 if integration["how"] == "branch from":
796                     continue
798                 if int(integration["change"]) == change:
799                     integrations.append(integration)
800                     continue
801                 if int(integration["change"]) > change:
802                     continue
804                 destRev = int(destFile["rev"])
806                 startRev = integration["startFromRev"][1:]
807                 if startRev == "none":
808                     startRev = 0
809                 else:
810                     startRev = int(startRev)
812                 endRev = integration["endFromRev"][1:]
813                 if endRev == "none":
814                     endRev = 0
815                 else:
816                     endRev = int(endRev)
818                 initialBranch = (destRev == 1 and integration["how"] != "branch into")
819                 inRange = (destRev >= startRev and destRev <= endRev)
820                 newer = (destRev > startRev and destRev > endRev)
822                 if initialBranch or inRange or newer:
823                     integrations.append(integration)
825             if deleted:
826                 continue
828             if len(integrations) == 0 and integrationCount > 1:
829                 print "file %s was not integrated from %s into %s" % (fileName, sourceBranch, destinationBranch)
830                 return False
832         return True
834     def getUserMap(self):
835         self.users = {}
837         for output in p4CmdList("users"):
838             if not output.has_key("User"):
839                 continue
840             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
842     def getLabels(self):
843         self.labels = {}
845         l = p4CmdList("labels %s..." % self.depotPath)
846         if len(l) > 0 and not self.silent:
847             print "Finding files belonging to labels in %s" % self.depotPath
849         for output in l:
850             label = output["label"]
851             revisions = {}
852             newestChange = 0
853             for file in p4CmdList("files //...@%s" % label):
854                 revisions[file["depotFile"]] = file["rev"]
855                 change = int(file["change"])
856                 if change > newestChange:
857                     newestChange = change
859             self.labels[newestChange] = [output, revisions]
861     def run(self, args):
862         self.depotPath = ""
863         self.changeRange = ""
864         self.initialParent = ""
865         self.previousDepotPath = ""
867         if len(self.branch) == 0:
868             self.branch = "refs/remotes/p4"
870         if len(args) == 0:
871             if not gitBranchExists(self.branch) and gitBranchExists("origin"):
872                 if not self.silent:
873                     print "Creating %s branch in git repository based on origin" % self.branch
874                 system("git branch %s origin" % self.branch)
876             [self.previousDepotPath, p4Change] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit(self.branch))
877             if len(self.previousDepotPath) > 0 and len(p4Change) > 0:
878                 p4Change = int(p4Change) + 1
879                 self.depotPath = self.previousDepotPath
880                 self.changeRange = "@%s,#head" % p4Change
881                 self.initialParent = mypopen("git rev-parse %s" % self.branch).read()[:-1]
882                 if not self.silent:
883                     print "Performing incremental import into %s git branch" % self.branch
885         if not self.branch.startswith("refs/"):
886             self.branch = "refs/heads/" + self.branch
888         if len(self.depotPath) != 0:
889             self.depotPath = self.depotPath[:-1]
891         if len(args) == 0 and len(self.depotPath) != 0:
892             if not self.silent:
893                 print "Depot path: %s" % self.depotPath
894         elif len(args) != 1:
895             return False
896         else:
897             if len(self.depotPath) != 0 and self.depotPath != args[0]:
898                 print "previous import used depot path %s and now %s was specified. this doesn't work!" % (self.depotPath, args[0])
899                 sys.exit(1)
900             self.depotPath = args[0]
902         self.revision = ""
903         self.users = {}
904         self.lastChange = 0
906         if self.depotPath.find("@") != -1:
907             atIdx = self.depotPath.index("@")
908             self.changeRange = self.depotPath[atIdx:]
909             if self.changeRange == "@all":
910                 self.changeRange = ""
911             elif self.changeRange.find(",") == -1:
912                 self.revision = self.changeRange
913                 self.changeRange = ""
914             self.depotPath = self.depotPath[0:atIdx]
915         elif self.depotPath.find("#") != -1:
916             hashIdx = self.depotPath.index("#")
917             self.revision = self.depotPath[hashIdx:]
918             self.depotPath = self.depotPath[0:hashIdx]
919         elif len(self.previousDepotPath) == 0:
920             self.revision = "#head"
922         if self.depotPath.endswith("..."):
923             self.depotPath = self.depotPath[:-3]
925         if not self.depotPath.endswith("/"):
926             self.depotPath += "/"
928         self.getUserMap()
929         self.labels = {}
930         if self.detectLabels:
931             self.getLabels();
933         if len(self.changeRange) == 0:
934             try:
935                 sout, sin, serr = popen2.popen3("git name-rev --tags `git rev-parse %s`" % self.branch)
936                 output = sout.read()
937                 if output.endswith("\n"):
938                     output = output[:-1]
939                 tagIdx = output.index(" tags/p4/")
940                 caretIdx = output.find("^")
941                 endPos = len(output)
942                 if caretIdx != -1:
943                     endPos = caretIdx
944                 self.rev = int(output[tagIdx + 9 : endPos]) + 1
945                 self.changeRange = "@%s,#head" % self.rev
946                 self.initialParent = mypopen("git rev-parse %s" % self.branch).read()[:-1]
947             except:
948                 pass
950         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
952         importProcess = subprocess.Popen(["git", "fast-import"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE);
953         self.gitOutput = importProcess.stdout
954         self.gitStream = importProcess.stdin
955         self.gitError = importProcess.stderr
957         if len(self.revision) > 0:
958             print "Doing initial import of %s from revision %s" % (self.depotPath, self.revision)
960             details = { "user" : "git perforce import user", "time" : int(time.time()) }
961             details["desc"] = "Initial import of %s from the state at revision %s" % (self.depotPath, self.revision)
962             details["change"] = self.revision
963             newestRevision = 0
965             fileCnt = 0
966             for info in p4CmdList("files %s...%s" % (self.depotPath, self.revision)):
967                 change = int(info["change"])
968                 if change > newestRevision:
969                     newestRevision = change
971                 if info["action"] == "delete":
972                     # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
973                     #fileCnt = fileCnt + 1
974                     continue
976                 for prop in [ "depotFile", "rev", "action", "type" ]:
977                     details["%s%s" % (prop, fileCnt)] = info[prop]
979                 fileCnt = fileCnt + 1
981             details["change"] = newestRevision
983             try:
984                 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPath)
985             except IOError:
986                 print "IO error with git fast-import. Is your git version recent enough?"
987                 print self.gitError.read()
989         else:
990             changes = []
992             if len(self.changesFile) > 0:
993                 output = open(self.changesFile).readlines()
994                 changeSet = Set()
995                 for line in output:
996                     changeSet.add(int(line))
998                 for change in changeSet:
999                     changes.append(change)
1001                 changes.sort()
1002             else:
1003                 output = mypopen("p4 changes %s...%s" % (self.depotPath, self.changeRange)).readlines()
1005                 for line in output:
1006                     changeNum = line.split(" ")[1]
1007                     changes.append(changeNum)
1009                 changes.reverse()
1011             if len(changes) == 0:
1012                 if not self.silent:
1013                     print "no changes to import!"
1014                 return True
1016             cnt = 1
1017             for change in changes:
1018                 description = p4Cmd("describe %s" % change)
1020                 if not self.silent:
1021                     sys.stdout.write("\rimporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1022                     sys.stdout.flush()
1023                 cnt = cnt + 1
1025                 try:
1026                     files = self.extractFilesFromCommit(description)
1027                     if self.detectBranches:
1028                         for branch in self.branchesForCommit(files):
1029                             self.knownBranches.add(branch)
1030                             branchPrefix = self.depotPath + branch + "/"
1032                             filesForCommit = self.extractFilesInCommitToBranch(files, branchPrefix)
1034                             merged = ""
1035                             parent = ""
1036                             ########### remove cnt!!!
1037                             if branch not in self.createdBranches and cnt > 2:
1038                                 self.createdBranches.add(branch)
1039                                 parent = self.findBranchParent(branchPrefix, files)
1040                                 if parent == branch:
1041                                     parent = ""
1042             #                    elif len(parent) > 0:
1043             #                        print "%s branched off of %s" % (branch, parent)
1045                             if len(parent) == 0:
1046                                 merged = self.findBranchSourceHeuristic(filesForCommit, branch, branchPrefix)
1047                                 if len(merged) > 0:
1048                                     print "change %s could be a merge from %s into %s" % (description["change"], merged, branch)
1049                                     if not self.changeIsBranchMerge(merged, branch, int(description["change"])):
1050                                         merged = ""
1052                             branch = "refs/heads/" + branch
1053                             if len(parent) > 0:
1054                                 parent = "refs/heads/" + parent
1055                             if len(merged) > 0:
1056                                 merged = "refs/heads/" + merged
1057                             self.commit(description, files, branch, branchPrefix, parent, merged)
1058                     else:
1059                         self.commit(description, files, self.branch, self.depotPath, self.initialParent)
1060                         self.initialParent = ""
1061                 except IOError:
1062                     print self.gitError.read()
1063                     sys.exit(1)
1065         if not self.silent:
1066             print ""
1069         self.gitStream.close()
1070         self.gitOutput.close()
1071         self.gitError.close()
1072         importProcess.wait()
1074         return True
1076 class P4Rebase(Command):
1077     def __init__(self):
1078         Command.__init__(self)
1079         self.options = [ ]
1080         self.description = "Fetches the latest revision from perforce and rebases the current work (branch) against it"
1082     def run(self, args):
1083         sync = P4Sync()
1084         sync.run([])
1085         print "Rebasing the current branch"
1086         oldHead = mypopen("git rev-parse HEAD").read()[:-1]
1087         system("git rebase p4")
1088         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1089         return True
1091 class P4Clone(P4Sync):
1092     def __init__(self):
1093         P4Sync.__init__(self)
1094         self.description = "Creates a new git repository and imports from Perforce into it"
1095         self.usage = "usage: %prog [options] //depot/path[@revRange] [directory]"
1096         self.needsGit = False
1098     def run(self, args):
1099         if len(args) < 1:
1100             return False
1101         depotPath = args[0]
1102         dir = ""
1103         if len(args) == 2:
1104             dir = args[1]
1105         elif len(args) > 2:
1106             return False
1108         if not depotPath.startswith("//"):
1109             return False
1111         if len(dir) == 0:
1112             dir = depotPath
1113             atPos = dir.rfind("@")
1114             if atPos != -1:
1115                 dir = dir[0:atPos]
1116             hashPos = dir.rfind("#")
1117             if hashPos != -1:
1118                 dir = dir[0:hashPos]
1120             if dir.endswith("..."):
1121                 dir = dir[:-3]
1123             if dir.endswith("/"):
1124                dir = dir[:-1]
1126             slashPos = dir.rfind("/")
1127             if slashPos != -1:
1128                 dir = dir[slashPos + 1:]
1130         print "Importing from %s into %s" % (depotPath, dir)
1131         os.makedirs(dir)
1132         os.chdir(dir)
1133         system("git init")
1134         if not P4Sync.run(self, [depotPath]):
1135             return False
1136         if self.branch != "master":
1137             system("git branch master p4")
1138             system("git checkout -f")
1139         return True
1141 class HelpFormatter(optparse.IndentedHelpFormatter):
1142     def __init__(self):
1143         optparse.IndentedHelpFormatter.__init__(self)
1145     def format_description(self, description):
1146         if description:
1147             return description + "\n"
1148         else:
1149             return ""
1151 def printUsage(commands):
1152     print "usage: %s <command> [options]" % sys.argv[0]
1153     print ""
1154     print "valid commands: %s" % ", ".join(commands)
1155     print ""
1156     print "Try %s <command> --help for command specific help." % sys.argv[0]
1157     print ""
1159 commands = {
1160     "debug" : P4Debug(),
1161     "clean-tags" : P4CleanTags(),
1162     "submit" : P4Submit(),
1163     "sync" : P4Sync(),
1164     "rebase" : P4Rebase(),
1165     "clone" : P4Clone()
1168 if len(sys.argv[1:]) == 0:
1169     printUsage(commands.keys())
1170     sys.exit(2)
1172 cmd = ""
1173 cmdName = sys.argv[1]
1174 try:
1175     cmd = commands[cmdName]
1176 except KeyError:
1177     print "unknown command %s" % cmdName
1178     print ""
1179     printUsage(commands.keys())
1180     sys.exit(2)
1182 options = cmd.options
1183 cmd.gitdir = gitdir
1185 args = sys.argv[2:]
1187 if len(options) > 0:
1188     options.append(optparse.make_option("--git-dir", dest="gitdir"))
1190     parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1191                                    options,
1192                                    description = cmd.description,
1193                                    formatter = HelpFormatter())
1195     (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1197 if cmd.needsGit:
1198     gitdir = cmd.gitdir
1199     if len(gitdir) == 0:
1200         gitdir = ".git"
1201         if not isValidGitDir(gitdir):
1202             gitdir = mypopen("git rev-parse --git-dir").read()[:-1]
1203             if os.path.exists(gitdir):
1204                 cdup = mypopen("git rev-parse --show-cdup").read()[:-1];
1205                 if len(cdup) > 0:
1206                     os.chdir(cdup);
1208     if not isValidGitDir(gitdir):
1209         if isValidGitDir(gitdir + "/.git"):
1210             gitdir += "/.git"
1211         else:
1212             die("fatal: cannot locate git repository at %s" % gitdir)
1214     os.environ["GIT_DIR"] = gitdir
1216 if not cmd.run(args):
1217     parser.print_help()