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