Code

Consistently use 'git-p4' for the configuration entries
[git.git] / contrib / fast-import / git-p4
1 #!/usr/bin/env python
2 #
3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
4 #
5 # Author: Simon Hausmann <simon@lst.de>
6 # Copyright: 2007 Simon Hausmann <simon@lst.de>
7 #            2007 Trolltech ASA
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
9 #
11 import optparse, sys, os, marshal, popen2, subprocess, shelve
12 import tempfile, getopt, sha, os.path, time, platform
13 import re
15 from sets import Set;
17 verbose = False
20 def p4_build_cmd(cmd):
21     """Build a suitable p4 command line.
23     This consolidates building and returning a p4 command line into one
24     location. It means that hooking into the environment, or other configuration
25     can be done more easily.
26     """
27     real_cmd = "%s " % "p4"
29     user = gitConfig("git-p4.user")
30     if len(user) > 0:
31         real_cmd += "-u %s " % user
33     password = gitConfig("git-p4.password")
34     if len(password) > 0:
35         real_cmd += "-P %s " % password
37     port = gitConfig("git-p4.port")
38     if len(port) > 0:
39         real_cmd += "-p %s " % port
41     host = gitConfig("git-p4.host")
42     if len(host) > 0:
43         real_cmd += "-h %s " % host
45     client = gitConfig("git-p4.client")
46     if len(client) > 0:
47         real_cmd += "-c %s " % client
49     real_cmd += "%s" % (cmd)
50     if verbose:
51         print real_cmd
52     return real_cmd
54 def die(msg):
55     if verbose:
56         raise Exception(msg)
57     else:
58         sys.stderr.write(msg + "\n")
59         sys.exit(1)
61 def write_pipe(c, str):
62     if verbose:
63         sys.stderr.write('Writing pipe: %s\n' % c)
65     pipe = os.popen(c, 'w')
66     val = pipe.write(str)
67     if pipe.close():
68         die('Command failed: %s' % c)
70     return val
72 def read_pipe(c, ignore_error=False):
73     if verbose:
74         sys.stderr.write('Reading pipe: %s\n' % c)
76     pipe = os.popen(c, 'rb')
77     val = pipe.read()
78     if pipe.close() and not ignore_error:
79         die('Command failed: %s' % c)
81     return val
84 def read_pipe_lines(c):
85     if verbose:
86         sys.stderr.write('Reading pipe: %s\n' % c)
87     ## todo: check return status
88     pipe = os.popen(c, 'rb')
89     val = pipe.readlines()
90     if pipe.close():
91         die('Command failed: %s' % c)
93     return val
95 def p4_read_pipe_lines(c):
96     """Specifically invoke p4 on the command supplied. """
97     real_cmd = p4_build_cmd(c)
98     return read_pipe_lines(real_cmd)
100 def system(cmd):
101     if verbose:
102         sys.stderr.write("executing %s\n" % cmd)
103     if os.system(cmd) != 0:
104         die("command failed: %s" % cmd)
106 def p4_system(cmd):
107     """Specifically invoke p4 as the system command. """
108     real_cmd = p4_build_cmd(cmd)
109     return system(real_cmd)
111 def isP4Exec(kind):
112     """Determine if a Perforce 'kind' should have execute permission
114     'p4 help filetypes' gives a list of the types.  If it starts with 'x',
115     or x follows one of a few letters.  Otherwise, if there is an 'x' after
116     a plus sign, it is also executable"""
117     return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
119 def setP4ExecBit(file, mode):
120     # Reopens an already open file and changes the execute bit to match
121     # the execute bit setting in the passed in mode.
123     p4Type = "+x"
125     if not isModeExec(mode):
126         p4Type = getP4OpenedType(file)
127         p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
128         p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
129         if p4Type[-1] == "+":
130             p4Type = p4Type[0:-1]
132     p4_system("reopen -t %s %s" % (p4Type, file))
134 def getP4OpenedType(file):
135     # Returns the perforce file type for the given file.
137     result = read_pipe("p4 opened %s" % file)
138     match = re.match(".*\((.+)\)\r?$", result)
139     if match:
140         return match.group(1)
141     else:
142         die("Could not determine file type for %s (result: '%s')" % (file, result))
144 def diffTreePattern():
145     # This is a simple generator for the diff tree regex pattern. This could be
146     # a class variable if this and parseDiffTreeEntry were a part of a class.
147     pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
148     while True:
149         yield pattern
151 def parseDiffTreeEntry(entry):
152     """Parses a single diff tree entry into its component elements.
154     See git-diff-tree(1) manpage for details about the format of the diff
155     output. This method returns a dictionary with the following elements:
157     src_mode - The mode of the source file
158     dst_mode - The mode of the destination file
159     src_sha1 - The sha1 for the source file
160     dst_sha1 - The sha1 fr the destination file
161     status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
162     status_score - The score for the status (applicable for 'C' and 'R'
163                    statuses). This is None if there is no score.
164     src - The path for the source file.
165     dst - The path for the destination file. This is only present for
166           copy or renames. If it is not present, this is None.
168     If the pattern is not matched, None is returned."""
170     match = diffTreePattern().next().match(entry)
171     if match:
172         return {
173             'src_mode': match.group(1),
174             'dst_mode': match.group(2),
175             'src_sha1': match.group(3),
176             'dst_sha1': match.group(4),
177             'status': match.group(5),
178             'status_score': match.group(6),
179             'src': match.group(7),
180             'dst': match.group(10)
181         }
182     return None
184 def isModeExec(mode):
185     # Returns True if the given git mode represents an executable file,
186     # otherwise False.
187     return mode[-3:] == "755"
189 def isModeExecChanged(src_mode, dst_mode):
190     return isModeExec(src_mode) != isModeExec(dst_mode)
192 def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
193     cmd = p4_build_cmd("-G %s" % (cmd))
194     if verbose:
195         sys.stderr.write("Opening pipe: %s\n" % cmd)
197     # Use a temporary file to avoid deadlocks without
198     # subprocess.communicate(), which would put another copy
199     # of stdout into memory.
200     stdin_file = None
201     if stdin is not None:
202         stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
203         stdin_file.write(stdin)
204         stdin_file.flush()
205         stdin_file.seek(0)
207     p4 = subprocess.Popen(cmd, shell=True,
208                           stdin=stdin_file,
209                           stdout=subprocess.PIPE)
211     result = []
212     try:
213         while True:
214             entry = marshal.load(p4.stdout)
215             result.append(entry)
216     except EOFError:
217         pass
218     exitCode = p4.wait()
219     if exitCode != 0:
220         entry = {}
221         entry["p4ExitCode"] = exitCode
222         result.append(entry)
224     return result
226 def p4Cmd(cmd):
227     list = p4CmdList(cmd)
228     result = {}
229     for entry in list:
230         result.update(entry)
231     return result;
233 def p4Where(depotPath):
234     if not depotPath.endswith("/"):
235         depotPath += "/"
236     output = p4Cmd("where %s..." % depotPath)
237     if output["code"] == "error":
238         return ""
239     clientPath = ""
240     if "path" in output:
241         clientPath = output.get("path")
242     elif "data" in output:
243         data = output.get("data")
244         lastSpace = data.rfind(" ")
245         clientPath = data[lastSpace + 1:]
247     if clientPath.endswith("..."):
248         clientPath = clientPath[:-3]
249     return clientPath
251 def currentGitBranch():
252     return read_pipe("git name-rev HEAD").split(" ")[1].strip()
254 def isValidGitDir(path):
255     if (os.path.exists(path + "/HEAD")
256         and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
257         return True;
258     return False
260 def parseRevision(ref):
261     return read_pipe("git rev-parse %s" % ref).strip()
263 def extractLogMessageFromGitCommit(commit):
264     logMessage = ""
266     ## fixme: title is first line of commit, not 1st paragraph.
267     foundTitle = False
268     for log in read_pipe_lines("git cat-file commit %s" % commit):
269        if not foundTitle:
270            if len(log) == 1:
271                foundTitle = True
272            continue
274        logMessage += log
275     return logMessage
277 def extractSettingsGitLog(log):
278     values = {}
279     for line in log.split("\n"):
280         line = line.strip()
281         m = re.search (r"^ *\[git-p4: (.*)\]$", line)
282         if not m:
283             continue
285         assignments = m.group(1).split (':')
286         for a in assignments:
287             vals = a.split ('=')
288             key = vals[0].strip()
289             val = ('='.join (vals[1:])).strip()
290             if val.endswith ('\"') and val.startswith('"'):
291                 val = val[1:-1]
293             values[key] = val
295     paths = values.get("depot-paths")
296     if not paths:
297         paths = values.get("depot-path")
298     if paths:
299         values['depot-paths'] = paths.split(',')
300     return values
302 def gitBranchExists(branch):
303     proc = subprocess.Popen(["git", "rev-parse", branch],
304                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);
305     return proc.wait() == 0;
307 def gitConfig(key):
308     return read_pipe("git config %s" % key, ignore_error=True).strip()
310 def p4BranchesInGit(branchesAreInRemotes = True):
311     branches = {}
313     cmdline = "git rev-parse --symbolic "
314     if branchesAreInRemotes:
315         cmdline += " --remotes"
316     else:
317         cmdline += " --branches"
319     for line in read_pipe_lines(cmdline):
320         line = line.strip()
322         ## only import to p4/
323         if not line.startswith('p4/') or line == "p4/HEAD":
324             continue
325         branch = line
327         # strip off p4
328         branch = re.sub ("^p4/", "", line)
330         branches[branch] = parseRevision(line)
331     return branches
333 def findUpstreamBranchPoint(head = "HEAD"):
334     branches = p4BranchesInGit()
335     # map from depot-path to branch name
336     branchByDepotPath = {}
337     for branch in branches.keys():
338         tip = branches[branch]
339         log = extractLogMessageFromGitCommit(tip)
340         settings = extractSettingsGitLog(log)
341         if settings.has_key("depot-paths"):
342             paths = ",".join(settings["depot-paths"])
343             branchByDepotPath[paths] = "remotes/p4/" + branch
345     settings = None
346     parent = 0
347     while parent < 65535:
348         commit = head + "~%s" % parent
349         log = extractLogMessageFromGitCommit(commit)
350         settings = extractSettingsGitLog(log)
351         if settings.has_key("depot-paths"):
352             paths = ",".join(settings["depot-paths"])
353             if branchByDepotPath.has_key(paths):
354                 return [branchByDepotPath[paths], settings]
356         parent = parent + 1
358     return ["", settings]
360 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
361     if not silent:
362         print ("Creating/updating branch(es) in %s based on origin branch(es)"
363                % localRefPrefix)
365     originPrefix = "origin/p4/"
367     for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
368         line = line.strip()
369         if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
370             continue
372         headName = line[len(originPrefix):]
373         remoteHead = localRefPrefix + headName
374         originHead = line
376         original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
377         if (not original.has_key('depot-paths')
378             or not original.has_key('change')):
379             continue
381         update = False
382         if not gitBranchExists(remoteHead):
383             if verbose:
384                 print "creating %s" % remoteHead
385             update = True
386         else:
387             settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
388             if settings.has_key('change') > 0:
389                 if settings['depot-paths'] == original['depot-paths']:
390                     originP4Change = int(original['change'])
391                     p4Change = int(settings['change'])
392                     if originP4Change > p4Change:
393                         print ("%s (%s) is newer than %s (%s). "
394                                "Updating p4 branch from origin."
395                                % (originHead, originP4Change,
396                                   remoteHead, p4Change))
397                         update = True
398                 else:
399                     print ("Ignoring: %s was imported from %s while "
400                            "%s was imported from %s"
401                            % (originHead, ','.join(original['depot-paths']),
402                               remoteHead, ','.join(settings['depot-paths'])))
404         if update:
405             system("git update-ref %s %s" % (remoteHead, originHead))
407 def originP4BranchesExist():
408         return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
410 def p4ChangesForPaths(depotPaths, changeRange):
411     assert depotPaths
412     output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
413                                                         for p in depotPaths]))
415     changes = []
416     for line in output:
417         changeNum = line.split(" ")[1]
418         changes.append(int(changeNum))
420     changes.sort()
421     return changes
423 class Command:
424     def __init__(self):
425         self.usage = "usage: %prog [options]"
426         self.needsGit = True
428 class P4Debug(Command):
429     def __init__(self):
430         Command.__init__(self)
431         self.options = [
432             optparse.make_option("--verbose", dest="verbose", action="store_true",
433                                  default=False),
434             ]
435         self.description = "A tool to debug the output of p4 -G."
436         self.needsGit = False
437         self.verbose = False
439     def run(self, args):
440         j = 0
441         for output in p4CmdList(" ".join(args)):
442             print 'Element: %d' % j
443             j += 1
444             print output
445         return True
447 class P4RollBack(Command):
448     def __init__(self):
449         Command.__init__(self)
450         self.options = [
451             optparse.make_option("--verbose", dest="verbose", action="store_true"),
452             optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
453         ]
454         self.description = "A tool to debug the multi-branch import. Don't use :)"
455         self.verbose = False
456         self.rollbackLocalBranches = False
458     def run(self, args):
459         if len(args) != 1:
460             return False
461         maxChange = int(args[0])
463         if "p4ExitCode" in p4Cmd("changes -m 1"):
464             die("Problems executing p4");
466         if self.rollbackLocalBranches:
467             refPrefix = "refs/heads/"
468             lines = read_pipe_lines("git rev-parse --symbolic --branches")
469         else:
470             refPrefix = "refs/remotes/"
471             lines = read_pipe_lines("git rev-parse --symbolic --remotes")
473         for line in lines:
474             if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
475                 line = line.strip()
476                 ref = refPrefix + line
477                 log = extractLogMessageFromGitCommit(ref)
478                 settings = extractSettingsGitLog(log)
480                 depotPaths = settings['depot-paths']
481                 change = settings['change']
483                 changed = False
485                 if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
486                                                            for p in depotPaths]))) == 0:
487                     print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
488                     system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
489                     continue
491                 while change and int(change) > maxChange:
492                     changed = True
493                     if self.verbose:
494                         print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
495                     system("git update-ref %s \"%s^\"" % (ref, ref))
496                     log = extractLogMessageFromGitCommit(ref)
497                     settings =  extractSettingsGitLog(log)
500                     depotPaths = settings['depot-paths']
501                     change = settings['change']
503                 if changed:
504                     print "%s rewound to %s" % (ref, change)
506         return True
508 class P4Submit(Command):
509     def __init__(self):
510         Command.__init__(self)
511         self.options = [
512                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
513                 optparse.make_option("--origin", dest="origin"),
514                 optparse.make_option("-M", dest="detectRename", action="store_true"),
515         ]
516         self.description = "Submit changes from git to the perforce depot."
517         self.usage += " [name of git branch to submit into perforce depot]"
518         self.interactive = True
519         self.origin = ""
520         self.detectRename = False
521         self.verbose = False
522         self.isWindows = (platform.system() == "Windows")
524     def check(self):
525         if len(p4CmdList("opened ...")) > 0:
526             die("You have files opened with perforce! Close them before starting the sync.")
528     # replaces everything between 'Description:' and the next P4 submit template field with the
529     # commit message
530     def prepareLogMessage(self, template, message):
531         result = ""
533         inDescriptionSection = False
535         for line in template.split("\n"):
536             if line.startswith("#"):
537                 result += line + "\n"
538                 continue
540             if inDescriptionSection:
541                 if line.startswith("Files:"):
542                     inDescriptionSection = False
543                 else:
544                     continue
545             else:
546                 if line.startswith("Description:"):
547                     inDescriptionSection = True
548                     line += "\n"
549                     for messageLine in message.split("\n"):
550                         line += "\t" + messageLine + "\n"
552             result += line + "\n"
554         return result
556     def prepareSubmitTemplate(self):
557         # remove lines in the Files section that show changes to files outside the depot path we're committing into
558         template = ""
559         inFilesSection = False
560         for line in p4_read_pipe_lines("change -o"):
561             if line.endswith("\r\n"):
562                 line = line[:-2] + "\n"
563             if inFilesSection:
564                 if line.startswith("\t"):
565                     # path starts and ends with a tab
566                     path = line[1:]
567                     lastTab = path.rfind("\t")
568                     if lastTab != -1:
569                         path = path[:lastTab]
570                         if not path.startswith(self.depotPath):
571                             continue
572                 else:
573                     inFilesSection = False
574             else:
575                 if line.startswith("Files:"):
576                     inFilesSection = True
578             template += line
580         return template
582     def applyCommit(self, id):
583         print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
584         diffOpts = ("", "-M")[self.detectRename]
585         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
586         filesToAdd = set()
587         filesToDelete = set()
588         editedFiles = set()
589         filesToChangeExecBit = {}
590         for line in diff:
591             diff = parseDiffTreeEntry(line)
592             modifier = diff['status']
593             path = diff['src']
594             if modifier == "M":
595                 p4_system("edit \"%s\"" % path)
596                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
597                     filesToChangeExecBit[path] = diff['dst_mode']
598                 editedFiles.add(path)
599             elif modifier == "A":
600                 filesToAdd.add(path)
601                 filesToChangeExecBit[path] = diff['dst_mode']
602                 if path in filesToDelete:
603                     filesToDelete.remove(path)
604             elif modifier == "D":
605                 filesToDelete.add(path)
606                 if path in filesToAdd:
607                     filesToAdd.remove(path)
608             elif modifier == "R":
609                 src, dest = diff['src'], diff['dst']
610                 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
611                 p4_system("edit \"%s\"" % (dest))
612                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
613                     filesToChangeExecBit[dest] = diff['dst_mode']
614                 os.unlink(dest)
615                 editedFiles.add(dest)
616                 filesToDelete.add(src)
617             else:
618                 die("unknown modifier %s for %s" % (modifier, path))
620         diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
621         patchcmd = diffcmd + " | git apply "
622         tryPatchCmd = patchcmd + "--check -"
623         applyPatchCmd = patchcmd + "--check --apply -"
625         if os.system(tryPatchCmd) != 0:
626             print "Unfortunately applying the change failed!"
627             print "What do you want to do?"
628             response = "x"
629             while response != "s" and response != "a" and response != "w":
630                 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
631                                      "and with .rej files / [w]rite the patch to a file (patch.txt) ")
632             if response == "s":
633                 print "Skipping! Good luck with the next patches..."
634                 for f in editedFiles:
635                     p4_system("revert \"%s\"" % f);
636                 for f in filesToAdd:
637                     system("rm %s" %f)
638                 return
639             elif response == "a":
640                 os.system(applyPatchCmd)
641                 if len(filesToAdd) > 0:
642                     print "You may also want to call p4 add on the following files:"
643                     print " ".join(filesToAdd)
644                 if len(filesToDelete):
645                     print "The following files should be scheduled for deletion with p4 delete:"
646                     print " ".join(filesToDelete)
647                 die("Please resolve and submit the conflict manually and "
648                     + "continue afterwards with git-p4 submit --continue")
649             elif response == "w":
650                 system(diffcmd + " > patch.txt")
651                 print "Patch saved to patch.txt in %s !" % self.clientPath
652                 die("Please resolve and submit the conflict manually and "
653                     "continue afterwards with git-p4 submit --continue")
655         system(applyPatchCmd)
657         for f in filesToAdd:
658             p4_system("add \"%s\"" % f)
659         for f in filesToDelete:
660             p4_system("revert \"%s\"" % f)
661             p4_system("delete \"%s\"" % f)
663         # Set/clear executable bits
664         for f in filesToChangeExecBit.keys():
665             mode = filesToChangeExecBit[f]
666             setP4ExecBit(f, mode)
668         logMessage = extractLogMessageFromGitCommit(id)
669         logMessage = logMessage.strip()
671         template = self.prepareSubmitTemplate()
673         if self.interactive:
674             submitTemplate = self.prepareLogMessage(template, logMessage)
675             if os.environ.has_key("P4DIFF"):
676                 del(os.environ["P4DIFF"])
677             diff = read_pipe("p4 diff -du ...")
679             newdiff = ""
680             for newFile in filesToAdd:
681                 newdiff += "==== new file ====\n"
682                 newdiff += "--- /dev/null\n"
683                 newdiff += "+++ %s\n" % newFile
684                 f = open(newFile, "r")
685                 for line in f.readlines():
686                     newdiff += "+" + line
687                 f.close()
689             separatorLine = "######## everything below this line is just the diff #######\n"
691             [handle, fileName] = tempfile.mkstemp()
692             tmpFile = os.fdopen(handle, "w+")
693             if self.isWindows:
694                 submitTemplate = submitTemplate.replace("\n", "\r\n")
695                 separatorLine = separatorLine.replace("\n", "\r\n")
696                 newdiff = newdiff.replace("\n", "\r\n")
697             tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
698             tmpFile.close()
699             defaultEditor = "vi"
700             if platform.system() == "Windows":
701                 defaultEditor = "notepad"
702             if os.environ.has_key("P4EDITOR"):
703                 editor = os.environ.get("P4EDITOR")
704             else:
705                 editor = os.environ.get("EDITOR", defaultEditor);
706             system(editor + " " + fileName)
707             tmpFile = open(fileName, "rb")
708             message = tmpFile.read()
709             tmpFile.close()
710             os.remove(fileName)
711             submitTemplate = message[:message.index(separatorLine)]
712             if self.isWindows:
713                 submitTemplate = submitTemplate.replace("\r\n", "\n")
715             write_pipe("p4 submit -i", submitTemplate)
716         else:
717             fileName = "submit.txt"
718             file = open(fileName, "w+")
719             file.write(self.prepareLogMessage(template, logMessage))
720             file.close()
721             print ("Perforce submit template written as %s. "
722                    + "Please review/edit and then use p4 submit -i < %s to submit directly!"
723                    % (fileName, fileName))
725     def run(self, args):
726         if len(args) == 0:
727             self.master = currentGitBranch()
728             if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
729                 die("Detecting current git branch failed!")
730         elif len(args) == 1:
731             self.master = args[0]
732         else:
733             return False
735         allowSubmit = gitConfig("git-p4.allowSubmit")
736         if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
737             die("%s is not in git-p4.allowSubmit" % self.master)
739         [upstream, settings] = findUpstreamBranchPoint()
740         self.depotPath = settings['depot-paths'][0]
741         if len(self.origin) == 0:
742             self.origin = upstream
744         if self.verbose:
745             print "Origin branch is " + self.origin
747         if len(self.depotPath) == 0:
748             print "Internal error: cannot locate perforce depot path from existing branches"
749             sys.exit(128)
751         self.clientPath = p4Where(self.depotPath)
753         if len(self.clientPath) == 0:
754             print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
755             sys.exit(128)
757         print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
758         self.oldWorkingDirectory = os.getcwd()
760         os.chdir(self.clientPath)
761         print "Syncronizing p4 checkout..."
762         p4_system("sync ...")
764         self.check()
766         commits = []
767         for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
768             commits.append(line.strip())
769         commits.reverse()
771         while len(commits) > 0:
772             commit = commits[0]
773             commits = commits[1:]
774             self.applyCommit(commit)
775             if not self.interactive:
776                 break
778         if len(commits) == 0:
779             print "All changes applied!"
780             os.chdir(self.oldWorkingDirectory)
782             sync = P4Sync()
783             sync.run([])
785             rebase = P4Rebase()
786             rebase.rebase()
788         return True
790 class P4Sync(Command):
791     def __init__(self):
792         Command.__init__(self)
793         self.options = [
794                 optparse.make_option("--branch", dest="branch"),
795                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
796                 optparse.make_option("--changesfile", dest="changesFile"),
797                 optparse.make_option("--silent", dest="silent", action="store_true"),
798                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
799                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
800                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
801                                      help="Import into refs/heads/ , not refs/remotes"),
802                 optparse.make_option("--max-changes", dest="maxChanges"),
803                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
804                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
805                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
806                                      help="Only sync files that are included in the Perforce Client Spec")
807         ]
808         self.description = """Imports from Perforce into a git repository.\n
809     example:
810     //depot/my/project/ -- to import the current head
811     //depot/my/project/@all -- to import everything
812     //depot/my/project/@1,6 -- to import only from revision 1 to 6
814     (a ... is not needed in the path p4 specification, it's added implicitly)"""
816         self.usage += " //depot/path[@revRange]"
817         self.silent = False
818         self.createdBranches = Set()
819         self.committedChanges = Set()
820         self.branch = ""
821         self.detectBranches = False
822         self.detectLabels = False
823         self.changesFile = ""
824         self.syncWithOrigin = True
825         self.verbose = False
826         self.importIntoRemotes = True
827         self.maxChanges = ""
828         self.isWindows = (platform.system() == "Windows")
829         self.keepRepoPath = False
830         self.depotPaths = None
831         self.p4BranchesInGit = []
832         self.cloneExclude = []
833         self.useClientSpec = False
834         self.clientSpecDirs = []
836         if gitConfig("git-p4.syncFromOrigin") == "false":
837             self.syncWithOrigin = False
839     def extractFilesFromCommit(self, commit):
840         self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
841                              for path in self.cloneExclude]
842         files = []
843         fnum = 0
844         while commit.has_key("depotFile%s" % fnum):
845             path =  commit["depotFile%s" % fnum]
847             if [p for p in self.cloneExclude
848                 if path.startswith (p)]:
849                 found = False
850             else:
851                 found = [p for p in self.depotPaths
852                          if path.startswith (p)]
853             if not found:
854                 fnum = fnum + 1
855                 continue
857             file = {}
858             file["path"] = path
859             file["rev"] = commit["rev%s" % fnum]
860             file["action"] = commit["action%s" % fnum]
861             file["type"] = commit["type%s" % fnum]
862             files.append(file)
863             fnum = fnum + 1
864         return files
866     def stripRepoPath(self, path, prefixes):
867         if self.keepRepoPath:
868             prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
870         for p in prefixes:
871             if path.startswith(p):
872                 path = path[len(p):]
874         return path
876     def splitFilesIntoBranches(self, commit):
877         branches = {}
878         fnum = 0
879         while commit.has_key("depotFile%s" % fnum):
880             path =  commit["depotFile%s" % fnum]
881             found = [p for p in self.depotPaths
882                      if path.startswith (p)]
883             if not found:
884                 fnum = fnum + 1
885                 continue
887             file = {}
888             file["path"] = path
889             file["rev"] = commit["rev%s" % fnum]
890             file["action"] = commit["action%s" % fnum]
891             file["type"] = commit["type%s" % fnum]
892             fnum = fnum + 1
894             relPath = self.stripRepoPath(path, self.depotPaths)
896             for branch in self.knownBranches.keys():
898                 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
899                 if relPath.startswith(branch + "/"):
900                     if branch not in branches:
901                         branches[branch] = []
902                     branches[branch].append(file)
903                     break
905         return branches
907     ## Should move this out, doesn't use SELF.
908     def readP4Files(self, files):
909         filesForCommit = []
910         filesToRead = []
912         for f in files:
913             includeFile = True
914             for val in self.clientSpecDirs:
915                 if f['path'].startswith(val[0]):
916                     if val[1] <= 0:
917                         includeFile = False
918                     break
920             if includeFile:
921                 filesForCommit.append(f)
922                 if f['action'] != 'delete':
923                     filesToRead.append(f)
925         filedata = []
926         if len(filesToRead) > 0:
927             filedata = p4CmdList('-x - print',
928                                  stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
929                                                   for f in filesToRead]),
930                                  stdin_mode='w+')
932             if "p4ExitCode" in filedata[0]:
933                 die("Problems executing p4. Error: [%d]."
934                     % (filedata[0]['p4ExitCode']));
936         j = 0;
937         contents = {}
938         while j < len(filedata):
939             stat = filedata[j]
940             j += 1
941             text = [];
942             while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
943                 text.append(filedata[j]['data'])
944                 j += 1
945             text = ''.join(text)
947             if not stat.has_key('depotFile'):
948                 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
949                 continue
951             if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
952                 text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
953             elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
954                 text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', text)
956             contents[stat['depotFile']] = text
958         for f in filesForCommit:
959             path = f['path']
960             if contents.has_key(path):
961                 f['data'] = contents[path]
963         return filesForCommit
965     def commit(self, details, files, branch, branchPrefixes, parent = ""):
966         epoch = details["time"]
967         author = details["user"]
969         if self.verbose:
970             print "commit into %s" % branch
972         # start with reading files; if that fails, we should not
973         # create a commit.
974         new_files = []
975         for f in files:
976             if [p for p in branchPrefixes if f['path'].startswith(p)]:
977                 new_files.append (f)
978             else:
979                 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
980         files = self.readP4Files(new_files)
982         self.gitStream.write("commit %s\n" % branch)
983 #        gitStream.write("mark :%s\n" % details["change"])
984         self.committedChanges.add(int(details["change"]))
985         committer = ""
986         if author not in self.users:
987             self.getUserMapFromPerforceServer()
988         if author in self.users:
989             committer = "%s %s %s" % (self.users[author], epoch, self.tz)
990         else:
991             committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
993         self.gitStream.write("committer %s\n" % committer)
995         self.gitStream.write("data <<EOT\n")
996         self.gitStream.write(details["desc"])
997         self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
998                              % (','.join (branchPrefixes), details["change"]))
999         if len(details['options']) > 0:
1000             self.gitStream.write(": options = %s" % details['options'])
1001         self.gitStream.write("]\nEOT\n\n")
1003         if len(parent) > 0:
1004             if self.verbose:
1005                 print "parent %s" % parent
1006             self.gitStream.write("from %s\n" % parent)
1008         for file in files:
1009             if file["type"] == "apple":
1010                 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1011                 continue
1013             relPath = self.stripRepoPath(file['path'], branchPrefixes)
1014             if file["action"] == "delete":
1015                 self.gitStream.write("D %s\n" % relPath)
1016             else:
1017                 data = file['data']
1019                 mode = "644"
1020                 if isP4Exec(file["type"]):
1021                     mode = "755"
1022                 elif file["type"] == "symlink":
1023                     mode = "120000"
1024                     # p4 print on a symlink contains "target\n", so strip it off
1025                     data = data[:-1]
1027                 if self.isWindows and file["type"].endswith("text"):
1028                     data = data.replace("\r\n", "\n")
1030                 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1031                 self.gitStream.write("data %s\n" % len(data))
1032                 self.gitStream.write(data)
1033                 self.gitStream.write("\n")
1035         self.gitStream.write("\n")
1037         change = int(details["change"])
1039         if self.labels.has_key(change):
1040             label = self.labels[change]
1041             labelDetails = label[0]
1042             labelRevisions = label[1]
1043             if self.verbose:
1044                 print "Change %s is labelled %s" % (change, labelDetails)
1046             files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1047                                                     for p in branchPrefixes]))
1049             if len(files) == len(labelRevisions):
1051                 cleanedFiles = {}
1052                 for info in files:
1053                     if info["action"] == "delete":
1054                         continue
1055                     cleanedFiles[info["depotFile"]] = info["rev"]
1057                 if cleanedFiles == labelRevisions:
1058                     self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1059                     self.gitStream.write("from %s\n" % branch)
1061                     owner = labelDetails["Owner"]
1062                     tagger = ""
1063                     if author in self.users:
1064                         tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1065                     else:
1066                         tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1067                     self.gitStream.write("tagger %s\n" % tagger)
1068                     self.gitStream.write("data <<EOT\n")
1069                     self.gitStream.write(labelDetails["Description"])
1070                     self.gitStream.write("EOT\n\n")
1072                 else:
1073                     if not self.silent:
1074                         print ("Tag %s does not match with change %s: files do not match."
1075                                % (labelDetails["label"], change))
1077             else:
1078                 if not self.silent:
1079                     print ("Tag %s does not match with change %s: file count is different."
1080                            % (labelDetails["label"], change))
1082     def getUserCacheFilename(self):
1083         home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1084         return home + "/.gitp4-usercache.txt"
1086     def getUserMapFromPerforceServer(self):
1087         if self.userMapFromPerforceServer:
1088             return
1089         self.users = {}
1091         for output in p4CmdList("users"):
1092             if not output.has_key("User"):
1093                 continue
1094             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1097         s = ''
1098         for (key, val) in self.users.items():
1099             s += "%s\t%s\n" % (key, val)
1101         open(self.getUserCacheFilename(), "wb").write(s)
1102         self.userMapFromPerforceServer = True
1104     def loadUserMapFromCache(self):
1105         self.users = {}
1106         self.userMapFromPerforceServer = False
1107         try:
1108             cache = open(self.getUserCacheFilename(), "rb")
1109             lines = cache.readlines()
1110             cache.close()
1111             for line in lines:
1112                 entry = line.strip().split("\t")
1113                 self.users[entry[0]] = entry[1]
1114         except IOError:
1115             self.getUserMapFromPerforceServer()
1117     def getLabels(self):
1118         self.labels = {}
1120         l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1121         if len(l) > 0 and not self.silent:
1122             print "Finding files belonging to labels in %s" % `self.depotPaths`
1124         for output in l:
1125             label = output["label"]
1126             revisions = {}
1127             newestChange = 0
1128             if self.verbose:
1129                 print "Querying files for label %s" % label
1130             for file in p4CmdList("files "
1131                                   +  ' '.join (["%s...@%s" % (p, label)
1132                                                 for p in self.depotPaths])):
1133                 revisions[file["depotFile"]] = file["rev"]
1134                 change = int(file["change"])
1135                 if change > newestChange:
1136                     newestChange = change
1138             self.labels[newestChange] = [output, revisions]
1140         if self.verbose:
1141             print "Label changes: %s" % self.labels.keys()
1143     def guessProjectName(self):
1144         for p in self.depotPaths:
1145             if p.endswith("/"):
1146                 p = p[:-1]
1147             p = p[p.strip().rfind("/") + 1:]
1148             if not p.endswith("/"):
1149                p += "/"
1150             return p
1152     def getBranchMapping(self):
1153         lostAndFoundBranches = set()
1155         for info in p4CmdList("branches"):
1156             details = p4Cmd("branch -o %s" % info["branch"])
1157             viewIdx = 0
1158             while details.has_key("View%s" % viewIdx):
1159                 paths = details["View%s" % viewIdx].split(" ")
1160                 viewIdx = viewIdx + 1
1161                 # require standard //depot/foo/... //depot/bar/... mapping
1162                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1163                     continue
1164                 source = paths[0]
1165                 destination = paths[1]
1166                 ## HACK
1167                 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1168                     source = source[len(self.depotPaths[0]):-4]
1169                     destination = destination[len(self.depotPaths[0]):-4]
1171                     if destination in self.knownBranches:
1172                         if not self.silent:
1173                             print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1174                             print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1175                         continue
1177                     self.knownBranches[destination] = source
1179                     lostAndFoundBranches.discard(destination)
1181                     if source not in self.knownBranches:
1182                         lostAndFoundBranches.add(source)
1185         for branch in lostAndFoundBranches:
1186             self.knownBranches[branch] = branch
1188     def getBranchMappingFromGitBranches(self):
1189         branches = p4BranchesInGit(self.importIntoRemotes)
1190         for branch in branches.keys():
1191             if branch == "master":
1192                 branch = "main"
1193             else:
1194                 branch = branch[len(self.projectName):]
1195             self.knownBranches[branch] = branch
1197     def listExistingP4GitBranches(self):
1198         # branches holds mapping from name to commit
1199         branches = p4BranchesInGit(self.importIntoRemotes)
1200         self.p4BranchesInGit = branches.keys()
1201         for branch in branches.keys():
1202             self.initialParents[self.refPrefix + branch] = branches[branch]
1204     def updateOptionDict(self, d):
1205         option_keys = {}
1206         if self.keepRepoPath:
1207             option_keys['keepRepoPath'] = 1
1209         d["options"] = ' '.join(sorted(option_keys.keys()))
1211     def readOptions(self, d):
1212         self.keepRepoPath = (d.has_key('options')
1213                              and ('keepRepoPath' in d['options']))
1215     def gitRefForBranch(self, branch):
1216         if branch == "main":
1217             return self.refPrefix + "master"
1219         if len(branch) <= 0:
1220             return branch
1222         return self.refPrefix + self.projectName + branch
1224     def gitCommitByP4Change(self, ref, change):
1225         if self.verbose:
1226             print "looking in ref " + ref + " for change %s using bisect..." % change
1228         earliestCommit = ""
1229         latestCommit = parseRevision(ref)
1231         while True:
1232             if self.verbose:
1233                 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1234             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1235             if len(next) == 0:
1236                 if self.verbose:
1237                     print "argh"
1238                 return ""
1239             log = extractLogMessageFromGitCommit(next)
1240             settings = extractSettingsGitLog(log)
1241             currentChange = int(settings['change'])
1242             if self.verbose:
1243                 print "current change %s" % currentChange
1245             if currentChange == change:
1246                 if self.verbose:
1247                     print "found %s" % next
1248                 return next
1250             if currentChange < change:
1251                 earliestCommit = "^%s" % next
1252             else:
1253                 latestCommit = "%s" % next
1255         return ""
1257     def importNewBranch(self, branch, maxChange):
1258         # make fast-import flush all changes to disk and update the refs using the checkpoint
1259         # command so that we can try to find the branch parent in the git history
1260         self.gitStream.write("checkpoint\n\n");
1261         self.gitStream.flush();
1262         branchPrefix = self.depotPaths[0] + branch + "/"
1263         range = "@1,%s" % maxChange
1264         #print "prefix" + branchPrefix
1265         changes = p4ChangesForPaths([branchPrefix], range)
1266         if len(changes) <= 0:
1267             return False
1268         firstChange = changes[0]
1269         #print "first change in branch: %s" % firstChange
1270         sourceBranch = self.knownBranches[branch]
1271         sourceDepotPath = self.depotPaths[0] + sourceBranch
1272         sourceRef = self.gitRefForBranch(sourceBranch)
1273         #print "source " + sourceBranch
1275         branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1276         #print "branch parent: %s" % branchParentChange
1277         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1278         if len(gitParent) > 0:
1279             self.initialParents[self.gitRefForBranch(branch)] = gitParent
1280             #print "parent git commit: %s" % gitParent
1282         self.importChanges(changes)
1283         return True
1285     def importChanges(self, changes):
1286         cnt = 1
1287         for change in changes:
1288             description = p4Cmd("describe %s" % change)
1289             self.updateOptionDict(description)
1291             if not self.silent:
1292                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1293                 sys.stdout.flush()
1294             cnt = cnt + 1
1296             try:
1297                 if self.detectBranches:
1298                     branches = self.splitFilesIntoBranches(description)
1299                     for branch in branches.keys():
1300                         ## HACK  --hwn
1301                         branchPrefix = self.depotPaths[0] + branch + "/"
1303                         parent = ""
1305                         filesForCommit = branches[branch]
1307                         if self.verbose:
1308                             print "branch is %s" % branch
1310                         self.updatedBranches.add(branch)
1312                         if branch not in self.createdBranches:
1313                             self.createdBranches.add(branch)
1314                             parent = self.knownBranches[branch]
1315                             if parent == branch:
1316                                 parent = ""
1317                             else:
1318                                 fullBranch = self.projectName + branch
1319                                 if fullBranch not in self.p4BranchesInGit:
1320                                     if not self.silent:
1321                                         print("\n    Importing new branch %s" % fullBranch);
1322                                     if self.importNewBranch(branch, change - 1):
1323                                         parent = ""
1324                                         self.p4BranchesInGit.append(fullBranch)
1325                                     if not self.silent:
1326                                         print("\n    Resuming with change %s" % change);
1328                                 if self.verbose:
1329                                     print "parent determined through known branches: %s" % parent
1331                         branch = self.gitRefForBranch(branch)
1332                         parent = self.gitRefForBranch(parent)
1334                         if self.verbose:
1335                             print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1337                         if len(parent) == 0 and branch in self.initialParents:
1338                             parent = self.initialParents[branch]
1339                             del self.initialParents[branch]
1341                         self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1342                 else:
1343                     files = self.extractFilesFromCommit(description)
1344                     self.commit(description, files, self.branch, self.depotPaths,
1345                                 self.initialParent)
1346                     self.initialParent = ""
1347             except IOError:
1348                 print self.gitError.read()
1349                 sys.exit(1)
1351     def importHeadRevision(self, revision):
1352         print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1354         details = { "user" : "git perforce import user", "time" : int(time.time()) }
1355         details["desc"] = ("Initial import of %s from the state at revision %s"
1356                            % (' '.join(self.depotPaths), revision))
1357         details["change"] = revision
1358         newestRevision = 0
1360         fileCnt = 0
1361         for info in p4CmdList("files "
1362                               +  ' '.join(["%s...%s"
1363                                            % (p, revision)
1364                                            for p in self.depotPaths])):
1366             if info['code'] == 'error':
1367                 sys.stderr.write("p4 returned an error: %s\n"
1368                                  % info['data'])
1369                 sys.exit(1)
1372             change = int(info["change"])
1373             if change > newestRevision:
1374                 newestRevision = change
1376             if info["action"] == "delete":
1377                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1378                 #fileCnt = fileCnt + 1
1379                 continue
1381             for prop in ["depotFile", "rev", "action", "type" ]:
1382                 details["%s%s" % (prop, fileCnt)] = info[prop]
1384             fileCnt = fileCnt + 1
1386         details["change"] = newestRevision
1387         self.updateOptionDict(details)
1388         try:
1389             self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1390         except IOError:
1391             print "IO error with git fast-import. Is your git version recent enough?"
1392             print self.gitError.read()
1395     def getClientSpec(self):
1396         specList = p4CmdList( "client -o" )
1397         temp = {}
1398         for entry in specList:
1399             for k,v in entry.iteritems():
1400                 if k.startswith("View"):
1401                     if v.startswith('"'):
1402                         start = 1
1403                     else:
1404                         start = 0
1405                     index = v.find("...")
1406                     v = v[start:index]
1407                     if v.startswith("-"):
1408                         v = v[1:]
1409                         temp[v] = -len(v)
1410                     else:
1411                         temp[v] = len(v)
1412         self.clientSpecDirs = temp.items()
1413         self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1415     def run(self, args):
1416         self.depotPaths = []
1417         self.changeRange = ""
1418         self.initialParent = ""
1419         self.previousDepotPaths = []
1421         # map from branch depot path to parent branch
1422         self.knownBranches = {}
1423         self.initialParents = {}
1424         self.hasOrigin = originP4BranchesExist()
1425         if not self.syncWithOrigin:
1426             self.hasOrigin = False
1428         if self.importIntoRemotes:
1429             self.refPrefix = "refs/remotes/p4/"
1430         else:
1431             self.refPrefix = "refs/heads/p4/"
1433         if self.syncWithOrigin and self.hasOrigin:
1434             if not self.silent:
1435                 print "Syncing with origin first by calling git fetch origin"
1436             system("git fetch origin")
1438         if len(self.branch) == 0:
1439             self.branch = self.refPrefix + "master"
1440             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1441                 system("git update-ref %s refs/heads/p4" % self.branch)
1442                 system("git branch -D p4");
1443             # create it /after/ importing, when master exists
1444             if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1445                 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1447         if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1448             self.getClientSpec()
1450         # TODO: should always look at previous commits,
1451         # merge with previous imports, if possible.
1452         if args == []:
1453             if self.hasOrigin:
1454                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1455             self.listExistingP4GitBranches()
1457             if len(self.p4BranchesInGit) > 1:
1458                 if not self.silent:
1459                     print "Importing from/into multiple branches"
1460                 self.detectBranches = True
1462             if self.verbose:
1463                 print "branches: %s" % self.p4BranchesInGit
1465             p4Change = 0
1466             for branch in self.p4BranchesInGit:
1467                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1469                 settings = extractSettingsGitLog(logMsg)
1471                 self.readOptions(settings)
1472                 if (settings.has_key('depot-paths')
1473                     and settings.has_key ('change')):
1474                     change = int(settings['change']) + 1
1475                     p4Change = max(p4Change, change)
1477                     depotPaths = sorted(settings['depot-paths'])
1478                     if self.previousDepotPaths == []:
1479                         self.previousDepotPaths = depotPaths
1480                     else:
1481                         paths = []
1482                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1483                             for i in range(0, min(len(cur), len(prev))):
1484                                 if cur[i] <> prev[i]:
1485                                     i = i - 1
1486                                     break
1488                             paths.append (cur[:i + 1])
1490                         self.previousDepotPaths = paths
1492             if p4Change > 0:
1493                 self.depotPaths = sorted(self.previousDepotPaths)
1494                 self.changeRange = "@%s,#head" % p4Change
1495                 if not self.detectBranches:
1496                     self.initialParent = parseRevision(self.branch)
1497                 if not self.silent and not self.detectBranches:
1498                     print "Performing incremental import into %s git branch" % self.branch
1500         if not self.branch.startswith("refs/"):
1501             self.branch = "refs/heads/" + self.branch
1503         if len(args) == 0 and self.depotPaths:
1504             if not self.silent:
1505                 print "Depot paths: %s" % ' '.join(self.depotPaths)
1506         else:
1507             if self.depotPaths and self.depotPaths != args:
1508                 print ("previous import used depot path %s and now %s was specified. "
1509                        "This doesn't work!" % (' '.join (self.depotPaths),
1510                                                ' '.join (args)))
1511                 sys.exit(1)
1513             self.depotPaths = sorted(args)
1515         revision = ""
1516         self.users = {}
1518         newPaths = []
1519         for p in self.depotPaths:
1520             if p.find("@") != -1:
1521                 atIdx = p.index("@")
1522                 self.changeRange = p[atIdx:]
1523                 if self.changeRange == "@all":
1524                     self.changeRange = ""
1525                 elif ',' not in self.changeRange:
1526                     revision = self.changeRange
1527                     self.changeRange = ""
1528                 p = p[:atIdx]
1529             elif p.find("#") != -1:
1530                 hashIdx = p.index("#")
1531                 revision = p[hashIdx:]
1532                 p = p[:hashIdx]
1533             elif self.previousDepotPaths == []:
1534                 revision = "#head"
1536             p = re.sub ("\.\.\.$", "", p)
1537             if not p.endswith("/"):
1538                 p += "/"
1540             newPaths.append(p)
1542         self.depotPaths = newPaths
1545         self.loadUserMapFromCache()
1546         self.labels = {}
1547         if self.detectLabels:
1548             self.getLabels();
1550         if self.detectBranches:
1551             ## FIXME - what's a P4 projectName ?
1552             self.projectName = self.guessProjectName()
1554             if self.hasOrigin:
1555                 self.getBranchMappingFromGitBranches()
1556             else:
1557                 self.getBranchMapping()
1558             if self.verbose:
1559                 print "p4-git branches: %s" % self.p4BranchesInGit
1560                 print "initial parents: %s" % self.initialParents
1561             for b in self.p4BranchesInGit:
1562                 if b != "master":
1564                     ## FIXME
1565                     b = b[len(self.projectName):]
1566                 self.createdBranches.add(b)
1568         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1570         importProcess = subprocess.Popen(["git", "fast-import"],
1571                                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1572                                          stderr=subprocess.PIPE);
1573         self.gitOutput = importProcess.stdout
1574         self.gitStream = importProcess.stdin
1575         self.gitError = importProcess.stderr
1577         if revision:
1578             self.importHeadRevision(revision)
1579         else:
1580             changes = []
1582             if len(self.changesFile) > 0:
1583                 output = open(self.changesFile).readlines()
1584                 changeSet = Set()
1585                 for line in output:
1586                     changeSet.add(int(line))
1588                 for change in changeSet:
1589                     changes.append(change)
1591                 changes.sort()
1592             else:
1593                 if self.verbose:
1594                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1595                                                               self.changeRange)
1596                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1598                 if len(self.maxChanges) > 0:
1599                     changes = changes[:min(int(self.maxChanges), len(changes))]
1601             if len(changes) == 0:
1602                 if not self.silent:
1603                     print "No changes to import!"
1604                 return True
1606             if not self.silent and not self.detectBranches:
1607                 print "Import destination: %s" % self.branch
1609             self.updatedBranches = set()
1611             self.importChanges(changes)
1613             if not self.silent:
1614                 print ""
1615                 if len(self.updatedBranches) > 0:
1616                     sys.stdout.write("Updated branches: ")
1617                     for b in self.updatedBranches:
1618                         sys.stdout.write("%s " % b)
1619                     sys.stdout.write("\n")
1621         self.gitStream.close()
1622         if importProcess.wait() != 0:
1623             die("fast-import failed: %s" % self.gitError.read())
1624         self.gitOutput.close()
1625         self.gitError.close()
1627         return True
1629 class P4Rebase(Command):
1630     def __init__(self):
1631         Command.__init__(self)
1632         self.options = [ ]
1633         self.description = ("Fetches the latest revision from perforce and "
1634                             + "rebases the current work (branch) against it")
1635         self.verbose = False
1637     def run(self, args):
1638         sync = P4Sync()
1639         sync.run([])
1641         return self.rebase()
1643     def rebase(self):
1644         if os.system("git update-index --refresh") != 0:
1645             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.");
1646         if len(read_pipe("git diff-index HEAD --")) > 0:
1647             die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1649         [upstream, settings] = findUpstreamBranchPoint()
1650         if len(upstream) == 0:
1651             die("Cannot find upstream branchpoint for rebase")
1653         # the branchpoint may be p4/foo~3, so strip off the parent
1654         upstream = re.sub("~[0-9]+$", "", upstream)
1656         print "Rebasing the current branch onto %s" % upstream
1657         oldHead = read_pipe("git rev-parse HEAD").strip()
1658         system("git rebase %s" % upstream)
1659         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1660         return True
1662 class P4Clone(P4Sync):
1663     def __init__(self):
1664         P4Sync.__init__(self)
1665         self.description = "Creates a new git repository and imports from Perforce into it"
1666         self.usage = "usage: %prog [options] //depot/path[@revRange]"
1667         self.options += [
1668             optparse.make_option("--destination", dest="cloneDestination",
1669                                  action='store', default=None,
1670                                  help="where to leave result of the clone"),
1671             optparse.make_option("-/", dest="cloneExclude",
1672                                  action="append", type="string",
1673                                  help="exclude depot path")
1674         ]
1675         self.cloneDestination = None
1676         self.needsGit = False
1678     # This is required for the "append" cloneExclude action
1679     def ensure_value(self, attr, value):
1680         if not hasattr(self, attr) or getattr(self, attr) is None:
1681             setattr(self, attr, value)
1682         return getattr(self, attr)
1684     def defaultDestination(self, args):
1685         ## TODO: use common prefix of args?
1686         depotPath = args[0]
1687         depotDir = re.sub("(@[^@]*)$", "", depotPath)
1688         depotDir = re.sub("(#[^#]*)$", "", depotDir)
1689         depotDir = re.sub(r"\.\.\.$", "", depotDir)
1690         depotDir = re.sub(r"/$", "", depotDir)
1691         return os.path.split(depotDir)[1]
1693     def run(self, args):
1694         if len(args) < 1:
1695             return False
1697         if self.keepRepoPath and not self.cloneDestination:
1698             sys.stderr.write("Must specify destination for --keep-path\n")
1699             sys.exit(1)
1701         depotPaths = args
1703         if not self.cloneDestination and len(depotPaths) > 1:
1704             self.cloneDestination = depotPaths[-1]
1705             depotPaths = depotPaths[:-1]
1707         self.cloneExclude = ["/"+p for p in self.cloneExclude]
1708         for p in depotPaths:
1709             if not p.startswith("//"):
1710                 return False
1712         if not self.cloneDestination:
1713             self.cloneDestination = self.defaultDestination(args)
1715         print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1716         if not os.path.exists(self.cloneDestination):
1717             os.makedirs(self.cloneDestination)
1718         os.chdir(self.cloneDestination)
1719         system("git init")
1720         self.gitdir = os.getcwd() + "/.git"
1721         if not P4Sync.run(self, depotPaths):
1722             return False
1723         if self.branch != "master":
1724             if gitBranchExists("refs/remotes/p4/master"):
1725                 system("git branch master refs/remotes/p4/master")
1726                 system("git checkout -f")
1727             else:
1728                 print "Could not detect main branch. No checkout/master branch created."
1730         return True
1732 class P4Branches(Command):
1733     def __init__(self):
1734         Command.__init__(self)
1735         self.options = [ ]
1736         self.description = ("Shows the git branches that hold imports and their "
1737                             + "corresponding perforce depot paths")
1738         self.verbose = False
1740     def run(self, args):
1741         if originP4BranchesExist():
1742             createOrUpdateBranchesFromOrigin()
1744         cmdline = "git rev-parse --symbolic "
1745         cmdline += " --remotes"
1747         for line in read_pipe_lines(cmdline):
1748             line = line.strip()
1750             if not line.startswith('p4/') or line == "p4/HEAD":
1751                 continue
1752             branch = line
1754             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1755             settings = extractSettingsGitLog(log)
1757             print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1758         return True
1760 class HelpFormatter(optparse.IndentedHelpFormatter):
1761     def __init__(self):
1762         optparse.IndentedHelpFormatter.__init__(self)
1764     def format_description(self, description):
1765         if description:
1766             return description + "\n"
1767         else:
1768             return ""
1770 def printUsage(commands):
1771     print "usage: %s <command> [options]" % sys.argv[0]
1772     print ""
1773     print "valid commands: %s" % ", ".join(commands)
1774     print ""
1775     print "Try %s <command> --help for command specific help." % sys.argv[0]
1776     print ""
1778 commands = {
1779     "debug" : P4Debug,
1780     "submit" : P4Submit,
1781     "commit" : P4Submit,
1782     "sync" : P4Sync,
1783     "rebase" : P4Rebase,
1784     "clone" : P4Clone,
1785     "rollback" : P4RollBack,
1786     "branches" : P4Branches
1790 def main():
1791     if len(sys.argv[1:]) == 0:
1792         printUsage(commands.keys())
1793         sys.exit(2)
1795     cmd = ""
1796     cmdName = sys.argv[1]
1797     try:
1798         klass = commands[cmdName]
1799         cmd = klass()
1800     except KeyError:
1801         print "unknown command %s" % cmdName
1802         print ""
1803         printUsage(commands.keys())
1804         sys.exit(2)
1806     options = cmd.options
1807     cmd.gitdir = os.environ.get("GIT_DIR", None)
1809     args = sys.argv[2:]
1811     if len(options) > 0:
1812         options.append(optparse.make_option("--git-dir", dest="gitdir"))
1814         parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1815                                        options,
1816                                        description = cmd.description,
1817                                        formatter = HelpFormatter())
1819         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1820     global verbose
1821     verbose = cmd.verbose
1822     if cmd.needsGit:
1823         if cmd.gitdir == None:
1824             cmd.gitdir = os.path.abspath(".git")
1825             if not isValidGitDir(cmd.gitdir):
1826                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1827                 if os.path.exists(cmd.gitdir):
1828                     cdup = read_pipe("git rev-parse --show-cdup").strip()
1829                     if len(cdup) > 0:
1830                         os.chdir(cdup);
1832         if not isValidGitDir(cmd.gitdir):
1833             if isValidGitDir(cmd.gitdir + "/.git"):
1834                 cmd.gitdir += "/.git"
1835             else:
1836                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1838         os.environ["GIT_DIR"] = cmd.gitdir
1840     if not cmd.run(args):
1841         parser.print_help()
1844 if __name__ == '__main__':
1845     main()