3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
5 # Author: Simon Hausmann <simon@lst.de>
6 # Copyright: 2007 Simon Hausmann <simon@lst.de>
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
11 import optparse, sys, os, marshal, subprocess, shelve
12 import tempfile, getopt, os.path, time, platform
18 def p4_build_cmd(cmd):
19 """Build a suitable p4 command line.
21 This consolidates building and returning a p4 command line into one
22 location. It means that hooking into the environment, or other configuration
23 can be done more easily.
27 user = gitConfig("git-p4.user")
29 real_cmd += ["-u",user]
31 password = gitConfig("git-p4.password")
33 real_cmd += ["-P", password]
35 port = gitConfig("git-p4.port")
37 real_cmd += ["-p", port]
39 host = gitConfig("git-p4.host")
41 real_cmd += ["-h", host]
43 client = gitConfig("git-p4.client")
45 real_cmd += ["-c", client]
48 if isinstance(cmd,basestring):
49 real_cmd = ' '.join(real_cmd) + ' ' + cmd
55 # P4 uses the PWD environment variable rather than getcwd(). Since we're
56 # not using the shell, we have to set it ourselves.
64 sys.stderr.write(msg + "\n")
67 def write_pipe(c, stdin):
69 sys.stderr.write('Writing pipe: %s\n' % str(c))
71 expand = isinstance(c,basestring)
72 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
74 val = pipe.write(stdin)
77 die('Command failed: %s' % str(c))
81 def p4_write_pipe(c, stdin):
82 real_cmd = p4_build_cmd(c)
83 return write_pipe(real_cmd, stdin)
85 def read_pipe(c, ignore_error=False):
87 sys.stderr.write('Reading pipe: %s\n' % str(c))
89 expand = isinstance(c,basestring)
90 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
93 if p.wait() and not ignore_error:
94 die('Command failed: %s' % str(c))
98 def p4_read_pipe(c, ignore_error=False):
99 real_cmd = p4_build_cmd(c)
100 return read_pipe(real_cmd, ignore_error)
102 def read_pipe_lines(c):
104 sys.stderr.write('Reading pipe: %s\n' % str(c))
106 expand = isinstance(c, basestring)
107 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
109 val = pipe.readlines()
110 if pipe.close() or p.wait():
111 die('Command failed: %s' % str(c))
115 def p4_read_pipe_lines(c):
116 """Specifically invoke p4 on the command supplied. """
117 real_cmd = p4_build_cmd(c)
118 return read_pipe_lines(real_cmd)
121 expand = isinstance(cmd,basestring)
123 sys.stderr.write("executing %s\n" % str(cmd))
124 subprocess.check_call(cmd, shell=expand)
127 """Specifically invoke p4 as the system command. """
128 real_cmd = p4_build_cmd(cmd)
129 expand = isinstance(real_cmd, basestring)
130 subprocess.check_call(real_cmd, shell=expand)
132 def p4_integrate(src, dest):
133 p4_system(["integrate", "-Dt", src, dest])
136 p4_system(["sync", path])
139 p4_system(["add", f])
142 p4_system(["delete", f])
145 p4_system(["edit", f])
148 p4_system(["revert", f])
150 def p4_reopen(type, file):
151 p4_system(["reopen", "-t", type, file])
154 # Canonicalize the p4 type and return a tuple of the
155 # base type, plus any modifiers. See "p4 help filetypes"
156 # for a list and explanation.
158 def split_p4_type(p4type):
160 p4_filetypes_historical = {
161 "ctempobj": "binary+Sw",
167 "tempobj": "binary+FSw",
168 "ubinary": "binary+F",
169 "uresource": "resource+F",
170 "uxbinary": "binary+Fx",
171 "xbinary": "binary+x",
173 "xtempobj": "binary+Swx",
175 "xunicode": "unicode+x",
178 if p4type in p4_filetypes_historical:
179 p4type = p4_filetypes_historical[p4type]
181 s = p4type.split("+")
189 def setP4ExecBit(file, mode):
190 # Reopens an already open file and changes the execute bit to match
191 # the execute bit setting in the passed in mode.
195 if not isModeExec(mode):
196 p4Type = getP4OpenedType(file)
197 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
198 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
199 if p4Type[-1] == "+":
200 p4Type = p4Type[0:-1]
202 p4_reopen(p4Type, file)
204 def getP4OpenedType(file):
205 # Returns the perforce file type for the given file.
207 result = p4_read_pipe(["opened", file])
208 match = re.match(".*\((.+)\)\r?$", result)
210 return match.group(1)
212 die("Could not determine file type for %s (result: '%s')" % (file, result))
214 def diffTreePattern():
215 # This is a simple generator for the diff tree regex pattern. This could be
216 # a class variable if this and parseDiffTreeEntry were a part of a class.
217 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
221 def parseDiffTreeEntry(entry):
222 """Parses a single diff tree entry into its component elements.
224 See git-diff-tree(1) manpage for details about the format of the diff
225 output. This method returns a dictionary with the following elements:
227 src_mode - The mode of the source file
228 dst_mode - The mode of the destination file
229 src_sha1 - The sha1 for the source file
230 dst_sha1 - The sha1 fr the destination file
231 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
232 status_score - The score for the status (applicable for 'C' and 'R'
233 statuses). This is None if there is no score.
234 src - The path for the source file.
235 dst - The path for the destination file. This is only present for
236 copy or renames. If it is not present, this is None.
238 If the pattern is not matched, None is returned."""
240 match = diffTreePattern().next().match(entry)
243 'src_mode': match.group(1),
244 'dst_mode': match.group(2),
245 'src_sha1': match.group(3),
246 'dst_sha1': match.group(4),
247 'status': match.group(5),
248 'status_score': match.group(6),
249 'src': match.group(7),
250 'dst': match.group(10)
254 def isModeExec(mode):
255 # Returns True if the given git mode represents an executable file,
257 return mode[-3:] == "755"
259 def isModeExecChanged(src_mode, dst_mode):
260 return isModeExec(src_mode) != isModeExec(dst_mode)
262 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
264 if isinstance(cmd,basestring):
271 cmd = p4_build_cmd(cmd)
273 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
275 # Use a temporary file to avoid deadlocks without
276 # subprocess.communicate(), which would put another copy
277 # of stdout into memory.
279 if stdin is not None:
280 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
281 if isinstance(stdin,basestring):
282 stdin_file.write(stdin)
285 stdin_file.write(i + '\n')
289 p4 = subprocess.Popen(cmd,
292 stdout=subprocess.PIPE)
297 entry = marshal.load(p4.stdout)
307 entry["p4ExitCode"] = exitCode
313 list = p4CmdList(cmd)
319 def p4Where(depotPath):
320 if not depotPath.endswith("/"):
322 depotPath = depotPath + "..."
323 outputList = p4CmdList(["where", depotPath])
325 for entry in outputList:
326 if "depotFile" in entry:
327 if entry["depotFile"] == depotPath:
330 elif "data" in entry:
331 data = entry.get("data")
332 space = data.find(" ")
333 if data[:space] == depotPath:
338 if output["code"] == "error":
342 clientPath = output.get("path")
343 elif "data" in output:
344 data = output.get("data")
345 lastSpace = data.rfind(" ")
346 clientPath = data[lastSpace + 1:]
348 if clientPath.endswith("..."):
349 clientPath = clientPath[:-3]
352 def currentGitBranch():
353 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
355 def isValidGitDir(path):
356 if (os.path.exists(path + "/HEAD")
357 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
361 def parseRevision(ref):
362 return read_pipe("git rev-parse %s" % ref).strip()
364 def extractLogMessageFromGitCommit(commit):
367 ## fixme: title is first line of commit, not 1st paragraph.
369 for log in read_pipe_lines("git cat-file commit %s" % commit):
378 def extractSettingsGitLog(log):
380 for line in log.split("\n"):
382 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
386 assignments = m.group(1).split (':')
387 for a in assignments:
389 key = vals[0].strip()
390 val = ('='.join (vals[1:])).strip()
391 if val.endswith ('\"') and val.startswith('"'):
396 paths = values.get("depot-paths")
398 paths = values.get("depot-path")
400 values['depot-paths'] = paths.split(',')
403 def gitBranchExists(branch):
404 proc = subprocess.Popen(["git", "rev-parse", branch],
405 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
406 return proc.wait() == 0;
409 def gitConfig(key, args = None): # set args to "--bool", for instance
410 if not _gitConfig.has_key(key):
413 argsFilter = "%s " % args
414 cmd = "git config %s%s" % (argsFilter, key)
415 _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
416 return _gitConfig[key]
418 def gitConfigList(key):
419 if not _gitConfig.has_key(key):
420 _gitConfig[key] = read_pipe("git config --get-all %s" % key, ignore_error=True).strip().split(os.linesep)
421 return _gitConfig[key]
423 def p4BranchesInGit(branchesAreInRemotes = True):
426 cmdline = "git rev-parse --symbolic "
427 if branchesAreInRemotes:
428 cmdline += " --remotes"
430 cmdline += " --branches"
432 for line in read_pipe_lines(cmdline):
435 ## only import to p4/
436 if not line.startswith('p4/') or line == "p4/HEAD":
441 branch = re.sub ("^p4/", "", line)
443 branches[branch] = parseRevision(line)
446 def findUpstreamBranchPoint(head = "HEAD"):
447 branches = p4BranchesInGit()
448 # map from depot-path to branch name
449 branchByDepotPath = {}
450 for branch in branches.keys():
451 tip = branches[branch]
452 log = extractLogMessageFromGitCommit(tip)
453 settings = extractSettingsGitLog(log)
454 if settings.has_key("depot-paths"):
455 paths = ",".join(settings["depot-paths"])
456 branchByDepotPath[paths] = "remotes/p4/" + branch
460 while parent < 65535:
461 commit = head + "~%s" % parent
462 log = extractLogMessageFromGitCommit(commit)
463 settings = extractSettingsGitLog(log)
464 if settings.has_key("depot-paths"):
465 paths = ",".join(settings["depot-paths"])
466 if branchByDepotPath.has_key(paths):
467 return [branchByDepotPath[paths], settings]
471 return ["", settings]
473 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
475 print ("Creating/updating branch(es) in %s based on origin branch(es)"
478 originPrefix = "origin/p4/"
480 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
482 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
485 headName = line[len(originPrefix):]
486 remoteHead = localRefPrefix + headName
489 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
490 if (not original.has_key('depot-paths')
491 or not original.has_key('change')):
495 if not gitBranchExists(remoteHead):
497 print "creating %s" % remoteHead
500 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
501 if settings.has_key('change') > 0:
502 if settings['depot-paths'] == original['depot-paths']:
503 originP4Change = int(original['change'])
504 p4Change = int(settings['change'])
505 if originP4Change > p4Change:
506 print ("%s (%s) is newer than %s (%s). "
507 "Updating p4 branch from origin."
508 % (originHead, originP4Change,
509 remoteHead, p4Change))
512 print ("Ignoring: %s was imported from %s while "
513 "%s was imported from %s"
514 % (originHead, ','.join(original['depot-paths']),
515 remoteHead, ','.join(settings['depot-paths'])))
518 system("git update-ref %s %s" % (remoteHead, originHead))
520 def originP4BranchesExist():
521 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
523 def p4ChangesForPaths(depotPaths, changeRange):
527 cmd += ["%s...%s" % (p, changeRange)]
528 output = p4_read_pipe_lines(cmd)
532 changeNum = int(line.split(" ")[1])
533 changes[changeNum] = True
535 changelist = changes.keys()
539 def p4PathStartsWith(path, prefix):
540 # This method tries to remedy a potential mixed-case issue:
542 # If UserA adds //depot/DirA/file1
543 # and UserB adds //depot/dira/file2
545 # we may or may not have a problem. If you have core.ignorecase=true,
546 # we treat DirA and dira as the same directory
547 ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
549 return path.lower().startswith(prefix.lower())
550 return path.startswith(prefix)
554 self.usage = "usage: %prog [options]"
559 self.userMapFromPerforceServer = False
561 def getUserCacheFilename(self):
562 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
563 return home + "/.gitp4-usercache.txt"
565 def getUserMapFromPerforceServer(self):
566 if self.userMapFromPerforceServer:
571 for output in p4CmdList("users"):
572 if not output.has_key("User"):
574 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
575 self.emails[output["Email"]] = output["User"]
579 for (key, val) in self.users.items():
580 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
582 open(self.getUserCacheFilename(), "wb").write(s)
583 self.userMapFromPerforceServer = True
585 def loadUserMapFromCache(self):
587 self.userMapFromPerforceServer = False
589 cache = open(self.getUserCacheFilename(), "rb")
590 lines = cache.readlines()
593 entry = line.strip().split("\t")
594 self.users[entry[0]] = entry[1]
596 self.getUserMapFromPerforceServer()
598 class P4Debug(Command):
600 Command.__init__(self)
602 optparse.make_option("--verbose", dest="verbose", action="store_true",
605 self.description = "A tool to debug the output of p4 -G."
606 self.needsGit = False
611 for output in p4CmdList(args):
612 print 'Element: %d' % j
617 class P4RollBack(Command):
619 Command.__init__(self)
621 optparse.make_option("--verbose", dest="verbose", action="store_true"),
622 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
624 self.description = "A tool to debug the multi-branch import. Don't use :)"
626 self.rollbackLocalBranches = False
631 maxChange = int(args[0])
633 if "p4ExitCode" in p4Cmd("changes -m 1"):
634 die("Problems executing p4");
636 if self.rollbackLocalBranches:
637 refPrefix = "refs/heads/"
638 lines = read_pipe_lines("git rev-parse --symbolic --branches")
640 refPrefix = "refs/remotes/"
641 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
644 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
646 ref = refPrefix + line
647 log = extractLogMessageFromGitCommit(ref)
648 settings = extractSettingsGitLog(log)
650 depotPaths = settings['depot-paths']
651 change = settings['change']
655 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
656 for p in depotPaths]))) == 0:
657 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
658 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
661 while change and int(change) > maxChange:
664 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
665 system("git update-ref %s \"%s^\"" % (ref, ref))
666 log = extractLogMessageFromGitCommit(ref)
667 settings = extractSettingsGitLog(log)
670 depotPaths = settings['depot-paths']
671 change = settings['change']
674 print "%s rewound to %s" % (ref, change)
678 class P4Submit(Command, P4UserMap):
680 Command.__init__(self)
681 P4UserMap.__init__(self)
683 optparse.make_option("--verbose", dest="verbose", action="store_true"),
684 optparse.make_option("--origin", dest="origin"),
685 optparse.make_option("-M", dest="detectRenames", action="store_true"),
686 # preserve the user, requires relevant p4 permissions
687 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
689 self.description = "Submit changes from git to the perforce depot."
690 self.usage += " [name of git branch to submit into perforce depot]"
691 self.interactive = True
693 self.detectRenames = False
695 self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
696 self.isWindows = (platform.system() == "Windows")
697 self.myP4UserId = None
700 if len(p4CmdList("opened ...")) > 0:
701 die("You have files opened with perforce! Close them before starting the sync.")
703 # replaces everything between 'Description:' and the next P4 submit template field with the
705 def prepareLogMessage(self, template, message):
708 inDescriptionSection = False
710 for line in template.split("\n"):
711 if line.startswith("#"):
712 result += line + "\n"
715 if inDescriptionSection:
716 if line.startswith("Files:") or line.startswith("Jobs:"):
717 inDescriptionSection = False
721 if line.startswith("Description:"):
722 inDescriptionSection = True
724 for messageLine in message.split("\n"):
725 line += "\t" + messageLine + "\n"
727 result += line + "\n"
731 def p4UserForCommit(self,id):
732 # Return the tuple (perforce user,git email) for a given git commit id
733 self.getUserMapFromPerforceServer()
734 gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
735 gitEmail = gitEmail.strip()
736 if not self.emails.has_key(gitEmail):
737 return (None,gitEmail)
739 return (self.emails[gitEmail],gitEmail)
741 def checkValidP4Users(self,commits):
742 # check if any git authors cannot be mapped to p4 users
744 (user,email) = self.p4UserForCommit(id)
746 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
747 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
750 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
752 def lastP4Changelist(self):
753 # Get back the last changelist number submitted in this client spec. This
754 # then gets used to patch up the username in the change. If the same
755 # client spec is being used by multiple processes then this might go
757 results = p4CmdList("client -o") # find the current client
760 if r.has_key('Client'):
764 die("could not get client spec")
765 results = p4CmdList(["changes", "-c", client, "-m", "1"])
767 if r.has_key('change'):
769 die("Could not get changelist number for last submit - cannot patch up user details")
771 def modifyChangelistUser(self, changelist, newUser):
772 # fixup the user field of a changelist after it has been submitted.
773 changes = p4CmdList("change -o %s" % changelist)
774 if len(changes) != 1:
775 die("Bad output from p4 change modifying %s to user %s" %
776 (changelist, newUser))
779 if c['User'] == newUser: return # nothing to do
781 input = marshal.dumps(c)
783 result = p4CmdList("change -f -i", stdin=input)
785 if r.has_key('code'):
786 if r['code'] == 'error':
787 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
788 if r.has_key('data'):
789 print("Updated user field for changelist %s to %s" % (changelist, newUser))
791 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
793 def canChangeChangelists(self):
794 # check to see if we have p4 admin or super-user permissions, either of
795 # which are required to modify changelists.
796 results = p4CmdList("protects %s" % self.depotPath)
798 if r.has_key('perm'):
799 if r['perm'] == 'admin':
801 if r['perm'] == 'super':
807 return self.myP4UserId
809 results = p4CmdList("user -o")
811 if r.has_key('User'):
812 self.myP4UserId = r['User']
814 die("Could not find your p4 user id")
816 def p4UserIsMe(self, p4User):
817 # return True if the given p4 user is actually me
819 if not p4User or p4User != me:
824 def prepareSubmitTemplate(self):
825 # remove lines in the Files section that show changes to files outside the depot path we're committing into
827 inFilesSection = False
828 for line in p4_read_pipe_lines(['change', '-o']):
829 if line.endswith("\r\n"):
830 line = line[:-2] + "\n"
832 if line.startswith("\t"):
833 # path starts and ends with a tab
835 lastTab = path.rfind("\t")
837 path = path[:lastTab]
838 if not p4PathStartsWith(path, self.depotPath):
841 inFilesSection = False
843 if line.startswith("Files:"):
844 inFilesSection = True
850 def edit_template(self, template_file):
851 """Invoke the editor to let the user change the submission
852 message. Return true if okay to continue with the submit."""
854 # if configured to skip the editing part, just submit
855 if gitConfig("git-p4.skipSubmitEdit") == "true":
858 # look at the modification time, to check later if the user saved
860 mtime = os.stat(template_file).st_mtime
863 if os.environ.has_key("P4EDITOR"):
864 editor = os.environ.get("P4EDITOR")
866 editor = read_pipe("git var GIT_EDITOR").strip()
867 system(editor + " " + template_file)
869 # If the file was not saved, prompt to see if this patch should
870 # be skipped. But skip this verification step if configured so.
871 if gitConfig("git-p4.skipSubmitEditCheck") == "true":
874 if os.stat(template_file).st_mtime <= mtime:
876 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
882 def applyCommit(self, id):
883 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
885 (p4User, gitEmail) = self.p4UserForCommit(id)
887 if not self.detectRenames:
888 # If not explicitly set check the config variable
889 self.detectRenames = gitConfig("git-p4.detectRenames")
891 if self.detectRenames.lower() == "false" or self.detectRenames == "":
893 elif self.detectRenames.lower() == "true":
896 diffOpts = "-M%s" % self.detectRenames
898 detectCopies = gitConfig("git-p4.detectCopies")
899 if detectCopies.lower() == "true":
901 elif detectCopies != "" and detectCopies.lower() != "false":
902 diffOpts += " -C%s" % detectCopies
904 if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
905 diffOpts += " --find-copies-harder"
907 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
909 filesToDelete = set()
911 filesToChangeExecBit = {}
913 diff = parseDiffTreeEntry(line)
914 modifier = diff['status']
918 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
919 filesToChangeExecBit[path] = diff['dst_mode']
920 editedFiles.add(path)
921 elif modifier == "A":
923 filesToChangeExecBit[path] = diff['dst_mode']
924 if path in filesToDelete:
925 filesToDelete.remove(path)
926 elif modifier == "D":
927 filesToDelete.add(path)
928 if path in filesToAdd:
929 filesToAdd.remove(path)
930 elif modifier == "C":
931 src, dest = diff['src'], diff['dst']
932 p4_integrate(src, dest)
933 if diff['src_sha1'] != diff['dst_sha1']:
935 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
937 filesToChangeExecBit[dest] = diff['dst_mode']
939 editedFiles.add(dest)
940 elif modifier == "R":
941 src, dest = diff['src'], diff['dst']
942 p4_integrate(src, dest)
943 if diff['src_sha1'] != diff['dst_sha1']:
945 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
947 filesToChangeExecBit[dest] = diff['dst_mode']
949 editedFiles.add(dest)
950 filesToDelete.add(src)
952 die("unknown modifier %s for %s" % (modifier, path))
954 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
955 patchcmd = diffcmd + " | git apply "
956 tryPatchCmd = patchcmd + "--check -"
957 applyPatchCmd = patchcmd + "--check --apply -"
959 if os.system(tryPatchCmd) != 0:
960 print "Unfortunately applying the change failed!"
961 print "What do you want to do?"
963 while response != "s" and response != "a" and response != "w":
964 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
965 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
967 print "Skipping! Good luck with the next patches..."
968 for f in editedFiles:
973 elif response == "a":
974 os.system(applyPatchCmd)
975 if len(filesToAdd) > 0:
976 print "You may also want to call p4 add on the following files:"
977 print " ".join(filesToAdd)
978 if len(filesToDelete):
979 print "The following files should be scheduled for deletion with p4 delete:"
980 print " ".join(filesToDelete)
981 die("Please resolve and submit the conflict manually and "
982 + "continue afterwards with git-p4 submit --continue")
983 elif response == "w":
984 system(diffcmd + " > patch.txt")
985 print "Patch saved to patch.txt in %s !" % self.clientPath
986 die("Please resolve and submit the conflict manually and "
987 "continue afterwards with git-p4 submit --continue")
989 system(applyPatchCmd)
993 for f in filesToDelete:
997 # Set/clear executable bits
998 for f in filesToChangeExecBit.keys():
999 mode = filesToChangeExecBit[f]
1000 setP4ExecBit(f, mode)
1002 logMessage = extractLogMessageFromGitCommit(id)
1003 logMessage = logMessage.strip()
1005 template = self.prepareSubmitTemplate()
1007 if self.interactive:
1008 submitTemplate = self.prepareLogMessage(template, logMessage)
1010 if self.preserveUser:
1011 submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
1013 if os.environ.has_key("P4DIFF"):
1014 del(os.environ["P4DIFF"])
1016 for editedFile in editedFiles:
1017 diff += p4_read_pipe(['diff', '-du', editedFile])
1020 for newFile in filesToAdd:
1021 newdiff += "==== new file ====\n"
1022 newdiff += "--- /dev/null\n"
1023 newdiff += "+++ %s\n" % newFile
1024 f = open(newFile, "r")
1025 for line in f.readlines():
1026 newdiff += "+" + line
1029 if self.checkAuthorship and not self.p4UserIsMe(p4User):
1030 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1031 submitTemplate += "######## Use git-p4 option --preserve-user to modify authorship\n"
1032 submitTemplate += "######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n"
1034 separatorLine = "######## everything below this line is just the diff #######\n"
1036 (handle, fileName) = tempfile.mkstemp()
1037 tmpFile = os.fdopen(handle, "w+")
1039 submitTemplate = submitTemplate.replace("\n", "\r\n")
1040 separatorLine = separatorLine.replace("\n", "\r\n")
1041 newdiff = newdiff.replace("\n", "\r\n")
1042 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
1045 if self.edit_template(fileName):
1046 # read the edited message and submit
1047 tmpFile = open(fileName, "rb")
1048 message = tmpFile.read()
1050 submitTemplate = message[:message.index(separatorLine)]
1052 submitTemplate = submitTemplate.replace("\r\n", "\n")
1053 p4_write_pipe(['submit', '-i'], submitTemplate)
1055 if self.preserveUser:
1057 # Get last changelist number. Cannot easily get it from
1058 # the submit command output as the output is
1060 changelist = self.lastP4Changelist()
1061 self.modifyChangelistUser(changelist, p4User)
1064 for f in editedFiles:
1066 for f in filesToAdd:
1072 fileName = "submit.txt"
1073 file = open(fileName, "w+")
1074 file.write(self.prepareLogMessage(template, logMessage))
1076 print ("Perforce submit template written as %s. "
1077 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
1078 % (fileName, fileName))
1080 def run(self, args):
1082 self.master = currentGitBranch()
1083 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1084 die("Detecting current git branch failed!")
1085 elif len(args) == 1:
1086 self.master = args[0]
1090 allowSubmit = gitConfig("git-p4.allowSubmit")
1091 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1092 die("%s is not in git-p4.allowSubmit" % self.master)
1094 [upstream, settings] = findUpstreamBranchPoint()
1095 self.depotPath = settings['depot-paths'][0]
1096 if len(self.origin) == 0:
1097 self.origin = upstream
1099 if self.preserveUser:
1100 if not self.canChangeChangelists():
1101 die("Cannot preserve user names without p4 super-user or admin permissions")
1104 print "Origin branch is " + self.origin
1106 if len(self.depotPath) == 0:
1107 print "Internal error: cannot locate perforce depot path from existing branches"
1110 self.clientPath = p4Where(self.depotPath)
1112 if len(self.clientPath) == 0:
1113 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
1116 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1117 self.oldWorkingDirectory = os.getcwd()
1119 chdir(self.clientPath)
1120 print "Synchronizing p4 checkout..."
1125 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1126 commits.append(line.strip())
1129 if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1130 self.checkAuthorship = False
1132 self.checkAuthorship = True
1134 if self.preserveUser:
1135 self.checkValidP4Users(commits)
1137 while len(commits) > 0:
1139 commits = commits[1:]
1140 self.applyCommit(commit)
1141 if not self.interactive:
1144 if len(commits) == 0:
1145 print "All changes applied!"
1146 chdir(self.oldWorkingDirectory)
1156 class P4Sync(Command, P4UserMap):
1157 delete_actions = ( "delete", "move/delete", "purge" )
1160 Command.__init__(self)
1161 P4UserMap.__init__(self)
1163 optparse.make_option("--branch", dest="branch"),
1164 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1165 optparse.make_option("--changesfile", dest="changesFile"),
1166 optparse.make_option("--silent", dest="silent", action="store_true"),
1167 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1168 optparse.make_option("--verbose", dest="verbose", action="store_true"),
1169 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1170 help="Import into refs/heads/ , not refs/remotes"),
1171 optparse.make_option("--max-changes", dest="maxChanges"),
1172 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1173 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1174 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1175 help="Only sync files that are included in the Perforce Client Spec")
1177 self.description = """Imports from Perforce into a git repository.\n
1179 //depot/my/project/ -- to import the current head
1180 //depot/my/project/@all -- to import everything
1181 //depot/my/project/@1,6 -- to import only from revision 1 to 6
1183 (a ... is not needed in the path p4 specification, it's added implicitly)"""
1185 self.usage += " //depot/path[@revRange]"
1187 self.createdBranches = set()
1188 self.committedChanges = set()
1190 self.detectBranches = False
1191 self.detectLabels = False
1192 self.changesFile = ""
1193 self.syncWithOrigin = True
1194 self.verbose = False
1195 self.importIntoRemotes = True
1196 self.maxChanges = ""
1197 self.isWindows = (platform.system() == "Windows")
1198 self.keepRepoPath = False
1199 self.depotPaths = None
1200 self.p4BranchesInGit = []
1201 self.cloneExclude = []
1202 self.useClientSpec = False
1203 self.clientSpecDirs = []
1205 if gitConfig("git-p4.syncFromOrigin") == "false":
1206 self.syncWithOrigin = False
1209 # P4 wildcards are not allowed in filenames. P4 complains
1210 # if you simply add them, but you can force it with "-f", in
1211 # which case it translates them into %xx encoding internally.
1212 # Search for and fix just these four characters. Do % last so
1213 # that fixing it does not inadvertently create new %-escapes.
1215 def wildcard_decode(self, path):
1216 # Cannot have * in a filename in windows; untested as to
1217 # what p4 would do in such a case.
1218 if not self.isWindows:
1219 path = path.replace("%2A", "*")
1220 path = path.replace("%23", "#") \
1221 .replace("%40", "@") \
1222 .replace("%25", "%")
1225 def extractFilesFromCommit(self, commit):
1226 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1227 for path in self.cloneExclude]
1230 while commit.has_key("depotFile%s" % fnum):
1231 path = commit["depotFile%s" % fnum]
1233 if [p for p in self.cloneExclude
1234 if p4PathStartsWith(path, p)]:
1237 found = [p for p in self.depotPaths
1238 if p4PathStartsWith(path, p)]
1245 file["rev"] = commit["rev%s" % fnum]
1246 file["action"] = commit["action%s" % fnum]
1247 file["type"] = commit["type%s" % fnum]
1252 def stripRepoPath(self, path, prefixes):
1253 if self.useClientSpec:
1255 # if using the client spec, we use the output directory
1256 # specified in the client. For example, a view
1257 # //depot/foo/branch/... //client/branch/foo/...
1258 # will end up putting all foo/branch files into
1260 for val in self.clientSpecDirs:
1261 if path.startswith(val[0]):
1262 # replace the depot path with the client path
1263 path = path.replace(val[0], val[1][1])
1264 # now strip out the client (//client/...)
1265 path = re.sub("^(//[^/]+/)", '', path)
1266 # the rest is all path
1269 if self.keepRepoPath:
1270 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1273 if p4PathStartsWith(path, p):
1274 path = path[len(p):]
1278 def splitFilesIntoBranches(self, commit):
1281 while commit.has_key("depotFile%s" % fnum):
1282 path = commit["depotFile%s" % fnum]
1283 found = [p for p in self.depotPaths
1284 if p4PathStartsWith(path, p)]
1291 file["rev"] = commit["rev%s" % fnum]
1292 file["action"] = commit["action%s" % fnum]
1293 file["type"] = commit["type%s" % fnum]
1296 relPath = self.stripRepoPath(path, self.depotPaths)
1298 for branch in self.knownBranches.keys():
1300 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1301 if relPath.startswith(branch + "/"):
1302 if branch not in branches:
1303 branches[branch] = []
1304 branches[branch].append(file)
1309 # output one file from the P4 stream
1310 # - helper for streamP4Files
1312 def streamOneP4File(self, file, contents):
1313 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1314 relPath = self.wildcard_decode(relPath)
1316 sys.stderr.write("%s\n" % relPath)
1318 (type_base, type_mods) = split_p4_type(file["type"])
1321 if "x" in type_mods:
1323 if type_base == "symlink":
1325 # p4 print on a symlink contains "target\n"; remove the newline
1326 data = ''.join(contents)
1327 contents = [data[:-1]]
1329 if type_base == "utf16":
1330 # p4 delivers different text in the python output to -G
1331 # than it does when using "print -o", or normal p4 client
1332 # operations. utf16 is converted to ascii or utf8, perhaps.
1333 # But ascii text saved as -t utf16 is completely mangled.
1334 # Invoke print -o to get the real contents.
1335 text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
1338 if type_base == "apple":
1339 # Apple filetype files will be streamed as a concatenation of
1340 # its appledouble header and the contents. This is useless
1341 # on both macs and non-macs. If using "print -q -o xx", it
1342 # will create "xx" with the data, and "%xx" with the header.
1343 # This is also not very useful.
1345 # Ideally, someday, this script can learn how to generate
1346 # appledouble files directly and import those to git, but
1347 # non-mac machines can never find a use for apple filetype.
1348 print "\nIgnoring apple filetype file %s" % file['depotFile']
1351 # Perhaps windows wants unicode, utf16 newlines translated too;
1352 # but this is not doing it.
1353 if self.isWindows and type_base == "text":
1355 for data in contents:
1356 data = data.replace("\r\n", "\n")
1357 mangled.append(data)
1360 # Note that we do not try to de-mangle keywords on utf16 files,
1361 # even though in theory somebody may want that.
1362 if type_base in ("text", "unicode", "binary"):
1363 if "ko" in type_mods:
1364 text = ''.join(contents)
1365 text = re.sub(r'\$(Id|Header):[^$]*\$', r'$\1$', text)
1367 elif "k" in type_mods:
1368 text = ''.join(contents)
1369 text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$', r'$\1$', text)
1372 self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
1377 length = length + len(d)
1379 self.gitStream.write("data %d\n" % length)
1381 self.gitStream.write(d)
1382 self.gitStream.write("\n")
1384 def streamOneP4Deletion(self, file):
1385 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1387 sys.stderr.write("delete %s\n" % relPath)
1388 self.gitStream.write("D %s\n" % relPath)
1390 # handle another chunk of streaming data
1391 def streamP4FilesCb(self, marshalled):
1393 if marshalled.has_key('depotFile') and self.stream_have_file_info:
1394 # start of a new file - output the old one first
1395 self.streamOneP4File(self.stream_file, self.stream_contents)
1396 self.stream_file = {}
1397 self.stream_contents = []
1398 self.stream_have_file_info = False
1400 # pick up the new file information... for the
1401 # 'data' field we need to append to our array
1402 for k in marshalled.keys():
1404 self.stream_contents.append(marshalled['data'])
1406 self.stream_file[k] = marshalled[k]
1408 self.stream_have_file_info = True
1410 # Stream directly from "p4 files" into "git fast-import"
1411 def streamP4Files(self, files):
1418 for val in self.clientSpecDirs:
1419 if f['path'].startswith(val[0]):
1425 filesForCommit.append(f)
1426 if f['action'] in self.delete_actions:
1427 filesToDelete.append(f)
1429 filesToRead.append(f)
1432 for f in filesToDelete:
1433 self.streamOneP4Deletion(f)
1435 if len(filesToRead) > 0:
1436 self.stream_file = {}
1437 self.stream_contents = []
1438 self.stream_have_file_info = False
1440 # curry self argument
1441 def streamP4FilesCbSelf(entry):
1442 self.streamP4FilesCb(entry)
1444 fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
1446 p4CmdList(["-x", "-", "print"],
1448 cb=streamP4FilesCbSelf)
1451 if self.stream_file.has_key('depotFile'):
1452 self.streamOneP4File(self.stream_file, self.stream_contents)
1454 def commit(self, details, files, branch, branchPrefixes, parent = ""):
1455 epoch = details["time"]
1456 author = details["user"]
1457 self.branchPrefixes = branchPrefixes
1460 print "commit into %s" % branch
1462 # start with reading files; if that fails, we should not
1466 if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1467 new_files.append (f)
1469 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1471 self.gitStream.write("commit %s\n" % branch)
1472 # gitStream.write("mark :%s\n" % details["change"])
1473 self.committedChanges.add(int(details["change"]))
1475 if author not in self.users:
1476 self.getUserMapFromPerforceServer()
1477 if author in self.users:
1478 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1480 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1482 self.gitStream.write("committer %s\n" % committer)
1484 self.gitStream.write("data <<EOT\n")
1485 self.gitStream.write(details["desc"])
1486 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1487 % (','.join (branchPrefixes), details["change"]))
1488 if len(details['options']) > 0:
1489 self.gitStream.write(": options = %s" % details['options'])
1490 self.gitStream.write("]\nEOT\n\n")
1494 print "parent %s" % parent
1495 self.gitStream.write("from %s\n" % parent)
1497 self.streamP4Files(new_files)
1498 self.gitStream.write("\n")
1500 change = int(details["change"])
1502 if self.labels.has_key(change):
1503 label = self.labels[change]
1504 labelDetails = label[0]
1505 labelRevisions = label[1]
1507 print "Change %s is labelled %s" % (change, labelDetails)
1509 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
1510 for p in branchPrefixes])
1512 if len(files) == len(labelRevisions):
1516 if info["action"] in self.delete_actions:
1518 cleanedFiles[info["depotFile"]] = info["rev"]
1520 if cleanedFiles == labelRevisions:
1521 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1522 self.gitStream.write("from %s\n" % branch)
1524 owner = labelDetails["Owner"]
1526 if author in self.users:
1527 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1529 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1530 self.gitStream.write("tagger %s\n" % tagger)
1531 self.gitStream.write("data <<EOT\n")
1532 self.gitStream.write(labelDetails["Description"])
1533 self.gitStream.write("EOT\n\n")
1537 print ("Tag %s does not match with change %s: files do not match."
1538 % (labelDetails["label"], change))
1542 print ("Tag %s does not match with change %s: file count is different."
1543 % (labelDetails["label"], change))
1545 def getLabels(self):
1548 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1549 if len(l) > 0 and not self.silent:
1550 print "Finding files belonging to labels in %s" % `self.depotPaths`
1553 label = output["label"]
1557 print "Querying files for label %s" % label
1558 for file in p4CmdList(["files"] +
1559 ["%s...@%s" % (p, label)
1560 for p in self.depotPaths]):
1561 revisions[file["depotFile"]] = file["rev"]
1562 change = int(file["change"])
1563 if change > newestChange:
1564 newestChange = change
1566 self.labels[newestChange] = [output, revisions]
1569 print "Label changes: %s" % self.labels.keys()
1571 def guessProjectName(self):
1572 for p in self.depotPaths:
1575 p = p[p.strip().rfind("/") + 1:]
1576 if not p.endswith("/"):
1580 def getBranchMapping(self):
1581 lostAndFoundBranches = set()
1583 user = gitConfig("git-p4.branchUser")
1585 command = "branches -u %s" % user
1587 command = "branches"
1589 for info in p4CmdList(command):
1590 details = p4Cmd("branch -o %s" % info["branch"])
1592 while details.has_key("View%s" % viewIdx):
1593 paths = details["View%s" % viewIdx].split(" ")
1594 viewIdx = viewIdx + 1
1595 # require standard //depot/foo/... //depot/bar/... mapping
1596 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1599 destination = paths[1]
1601 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
1602 source = source[len(self.depotPaths[0]):-4]
1603 destination = destination[len(self.depotPaths[0]):-4]
1605 if destination in self.knownBranches:
1607 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1608 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1611 self.knownBranches[destination] = source
1613 lostAndFoundBranches.discard(destination)
1615 if source not in self.knownBranches:
1616 lostAndFoundBranches.add(source)
1618 # Perforce does not strictly require branches to be defined, so we also
1619 # check git config for a branch list.
1621 # Example of branch definition in git config file:
1623 # branchList=main:branchA
1624 # branchList=main:branchB
1625 # branchList=branchA:branchC
1626 configBranches = gitConfigList("git-p4.branchList")
1627 for branch in configBranches:
1629 (source, destination) = branch.split(":")
1630 self.knownBranches[destination] = source
1632 lostAndFoundBranches.discard(destination)
1634 if source not in self.knownBranches:
1635 lostAndFoundBranches.add(source)
1638 for branch in lostAndFoundBranches:
1639 self.knownBranches[branch] = branch
1641 def getBranchMappingFromGitBranches(self):
1642 branches = p4BranchesInGit(self.importIntoRemotes)
1643 for branch in branches.keys():
1644 if branch == "master":
1647 branch = branch[len(self.projectName):]
1648 self.knownBranches[branch] = branch
1650 def listExistingP4GitBranches(self):
1651 # branches holds mapping from name to commit
1652 branches = p4BranchesInGit(self.importIntoRemotes)
1653 self.p4BranchesInGit = branches.keys()
1654 for branch in branches.keys():
1655 self.initialParents[self.refPrefix + branch] = branches[branch]
1657 def updateOptionDict(self, d):
1659 if self.keepRepoPath:
1660 option_keys['keepRepoPath'] = 1
1662 d["options"] = ' '.join(sorted(option_keys.keys()))
1664 def readOptions(self, d):
1665 self.keepRepoPath = (d.has_key('options')
1666 and ('keepRepoPath' in d['options']))
1668 def gitRefForBranch(self, branch):
1669 if branch == "main":
1670 return self.refPrefix + "master"
1672 if len(branch) <= 0:
1675 return self.refPrefix + self.projectName + branch
1677 def gitCommitByP4Change(self, ref, change):
1679 print "looking in ref " + ref + " for change %s using bisect..." % change
1682 latestCommit = parseRevision(ref)
1686 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1687 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1692 log = extractLogMessageFromGitCommit(next)
1693 settings = extractSettingsGitLog(log)
1694 currentChange = int(settings['change'])
1696 print "current change %s" % currentChange
1698 if currentChange == change:
1700 print "found %s" % next
1703 if currentChange < change:
1704 earliestCommit = "^%s" % next
1706 latestCommit = "%s" % next
1710 def importNewBranch(self, branch, maxChange):
1711 # make fast-import flush all changes to disk and update the refs using the checkpoint
1712 # command so that we can try to find the branch parent in the git history
1713 self.gitStream.write("checkpoint\n\n");
1714 self.gitStream.flush();
1715 branchPrefix = self.depotPaths[0] + branch + "/"
1716 range = "@1,%s" % maxChange
1717 #print "prefix" + branchPrefix
1718 changes = p4ChangesForPaths([branchPrefix], range)
1719 if len(changes) <= 0:
1721 firstChange = changes[0]
1722 #print "first change in branch: %s" % firstChange
1723 sourceBranch = self.knownBranches[branch]
1724 sourceDepotPath = self.depotPaths[0] + sourceBranch
1725 sourceRef = self.gitRefForBranch(sourceBranch)
1726 #print "source " + sourceBranch
1728 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1729 #print "branch parent: %s" % branchParentChange
1730 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1731 if len(gitParent) > 0:
1732 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1733 #print "parent git commit: %s" % gitParent
1735 self.importChanges(changes)
1738 def importChanges(self, changes):
1740 for change in changes:
1741 description = p4Cmd("describe %s" % change)
1742 self.updateOptionDict(description)
1745 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1750 if self.detectBranches:
1751 branches = self.splitFilesIntoBranches(description)
1752 for branch in branches.keys():
1754 branchPrefix = self.depotPaths[0] + branch + "/"
1758 filesForCommit = branches[branch]
1761 print "branch is %s" % branch
1763 self.updatedBranches.add(branch)
1765 if branch not in self.createdBranches:
1766 self.createdBranches.add(branch)
1767 parent = self.knownBranches[branch]
1768 if parent == branch:
1771 fullBranch = self.projectName + branch
1772 if fullBranch not in self.p4BranchesInGit:
1774 print("\n Importing new branch %s" % fullBranch);
1775 if self.importNewBranch(branch, change - 1):
1777 self.p4BranchesInGit.append(fullBranch)
1779 print("\n Resuming with change %s" % change);
1782 print "parent determined through known branches: %s" % parent
1784 branch = self.gitRefForBranch(branch)
1785 parent = self.gitRefForBranch(parent)
1788 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1790 if len(parent) == 0 and branch in self.initialParents:
1791 parent = self.initialParents[branch]
1792 del self.initialParents[branch]
1794 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1796 files = self.extractFilesFromCommit(description)
1797 self.commit(description, files, self.branch, self.depotPaths,
1799 self.initialParent = ""
1801 print self.gitError.read()
1804 def importHeadRevision(self, revision):
1805 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1808 details["user"] = "git perforce import user"
1809 details["desc"] = ("Initial import of %s from the state at revision %s\n"
1810 % (' '.join(self.depotPaths), revision))
1811 details["change"] = revision
1815 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
1817 for info in p4CmdList(["files"] + fileArgs):
1819 if 'code' in info and info['code'] == 'error':
1820 sys.stderr.write("p4 returned an error: %s\n"
1822 if info['data'].find("must refer to client") >= 0:
1823 sys.stderr.write("This particular p4 error is misleading.\n")
1824 sys.stderr.write("Perhaps the depot path was misspelled.\n");
1825 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
1827 if 'p4ExitCode' in info:
1828 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
1832 change = int(info["change"])
1833 if change > newestRevision:
1834 newestRevision = change
1836 if info["action"] in self.delete_actions:
1837 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1838 #fileCnt = fileCnt + 1
1841 for prop in ["depotFile", "rev", "action", "type" ]:
1842 details["%s%s" % (prop, fileCnt)] = info[prop]
1844 fileCnt = fileCnt + 1
1846 details["change"] = newestRevision
1848 # Use time from top-most change so that all git-p4 clones of
1849 # the same p4 repo have the same commit SHA1s.
1850 res = p4CmdList("describe -s %d" % newestRevision)
1853 if r.has_key('time'):
1854 newestTime = int(r['time'])
1855 if newestTime is None:
1856 die("\"describe -s\" on newest change %d did not give a time")
1857 details["time"] = newestTime
1859 self.updateOptionDict(details)
1861 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1863 print "IO error with git fast-import. Is your git version recent enough?"
1864 print self.gitError.read()
1867 def getClientSpec(self):
1868 specList = p4CmdList( "client -o" )
1870 for entry in specList:
1871 for k,v in entry.iteritems():
1872 if k.startswith("View"):
1874 # p4 has these %%1 to %%9 arguments in specs to
1875 # reorder paths; which we can't handle (yet :)
1876 if re.match('%%\d', v) != None:
1877 print "Sorry, can't handle %%n arguments in client specs"
1880 if v.startswith('"'):
1884 index = v.find("...")
1886 # save the "client view"; i.e the RHS of the view
1887 # line that tells the client where to put the
1888 # files for this view.
1889 cv = v[index+3:].strip() # +3 to remove previous '...'
1891 # if the client view doesn't end with a
1892 # ... wildcard, then we're going to mess up the
1893 # output directory, so fail gracefully.
1894 if not cv.endswith('...'):
1895 print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1899 # now save the view; +index means included, -index
1900 # means it should be filtered out.
1902 if v.startswith("-"):
1908 temp[v] = (include, cv)
1910 self.clientSpecDirs = temp.items()
1911 self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1913 def run(self, args):
1914 self.depotPaths = []
1915 self.changeRange = ""
1916 self.initialParent = ""
1917 self.previousDepotPaths = []
1919 # map from branch depot path to parent branch
1920 self.knownBranches = {}
1921 self.initialParents = {}
1922 self.hasOrigin = originP4BranchesExist()
1923 if not self.syncWithOrigin:
1924 self.hasOrigin = False
1926 if self.importIntoRemotes:
1927 self.refPrefix = "refs/remotes/p4/"
1929 self.refPrefix = "refs/heads/p4/"
1931 if self.syncWithOrigin and self.hasOrigin:
1933 print "Syncing with origin first by calling git fetch origin"
1934 system("git fetch origin")
1936 if len(self.branch) == 0:
1937 self.branch = self.refPrefix + "master"
1938 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1939 system("git update-ref %s refs/heads/p4" % self.branch)
1940 system("git branch -D p4");
1941 # create it /after/ importing, when master exists
1942 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1943 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1945 if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1946 self.getClientSpec()
1948 # TODO: should always look at previous commits,
1949 # merge with previous imports, if possible.
1952 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1953 self.listExistingP4GitBranches()
1955 if len(self.p4BranchesInGit) > 1:
1957 print "Importing from/into multiple branches"
1958 self.detectBranches = True
1961 print "branches: %s" % self.p4BranchesInGit
1964 for branch in self.p4BranchesInGit:
1965 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1967 settings = extractSettingsGitLog(logMsg)
1969 self.readOptions(settings)
1970 if (settings.has_key('depot-paths')
1971 and settings.has_key ('change')):
1972 change = int(settings['change']) + 1
1973 p4Change = max(p4Change, change)
1975 depotPaths = sorted(settings['depot-paths'])
1976 if self.previousDepotPaths == []:
1977 self.previousDepotPaths = depotPaths
1980 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1981 prev_list = prev.split("/")
1982 cur_list = cur.split("/")
1983 for i in range(0, min(len(cur_list), len(prev_list))):
1984 if cur_list[i] <> prev_list[i]:
1988 paths.append ("/".join(cur_list[:i + 1]))
1990 self.previousDepotPaths = paths
1993 self.depotPaths = sorted(self.previousDepotPaths)
1994 self.changeRange = "@%s,#head" % p4Change
1995 if not self.detectBranches:
1996 self.initialParent = parseRevision(self.branch)
1997 if not self.silent and not self.detectBranches:
1998 print "Performing incremental import into %s git branch" % self.branch
2000 if not self.branch.startswith("refs/"):
2001 self.branch = "refs/heads/" + self.branch
2003 if len(args) == 0 and self.depotPaths:
2005 print "Depot paths: %s" % ' '.join(self.depotPaths)
2007 if self.depotPaths and self.depotPaths != args:
2008 print ("previous import used depot path %s and now %s was specified. "
2009 "This doesn't work!" % (' '.join (self.depotPaths),
2013 self.depotPaths = sorted(args)
2019 for p in self.depotPaths:
2020 if p.find("@") != -1:
2021 atIdx = p.index("@")
2022 self.changeRange = p[atIdx:]
2023 if self.changeRange == "@all":
2024 self.changeRange = ""
2025 elif ',' not in self.changeRange:
2026 revision = self.changeRange
2027 self.changeRange = ""
2029 elif p.find("#") != -1:
2030 hashIdx = p.index("#")
2031 revision = p[hashIdx:]
2033 elif self.previousDepotPaths == []:
2036 p = re.sub ("\.\.\.$", "", p)
2037 if not p.endswith("/"):
2042 self.depotPaths = newPaths
2045 self.loadUserMapFromCache()
2047 if self.detectLabels:
2050 if self.detectBranches:
2051 ## FIXME - what's a P4 projectName ?
2052 self.projectName = self.guessProjectName()
2055 self.getBranchMappingFromGitBranches()
2057 self.getBranchMapping()
2059 print "p4-git branches: %s" % self.p4BranchesInGit
2060 print "initial parents: %s" % self.initialParents
2061 for b in self.p4BranchesInGit:
2065 b = b[len(self.projectName):]
2066 self.createdBranches.add(b)
2068 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2070 importProcess = subprocess.Popen(["git", "fast-import"],
2071 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2072 stderr=subprocess.PIPE);
2073 self.gitOutput = importProcess.stdout
2074 self.gitStream = importProcess.stdin
2075 self.gitError = importProcess.stderr
2078 self.importHeadRevision(revision)
2082 if len(self.changesFile) > 0:
2083 output = open(self.changesFile).readlines()
2086 changeSet.add(int(line))
2088 for change in changeSet:
2089 changes.append(change)
2093 # catch "git-p4 sync" with no new branches, in a repo that
2094 # does not have any existing git-p4 branches
2095 if len(args) == 0 and not self.p4BranchesInGit:
2096 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
2098 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2100 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2102 if len(self.maxChanges) > 0:
2103 changes = changes[:min(int(self.maxChanges), len(changes))]
2105 if len(changes) == 0:
2107 print "No changes to import!"
2110 if not self.silent and not self.detectBranches:
2111 print "Import destination: %s" % self.branch
2113 self.updatedBranches = set()
2115 self.importChanges(changes)
2119 if len(self.updatedBranches) > 0:
2120 sys.stdout.write("Updated branches: ")
2121 for b in self.updatedBranches:
2122 sys.stdout.write("%s " % b)
2123 sys.stdout.write("\n")
2125 self.gitStream.close()
2126 if importProcess.wait() != 0:
2127 die("fast-import failed: %s" % self.gitError.read())
2128 self.gitOutput.close()
2129 self.gitError.close()
2133 class P4Rebase(Command):
2135 Command.__init__(self)
2137 self.description = ("Fetches the latest revision from perforce and "
2138 + "rebases the current work (branch) against it")
2139 self.verbose = False
2141 def run(self, args):
2145 return self.rebase()
2148 if os.system("git update-index --refresh") != 0:
2149 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.");
2150 if len(read_pipe("git diff-index HEAD --")) > 0:
2151 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2153 [upstream, settings] = findUpstreamBranchPoint()
2154 if len(upstream) == 0:
2155 die("Cannot find upstream branchpoint for rebase")
2157 # the branchpoint may be p4/foo~3, so strip off the parent
2158 upstream = re.sub("~[0-9]+$", "", upstream)
2160 print "Rebasing the current branch onto %s" % upstream
2161 oldHead = read_pipe("git rev-parse HEAD").strip()
2162 system("git rebase %s" % upstream)
2163 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
2166 class P4Clone(P4Sync):
2168 P4Sync.__init__(self)
2169 self.description = "Creates a new git repository and imports from Perforce into it"
2170 self.usage = "usage: %prog [options] //depot/path[@revRange]"
2172 optparse.make_option("--destination", dest="cloneDestination",
2173 action='store', default=None,
2174 help="where to leave result of the clone"),
2175 optparse.make_option("-/", dest="cloneExclude",
2176 action="append", type="string",
2177 help="exclude depot path"),
2178 optparse.make_option("--bare", dest="cloneBare",
2179 action="store_true", default=False),
2181 self.cloneDestination = None
2182 self.needsGit = False
2183 self.cloneBare = False
2185 # This is required for the "append" cloneExclude action
2186 def ensure_value(self, attr, value):
2187 if not hasattr(self, attr) or getattr(self, attr) is None:
2188 setattr(self, attr, value)
2189 return getattr(self, attr)
2191 def defaultDestination(self, args):
2192 ## TODO: use common prefix of args?
2194 depotDir = re.sub("(@[^@]*)$", "", depotPath)
2195 depotDir = re.sub("(#[^#]*)$", "", depotDir)
2196 depotDir = re.sub(r"\.\.\.$", "", depotDir)
2197 depotDir = re.sub(r"/$", "", depotDir)
2198 return os.path.split(depotDir)[1]
2200 def run(self, args):
2204 if self.keepRepoPath and not self.cloneDestination:
2205 sys.stderr.write("Must specify destination for --keep-path\n")
2210 if not self.cloneDestination and len(depotPaths) > 1:
2211 self.cloneDestination = depotPaths[-1]
2212 depotPaths = depotPaths[:-1]
2214 self.cloneExclude = ["/"+p for p in self.cloneExclude]
2215 for p in depotPaths:
2216 if not p.startswith("//"):
2219 if not self.cloneDestination:
2220 self.cloneDestination = self.defaultDestination(args)
2222 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2224 if not os.path.exists(self.cloneDestination):
2225 os.makedirs(self.cloneDestination)
2226 chdir(self.cloneDestination)
2228 init_cmd = [ "git", "init" ]
2230 init_cmd.append("--bare")
2231 subprocess.check_call(init_cmd)
2233 if not P4Sync.run(self, depotPaths):
2235 if self.branch != "master":
2236 if self.importIntoRemotes:
2237 masterbranch = "refs/remotes/p4/master"
2239 masterbranch = "refs/heads/p4/master"
2240 if gitBranchExists(masterbranch):
2241 system("git branch master %s" % masterbranch)
2242 if not self.cloneBare:
2243 system("git checkout -f")
2245 print "Could not detect main branch. No checkout/master branch created."
2249 class P4Branches(Command):
2251 Command.__init__(self)
2253 self.description = ("Shows the git branches that hold imports and their "
2254 + "corresponding perforce depot paths")
2255 self.verbose = False
2257 def run(self, args):
2258 if originP4BranchesExist():
2259 createOrUpdateBranchesFromOrigin()
2261 cmdline = "git rev-parse --symbolic "
2262 cmdline += " --remotes"
2264 for line in read_pipe_lines(cmdline):
2267 if not line.startswith('p4/') or line == "p4/HEAD":
2271 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2272 settings = extractSettingsGitLog(log)
2274 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2277 class HelpFormatter(optparse.IndentedHelpFormatter):
2279 optparse.IndentedHelpFormatter.__init__(self)
2281 def format_description(self, description):
2283 return description + "\n"
2287 def printUsage(commands):
2288 print "usage: %s <command> [options]" % sys.argv[0]
2290 print "valid commands: %s" % ", ".join(commands)
2292 print "Try %s <command> --help for command specific help." % sys.argv[0]
2297 "submit" : P4Submit,
2298 "commit" : P4Submit,
2300 "rebase" : P4Rebase,
2302 "rollback" : P4RollBack,
2303 "branches" : P4Branches
2308 if len(sys.argv[1:]) == 0:
2309 printUsage(commands.keys())
2313 cmdName = sys.argv[1]
2315 klass = commands[cmdName]
2318 print "unknown command %s" % cmdName
2320 printUsage(commands.keys())
2323 options = cmd.options
2324 cmd.gitdir = os.environ.get("GIT_DIR", None)
2328 if len(options) > 0:
2329 options.append(optparse.make_option("--git-dir", dest="gitdir"))
2331 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2333 description = cmd.description,
2334 formatter = HelpFormatter())
2336 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2338 verbose = cmd.verbose
2340 if cmd.gitdir == None:
2341 cmd.gitdir = os.path.abspath(".git")
2342 if not isValidGitDir(cmd.gitdir):
2343 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2344 if os.path.exists(cmd.gitdir):
2345 cdup = read_pipe("git rev-parse --show-cdup").strip()
2349 if not isValidGitDir(cmd.gitdir):
2350 if isValidGitDir(cmd.gitdir + "/.git"):
2351 cmd.gitdir += "/.git"
2353 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2355 os.environ["GIT_DIR"] = cmd.gitdir
2357 if not cmd.run(args):
2361 if __name__ == '__main__':