Code

If we are in verbose mode, output what we are about to run (or return)
[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 %s" % ("p4", cmd)
28     if verbose:
29         print real_cmd
30     return real_cmd
32 def die(msg):
33     if verbose:
34         raise Exception(msg)
35     else:
36         sys.stderr.write(msg + "\n")
37         sys.exit(1)
39 def write_pipe(c, str):
40     if verbose:
41         sys.stderr.write('Writing pipe: %s\n' % c)
43     pipe = os.popen(c, 'w')
44     val = pipe.write(str)
45     if pipe.close():
46         die('Command failed: %s' % c)
48     return val
50 def read_pipe(c, ignore_error=False):
51     if verbose:
52         sys.stderr.write('Reading pipe: %s\n' % c)
54     pipe = os.popen(c, 'rb')
55     val = pipe.read()
56     if pipe.close() and not ignore_error:
57         die('Command failed: %s' % c)
59     return val
62 def read_pipe_lines(c):
63     if verbose:
64         sys.stderr.write('Reading pipe: %s\n' % c)
65     ## todo: check return status
66     pipe = os.popen(c, 'rb')
67     val = pipe.readlines()
68     if pipe.close():
69         die('Command failed: %s' % c)
71     return val
73 def p4_read_pipe_lines(c):
74     """Specifically invoke p4 on the command supplied. """
75     real_cmd = "%s %s" % ("p4", c)
76     if verbose:
77         print real_cmd
78     return read_pipe_lines(real_cmd)
80 def system(cmd):
81     if verbose:
82         sys.stderr.write("executing %s\n" % cmd)
83     if os.system(cmd) != 0:
84         die("command failed: %s" % cmd)
86 def p4_system(cmd):
87     """Specifically invoke p4 as the system command. """
88     real_cmd = "%s %s" % ("p4", cmd)
89     if verbose:
90         print real_cmd
91     return system(real_cmd)
93 def isP4Exec(kind):
94     """Determine if a Perforce 'kind' should have execute permission
96     'p4 help filetypes' gives a list of the types.  If it starts with 'x',
97     or x follows one of a few letters.  Otherwise, if there is an 'x' after
98     a plus sign, it is also executable"""
99     return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
101 def setP4ExecBit(file, mode):
102     # Reopens an already open file and changes the execute bit to match
103     # the execute bit setting in the passed in mode.
105     p4Type = "+x"
107     if not isModeExec(mode):
108         p4Type = getP4OpenedType(file)
109         p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
110         p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
111         if p4Type[-1] == "+":
112             p4Type = p4Type[0:-1]
114     p4_system("reopen -t %s %s" % (p4Type, file))
116 def getP4OpenedType(file):
117     # Returns the perforce file type for the given file.
119     result = read_pipe("p4 opened %s" % file)
120     match = re.match(".*\((.+)\)\r?$", result)
121     if match:
122         return match.group(1)
123     else:
124         die("Could not determine file type for %s (result: '%s')" % (file, result))
126 def diffTreePattern():
127     # This is a simple generator for the diff tree regex pattern. This could be
128     # a class variable if this and parseDiffTreeEntry were a part of a class.
129     pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
130     while True:
131         yield pattern
133 def parseDiffTreeEntry(entry):
134     """Parses a single diff tree entry into its component elements.
136     See git-diff-tree(1) manpage for details about the format of the diff
137     output. This method returns a dictionary with the following elements:
139     src_mode - The mode of the source file
140     dst_mode - The mode of the destination file
141     src_sha1 - The sha1 for the source file
142     dst_sha1 - The sha1 fr the destination file
143     status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
144     status_score - The score for the status (applicable for 'C' and 'R'
145                    statuses). This is None if there is no score.
146     src - The path for the source file.
147     dst - The path for the destination file. This is only present for
148           copy or renames. If it is not present, this is None.
150     If the pattern is not matched, None is returned."""
152     match = diffTreePattern().next().match(entry)
153     if match:
154         return {
155             'src_mode': match.group(1),
156             'dst_mode': match.group(2),
157             'src_sha1': match.group(3),
158             'dst_sha1': match.group(4),
159             'status': match.group(5),
160             'status_score': match.group(6),
161             'src': match.group(7),
162             'dst': match.group(10)
163         }
164     return None
166 def isModeExec(mode):
167     # Returns True if the given git mode represents an executable file,
168     # otherwise False.
169     return mode[-3:] == "755"
171 def isModeExecChanged(src_mode, dst_mode):
172     return isModeExec(src_mode) != isModeExec(dst_mode)
174 def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
175     cmd = "p4 -G %s" % cmd
176     if verbose:
177         sys.stderr.write("Opening pipe: %s\n" % cmd)
179     # Use a temporary file to avoid deadlocks without
180     # subprocess.communicate(), which would put another copy
181     # of stdout into memory.
182     stdin_file = None
183     if stdin is not None:
184         stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
185         stdin_file.write(stdin)
186         stdin_file.flush()
187         stdin_file.seek(0)
189     p4 = subprocess.Popen(cmd, shell=True,
190                           stdin=stdin_file,
191                           stdout=subprocess.PIPE)
193     result = []
194     try:
195         while True:
196             entry = marshal.load(p4.stdout)
197             result.append(entry)
198     except EOFError:
199         pass
200     exitCode = p4.wait()
201     if exitCode != 0:
202         entry = {}
203         entry["p4ExitCode"] = exitCode
204         result.append(entry)
206     return result
208 def p4Cmd(cmd):
209     list = p4CmdList(cmd)
210     result = {}
211     for entry in list:
212         result.update(entry)
213     return result;
215 def p4Where(depotPath):
216     if not depotPath.endswith("/"):
217         depotPath += "/"
218     output = p4Cmd("where %s..." % depotPath)
219     if output["code"] == "error":
220         return ""
221     clientPath = ""
222     if "path" in output:
223         clientPath = output.get("path")
224     elif "data" in output:
225         data = output.get("data")
226         lastSpace = data.rfind(" ")
227         clientPath = data[lastSpace + 1:]
229     if clientPath.endswith("..."):
230         clientPath = clientPath[:-3]
231     return clientPath
233 def currentGitBranch():
234     return read_pipe("git name-rev HEAD").split(" ")[1].strip()
236 def isValidGitDir(path):
237     if (os.path.exists(path + "/HEAD")
238         and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
239         return True;
240     return False
242 def parseRevision(ref):
243     return read_pipe("git rev-parse %s" % ref).strip()
245 def extractLogMessageFromGitCommit(commit):
246     logMessage = ""
248     ## fixme: title is first line of commit, not 1st paragraph.
249     foundTitle = False
250     for log in read_pipe_lines("git cat-file commit %s" % commit):
251        if not foundTitle:
252            if len(log) == 1:
253                foundTitle = True
254            continue
256        logMessage += log
257     return logMessage
259 def extractSettingsGitLog(log):
260     values = {}
261     for line in log.split("\n"):
262         line = line.strip()
263         m = re.search (r"^ *\[git-p4: (.*)\]$", line)
264         if not m:
265             continue
267         assignments = m.group(1).split (':')
268         for a in assignments:
269             vals = a.split ('=')
270             key = vals[0].strip()
271             val = ('='.join (vals[1:])).strip()
272             if val.endswith ('\"') and val.startswith('"'):
273                 val = val[1:-1]
275             values[key] = val
277     paths = values.get("depot-paths")
278     if not paths:
279         paths = values.get("depot-path")
280     if paths:
281         values['depot-paths'] = paths.split(',')
282     return values
284 def gitBranchExists(branch):
285     proc = subprocess.Popen(["git", "rev-parse", branch],
286                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);
287     return proc.wait() == 0;
289 def gitConfig(key):
290     return read_pipe("git config %s" % key, ignore_error=True).strip()
292 def p4BranchesInGit(branchesAreInRemotes = True):
293     branches = {}
295     cmdline = "git rev-parse --symbolic "
296     if branchesAreInRemotes:
297         cmdline += " --remotes"
298     else:
299         cmdline += " --branches"
301     for line in read_pipe_lines(cmdline):
302         line = line.strip()
304         ## only import to p4/
305         if not line.startswith('p4/') or line == "p4/HEAD":
306             continue
307         branch = line
309         # strip off p4
310         branch = re.sub ("^p4/", "", line)
312         branches[branch] = parseRevision(line)
313     return branches
315 def findUpstreamBranchPoint(head = "HEAD"):
316     branches = p4BranchesInGit()
317     # map from depot-path to branch name
318     branchByDepotPath = {}
319     for branch in branches.keys():
320         tip = branches[branch]
321         log = extractLogMessageFromGitCommit(tip)
322         settings = extractSettingsGitLog(log)
323         if settings.has_key("depot-paths"):
324             paths = ",".join(settings["depot-paths"])
325             branchByDepotPath[paths] = "remotes/p4/" + branch
327     settings = None
328     parent = 0
329     while parent < 65535:
330         commit = head + "~%s" % parent
331         log = extractLogMessageFromGitCommit(commit)
332         settings = extractSettingsGitLog(log)
333         if settings.has_key("depot-paths"):
334             paths = ",".join(settings["depot-paths"])
335             if branchByDepotPath.has_key(paths):
336                 return [branchByDepotPath[paths], settings]
338         parent = parent + 1
340     return ["", settings]
342 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
343     if not silent:
344         print ("Creating/updating branch(es) in %s based on origin branch(es)"
345                % localRefPrefix)
347     originPrefix = "origin/p4/"
349     for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
350         line = line.strip()
351         if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
352             continue
354         headName = line[len(originPrefix):]
355         remoteHead = localRefPrefix + headName
356         originHead = line
358         original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
359         if (not original.has_key('depot-paths')
360             or not original.has_key('change')):
361             continue
363         update = False
364         if not gitBranchExists(remoteHead):
365             if verbose:
366                 print "creating %s" % remoteHead
367             update = True
368         else:
369             settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
370             if settings.has_key('change') > 0:
371                 if settings['depot-paths'] == original['depot-paths']:
372                     originP4Change = int(original['change'])
373                     p4Change = int(settings['change'])
374                     if originP4Change > p4Change:
375                         print ("%s (%s) is newer than %s (%s). "
376                                "Updating p4 branch from origin."
377                                % (originHead, originP4Change,
378                                   remoteHead, p4Change))
379                         update = True
380                 else:
381                     print ("Ignoring: %s was imported from %s while "
382                            "%s was imported from %s"
383                            % (originHead, ','.join(original['depot-paths']),
384                               remoteHead, ','.join(settings['depot-paths'])))
386         if update:
387             system("git update-ref %s %s" % (remoteHead, originHead))
389 def originP4BranchesExist():
390         return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
392 def p4ChangesForPaths(depotPaths, changeRange):
393     assert depotPaths
394     output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
395                                                         for p in depotPaths]))
397     changes = []
398     for line in output:
399         changeNum = line.split(" ")[1]
400         changes.append(int(changeNum))
402     changes.sort()
403     return changes
405 class Command:
406     def __init__(self):
407         self.usage = "usage: %prog [options]"
408         self.needsGit = True
410 class P4Debug(Command):
411     def __init__(self):
412         Command.__init__(self)
413         self.options = [
414             optparse.make_option("--verbose", dest="verbose", action="store_true",
415                                  default=False),
416             ]
417         self.description = "A tool to debug the output of p4 -G."
418         self.needsGit = False
419         self.verbose = False
421     def run(self, args):
422         j = 0
423         for output in p4CmdList(" ".join(args)):
424             print 'Element: %d' % j
425             j += 1
426             print output
427         return True
429 class P4RollBack(Command):
430     def __init__(self):
431         Command.__init__(self)
432         self.options = [
433             optparse.make_option("--verbose", dest="verbose", action="store_true"),
434             optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
435         ]
436         self.description = "A tool to debug the multi-branch import. Don't use :)"
437         self.verbose = False
438         self.rollbackLocalBranches = False
440     def run(self, args):
441         if len(args) != 1:
442             return False
443         maxChange = int(args[0])
445         if "p4ExitCode" in p4Cmd("changes -m 1"):
446             die("Problems executing p4");
448         if self.rollbackLocalBranches:
449             refPrefix = "refs/heads/"
450             lines = read_pipe_lines("git rev-parse --symbolic --branches")
451         else:
452             refPrefix = "refs/remotes/"
453             lines = read_pipe_lines("git rev-parse --symbolic --remotes")
455         for line in lines:
456             if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
457                 line = line.strip()
458                 ref = refPrefix + line
459                 log = extractLogMessageFromGitCommit(ref)
460                 settings = extractSettingsGitLog(log)
462                 depotPaths = settings['depot-paths']
463                 change = settings['change']
465                 changed = False
467                 if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
468                                                            for p in depotPaths]))) == 0:
469                     print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
470                     system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
471                     continue
473                 while change and int(change) > maxChange:
474                     changed = True
475                     if self.verbose:
476                         print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
477                     system("git update-ref %s \"%s^\"" % (ref, ref))
478                     log = extractLogMessageFromGitCommit(ref)
479                     settings =  extractSettingsGitLog(log)
482                     depotPaths = settings['depot-paths']
483                     change = settings['change']
485                 if changed:
486                     print "%s rewound to %s" % (ref, change)
488         return True
490 class P4Submit(Command):
491     def __init__(self):
492         Command.__init__(self)
493         self.options = [
494                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
495                 optparse.make_option("--origin", dest="origin"),
496                 optparse.make_option("-M", dest="detectRename", action="store_true"),
497         ]
498         self.description = "Submit changes from git to the perforce depot."
499         self.usage += " [name of git branch to submit into perforce depot]"
500         self.interactive = True
501         self.origin = ""
502         self.detectRename = False
503         self.verbose = False
504         self.isWindows = (platform.system() == "Windows")
506     def check(self):
507         if len(p4CmdList("opened ...")) > 0:
508             die("You have files opened with perforce! Close them before starting the sync.")
510     # replaces everything between 'Description:' and the next P4 submit template field with the
511     # commit message
512     def prepareLogMessage(self, template, message):
513         result = ""
515         inDescriptionSection = False
517         for line in template.split("\n"):
518             if line.startswith("#"):
519                 result += line + "\n"
520                 continue
522             if inDescriptionSection:
523                 if line.startswith("Files:"):
524                     inDescriptionSection = False
525                 else:
526                     continue
527             else:
528                 if line.startswith("Description:"):
529                     inDescriptionSection = True
530                     line += "\n"
531                     for messageLine in message.split("\n"):
532                         line += "\t" + messageLine + "\n"
534             result += line + "\n"
536         return result
538     def prepareSubmitTemplate(self):
539         # remove lines in the Files section that show changes to files outside the depot path we're committing into
540         template = ""
541         inFilesSection = False
542         for line in p4_read_pipe_lines("change -o"):
543             if line.endswith("\r\n"):
544                 line = line[:-2] + "\n"
545             if inFilesSection:
546                 if line.startswith("\t"):
547                     # path starts and ends with a tab
548                     path = line[1:]
549                     lastTab = path.rfind("\t")
550                     if lastTab != -1:
551                         path = path[:lastTab]
552                         if not path.startswith(self.depotPath):
553                             continue
554                 else:
555                     inFilesSection = False
556             else:
557                 if line.startswith("Files:"):
558                     inFilesSection = True
560             template += line
562         return template
564     def applyCommit(self, id):
565         print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
566         diffOpts = ("", "-M")[self.detectRename]
567         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
568         filesToAdd = set()
569         filesToDelete = set()
570         editedFiles = set()
571         filesToChangeExecBit = {}
572         for line in diff:
573             diff = parseDiffTreeEntry(line)
574             modifier = diff['status']
575             path = diff['src']
576             if modifier == "M":
577                 p4_system("edit \"%s\"" % path)
578                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
579                     filesToChangeExecBit[path] = diff['dst_mode']
580                 editedFiles.add(path)
581             elif modifier == "A":
582                 filesToAdd.add(path)
583                 filesToChangeExecBit[path] = diff['dst_mode']
584                 if path in filesToDelete:
585                     filesToDelete.remove(path)
586             elif modifier == "D":
587                 filesToDelete.add(path)
588                 if path in filesToAdd:
589                     filesToAdd.remove(path)
590             elif modifier == "R":
591                 src, dest = diff['src'], diff['dst']
592                 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
593                 p4_system("edit \"%s\"" % (dest))
594                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
595                     filesToChangeExecBit[dest] = diff['dst_mode']
596                 os.unlink(dest)
597                 editedFiles.add(dest)
598                 filesToDelete.add(src)
599             else:
600                 die("unknown modifier %s for %s" % (modifier, path))
602         diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
603         patchcmd = diffcmd + " | git apply "
604         tryPatchCmd = patchcmd + "--check -"
605         applyPatchCmd = patchcmd + "--check --apply -"
607         if os.system(tryPatchCmd) != 0:
608             print "Unfortunately applying the change failed!"
609             print "What do you want to do?"
610             response = "x"
611             while response != "s" and response != "a" and response != "w":
612                 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
613                                      "and with .rej files / [w]rite the patch to a file (patch.txt) ")
614             if response == "s":
615                 print "Skipping! Good luck with the next patches..."
616                 for f in editedFiles:
617                     p4_system("revert \"%s\"" % f);
618                 for f in filesToAdd:
619                     system("rm %s" %f)
620                 return
621             elif response == "a":
622                 os.system(applyPatchCmd)
623                 if len(filesToAdd) > 0:
624                     print "You may also want to call p4 add on the following files:"
625                     print " ".join(filesToAdd)
626                 if len(filesToDelete):
627                     print "The following files should be scheduled for deletion with p4 delete:"
628                     print " ".join(filesToDelete)
629                 die("Please resolve and submit the conflict manually and "
630                     + "continue afterwards with git-p4 submit --continue")
631             elif response == "w":
632                 system(diffcmd + " > patch.txt")
633                 print "Patch saved to patch.txt in %s !" % self.clientPath
634                 die("Please resolve and submit the conflict manually and "
635                     "continue afterwards with git-p4 submit --continue")
637         system(applyPatchCmd)
639         for f in filesToAdd:
640             p4_system("add \"%s\"" % f)
641         for f in filesToDelete:
642             p4_system("revert \"%s\"" % f)
643             p4_system("delete \"%s\"" % f)
645         # Set/clear executable bits
646         for f in filesToChangeExecBit.keys():
647             mode = filesToChangeExecBit[f]
648             setP4ExecBit(f, mode)
650         logMessage = extractLogMessageFromGitCommit(id)
651         logMessage = logMessage.strip()
653         template = self.prepareSubmitTemplate()
655         if self.interactive:
656             submitTemplate = self.prepareLogMessage(template, logMessage)
657             if os.environ.has_key("P4DIFF"):
658                 del(os.environ["P4DIFF"])
659             diff = read_pipe("p4 diff -du ...")
661             newdiff = ""
662             for newFile in filesToAdd:
663                 newdiff += "==== new file ====\n"
664                 newdiff += "--- /dev/null\n"
665                 newdiff += "+++ %s\n" % newFile
666                 f = open(newFile, "r")
667                 for line in f.readlines():
668                     newdiff += "+" + line
669                 f.close()
671             separatorLine = "######## everything below this line is just the diff #######\n"
673             [handle, fileName] = tempfile.mkstemp()
674             tmpFile = os.fdopen(handle, "w+")
675             if self.isWindows:
676                 submitTemplate = submitTemplate.replace("\n", "\r\n")
677                 separatorLine = separatorLine.replace("\n", "\r\n")
678                 newdiff = newdiff.replace("\n", "\r\n")
679             tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
680             tmpFile.close()
681             defaultEditor = "vi"
682             if platform.system() == "Windows":
683                 defaultEditor = "notepad"
684             if os.environ.has_key("P4EDITOR"):
685                 editor = os.environ.get("P4EDITOR")
686             else:
687                 editor = os.environ.get("EDITOR", defaultEditor);
688             system(editor + " " + fileName)
689             tmpFile = open(fileName, "rb")
690             message = tmpFile.read()
691             tmpFile.close()
692             os.remove(fileName)
693             submitTemplate = message[:message.index(separatorLine)]
694             if self.isWindows:
695                 submitTemplate = submitTemplate.replace("\r\n", "\n")
697             write_pipe("p4 submit -i", submitTemplate)
698         else:
699             fileName = "submit.txt"
700             file = open(fileName, "w+")
701             file.write(self.prepareLogMessage(template, logMessage))
702             file.close()
703             print ("Perforce submit template written as %s. "
704                    + "Please review/edit and then use p4 submit -i < %s to submit directly!"
705                    % (fileName, fileName))
707     def run(self, args):
708         if len(args) == 0:
709             self.master = currentGitBranch()
710             if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
711                 die("Detecting current git branch failed!")
712         elif len(args) == 1:
713             self.master = args[0]
714         else:
715             return False
717         allowSubmit = gitConfig("git-p4.allowSubmit")
718         if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
719             die("%s is not in git-p4.allowSubmit" % self.master)
721         [upstream, settings] = findUpstreamBranchPoint()
722         self.depotPath = settings['depot-paths'][0]
723         if len(self.origin) == 0:
724             self.origin = upstream
726         if self.verbose:
727             print "Origin branch is " + self.origin
729         if len(self.depotPath) == 0:
730             print "Internal error: cannot locate perforce depot path from existing branches"
731             sys.exit(128)
733         self.clientPath = p4Where(self.depotPath)
735         if len(self.clientPath) == 0:
736             print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
737             sys.exit(128)
739         print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
740         self.oldWorkingDirectory = os.getcwd()
742         os.chdir(self.clientPath)
743         print "Syncronizing p4 checkout..."
744         p4_system("sync ...")
746         self.check()
748         commits = []
749         for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
750             commits.append(line.strip())
751         commits.reverse()
753         while len(commits) > 0:
754             commit = commits[0]
755             commits = commits[1:]
756             self.applyCommit(commit)
757             if not self.interactive:
758                 break
760         if len(commits) == 0:
761             print "All changes applied!"
762             os.chdir(self.oldWorkingDirectory)
764             sync = P4Sync()
765             sync.run([])
767             rebase = P4Rebase()
768             rebase.rebase()
770         return True
772 class P4Sync(Command):
773     def __init__(self):
774         Command.__init__(self)
775         self.options = [
776                 optparse.make_option("--branch", dest="branch"),
777                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
778                 optparse.make_option("--changesfile", dest="changesFile"),
779                 optparse.make_option("--silent", dest="silent", action="store_true"),
780                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
781                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
782                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
783                                      help="Import into refs/heads/ , not refs/remotes"),
784                 optparse.make_option("--max-changes", dest="maxChanges"),
785                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
786                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
787                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
788                                      help="Only sync files that are included in the Perforce Client Spec")
789         ]
790         self.description = """Imports from Perforce into a git repository.\n
791     example:
792     //depot/my/project/ -- to import the current head
793     //depot/my/project/@all -- to import everything
794     //depot/my/project/@1,6 -- to import only from revision 1 to 6
796     (a ... is not needed in the path p4 specification, it's added implicitly)"""
798         self.usage += " //depot/path[@revRange]"
799         self.silent = False
800         self.createdBranches = Set()
801         self.committedChanges = Set()
802         self.branch = ""
803         self.detectBranches = False
804         self.detectLabels = False
805         self.changesFile = ""
806         self.syncWithOrigin = True
807         self.verbose = False
808         self.importIntoRemotes = True
809         self.maxChanges = ""
810         self.isWindows = (platform.system() == "Windows")
811         self.keepRepoPath = False
812         self.depotPaths = None
813         self.p4BranchesInGit = []
814         self.cloneExclude = []
815         self.useClientSpec = False
816         self.clientSpecDirs = []
818         if gitConfig("git-p4.syncFromOrigin") == "false":
819             self.syncWithOrigin = False
821     def extractFilesFromCommit(self, commit):
822         self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
823                              for path in self.cloneExclude]
824         files = []
825         fnum = 0
826         while commit.has_key("depotFile%s" % fnum):
827             path =  commit["depotFile%s" % fnum]
829             if [p for p in self.cloneExclude
830                 if path.startswith (p)]:
831                 found = False
832             else:
833                 found = [p for p in self.depotPaths
834                          if path.startswith (p)]
835             if not found:
836                 fnum = fnum + 1
837                 continue
839             file = {}
840             file["path"] = path
841             file["rev"] = commit["rev%s" % fnum]
842             file["action"] = commit["action%s" % fnum]
843             file["type"] = commit["type%s" % fnum]
844             files.append(file)
845             fnum = fnum + 1
846         return files
848     def stripRepoPath(self, path, prefixes):
849         if self.keepRepoPath:
850             prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
852         for p in prefixes:
853             if path.startswith(p):
854                 path = path[len(p):]
856         return path
858     def splitFilesIntoBranches(self, commit):
859         branches = {}
860         fnum = 0
861         while commit.has_key("depotFile%s" % fnum):
862             path =  commit["depotFile%s" % fnum]
863             found = [p for p in self.depotPaths
864                      if path.startswith (p)]
865             if not found:
866                 fnum = fnum + 1
867                 continue
869             file = {}
870             file["path"] = path
871             file["rev"] = commit["rev%s" % fnum]
872             file["action"] = commit["action%s" % fnum]
873             file["type"] = commit["type%s" % fnum]
874             fnum = fnum + 1
876             relPath = self.stripRepoPath(path, self.depotPaths)
878             for branch in self.knownBranches.keys():
880                 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
881                 if relPath.startswith(branch + "/"):
882                     if branch not in branches:
883                         branches[branch] = []
884                     branches[branch].append(file)
885                     break
887         return branches
889     ## Should move this out, doesn't use SELF.
890     def readP4Files(self, files):
891         filesForCommit = []
892         filesToRead = []
894         for f in files:
895             includeFile = True
896             for val in self.clientSpecDirs:
897                 if f['path'].startswith(val[0]):
898                     if val[1] <= 0:
899                         includeFile = False
900                     break
902             if includeFile:
903                 filesForCommit.append(f)
904                 if f['action'] != 'delete':
905                     filesToRead.append(f)
907         filedata = []
908         if len(filesToRead) > 0:
909             filedata = p4CmdList('-x - print',
910                                  stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
911                                                   for f in filesToRead]),
912                                  stdin_mode='w+')
914             if "p4ExitCode" in filedata[0]:
915                 die("Problems executing p4. Error: [%d]."
916                     % (filedata[0]['p4ExitCode']));
918         j = 0;
919         contents = {}
920         while j < len(filedata):
921             stat = filedata[j]
922             j += 1
923             text = [];
924             while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
925                 text.append(filedata[j]['data'])
926                 j += 1
927             text = ''.join(text)
929             if not stat.has_key('depotFile'):
930                 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
931                 continue
933             if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
934                 text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
935             elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
936                 text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', text)
938             contents[stat['depotFile']] = text
940         for f in filesForCommit:
941             path = f['path']
942             if contents.has_key(path):
943                 f['data'] = contents[path]
945         return filesForCommit
947     def commit(self, details, files, branch, branchPrefixes, parent = ""):
948         epoch = details["time"]
949         author = details["user"]
951         if self.verbose:
952             print "commit into %s" % branch
954         # start with reading files; if that fails, we should not
955         # create a commit.
956         new_files = []
957         for f in files:
958             if [p for p in branchPrefixes if f['path'].startswith(p)]:
959                 new_files.append (f)
960             else:
961                 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
962         files = self.readP4Files(new_files)
964         self.gitStream.write("commit %s\n" % branch)
965 #        gitStream.write("mark :%s\n" % details["change"])
966         self.committedChanges.add(int(details["change"]))
967         committer = ""
968         if author not in self.users:
969             self.getUserMapFromPerforceServer()
970         if author in self.users:
971             committer = "%s %s %s" % (self.users[author], epoch, self.tz)
972         else:
973             committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
975         self.gitStream.write("committer %s\n" % committer)
977         self.gitStream.write("data <<EOT\n")
978         self.gitStream.write(details["desc"])
979         self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
980                              % (','.join (branchPrefixes), details["change"]))
981         if len(details['options']) > 0:
982             self.gitStream.write(": options = %s" % details['options'])
983         self.gitStream.write("]\nEOT\n\n")
985         if len(parent) > 0:
986             if self.verbose:
987                 print "parent %s" % parent
988             self.gitStream.write("from %s\n" % parent)
990         for file in files:
991             if file["type"] == "apple":
992                 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
993                 continue
995             relPath = self.stripRepoPath(file['path'], branchPrefixes)
996             if file["action"] == "delete":
997                 self.gitStream.write("D %s\n" % relPath)
998             else:
999                 data = file['data']
1001                 mode = "644"
1002                 if isP4Exec(file["type"]):
1003                     mode = "755"
1004                 elif file["type"] == "symlink":
1005                     mode = "120000"
1006                     # p4 print on a symlink contains "target\n", so strip it off
1007                     data = data[:-1]
1009                 if self.isWindows and file["type"].endswith("text"):
1010                     data = data.replace("\r\n", "\n")
1012                 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1013                 self.gitStream.write("data %s\n" % len(data))
1014                 self.gitStream.write(data)
1015                 self.gitStream.write("\n")
1017         self.gitStream.write("\n")
1019         change = int(details["change"])
1021         if self.labels.has_key(change):
1022             label = self.labels[change]
1023             labelDetails = label[0]
1024             labelRevisions = label[1]
1025             if self.verbose:
1026                 print "Change %s is labelled %s" % (change, labelDetails)
1028             files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1029                                                     for p in branchPrefixes]))
1031             if len(files) == len(labelRevisions):
1033                 cleanedFiles = {}
1034                 for info in files:
1035                     if info["action"] == "delete":
1036                         continue
1037                     cleanedFiles[info["depotFile"]] = info["rev"]
1039                 if cleanedFiles == labelRevisions:
1040                     self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1041                     self.gitStream.write("from %s\n" % branch)
1043                     owner = labelDetails["Owner"]
1044                     tagger = ""
1045                     if author in self.users:
1046                         tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1047                     else:
1048                         tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1049                     self.gitStream.write("tagger %s\n" % tagger)
1050                     self.gitStream.write("data <<EOT\n")
1051                     self.gitStream.write(labelDetails["Description"])
1052                     self.gitStream.write("EOT\n\n")
1054                 else:
1055                     if not self.silent:
1056                         print ("Tag %s does not match with change %s: files do not match."
1057                                % (labelDetails["label"], change))
1059             else:
1060                 if not self.silent:
1061                     print ("Tag %s does not match with change %s: file count is different."
1062                            % (labelDetails["label"], change))
1064     def getUserCacheFilename(self):
1065         home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1066         return home + "/.gitp4-usercache.txt"
1068     def getUserMapFromPerforceServer(self):
1069         if self.userMapFromPerforceServer:
1070             return
1071         self.users = {}
1073         for output in p4CmdList("users"):
1074             if not output.has_key("User"):
1075                 continue
1076             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1079         s = ''
1080         for (key, val) in self.users.items():
1081             s += "%s\t%s\n" % (key, val)
1083         open(self.getUserCacheFilename(), "wb").write(s)
1084         self.userMapFromPerforceServer = True
1086     def loadUserMapFromCache(self):
1087         self.users = {}
1088         self.userMapFromPerforceServer = False
1089         try:
1090             cache = open(self.getUserCacheFilename(), "rb")
1091             lines = cache.readlines()
1092             cache.close()
1093             for line in lines:
1094                 entry = line.strip().split("\t")
1095                 self.users[entry[0]] = entry[1]
1096         except IOError:
1097             self.getUserMapFromPerforceServer()
1099     def getLabels(self):
1100         self.labels = {}
1102         l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1103         if len(l) > 0 and not self.silent:
1104             print "Finding files belonging to labels in %s" % `self.depotPaths`
1106         for output in l:
1107             label = output["label"]
1108             revisions = {}
1109             newestChange = 0
1110             if self.verbose:
1111                 print "Querying files for label %s" % label
1112             for file in p4CmdList("files "
1113                                   +  ' '.join (["%s...@%s" % (p, label)
1114                                                 for p in self.depotPaths])):
1115                 revisions[file["depotFile"]] = file["rev"]
1116                 change = int(file["change"])
1117                 if change > newestChange:
1118                     newestChange = change
1120             self.labels[newestChange] = [output, revisions]
1122         if self.verbose:
1123             print "Label changes: %s" % self.labels.keys()
1125     def guessProjectName(self):
1126         for p in self.depotPaths:
1127             if p.endswith("/"):
1128                 p = p[:-1]
1129             p = p[p.strip().rfind("/") + 1:]
1130             if not p.endswith("/"):
1131                p += "/"
1132             return p
1134     def getBranchMapping(self):
1135         lostAndFoundBranches = set()
1137         for info in p4CmdList("branches"):
1138             details = p4Cmd("branch -o %s" % info["branch"])
1139             viewIdx = 0
1140             while details.has_key("View%s" % viewIdx):
1141                 paths = details["View%s" % viewIdx].split(" ")
1142                 viewIdx = viewIdx + 1
1143                 # require standard //depot/foo/... //depot/bar/... mapping
1144                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1145                     continue
1146                 source = paths[0]
1147                 destination = paths[1]
1148                 ## HACK
1149                 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1150                     source = source[len(self.depotPaths[0]):-4]
1151                     destination = destination[len(self.depotPaths[0]):-4]
1153                     if destination in self.knownBranches:
1154                         if not self.silent:
1155                             print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1156                             print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1157                         continue
1159                     self.knownBranches[destination] = source
1161                     lostAndFoundBranches.discard(destination)
1163                     if source not in self.knownBranches:
1164                         lostAndFoundBranches.add(source)
1167         for branch in lostAndFoundBranches:
1168             self.knownBranches[branch] = branch
1170     def getBranchMappingFromGitBranches(self):
1171         branches = p4BranchesInGit(self.importIntoRemotes)
1172         for branch in branches.keys():
1173             if branch == "master":
1174                 branch = "main"
1175             else:
1176                 branch = branch[len(self.projectName):]
1177             self.knownBranches[branch] = branch
1179     def listExistingP4GitBranches(self):
1180         # branches holds mapping from name to commit
1181         branches = p4BranchesInGit(self.importIntoRemotes)
1182         self.p4BranchesInGit = branches.keys()
1183         for branch in branches.keys():
1184             self.initialParents[self.refPrefix + branch] = branches[branch]
1186     def updateOptionDict(self, d):
1187         option_keys = {}
1188         if self.keepRepoPath:
1189             option_keys['keepRepoPath'] = 1
1191         d["options"] = ' '.join(sorted(option_keys.keys()))
1193     def readOptions(self, d):
1194         self.keepRepoPath = (d.has_key('options')
1195                              and ('keepRepoPath' in d['options']))
1197     def gitRefForBranch(self, branch):
1198         if branch == "main":
1199             return self.refPrefix + "master"
1201         if len(branch) <= 0:
1202             return branch
1204         return self.refPrefix + self.projectName + branch
1206     def gitCommitByP4Change(self, ref, change):
1207         if self.verbose:
1208             print "looking in ref " + ref + " for change %s using bisect..." % change
1210         earliestCommit = ""
1211         latestCommit = parseRevision(ref)
1213         while True:
1214             if self.verbose:
1215                 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1216             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1217             if len(next) == 0:
1218                 if self.verbose:
1219                     print "argh"
1220                 return ""
1221             log = extractLogMessageFromGitCommit(next)
1222             settings = extractSettingsGitLog(log)
1223             currentChange = int(settings['change'])
1224             if self.verbose:
1225                 print "current change %s" % currentChange
1227             if currentChange == change:
1228                 if self.verbose:
1229                     print "found %s" % next
1230                 return next
1232             if currentChange < change:
1233                 earliestCommit = "^%s" % next
1234             else:
1235                 latestCommit = "%s" % next
1237         return ""
1239     def importNewBranch(self, branch, maxChange):
1240         # make fast-import flush all changes to disk and update the refs using the checkpoint
1241         # command so that we can try to find the branch parent in the git history
1242         self.gitStream.write("checkpoint\n\n");
1243         self.gitStream.flush();
1244         branchPrefix = self.depotPaths[0] + branch + "/"
1245         range = "@1,%s" % maxChange
1246         #print "prefix" + branchPrefix
1247         changes = p4ChangesForPaths([branchPrefix], range)
1248         if len(changes) <= 0:
1249             return False
1250         firstChange = changes[0]
1251         #print "first change in branch: %s" % firstChange
1252         sourceBranch = self.knownBranches[branch]
1253         sourceDepotPath = self.depotPaths[0] + sourceBranch
1254         sourceRef = self.gitRefForBranch(sourceBranch)
1255         #print "source " + sourceBranch
1257         branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1258         #print "branch parent: %s" % branchParentChange
1259         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1260         if len(gitParent) > 0:
1261             self.initialParents[self.gitRefForBranch(branch)] = gitParent
1262             #print "parent git commit: %s" % gitParent
1264         self.importChanges(changes)
1265         return True
1267     def importChanges(self, changes):
1268         cnt = 1
1269         for change in changes:
1270             description = p4Cmd("describe %s" % change)
1271             self.updateOptionDict(description)
1273             if not self.silent:
1274                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1275                 sys.stdout.flush()
1276             cnt = cnt + 1
1278             try:
1279                 if self.detectBranches:
1280                     branches = self.splitFilesIntoBranches(description)
1281                     for branch in branches.keys():
1282                         ## HACK  --hwn
1283                         branchPrefix = self.depotPaths[0] + branch + "/"
1285                         parent = ""
1287                         filesForCommit = branches[branch]
1289                         if self.verbose:
1290                             print "branch is %s" % branch
1292                         self.updatedBranches.add(branch)
1294                         if branch not in self.createdBranches:
1295                             self.createdBranches.add(branch)
1296                             parent = self.knownBranches[branch]
1297                             if parent == branch:
1298                                 parent = ""
1299                             else:
1300                                 fullBranch = self.projectName + branch
1301                                 if fullBranch not in self.p4BranchesInGit:
1302                                     if not self.silent:
1303                                         print("\n    Importing new branch %s" % fullBranch);
1304                                     if self.importNewBranch(branch, change - 1):
1305                                         parent = ""
1306                                         self.p4BranchesInGit.append(fullBranch)
1307                                     if not self.silent:
1308                                         print("\n    Resuming with change %s" % change);
1310                                 if self.verbose:
1311                                     print "parent determined through known branches: %s" % parent
1313                         branch = self.gitRefForBranch(branch)
1314                         parent = self.gitRefForBranch(parent)
1316                         if self.verbose:
1317                             print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1319                         if len(parent) == 0 and branch in self.initialParents:
1320                             parent = self.initialParents[branch]
1321                             del self.initialParents[branch]
1323                         self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1324                 else:
1325                     files = self.extractFilesFromCommit(description)
1326                     self.commit(description, files, self.branch, self.depotPaths,
1327                                 self.initialParent)
1328                     self.initialParent = ""
1329             except IOError:
1330                 print self.gitError.read()
1331                 sys.exit(1)
1333     def importHeadRevision(self, revision):
1334         print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1336         details = { "user" : "git perforce import user", "time" : int(time.time()) }
1337         details["desc"] = ("Initial import of %s from the state at revision %s"
1338                            % (' '.join(self.depotPaths), revision))
1339         details["change"] = revision
1340         newestRevision = 0
1342         fileCnt = 0
1343         for info in p4CmdList("files "
1344                               +  ' '.join(["%s...%s"
1345                                            % (p, revision)
1346                                            for p in self.depotPaths])):
1348             if info['code'] == 'error':
1349                 sys.stderr.write("p4 returned an error: %s\n"
1350                                  % info['data'])
1351                 sys.exit(1)
1354             change = int(info["change"])
1355             if change > newestRevision:
1356                 newestRevision = change
1358             if info["action"] == "delete":
1359                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1360                 #fileCnt = fileCnt + 1
1361                 continue
1363             for prop in ["depotFile", "rev", "action", "type" ]:
1364                 details["%s%s" % (prop, fileCnt)] = info[prop]
1366             fileCnt = fileCnt + 1
1368         details["change"] = newestRevision
1369         self.updateOptionDict(details)
1370         try:
1371             self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1372         except IOError:
1373             print "IO error with git fast-import. Is your git version recent enough?"
1374             print self.gitError.read()
1377     def getClientSpec(self):
1378         specList = p4CmdList( "client -o" )
1379         temp = {}
1380         for entry in specList:
1381             for k,v in entry.iteritems():
1382                 if k.startswith("View"):
1383                     if v.startswith('"'):
1384                         start = 1
1385                     else:
1386                         start = 0
1387                     index = v.find("...")
1388                     v = v[start:index]
1389                     if v.startswith("-"):
1390                         v = v[1:]
1391                         temp[v] = -len(v)
1392                     else:
1393                         temp[v] = len(v)
1394         self.clientSpecDirs = temp.items()
1395         self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1397     def run(self, args):
1398         self.depotPaths = []
1399         self.changeRange = ""
1400         self.initialParent = ""
1401         self.previousDepotPaths = []
1403         # map from branch depot path to parent branch
1404         self.knownBranches = {}
1405         self.initialParents = {}
1406         self.hasOrigin = originP4BranchesExist()
1407         if not self.syncWithOrigin:
1408             self.hasOrigin = False
1410         if self.importIntoRemotes:
1411             self.refPrefix = "refs/remotes/p4/"
1412         else:
1413             self.refPrefix = "refs/heads/p4/"
1415         if self.syncWithOrigin and self.hasOrigin:
1416             if not self.silent:
1417                 print "Syncing with origin first by calling git fetch origin"
1418             system("git fetch origin")
1420         if len(self.branch) == 0:
1421             self.branch = self.refPrefix + "master"
1422             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1423                 system("git update-ref %s refs/heads/p4" % self.branch)
1424                 system("git branch -D p4");
1425             # create it /after/ importing, when master exists
1426             if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1427                 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1429         if self.useClientSpec or gitConfig("p4.useclientspec") == "true":
1430             self.getClientSpec()
1432         # TODO: should always look at previous commits,
1433         # merge with previous imports, if possible.
1434         if args == []:
1435             if self.hasOrigin:
1436                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1437             self.listExistingP4GitBranches()
1439             if len(self.p4BranchesInGit) > 1:
1440                 if not self.silent:
1441                     print "Importing from/into multiple branches"
1442                 self.detectBranches = True
1444             if self.verbose:
1445                 print "branches: %s" % self.p4BranchesInGit
1447             p4Change = 0
1448             for branch in self.p4BranchesInGit:
1449                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1451                 settings = extractSettingsGitLog(logMsg)
1453                 self.readOptions(settings)
1454                 if (settings.has_key('depot-paths')
1455                     and settings.has_key ('change')):
1456                     change = int(settings['change']) + 1
1457                     p4Change = max(p4Change, change)
1459                     depotPaths = sorted(settings['depot-paths'])
1460                     if self.previousDepotPaths == []:
1461                         self.previousDepotPaths = depotPaths
1462                     else:
1463                         paths = []
1464                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1465                             for i in range(0, min(len(cur), len(prev))):
1466                                 if cur[i] <> prev[i]:
1467                                     i = i - 1
1468                                     break
1470                             paths.append (cur[:i + 1])
1472                         self.previousDepotPaths = paths
1474             if p4Change > 0:
1475                 self.depotPaths = sorted(self.previousDepotPaths)
1476                 self.changeRange = "@%s,#head" % p4Change
1477                 if not self.detectBranches:
1478                     self.initialParent = parseRevision(self.branch)
1479                 if not self.silent and not self.detectBranches:
1480                     print "Performing incremental import into %s git branch" % self.branch
1482         if not self.branch.startswith("refs/"):
1483             self.branch = "refs/heads/" + self.branch
1485         if len(args) == 0 and self.depotPaths:
1486             if not self.silent:
1487                 print "Depot paths: %s" % ' '.join(self.depotPaths)
1488         else:
1489             if self.depotPaths and self.depotPaths != args:
1490                 print ("previous import used depot path %s and now %s was specified. "
1491                        "This doesn't work!" % (' '.join (self.depotPaths),
1492                                                ' '.join (args)))
1493                 sys.exit(1)
1495             self.depotPaths = sorted(args)
1497         revision = ""
1498         self.users = {}
1500         newPaths = []
1501         for p in self.depotPaths:
1502             if p.find("@") != -1:
1503                 atIdx = p.index("@")
1504                 self.changeRange = p[atIdx:]
1505                 if self.changeRange == "@all":
1506                     self.changeRange = ""
1507                 elif ',' not in self.changeRange:
1508                     revision = self.changeRange
1509                     self.changeRange = ""
1510                 p = p[:atIdx]
1511             elif p.find("#") != -1:
1512                 hashIdx = p.index("#")
1513                 revision = p[hashIdx:]
1514                 p = p[:hashIdx]
1515             elif self.previousDepotPaths == []:
1516                 revision = "#head"
1518             p = re.sub ("\.\.\.$", "", p)
1519             if not p.endswith("/"):
1520                 p += "/"
1522             newPaths.append(p)
1524         self.depotPaths = newPaths
1527         self.loadUserMapFromCache()
1528         self.labels = {}
1529         if self.detectLabels:
1530             self.getLabels();
1532         if self.detectBranches:
1533             ## FIXME - what's a P4 projectName ?
1534             self.projectName = self.guessProjectName()
1536             if self.hasOrigin:
1537                 self.getBranchMappingFromGitBranches()
1538             else:
1539                 self.getBranchMapping()
1540             if self.verbose:
1541                 print "p4-git branches: %s" % self.p4BranchesInGit
1542                 print "initial parents: %s" % self.initialParents
1543             for b in self.p4BranchesInGit:
1544                 if b != "master":
1546                     ## FIXME
1547                     b = b[len(self.projectName):]
1548                 self.createdBranches.add(b)
1550         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1552         importProcess = subprocess.Popen(["git", "fast-import"],
1553                                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1554                                          stderr=subprocess.PIPE);
1555         self.gitOutput = importProcess.stdout
1556         self.gitStream = importProcess.stdin
1557         self.gitError = importProcess.stderr
1559         if revision:
1560             self.importHeadRevision(revision)
1561         else:
1562             changes = []
1564             if len(self.changesFile) > 0:
1565                 output = open(self.changesFile).readlines()
1566                 changeSet = Set()
1567                 for line in output:
1568                     changeSet.add(int(line))
1570                 for change in changeSet:
1571                     changes.append(change)
1573                 changes.sort()
1574             else:
1575                 if self.verbose:
1576                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1577                                                               self.changeRange)
1578                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1580                 if len(self.maxChanges) > 0:
1581                     changes = changes[:min(int(self.maxChanges), len(changes))]
1583             if len(changes) == 0:
1584                 if not self.silent:
1585                     print "No changes to import!"
1586                 return True
1588             if not self.silent and not self.detectBranches:
1589                 print "Import destination: %s" % self.branch
1591             self.updatedBranches = set()
1593             self.importChanges(changes)
1595             if not self.silent:
1596                 print ""
1597                 if len(self.updatedBranches) > 0:
1598                     sys.stdout.write("Updated branches: ")
1599                     for b in self.updatedBranches:
1600                         sys.stdout.write("%s " % b)
1601                     sys.stdout.write("\n")
1603         self.gitStream.close()
1604         if importProcess.wait() != 0:
1605             die("fast-import failed: %s" % self.gitError.read())
1606         self.gitOutput.close()
1607         self.gitError.close()
1609         return True
1611 class P4Rebase(Command):
1612     def __init__(self):
1613         Command.__init__(self)
1614         self.options = [ ]
1615         self.description = ("Fetches the latest revision from perforce and "
1616                             + "rebases the current work (branch) against it")
1617         self.verbose = False
1619     def run(self, args):
1620         sync = P4Sync()
1621         sync.run([])
1623         return self.rebase()
1625     def rebase(self):
1626         if os.system("git update-index --refresh") != 0:
1627             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.");
1628         if len(read_pipe("git diff-index HEAD --")) > 0:
1629             die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1631         [upstream, settings] = findUpstreamBranchPoint()
1632         if len(upstream) == 0:
1633             die("Cannot find upstream branchpoint for rebase")
1635         # the branchpoint may be p4/foo~3, so strip off the parent
1636         upstream = re.sub("~[0-9]+$", "", upstream)
1638         print "Rebasing the current branch onto %s" % upstream
1639         oldHead = read_pipe("git rev-parse HEAD").strip()
1640         system("git rebase %s" % upstream)
1641         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1642         return True
1644 class P4Clone(P4Sync):
1645     def __init__(self):
1646         P4Sync.__init__(self)
1647         self.description = "Creates a new git repository and imports from Perforce into it"
1648         self.usage = "usage: %prog [options] //depot/path[@revRange]"
1649         self.options += [
1650             optparse.make_option("--destination", dest="cloneDestination",
1651                                  action='store', default=None,
1652                                  help="where to leave result of the clone"),
1653             optparse.make_option("-/", dest="cloneExclude",
1654                                  action="append", type="string",
1655                                  help="exclude depot path")
1656         ]
1657         self.cloneDestination = None
1658         self.needsGit = False
1660     # This is required for the "append" cloneExclude action
1661     def ensure_value(self, attr, value):
1662         if not hasattr(self, attr) or getattr(self, attr) is None:
1663             setattr(self, attr, value)
1664         return getattr(self, attr)
1666     def defaultDestination(self, args):
1667         ## TODO: use common prefix of args?
1668         depotPath = args[0]
1669         depotDir = re.sub("(@[^@]*)$", "", depotPath)
1670         depotDir = re.sub("(#[^#]*)$", "", depotDir)
1671         depotDir = re.sub(r"\.\.\.$", "", depotDir)
1672         depotDir = re.sub(r"/$", "", depotDir)
1673         return os.path.split(depotDir)[1]
1675     def run(self, args):
1676         if len(args) < 1:
1677             return False
1679         if self.keepRepoPath and not self.cloneDestination:
1680             sys.stderr.write("Must specify destination for --keep-path\n")
1681             sys.exit(1)
1683         depotPaths = args
1685         if not self.cloneDestination and len(depotPaths) > 1:
1686             self.cloneDestination = depotPaths[-1]
1687             depotPaths = depotPaths[:-1]
1689         self.cloneExclude = ["/"+p for p in self.cloneExclude]
1690         for p in depotPaths:
1691             if not p.startswith("//"):
1692                 return False
1694         if not self.cloneDestination:
1695             self.cloneDestination = self.defaultDestination(args)
1697         print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1698         if not os.path.exists(self.cloneDestination):
1699             os.makedirs(self.cloneDestination)
1700         os.chdir(self.cloneDestination)
1701         system("git init")
1702         self.gitdir = os.getcwd() + "/.git"
1703         if not P4Sync.run(self, depotPaths):
1704             return False
1705         if self.branch != "master":
1706             if gitBranchExists("refs/remotes/p4/master"):
1707                 system("git branch master refs/remotes/p4/master")
1708                 system("git checkout -f")
1709             else:
1710                 print "Could not detect main branch. No checkout/master branch created."
1712         return True
1714 class P4Branches(Command):
1715     def __init__(self):
1716         Command.__init__(self)
1717         self.options = [ ]
1718         self.description = ("Shows the git branches that hold imports and their "
1719                             + "corresponding perforce depot paths")
1720         self.verbose = False
1722     def run(self, args):
1723         if originP4BranchesExist():
1724             createOrUpdateBranchesFromOrigin()
1726         cmdline = "git rev-parse --symbolic "
1727         cmdline += " --remotes"
1729         for line in read_pipe_lines(cmdline):
1730             line = line.strip()
1732             if not line.startswith('p4/') or line == "p4/HEAD":
1733                 continue
1734             branch = line
1736             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1737             settings = extractSettingsGitLog(log)
1739             print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1740         return True
1742 class HelpFormatter(optparse.IndentedHelpFormatter):
1743     def __init__(self):
1744         optparse.IndentedHelpFormatter.__init__(self)
1746     def format_description(self, description):
1747         if description:
1748             return description + "\n"
1749         else:
1750             return ""
1752 def printUsage(commands):
1753     print "usage: %s <command> [options]" % sys.argv[0]
1754     print ""
1755     print "valid commands: %s" % ", ".join(commands)
1756     print ""
1757     print "Try %s <command> --help for command specific help." % sys.argv[0]
1758     print ""
1760 commands = {
1761     "debug" : P4Debug,
1762     "submit" : P4Submit,
1763     "commit" : P4Submit,
1764     "sync" : P4Sync,
1765     "rebase" : P4Rebase,
1766     "clone" : P4Clone,
1767     "rollback" : P4RollBack,
1768     "branches" : P4Branches
1772 def main():
1773     if len(sys.argv[1:]) == 0:
1774         printUsage(commands.keys())
1775         sys.exit(2)
1777     cmd = ""
1778     cmdName = sys.argv[1]
1779     try:
1780         klass = commands[cmdName]
1781         cmd = klass()
1782     except KeyError:
1783         print "unknown command %s" % cmdName
1784         print ""
1785         printUsage(commands.keys())
1786         sys.exit(2)
1788     options = cmd.options
1789     cmd.gitdir = os.environ.get("GIT_DIR", None)
1791     args = sys.argv[2:]
1793     if len(options) > 0:
1794         options.append(optparse.make_option("--git-dir", dest="gitdir"))
1796         parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1797                                        options,
1798                                        description = cmd.description,
1799                                        formatter = HelpFormatter())
1801         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1802     global verbose
1803     verbose = cmd.verbose
1804     if cmd.needsGit:
1805         if cmd.gitdir == None:
1806             cmd.gitdir = os.path.abspath(".git")
1807             if not isValidGitDir(cmd.gitdir):
1808                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1809                 if os.path.exists(cmd.gitdir):
1810                     cdup = read_pipe("git rev-parse --show-cdup").strip()
1811                     if len(cdup) > 0:
1812                         os.chdir(cdup);
1814         if not isValidGitDir(cmd.gitdir):
1815             if isValidGitDir(cmd.gitdir + "/.git"):
1816                 cmd.gitdir += "/.git"
1817             else:
1818                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1820         os.environ["GIT_DIR"] = cmd.gitdir
1822     if not cmd.run(args):
1823         parser.print_help()
1826 if __name__ == '__main__':
1827     main()