Code

Merge branch 'maint'
[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 <simon@lst.de>
6 # Copyright: 2007 Simon Hausmann <simon@lst.de>
7 #            2007 Trolltech ASA
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
9 #
11 import optparse, sys, os, marshal, popen2, subprocess, shelve
12 import tempfile, getopt, sha, os.path, time, platform
13 import re
15 from sets import Set;
17 verbose = False
20 def p4_build_cmd(cmd):
21     """Build a suitable p4 command line.
23     This consolidates building and returning a p4 command line into one
24     location. It means that hooking into the environment, or other configuration
25     can be done more easily.
26     """
27     real_cmd = "%s " % "p4"
29     user = gitConfig("git-p4.user")
30     if len(user) > 0:
31         real_cmd += "-u %s " % user
33     password = gitConfig("git-p4.password")
34     if len(password) > 0:
35         real_cmd += "-P %s " % password
37     port = gitConfig("git-p4.port")
38     if len(port) > 0:
39         real_cmd += "-p %s " % port
41     host = gitConfig("git-p4.host")
42     if len(host) > 0:
43         real_cmd += "-h %s " % host
45     client = gitConfig("git-p4.client")
46     if len(client) > 0:
47         real_cmd += "-c %s " % client
49     real_cmd += "%s" % (cmd)
50     if verbose:
51         print real_cmd
52     return real_cmd
54 def chdir(dir):
55     if os.name == 'nt':
56         os.environ['PWD']=dir
57     os.chdir(dir)
59 def die(msg):
60     if verbose:
61         raise Exception(msg)
62     else:
63         sys.stderr.write(msg + "\n")
64         sys.exit(1)
66 def write_pipe(c, str):
67     if verbose:
68         sys.stderr.write('Writing pipe: %s\n' % c)
70     pipe = os.popen(c, 'w')
71     val = pipe.write(str)
72     if pipe.close():
73         die('Command failed: %s' % c)
75     return val
77 def p4_write_pipe(c, str):
78     real_cmd = p4_build_cmd(c)
79     return write_pipe(real_cmd, str)
81 def read_pipe(c, ignore_error=False):
82     if verbose:
83         sys.stderr.write('Reading pipe: %s\n' % c)
85     pipe = os.popen(c, 'rb')
86     val = pipe.read()
87     if pipe.close() and not ignore_error:
88         die('Command failed: %s' % c)
90     return val
92 def p4_read_pipe(c, ignore_error=False):
93     real_cmd = p4_build_cmd(c)
94     return read_pipe(real_cmd, ignore_error)
96 def read_pipe_lines(c):
97     if verbose:
98         sys.stderr.write('Reading pipe: %s\n' % c)
99     ## todo: check return status
100     pipe = os.popen(c, 'rb')
101     val = pipe.readlines()
102     if pipe.close():
103         die('Command failed: %s' % c)
105     return val
107 def p4_read_pipe_lines(c):
108     """Specifically invoke p4 on the command supplied. """
109     real_cmd = p4_build_cmd(c)
110     return read_pipe_lines(real_cmd)
112 def system(cmd):
113     if verbose:
114         sys.stderr.write("executing %s\n" % cmd)
115     if os.system(cmd) != 0:
116         die("command failed: %s" % cmd)
118 def p4_system(cmd):
119     """Specifically invoke p4 as the system command. """
120     real_cmd = p4_build_cmd(cmd)
121     return system(real_cmd)
123 def isP4Exec(kind):
124     """Determine if a Perforce 'kind' should have execute permission
126     'p4 help filetypes' gives a list of the types.  If it starts with 'x',
127     or x follows one of a few letters.  Otherwise, if there is an 'x' after
128     a plus sign, it is also executable"""
129     return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
131 def setP4ExecBit(file, mode):
132     # Reopens an already open file and changes the execute bit to match
133     # the execute bit setting in the passed in mode.
135     p4Type = "+x"
137     if not isModeExec(mode):
138         p4Type = getP4OpenedType(file)
139         p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
140         p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
141         if p4Type[-1] == "+":
142             p4Type = p4Type[0:-1]
144     p4_system("reopen -t %s %s" % (p4Type, file))
146 def getP4OpenedType(file):
147     # Returns the perforce file type for the given file.
149     result = p4_read_pipe("opened %s" % file)
150     match = re.match(".*\((.+)\)\r?$", result)
151     if match:
152         return match.group(1)
153     else:
154         die("Could not determine file type for %s (result: '%s')" % (file, result))
156 def diffTreePattern():
157     # This is a simple generator for the diff tree regex pattern. This could be
158     # a class variable if this and parseDiffTreeEntry were a part of a class.
159     pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
160     while True:
161         yield pattern
163 def parseDiffTreeEntry(entry):
164     """Parses a single diff tree entry into its component elements.
166     See git-diff-tree(1) manpage for details about the format of the diff
167     output. This method returns a dictionary with the following elements:
169     src_mode - The mode of the source file
170     dst_mode - The mode of the destination file
171     src_sha1 - The sha1 for the source file
172     dst_sha1 - The sha1 fr the destination file
173     status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
174     status_score - The score for the status (applicable for 'C' and 'R'
175                    statuses). This is None if there is no score.
176     src - The path for the source file.
177     dst - The path for the destination file. This is only present for
178           copy or renames. If it is not present, this is None.
180     If the pattern is not matched, None is returned."""
182     match = diffTreePattern().next().match(entry)
183     if match:
184         return {
185             'src_mode': match.group(1),
186             'dst_mode': match.group(2),
187             'src_sha1': match.group(3),
188             'dst_sha1': match.group(4),
189             'status': match.group(5),
190             'status_score': match.group(6),
191             'src': match.group(7),
192             'dst': match.group(10)
193         }
194     return None
196 def isModeExec(mode):
197     # Returns True if the given git mode represents an executable file,
198     # otherwise False.
199     return mode[-3:] == "755"
201 def isModeExecChanged(src_mode, dst_mode):
202     return isModeExec(src_mode) != isModeExec(dst_mode)
204 def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
205     cmd = p4_build_cmd("-G %s" % (cmd))
206     if verbose:
207         sys.stderr.write("Opening pipe: %s\n" % cmd)
209     # Use a temporary file to avoid deadlocks without
210     # subprocess.communicate(), which would put another copy
211     # of stdout into memory.
212     stdin_file = None
213     if stdin is not None:
214         stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
215         stdin_file.write(stdin)
216         stdin_file.flush()
217         stdin_file.seek(0)
219     p4 = subprocess.Popen(cmd, shell=True,
220                           stdin=stdin_file,
221                           stdout=subprocess.PIPE)
223     result = []
224     try:
225         while True:
226             entry = marshal.load(p4.stdout)
227             result.append(entry)
228     except EOFError:
229         pass
230     exitCode = p4.wait()
231     if exitCode != 0:
232         entry = {}
233         entry["p4ExitCode"] = exitCode
234         result.append(entry)
236     return result
238 def p4Cmd(cmd):
239     list = p4CmdList(cmd)
240     result = {}
241     for entry in list:
242         result.update(entry)
243     return result;
245 def p4Where(depotPath):
246     if not depotPath.endswith("/"):
247         depotPath += "/"
248     output = p4Cmd("where %s..." % depotPath)
249     if output["code"] == "error":
250         return ""
251     clientPath = ""
252     if "path" in output:
253         clientPath = output.get("path")
254     elif "data" in output:
255         data = output.get("data")
256         lastSpace = data.rfind(" ")
257         clientPath = data[lastSpace + 1:]
259     if clientPath.endswith("..."):
260         clientPath = clientPath[:-3]
261     return clientPath
263 def currentGitBranch():
264     return read_pipe("git name-rev HEAD").split(" ")[1].strip()
266 def isValidGitDir(path):
267     if (os.path.exists(path + "/HEAD")
268         and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
269         return True;
270     return False
272 def parseRevision(ref):
273     return read_pipe("git rev-parse %s" % ref).strip()
275 def extractLogMessageFromGitCommit(commit):
276     logMessage = ""
278     ## fixme: title is first line of commit, not 1st paragraph.
279     foundTitle = False
280     for log in read_pipe_lines("git cat-file commit %s" % commit):
281        if not foundTitle:
282            if len(log) == 1:
283                foundTitle = True
284            continue
286        logMessage += log
287     return logMessage
289 def extractSettingsGitLog(log):
290     values = {}
291     for line in log.split("\n"):
292         line = line.strip()
293         m = re.search (r"^ *\[git-p4: (.*)\]$", line)
294         if not m:
295             continue
297         assignments = m.group(1).split (':')
298         for a in assignments:
299             vals = a.split ('=')
300             key = vals[0].strip()
301             val = ('='.join (vals[1:])).strip()
302             if val.endswith ('\"') and val.startswith('"'):
303                 val = val[1:-1]
305             values[key] = val
307     paths = values.get("depot-paths")
308     if not paths:
309         paths = values.get("depot-path")
310     if paths:
311         values['depot-paths'] = paths.split(',')
312     return values
314 def gitBranchExists(branch):
315     proc = subprocess.Popen(["git", "rev-parse", branch],
316                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);
317     return proc.wait() == 0;
319 def gitConfig(key):
320     return read_pipe("git config %s" % key, ignore_error=True).strip()
322 def p4BranchesInGit(branchesAreInRemotes = True):
323     branches = {}
325     cmdline = "git rev-parse --symbolic "
326     if branchesAreInRemotes:
327         cmdline += " --remotes"
328     else:
329         cmdline += " --branches"
331     for line in read_pipe_lines(cmdline):
332         line = line.strip()
334         ## only import to p4/
335         if not line.startswith('p4/') or line == "p4/HEAD":
336             continue
337         branch = line
339         # strip off p4
340         branch = re.sub ("^p4/", "", line)
342         branches[branch] = parseRevision(line)
343     return branches
345 def findUpstreamBranchPoint(head = "HEAD"):
346     branches = p4BranchesInGit()
347     # map from depot-path to branch name
348     branchByDepotPath = {}
349     for branch in branches.keys():
350         tip = branches[branch]
351         log = extractLogMessageFromGitCommit(tip)
352         settings = extractSettingsGitLog(log)
353         if settings.has_key("depot-paths"):
354             paths = ",".join(settings["depot-paths"])
355             branchByDepotPath[paths] = "remotes/p4/" + branch
357     settings = None
358     parent = 0
359     while parent < 65535:
360         commit = head + "~%s" % parent
361         log = extractLogMessageFromGitCommit(commit)
362         settings = extractSettingsGitLog(log)
363         if settings.has_key("depot-paths"):
364             paths = ",".join(settings["depot-paths"])
365             if branchByDepotPath.has_key(paths):
366                 return [branchByDepotPath[paths], settings]
368         parent = parent + 1
370     return ["", settings]
372 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
373     if not silent:
374         print ("Creating/updating branch(es) in %s based on origin branch(es)"
375                % localRefPrefix)
377     originPrefix = "origin/p4/"
379     for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
380         line = line.strip()
381         if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
382             continue
384         headName = line[len(originPrefix):]
385         remoteHead = localRefPrefix + headName
386         originHead = line
388         original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
389         if (not original.has_key('depot-paths')
390             or not original.has_key('change')):
391             continue
393         update = False
394         if not gitBranchExists(remoteHead):
395             if verbose:
396                 print "creating %s" % remoteHead
397             update = True
398         else:
399             settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
400             if settings.has_key('change') > 0:
401                 if settings['depot-paths'] == original['depot-paths']:
402                     originP4Change = int(original['change'])
403                     p4Change = int(settings['change'])
404                     if originP4Change > p4Change:
405                         print ("%s (%s) is newer than %s (%s). "
406                                "Updating p4 branch from origin."
407                                % (originHead, originP4Change,
408                                   remoteHead, p4Change))
409                         update = True
410                 else:
411                     print ("Ignoring: %s was imported from %s while "
412                            "%s was imported from %s"
413                            % (originHead, ','.join(original['depot-paths']),
414                               remoteHead, ','.join(settings['depot-paths'])))
416         if update:
417             system("git update-ref %s %s" % (remoteHead, originHead))
419 def originP4BranchesExist():
420         return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
422 def p4ChangesForPaths(depotPaths, changeRange):
423     assert depotPaths
424     output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
425                                                         for p in depotPaths]))
427     changes = []
428     for line in output:
429         changeNum = line.split(" ")[1]
430         changes.append(int(changeNum))
432     changes.sort()
433     return changes
435 class Command:
436     def __init__(self):
437         self.usage = "usage: %prog [options]"
438         self.needsGit = True
440 class P4Debug(Command):
441     def __init__(self):
442         Command.__init__(self)
443         self.options = [
444             optparse.make_option("--verbose", dest="verbose", action="store_true",
445                                  default=False),
446             ]
447         self.description = "A tool to debug the output of p4 -G."
448         self.needsGit = False
449         self.verbose = False
451     def run(self, args):
452         j = 0
453         for output in p4CmdList(" ".join(args)):
454             print 'Element: %d' % j
455             j += 1
456             print output
457         return True
459 class P4RollBack(Command):
460     def __init__(self):
461         Command.__init__(self)
462         self.options = [
463             optparse.make_option("--verbose", dest="verbose", action="store_true"),
464             optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
465         ]
466         self.description = "A tool to debug the multi-branch import. Don't use :)"
467         self.verbose = False
468         self.rollbackLocalBranches = False
470     def run(self, args):
471         if len(args) != 1:
472             return False
473         maxChange = int(args[0])
475         if "p4ExitCode" in p4Cmd("changes -m 1"):
476             die("Problems executing p4");
478         if self.rollbackLocalBranches:
479             refPrefix = "refs/heads/"
480             lines = read_pipe_lines("git rev-parse --symbolic --branches")
481         else:
482             refPrefix = "refs/remotes/"
483             lines = read_pipe_lines("git rev-parse --symbolic --remotes")
485         for line in lines:
486             if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
487                 line = line.strip()
488                 ref = refPrefix + line
489                 log = extractLogMessageFromGitCommit(ref)
490                 settings = extractSettingsGitLog(log)
492                 depotPaths = settings['depot-paths']
493                 change = settings['change']
495                 changed = False
497                 if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
498                                                            for p in depotPaths]))) == 0:
499                     print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
500                     system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
501                     continue
503                 while change and int(change) > maxChange:
504                     changed = True
505                     if self.verbose:
506                         print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
507                     system("git update-ref %s \"%s^\"" % (ref, ref))
508                     log = extractLogMessageFromGitCommit(ref)
509                     settings =  extractSettingsGitLog(log)
512                     depotPaths = settings['depot-paths']
513                     change = settings['change']
515                 if changed:
516                     print "%s rewound to %s" % (ref, change)
518         return True
520 class P4Submit(Command):
521     def __init__(self):
522         Command.__init__(self)
523         self.options = [
524                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
525                 optparse.make_option("--origin", dest="origin"),
526                 optparse.make_option("-M", dest="detectRename", action="store_true"),
527         ]
528         self.description = "Submit changes from git to the perforce depot."
529         self.usage += " [name of git branch to submit into perforce depot]"
530         self.interactive = True
531         self.origin = ""
532         self.detectRename = False
533         self.verbose = False
534         self.isWindows = (platform.system() == "Windows")
536     def check(self):
537         if len(p4CmdList("opened ...")) > 0:
538             die("You have files opened with perforce! Close them before starting the sync.")
540     # replaces everything between 'Description:' and the next P4 submit template field with the
541     # commit message
542     def prepareLogMessage(self, template, message):
543         result = ""
545         inDescriptionSection = False
547         for line in template.split("\n"):
548             if line.startswith("#"):
549                 result += line + "\n"
550                 continue
552             if inDescriptionSection:
553                 if line.startswith("Files:"):
554                     inDescriptionSection = False
555                 else:
556                     continue
557             else:
558                 if line.startswith("Description:"):
559                     inDescriptionSection = True
560                     line += "\n"
561                     for messageLine in message.split("\n"):
562                         line += "\t" + messageLine + "\n"
564             result += line + "\n"
566         return result
568     def prepareSubmitTemplate(self):
569         # remove lines in the Files section that show changes to files outside the depot path we're committing into
570         template = ""
571         inFilesSection = False
572         for line in p4_read_pipe_lines("change -o"):
573             if line.endswith("\r\n"):
574                 line = line[:-2] + "\n"
575             if inFilesSection:
576                 if line.startswith("\t"):
577                     # path starts and ends with a tab
578                     path = line[1:]
579                     lastTab = path.rfind("\t")
580                     if lastTab != -1:
581                         path = path[:lastTab]
582                         if not path.startswith(self.depotPath):
583                             continue
584                 else:
585                     inFilesSection = False
586             else:
587                 if line.startswith("Files:"):
588                     inFilesSection = True
590             template += line
592         return template
594     def applyCommit(self, id):
595         print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
596         diffOpts = ("", "-M")[self.detectRename]
597         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
598         filesToAdd = set()
599         filesToDelete = set()
600         editedFiles = set()
601         filesToChangeExecBit = {}
602         for line in diff:
603             diff = parseDiffTreeEntry(line)
604             modifier = diff['status']
605             path = diff['src']
606             if modifier == "M":
607                 p4_system("edit \"%s\"" % path)
608                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
609                     filesToChangeExecBit[path] = diff['dst_mode']
610                 editedFiles.add(path)
611             elif modifier == "A":
612                 filesToAdd.add(path)
613                 filesToChangeExecBit[path] = diff['dst_mode']
614                 if path in filesToDelete:
615                     filesToDelete.remove(path)
616             elif modifier == "D":
617                 filesToDelete.add(path)
618                 if path in filesToAdd:
619                     filesToAdd.remove(path)
620             elif modifier == "R":
621                 src, dest = diff['src'], diff['dst']
622                 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
623                 p4_system("edit \"%s\"" % (dest))
624                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
625                     filesToChangeExecBit[dest] = diff['dst_mode']
626                 os.unlink(dest)
627                 editedFiles.add(dest)
628                 filesToDelete.add(src)
629             else:
630                 die("unknown modifier %s for %s" % (modifier, path))
632         diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
633         patchcmd = diffcmd + " | git apply "
634         tryPatchCmd = patchcmd + "--check -"
635         applyPatchCmd = patchcmd + "--check --apply -"
637         if os.system(tryPatchCmd) != 0:
638             print "Unfortunately applying the change failed!"
639             print "What do you want to do?"
640             response = "x"
641             while response != "s" and response != "a" and response != "w":
642                 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
643                                      "and with .rej files / [w]rite the patch to a file (patch.txt) ")
644             if response == "s":
645                 print "Skipping! Good luck with the next patches..."
646                 for f in editedFiles:
647                     p4_system("revert \"%s\"" % f);
648                 for f in filesToAdd:
649                     system("rm %s" %f)
650                 return
651             elif response == "a":
652                 os.system(applyPatchCmd)
653                 if len(filesToAdd) > 0:
654                     print "You may also want to call p4 add on the following files:"
655                     print " ".join(filesToAdd)
656                 if len(filesToDelete):
657                     print "The following files should be scheduled for deletion with p4 delete:"
658                     print " ".join(filesToDelete)
659                 die("Please resolve and submit the conflict manually and "
660                     + "continue afterwards with git-p4 submit --continue")
661             elif response == "w":
662                 system(diffcmd + " > patch.txt")
663                 print "Patch saved to patch.txt in %s !" % self.clientPath
664                 die("Please resolve and submit the conflict manually and "
665                     "continue afterwards with git-p4 submit --continue")
667         system(applyPatchCmd)
669         for f in filesToAdd:
670             p4_system("add \"%s\"" % f)
671         for f in filesToDelete:
672             p4_system("revert \"%s\"" % f)
673             p4_system("delete \"%s\"" % f)
675         # Set/clear executable bits
676         for f in filesToChangeExecBit.keys():
677             mode = filesToChangeExecBit[f]
678             setP4ExecBit(f, mode)
680         logMessage = extractLogMessageFromGitCommit(id)
681         logMessage = logMessage.strip()
683         template = self.prepareSubmitTemplate()
685         if self.interactive:
686             submitTemplate = self.prepareLogMessage(template, logMessage)
687             if os.environ.has_key("P4DIFF"):
688                 del(os.environ["P4DIFF"])
689             diff = p4_read_pipe("diff -du ...")
691             newdiff = ""
692             for newFile in filesToAdd:
693                 newdiff += "==== new file ====\n"
694                 newdiff += "--- /dev/null\n"
695                 newdiff += "+++ %s\n" % newFile
696                 f = open(newFile, "r")
697                 for line in f.readlines():
698                     newdiff += "+" + line
699                 f.close()
701             separatorLine = "######## everything below this line is just the diff #######\n"
703             [handle, fileName] = tempfile.mkstemp()
704             tmpFile = os.fdopen(handle, "w+")
705             if self.isWindows:
706                 submitTemplate = submitTemplate.replace("\n", "\r\n")
707                 separatorLine = separatorLine.replace("\n", "\r\n")
708                 newdiff = newdiff.replace("\n", "\r\n")
709             tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
710             tmpFile.close()
711             mtime = os.stat(fileName).st_mtime
712             defaultEditor = "vi"
713             if platform.system() == "Windows":
714                 defaultEditor = "notepad"
715             if os.environ.has_key("P4EDITOR"):
716                 editor = os.environ.get("P4EDITOR")
717             else:
718                 editor = os.environ.get("EDITOR", defaultEditor);
719             system(editor + " " + fileName)
721             response = "y"
722             if os.stat(fileName).st_mtime <= mtime:
723                 response = "x"
724                 while response != "y" and response != "n":
725                     response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
727             if response == "y":
728                 tmpFile = open(fileName, "rb")
729                 message = tmpFile.read()
730                 tmpFile.close()
731                 submitTemplate = message[:message.index(separatorLine)]
732                 if self.isWindows:
733                     submitTemplate = submitTemplate.replace("\r\n", "\n")
734                 p4_write_pipe("submit -i", submitTemplate)
735             else:
736                 for f in editedFiles:
737                     p4_system("revert \"%s\"" % f);
738                 for f in filesToAdd:
739                     p4_system("revert \"%s\"" % f);
740                     system("rm %s" %f)
742             os.remove(fileName)
743         else:
744             fileName = "submit.txt"
745             file = open(fileName, "w+")
746             file.write(self.prepareLogMessage(template, logMessage))
747             file.close()
748             print ("Perforce submit template written as %s. "
749                    + "Please review/edit and then use p4 submit -i < %s to submit directly!"
750                    % (fileName, fileName))
752     def run(self, args):
753         if len(args) == 0:
754             self.master = currentGitBranch()
755             if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
756                 die("Detecting current git branch failed!")
757         elif len(args) == 1:
758             self.master = args[0]
759         else:
760             return False
762         allowSubmit = gitConfig("git-p4.allowSubmit")
763         if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
764             die("%s is not in git-p4.allowSubmit" % self.master)
766         [upstream, settings] = findUpstreamBranchPoint()
767         self.depotPath = settings['depot-paths'][0]
768         if len(self.origin) == 0:
769             self.origin = upstream
771         if self.verbose:
772             print "Origin branch is " + self.origin
774         if len(self.depotPath) == 0:
775             print "Internal error: cannot locate perforce depot path from existing branches"
776             sys.exit(128)
778         self.clientPath = p4Where(self.depotPath)
780         if len(self.clientPath) == 0:
781             print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
782             sys.exit(128)
784         print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
785         self.oldWorkingDirectory = os.getcwd()
787         chdir(self.clientPath)
788         print "Syncronizing p4 checkout..."
789         p4_system("sync ...")
791         self.check()
793         commits = []
794         for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
795             commits.append(line.strip())
796         commits.reverse()
798         while len(commits) > 0:
799             commit = commits[0]
800             commits = commits[1:]
801             self.applyCommit(commit)
802             if not self.interactive:
803                 break
805         if len(commits) == 0:
806             print "All changes applied!"
807             chdir(self.oldWorkingDirectory)
809             sync = P4Sync()
810             sync.run([])
812             rebase = P4Rebase()
813             rebase.rebase()
815         return True
817 class P4Sync(Command):
818     def __init__(self):
819         Command.__init__(self)
820         self.options = [
821                 optparse.make_option("--branch", dest="branch"),
822                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
823                 optparse.make_option("--changesfile", dest="changesFile"),
824                 optparse.make_option("--silent", dest="silent", action="store_true"),
825                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
826                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
827                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
828                                      help="Import into refs/heads/ , not refs/remotes"),
829                 optparse.make_option("--max-changes", dest="maxChanges"),
830                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
831                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
832                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
833                                      help="Only sync files that are included in the Perforce Client Spec")
834         ]
835         self.description = """Imports from Perforce into a git repository.\n
836     example:
837     //depot/my/project/ -- to import the current head
838     //depot/my/project/@all -- to import everything
839     //depot/my/project/@1,6 -- to import only from revision 1 to 6
841     (a ... is not needed in the path p4 specification, it's added implicitly)"""
843         self.usage += " //depot/path[@revRange]"
844         self.silent = False
845         self.createdBranches = Set()
846         self.committedChanges = Set()
847         self.branch = ""
848         self.detectBranches = False
849         self.detectLabels = False
850         self.changesFile = ""
851         self.syncWithOrigin = True
852         self.verbose = False
853         self.importIntoRemotes = True
854         self.maxChanges = ""
855         self.isWindows = (platform.system() == "Windows")
856         self.keepRepoPath = False
857         self.depotPaths = None
858         self.p4BranchesInGit = []
859         self.cloneExclude = []
860         self.useClientSpec = False
861         self.clientSpecDirs = []
863         if gitConfig("git-p4.syncFromOrigin") == "false":
864             self.syncWithOrigin = False
866     def extractFilesFromCommit(self, commit):
867         self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
868                              for path in self.cloneExclude]
869         files = []
870         fnum = 0
871         while commit.has_key("depotFile%s" % fnum):
872             path =  commit["depotFile%s" % fnum]
874             if [p for p in self.cloneExclude
875                 if path.startswith (p)]:
876                 found = False
877             else:
878                 found = [p for p in self.depotPaths
879                          if path.startswith (p)]
880             if not found:
881                 fnum = fnum + 1
882                 continue
884             file = {}
885             file["path"] = path
886             file["rev"] = commit["rev%s" % fnum]
887             file["action"] = commit["action%s" % fnum]
888             file["type"] = commit["type%s" % fnum]
889             files.append(file)
890             fnum = fnum + 1
891         return files
893     def stripRepoPath(self, path, prefixes):
894         if self.keepRepoPath:
895             prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
897         for p in prefixes:
898             if path.startswith(p):
899                 path = path[len(p):]
901         return path
903     def splitFilesIntoBranches(self, commit):
904         branches = {}
905         fnum = 0
906         while commit.has_key("depotFile%s" % fnum):
907             path =  commit["depotFile%s" % fnum]
908             found = [p for p in self.depotPaths
909                      if path.startswith (p)]
910             if not found:
911                 fnum = fnum + 1
912                 continue
914             file = {}
915             file["path"] = path
916             file["rev"] = commit["rev%s" % fnum]
917             file["action"] = commit["action%s" % fnum]
918             file["type"] = commit["type%s" % fnum]
919             fnum = fnum + 1
921             relPath = self.stripRepoPath(path, self.depotPaths)
923             for branch in self.knownBranches.keys():
925                 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
926                 if relPath.startswith(branch + "/"):
927                     if branch not in branches:
928                         branches[branch] = []
929                     branches[branch].append(file)
930                     break
932         return branches
934     ## Should move this out, doesn't use SELF.
935     def readP4Files(self, files):
936         filesForCommit = []
937         filesToRead = []
939         for f in files:
940             includeFile = True
941             for val in self.clientSpecDirs:
942                 if f['path'].startswith(val[0]):
943                     if val[1] <= 0:
944                         includeFile = False
945                     break
947             if includeFile:
948                 filesForCommit.append(f)
949                 if f['action'] != 'delete':
950                     filesToRead.append(f)
952         filedata = []
953         if len(filesToRead) > 0:
954             filedata = p4CmdList('-x - print',
955                                  stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
956                                                   for f in filesToRead]),
957                                  stdin_mode='w+')
959             if "p4ExitCode" in filedata[0]:
960                 die("Problems executing p4. Error: [%d]."
961                     % (filedata[0]['p4ExitCode']));
963         j = 0;
964         contents = {}
965         while j < len(filedata):
966             stat = filedata[j]
967             j += 1
968             text = [];
969             while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
970                 text.append(filedata[j]['data'])
971                 j += 1
972             text = ''.join(text)
974             if not stat.has_key('depotFile'):
975                 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
976                 continue
978             if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
979                 text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
980             elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
981                 text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', text)
983             contents[stat['depotFile']] = text
985         for f in filesForCommit:
986             path = f['path']
987             if contents.has_key(path):
988                 f['data'] = contents[path]
990         return filesForCommit
992     def commit(self, details, files, branch, branchPrefixes, parent = ""):
993         epoch = details["time"]
994         author = details["user"]
996         if self.verbose:
997             print "commit into %s" % branch
999         # start with reading files; if that fails, we should not
1000         # create a commit.
1001         new_files = []
1002         for f in files:
1003             if [p for p in branchPrefixes if f['path'].startswith(p)]:
1004                 new_files.append (f)
1005             else:
1006                 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
1007         files = self.readP4Files(new_files)
1009         self.gitStream.write("commit %s\n" % branch)
1010 #        gitStream.write("mark :%s\n" % details["change"])
1011         self.committedChanges.add(int(details["change"]))
1012         committer = ""
1013         if author not in self.users:
1014             self.getUserMapFromPerforceServer()
1015         if author in self.users:
1016             committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1017         else:
1018             committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1020         self.gitStream.write("committer %s\n" % committer)
1022         self.gitStream.write("data <<EOT\n")
1023         self.gitStream.write(details["desc"])
1024         self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1025                              % (','.join (branchPrefixes), details["change"]))
1026         if len(details['options']) > 0:
1027             self.gitStream.write(": options = %s" % details['options'])
1028         self.gitStream.write("]\nEOT\n\n")
1030         if len(parent) > 0:
1031             if self.verbose:
1032                 print "parent %s" % parent
1033             self.gitStream.write("from %s\n" % parent)
1035         for file in files:
1036             if file["type"] == "apple":
1037                 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1038                 continue
1040             relPath = self.stripRepoPath(file['path'], branchPrefixes)
1041             if file["action"] == "delete":
1042                 self.gitStream.write("D %s\n" % relPath)
1043             else:
1044                 data = file['data']
1046                 mode = "644"
1047                 if isP4Exec(file["type"]):
1048                     mode = "755"
1049                 elif file["type"] == "symlink":
1050                     mode = "120000"
1051                     # p4 print on a symlink contains "target\n", so strip it off
1052                     data = data[:-1]
1054                 if self.isWindows and file["type"].endswith("text"):
1055                     data = data.replace("\r\n", "\n")
1057                 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1058                 self.gitStream.write("data %s\n" % len(data))
1059                 self.gitStream.write(data)
1060                 self.gitStream.write("\n")
1062         self.gitStream.write("\n")
1064         change = int(details["change"])
1066         if self.labels.has_key(change):
1067             label = self.labels[change]
1068             labelDetails = label[0]
1069             labelRevisions = label[1]
1070             if self.verbose:
1071                 print "Change %s is labelled %s" % (change, labelDetails)
1073             files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1074                                                     for p in branchPrefixes]))
1076             if len(files) == len(labelRevisions):
1078                 cleanedFiles = {}
1079                 for info in files:
1080                     if info["action"] == "delete":
1081                         continue
1082                     cleanedFiles[info["depotFile"]] = info["rev"]
1084                 if cleanedFiles == labelRevisions:
1085                     self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1086                     self.gitStream.write("from %s\n" % branch)
1088                     owner = labelDetails["Owner"]
1089                     tagger = ""
1090                     if author in self.users:
1091                         tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1092                     else:
1093                         tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1094                     self.gitStream.write("tagger %s\n" % tagger)
1095                     self.gitStream.write("data <<EOT\n")
1096                     self.gitStream.write(labelDetails["Description"])
1097                     self.gitStream.write("EOT\n\n")
1099                 else:
1100                     if not self.silent:
1101                         print ("Tag %s does not match with change %s: files do not match."
1102                                % (labelDetails["label"], change))
1104             else:
1105                 if not self.silent:
1106                     print ("Tag %s does not match with change %s: file count is different."
1107                            % (labelDetails["label"], change))
1109     def getUserCacheFilename(self):
1110         home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1111         return home + "/.gitp4-usercache.txt"
1113     def getUserMapFromPerforceServer(self):
1114         if self.userMapFromPerforceServer:
1115             return
1116         self.users = {}
1118         for output in p4CmdList("users"):
1119             if not output.has_key("User"):
1120                 continue
1121             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1124         s = ''
1125         for (key, val) in self.users.items():
1126             s += "%s\t%s\n" % (key, val)
1128         open(self.getUserCacheFilename(), "wb").write(s)
1129         self.userMapFromPerforceServer = True
1131     def loadUserMapFromCache(self):
1132         self.users = {}
1133         self.userMapFromPerforceServer = False
1134         try:
1135             cache = open(self.getUserCacheFilename(), "rb")
1136             lines = cache.readlines()
1137             cache.close()
1138             for line in lines:
1139                 entry = line.strip().split("\t")
1140                 self.users[entry[0]] = entry[1]
1141         except IOError:
1142             self.getUserMapFromPerforceServer()
1144     def getLabels(self):
1145         self.labels = {}
1147         l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1148         if len(l) > 0 and not self.silent:
1149             print "Finding files belonging to labels in %s" % `self.depotPaths`
1151         for output in l:
1152             label = output["label"]
1153             revisions = {}
1154             newestChange = 0
1155             if self.verbose:
1156                 print "Querying files for label %s" % label
1157             for file in p4CmdList("files "
1158                                   +  ' '.join (["%s...@%s" % (p, label)
1159                                                 for p in self.depotPaths])):
1160                 revisions[file["depotFile"]] = file["rev"]
1161                 change = int(file["change"])
1162                 if change > newestChange:
1163                     newestChange = change
1165             self.labels[newestChange] = [output, revisions]
1167         if self.verbose:
1168             print "Label changes: %s" % self.labels.keys()
1170     def guessProjectName(self):
1171         for p in self.depotPaths:
1172             if p.endswith("/"):
1173                 p = p[:-1]
1174             p = p[p.strip().rfind("/") + 1:]
1175             if not p.endswith("/"):
1176                p += "/"
1177             return p
1179     def getBranchMapping(self):
1180         lostAndFoundBranches = set()
1182         for info in p4CmdList("branches"):
1183             details = p4Cmd("branch -o %s" % info["branch"])
1184             viewIdx = 0
1185             while details.has_key("View%s" % viewIdx):
1186                 paths = details["View%s" % viewIdx].split(" ")
1187                 viewIdx = viewIdx + 1
1188                 # require standard //depot/foo/... //depot/bar/... mapping
1189                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1190                     continue
1191                 source = paths[0]
1192                 destination = paths[1]
1193                 ## HACK
1194                 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1195                     source = source[len(self.depotPaths[0]):-4]
1196                     destination = destination[len(self.depotPaths[0]):-4]
1198                     if destination in self.knownBranches:
1199                         if not self.silent:
1200                             print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1201                             print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1202                         continue
1204                     self.knownBranches[destination] = source
1206                     lostAndFoundBranches.discard(destination)
1208                     if source not in self.knownBranches:
1209                         lostAndFoundBranches.add(source)
1212         for branch in lostAndFoundBranches:
1213             self.knownBranches[branch] = branch
1215     def getBranchMappingFromGitBranches(self):
1216         branches = p4BranchesInGit(self.importIntoRemotes)
1217         for branch in branches.keys():
1218             if branch == "master":
1219                 branch = "main"
1220             else:
1221                 branch = branch[len(self.projectName):]
1222             self.knownBranches[branch] = branch
1224     def listExistingP4GitBranches(self):
1225         # branches holds mapping from name to commit
1226         branches = p4BranchesInGit(self.importIntoRemotes)
1227         self.p4BranchesInGit = branches.keys()
1228         for branch in branches.keys():
1229             self.initialParents[self.refPrefix + branch] = branches[branch]
1231     def updateOptionDict(self, d):
1232         option_keys = {}
1233         if self.keepRepoPath:
1234             option_keys['keepRepoPath'] = 1
1236         d["options"] = ' '.join(sorted(option_keys.keys()))
1238     def readOptions(self, d):
1239         self.keepRepoPath = (d.has_key('options')
1240                              and ('keepRepoPath' in d['options']))
1242     def gitRefForBranch(self, branch):
1243         if branch == "main":
1244             return self.refPrefix + "master"
1246         if len(branch) <= 0:
1247             return branch
1249         return self.refPrefix + self.projectName + branch
1251     def gitCommitByP4Change(self, ref, change):
1252         if self.verbose:
1253             print "looking in ref " + ref + " for change %s using bisect..." % change
1255         earliestCommit = ""
1256         latestCommit = parseRevision(ref)
1258         while True:
1259             if self.verbose:
1260                 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1261             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1262             if len(next) == 0:
1263                 if self.verbose:
1264                     print "argh"
1265                 return ""
1266             log = extractLogMessageFromGitCommit(next)
1267             settings = extractSettingsGitLog(log)
1268             currentChange = int(settings['change'])
1269             if self.verbose:
1270                 print "current change %s" % currentChange
1272             if currentChange == change:
1273                 if self.verbose:
1274                     print "found %s" % next
1275                 return next
1277             if currentChange < change:
1278                 earliestCommit = "^%s" % next
1279             else:
1280                 latestCommit = "%s" % next
1282         return ""
1284     def importNewBranch(self, branch, maxChange):
1285         # make fast-import flush all changes to disk and update the refs using the checkpoint
1286         # command so that we can try to find the branch parent in the git history
1287         self.gitStream.write("checkpoint\n\n");
1288         self.gitStream.flush();
1289         branchPrefix = self.depotPaths[0] + branch + "/"
1290         range = "@1,%s" % maxChange
1291         #print "prefix" + branchPrefix
1292         changes = p4ChangesForPaths([branchPrefix], range)
1293         if len(changes) <= 0:
1294             return False
1295         firstChange = changes[0]
1296         #print "first change in branch: %s" % firstChange
1297         sourceBranch = self.knownBranches[branch]
1298         sourceDepotPath = self.depotPaths[0] + sourceBranch
1299         sourceRef = self.gitRefForBranch(sourceBranch)
1300         #print "source " + sourceBranch
1302         branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1303         #print "branch parent: %s" % branchParentChange
1304         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1305         if len(gitParent) > 0:
1306             self.initialParents[self.gitRefForBranch(branch)] = gitParent
1307             #print "parent git commit: %s" % gitParent
1309         self.importChanges(changes)
1310         return True
1312     def importChanges(self, changes):
1313         cnt = 1
1314         for change in changes:
1315             description = p4Cmd("describe %s" % change)
1316             self.updateOptionDict(description)
1318             if not self.silent:
1319                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1320                 sys.stdout.flush()
1321             cnt = cnt + 1
1323             try:
1324                 if self.detectBranches:
1325                     branches = self.splitFilesIntoBranches(description)
1326                     for branch in branches.keys():
1327                         ## HACK  --hwn
1328                         branchPrefix = self.depotPaths[0] + branch + "/"
1330                         parent = ""
1332                         filesForCommit = branches[branch]
1334                         if self.verbose:
1335                             print "branch is %s" % branch
1337                         self.updatedBranches.add(branch)
1339                         if branch not in self.createdBranches:
1340                             self.createdBranches.add(branch)
1341                             parent = self.knownBranches[branch]
1342                             if parent == branch:
1343                                 parent = ""
1344                             else:
1345                                 fullBranch = self.projectName + branch
1346                                 if fullBranch not in self.p4BranchesInGit:
1347                                     if not self.silent:
1348                                         print("\n    Importing new branch %s" % fullBranch);
1349                                     if self.importNewBranch(branch, change - 1):
1350                                         parent = ""
1351                                         self.p4BranchesInGit.append(fullBranch)
1352                                     if not self.silent:
1353                                         print("\n    Resuming with change %s" % change);
1355                                 if self.verbose:
1356                                     print "parent determined through known branches: %s" % parent
1358                         branch = self.gitRefForBranch(branch)
1359                         parent = self.gitRefForBranch(parent)
1361                         if self.verbose:
1362                             print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1364                         if len(parent) == 0 and branch in self.initialParents:
1365                             parent = self.initialParents[branch]
1366                             del self.initialParents[branch]
1368                         self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1369                 else:
1370                     files = self.extractFilesFromCommit(description)
1371                     self.commit(description, files, self.branch, self.depotPaths,
1372                                 self.initialParent)
1373                     self.initialParent = ""
1374             except IOError:
1375                 print self.gitError.read()
1376                 sys.exit(1)
1378     def importHeadRevision(self, revision):
1379         print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1381         details = { "user" : "git perforce import user", "time" : int(time.time()) }
1382         details["desc"] = ("Initial import of %s from the state at revision %s"
1383                            % (' '.join(self.depotPaths), revision))
1384         details["change"] = revision
1385         newestRevision = 0
1387         fileCnt = 0
1388         for info in p4CmdList("files "
1389                               +  ' '.join(["%s...%s"
1390                                            % (p, revision)
1391                                            for p in self.depotPaths])):
1393             if info['code'] == 'error':
1394                 sys.stderr.write("p4 returned an error: %s\n"
1395                                  % info['data'])
1396                 sys.exit(1)
1399             change = int(info["change"])
1400             if change > newestRevision:
1401                 newestRevision = change
1403             if info["action"] == "delete":
1404                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1405                 #fileCnt = fileCnt + 1
1406                 continue
1408             for prop in ["depotFile", "rev", "action", "type" ]:
1409                 details["%s%s" % (prop, fileCnt)] = info[prop]
1411             fileCnt = fileCnt + 1
1413         details["change"] = newestRevision
1414         self.updateOptionDict(details)
1415         try:
1416             self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1417         except IOError:
1418             print "IO error with git fast-import. Is your git version recent enough?"
1419             print self.gitError.read()
1422     def getClientSpec(self):
1423         specList = p4CmdList( "client -o" )
1424         temp = {}
1425         for entry in specList:
1426             for k,v in entry.iteritems():
1427                 if k.startswith("View"):
1428                     if v.startswith('"'):
1429                         start = 1
1430                     else:
1431                         start = 0
1432                     index = v.find("...")
1433                     v = v[start:index]
1434                     if v.startswith("-"):
1435                         v = v[1:]
1436                         temp[v] = -len(v)
1437                     else:
1438                         temp[v] = len(v)
1439         self.clientSpecDirs = temp.items()
1440         self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1442     def run(self, args):
1443         self.depotPaths = []
1444         self.changeRange = ""
1445         self.initialParent = ""
1446         self.previousDepotPaths = []
1448         # map from branch depot path to parent branch
1449         self.knownBranches = {}
1450         self.initialParents = {}
1451         self.hasOrigin = originP4BranchesExist()
1452         if not self.syncWithOrigin:
1453             self.hasOrigin = False
1455         if self.importIntoRemotes:
1456             self.refPrefix = "refs/remotes/p4/"
1457         else:
1458             self.refPrefix = "refs/heads/p4/"
1460         if self.syncWithOrigin and self.hasOrigin:
1461             if not self.silent:
1462                 print "Syncing with origin first by calling git fetch origin"
1463             system("git fetch origin")
1465         if len(self.branch) == 0:
1466             self.branch = self.refPrefix + "master"
1467             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1468                 system("git update-ref %s refs/heads/p4" % self.branch)
1469                 system("git branch -D p4");
1470             # create it /after/ importing, when master exists
1471             if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1472                 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1474         if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1475             self.getClientSpec()
1477         # TODO: should always look at previous commits,
1478         # merge with previous imports, if possible.
1479         if args == []:
1480             if self.hasOrigin:
1481                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1482             self.listExistingP4GitBranches()
1484             if len(self.p4BranchesInGit) > 1:
1485                 if not self.silent:
1486                     print "Importing from/into multiple branches"
1487                 self.detectBranches = True
1489             if self.verbose:
1490                 print "branches: %s" % self.p4BranchesInGit
1492             p4Change = 0
1493             for branch in self.p4BranchesInGit:
1494                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1496                 settings = extractSettingsGitLog(logMsg)
1498                 self.readOptions(settings)
1499                 if (settings.has_key('depot-paths')
1500                     and settings.has_key ('change')):
1501                     change = int(settings['change']) + 1
1502                     p4Change = max(p4Change, change)
1504                     depotPaths = sorted(settings['depot-paths'])
1505                     if self.previousDepotPaths == []:
1506                         self.previousDepotPaths = depotPaths
1507                     else:
1508                         paths = []
1509                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1510                             for i in range(0, min(len(cur), len(prev))):
1511                                 if cur[i] <> prev[i]:
1512                                     i = i - 1
1513                                     break
1515                             paths.append (cur[:i + 1])
1517                         self.previousDepotPaths = paths
1519             if p4Change > 0:
1520                 self.depotPaths = sorted(self.previousDepotPaths)
1521                 self.changeRange = "@%s,#head" % p4Change
1522                 if not self.detectBranches:
1523                     self.initialParent = parseRevision(self.branch)
1524                 if not self.silent and not self.detectBranches:
1525                     print "Performing incremental import into %s git branch" % self.branch
1527         if not self.branch.startswith("refs/"):
1528             self.branch = "refs/heads/" + self.branch
1530         if len(args) == 0 and self.depotPaths:
1531             if not self.silent:
1532                 print "Depot paths: %s" % ' '.join(self.depotPaths)
1533         else:
1534             if self.depotPaths and self.depotPaths != args:
1535                 print ("previous import used depot path %s and now %s was specified. "
1536                        "This doesn't work!" % (' '.join (self.depotPaths),
1537                                                ' '.join (args)))
1538                 sys.exit(1)
1540             self.depotPaths = sorted(args)
1542         revision = ""
1543         self.users = {}
1545         newPaths = []
1546         for p in self.depotPaths:
1547             if p.find("@") != -1:
1548                 atIdx = p.index("@")
1549                 self.changeRange = p[atIdx:]
1550                 if self.changeRange == "@all":
1551                     self.changeRange = ""
1552                 elif ',' not in self.changeRange:
1553                     revision = self.changeRange
1554                     self.changeRange = ""
1555                 p = p[:atIdx]
1556             elif p.find("#") != -1:
1557                 hashIdx = p.index("#")
1558                 revision = p[hashIdx:]
1559                 p = p[:hashIdx]
1560             elif self.previousDepotPaths == []:
1561                 revision = "#head"
1563             p = re.sub ("\.\.\.$", "", p)
1564             if not p.endswith("/"):
1565                 p += "/"
1567             newPaths.append(p)
1569         self.depotPaths = newPaths
1572         self.loadUserMapFromCache()
1573         self.labels = {}
1574         if self.detectLabels:
1575             self.getLabels();
1577         if self.detectBranches:
1578             ## FIXME - what's a P4 projectName ?
1579             self.projectName = self.guessProjectName()
1581             if self.hasOrigin:
1582                 self.getBranchMappingFromGitBranches()
1583             else:
1584                 self.getBranchMapping()
1585             if self.verbose:
1586                 print "p4-git branches: %s" % self.p4BranchesInGit
1587                 print "initial parents: %s" % self.initialParents
1588             for b in self.p4BranchesInGit:
1589                 if b != "master":
1591                     ## FIXME
1592                     b = b[len(self.projectName):]
1593                 self.createdBranches.add(b)
1595         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1597         importProcess = subprocess.Popen(["git", "fast-import"],
1598                                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1599                                          stderr=subprocess.PIPE);
1600         self.gitOutput = importProcess.stdout
1601         self.gitStream = importProcess.stdin
1602         self.gitError = importProcess.stderr
1604         if revision:
1605             self.importHeadRevision(revision)
1606         else:
1607             changes = []
1609             if len(self.changesFile) > 0:
1610                 output = open(self.changesFile).readlines()
1611                 changeSet = Set()
1612                 for line in output:
1613                     changeSet.add(int(line))
1615                 for change in changeSet:
1616                     changes.append(change)
1618                 changes.sort()
1619             else:
1620                 if self.verbose:
1621                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1622                                                               self.changeRange)
1623                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1625                 if len(self.maxChanges) > 0:
1626                     changes = changes[:min(int(self.maxChanges), len(changes))]
1628             if len(changes) == 0:
1629                 if not self.silent:
1630                     print "No changes to import!"
1631                 return True
1633             if not self.silent and not self.detectBranches:
1634                 print "Import destination: %s" % self.branch
1636             self.updatedBranches = set()
1638             self.importChanges(changes)
1640             if not self.silent:
1641                 print ""
1642                 if len(self.updatedBranches) > 0:
1643                     sys.stdout.write("Updated branches: ")
1644                     for b in self.updatedBranches:
1645                         sys.stdout.write("%s " % b)
1646                     sys.stdout.write("\n")
1648         self.gitStream.close()
1649         if importProcess.wait() != 0:
1650             die("fast-import failed: %s" % self.gitError.read())
1651         self.gitOutput.close()
1652         self.gitError.close()
1654         return True
1656 class P4Rebase(Command):
1657     def __init__(self):
1658         Command.__init__(self)
1659         self.options = [ ]
1660         self.description = ("Fetches the latest revision from perforce and "
1661                             + "rebases the current work (branch) against it")
1662         self.verbose = False
1664     def run(self, args):
1665         sync = P4Sync()
1666         sync.run([])
1668         return self.rebase()
1670     def rebase(self):
1671         if os.system("git update-index --refresh") != 0:
1672             die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up-to-date or stash away all your changes with git stash.");
1673         if len(read_pipe("git diff-index HEAD --")) > 0:
1674             die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1676         [upstream, settings] = findUpstreamBranchPoint()
1677         if len(upstream) == 0:
1678             die("Cannot find upstream branchpoint for rebase")
1680         # the branchpoint may be p4/foo~3, so strip off the parent
1681         upstream = re.sub("~[0-9]+$", "", upstream)
1683         print "Rebasing the current branch onto %s" % upstream
1684         oldHead = read_pipe("git rev-parse HEAD").strip()
1685         system("git rebase %s" % upstream)
1686         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1687         return True
1689 class P4Clone(P4Sync):
1690     def __init__(self):
1691         P4Sync.__init__(self)
1692         self.description = "Creates a new git repository and imports from Perforce into it"
1693         self.usage = "usage: %prog [options] //depot/path[@revRange]"
1694         self.options += [
1695             optparse.make_option("--destination", dest="cloneDestination",
1696                                  action='store', default=None,
1697                                  help="where to leave result of the clone"),
1698             optparse.make_option("-/", dest="cloneExclude",
1699                                  action="append", type="string",
1700                                  help="exclude depot path")
1701         ]
1702         self.cloneDestination = None
1703         self.needsGit = False
1705     # This is required for the "append" cloneExclude action
1706     def ensure_value(self, attr, value):
1707         if not hasattr(self, attr) or getattr(self, attr) is None:
1708             setattr(self, attr, value)
1709         return getattr(self, attr)
1711     def defaultDestination(self, args):
1712         ## TODO: use common prefix of args?
1713         depotPath = args[0]
1714         depotDir = re.sub("(@[^@]*)$", "", depotPath)
1715         depotDir = re.sub("(#[^#]*)$", "", depotDir)
1716         depotDir = re.sub(r"\.\.\.$", "", depotDir)
1717         depotDir = re.sub(r"/$", "", depotDir)
1718         return os.path.split(depotDir)[1]
1720     def run(self, args):
1721         if len(args) < 1:
1722             return False
1724         if self.keepRepoPath and not self.cloneDestination:
1725             sys.stderr.write("Must specify destination for --keep-path\n")
1726             sys.exit(1)
1728         depotPaths = args
1730         if not self.cloneDestination and len(depotPaths) > 1:
1731             self.cloneDestination = depotPaths[-1]
1732             depotPaths = depotPaths[:-1]
1734         self.cloneExclude = ["/"+p for p in self.cloneExclude]
1735         for p in depotPaths:
1736             if not p.startswith("//"):
1737                 return False
1739         if not self.cloneDestination:
1740             self.cloneDestination = self.defaultDestination(args)
1742         print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1743         if not os.path.exists(self.cloneDestination):
1744             os.makedirs(self.cloneDestination)
1745         chdir(self.cloneDestination)
1746         system("git init")
1747         self.gitdir = os.getcwd() + "/.git"
1748         if not P4Sync.run(self, depotPaths):
1749             return False
1750         if self.branch != "master":
1751             if self.importIntoRemotes:
1752                 masterbranch = "refs/remotes/p4/master"
1753             else:
1754                 masterbranch = "refs/heads/p4/master"
1755             if gitBranchExists(masterbranch):
1756                 system("git branch master %s" % masterbranch)
1757                 system("git checkout -f")
1758             else:
1759                 print "Could not detect main branch. No checkout/master branch created."
1761         return True
1763 class P4Branches(Command):
1764     def __init__(self):
1765         Command.__init__(self)
1766         self.options = [ ]
1767         self.description = ("Shows the git branches that hold imports and their "
1768                             + "corresponding perforce depot paths")
1769         self.verbose = False
1771     def run(self, args):
1772         if originP4BranchesExist():
1773             createOrUpdateBranchesFromOrigin()
1775         cmdline = "git rev-parse --symbolic "
1776         cmdline += " --remotes"
1778         for line in read_pipe_lines(cmdline):
1779             line = line.strip()
1781             if not line.startswith('p4/') or line == "p4/HEAD":
1782                 continue
1783             branch = line
1785             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1786             settings = extractSettingsGitLog(log)
1788             print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1789         return True
1791 class HelpFormatter(optparse.IndentedHelpFormatter):
1792     def __init__(self):
1793         optparse.IndentedHelpFormatter.__init__(self)
1795     def format_description(self, description):
1796         if description:
1797             return description + "\n"
1798         else:
1799             return ""
1801 def printUsage(commands):
1802     print "usage: %s <command> [options]" % sys.argv[0]
1803     print ""
1804     print "valid commands: %s" % ", ".join(commands)
1805     print ""
1806     print "Try %s <command> --help for command specific help." % sys.argv[0]
1807     print ""
1809 commands = {
1810     "debug" : P4Debug,
1811     "submit" : P4Submit,
1812     "commit" : P4Submit,
1813     "sync" : P4Sync,
1814     "rebase" : P4Rebase,
1815     "clone" : P4Clone,
1816     "rollback" : P4RollBack,
1817     "branches" : P4Branches
1821 def main():
1822     if len(sys.argv[1:]) == 0:
1823         printUsage(commands.keys())
1824         sys.exit(2)
1826     cmd = ""
1827     cmdName = sys.argv[1]
1828     try:
1829         klass = commands[cmdName]
1830         cmd = klass()
1831     except KeyError:
1832         print "unknown command %s" % cmdName
1833         print ""
1834         printUsage(commands.keys())
1835         sys.exit(2)
1837     options = cmd.options
1838     cmd.gitdir = os.environ.get("GIT_DIR", None)
1840     args = sys.argv[2:]
1842     if len(options) > 0:
1843         options.append(optparse.make_option("--git-dir", dest="gitdir"))
1845         parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1846                                        options,
1847                                        description = cmd.description,
1848                                        formatter = HelpFormatter())
1850         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1851     global verbose
1852     verbose = cmd.verbose
1853     if cmd.needsGit:
1854         if cmd.gitdir == None:
1855             cmd.gitdir = os.path.abspath(".git")
1856             if not isValidGitDir(cmd.gitdir):
1857                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1858                 if os.path.exists(cmd.gitdir):
1859                     cdup = read_pipe("git rev-parse --show-cdup").strip()
1860                     if len(cdup) > 0:
1861                         chdir(cdup);
1863         if not isValidGitDir(cmd.gitdir):
1864             if isValidGitDir(cmd.gitdir + "/.git"):
1865                 cmd.gitdir += "/.git"
1866             else:
1867                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1869         os.environ["GIT_DIR"] = cmd.gitdir
1871     if not cmd.run(args):
1872         parser.print_help()
1875 if __name__ == '__main__':
1876     main()