Code

Merge branch 'maint'
[git.git] / contrib / fast-import / git-p4
1 #!/usr/bin/env python
2 #
3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
4 #
5 # Author: Simon Hausmann <simon@lst.de>
6 # Copyright: 2007 Simon Hausmann <simon@lst.de>
7 #            2007 Trolltech ASA
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
9 #
11 import optparse, sys, os, marshal, popen2, subprocess, shelve
12 import tempfile, getopt, sha, os.path, time, platform
13 import re
15 from sets import Set;
17 verbose = False
19 def die(msg):
20     if verbose:
21         raise Exception(msg)
22     else:
23         sys.stderr.write(msg + "\n")
24         sys.exit(1)
26 def write_pipe(c, str):
27     if verbose:
28         sys.stderr.write('Writing pipe: %s\n' % c)
30     pipe = os.popen(c, 'w')
31     val = pipe.write(str)
32     if pipe.close():
33         die('Command failed: %s' % c)
35     return val
37 def read_pipe(c, ignore_error=False):
38     if verbose:
39         sys.stderr.write('Reading pipe: %s\n' % c)
41     pipe = os.popen(c, 'rb')
42     val = pipe.read()
43     if pipe.close() and not ignore_error:
44         die('Command failed: %s' % c)
46     return val
49 def read_pipe_lines(c):
50     if verbose:
51         sys.stderr.write('Reading pipe: %s\n' % c)
52     ## todo: check return status
53     pipe = os.popen(c, 'rb')
54     val = pipe.readlines()
55     if pipe.close():
56         die('Command failed: %s' % c)
58     return val
60 def system(cmd):
61     if verbose:
62         sys.stderr.write("executing %s\n" % cmd)
63     if os.system(cmd) != 0:
64         die("command failed: %s" % cmd)
66 def isP4Exec(kind):
67     """Determine if a Perforce 'kind' should have execute permission
69     'p4 help filetypes' gives a list of the types.  If it starts with 'x',
70     or x follows one of a few letters.  Otherwise, if there is an 'x' after
71     a plus sign, it is also executable"""
72     return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
74 def setP4ExecBit(file, mode):
75     # Reopens an already open file and changes the execute bit to match
76     # the execute bit setting in the passed in mode.
78     p4Type = "+x"
80     if not isModeExec(mode):
81         p4Type = getP4OpenedType(file)
82         p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
83         p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
84         if p4Type[-1] == "+":
85             p4Type = p4Type[0:-1]
87     system("p4 reopen -t %s %s" % (p4Type, file))
89 def getP4OpenedType(file):
90     # Returns the perforce file type for the given file.
92     result = read_pipe("p4 opened %s" % file)
93     match = re.match(".*\((.+)\)$", result)
94     if match:
95         return match.group(1)
96     else:
97         die("Could not determine file type for %s" % file)
99 def diffTreePattern():
100     # This is a simple generator for the diff tree regex pattern. This could be
101     # a class variable if this and parseDiffTreeEntry were a part of a class.
102     pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
103     while True:
104         yield pattern
106 def parseDiffTreeEntry(entry):
107     """Parses a single diff tree entry into its component elements.
109     See git-diff-tree(1) manpage for details about the format of the diff
110     output. This method returns a dictionary with the following elements:
112     src_mode - The mode of the source file
113     dst_mode - The mode of the destination file
114     src_sha1 - The sha1 for the source file
115     dst_sha1 - The sha1 fr the destination file
116     status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
117     status_score - The score for the status (applicable for 'C' and 'R'
118                    statuses). This is None if there is no score.
119     src - The path for the source file.
120     dst - The path for the destination file. This is only present for
121           copy or renames. If it is not present, this is None.
123     If the pattern is not matched, None is returned."""
125     match = diffTreePattern().next().match(entry)
126     if match:
127         return {
128             'src_mode': match.group(1),
129             'dst_mode': match.group(2),
130             'src_sha1': match.group(3),
131             'dst_sha1': match.group(4),
132             'status': match.group(5),
133             'status_score': match.group(6),
134             'src': match.group(7),
135             'dst': match.group(10)
136         }
137     return None
139 def isModeExec(mode):
140     # Returns True if the given git mode represents an executable file,
141     # otherwise False.
142     return mode[-3:] == "755"
144 def isModeExecChanged(src_mode, dst_mode):
145     return isModeExec(src_mode) != isModeExec(dst_mode)
147 def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
148     cmd = "p4 -G %s" % cmd
149     if verbose:
150         sys.stderr.write("Opening pipe: %s\n" % cmd)
152     # Use a temporary file to avoid deadlocks without
153     # subprocess.communicate(), which would put another copy
154     # of stdout into memory.
155     stdin_file = None
156     if stdin is not None:
157         stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
158         stdin_file.write(stdin)
159         stdin_file.flush()
160         stdin_file.seek(0)
162     p4 = subprocess.Popen(cmd, shell=True,
163                           stdin=stdin_file,
164                           stdout=subprocess.PIPE)
166     result = []
167     try:
168         while True:
169             entry = marshal.load(p4.stdout)
170             result.append(entry)
171     except EOFError:
172         pass
173     exitCode = p4.wait()
174     if exitCode != 0:
175         entry = {}
176         entry["p4ExitCode"] = exitCode
177         result.append(entry)
179     return result
181 def p4Cmd(cmd):
182     list = p4CmdList(cmd)
183     result = {}
184     for entry in list:
185         result.update(entry)
186     return result;
188 def p4Where(depotPath):
189     if not depotPath.endswith("/"):
190         depotPath += "/"
191     output = p4Cmd("where %s..." % depotPath)
192     if output["code"] == "error":
193         return ""
194     clientPath = ""
195     if "path" in output:
196         clientPath = output.get("path")
197     elif "data" in output:
198         data = output.get("data")
199         lastSpace = data.rfind(" ")
200         clientPath = data[lastSpace + 1:]
202     if clientPath.endswith("..."):
203         clientPath = clientPath[:-3]
204     return clientPath
206 def currentGitBranch():
207     return read_pipe("git name-rev HEAD").split(" ")[1].strip()
209 def isValidGitDir(path):
210     if (os.path.exists(path + "/HEAD")
211         and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
212         return True;
213     return False
215 def parseRevision(ref):
216     return read_pipe("git rev-parse %s" % ref).strip()
218 def extractLogMessageFromGitCommit(commit):
219     logMessage = ""
221     ## fixme: title is first line of commit, not 1st paragraph.
222     foundTitle = False
223     for log in read_pipe_lines("git cat-file commit %s" % commit):
224        if not foundTitle:
225            if len(log) == 1:
226                foundTitle = True
227            continue
229        logMessage += log
230     return logMessage
232 def extractSettingsGitLog(log):
233     values = {}
234     for line in log.split("\n"):
235         line = line.strip()
236         m = re.search (r"^ *\[git-p4: (.*)\]$", line)
237         if not m:
238             continue
240         assignments = m.group(1).split (':')
241         for a in assignments:
242             vals = a.split ('=')
243             key = vals[0].strip()
244             val = ('='.join (vals[1:])).strip()
245             if val.endswith ('\"') and val.startswith('"'):
246                 val = val[1:-1]
248             values[key] = val
250     paths = values.get("depot-paths")
251     if not paths:
252         paths = values.get("depot-path")
253     if paths:
254         values['depot-paths'] = paths.split(',')
255     return values
257 def gitBranchExists(branch):
258     proc = subprocess.Popen(["git", "rev-parse", branch],
259                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);
260     return proc.wait() == 0;
262 def gitConfig(key):
263     return read_pipe("git config %s" % key, ignore_error=True).strip()
265 def p4BranchesInGit(branchesAreInRemotes = True):
266     branches = {}
268     cmdline = "git rev-parse --symbolic "
269     if branchesAreInRemotes:
270         cmdline += " --remotes"
271     else:
272         cmdline += " --branches"
274     for line in read_pipe_lines(cmdline):
275         line = line.strip()
277         ## only import to p4/
278         if not line.startswith('p4/') or line == "p4/HEAD":
279             continue
280         branch = line
282         # strip off p4
283         branch = re.sub ("^p4/", "", line)
285         branches[branch] = parseRevision(line)
286     return branches
288 def findUpstreamBranchPoint(head = "HEAD"):
289     branches = p4BranchesInGit()
290     # map from depot-path to branch name
291     branchByDepotPath = {}
292     for branch in branches.keys():
293         tip = branches[branch]
294         log = extractLogMessageFromGitCommit(tip)
295         settings = extractSettingsGitLog(log)
296         if settings.has_key("depot-paths"):
297             paths = ",".join(settings["depot-paths"])
298             branchByDepotPath[paths] = "remotes/p4/" + branch
300     settings = None
301     parent = 0
302     while parent < 65535:
303         commit = head + "~%s" % parent
304         log = extractLogMessageFromGitCommit(commit)
305         settings = extractSettingsGitLog(log)
306         if settings.has_key("depot-paths"):
307             paths = ",".join(settings["depot-paths"])
308             if branchByDepotPath.has_key(paths):
309                 return [branchByDepotPath[paths], settings]
311         parent = parent + 1
313     return ["", settings]
315 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
316     if not silent:
317         print ("Creating/updating branch(es) in %s based on origin branch(es)"
318                % localRefPrefix)
320     originPrefix = "origin/p4/"
322     for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
323         line = line.strip()
324         if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
325             continue
327         headName = line[len(originPrefix):]
328         remoteHead = localRefPrefix + headName
329         originHead = line
331         original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
332         if (not original.has_key('depot-paths')
333             or not original.has_key('change')):
334             continue
336         update = False
337         if not gitBranchExists(remoteHead):
338             if verbose:
339                 print "creating %s" % remoteHead
340             update = True
341         else:
342             settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
343             if settings.has_key('change') > 0:
344                 if settings['depot-paths'] == original['depot-paths']:
345                     originP4Change = int(original['change'])
346                     p4Change = int(settings['change'])
347                     if originP4Change > p4Change:
348                         print ("%s (%s) is newer than %s (%s). "
349                                "Updating p4 branch from origin."
350                                % (originHead, originP4Change,
351                                   remoteHead, p4Change))
352                         update = True
353                 else:
354                     print ("Ignoring: %s was imported from %s while "
355                            "%s was imported from %s"
356                            % (originHead, ','.join(original['depot-paths']),
357                               remoteHead, ','.join(settings['depot-paths'])))
359         if update:
360             system("git update-ref %s %s" % (remoteHead, originHead))
362 def originP4BranchesExist():
363         return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
365 def p4ChangesForPaths(depotPaths, changeRange):
366     assert depotPaths
367     output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, changeRange)
368                                                         for p in depotPaths]))
370     changes = []
371     for line in output:
372         changeNum = line.split(" ")[1]
373         changes.append(int(changeNum))
375     changes.sort()
376     return changes
378 class Command:
379     def __init__(self):
380         self.usage = "usage: %prog [options]"
381         self.needsGit = True
383 class P4Debug(Command):
384     def __init__(self):
385         Command.__init__(self)
386         self.options = [
387             optparse.make_option("--verbose", dest="verbose", action="store_true",
388                                  default=False),
389             ]
390         self.description = "A tool to debug the output of p4 -G."
391         self.needsGit = False
392         self.verbose = False
394     def run(self, args):
395         j = 0
396         for output in p4CmdList(" ".join(args)):
397             print 'Element: %d' % j
398             j += 1
399             print output
400         return True
402 class P4RollBack(Command):
403     def __init__(self):
404         Command.__init__(self)
405         self.options = [
406             optparse.make_option("--verbose", dest="verbose", action="store_true"),
407             optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
408         ]
409         self.description = "A tool to debug the multi-branch import. Don't use :)"
410         self.verbose = False
411         self.rollbackLocalBranches = False
413     def run(self, args):
414         if len(args) != 1:
415             return False
416         maxChange = int(args[0])
418         if "p4ExitCode" in p4Cmd("changes -m 1"):
419             die("Problems executing p4");
421         if self.rollbackLocalBranches:
422             refPrefix = "refs/heads/"
423             lines = read_pipe_lines("git rev-parse --symbolic --branches")
424         else:
425             refPrefix = "refs/remotes/"
426             lines = read_pipe_lines("git rev-parse --symbolic --remotes")
428         for line in lines:
429             if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
430                 line = line.strip()
431                 ref = refPrefix + line
432                 log = extractLogMessageFromGitCommit(ref)
433                 settings = extractSettingsGitLog(log)
435                 depotPaths = settings['depot-paths']
436                 change = settings['change']
438                 changed = False
440                 if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
441                                                            for p in depotPaths]))) == 0:
442                     print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
443                     system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
444                     continue
446                 while change and int(change) > maxChange:
447                     changed = True
448                     if self.verbose:
449                         print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
450                     system("git update-ref %s \"%s^\"" % (ref, ref))
451                     log = extractLogMessageFromGitCommit(ref)
452                     settings =  extractSettingsGitLog(log)
455                     depotPaths = settings['depot-paths']
456                     change = settings['change']
458                 if changed:
459                     print "%s rewound to %s" % (ref, change)
461         return True
463 class P4Submit(Command):
464     def __init__(self):
465         Command.__init__(self)
466         self.options = [
467                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
468                 optparse.make_option("--origin", dest="origin"),
469                 optparse.make_option("-M", dest="detectRename", action="store_true"),
470         ]
471         self.description = "Submit changes from git to the perforce depot."
472         self.usage += " [name of git branch to submit into perforce depot]"
473         self.interactive = True
474         self.origin = ""
475         self.detectRename = False
476         self.verbose = False
477         self.isWindows = (platform.system() == "Windows")
479     def check(self):
480         if len(p4CmdList("opened ...")) > 0:
481             die("You have files opened with perforce! Close them before starting the sync.")
483     # replaces everything between 'Description:' and the next P4 submit template field with the
484     # commit message
485     def prepareLogMessage(self, template, message):
486         result = ""
488         inDescriptionSection = False
490         for line in template.split("\n"):
491             if line.startswith("#"):
492                 result += line + "\n"
493                 continue
495             if inDescriptionSection:
496                 if line.startswith("Files:"):
497                     inDescriptionSection = False
498                 else:
499                     continue
500             else:
501                 if line.startswith("Description:"):
502                     inDescriptionSection = True
503                     line += "\n"
504                     for messageLine in message.split("\n"):
505                         line += "\t" + messageLine + "\n"
507             result += line + "\n"
509         return result
511     def prepareSubmitTemplate(self):
512         # remove lines in the Files section that show changes to files outside the depot path we're committing into
513         template = ""
514         inFilesSection = False
515         for line in read_pipe_lines("p4 change -o"):
516             if inFilesSection:
517                 if line.startswith("\t"):
518                     # path starts and ends with a tab
519                     path = line[1:]
520                     lastTab = path.rfind("\t")
521                     if lastTab != -1:
522                         path = path[:lastTab]
523                         if not path.startswith(self.depotPath):
524                             continue
525                 else:
526                     inFilesSection = False
527             else:
528                 if line.startswith("Files:"):
529                     inFilesSection = True
531             template += line
533         return template
535     def applyCommit(self, id):
536         print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
537         diffOpts = ("", "-M")[self.detectRename]
538         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
539         filesToAdd = set()
540         filesToDelete = set()
541         editedFiles = set()
542         filesToChangeExecBit = {}
543         for line in diff:
544             diff = parseDiffTreeEntry(line)
545             modifier = diff['status']
546             path = diff['src']
547             if modifier == "M":
548                 system("p4 edit \"%s\"" % path)
549                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
550                     filesToChangeExecBit[path] = diff['dst_mode']
551                 editedFiles.add(path)
552             elif modifier == "A":
553                 filesToAdd.add(path)
554                 filesToChangeExecBit[path] = diff['dst_mode']
555                 if path in filesToDelete:
556                     filesToDelete.remove(path)
557             elif modifier == "D":
558                 filesToDelete.add(path)
559                 if path in filesToAdd:
560                     filesToAdd.remove(path)
561             elif modifier == "R":
562                 src, dest = diff['src'], diff['dst']
563                 system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
564                 system("p4 edit \"%s\"" % (dest))
565                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
566                     filesToChangeExecBit[dest] = diff['dst_mode']
567                 os.unlink(dest)
568                 editedFiles.add(dest)
569                 filesToDelete.add(src)
570             else:
571                 die("unknown modifier %s for %s" % (modifier, path))
573         diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
574         patchcmd = diffcmd + " | git apply "
575         tryPatchCmd = patchcmd + "--check -"
576         applyPatchCmd = patchcmd + "--check --apply -"
578         if os.system(tryPatchCmd) != 0:
579             print "Unfortunately applying the change failed!"
580             print "What do you want to do?"
581             response = "x"
582             while response != "s" and response != "a" and response != "w":
583                 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
584                                      "and with .rej files / [w]rite the patch to a file (patch.txt) ")
585             if response == "s":
586                 print "Skipping! Good luck with the next patches..."
587                 for f in editedFiles:
588                     system("p4 revert \"%s\"" % f);
589                 for f in filesToAdd:
590                     system("rm %s" %f)
591                 return
592             elif response == "a":
593                 os.system(applyPatchCmd)
594                 if len(filesToAdd) > 0:
595                     print "You may also want to call p4 add on the following files:"
596                     print " ".join(filesToAdd)
597                 if len(filesToDelete):
598                     print "The following files should be scheduled for deletion with p4 delete:"
599                     print " ".join(filesToDelete)
600                 die("Please resolve and submit the conflict manually and "
601                     + "continue afterwards with git-p4 submit --continue")
602             elif response == "w":
603                 system(diffcmd + " > patch.txt")
604                 print "Patch saved to patch.txt in %s !" % self.clientPath
605                 die("Please resolve and submit the conflict manually and "
606                     "continue afterwards with git-p4 submit --continue")
608         system(applyPatchCmd)
610         for f in filesToAdd:
611             system("p4 add \"%s\"" % f)
612         for f in filesToDelete:
613             system("p4 revert \"%s\"" % f)
614             system("p4 delete \"%s\"" % f)
616         # Set/clear executable bits
617         for f in filesToChangeExecBit.keys():
618             mode = filesToChangeExecBit[f]
619             setP4ExecBit(f, mode)
621         logMessage = extractLogMessageFromGitCommit(id)
622         if self.isWindows:
623             logMessage = logMessage.replace("\n", "\r\n")
624         logMessage = logMessage.strip()
626         template = self.prepareSubmitTemplate()
628         if self.interactive:
629             submitTemplate = self.prepareLogMessage(template, logMessage)
630             diff = read_pipe("p4 diff -du ...")
632             for newFile in filesToAdd:
633                 diff += "==== new file ====\n"
634                 diff += "--- /dev/null\n"
635                 diff += "+++ %s\n" % newFile
636                 f = open(newFile, "r")
637                 for line in f.readlines():
638                     diff += "+" + line
639                 f.close()
641             separatorLine = "######## everything below this line is just the diff #######"
642             if platform.system() == "Windows":
643                 separatorLine += "\r"
644             separatorLine += "\n"
646             [handle, fileName] = tempfile.mkstemp()
647             tmpFile = os.fdopen(handle, "w+")
648             tmpFile.write(submitTemplate + separatorLine + diff)
649             tmpFile.close()
650             defaultEditor = "vi"
651             if platform.system() == "Windows":
652                 defaultEditor = "notepad"
653             editor = os.environ.get("EDITOR", defaultEditor);
654             system(editor + " " + fileName)
655             tmpFile = open(fileName, "rb")
656             message = tmpFile.read()
657             tmpFile.close()
658             os.remove(fileName)
659             submitTemplate = message[:message.index(separatorLine)]
660             if self.isWindows:
661                 submitTemplate = submitTemplate.replace("\r\n", "\n")
663             write_pipe("p4 submit -i", submitTemplate)
664         else:
665             fileName = "submit.txt"
666             file = open(fileName, "w+")
667             file.write(self.prepareLogMessage(template, logMessage))
668             file.close()
669             print ("Perforce submit template written as %s. "
670                    + "Please review/edit and then use p4 submit -i < %s to submit directly!"
671                    % (fileName, fileName))
673     def run(self, args):
674         if len(args) == 0:
675             self.master = currentGitBranch()
676             if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
677                 die("Detecting current git branch failed!")
678         elif len(args) == 1:
679             self.master = args[0]
680         else:
681             return False
683         [upstream, settings] = findUpstreamBranchPoint()
684         self.depotPath = settings['depot-paths'][0]
685         if len(self.origin) == 0:
686             self.origin = upstream
688         if self.verbose:
689             print "Origin branch is " + self.origin
691         if len(self.depotPath) == 0:
692             print "Internal error: cannot locate perforce depot path from existing branches"
693             sys.exit(128)
695         self.clientPath = p4Where(self.depotPath)
697         if len(self.clientPath) == 0:
698             print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
699             sys.exit(128)
701         print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
702         self.oldWorkingDirectory = os.getcwd()
704         os.chdir(self.clientPath)
705         print "Syncronizing p4 checkout..."
706         system("p4 sync ...")
708         self.check()
710         commits = []
711         for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
712             commits.append(line.strip())
713         commits.reverse()
715         while len(commits) > 0:
716             commit = commits[0]
717             commits = commits[1:]
718             self.applyCommit(commit)
719             if not self.interactive:
720                 break
722         if len(commits) == 0:
723             print "All changes applied!"
724             os.chdir(self.oldWorkingDirectory)
726             sync = P4Sync()
727             sync.run([])
729             rebase = P4Rebase()
730             rebase.rebase()
732         return True
734 class P4Sync(Command):
735     def __init__(self):
736         Command.__init__(self)
737         self.options = [
738                 optparse.make_option("--branch", dest="branch"),
739                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
740                 optparse.make_option("--changesfile", dest="changesFile"),
741                 optparse.make_option("--silent", dest="silent", action="store_true"),
742                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
743                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
744                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
745                                      help="Import into refs/heads/ , not refs/remotes"),
746                 optparse.make_option("--max-changes", dest="maxChanges"),
747                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
748                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
749                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
750                                      help="Only sync files that are included in the Perforce Client Spec")
751         ]
752         self.description = """Imports from Perforce into a git repository.\n
753     example:
754     //depot/my/project/ -- to import the current head
755     //depot/my/project/@all -- to import everything
756     //depot/my/project/@1,6 -- to import only from revision 1 to 6
758     (a ... is not needed in the path p4 specification, it's added implicitly)"""
760         self.usage += " //depot/path[@revRange]"
761         self.silent = False
762         self.createdBranches = Set()
763         self.committedChanges = Set()
764         self.branch = ""
765         self.detectBranches = False
766         self.detectLabels = False
767         self.changesFile = ""
768         self.syncWithOrigin = True
769         self.verbose = False
770         self.importIntoRemotes = True
771         self.maxChanges = ""
772         self.isWindows = (platform.system() == "Windows")
773         self.keepRepoPath = False
774         self.depotPaths = None
775         self.p4BranchesInGit = []
776         self.cloneExclude = []
777         self.useClientSpec = False
778         self.clientSpecDirs = []
780         if gitConfig("git-p4.syncFromOrigin") == "false":
781             self.syncWithOrigin = False
783     def extractFilesFromCommit(self, commit):
784         self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
785                              for path in self.cloneExclude]
786         files = []
787         fnum = 0
788         while commit.has_key("depotFile%s" % fnum):
789             path =  commit["depotFile%s" % fnum]
791             if [p for p in self.cloneExclude
792                 if path.startswith (p)]:
793                 found = False
794             else:
795                 found = [p for p in self.depotPaths
796                          if path.startswith (p)]
797             if not found:
798                 fnum = fnum + 1
799                 continue
801             file = {}
802             file["path"] = path
803             file["rev"] = commit["rev%s" % fnum]
804             file["action"] = commit["action%s" % fnum]
805             file["type"] = commit["type%s" % fnum]
806             files.append(file)
807             fnum = fnum + 1
808         return files
810     def stripRepoPath(self, path, prefixes):
811         if self.keepRepoPath:
812             prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
814         for p in prefixes:
815             if path.startswith(p):
816                 path = path[len(p):]
818         return path
820     def splitFilesIntoBranches(self, commit):
821         branches = {}
822         fnum = 0
823         while commit.has_key("depotFile%s" % fnum):
824             path =  commit["depotFile%s" % fnum]
825             found = [p for p in self.depotPaths
826                      if path.startswith (p)]
827             if not found:
828                 fnum = fnum + 1
829                 continue
831             file = {}
832             file["path"] = path
833             file["rev"] = commit["rev%s" % fnum]
834             file["action"] = commit["action%s" % fnum]
835             file["type"] = commit["type%s" % fnum]
836             fnum = fnum + 1
838             relPath = self.stripRepoPath(path, self.depotPaths)
840             for branch in self.knownBranches.keys():
842                 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
843                 if relPath.startswith(branch + "/"):
844                     if branch not in branches:
845                         branches[branch] = []
846                     branches[branch].append(file)
847                     break
849         return branches
851     ## Should move this out, doesn't use SELF.
852     def readP4Files(self, files):
853         for f in files:
854             for val in self.clientSpecDirs:
855                 if f['path'].startswith(val[0]):
856                     if val[1] > 0:
857                         f['include'] = True
858                     else:
859                         f['include'] = False
860                     break
862         files = [f for f in files
863                  if f['action'] != 'delete' and
864                  (f.has_key('include') == False or f['include'] == True)]
866         if not files:
867             return []
869         filedata = p4CmdList('-x - print',
870                              stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
871                                               for f in files]),
872                              stdin_mode='w+')
873         if "p4ExitCode" in filedata[0]:
874             die("Problems executing p4. Error: [%d]."
875                 % (filedata[0]['p4ExitCode']));
877         j = 0;
878         contents = {}
879         while j < len(filedata):
880             stat = filedata[j]
881             j += 1
882             text = ''
883             while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
884                 tmp = filedata[j]['data']
885                 if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
886                     tmp = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', tmp)
887                 elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
888                     tmp = re.sub(r'(?i)\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', tmp)
889                 text += tmp
890                 j += 1
893             if not stat.has_key('depotFile'):
894                 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
895                 continue
897             contents[stat['depotFile']] = text
899         for f in files:
900             assert not f.has_key('data')
901             f['data'] = contents[f['path']]
902         return files
904     def commit(self, details, files, branch, branchPrefixes, parent = ""):
905         epoch = details["time"]
906         author = details["user"]
908         if self.verbose:
909             print "commit into %s" % branch
911         # start with reading files; if that fails, we should not
912         # create a commit.
913         new_files = []
914         for f in files:
915             if [p for p in branchPrefixes if f['path'].startswith(p)]:
916                 new_files.append (f)
917             else:
918                 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
919         files = self.readP4Files(new_files)
921         self.gitStream.write("commit %s\n" % branch)
922 #        gitStream.write("mark :%s\n" % details["change"])
923         self.committedChanges.add(int(details["change"]))
924         committer = ""
925         if author not in self.users:
926             self.getUserMapFromPerforceServer()
927         if author in self.users:
928             committer = "%s %s %s" % (self.users[author], epoch, self.tz)
929         else:
930             committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
932         self.gitStream.write("committer %s\n" % committer)
934         self.gitStream.write("data <<EOT\n")
935         self.gitStream.write(details["desc"])
936         self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
937                              % (','.join (branchPrefixes), details["change"]))
938         if len(details['options']) > 0:
939             self.gitStream.write(": options = %s" % details['options'])
940         self.gitStream.write("]\nEOT\n\n")
942         if len(parent) > 0:
943             if self.verbose:
944                 print "parent %s" % parent
945             self.gitStream.write("from %s\n" % parent)
947         for file in files:
948             if file["type"] == "apple":
949                 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
950                 continue
952             relPath = self.stripRepoPath(file['path'], branchPrefixes)
953             if file["action"] == "delete":
954                 self.gitStream.write("D %s\n" % relPath)
955             else:
956                 data = file['data']
958                 mode = "644"
959                 if isP4Exec(file["type"]):
960                     mode = "755"
961                 elif file["type"] == "symlink":
962                     mode = "120000"
963                     # p4 print on a symlink contains "target\n", so strip it off
964                     data = data[:-1]
966                 if self.isWindows and file["type"].endswith("text"):
967                     data = data.replace("\r\n", "\n")
969                 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
970                 self.gitStream.write("data %s\n" % len(data))
971                 self.gitStream.write(data)
972                 self.gitStream.write("\n")
974         self.gitStream.write("\n")
976         change = int(details["change"])
978         if self.labels.has_key(change):
979             label = self.labels[change]
980             labelDetails = label[0]
981             labelRevisions = label[1]
982             if self.verbose:
983                 print "Change %s is labelled %s" % (change, labelDetails)
985             files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
986                                                     for p in branchPrefixes]))
988             if len(files) == len(labelRevisions):
990                 cleanedFiles = {}
991                 for info in files:
992                     if info["action"] == "delete":
993                         continue
994                     cleanedFiles[info["depotFile"]] = info["rev"]
996                 if cleanedFiles == labelRevisions:
997                     self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
998                     self.gitStream.write("from %s\n" % branch)
1000                     owner = labelDetails["Owner"]
1001                     tagger = ""
1002                     if author in self.users:
1003                         tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1004                     else:
1005                         tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1006                     self.gitStream.write("tagger %s\n" % tagger)
1007                     self.gitStream.write("data <<EOT\n")
1008                     self.gitStream.write(labelDetails["Description"])
1009                     self.gitStream.write("EOT\n\n")
1011                 else:
1012                     if not self.silent:
1013                         print ("Tag %s does not match with change %s: files do not match."
1014                                % (labelDetails["label"], change))
1016             else:
1017                 if not self.silent:
1018                     print ("Tag %s does not match with change %s: file count is different."
1019                            % (labelDetails["label"], change))
1021     def getUserCacheFilename(self):
1022         home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1023         return home + "/.gitp4-usercache.txt"
1025     def getUserMapFromPerforceServer(self):
1026         if self.userMapFromPerforceServer:
1027             return
1028         self.users = {}
1030         for output in p4CmdList("users"):
1031             if not output.has_key("User"):
1032                 continue
1033             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1036         s = ''
1037         for (key, val) in self.users.items():
1038             s += "%s\t%s\n" % (key, val)
1040         open(self.getUserCacheFilename(), "wb").write(s)
1041         self.userMapFromPerforceServer = True
1043     def loadUserMapFromCache(self):
1044         self.users = {}
1045         self.userMapFromPerforceServer = False
1046         try:
1047             cache = open(self.getUserCacheFilename(), "rb")
1048             lines = cache.readlines()
1049             cache.close()
1050             for line in lines:
1051                 entry = line.strip().split("\t")
1052                 self.users[entry[0]] = entry[1]
1053         except IOError:
1054             self.getUserMapFromPerforceServer()
1056     def getLabels(self):
1057         self.labels = {}
1059         l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1060         if len(l) > 0 and not self.silent:
1061             print "Finding files belonging to labels in %s" % `self.depotPaths`
1063         for output in l:
1064             label = output["label"]
1065             revisions = {}
1066             newestChange = 0
1067             if self.verbose:
1068                 print "Querying files for label %s" % label
1069             for file in p4CmdList("files "
1070                                   +  ' '.join (["%s...@%s" % (p, label)
1071                                                 for p in self.depotPaths])):
1072                 revisions[file["depotFile"]] = file["rev"]
1073                 change = int(file["change"])
1074                 if change > newestChange:
1075                     newestChange = change
1077             self.labels[newestChange] = [output, revisions]
1079         if self.verbose:
1080             print "Label changes: %s" % self.labels.keys()
1082     def guessProjectName(self):
1083         for p in self.depotPaths:
1084             if p.endswith("/"):
1085                 p = p[:-1]
1086             p = p[p.strip().rfind("/") + 1:]
1087             if not p.endswith("/"):
1088                p += "/"
1089             return p
1091     def getBranchMapping(self):
1092         lostAndFoundBranches = set()
1094         for info in p4CmdList("branches"):
1095             details = p4Cmd("branch -o %s" % info["branch"])
1096             viewIdx = 0
1097             while details.has_key("View%s" % viewIdx):
1098                 paths = details["View%s" % viewIdx].split(" ")
1099                 viewIdx = viewIdx + 1
1100                 # require standard //depot/foo/... //depot/bar/... mapping
1101                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1102                     continue
1103                 source = paths[0]
1104                 destination = paths[1]
1105                 ## HACK
1106                 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1107                     source = source[len(self.depotPaths[0]):-4]
1108                     destination = destination[len(self.depotPaths[0]):-4]
1110                     if destination in self.knownBranches:
1111                         if not self.silent:
1112                             print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1113                             print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1114                         continue
1116                     self.knownBranches[destination] = source
1118                     lostAndFoundBranches.discard(destination)
1120                     if source not in self.knownBranches:
1121                         lostAndFoundBranches.add(source)
1124         for branch in lostAndFoundBranches:
1125             self.knownBranches[branch] = branch
1127     def getBranchMappingFromGitBranches(self):
1128         branches = p4BranchesInGit(self.importIntoRemotes)
1129         for branch in branches.keys():
1130             if branch == "master":
1131                 branch = "main"
1132             else:
1133                 branch = branch[len(self.projectName):]
1134             self.knownBranches[branch] = branch
1136     def listExistingP4GitBranches(self):
1137         # branches holds mapping from name to commit
1138         branches = p4BranchesInGit(self.importIntoRemotes)
1139         self.p4BranchesInGit = branches.keys()
1140         for branch in branches.keys():
1141             self.initialParents[self.refPrefix + branch] = branches[branch]
1143     def updateOptionDict(self, d):
1144         option_keys = {}
1145         if self.keepRepoPath:
1146             option_keys['keepRepoPath'] = 1
1148         d["options"] = ' '.join(sorted(option_keys.keys()))
1150     def readOptions(self, d):
1151         self.keepRepoPath = (d.has_key('options')
1152                              and ('keepRepoPath' in d['options']))
1154     def gitRefForBranch(self, branch):
1155         if branch == "main":
1156             return self.refPrefix + "master"
1158         if len(branch) <= 0:
1159             return branch
1161         return self.refPrefix + self.projectName + branch
1163     def gitCommitByP4Change(self, ref, change):
1164         if self.verbose:
1165             print "looking in ref " + ref + " for change %s using bisect..." % change
1167         earliestCommit = ""
1168         latestCommit = parseRevision(ref)
1170         while True:
1171             if self.verbose:
1172                 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1173             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1174             if len(next) == 0:
1175                 if self.verbose:
1176                     print "argh"
1177                 return ""
1178             log = extractLogMessageFromGitCommit(next)
1179             settings = extractSettingsGitLog(log)
1180             currentChange = int(settings['change'])
1181             if self.verbose:
1182                 print "current change %s" % currentChange
1184             if currentChange == change:
1185                 if self.verbose:
1186                     print "found %s" % next
1187                 return next
1189             if currentChange < change:
1190                 earliestCommit = "^%s" % next
1191             else:
1192                 latestCommit = "%s" % next
1194         return ""
1196     def importNewBranch(self, branch, maxChange):
1197         # make fast-import flush all changes to disk and update the refs using the checkpoint
1198         # command so that we can try to find the branch parent in the git history
1199         self.gitStream.write("checkpoint\n\n");
1200         self.gitStream.flush();
1201         branchPrefix = self.depotPaths[0] + branch + "/"
1202         range = "@1,%s" % maxChange
1203         #print "prefix" + branchPrefix
1204         changes = p4ChangesForPaths([branchPrefix], range)
1205         if len(changes) <= 0:
1206             return False
1207         firstChange = changes[0]
1208         #print "first change in branch: %s" % firstChange
1209         sourceBranch = self.knownBranches[branch]
1210         sourceDepotPath = self.depotPaths[0] + sourceBranch
1211         sourceRef = self.gitRefForBranch(sourceBranch)
1212         #print "source " + sourceBranch
1214         branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1215         #print "branch parent: %s" % branchParentChange
1216         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1217         if len(gitParent) > 0:
1218             self.initialParents[self.gitRefForBranch(branch)] = gitParent
1219             #print "parent git commit: %s" % gitParent
1221         self.importChanges(changes)
1222         return True
1224     def importChanges(self, changes):
1225         cnt = 1
1226         for change in changes:
1227             description = p4Cmd("describe %s" % change)
1228             self.updateOptionDict(description)
1230             if not self.silent:
1231                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1232                 sys.stdout.flush()
1233             cnt = cnt + 1
1235             try:
1236                 if self.detectBranches:
1237                     branches = self.splitFilesIntoBranches(description)
1238                     for branch in branches.keys():
1239                         ## HACK  --hwn
1240                         branchPrefix = self.depotPaths[0] + branch + "/"
1242                         parent = ""
1244                         filesForCommit = branches[branch]
1246                         if self.verbose:
1247                             print "branch is %s" % branch
1249                         self.updatedBranches.add(branch)
1251                         if branch not in self.createdBranches:
1252                             self.createdBranches.add(branch)
1253                             parent = self.knownBranches[branch]
1254                             if parent == branch:
1255                                 parent = ""
1256                             else:
1257                                 fullBranch = self.projectName + branch
1258                                 if fullBranch not in self.p4BranchesInGit:
1259                                     if not self.silent:
1260                                         print("\n    Importing new branch %s" % fullBranch);
1261                                     if self.importNewBranch(branch, change - 1):
1262                                         parent = ""
1263                                         self.p4BranchesInGit.append(fullBranch)
1264                                     if not self.silent:
1265                                         print("\n    Resuming with change %s" % change);
1267                                 if self.verbose:
1268                                     print "parent determined through known branches: %s" % parent
1270                         branch = self.gitRefForBranch(branch)
1271                         parent = self.gitRefForBranch(parent)
1273                         if self.verbose:
1274                             print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1276                         if len(parent) == 0 and branch in self.initialParents:
1277                             parent = self.initialParents[branch]
1278                             del self.initialParents[branch]
1280                         self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1281                 else:
1282                     files = self.extractFilesFromCommit(description)
1283                     self.commit(description, files, self.branch, self.depotPaths,
1284                                 self.initialParent)
1285                     self.initialParent = ""
1286             except IOError:
1287                 print self.gitError.read()
1288                 sys.exit(1)
1290     def importHeadRevision(self, revision):
1291         print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1293         details = { "user" : "git perforce import user", "time" : int(time.time()) }
1294         details["desc"] = ("Initial import of %s from the state at revision %s"
1295                            % (' '.join(self.depotPaths), revision))
1296         details["change"] = revision
1297         newestRevision = 0
1299         fileCnt = 0
1300         for info in p4CmdList("files "
1301                               +  ' '.join(["%s...%s"
1302                                            % (p, revision)
1303                                            for p in self.depotPaths])):
1305             if info['code'] == 'error':
1306                 sys.stderr.write("p4 returned an error: %s\n"
1307                                  % info['data'])
1308                 sys.exit(1)
1311             change = int(info["change"])
1312             if change > newestRevision:
1313                 newestRevision = change
1315             if info["action"] == "delete":
1316                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1317                 #fileCnt = fileCnt + 1
1318                 continue
1320             for prop in ["depotFile", "rev", "action", "type" ]:
1321                 details["%s%s" % (prop, fileCnt)] = info[prop]
1323             fileCnt = fileCnt + 1
1325         details["change"] = newestRevision
1326         self.updateOptionDict(details)
1327         try:
1328             self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1329         except IOError:
1330             print "IO error with git fast-import. Is your git version recent enough?"
1331             print self.gitError.read()
1334     def getClientSpec(self):
1335         specList = p4CmdList( "client -o" )
1336         temp = {}
1337         for entry in specList:
1338             for k,v in entry.iteritems():
1339                 if k.startswith("View"):
1340                     if v.startswith('"'):
1341                         start = 1
1342                     else:
1343                         start = 0
1344                     index = v.find("...")
1345                     v = v[start:index]
1346                     if v.startswith("-"):
1347                         v = v[1:]
1348                         temp[v] = -len(v)
1349                     else:
1350                         temp[v] = len(v)
1351         self.clientSpecDirs = temp.items()
1352         self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1354     def run(self, args):
1355         self.depotPaths = []
1356         self.changeRange = ""
1357         self.initialParent = ""
1358         self.previousDepotPaths = []
1360         # map from branch depot path to parent branch
1361         self.knownBranches = {}
1362         self.initialParents = {}
1363         self.hasOrigin = originP4BranchesExist()
1364         if not self.syncWithOrigin:
1365             self.hasOrigin = False
1367         if self.importIntoRemotes:
1368             self.refPrefix = "refs/remotes/p4/"
1369         else:
1370             self.refPrefix = "refs/heads/p4/"
1372         if self.syncWithOrigin and self.hasOrigin:
1373             if not self.silent:
1374                 print "Syncing with origin first by calling git fetch origin"
1375             system("git fetch origin")
1377         if len(self.branch) == 0:
1378             self.branch = self.refPrefix + "master"
1379             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1380                 system("git update-ref %s refs/heads/p4" % self.branch)
1381                 system("git branch -D p4");
1382             # create it /after/ importing, when master exists
1383             if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1384                 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1386         if self.useClientSpec or gitConfig("p4.useclientspec") == "true":
1387             self.getClientSpec()
1389         # TODO: should always look at previous commits,
1390         # merge with previous imports, if possible.
1391         if args == []:
1392             if self.hasOrigin:
1393                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1394             self.listExistingP4GitBranches()
1396             if len(self.p4BranchesInGit) > 1:
1397                 if not self.silent:
1398                     print "Importing from/into multiple branches"
1399                 self.detectBranches = True
1401             if self.verbose:
1402                 print "branches: %s" % self.p4BranchesInGit
1404             p4Change = 0
1405             for branch in self.p4BranchesInGit:
1406                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1408                 settings = extractSettingsGitLog(logMsg)
1410                 self.readOptions(settings)
1411                 if (settings.has_key('depot-paths')
1412                     and settings.has_key ('change')):
1413                     change = int(settings['change']) + 1
1414                     p4Change = max(p4Change, change)
1416                     depotPaths = sorted(settings['depot-paths'])
1417                     if self.previousDepotPaths == []:
1418                         self.previousDepotPaths = depotPaths
1419                     else:
1420                         paths = []
1421                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1422                             for i in range(0, min(len(cur), len(prev))):
1423                                 if cur[i] <> prev[i]:
1424                                     i = i - 1
1425                                     break
1427                             paths.append (cur[:i + 1])
1429                         self.previousDepotPaths = paths
1431             if p4Change > 0:
1432                 self.depotPaths = sorted(self.previousDepotPaths)
1433                 self.changeRange = "@%s,#head" % p4Change
1434                 if not self.detectBranches:
1435                     self.initialParent = parseRevision(self.branch)
1436                 if not self.silent and not self.detectBranches:
1437                     print "Performing incremental import into %s git branch" % self.branch
1439         if not self.branch.startswith("refs/"):
1440             self.branch = "refs/heads/" + self.branch
1442         if len(args) == 0 and self.depotPaths:
1443             if not self.silent:
1444                 print "Depot paths: %s" % ' '.join(self.depotPaths)
1445         else:
1446             if self.depotPaths and self.depotPaths != args:
1447                 print ("previous import used depot path %s and now %s was specified. "
1448                        "This doesn't work!" % (' '.join (self.depotPaths),
1449                                                ' '.join (args)))
1450                 sys.exit(1)
1452             self.depotPaths = sorted(args)
1454         revision = ""
1455         self.users = {}
1457         newPaths = []
1458         for p in self.depotPaths:
1459             if p.find("@") != -1:
1460                 atIdx = p.index("@")
1461                 self.changeRange = p[atIdx:]
1462                 if self.changeRange == "@all":
1463                     self.changeRange = ""
1464                 elif ',' not in self.changeRange:
1465                     revision = self.changeRange
1466                     self.changeRange = ""
1467                 p = p[:atIdx]
1468             elif p.find("#") != -1:
1469                 hashIdx = p.index("#")
1470                 revision = p[hashIdx:]
1471                 p = p[:hashIdx]
1472             elif self.previousDepotPaths == []:
1473                 revision = "#head"
1475             p = re.sub ("\.\.\.$", "", p)
1476             if not p.endswith("/"):
1477                 p += "/"
1479             newPaths.append(p)
1481         self.depotPaths = newPaths
1484         self.loadUserMapFromCache()
1485         self.labels = {}
1486         if self.detectLabels:
1487             self.getLabels();
1489         if self.detectBranches:
1490             ## FIXME - what's a P4 projectName ?
1491             self.projectName = self.guessProjectName()
1493             if self.hasOrigin:
1494                 self.getBranchMappingFromGitBranches()
1495             else:
1496                 self.getBranchMapping()
1497             if self.verbose:
1498                 print "p4-git branches: %s" % self.p4BranchesInGit
1499                 print "initial parents: %s" % self.initialParents
1500             for b in self.p4BranchesInGit:
1501                 if b != "master":
1503                     ## FIXME
1504                     b = b[len(self.projectName):]
1505                 self.createdBranches.add(b)
1507         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1509         importProcess = subprocess.Popen(["git", "fast-import"],
1510                                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1511                                          stderr=subprocess.PIPE);
1512         self.gitOutput = importProcess.stdout
1513         self.gitStream = importProcess.stdin
1514         self.gitError = importProcess.stderr
1516         if revision:
1517             self.importHeadRevision(revision)
1518         else:
1519             changes = []
1521             if len(self.changesFile) > 0:
1522                 output = open(self.changesFile).readlines()
1523                 changeSet = Set()
1524                 for line in output:
1525                     changeSet.add(int(line))
1527                 for change in changeSet:
1528                     changes.append(change)
1530                 changes.sort()
1531             else:
1532                 if self.verbose:
1533                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1534                                                               self.changeRange)
1535                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1537                 if len(self.maxChanges) > 0:
1538                     changes = changes[:min(int(self.maxChanges), len(changes))]
1540             if len(changes) == 0:
1541                 if not self.silent:
1542                     print "No changes to import!"
1543                 return True
1545             if not self.silent and not self.detectBranches:
1546                 print "Import destination: %s" % self.branch
1548             self.updatedBranches = set()
1550             self.importChanges(changes)
1552             if not self.silent:
1553                 print ""
1554                 if len(self.updatedBranches) > 0:
1555                     sys.stdout.write("Updated branches: ")
1556                     for b in self.updatedBranches:
1557                         sys.stdout.write("%s " % b)
1558                     sys.stdout.write("\n")
1560         self.gitStream.close()
1561         if importProcess.wait() != 0:
1562             die("fast-import failed: %s" % self.gitError.read())
1563         self.gitOutput.close()
1564         self.gitError.close()
1566         return True
1568 class P4Rebase(Command):
1569     def __init__(self):
1570         Command.__init__(self)
1571         self.options = [ ]
1572         self.description = ("Fetches the latest revision from perforce and "
1573                             + "rebases the current work (branch) against it")
1574         self.verbose = False
1576     def run(self, args):
1577         sync = P4Sync()
1578         sync.run([])
1580         return self.rebase()
1582     def rebase(self):
1583         if os.system("git update-index --refresh") != 0:
1584             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.");
1585         if len(read_pipe("git diff-index HEAD --")) > 0:
1586             die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1588         [upstream, settings] = findUpstreamBranchPoint()
1589         if len(upstream) == 0:
1590             die("Cannot find upstream branchpoint for rebase")
1592         # the branchpoint may be p4/foo~3, so strip off the parent
1593         upstream = re.sub("~[0-9]+$", "", upstream)
1595         print "Rebasing the current branch onto %s" % upstream
1596         oldHead = read_pipe("git rev-parse HEAD").strip()
1597         system("git rebase %s" % upstream)
1598         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1599         return True
1601 class P4Clone(P4Sync):
1602     def __init__(self):
1603         P4Sync.__init__(self)
1604         self.description = "Creates a new git repository and imports from Perforce into it"
1605         self.usage = "usage: %prog [options] //depot/path[@revRange]"
1606         self.options += [
1607             optparse.make_option("--destination", dest="cloneDestination",
1608                                  action='store', default=None,
1609                                  help="where to leave result of the clone"),
1610             optparse.make_option("-/", dest="cloneExclude",
1611                                  action="append", type="string",
1612                                  help="exclude depot path")
1613         ]
1614         self.cloneDestination = None
1615         self.needsGit = False
1617     # This is required for the "append" cloneExclude action
1618     def ensure_value(self, attr, value):
1619         if not hasattr(self, attr) or getattr(self, attr) is None:
1620             setattr(self, attr, value)
1621         return getattr(self, attr)
1623     def defaultDestination(self, args):
1624         ## TODO: use common prefix of args?
1625         depotPath = args[0]
1626         depotDir = re.sub("(@[^@]*)$", "", depotPath)
1627         depotDir = re.sub("(#[^#]*)$", "", depotDir)
1628         depotDir = re.sub(r"\.\.\.$", "", depotDir)
1629         depotDir = re.sub(r"/$", "", depotDir)
1630         return os.path.split(depotDir)[1]
1632     def run(self, args):
1633         if len(args) < 1:
1634             return False
1636         if self.keepRepoPath and not self.cloneDestination:
1637             sys.stderr.write("Must specify destination for --keep-path\n")
1638             sys.exit(1)
1640         depotPaths = args
1642         if not self.cloneDestination and len(depotPaths) > 1:
1643             self.cloneDestination = depotPaths[-1]
1644             depotPaths = depotPaths[:-1]
1646         self.cloneExclude = ["/"+p for p in self.cloneExclude]
1647         for p in depotPaths:
1648             if not p.startswith("//"):
1649                 return False
1651         if not self.cloneDestination:
1652             self.cloneDestination = self.defaultDestination(args)
1654         print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1655         if not os.path.exists(self.cloneDestination):
1656             os.makedirs(self.cloneDestination)
1657         os.chdir(self.cloneDestination)
1658         system("git init")
1659         self.gitdir = os.getcwd() + "/.git"
1660         if not P4Sync.run(self, depotPaths):
1661             return False
1662         if self.branch != "master":
1663             if gitBranchExists("refs/remotes/p4/master"):
1664                 system("git branch master refs/remotes/p4/master")
1665                 system("git checkout -f")
1666             else:
1667                 print "Could not detect main branch. No checkout/master branch created."
1669         return True
1671 class P4Branches(Command):
1672     def __init__(self):
1673         Command.__init__(self)
1674         self.options = [ ]
1675         self.description = ("Shows the git branches that hold imports and their "
1676                             + "corresponding perforce depot paths")
1677         self.verbose = False
1679     def run(self, args):
1680         if originP4BranchesExist():
1681             createOrUpdateBranchesFromOrigin()
1683         cmdline = "git rev-parse --symbolic "
1684         cmdline += " --remotes"
1686         for line in read_pipe_lines(cmdline):
1687             line = line.strip()
1689             if not line.startswith('p4/') or line == "p4/HEAD":
1690                 continue
1691             branch = line
1693             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1694             settings = extractSettingsGitLog(log)
1696             print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1697         return True
1699 class HelpFormatter(optparse.IndentedHelpFormatter):
1700     def __init__(self):
1701         optparse.IndentedHelpFormatter.__init__(self)
1703     def format_description(self, description):
1704         if description:
1705             return description + "\n"
1706         else:
1707             return ""
1709 def printUsage(commands):
1710     print "usage: %s <command> [options]" % sys.argv[0]
1711     print ""
1712     print "valid commands: %s" % ", ".join(commands)
1713     print ""
1714     print "Try %s <command> --help for command specific help." % sys.argv[0]
1715     print ""
1717 commands = {
1718     "debug" : P4Debug,
1719     "submit" : P4Submit,
1720     "commit" : P4Submit,
1721     "sync" : P4Sync,
1722     "rebase" : P4Rebase,
1723     "clone" : P4Clone,
1724     "rollback" : P4RollBack,
1725     "branches" : P4Branches
1729 def main():
1730     if len(sys.argv[1:]) == 0:
1731         printUsage(commands.keys())
1732         sys.exit(2)
1734     cmd = ""
1735     cmdName = sys.argv[1]
1736     try:
1737         klass = commands[cmdName]
1738         cmd = klass()
1739     except KeyError:
1740         print "unknown command %s" % cmdName
1741         print ""
1742         printUsage(commands.keys())
1743         sys.exit(2)
1745     options = cmd.options
1746     cmd.gitdir = os.environ.get("GIT_DIR", None)
1748     args = sys.argv[2:]
1750     if len(options) > 0:
1751         options.append(optparse.make_option("--git-dir", dest="gitdir"))
1753         parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1754                                        options,
1755                                        description = cmd.description,
1756                                        formatter = HelpFormatter())
1758         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1759     global verbose
1760     verbose = cmd.verbose
1761     if cmd.needsGit:
1762         if cmd.gitdir == None:
1763             cmd.gitdir = os.path.abspath(".git")
1764             if not isValidGitDir(cmd.gitdir):
1765                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1766                 if os.path.exists(cmd.gitdir):
1767                     cdup = read_pipe("git rev-parse --show-cdup").strip()
1768                     if len(cdup) > 0:
1769                         os.chdir(cdup);
1771         if not isValidGitDir(cmd.gitdir):
1772             if isValidGitDir(cmd.gitdir + "/.git"):
1773                 cmd.gitdir += "/.git"
1774             else:
1775                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1777         os.environ["GIT_DIR"] = cmd.gitdir
1779     if not cmd.run(args):
1780         parser.print_help()
1783 if __name__ == '__main__':
1784     main()