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, subprocess, shelve
12 import tempfile, getopt, os.path, time, platform
13 import re
15 verbose = False
18 def p4_build_cmd(cmd):
19     """Build a suitable p4 command line.
21     This consolidates building and returning a p4 command line into one
22     location. It means that hooking into the environment, or other configuration
23     can be done more easily.
24     """
25     real_cmd = "%s " % "p4"
27     user = gitConfig("git-p4.user")
28     if len(user) > 0:
29         real_cmd += "-u %s " % user
31     password = gitConfig("git-p4.password")
32     if len(password) > 0:
33         real_cmd += "-P %s " % password
35     port = gitConfig("git-p4.port")
36     if len(port) > 0:
37         real_cmd += "-p %s " % port
39     host = gitConfig("git-p4.host")
40     if len(host) > 0:
41         real_cmd += "-h %s " % host
43     client = gitConfig("git-p4.client")
44     if len(client) > 0:
45         real_cmd += "-c %s " % client
47     real_cmd += "%s" % (cmd)
48     if verbose:
49         print real_cmd
50     return real_cmd
52 def chdir(dir):
53     if os.name == 'nt':
54         os.environ['PWD']=dir
55     os.chdir(dir)
57 def die(msg):
58     if verbose:
59         raise Exception(msg)
60     else:
61         sys.stderr.write(msg + "\n")
62         sys.exit(1)
64 def write_pipe(c, str):
65     if verbose:
66         sys.stderr.write('Writing pipe: %s\n' % c)
68     pipe = os.popen(c, 'w')
69     val = pipe.write(str)
70     if pipe.close():
71         die('Command failed: %s' % c)
73     return val
75 def p4_write_pipe(c, str):
76     real_cmd = p4_build_cmd(c)
77     return write_pipe(real_cmd, str)
79 def read_pipe(c, ignore_error=False):
80     if verbose:
81         sys.stderr.write('Reading pipe: %s\n' % c)
83     pipe = os.popen(c, 'rb')
84     val = pipe.read()
85     if pipe.close() and not ignore_error:
86         die('Command failed: %s' % c)
88     return val
90 def p4_read_pipe(c, ignore_error=False):
91     real_cmd = p4_build_cmd(c)
92     return read_pipe(real_cmd, ignore_error)
94 def read_pipe_lines(c):
95     if verbose:
96         sys.stderr.write('Reading pipe: %s\n' % c)
97     ## todo: check return status
98     pipe = os.popen(c, 'rb')
99     val = pipe.readlines()
100     if pipe.close():
101         die('Command failed: %s' % c)
103     return val
105 def p4_read_pipe_lines(c):
106     """Specifically invoke p4 on the command supplied. """
107     real_cmd = p4_build_cmd(c)
108     return read_pipe_lines(real_cmd)
110 def system(cmd):
111     if verbose:
112         sys.stderr.write("executing %s\n" % cmd)
113     if os.system(cmd) != 0:
114         die("command failed: %s" % cmd)
116 def p4_system(cmd):
117     """Specifically invoke p4 as the system command. """
118     real_cmd = p4_build_cmd(cmd)
119     return system(real_cmd)
121 def isP4Exec(kind):
122     """Determine if a Perforce 'kind' should have execute permission
124     'p4 help filetypes' gives a list of the types.  If it starts with 'x',
125     or x follows one of a few letters.  Otherwise, if there is an 'x' after
126     a plus sign, it is also executable"""
127     return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
129 def setP4ExecBit(file, mode):
130     # Reopens an already open file and changes the execute bit to match
131     # the execute bit setting in the passed in mode.
133     p4Type = "+x"
135     if not isModeExec(mode):
136         p4Type = getP4OpenedType(file)
137         p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
138         p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
139         if p4Type[-1] == "+":
140             p4Type = p4Type[0:-1]
142     p4_system("reopen -t %s %s" % (p4Type, file))
144 def getP4OpenedType(file):
145     # Returns the perforce file type for the given file.
147     result = p4_read_pipe("opened %s" % file)
148     match = re.match(".*\((.+)\)\r?$", result)
149     if match:
150         return match.group(1)
151     else:
152         die("Could not determine file type for %s (result: '%s')" % (file, result))
154 def diffTreePattern():
155     # This is a simple generator for the diff tree regex pattern. This could be
156     # a class variable if this and parseDiffTreeEntry were a part of a class.
157     pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
158     while True:
159         yield pattern
161 def parseDiffTreeEntry(entry):
162     """Parses a single diff tree entry into its component elements.
164     See git-diff-tree(1) manpage for details about the format of the diff
165     output. This method returns a dictionary with the following elements:
167     src_mode - The mode of the source file
168     dst_mode - The mode of the destination file
169     src_sha1 - The sha1 for the source file
170     dst_sha1 - The sha1 fr the destination file
171     status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
172     status_score - The score for the status (applicable for 'C' and 'R'
173                    statuses). This is None if there is no score.
174     src - The path for the source file.
175     dst - The path for the destination file. This is only present for
176           copy or renames. If it is not present, this is None.
178     If the pattern is not matched, None is returned."""
180     match = diffTreePattern().next().match(entry)
181     if match:
182         return {
183             'src_mode': match.group(1),
184             'dst_mode': match.group(2),
185             'src_sha1': match.group(3),
186             'dst_sha1': match.group(4),
187             'status': match.group(5),
188             'status_score': match.group(6),
189             'src': match.group(7),
190             'dst': match.group(10)
191         }
192     return None
194 def isModeExec(mode):
195     # Returns True if the given git mode represents an executable file,
196     # otherwise False.
197     return mode[-3:] == "755"
199 def isModeExecChanged(src_mode, dst_mode):
200     return isModeExec(src_mode) != isModeExec(dst_mode)
202 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
203     cmd = p4_build_cmd("-G %s" % (cmd))
204     if verbose:
205         sys.stderr.write("Opening pipe: %s\n" % cmd)
207     # Use a temporary file to avoid deadlocks without
208     # subprocess.communicate(), which would put another copy
209     # of stdout into memory.
210     stdin_file = None
211     if stdin is not None:
212         stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
213         stdin_file.write(stdin)
214         stdin_file.flush()
215         stdin_file.seek(0)
217     p4 = subprocess.Popen(cmd, shell=True,
218                           stdin=stdin_file,
219                           stdout=subprocess.PIPE)
221     result = []
222     try:
223         while True:
224             entry = marshal.load(p4.stdout)
225             if cb is not None:
226                 cb(entry)
227             else:
228                 result.append(entry)
229     except EOFError:
230         pass
231     exitCode = p4.wait()
232     if exitCode != 0:
233         entry = {}
234         entry["p4ExitCode"] = exitCode
235         result.append(entry)
237     return result
239 def p4Cmd(cmd):
240     list = p4CmdList(cmd)
241     result = {}
242     for entry in list:
243         result.update(entry)
244     return result;
246 def p4Where(depotPath):
247     if not depotPath.endswith("/"):
248         depotPath += "/"
249     depotPath = depotPath + "..."
250     outputList = p4CmdList("where %s" % depotPath)
251     output = None
252     for entry in outputList:
253         if "depotFile" in entry:
254             if entry["depotFile"] == depotPath:
255                 output = entry
256                 break
257         elif "data" in entry:
258             data = entry.get("data")
259             space = data.find(" ")
260             if data[:space] == depotPath:
261                 output = entry
262                 break
263     if output == None:
264         return ""
265     if output["code"] == "error":
266         return ""
267     clientPath = ""
268     if "path" in output:
269         clientPath = output.get("path")
270     elif "data" in output:
271         data = output.get("data")
272         lastSpace = data.rfind(" ")
273         clientPath = data[lastSpace + 1:]
275     if clientPath.endswith("..."):
276         clientPath = clientPath[:-3]
277     return clientPath
279 def currentGitBranch():
280     return read_pipe("git name-rev HEAD").split(" ")[1].strip()
282 def isValidGitDir(path):
283     if (os.path.exists(path + "/HEAD")
284         and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
285         return True;
286     return False
288 def parseRevision(ref):
289     return read_pipe("git rev-parse %s" % ref).strip()
291 def extractLogMessageFromGitCommit(commit):
292     logMessage = ""
294     ## fixme: title is first line of commit, not 1st paragraph.
295     foundTitle = False
296     for log in read_pipe_lines("git cat-file commit %s" % commit):
297        if not foundTitle:
298            if len(log) == 1:
299                foundTitle = True
300            continue
302        logMessage += log
303     return logMessage
305 def extractSettingsGitLog(log):
306     values = {}
307     for line in log.split("\n"):
308         line = line.strip()
309         m = re.search (r"^ *\[git-p4: (.*)\]$", line)
310         if not m:
311             continue
313         assignments = m.group(1).split (':')
314         for a in assignments:
315             vals = a.split ('=')
316             key = vals[0].strip()
317             val = ('='.join (vals[1:])).strip()
318             if val.endswith ('\"') and val.startswith('"'):
319                 val = val[1:-1]
321             values[key] = val
323     paths = values.get("depot-paths")
324     if not paths:
325         paths = values.get("depot-path")
326     if paths:
327         values['depot-paths'] = paths.split(',')
328     return values
330 def gitBranchExists(branch):
331     proc = subprocess.Popen(["git", "rev-parse", branch],
332                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);
333     return proc.wait() == 0;
335 _gitConfig = {}
336 def gitConfig(key):
337     if not _gitConfig.has_key(key):
338         _gitConfig[key] = read_pipe("git config %s" % key, ignore_error=True).strip()
339     return _gitConfig[key]
341 def p4BranchesInGit(branchesAreInRemotes = True):
342     branches = {}
344     cmdline = "git rev-parse --symbolic "
345     if branchesAreInRemotes:
346         cmdline += " --remotes"
347     else:
348         cmdline += " --branches"
350     for line in read_pipe_lines(cmdline):
351         line = line.strip()
353         ## only import to p4/
354         if not line.startswith('p4/') or line == "p4/HEAD":
355             continue
356         branch = line
358         # strip off p4
359         branch = re.sub ("^p4/", "", line)
361         branches[branch] = parseRevision(line)
362     return branches
364 def findUpstreamBranchPoint(head = "HEAD"):
365     branches = p4BranchesInGit()
366     # map from depot-path to branch name
367     branchByDepotPath = {}
368     for branch in branches.keys():
369         tip = branches[branch]
370         log = extractLogMessageFromGitCommit(tip)
371         settings = extractSettingsGitLog(log)
372         if settings.has_key("depot-paths"):
373             paths = ",".join(settings["depot-paths"])
374             branchByDepotPath[paths] = "remotes/p4/" + branch
376     settings = None
377     parent = 0
378     while parent < 65535:
379         commit = head + "~%s" % parent
380         log = extractLogMessageFromGitCommit(commit)
381         settings = extractSettingsGitLog(log)
382         if settings.has_key("depot-paths"):
383             paths = ",".join(settings["depot-paths"])
384             if branchByDepotPath.has_key(paths):
385                 return [branchByDepotPath[paths], settings]
387         parent = parent + 1
389     return ["", settings]
391 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
392     if not silent:
393         print ("Creating/updating branch(es) in %s based on origin branch(es)"
394                % localRefPrefix)
396     originPrefix = "origin/p4/"
398     for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
399         line = line.strip()
400         if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
401             continue
403         headName = line[len(originPrefix):]
404         remoteHead = localRefPrefix + headName
405         originHead = line
407         original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
408         if (not original.has_key('depot-paths')
409             or not original.has_key('change')):
410             continue
412         update = False
413         if not gitBranchExists(remoteHead):
414             if verbose:
415                 print "creating %s" % remoteHead
416             update = True
417         else:
418             settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
419             if settings.has_key('change') > 0:
420                 if settings['depot-paths'] == original['depot-paths']:
421                     originP4Change = int(original['change'])
422                     p4Change = int(settings['change'])
423                     if originP4Change > p4Change:
424                         print ("%s (%s) is newer than %s (%s). "
425                                "Updating p4 branch from origin."
426                                % (originHead, originP4Change,
427                                   remoteHead, p4Change))
428                         update = True
429                 else:
430                     print ("Ignoring: %s was imported from %s while "
431                            "%s was imported from %s"
432                            % (originHead, ','.join(original['depot-paths']),
433                               remoteHead, ','.join(settings['depot-paths'])))
435         if update:
436             system("git update-ref %s %s" % (remoteHead, originHead))
438 def originP4BranchesExist():
439         return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
441 def p4ChangesForPaths(depotPaths, changeRange):
442     assert depotPaths
443     output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
444                                                         for p in depotPaths]))
446     changes = {}
447     for line in output:
448         changeNum = int(line.split(" ")[1])
449         changes[changeNum] = True
451     changelist = changes.keys()
452     changelist.sort()
453     return changelist
455 class Command:
456     def __init__(self):
457         self.usage = "usage: %prog [options]"
458         self.needsGit = True
460 class P4Debug(Command):
461     def __init__(self):
462         Command.__init__(self)
463         self.options = [
464             optparse.make_option("--verbose", dest="verbose", action="store_true",
465                                  default=False),
466             ]
467         self.description = "A tool to debug the output of p4 -G."
468         self.needsGit = False
469         self.verbose = False
471     def run(self, args):
472         j = 0
473         for output in p4CmdList(" ".join(args)):
474             print 'Element: %d' % j
475             j += 1
476             print output
477         return True
479 class P4RollBack(Command):
480     def __init__(self):
481         Command.__init__(self)
482         self.options = [
483             optparse.make_option("--verbose", dest="verbose", action="store_true"),
484             optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
485         ]
486         self.description = "A tool to debug the multi-branch import. Don't use :)"
487         self.verbose = False
488         self.rollbackLocalBranches = False
490     def run(self, args):
491         if len(args) != 1:
492             return False
493         maxChange = int(args[0])
495         if "p4ExitCode" in p4Cmd("changes -m 1"):
496             die("Problems executing p4");
498         if self.rollbackLocalBranches:
499             refPrefix = "refs/heads/"
500             lines = read_pipe_lines("git rev-parse --symbolic --branches")
501         else:
502             refPrefix = "refs/remotes/"
503             lines = read_pipe_lines("git rev-parse --symbolic --remotes")
505         for line in lines:
506             if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
507                 line = line.strip()
508                 ref = refPrefix + line
509                 log = extractLogMessageFromGitCommit(ref)
510                 settings = extractSettingsGitLog(log)
512                 depotPaths = settings['depot-paths']
513                 change = settings['change']
515                 changed = False
517                 if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
518                                                            for p in depotPaths]))) == 0:
519                     print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
520                     system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
521                     continue
523                 while change and int(change) > maxChange:
524                     changed = True
525                     if self.verbose:
526                         print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
527                     system("git update-ref %s \"%s^\"" % (ref, ref))
528                     log = extractLogMessageFromGitCommit(ref)
529                     settings =  extractSettingsGitLog(log)
532                     depotPaths = settings['depot-paths']
533                     change = settings['change']
535                 if changed:
536                     print "%s rewound to %s" % (ref, change)
538         return True
540 class P4Submit(Command):
541     def __init__(self):
542         Command.__init__(self)
543         self.options = [
544                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
545                 optparse.make_option("--origin", dest="origin"),
546                 optparse.make_option("-M", dest="detectRename", action="store_true"),
547         ]
548         self.description = "Submit changes from git to the perforce depot."
549         self.usage += " [name of git branch to submit into perforce depot]"
550         self.interactive = True
551         self.origin = ""
552         self.detectRename = False
553         self.verbose = False
554         self.isWindows = (platform.system() == "Windows")
556     def check(self):
557         if len(p4CmdList("opened ...")) > 0:
558             die("You have files opened with perforce! Close them before starting the sync.")
560     # replaces everything between 'Description:' and the next P4 submit template field with the
561     # commit message
562     def prepareLogMessage(self, template, message):
563         result = ""
565         inDescriptionSection = False
567         for line in template.split("\n"):
568             if line.startswith("#"):
569                 result += line + "\n"
570                 continue
572             if inDescriptionSection:
573                 if line.startswith("Files:"):
574                     inDescriptionSection = False
575                 else:
576                     continue
577             else:
578                 if line.startswith("Description:"):
579                     inDescriptionSection = True
580                     line += "\n"
581                     for messageLine in message.split("\n"):
582                         line += "\t" + messageLine + "\n"
584             result += line + "\n"
586         return result
588     def prepareSubmitTemplate(self):
589         # remove lines in the Files section that show changes to files outside the depot path we're committing into
590         template = ""
591         inFilesSection = False
592         for line in p4_read_pipe_lines("change -o"):
593             if line.endswith("\r\n"):
594                 line = line[:-2] + "\n"
595             if inFilesSection:
596                 if line.startswith("\t"):
597                     # path starts and ends with a tab
598                     path = line[1:]
599                     lastTab = path.rfind("\t")
600                     if lastTab != -1:
601                         path = path[:lastTab]
602                         if not path.startswith(self.depotPath):
603                             continue
604                 else:
605                     inFilesSection = False
606             else:
607                 if line.startswith("Files:"):
608                     inFilesSection = True
610             template += line
612         return template
614     def applyCommit(self, id):
615         print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
616         diffOpts = ("", "-M")[self.detectRename]
617         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
618         filesToAdd = set()
619         filesToDelete = set()
620         editedFiles = set()
621         filesToChangeExecBit = {}
622         for line in diff:
623             diff = parseDiffTreeEntry(line)
624             modifier = diff['status']
625             path = diff['src']
626             if modifier == "M":
627                 p4_system("edit \"%s\"" % path)
628                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
629                     filesToChangeExecBit[path] = diff['dst_mode']
630                 editedFiles.add(path)
631             elif modifier == "A":
632                 filesToAdd.add(path)
633                 filesToChangeExecBit[path] = diff['dst_mode']
634                 if path in filesToDelete:
635                     filesToDelete.remove(path)
636             elif modifier == "D":
637                 filesToDelete.add(path)
638                 if path in filesToAdd:
639                     filesToAdd.remove(path)
640             elif modifier == "R":
641                 src, dest = diff['src'], diff['dst']
642                 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
643                 p4_system("edit \"%s\"" % (dest))
644                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
645                     filesToChangeExecBit[dest] = diff['dst_mode']
646                 os.unlink(dest)
647                 editedFiles.add(dest)
648                 filesToDelete.add(src)
649             else:
650                 die("unknown modifier %s for %s" % (modifier, path))
652         diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
653         patchcmd = diffcmd + " | git apply "
654         tryPatchCmd = patchcmd + "--check -"
655         applyPatchCmd = patchcmd + "--check --apply -"
657         if os.system(tryPatchCmd) != 0:
658             print "Unfortunately applying the change failed!"
659             print "What do you want to do?"
660             response = "x"
661             while response != "s" and response != "a" and response != "w":
662                 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
663                                      "and with .rej files / [w]rite the patch to a file (patch.txt) ")
664             if response == "s":
665                 print "Skipping! Good luck with the next patches..."
666                 for f in editedFiles:
667                     p4_system("revert \"%s\"" % f);
668                 for f in filesToAdd:
669                     system("rm %s" %f)
670                 return
671             elif response == "a":
672                 os.system(applyPatchCmd)
673                 if len(filesToAdd) > 0:
674                     print "You may also want to call p4 add on the following files:"
675                     print " ".join(filesToAdd)
676                 if len(filesToDelete):
677                     print "The following files should be scheduled for deletion with p4 delete:"
678                     print " ".join(filesToDelete)
679                 die("Please resolve and submit the conflict manually and "
680                     + "continue afterwards with git-p4 submit --continue")
681             elif response == "w":
682                 system(diffcmd + " > patch.txt")
683                 print "Patch saved to patch.txt in %s !" % self.clientPath
684                 die("Please resolve and submit the conflict manually and "
685                     "continue afterwards with git-p4 submit --continue")
687         system(applyPatchCmd)
689         for f in filesToAdd:
690             p4_system("add \"%s\"" % f)
691         for f in filesToDelete:
692             p4_system("revert \"%s\"" % f)
693             p4_system("delete \"%s\"" % f)
695         # Set/clear executable bits
696         for f in filesToChangeExecBit.keys():
697             mode = filesToChangeExecBit[f]
698             setP4ExecBit(f, mode)
700         logMessage = extractLogMessageFromGitCommit(id)
701         logMessage = logMessage.strip()
703         template = self.prepareSubmitTemplate()
705         if self.interactive:
706             submitTemplate = self.prepareLogMessage(template, logMessage)
707             if os.environ.has_key("P4DIFF"):
708                 del(os.environ["P4DIFF"])
709             diff = ""
710             for editedFile in editedFiles:
711                 diff += p4_read_pipe("diff -du %r" % editedFile)
713             newdiff = ""
714             for newFile in filesToAdd:
715                 newdiff += "==== new file ====\n"
716                 newdiff += "--- /dev/null\n"
717                 newdiff += "+++ %s\n" % newFile
718                 f = open(newFile, "r")
719                 for line in f.readlines():
720                     newdiff += "+" + line
721                 f.close()
723             separatorLine = "######## everything below this line is just the diff #######\n"
725             [handle, fileName] = tempfile.mkstemp()
726             tmpFile = os.fdopen(handle, "w+")
727             if self.isWindows:
728                 submitTemplate = submitTemplate.replace("\n", "\r\n")
729                 separatorLine = separatorLine.replace("\n", "\r\n")
730                 newdiff = newdiff.replace("\n", "\r\n")
731             tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
732             tmpFile.close()
733             mtime = os.stat(fileName).st_mtime
734             if os.environ.has_key("P4EDITOR"):
735                 editor = os.environ.get("P4EDITOR")
736             else:
737                 editor = read_pipe("git var GIT_EDITOR").strip()
738             system(editor + " " + fileName)
740             response = "y"
741             if os.stat(fileName).st_mtime <= mtime:
742                 response = "x"
743                 while response != "y" and response != "n":
744                     response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
746             if response == "y":
747                 tmpFile = open(fileName, "rb")
748                 message = tmpFile.read()
749                 tmpFile.close()
750                 submitTemplate = message[:message.index(separatorLine)]
751                 if self.isWindows:
752                     submitTemplate = submitTemplate.replace("\r\n", "\n")
753                 p4_write_pipe("submit -i", submitTemplate)
754             else:
755                 for f in editedFiles:
756                     p4_system("revert \"%s\"" % f);
757                 for f in filesToAdd:
758                     p4_system("revert \"%s\"" % f);
759                     system("rm %s" %f)
761             os.remove(fileName)
762         else:
763             fileName = "submit.txt"
764             file = open(fileName, "w+")
765             file.write(self.prepareLogMessage(template, logMessage))
766             file.close()
767             print ("Perforce submit template written as %s. "
768                    + "Please review/edit and then use p4 submit -i < %s to submit directly!"
769                    % (fileName, fileName))
771     def run(self, args):
772         if len(args) == 0:
773             self.master = currentGitBranch()
774             if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
775                 die("Detecting current git branch failed!")
776         elif len(args) == 1:
777             self.master = args[0]
778         else:
779             return False
781         allowSubmit = gitConfig("git-p4.allowSubmit")
782         if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
783             die("%s is not in git-p4.allowSubmit" % self.master)
785         [upstream, settings] = findUpstreamBranchPoint()
786         self.depotPath = settings['depot-paths'][0]
787         if len(self.origin) == 0:
788             self.origin = upstream
790         if self.verbose:
791             print "Origin branch is " + self.origin
793         if len(self.depotPath) == 0:
794             print "Internal error: cannot locate perforce depot path from existing branches"
795             sys.exit(128)
797         self.clientPath = p4Where(self.depotPath)
799         if len(self.clientPath) == 0:
800             print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
801             sys.exit(128)
803         print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
804         self.oldWorkingDirectory = os.getcwd()
806         chdir(self.clientPath)
807         print "Synchronizing p4 checkout..."
808         p4_system("sync ...")
810         self.check()
812         commits = []
813         for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
814             commits.append(line.strip())
815         commits.reverse()
817         while len(commits) > 0:
818             commit = commits[0]
819             commits = commits[1:]
820             self.applyCommit(commit)
821             if not self.interactive:
822                 break
824         if len(commits) == 0:
825             print "All changes applied!"
826             chdir(self.oldWorkingDirectory)
828             sync = P4Sync()
829             sync.run([])
831             rebase = P4Rebase()
832             rebase.rebase()
834         return True
836 class P4Sync(Command):
837     def __init__(self):
838         Command.__init__(self)
839         self.options = [
840                 optparse.make_option("--branch", dest="branch"),
841                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
842                 optparse.make_option("--changesfile", dest="changesFile"),
843                 optparse.make_option("--silent", dest="silent", action="store_true"),
844                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
845                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
846                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
847                                      help="Import into refs/heads/ , not refs/remotes"),
848                 optparse.make_option("--max-changes", dest="maxChanges"),
849                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
850                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
851                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
852                                      help="Only sync files that are included in the Perforce Client Spec")
853         ]
854         self.description = """Imports from Perforce into a git repository.\n
855     example:
856     //depot/my/project/ -- to import the current head
857     //depot/my/project/@all -- to import everything
858     //depot/my/project/@1,6 -- to import only from revision 1 to 6
860     (a ... is not needed in the path p4 specification, it's added implicitly)"""
862         self.usage += " //depot/path[@revRange]"
863         self.silent = False
864         self.createdBranches = set()
865         self.committedChanges = set()
866         self.branch = ""
867         self.detectBranches = False
868         self.detectLabels = False
869         self.changesFile = ""
870         self.syncWithOrigin = True
871         self.verbose = False
872         self.importIntoRemotes = True
873         self.maxChanges = ""
874         self.isWindows = (platform.system() == "Windows")
875         self.keepRepoPath = False
876         self.depotPaths = None
877         self.p4BranchesInGit = []
878         self.cloneExclude = []
879         self.useClientSpec = False
880         self.clientSpecDirs = []
882         if gitConfig("git-p4.syncFromOrigin") == "false":
883             self.syncWithOrigin = False
885     def extractFilesFromCommit(self, commit):
886         self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
887                              for path in self.cloneExclude]
888         files = []
889         fnum = 0
890         while commit.has_key("depotFile%s" % fnum):
891             path =  commit["depotFile%s" % fnum]
893             if [p for p in self.cloneExclude
894                 if path.startswith (p)]:
895                 found = False
896             else:
897                 found = [p for p in self.depotPaths
898                          if path.startswith (p)]
899             if not found:
900                 fnum = fnum + 1
901                 continue
903             file = {}
904             file["path"] = path
905             file["rev"] = commit["rev%s" % fnum]
906             file["action"] = commit["action%s" % fnum]
907             file["type"] = commit["type%s" % fnum]
908             files.append(file)
909             fnum = fnum + 1
910         return files
912     def stripRepoPath(self, path, prefixes):
913         if self.useClientSpec:
915             # if using the client spec, we use the output directory
916             # specified in the client.  For example, a view
917             #   //depot/foo/branch/... //client/branch/foo/...
918             # will end up putting all foo/branch files into
919             #  branch/foo/
920             for val in self.clientSpecDirs:
921                 if path.startswith(val[0]):
922                     # replace the depot path with the client path
923                     path = path.replace(val[0], val[1][1])
924                     # now strip out the client (//client/...)
925                     path = re.sub("^(//[^/]+/)", '', path)
926                     # the rest is all path
927                     return path
929         if self.keepRepoPath:
930             prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
932         for p in prefixes:
933             if path.startswith(p):
934                 path = path[len(p):]
936         return path
938     def splitFilesIntoBranches(self, commit):
939         branches = {}
940         fnum = 0
941         while commit.has_key("depotFile%s" % fnum):
942             path =  commit["depotFile%s" % fnum]
943             found = [p for p in self.depotPaths
944                      if path.startswith (p)]
945             if not found:
946                 fnum = fnum + 1
947                 continue
949             file = {}
950             file["path"] = path
951             file["rev"] = commit["rev%s" % fnum]
952             file["action"] = commit["action%s" % fnum]
953             file["type"] = commit["type%s" % fnum]
954             fnum = fnum + 1
956             relPath = self.stripRepoPath(path, self.depotPaths)
958             for branch in self.knownBranches.keys():
960                 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
961                 if relPath.startswith(branch + "/"):
962                     if branch not in branches:
963                         branches[branch] = []
964                     branches[branch].append(file)
965                     break
967         return branches
969     # output one file from the P4 stream
970     # - helper for streamP4Files
972     def streamOneP4File(self, file, contents):
973         if file["type"] == "apple":
974             print "\nfile %s is a strange apple file that forks. Ignoring" % \
975                 file['depotFile']
976             return
978         relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
979         if verbose:
980             sys.stderr.write("%s\n" % relPath)
982         mode = "644"
983         if isP4Exec(file["type"]):
984             mode = "755"
985         elif file["type"] == "symlink":
986             mode = "120000"
987             # p4 print on a symlink contains "target\n", so strip it off
988             data = ''.join(contents)
989             contents = [data[:-1]]
991         if self.isWindows and file["type"].endswith("text"):
992             mangled = []
993             for data in contents:
994                 data = data.replace("\r\n", "\n")
995                 mangled.append(data)
996             contents = mangled
998         if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
999             contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1000         elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1001             contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1003         self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1005         # total length...
1006         length = 0
1007         for d in contents:
1008             length = length + len(d)
1010         self.gitStream.write("data %d\n" % length)
1011         for d in contents:
1012             self.gitStream.write(d)
1013         self.gitStream.write("\n")
1015     def streamOneP4Deletion(self, file):
1016         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1017         if verbose:
1018             sys.stderr.write("delete %s\n" % relPath)
1019         self.gitStream.write("D %s\n" % relPath)
1021     # handle another chunk of streaming data
1022     def streamP4FilesCb(self, marshalled):
1024         if marshalled.has_key('depotFile') and self.stream_have_file_info:
1025             # start of a new file - output the old one first
1026             self.streamOneP4File(self.stream_file, self.stream_contents)
1027             self.stream_file = {}
1028             self.stream_contents = []
1029             self.stream_have_file_info = False
1031         # pick up the new file information... for the
1032         # 'data' field we need to append to our array
1033         for k in marshalled.keys():
1034             if k == 'data':
1035                 self.stream_contents.append(marshalled['data'])
1036             else:
1037                 self.stream_file[k] = marshalled[k]
1039         self.stream_have_file_info = True
1041     # Stream directly from "p4 files" into "git fast-import"
1042     def streamP4Files(self, files):
1043         filesForCommit = []
1044         filesToRead = []
1045         filesToDelete = []
1047         for f in files:
1048             includeFile = True
1049             for val in self.clientSpecDirs:
1050                 if f['path'].startswith(val[0]):
1051                     if val[1][0] <= 0:
1052                         includeFile = False
1053                     break
1055             if includeFile:
1056                 filesForCommit.append(f)
1057                 if f['action'] not in ('delete', 'move/delete', 'purge'):
1058                     filesToRead.append(f)
1059                 else:
1060                     filesToDelete.append(f)
1062         # deleted files...
1063         for f in filesToDelete:
1064             self.streamOneP4Deletion(f)
1066         if len(filesToRead) > 0:
1067             self.stream_file = {}
1068             self.stream_contents = []
1069             self.stream_have_file_info = False
1071             # curry self argument
1072             def streamP4FilesCbSelf(entry):
1073                 self.streamP4FilesCb(entry)
1075             p4CmdList("-x - print",
1076                 '\n'.join(['%s#%s' % (f['path'], f['rev'])
1077                                                   for f in filesToRead]),
1078                 cb=streamP4FilesCbSelf)
1080             # do the last chunk
1081             if self.stream_file.has_key('depotFile'):
1082                 self.streamOneP4File(self.stream_file, self.stream_contents)
1084     def commit(self, details, files, branch, branchPrefixes, parent = ""):
1085         epoch = details["time"]
1086         author = details["user"]
1087         self.branchPrefixes = branchPrefixes
1089         if self.verbose:
1090             print "commit into %s" % branch
1092         # start with reading files; if that fails, we should not
1093         # create a commit.
1094         new_files = []
1095         for f in files:
1096             if [p for p in branchPrefixes if f['path'].startswith(p)]:
1097                 new_files.append (f)
1098             else:
1099                 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
1101         self.gitStream.write("commit %s\n" % branch)
1102 #        gitStream.write("mark :%s\n" % details["change"])
1103         self.committedChanges.add(int(details["change"]))
1104         committer = ""
1105         if author not in self.users:
1106             self.getUserMapFromPerforceServer()
1107         if author in self.users:
1108             committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1109         else:
1110             committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1112         self.gitStream.write("committer %s\n" % committer)
1114         self.gitStream.write("data <<EOT\n")
1115         self.gitStream.write(details["desc"])
1116         self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1117                              % (','.join (branchPrefixes), details["change"]))
1118         if len(details['options']) > 0:
1119             self.gitStream.write(": options = %s" % details['options'])
1120         self.gitStream.write("]\nEOT\n\n")
1122         if len(parent) > 0:
1123             if self.verbose:
1124                 print "parent %s" % parent
1125             self.gitStream.write("from %s\n" % parent)
1127         self.streamP4Files(new_files)
1128         self.gitStream.write("\n")
1130         change = int(details["change"])
1132         if self.labels.has_key(change):
1133             label = self.labels[change]
1134             labelDetails = label[0]
1135             labelRevisions = label[1]
1136             if self.verbose:
1137                 print "Change %s is labelled %s" % (change, labelDetails)
1139             files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1140                                                     for p in branchPrefixes]))
1142             if len(files) == len(labelRevisions):
1144                 cleanedFiles = {}
1145                 for info in files:
1146                     if info["action"] in ("delete", "purge"):
1147                         continue
1148                     cleanedFiles[info["depotFile"]] = info["rev"]
1150                 if cleanedFiles == labelRevisions:
1151                     self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1152                     self.gitStream.write("from %s\n" % branch)
1154                     owner = labelDetails["Owner"]
1155                     tagger = ""
1156                     if author in self.users:
1157                         tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1158                     else:
1159                         tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1160                     self.gitStream.write("tagger %s\n" % tagger)
1161                     self.gitStream.write("data <<EOT\n")
1162                     self.gitStream.write(labelDetails["Description"])
1163                     self.gitStream.write("EOT\n\n")
1165                 else:
1166                     if not self.silent:
1167                         print ("Tag %s does not match with change %s: files do not match."
1168                                % (labelDetails["label"], change))
1170             else:
1171                 if not self.silent:
1172                     print ("Tag %s does not match with change %s: file count is different."
1173                            % (labelDetails["label"], change))
1175     def getUserCacheFilename(self):
1176         home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1177         return home + "/.gitp4-usercache.txt"
1179     def getUserMapFromPerforceServer(self):
1180         if self.userMapFromPerforceServer:
1181             return
1182         self.users = {}
1184         for output in p4CmdList("users"):
1185             if not output.has_key("User"):
1186                 continue
1187             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1190         s = ''
1191         for (key, val) in self.users.items():
1192             s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1194         open(self.getUserCacheFilename(), "wb").write(s)
1195         self.userMapFromPerforceServer = True
1197     def loadUserMapFromCache(self):
1198         self.users = {}
1199         self.userMapFromPerforceServer = False
1200         try:
1201             cache = open(self.getUserCacheFilename(), "rb")
1202             lines = cache.readlines()
1203             cache.close()
1204             for line in lines:
1205                 entry = line.strip().split("\t")
1206                 self.users[entry[0]] = entry[1]
1207         except IOError:
1208             self.getUserMapFromPerforceServer()
1210     def getLabels(self):
1211         self.labels = {}
1213         l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1214         if len(l) > 0 and not self.silent:
1215             print "Finding files belonging to labels in %s" % `self.depotPaths`
1217         for output in l:
1218             label = output["label"]
1219             revisions = {}
1220             newestChange = 0
1221             if self.verbose:
1222                 print "Querying files for label %s" % label
1223             for file in p4CmdList("files "
1224                                   +  ' '.join (["%s...@%s" % (p, label)
1225                                                 for p in self.depotPaths])):
1226                 revisions[file["depotFile"]] = file["rev"]
1227                 change = int(file["change"])
1228                 if change > newestChange:
1229                     newestChange = change
1231             self.labels[newestChange] = [output, revisions]
1233         if self.verbose:
1234             print "Label changes: %s" % self.labels.keys()
1236     def guessProjectName(self):
1237         for p in self.depotPaths:
1238             if p.endswith("/"):
1239                 p = p[:-1]
1240             p = p[p.strip().rfind("/") + 1:]
1241             if not p.endswith("/"):
1242                p += "/"
1243             return p
1245     def getBranchMapping(self):
1246         lostAndFoundBranches = set()
1248         for info in p4CmdList("branches"):
1249             details = p4Cmd("branch -o %s" % info["branch"])
1250             viewIdx = 0
1251             while details.has_key("View%s" % viewIdx):
1252                 paths = details["View%s" % viewIdx].split(" ")
1253                 viewIdx = viewIdx + 1
1254                 # require standard //depot/foo/... //depot/bar/... mapping
1255                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1256                     continue
1257                 source = paths[0]
1258                 destination = paths[1]
1259                 ## HACK
1260                 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1261                     source = source[len(self.depotPaths[0]):-4]
1262                     destination = destination[len(self.depotPaths[0]):-4]
1264                     if destination in self.knownBranches:
1265                         if not self.silent:
1266                             print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1267                             print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1268                         continue
1270                     self.knownBranches[destination] = source
1272                     lostAndFoundBranches.discard(destination)
1274                     if source not in self.knownBranches:
1275                         lostAndFoundBranches.add(source)
1278         for branch in lostAndFoundBranches:
1279             self.knownBranches[branch] = branch
1281     def getBranchMappingFromGitBranches(self):
1282         branches = p4BranchesInGit(self.importIntoRemotes)
1283         for branch in branches.keys():
1284             if branch == "master":
1285                 branch = "main"
1286             else:
1287                 branch = branch[len(self.projectName):]
1288             self.knownBranches[branch] = branch
1290     def listExistingP4GitBranches(self):
1291         # branches holds mapping from name to commit
1292         branches = p4BranchesInGit(self.importIntoRemotes)
1293         self.p4BranchesInGit = branches.keys()
1294         for branch in branches.keys():
1295             self.initialParents[self.refPrefix + branch] = branches[branch]
1297     def updateOptionDict(self, d):
1298         option_keys = {}
1299         if self.keepRepoPath:
1300             option_keys['keepRepoPath'] = 1
1302         d["options"] = ' '.join(sorted(option_keys.keys()))
1304     def readOptions(self, d):
1305         self.keepRepoPath = (d.has_key('options')
1306                              and ('keepRepoPath' in d['options']))
1308     def gitRefForBranch(self, branch):
1309         if branch == "main":
1310             return self.refPrefix + "master"
1312         if len(branch) <= 0:
1313             return branch
1315         return self.refPrefix + self.projectName + branch
1317     def gitCommitByP4Change(self, ref, change):
1318         if self.verbose:
1319             print "looking in ref " + ref + " for change %s using bisect..." % change
1321         earliestCommit = ""
1322         latestCommit = parseRevision(ref)
1324         while True:
1325             if self.verbose:
1326                 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1327             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1328             if len(next) == 0:
1329                 if self.verbose:
1330                     print "argh"
1331                 return ""
1332             log = extractLogMessageFromGitCommit(next)
1333             settings = extractSettingsGitLog(log)
1334             currentChange = int(settings['change'])
1335             if self.verbose:
1336                 print "current change %s" % currentChange
1338             if currentChange == change:
1339                 if self.verbose:
1340                     print "found %s" % next
1341                 return next
1343             if currentChange < change:
1344                 earliestCommit = "^%s" % next
1345             else:
1346                 latestCommit = "%s" % next
1348         return ""
1350     def importNewBranch(self, branch, maxChange):
1351         # make fast-import flush all changes to disk and update the refs using the checkpoint
1352         # command so that we can try to find the branch parent in the git history
1353         self.gitStream.write("checkpoint\n\n");
1354         self.gitStream.flush();
1355         branchPrefix = self.depotPaths[0] + branch + "/"
1356         range = "@1,%s" % maxChange
1357         #print "prefix" + branchPrefix
1358         changes = p4ChangesForPaths([branchPrefix], range)
1359         if len(changes) <= 0:
1360             return False
1361         firstChange = changes[0]
1362         #print "first change in branch: %s" % firstChange
1363         sourceBranch = self.knownBranches[branch]
1364         sourceDepotPath = self.depotPaths[0] + sourceBranch
1365         sourceRef = self.gitRefForBranch(sourceBranch)
1366         #print "source " + sourceBranch
1368         branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1369         #print "branch parent: %s" % branchParentChange
1370         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1371         if len(gitParent) > 0:
1372             self.initialParents[self.gitRefForBranch(branch)] = gitParent
1373             #print "parent git commit: %s" % gitParent
1375         self.importChanges(changes)
1376         return True
1378     def importChanges(self, changes):
1379         cnt = 1
1380         for change in changes:
1381             description = p4Cmd("describe %s" % change)
1382             self.updateOptionDict(description)
1384             if not self.silent:
1385                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1386                 sys.stdout.flush()
1387             cnt = cnt + 1
1389             try:
1390                 if self.detectBranches:
1391                     branches = self.splitFilesIntoBranches(description)
1392                     for branch in branches.keys():
1393                         ## HACK  --hwn
1394                         branchPrefix = self.depotPaths[0] + branch + "/"
1396                         parent = ""
1398                         filesForCommit = branches[branch]
1400                         if self.verbose:
1401                             print "branch is %s" % branch
1403                         self.updatedBranches.add(branch)
1405                         if branch not in self.createdBranches:
1406                             self.createdBranches.add(branch)
1407                             parent = self.knownBranches[branch]
1408                             if parent == branch:
1409                                 parent = ""
1410                             else:
1411                                 fullBranch = self.projectName + branch
1412                                 if fullBranch not in self.p4BranchesInGit:
1413                                     if not self.silent:
1414                                         print("\n    Importing new branch %s" % fullBranch);
1415                                     if self.importNewBranch(branch, change - 1):
1416                                         parent = ""
1417                                         self.p4BranchesInGit.append(fullBranch)
1418                                     if not self.silent:
1419                                         print("\n    Resuming with change %s" % change);
1421                                 if self.verbose:
1422                                     print "parent determined through known branches: %s" % parent
1424                         branch = self.gitRefForBranch(branch)
1425                         parent = self.gitRefForBranch(parent)
1427                         if self.verbose:
1428                             print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1430                         if len(parent) == 0 and branch in self.initialParents:
1431                             parent = self.initialParents[branch]
1432                             del self.initialParents[branch]
1434                         self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1435                 else:
1436                     files = self.extractFilesFromCommit(description)
1437                     self.commit(description, files, self.branch, self.depotPaths,
1438                                 self.initialParent)
1439                     self.initialParent = ""
1440             except IOError:
1441                 print self.gitError.read()
1442                 sys.exit(1)
1444     def importHeadRevision(self, revision):
1445         print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1447         details = { "user" : "git perforce import user", "time" : int(time.time()) }
1448         details["desc"] = ("Initial import of %s from the state at revision %s"
1449                            % (' '.join(self.depotPaths), revision))
1450         details["change"] = revision
1451         newestRevision = 0
1453         fileCnt = 0
1454         for info in p4CmdList("files "
1455                               +  ' '.join(["%s...%s"
1456                                            % (p, revision)
1457                                            for p in self.depotPaths])):
1459             if info['code'] == 'error':
1460                 sys.stderr.write("p4 returned an error: %s\n"
1461                                  % info['data'])
1462                 sys.exit(1)
1465             change = int(info["change"])
1466             if change > newestRevision:
1467                 newestRevision = change
1469             if info["action"] in ("delete", "purge"):
1470                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1471                 #fileCnt = fileCnt + 1
1472                 continue
1474             for prop in ["depotFile", "rev", "action", "type" ]:
1475                 details["%s%s" % (prop, fileCnt)] = info[prop]
1477             fileCnt = fileCnt + 1
1479         details["change"] = newestRevision
1480         self.updateOptionDict(details)
1481         try:
1482             self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1483         except IOError:
1484             print "IO error with git fast-import. Is your git version recent enough?"
1485             print self.gitError.read()
1488     def getClientSpec(self):
1489         specList = p4CmdList( "client -o" )
1490         temp = {}
1491         for entry in specList:
1492             for k,v in entry.iteritems():
1493                 if k.startswith("View"):
1495                     # p4 has these %%1 to %%9 arguments in specs to
1496                     # reorder paths; which we can't handle (yet :)
1497                     if re.match('%%\d', v) != None:
1498                         print "Sorry, can't handle %%n arguments in client specs"
1499                         sys.exit(1)
1501                     if v.startswith('"'):
1502                         start = 1
1503                     else:
1504                         start = 0
1505                     index = v.find("...")
1507                     # save the "client view"; i.e the RHS of the view
1508                     # line that tells the client where to put the
1509                     # files for this view.
1510                     cv = v[index+3:].strip() # +3 to remove previous '...'
1512                     # if the client view doesn't end with a
1513                     # ... wildcard, then we're going to mess up the
1514                     # output directory, so fail gracefully.
1515                     if not cv.endswith('...'):
1516                         print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1517                         sys.exit(1)
1518                     cv=cv[:-3]
1520                     # now save the view; +index means included, -index
1521                     # means it should be filtered out.
1522                     v = v[start:index]
1523                     if v.startswith("-"):
1524                         v = v[1:]
1525                         include = -len(v)
1526                     else:
1527                         include = len(v)
1529                     temp[v] = (include, cv)
1531         self.clientSpecDirs = temp.items()
1532         self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1534     def run(self, args):
1535         self.depotPaths = []
1536         self.changeRange = ""
1537         self.initialParent = ""
1538         self.previousDepotPaths = []
1540         # map from branch depot path to parent branch
1541         self.knownBranches = {}
1542         self.initialParents = {}
1543         self.hasOrigin = originP4BranchesExist()
1544         if not self.syncWithOrigin:
1545             self.hasOrigin = False
1547         if self.importIntoRemotes:
1548             self.refPrefix = "refs/remotes/p4/"
1549         else:
1550             self.refPrefix = "refs/heads/p4/"
1552         if self.syncWithOrigin and self.hasOrigin:
1553             if not self.silent:
1554                 print "Syncing with origin first by calling git fetch origin"
1555             system("git fetch origin")
1557         if len(self.branch) == 0:
1558             self.branch = self.refPrefix + "master"
1559             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1560                 system("git update-ref %s refs/heads/p4" % self.branch)
1561                 system("git branch -D p4");
1562             # create it /after/ importing, when master exists
1563             if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1564                 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1566         if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1567             self.getClientSpec()
1569         # TODO: should always look at previous commits,
1570         # merge with previous imports, if possible.
1571         if args == []:
1572             if self.hasOrigin:
1573                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1574             self.listExistingP4GitBranches()
1576             if len(self.p4BranchesInGit) > 1:
1577                 if not self.silent:
1578                     print "Importing from/into multiple branches"
1579                 self.detectBranches = True
1581             if self.verbose:
1582                 print "branches: %s" % self.p4BranchesInGit
1584             p4Change = 0
1585             for branch in self.p4BranchesInGit:
1586                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1588                 settings = extractSettingsGitLog(logMsg)
1590                 self.readOptions(settings)
1591                 if (settings.has_key('depot-paths')
1592                     and settings.has_key ('change')):
1593                     change = int(settings['change']) + 1
1594                     p4Change = max(p4Change, change)
1596                     depotPaths = sorted(settings['depot-paths'])
1597                     if self.previousDepotPaths == []:
1598                         self.previousDepotPaths = depotPaths
1599                     else:
1600                         paths = []
1601                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1602                             for i in range(0, min(len(cur), len(prev))):
1603                                 if cur[i] <> prev[i]:
1604                                     i = i - 1
1605                                     break
1607                             paths.append (cur[:i + 1])
1609                         self.previousDepotPaths = paths
1611             if p4Change > 0:
1612                 self.depotPaths = sorted(self.previousDepotPaths)
1613                 self.changeRange = "@%s,#head" % p4Change
1614                 if not self.detectBranches:
1615                     self.initialParent = parseRevision(self.branch)
1616                 if not self.silent and not self.detectBranches:
1617                     print "Performing incremental import into %s git branch" % self.branch
1619         if not self.branch.startswith("refs/"):
1620             self.branch = "refs/heads/" + self.branch
1622         if len(args) == 0 and self.depotPaths:
1623             if not self.silent:
1624                 print "Depot paths: %s" % ' '.join(self.depotPaths)
1625         else:
1626             if self.depotPaths and self.depotPaths != args:
1627                 print ("previous import used depot path %s and now %s was specified. "
1628                        "This doesn't work!" % (' '.join (self.depotPaths),
1629                                                ' '.join (args)))
1630                 sys.exit(1)
1632             self.depotPaths = sorted(args)
1634         revision = ""
1635         self.users = {}
1637         newPaths = []
1638         for p in self.depotPaths:
1639             if p.find("@") != -1:
1640                 atIdx = p.index("@")
1641                 self.changeRange = p[atIdx:]
1642                 if self.changeRange == "@all":
1643                     self.changeRange = ""
1644                 elif ',' not in self.changeRange:
1645                     revision = self.changeRange
1646                     self.changeRange = ""
1647                 p = p[:atIdx]
1648             elif p.find("#") != -1:
1649                 hashIdx = p.index("#")
1650                 revision = p[hashIdx:]
1651                 p = p[:hashIdx]
1652             elif self.previousDepotPaths == []:
1653                 revision = "#head"
1655             p = re.sub ("\.\.\.$", "", p)
1656             if not p.endswith("/"):
1657                 p += "/"
1659             newPaths.append(p)
1661         self.depotPaths = newPaths
1664         self.loadUserMapFromCache()
1665         self.labels = {}
1666         if self.detectLabels:
1667             self.getLabels();
1669         if self.detectBranches:
1670             ## FIXME - what's a P4 projectName ?
1671             self.projectName = self.guessProjectName()
1673             if self.hasOrigin:
1674                 self.getBranchMappingFromGitBranches()
1675             else:
1676                 self.getBranchMapping()
1677             if self.verbose:
1678                 print "p4-git branches: %s" % self.p4BranchesInGit
1679                 print "initial parents: %s" % self.initialParents
1680             for b in self.p4BranchesInGit:
1681                 if b != "master":
1683                     ## FIXME
1684                     b = b[len(self.projectName):]
1685                 self.createdBranches.add(b)
1687         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1689         importProcess = subprocess.Popen(["git", "fast-import"],
1690                                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1691                                          stderr=subprocess.PIPE);
1692         self.gitOutput = importProcess.stdout
1693         self.gitStream = importProcess.stdin
1694         self.gitError = importProcess.stderr
1696         if revision:
1697             self.importHeadRevision(revision)
1698         else:
1699             changes = []
1701             if len(self.changesFile) > 0:
1702                 output = open(self.changesFile).readlines()
1703                 changeSet = set()
1704                 for line in output:
1705                     changeSet.add(int(line))
1707                 for change in changeSet:
1708                     changes.append(change)
1710                 changes.sort()
1711             else:
1712                 if self.verbose:
1713                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1714                                                               self.changeRange)
1715                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1717                 if len(self.maxChanges) > 0:
1718                     changes = changes[:min(int(self.maxChanges), len(changes))]
1720             if len(changes) == 0:
1721                 if not self.silent:
1722                     print "No changes to import!"
1723                 return True
1725             if not self.silent and not self.detectBranches:
1726                 print "Import destination: %s" % self.branch
1728             self.updatedBranches = set()
1730             self.importChanges(changes)
1732             if not self.silent:
1733                 print ""
1734                 if len(self.updatedBranches) > 0:
1735                     sys.stdout.write("Updated branches: ")
1736                     for b in self.updatedBranches:
1737                         sys.stdout.write("%s " % b)
1738                     sys.stdout.write("\n")
1740         self.gitStream.close()
1741         if importProcess.wait() != 0:
1742             die("fast-import failed: %s" % self.gitError.read())
1743         self.gitOutput.close()
1744         self.gitError.close()
1746         return True
1748 class P4Rebase(Command):
1749     def __init__(self):
1750         Command.__init__(self)
1751         self.options = [ ]
1752         self.description = ("Fetches the latest revision from perforce and "
1753                             + "rebases the current work (branch) against it")
1754         self.verbose = False
1756     def run(self, args):
1757         sync = P4Sync()
1758         sync.run([])
1760         return self.rebase()
1762     def rebase(self):
1763         if os.system("git update-index --refresh") != 0:
1764             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.");
1765         if len(read_pipe("git diff-index HEAD --")) > 0:
1766             die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1768         [upstream, settings] = findUpstreamBranchPoint()
1769         if len(upstream) == 0:
1770             die("Cannot find upstream branchpoint for rebase")
1772         # the branchpoint may be p4/foo~3, so strip off the parent
1773         upstream = re.sub("~[0-9]+$", "", upstream)
1775         print "Rebasing the current branch onto %s" % upstream
1776         oldHead = read_pipe("git rev-parse HEAD").strip()
1777         system("git rebase %s" % upstream)
1778         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1779         return True
1781 class P4Clone(P4Sync):
1782     def __init__(self):
1783         P4Sync.__init__(self)
1784         self.description = "Creates a new git repository and imports from Perforce into it"
1785         self.usage = "usage: %prog [options] //depot/path[@revRange]"
1786         self.options += [
1787             optparse.make_option("--destination", dest="cloneDestination",
1788                                  action='store', default=None,
1789                                  help="where to leave result of the clone"),
1790             optparse.make_option("-/", dest="cloneExclude",
1791                                  action="append", type="string",
1792                                  help="exclude depot path")
1793         ]
1794         self.cloneDestination = None
1795         self.needsGit = False
1797     # This is required for the "append" cloneExclude action
1798     def ensure_value(self, attr, value):
1799         if not hasattr(self, attr) or getattr(self, attr) is None:
1800             setattr(self, attr, value)
1801         return getattr(self, attr)
1803     def defaultDestination(self, args):
1804         ## TODO: use common prefix of args?
1805         depotPath = args[0]
1806         depotDir = re.sub("(@[^@]*)$", "", depotPath)
1807         depotDir = re.sub("(#[^#]*)$", "", depotDir)
1808         depotDir = re.sub(r"\.\.\.$", "", depotDir)
1809         depotDir = re.sub(r"/$", "", depotDir)
1810         return os.path.split(depotDir)[1]
1812     def run(self, args):
1813         if len(args) < 1:
1814             return False
1816         if self.keepRepoPath and not self.cloneDestination:
1817             sys.stderr.write("Must specify destination for --keep-path\n")
1818             sys.exit(1)
1820         depotPaths = args
1822         if not self.cloneDestination and len(depotPaths) > 1:
1823             self.cloneDestination = depotPaths[-1]
1824             depotPaths = depotPaths[:-1]
1826         self.cloneExclude = ["/"+p for p in self.cloneExclude]
1827         for p in depotPaths:
1828             if not p.startswith("//"):
1829                 return False
1831         if not self.cloneDestination:
1832             self.cloneDestination = self.defaultDestination(args)
1834         print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1835         if not os.path.exists(self.cloneDestination):
1836             os.makedirs(self.cloneDestination)
1837         chdir(self.cloneDestination)
1838         system("git init")
1839         self.gitdir = os.getcwd() + "/.git"
1840         if not P4Sync.run(self, depotPaths):
1841             return False
1842         if self.branch != "master":
1843             if self.importIntoRemotes:
1844                 masterbranch = "refs/remotes/p4/master"
1845             else:
1846                 masterbranch = "refs/heads/p4/master"
1847             if gitBranchExists(masterbranch):
1848                 system("git branch master %s" % masterbranch)
1849                 system("git checkout -f")
1850             else:
1851                 print "Could not detect main branch. No checkout/master branch created."
1853         return True
1855 class P4Branches(Command):
1856     def __init__(self):
1857         Command.__init__(self)
1858         self.options = [ ]
1859         self.description = ("Shows the git branches that hold imports and their "
1860                             + "corresponding perforce depot paths")
1861         self.verbose = False
1863     def run(self, args):
1864         if originP4BranchesExist():
1865             createOrUpdateBranchesFromOrigin()
1867         cmdline = "git rev-parse --symbolic "
1868         cmdline += " --remotes"
1870         for line in read_pipe_lines(cmdline):
1871             line = line.strip()
1873             if not line.startswith('p4/') or line == "p4/HEAD":
1874                 continue
1875             branch = line
1877             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1878             settings = extractSettingsGitLog(log)
1880             print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1881         return True
1883 class HelpFormatter(optparse.IndentedHelpFormatter):
1884     def __init__(self):
1885         optparse.IndentedHelpFormatter.__init__(self)
1887     def format_description(self, description):
1888         if description:
1889             return description + "\n"
1890         else:
1891             return ""
1893 def printUsage(commands):
1894     print "usage: %s <command> [options]" % sys.argv[0]
1895     print ""
1896     print "valid commands: %s" % ", ".join(commands)
1897     print ""
1898     print "Try %s <command> --help for command specific help." % sys.argv[0]
1899     print ""
1901 commands = {
1902     "debug" : P4Debug,
1903     "submit" : P4Submit,
1904     "commit" : P4Submit,
1905     "sync" : P4Sync,
1906     "rebase" : P4Rebase,
1907     "clone" : P4Clone,
1908     "rollback" : P4RollBack,
1909     "branches" : P4Branches
1913 def main():
1914     if len(sys.argv[1:]) == 0:
1915         printUsage(commands.keys())
1916         sys.exit(2)
1918     cmd = ""
1919     cmdName = sys.argv[1]
1920     try:
1921         klass = commands[cmdName]
1922         cmd = klass()
1923     except KeyError:
1924         print "unknown command %s" % cmdName
1925         print ""
1926         printUsage(commands.keys())
1927         sys.exit(2)
1929     options = cmd.options
1930     cmd.gitdir = os.environ.get("GIT_DIR", None)
1932     args = sys.argv[2:]
1934     if len(options) > 0:
1935         options.append(optparse.make_option("--git-dir", dest="gitdir"))
1937         parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1938                                        options,
1939                                        description = cmd.description,
1940                                        formatter = HelpFormatter())
1942         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1943     global verbose
1944     verbose = cmd.verbose
1945     if cmd.needsGit:
1946         if cmd.gitdir == None:
1947             cmd.gitdir = os.path.abspath(".git")
1948             if not isValidGitDir(cmd.gitdir):
1949                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1950                 if os.path.exists(cmd.gitdir):
1951                     cdup = read_pipe("git rev-parse --show-cdup").strip()
1952                     if len(cdup) > 0:
1953                         chdir(cdup);
1955         if not isValidGitDir(cmd.gitdir):
1956             if isValidGitDir(cmd.gitdir + "/.git"):
1957                 cmd.gitdir += "/.git"
1958             else:
1959                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1961         os.environ["GIT_DIR"] = cmd.gitdir
1963     if not cmd.run(args):
1964         parser.print_help()
1967 if __name__ == '__main__':
1968     main()