Add p4 read_pipe and write_pipe wrappers
[git/jrn.git] / contrib / fast-import / git-p4
blob3e9df70f29dcfa16ab263d5ace42d8d6acf9aecd
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 p4_write_pipe(c, str):
73 real_cmd = p4_build_cmd(c)
74 return write_pipe(c, str)
76 def read_pipe(c, ignore_error=False):
77 if verbose:
78 sys.stderr.write('Reading pipe: %s\n' % c)
80 pipe = os.popen(c, 'rb')
81 val = pipe.read()
82 if pipe.close() and not ignore_error:
83 die('Command failed: %s' % c)
85 return val
87 def p4_read_pipe(c, ignore_error=False):
88 real_cmd = p4_build_cmd(c)
89 return read_pipe(real_cmd, ignore_error)
91 def read_pipe_lines(c):
92 if verbose:
93 sys.stderr.write('Reading pipe: %s\n' % c)
94 ## todo: check return status
95 pipe = os.popen(c, 'rb')
96 val = pipe.readlines()
97 if pipe.close():
98 die('Command failed: %s' % c)
100 return val
102 def p4_read_pipe_lines(c):
103 """Specifically invoke p4 on the command supplied. """
104 real_cmd = p4_build_cmd(c)
105 return read_pipe_lines(real_cmd)
107 def system(cmd):
108 if verbose:
109 sys.stderr.write("executing %s\n" % cmd)
110 if os.system(cmd) != 0:
111 die("command failed: %s" % cmd)
113 def p4_system(cmd):
114 """Specifically invoke p4 as the system command. """
115 real_cmd = p4_build_cmd(cmd)
116 return system(real_cmd)
118 def isP4Exec(kind):
119 """Determine if a Perforce 'kind' should have execute permission
121 'p4 help filetypes' gives a list of the types. If it starts with 'x',
122 or x follows one of a few letters. Otherwise, if there is an 'x' after
123 a plus sign, it is also executable"""
124 return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
126 def setP4ExecBit(file, mode):
127 # Reopens an already open file and changes the execute bit to match
128 # the execute bit setting in the passed in mode.
130 p4Type = "+x"
132 if not isModeExec(mode):
133 p4Type = getP4OpenedType(file)
134 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
135 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
136 if p4Type[-1] == "+":
137 p4Type = p4Type[0:-1]
139 p4_system("reopen -t %s %s" % (p4Type, file))
141 def getP4OpenedType(file):
142 # Returns the perforce file type for the given file.
144 result = read_pipe("p4 opened %s" % file)
145 match = re.match(".*\((.+)\)\r?$", result)
146 if match:
147 return match.group(1)
148 else:
149 die("Could not determine file type for %s (result: '%s')" % (file, result))
151 def diffTreePattern():
152 # This is a simple generator for the diff tree regex pattern. This could be
153 # a class variable if this and parseDiffTreeEntry were a part of a class.
154 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
155 while True:
156 yield pattern
158 def parseDiffTreeEntry(entry):
159 """Parses a single diff tree entry into its component elements.
161 See git-diff-tree(1) manpage for details about the format of the diff
162 output. This method returns a dictionary with the following elements:
164 src_mode - The mode of the source file
165 dst_mode - The mode of the destination file
166 src_sha1 - The sha1 for the source file
167 dst_sha1 - The sha1 fr the destination file
168 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
169 status_score - The score for the status (applicable for 'C' and 'R'
170 statuses). This is None if there is no score.
171 src - The path for the source file.
172 dst - The path for the destination file. This is only present for
173 copy or renames. If it is not present, this is None.
175 If the pattern is not matched, None is returned."""
177 match = diffTreePattern().next().match(entry)
178 if match:
179 return {
180 'src_mode': match.group(1),
181 'dst_mode': match.group(2),
182 'src_sha1': match.group(3),
183 'dst_sha1': match.group(4),
184 'status': match.group(5),
185 'status_score': match.group(6),
186 'src': match.group(7),
187 'dst': match.group(10)
189 return None
191 def isModeExec(mode):
192 # Returns True if the given git mode represents an executable file,
193 # otherwise False.
194 return mode[-3:] == "755"
196 def isModeExecChanged(src_mode, dst_mode):
197 return isModeExec(src_mode) != isModeExec(dst_mode)
199 def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
200 cmd = p4_build_cmd("-G %s" % (cmd))
201 if verbose:
202 sys.stderr.write("Opening pipe: %s\n" % cmd)
204 # Use a temporary file to avoid deadlocks without
205 # subprocess.communicate(), which would put another copy
206 # of stdout into memory.
207 stdin_file = None
208 if stdin is not None:
209 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
210 stdin_file.write(stdin)
211 stdin_file.flush()
212 stdin_file.seek(0)
214 p4 = subprocess.Popen(cmd, shell=True,
215 stdin=stdin_file,
216 stdout=subprocess.PIPE)
218 result = []
219 try:
220 while True:
221 entry = marshal.load(p4.stdout)
222 result.append(entry)
223 except EOFError:
224 pass
225 exitCode = p4.wait()
226 if exitCode != 0:
227 entry = {}
228 entry["p4ExitCode"] = exitCode
229 result.append(entry)
231 return result
233 def p4Cmd(cmd):
234 list = p4CmdList(cmd)
235 result = {}
236 for entry in list:
237 result.update(entry)
238 return result;
240 def p4Where(depotPath):
241 if not depotPath.endswith("/"):
242 depotPath += "/"
243 output = p4Cmd("where %s..." % depotPath)
244 if output["code"] == "error":
245 return ""
246 clientPath = ""
247 if "path" in output:
248 clientPath = output.get("path")
249 elif "data" in output:
250 data = output.get("data")
251 lastSpace = data.rfind(" ")
252 clientPath = data[lastSpace + 1:]
254 if clientPath.endswith("..."):
255 clientPath = clientPath[:-3]
256 return clientPath
258 def currentGitBranch():
259 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
261 def isValidGitDir(path):
262 if (os.path.exists(path + "/HEAD")
263 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
264 return True;
265 return False
267 def parseRevision(ref):
268 return read_pipe("git rev-parse %s" % ref).strip()
270 def extractLogMessageFromGitCommit(commit):
271 logMessage = ""
273 ## fixme: title is first line of commit, not 1st paragraph.
274 foundTitle = False
275 for log in read_pipe_lines("git cat-file commit %s" % commit):
276 if not foundTitle:
277 if len(log) == 1:
278 foundTitle = True
279 continue
281 logMessage += log
282 return logMessage
284 def extractSettingsGitLog(log):
285 values = {}
286 for line in log.split("\n"):
287 line = line.strip()
288 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
289 if not m:
290 continue
292 assignments = m.group(1).split (':')
293 for a in assignments:
294 vals = a.split ('=')
295 key = vals[0].strip()
296 val = ('='.join (vals[1:])).strip()
297 if val.endswith ('\"') and val.startswith('"'):
298 val = val[1:-1]
300 values[key] = val
302 paths = values.get("depot-paths")
303 if not paths:
304 paths = values.get("depot-path")
305 if paths:
306 values['depot-paths'] = paths.split(',')
307 return values
309 def gitBranchExists(branch):
310 proc = subprocess.Popen(["git", "rev-parse", branch],
311 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
312 return proc.wait() == 0;
314 def gitConfig(key):
315 return read_pipe("git config %s" % key, ignore_error=True).strip()
317 def p4BranchesInGit(branchesAreInRemotes = True):
318 branches = {}
320 cmdline = "git rev-parse --symbolic "
321 if branchesAreInRemotes:
322 cmdline += " --remotes"
323 else:
324 cmdline += " --branches"
326 for line in read_pipe_lines(cmdline):
327 line = line.strip()
329 ## only import to p4/
330 if not line.startswith('p4/') or line == "p4/HEAD":
331 continue
332 branch = line
334 # strip off p4
335 branch = re.sub ("^p4/", "", line)
337 branches[branch] = parseRevision(line)
338 return branches
340 def findUpstreamBranchPoint(head = "HEAD"):
341 branches = p4BranchesInGit()
342 # map from depot-path to branch name
343 branchByDepotPath = {}
344 for branch in branches.keys():
345 tip = branches[branch]
346 log = extractLogMessageFromGitCommit(tip)
347 settings = extractSettingsGitLog(log)
348 if settings.has_key("depot-paths"):
349 paths = ",".join(settings["depot-paths"])
350 branchByDepotPath[paths] = "remotes/p4/" + branch
352 settings = None
353 parent = 0
354 while parent < 65535:
355 commit = head + "~%s" % parent
356 log = extractLogMessageFromGitCommit(commit)
357 settings = extractSettingsGitLog(log)
358 if settings.has_key("depot-paths"):
359 paths = ",".join(settings["depot-paths"])
360 if branchByDepotPath.has_key(paths):
361 return [branchByDepotPath[paths], settings]
363 parent = parent + 1
365 return ["", settings]
367 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
368 if not silent:
369 print ("Creating/updating branch(es) in %s based on origin branch(es)"
370 % localRefPrefix)
372 originPrefix = "origin/p4/"
374 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
375 line = line.strip()
376 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
377 continue
379 headName = line[len(originPrefix):]
380 remoteHead = localRefPrefix + headName
381 originHead = line
383 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
384 if (not original.has_key('depot-paths')
385 or not original.has_key('change')):
386 continue
388 update = False
389 if not gitBranchExists(remoteHead):
390 if verbose:
391 print "creating %s" % remoteHead
392 update = True
393 else:
394 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
395 if settings.has_key('change') > 0:
396 if settings['depot-paths'] == original['depot-paths']:
397 originP4Change = int(original['change'])
398 p4Change = int(settings['change'])
399 if originP4Change > p4Change:
400 print ("%s (%s) is newer than %s (%s). "
401 "Updating p4 branch from origin."
402 % (originHead, originP4Change,
403 remoteHead, p4Change))
404 update = True
405 else:
406 print ("Ignoring: %s was imported from %s while "
407 "%s was imported from %s"
408 % (originHead, ','.join(original['depot-paths']),
409 remoteHead, ','.join(settings['depot-paths'])))
411 if update:
412 system("git update-ref %s %s" % (remoteHead, originHead))
414 def originP4BranchesExist():
415 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
417 def p4ChangesForPaths(depotPaths, changeRange):
418 assert depotPaths
419 output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
420 for p in depotPaths]))
422 changes = []
423 for line in output:
424 changeNum = line.split(" ")[1]
425 changes.append(int(changeNum))
427 changes.sort()
428 return changes
430 class Command:
431 def __init__(self):
432 self.usage = "usage: %prog [options]"
433 self.needsGit = True
435 class P4Debug(Command):
436 def __init__(self):
437 Command.__init__(self)
438 self.options = [
439 optparse.make_option("--verbose", dest="verbose", action="store_true",
440 default=False),
442 self.description = "A tool to debug the output of p4 -G."
443 self.needsGit = False
444 self.verbose = False
446 def run(self, args):
447 j = 0
448 for output in p4CmdList(" ".join(args)):
449 print 'Element: %d' % j
450 j += 1
451 print output
452 return True
454 class P4RollBack(Command):
455 def __init__(self):
456 Command.__init__(self)
457 self.options = [
458 optparse.make_option("--verbose", dest="verbose", action="store_true"),
459 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
461 self.description = "A tool to debug the multi-branch import. Don't use :)"
462 self.verbose = False
463 self.rollbackLocalBranches = False
465 def run(self, args):
466 if len(args) != 1:
467 return False
468 maxChange = int(args[0])
470 if "p4ExitCode" in p4Cmd("changes -m 1"):
471 die("Problems executing p4");
473 if self.rollbackLocalBranches:
474 refPrefix = "refs/heads/"
475 lines = read_pipe_lines("git rev-parse --symbolic --branches")
476 else:
477 refPrefix = "refs/remotes/"
478 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
480 for line in lines:
481 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
482 line = line.strip()
483 ref = refPrefix + line
484 log = extractLogMessageFromGitCommit(ref)
485 settings = extractSettingsGitLog(log)
487 depotPaths = settings['depot-paths']
488 change = settings['change']
490 changed = False
492 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
493 for p in depotPaths]))) == 0:
494 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
495 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
496 continue
498 while change and int(change) > maxChange:
499 changed = True
500 if self.verbose:
501 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
502 system("git update-ref %s \"%s^\"" % (ref, ref))
503 log = extractLogMessageFromGitCommit(ref)
504 settings = extractSettingsGitLog(log)
507 depotPaths = settings['depot-paths']
508 change = settings['change']
510 if changed:
511 print "%s rewound to %s" % (ref, change)
513 return True
515 class P4Submit(Command):
516 def __init__(self):
517 Command.__init__(self)
518 self.options = [
519 optparse.make_option("--verbose", dest="verbose", action="store_true"),
520 optparse.make_option("--origin", dest="origin"),
521 optparse.make_option("-M", dest="detectRename", action="store_true"),
523 self.description = "Submit changes from git to the perforce depot."
524 self.usage += " [name of git branch to submit into perforce depot]"
525 self.interactive = True
526 self.origin = ""
527 self.detectRename = False
528 self.verbose = False
529 self.isWindows = (platform.system() == "Windows")
531 def check(self):
532 if len(p4CmdList("opened ...")) > 0:
533 die("You have files opened with perforce! Close them before starting the sync.")
535 # replaces everything between 'Description:' and the next P4 submit template field with the
536 # commit message
537 def prepareLogMessage(self, template, message):
538 result = ""
540 inDescriptionSection = False
542 for line in template.split("\n"):
543 if line.startswith("#"):
544 result += line + "\n"
545 continue
547 if inDescriptionSection:
548 if line.startswith("Files:"):
549 inDescriptionSection = False
550 else:
551 continue
552 else:
553 if line.startswith("Description:"):
554 inDescriptionSection = True
555 line += "\n"
556 for messageLine in message.split("\n"):
557 line += "\t" + messageLine + "\n"
559 result += line + "\n"
561 return result
563 def prepareSubmitTemplate(self):
564 # remove lines in the Files section that show changes to files outside the depot path we're committing into
565 template = ""
566 inFilesSection = False
567 for line in p4_read_pipe_lines("change -o"):
568 if line.endswith("\r\n"):
569 line = line[:-2] + "\n"
570 if inFilesSection:
571 if line.startswith("\t"):
572 # path starts and ends with a tab
573 path = line[1:]
574 lastTab = path.rfind("\t")
575 if lastTab != -1:
576 path = path[:lastTab]
577 if not path.startswith(self.depotPath):
578 continue
579 else:
580 inFilesSection = False
581 else:
582 if line.startswith("Files:"):
583 inFilesSection = True
585 template += line
587 return template
589 def applyCommit(self, id):
590 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
591 diffOpts = ("", "-M")[self.detectRename]
592 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
593 filesToAdd = set()
594 filesToDelete = set()
595 editedFiles = set()
596 filesToChangeExecBit = {}
597 for line in diff:
598 diff = parseDiffTreeEntry(line)
599 modifier = diff['status']
600 path = diff['src']
601 if modifier == "M":
602 p4_system("edit \"%s\"" % path)
603 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
604 filesToChangeExecBit[path] = diff['dst_mode']
605 editedFiles.add(path)
606 elif modifier == "A":
607 filesToAdd.add(path)
608 filesToChangeExecBit[path] = diff['dst_mode']
609 if path in filesToDelete:
610 filesToDelete.remove(path)
611 elif modifier == "D":
612 filesToDelete.add(path)
613 if path in filesToAdd:
614 filesToAdd.remove(path)
615 elif modifier == "R":
616 src, dest = diff['src'], diff['dst']
617 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
618 p4_system("edit \"%s\"" % (dest))
619 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
620 filesToChangeExecBit[dest] = diff['dst_mode']
621 os.unlink(dest)
622 editedFiles.add(dest)
623 filesToDelete.add(src)
624 else:
625 die("unknown modifier %s for %s" % (modifier, path))
627 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
628 patchcmd = diffcmd + " | git apply "
629 tryPatchCmd = patchcmd + "--check -"
630 applyPatchCmd = patchcmd + "--check --apply -"
632 if os.system(tryPatchCmd) != 0:
633 print "Unfortunately applying the change failed!"
634 print "What do you want to do?"
635 response = "x"
636 while response != "s" and response != "a" and response != "w":
637 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
638 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
639 if response == "s":
640 print "Skipping! Good luck with the next patches..."
641 for f in editedFiles:
642 p4_system("revert \"%s\"" % f);
643 for f in filesToAdd:
644 system("rm %s" %f)
645 return
646 elif response == "a":
647 os.system(applyPatchCmd)
648 if len(filesToAdd) > 0:
649 print "You may also want to call p4 add on the following files:"
650 print " ".join(filesToAdd)
651 if len(filesToDelete):
652 print "The following files should be scheduled for deletion with p4 delete:"
653 print " ".join(filesToDelete)
654 die("Please resolve and submit the conflict manually and "
655 + "continue afterwards with git-p4 submit --continue")
656 elif response == "w":
657 system(diffcmd + " > patch.txt")
658 print "Patch saved to patch.txt in %s !" % self.clientPath
659 die("Please resolve and submit the conflict manually and "
660 "continue afterwards with git-p4 submit --continue")
662 system(applyPatchCmd)
664 for f in filesToAdd:
665 p4_system("add \"%s\"" % f)
666 for f in filesToDelete:
667 p4_system("revert \"%s\"" % f)
668 p4_system("delete \"%s\"" % f)
670 # Set/clear executable bits
671 for f in filesToChangeExecBit.keys():
672 mode = filesToChangeExecBit[f]
673 setP4ExecBit(f, mode)
675 logMessage = extractLogMessageFromGitCommit(id)
676 logMessage = logMessage.strip()
678 template = self.prepareSubmitTemplate()
680 if self.interactive:
681 submitTemplate = self.prepareLogMessage(template, logMessage)
682 if os.environ.has_key("P4DIFF"):
683 del(os.environ["P4DIFF"])
684 diff = read_pipe("p4 diff -du ...")
686 newdiff = ""
687 for newFile in filesToAdd:
688 newdiff += "==== new file ====\n"
689 newdiff += "--- /dev/null\n"
690 newdiff += "+++ %s\n" % newFile
691 f = open(newFile, "r")
692 for line in f.readlines():
693 newdiff += "+" + line
694 f.close()
696 separatorLine = "######## everything below this line is just the diff #######\n"
698 [handle, fileName] = tempfile.mkstemp()
699 tmpFile = os.fdopen(handle, "w+")
700 if self.isWindows:
701 submitTemplate = submitTemplate.replace("\n", "\r\n")
702 separatorLine = separatorLine.replace("\n", "\r\n")
703 newdiff = newdiff.replace("\n", "\r\n")
704 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
705 tmpFile.close()
706 defaultEditor = "vi"
707 if platform.system() == "Windows":
708 defaultEditor = "notepad"
709 if os.environ.has_key("P4EDITOR"):
710 editor = os.environ.get("P4EDITOR")
711 else:
712 editor = os.environ.get("EDITOR", defaultEditor);
713 system(editor + " " + fileName)
714 tmpFile = open(fileName, "rb")
715 message = tmpFile.read()
716 tmpFile.close()
717 os.remove(fileName)
718 submitTemplate = message[:message.index(separatorLine)]
719 if self.isWindows:
720 submitTemplate = submitTemplate.replace("\r\n", "\n")
722 write_pipe("p4 submit -i", submitTemplate)
723 else:
724 fileName = "submit.txt"
725 file = open(fileName, "w+")
726 file.write(self.prepareLogMessage(template, logMessage))
727 file.close()
728 print ("Perforce submit template written as %s. "
729 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
730 % (fileName, fileName))
732 def run(self, args):
733 if len(args) == 0:
734 self.master = currentGitBranch()
735 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
736 die("Detecting current git branch failed!")
737 elif len(args) == 1:
738 self.master = args[0]
739 else:
740 return False
742 allowSubmit = gitConfig("git-p4.allowSubmit")
743 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
744 die("%s is not in git-p4.allowSubmit" % self.master)
746 [upstream, settings] = findUpstreamBranchPoint()
747 self.depotPath = settings['depot-paths'][0]
748 if len(self.origin) == 0:
749 self.origin = upstream
751 if self.verbose:
752 print "Origin branch is " + self.origin
754 if len(self.depotPath) == 0:
755 print "Internal error: cannot locate perforce depot path from existing branches"
756 sys.exit(128)
758 self.clientPath = p4Where(self.depotPath)
760 if len(self.clientPath) == 0:
761 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
762 sys.exit(128)
764 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
765 self.oldWorkingDirectory = os.getcwd()
767 os.chdir(self.clientPath)
768 print "Syncronizing p4 checkout..."
769 p4_system("sync ...")
771 self.check()
773 commits = []
774 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
775 commits.append(line.strip())
776 commits.reverse()
778 while len(commits) > 0:
779 commit = commits[0]
780 commits = commits[1:]
781 self.applyCommit(commit)
782 if not self.interactive:
783 break
785 if len(commits) == 0:
786 print "All changes applied!"
787 os.chdir(self.oldWorkingDirectory)
789 sync = P4Sync()
790 sync.run([])
792 rebase = P4Rebase()
793 rebase.rebase()
795 return True
797 class P4Sync(Command):
798 def __init__(self):
799 Command.__init__(self)
800 self.options = [
801 optparse.make_option("--branch", dest="branch"),
802 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
803 optparse.make_option("--changesfile", dest="changesFile"),
804 optparse.make_option("--silent", dest="silent", action="store_true"),
805 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
806 optparse.make_option("--verbose", dest="verbose", action="store_true"),
807 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
808 help="Import into refs/heads/ , not refs/remotes"),
809 optparse.make_option("--max-changes", dest="maxChanges"),
810 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
811 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
812 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
813 help="Only sync files that are included in the Perforce Client Spec")
815 self.description = """Imports from Perforce into a git repository.\n
816 example:
817 //depot/my/project/ -- to import the current head
818 //depot/my/project/@all -- to import everything
819 //depot/my/project/@1,6 -- to import only from revision 1 to 6
821 (a ... is not needed in the path p4 specification, it's added implicitly)"""
823 self.usage += " //depot/path[@revRange]"
824 self.silent = False
825 self.createdBranches = Set()
826 self.committedChanges = Set()
827 self.branch = ""
828 self.detectBranches = False
829 self.detectLabels = False
830 self.changesFile = ""
831 self.syncWithOrigin = True
832 self.verbose = False
833 self.importIntoRemotes = True
834 self.maxChanges = ""
835 self.isWindows = (platform.system() == "Windows")
836 self.keepRepoPath = False
837 self.depotPaths = None
838 self.p4BranchesInGit = []
839 self.cloneExclude = []
840 self.useClientSpec = False
841 self.clientSpecDirs = []
843 if gitConfig("git-p4.syncFromOrigin") == "false":
844 self.syncWithOrigin = False
846 def extractFilesFromCommit(self, commit):
847 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
848 for path in self.cloneExclude]
849 files = []
850 fnum = 0
851 while commit.has_key("depotFile%s" % fnum):
852 path = commit["depotFile%s" % fnum]
854 if [p for p in self.cloneExclude
855 if path.startswith (p)]:
856 found = False
857 else:
858 found = [p for p in self.depotPaths
859 if path.startswith (p)]
860 if not found:
861 fnum = fnum + 1
862 continue
864 file = {}
865 file["path"] = path
866 file["rev"] = commit["rev%s" % fnum]
867 file["action"] = commit["action%s" % fnum]
868 file["type"] = commit["type%s" % fnum]
869 files.append(file)
870 fnum = fnum + 1
871 return files
873 def stripRepoPath(self, path, prefixes):
874 if self.keepRepoPath:
875 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
877 for p in prefixes:
878 if path.startswith(p):
879 path = path[len(p):]
881 return path
883 def splitFilesIntoBranches(self, commit):
884 branches = {}
885 fnum = 0
886 while commit.has_key("depotFile%s" % fnum):
887 path = commit["depotFile%s" % fnum]
888 found = [p for p in self.depotPaths
889 if path.startswith (p)]
890 if not found:
891 fnum = fnum + 1
892 continue
894 file = {}
895 file["path"] = path
896 file["rev"] = commit["rev%s" % fnum]
897 file["action"] = commit["action%s" % fnum]
898 file["type"] = commit["type%s" % fnum]
899 fnum = fnum + 1
901 relPath = self.stripRepoPath(path, self.depotPaths)
903 for branch in self.knownBranches.keys():
905 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
906 if relPath.startswith(branch + "/"):
907 if branch not in branches:
908 branches[branch] = []
909 branches[branch].append(file)
910 break
912 return branches
914 ## Should move this out, doesn't use SELF.
915 def readP4Files(self, files):
916 filesForCommit = []
917 filesToRead = []
919 for f in files:
920 includeFile = True
921 for val in self.clientSpecDirs:
922 if f['path'].startswith(val[0]):
923 if val[1] <= 0:
924 includeFile = False
925 break
927 if includeFile:
928 filesForCommit.append(f)
929 if f['action'] != 'delete':
930 filesToRead.append(f)
932 filedata = []
933 if len(filesToRead) > 0:
934 filedata = p4CmdList('-x - print',
935 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
936 for f in filesToRead]),
937 stdin_mode='w+')
939 if "p4ExitCode" in filedata[0]:
940 die("Problems executing p4. Error: [%d]."
941 % (filedata[0]['p4ExitCode']));
943 j = 0;
944 contents = {}
945 while j < len(filedata):
946 stat = filedata[j]
947 j += 1
948 text = [];
949 while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
950 text.append(filedata[j]['data'])
951 j += 1
952 text = ''.join(text)
954 if not stat.has_key('depotFile'):
955 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
956 continue
958 if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
959 text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
960 elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
961 text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', text)
963 contents[stat['depotFile']] = text
965 for f in filesForCommit:
966 path = f['path']
967 if contents.has_key(path):
968 f['data'] = contents[path]
970 return filesForCommit
972 def commit(self, details, files, branch, branchPrefixes, parent = ""):
973 epoch = details["time"]
974 author = details["user"]
976 if self.verbose:
977 print "commit into %s" % branch
979 # start with reading files; if that fails, we should not
980 # create a commit.
981 new_files = []
982 for f in files:
983 if [p for p in branchPrefixes if f['path'].startswith(p)]:
984 new_files.append (f)
985 else:
986 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
987 files = self.readP4Files(new_files)
989 self.gitStream.write("commit %s\n" % branch)
990 # gitStream.write("mark :%s\n" % details["change"])
991 self.committedChanges.add(int(details["change"]))
992 committer = ""
993 if author not in self.users:
994 self.getUserMapFromPerforceServer()
995 if author in self.users:
996 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
997 else:
998 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1000 self.gitStream.write("committer %s\n" % committer)
1002 self.gitStream.write("data <<EOT\n")
1003 self.gitStream.write(details["desc"])
1004 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1005 % (','.join (branchPrefixes), details["change"]))
1006 if len(details['options']) > 0:
1007 self.gitStream.write(": options = %s" % details['options'])
1008 self.gitStream.write("]\nEOT\n\n")
1010 if len(parent) > 0:
1011 if self.verbose:
1012 print "parent %s" % parent
1013 self.gitStream.write("from %s\n" % parent)
1015 for file in files:
1016 if file["type"] == "apple":
1017 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1018 continue
1020 relPath = self.stripRepoPath(file['path'], branchPrefixes)
1021 if file["action"] == "delete":
1022 self.gitStream.write("D %s\n" % relPath)
1023 else:
1024 data = file['data']
1026 mode = "644"
1027 if isP4Exec(file["type"]):
1028 mode = "755"
1029 elif file["type"] == "symlink":
1030 mode = "120000"
1031 # p4 print on a symlink contains "target\n", so strip it off
1032 data = data[:-1]
1034 if self.isWindows and file["type"].endswith("text"):
1035 data = data.replace("\r\n", "\n")
1037 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1038 self.gitStream.write("data %s\n" % len(data))
1039 self.gitStream.write(data)
1040 self.gitStream.write("\n")
1042 self.gitStream.write("\n")
1044 change = int(details["change"])
1046 if self.labels.has_key(change):
1047 label = self.labels[change]
1048 labelDetails = label[0]
1049 labelRevisions = label[1]
1050 if self.verbose:
1051 print "Change %s is labelled %s" % (change, labelDetails)
1053 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1054 for p in branchPrefixes]))
1056 if len(files) == len(labelRevisions):
1058 cleanedFiles = {}
1059 for info in files:
1060 if info["action"] == "delete":
1061 continue
1062 cleanedFiles[info["depotFile"]] = info["rev"]
1064 if cleanedFiles == labelRevisions:
1065 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1066 self.gitStream.write("from %s\n" % branch)
1068 owner = labelDetails["Owner"]
1069 tagger = ""
1070 if author in self.users:
1071 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1072 else:
1073 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1074 self.gitStream.write("tagger %s\n" % tagger)
1075 self.gitStream.write("data <<EOT\n")
1076 self.gitStream.write(labelDetails["Description"])
1077 self.gitStream.write("EOT\n\n")
1079 else:
1080 if not self.silent:
1081 print ("Tag %s does not match with change %s: files do not match."
1082 % (labelDetails["label"], change))
1084 else:
1085 if not self.silent:
1086 print ("Tag %s does not match with change %s: file count is different."
1087 % (labelDetails["label"], change))
1089 def getUserCacheFilename(self):
1090 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1091 return home + "/.gitp4-usercache.txt"
1093 def getUserMapFromPerforceServer(self):
1094 if self.userMapFromPerforceServer:
1095 return
1096 self.users = {}
1098 for output in p4CmdList("users"):
1099 if not output.has_key("User"):
1100 continue
1101 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1104 s = ''
1105 for (key, val) in self.users.items():
1106 s += "%s\t%s\n" % (key, val)
1108 open(self.getUserCacheFilename(), "wb").write(s)
1109 self.userMapFromPerforceServer = True
1111 def loadUserMapFromCache(self):
1112 self.users = {}
1113 self.userMapFromPerforceServer = False
1114 try:
1115 cache = open(self.getUserCacheFilename(), "rb")
1116 lines = cache.readlines()
1117 cache.close()
1118 for line in lines:
1119 entry = line.strip().split("\t")
1120 self.users[entry[0]] = entry[1]
1121 except IOError:
1122 self.getUserMapFromPerforceServer()
1124 def getLabels(self):
1125 self.labels = {}
1127 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1128 if len(l) > 0 and not self.silent:
1129 print "Finding files belonging to labels in %s" % `self.depotPaths`
1131 for output in l:
1132 label = output["label"]
1133 revisions = {}
1134 newestChange = 0
1135 if self.verbose:
1136 print "Querying files for label %s" % label
1137 for file in p4CmdList("files "
1138 + ' '.join (["%s...@%s" % (p, label)
1139 for p in self.depotPaths])):
1140 revisions[file["depotFile"]] = file["rev"]
1141 change = int(file["change"])
1142 if change > newestChange:
1143 newestChange = change
1145 self.labels[newestChange] = [output, revisions]
1147 if self.verbose:
1148 print "Label changes: %s" % self.labels.keys()
1150 def guessProjectName(self):
1151 for p in self.depotPaths:
1152 if p.endswith("/"):
1153 p = p[:-1]
1154 p = p[p.strip().rfind("/") + 1:]
1155 if not p.endswith("/"):
1156 p += "/"
1157 return p
1159 def getBranchMapping(self):
1160 lostAndFoundBranches = set()
1162 for info in p4CmdList("branches"):
1163 details = p4Cmd("branch -o %s" % info["branch"])
1164 viewIdx = 0
1165 while details.has_key("View%s" % viewIdx):
1166 paths = details["View%s" % viewIdx].split(" ")
1167 viewIdx = viewIdx + 1
1168 # require standard //depot/foo/... //depot/bar/... mapping
1169 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1170 continue
1171 source = paths[0]
1172 destination = paths[1]
1173 ## HACK
1174 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1175 source = source[len(self.depotPaths[0]):-4]
1176 destination = destination[len(self.depotPaths[0]):-4]
1178 if destination in self.knownBranches:
1179 if not self.silent:
1180 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1181 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1182 continue
1184 self.knownBranches[destination] = source
1186 lostAndFoundBranches.discard(destination)
1188 if source not in self.knownBranches:
1189 lostAndFoundBranches.add(source)
1192 for branch in lostAndFoundBranches:
1193 self.knownBranches[branch] = branch
1195 def getBranchMappingFromGitBranches(self):
1196 branches = p4BranchesInGit(self.importIntoRemotes)
1197 for branch in branches.keys():
1198 if branch == "master":
1199 branch = "main"
1200 else:
1201 branch = branch[len(self.projectName):]
1202 self.knownBranches[branch] = branch
1204 def listExistingP4GitBranches(self):
1205 # branches holds mapping from name to commit
1206 branches = p4BranchesInGit(self.importIntoRemotes)
1207 self.p4BranchesInGit = branches.keys()
1208 for branch in branches.keys():
1209 self.initialParents[self.refPrefix + branch] = branches[branch]
1211 def updateOptionDict(self, d):
1212 option_keys = {}
1213 if self.keepRepoPath:
1214 option_keys['keepRepoPath'] = 1
1216 d["options"] = ' '.join(sorted(option_keys.keys()))
1218 def readOptions(self, d):
1219 self.keepRepoPath = (d.has_key('options')
1220 and ('keepRepoPath' in d['options']))
1222 def gitRefForBranch(self, branch):
1223 if branch == "main":
1224 return self.refPrefix + "master"
1226 if len(branch) <= 0:
1227 return branch
1229 return self.refPrefix + self.projectName + branch
1231 def gitCommitByP4Change(self, ref, change):
1232 if self.verbose:
1233 print "looking in ref " + ref + " for change %s using bisect..." % change
1235 earliestCommit = ""
1236 latestCommit = parseRevision(ref)
1238 while True:
1239 if self.verbose:
1240 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1241 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1242 if len(next) == 0:
1243 if self.verbose:
1244 print "argh"
1245 return ""
1246 log = extractLogMessageFromGitCommit(next)
1247 settings = extractSettingsGitLog(log)
1248 currentChange = int(settings['change'])
1249 if self.verbose:
1250 print "current change %s" % currentChange
1252 if currentChange == change:
1253 if self.verbose:
1254 print "found %s" % next
1255 return next
1257 if currentChange < change:
1258 earliestCommit = "^%s" % next
1259 else:
1260 latestCommit = "%s" % next
1262 return ""
1264 def importNewBranch(self, branch, maxChange):
1265 # make fast-import flush all changes to disk and update the refs using the checkpoint
1266 # command so that we can try to find the branch parent in the git history
1267 self.gitStream.write("checkpoint\n\n");
1268 self.gitStream.flush();
1269 branchPrefix = self.depotPaths[0] + branch + "/"
1270 range = "@1,%s" % maxChange
1271 #print "prefix" + branchPrefix
1272 changes = p4ChangesForPaths([branchPrefix], range)
1273 if len(changes) <= 0:
1274 return False
1275 firstChange = changes[0]
1276 #print "first change in branch: %s" % firstChange
1277 sourceBranch = self.knownBranches[branch]
1278 sourceDepotPath = self.depotPaths[0] + sourceBranch
1279 sourceRef = self.gitRefForBranch(sourceBranch)
1280 #print "source " + sourceBranch
1282 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1283 #print "branch parent: %s" % branchParentChange
1284 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1285 if len(gitParent) > 0:
1286 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1287 #print "parent git commit: %s" % gitParent
1289 self.importChanges(changes)
1290 return True
1292 def importChanges(self, changes):
1293 cnt = 1
1294 for change in changes:
1295 description = p4Cmd("describe %s" % change)
1296 self.updateOptionDict(description)
1298 if not self.silent:
1299 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1300 sys.stdout.flush()
1301 cnt = cnt + 1
1303 try:
1304 if self.detectBranches:
1305 branches = self.splitFilesIntoBranches(description)
1306 for branch in branches.keys():
1307 ## HACK --hwn
1308 branchPrefix = self.depotPaths[0] + branch + "/"
1310 parent = ""
1312 filesForCommit = branches[branch]
1314 if self.verbose:
1315 print "branch is %s" % branch
1317 self.updatedBranches.add(branch)
1319 if branch not in self.createdBranches:
1320 self.createdBranches.add(branch)
1321 parent = self.knownBranches[branch]
1322 if parent == branch:
1323 parent = ""
1324 else:
1325 fullBranch = self.projectName + branch
1326 if fullBranch not in self.p4BranchesInGit:
1327 if not self.silent:
1328 print("\n Importing new branch %s" % fullBranch);
1329 if self.importNewBranch(branch, change - 1):
1330 parent = ""
1331 self.p4BranchesInGit.append(fullBranch)
1332 if not self.silent:
1333 print("\n Resuming with change %s" % change);
1335 if self.verbose:
1336 print "parent determined through known branches: %s" % parent
1338 branch = self.gitRefForBranch(branch)
1339 parent = self.gitRefForBranch(parent)
1341 if self.verbose:
1342 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1344 if len(parent) == 0 and branch in self.initialParents:
1345 parent = self.initialParents[branch]
1346 del self.initialParents[branch]
1348 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1349 else:
1350 files = self.extractFilesFromCommit(description)
1351 self.commit(description, files, self.branch, self.depotPaths,
1352 self.initialParent)
1353 self.initialParent = ""
1354 except IOError:
1355 print self.gitError.read()
1356 sys.exit(1)
1358 def importHeadRevision(self, revision):
1359 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1361 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1362 details["desc"] = ("Initial import of %s from the state at revision %s"
1363 % (' '.join(self.depotPaths), revision))
1364 details["change"] = revision
1365 newestRevision = 0
1367 fileCnt = 0
1368 for info in p4CmdList("files "
1369 + ' '.join(["%s...%s"
1370 % (p, revision)
1371 for p in self.depotPaths])):
1373 if info['code'] == 'error':
1374 sys.stderr.write("p4 returned an error: %s\n"
1375 % info['data'])
1376 sys.exit(1)
1379 change = int(info["change"])
1380 if change > newestRevision:
1381 newestRevision = change
1383 if info["action"] == "delete":
1384 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1385 #fileCnt = fileCnt + 1
1386 continue
1388 for prop in ["depotFile", "rev", "action", "type" ]:
1389 details["%s%s" % (prop, fileCnt)] = info[prop]
1391 fileCnt = fileCnt + 1
1393 details["change"] = newestRevision
1394 self.updateOptionDict(details)
1395 try:
1396 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1397 except IOError:
1398 print "IO error with git fast-import. Is your git version recent enough?"
1399 print self.gitError.read()
1402 def getClientSpec(self):
1403 specList = p4CmdList( "client -o" )
1404 temp = {}
1405 for entry in specList:
1406 for k,v in entry.iteritems():
1407 if k.startswith("View"):
1408 if v.startswith('"'):
1409 start = 1
1410 else:
1411 start = 0
1412 index = v.find("...")
1413 v = v[start:index]
1414 if v.startswith("-"):
1415 v = v[1:]
1416 temp[v] = -len(v)
1417 else:
1418 temp[v] = len(v)
1419 self.clientSpecDirs = temp.items()
1420 self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1422 def run(self, args):
1423 self.depotPaths = []
1424 self.changeRange = ""
1425 self.initialParent = ""
1426 self.previousDepotPaths = []
1428 # map from branch depot path to parent branch
1429 self.knownBranches = {}
1430 self.initialParents = {}
1431 self.hasOrigin = originP4BranchesExist()
1432 if not self.syncWithOrigin:
1433 self.hasOrigin = False
1435 if self.importIntoRemotes:
1436 self.refPrefix = "refs/remotes/p4/"
1437 else:
1438 self.refPrefix = "refs/heads/p4/"
1440 if self.syncWithOrigin and self.hasOrigin:
1441 if not self.silent:
1442 print "Syncing with origin first by calling git fetch origin"
1443 system("git fetch origin")
1445 if len(self.branch) == 0:
1446 self.branch = self.refPrefix + "master"
1447 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1448 system("git update-ref %s refs/heads/p4" % self.branch)
1449 system("git branch -D p4");
1450 # create it /after/ importing, when master exists
1451 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1452 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1454 if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1455 self.getClientSpec()
1457 # TODO: should always look at previous commits,
1458 # merge with previous imports, if possible.
1459 if args == []:
1460 if self.hasOrigin:
1461 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1462 self.listExistingP4GitBranches()
1464 if len(self.p4BranchesInGit) > 1:
1465 if not self.silent:
1466 print "Importing from/into multiple branches"
1467 self.detectBranches = True
1469 if self.verbose:
1470 print "branches: %s" % self.p4BranchesInGit
1472 p4Change = 0
1473 for branch in self.p4BranchesInGit:
1474 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1476 settings = extractSettingsGitLog(logMsg)
1478 self.readOptions(settings)
1479 if (settings.has_key('depot-paths')
1480 and settings.has_key ('change')):
1481 change = int(settings['change']) + 1
1482 p4Change = max(p4Change, change)
1484 depotPaths = sorted(settings['depot-paths'])
1485 if self.previousDepotPaths == []:
1486 self.previousDepotPaths = depotPaths
1487 else:
1488 paths = []
1489 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1490 for i in range(0, min(len(cur), len(prev))):
1491 if cur[i] <> prev[i]:
1492 i = i - 1
1493 break
1495 paths.append (cur[:i + 1])
1497 self.previousDepotPaths = paths
1499 if p4Change > 0:
1500 self.depotPaths = sorted(self.previousDepotPaths)
1501 self.changeRange = "@%s,#head" % p4Change
1502 if not self.detectBranches:
1503 self.initialParent = parseRevision(self.branch)
1504 if not self.silent and not self.detectBranches:
1505 print "Performing incremental import into %s git branch" % self.branch
1507 if not self.branch.startswith("refs/"):
1508 self.branch = "refs/heads/" + self.branch
1510 if len(args) == 0 and self.depotPaths:
1511 if not self.silent:
1512 print "Depot paths: %s" % ' '.join(self.depotPaths)
1513 else:
1514 if self.depotPaths and self.depotPaths != args:
1515 print ("previous import used depot path %s and now %s was specified. "
1516 "This doesn't work!" % (' '.join (self.depotPaths),
1517 ' '.join (args)))
1518 sys.exit(1)
1520 self.depotPaths = sorted(args)
1522 revision = ""
1523 self.users = {}
1525 newPaths = []
1526 for p in self.depotPaths:
1527 if p.find("@") != -1:
1528 atIdx = p.index("@")
1529 self.changeRange = p[atIdx:]
1530 if self.changeRange == "@all":
1531 self.changeRange = ""
1532 elif ',' not in self.changeRange:
1533 revision = self.changeRange
1534 self.changeRange = ""
1535 p = p[:atIdx]
1536 elif p.find("#") != -1:
1537 hashIdx = p.index("#")
1538 revision = p[hashIdx:]
1539 p = p[:hashIdx]
1540 elif self.previousDepotPaths == []:
1541 revision = "#head"
1543 p = re.sub ("\.\.\.$", "", p)
1544 if not p.endswith("/"):
1545 p += "/"
1547 newPaths.append(p)
1549 self.depotPaths = newPaths
1552 self.loadUserMapFromCache()
1553 self.labels = {}
1554 if self.detectLabels:
1555 self.getLabels();
1557 if self.detectBranches:
1558 ## FIXME - what's a P4 projectName ?
1559 self.projectName = self.guessProjectName()
1561 if self.hasOrigin:
1562 self.getBranchMappingFromGitBranches()
1563 else:
1564 self.getBranchMapping()
1565 if self.verbose:
1566 print "p4-git branches: %s" % self.p4BranchesInGit
1567 print "initial parents: %s" % self.initialParents
1568 for b in self.p4BranchesInGit:
1569 if b != "master":
1571 ## FIXME
1572 b = b[len(self.projectName):]
1573 self.createdBranches.add(b)
1575 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1577 importProcess = subprocess.Popen(["git", "fast-import"],
1578 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1579 stderr=subprocess.PIPE);
1580 self.gitOutput = importProcess.stdout
1581 self.gitStream = importProcess.stdin
1582 self.gitError = importProcess.stderr
1584 if revision:
1585 self.importHeadRevision(revision)
1586 else:
1587 changes = []
1589 if len(self.changesFile) > 0:
1590 output = open(self.changesFile).readlines()
1591 changeSet = Set()
1592 for line in output:
1593 changeSet.add(int(line))
1595 for change in changeSet:
1596 changes.append(change)
1598 changes.sort()
1599 else:
1600 if self.verbose:
1601 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1602 self.changeRange)
1603 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1605 if len(self.maxChanges) > 0:
1606 changes = changes[:min(int(self.maxChanges), len(changes))]
1608 if len(changes) == 0:
1609 if not self.silent:
1610 print "No changes to import!"
1611 return True
1613 if not self.silent and not self.detectBranches:
1614 print "Import destination: %s" % self.branch
1616 self.updatedBranches = set()
1618 self.importChanges(changes)
1620 if not self.silent:
1621 print ""
1622 if len(self.updatedBranches) > 0:
1623 sys.stdout.write("Updated branches: ")
1624 for b in self.updatedBranches:
1625 sys.stdout.write("%s " % b)
1626 sys.stdout.write("\n")
1628 self.gitStream.close()
1629 if importProcess.wait() != 0:
1630 die("fast-import failed: %s" % self.gitError.read())
1631 self.gitOutput.close()
1632 self.gitError.close()
1634 return True
1636 class P4Rebase(Command):
1637 def __init__(self):
1638 Command.__init__(self)
1639 self.options = [ ]
1640 self.description = ("Fetches the latest revision from perforce and "
1641 + "rebases the current work (branch) against it")
1642 self.verbose = False
1644 def run(self, args):
1645 sync = P4Sync()
1646 sync.run([])
1648 return self.rebase()
1650 def rebase(self):
1651 if os.system("git update-index --refresh") != 0:
1652 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.");
1653 if len(read_pipe("git diff-index HEAD --")) > 0:
1654 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1656 [upstream, settings] = findUpstreamBranchPoint()
1657 if len(upstream) == 0:
1658 die("Cannot find upstream branchpoint for rebase")
1660 # the branchpoint may be p4/foo~3, so strip off the parent
1661 upstream = re.sub("~[0-9]+$", "", upstream)
1663 print "Rebasing the current branch onto %s" % upstream
1664 oldHead = read_pipe("git rev-parse HEAD").strip()
1665 system("git rebase %s" % upstream)
1666 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1667 return True
1669 class P4Clone(P4Sync):
1670 def __init__(self):
1671 P4Sync.__init__(self)
1672 self.description = "Creates a new git repository and imports from Perforce into it"
1673 self.usage = "usage: %prog [options] //depot/path[@revRange]"
1674 self.options += [
1675 optparse.make_option("--destination", dest="cloneDestination",
1676 action='store', default=None,
1677 help="where to leave result of the clone"),
1678 optparse.make_option("-/", dest="cloneExclude",
1679 action="append", type="string",
1680 help="exclude depot path")
1682 self.cloneDestination = None
1683 self.needsGit = False
1685 # This is required for the "append" cloneExclude action
1686 def ensure_value(self, attr, value):
1687 if not hasattr(self, attr) or getattr(self, attr) is None:
1688 setattr(self, attr, value)
1689 return getattr(self, attr)
1691 def defaultDestination(self, args):
1692 ## TODO: use common prefix of args?
1693 depotPath = args[0]
1694 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1695 depotDir = re.sub("(#[^#]*)$", "", depotDir)
1696 depotDir = re.sub(r"\.\.\.$", "", depotDir)
1697 depotDir = re.sub(r"/$", "", depotDir)
1698 return os.path.split(depotDir)[1]
1700 def run(self, args):
1701 if len(args) < 1:
1702 return False
1704 if self.keepRepoPath and not self.cloneDestination:
1705 sys.stderr.write("Must specify destination for --keep-path\n")
1706 sys.exit(1)
1708 depotPaths = args
1710 if not self.cloneDestination and len(depotPaths) > 1:
1711 self.cloneDestination = depotPaths[-1]
1712 depotPaths = depotPaths[:-1]
1714 self.cloneExclude = ["/"+p for p in self.cloneExclude]
1715 for p in depotPaths:
1716 if not p.startswith("//"):
1717 return False
1719 if not self.cloneDestination:
1720 self.cloneDestination = self.defaultDestination(args)
1722 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1723 if not os.path.exists(self.cloneDestination):
1724 os.makedirs(self.cloneDestination)
1725 os.chdir(self.cloneDestination)
1726 system("git init")
1727 self.gitdir = os.getcwd() + "/.git"
1728 if not P4Sync.run(self, depotPaths):
1729 return False
1730 if self.branch != "master":
1731 if gitBranchExists("refs/remotes/p4/master"):
1732 system("git branch master refs/remotes/p4/master")
1733 system("git checkout -f")
1734 else:
1735 print "Could not detect main branch. No checkout/master branch created."
1737 return True
1739 class P4Branches(Command):
1740 def __init__(self):
1741 Command.__init__(self)
1742 self.options = [ ]
1743 self.description = ("Shows the git branches that hold imports and their "
1744 + "corresponding perforce depot paths")
1745 self.verbose = False
1747 def run(self, args):
1748 if originP4BranchesExist():
1749 createOrUpdateBranchesFromOrigin()
1751 cmdline = "git rev-parse --symbolic "
1752 cmdline += " --remotes"
1754 for line in read_pipe_lines(cmdline):
1755 line = line.strip()
1757 if not line.startswith('p4/') or line == "p4/HEAD":
1758 continue
1759 branch = line
1761 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1762 settings = extractSettingsGitLog(log)
1764 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1765 return True
1767 class HelpFormatter(optparse.IndentedHelpFormatter):
1768 def __init__(self):
1769 optparse.IndentedHelpFormatter.__init__(self)
1771 def format_description(self, description):
1772 if description:
1773 return description + "\n"
1774 else:
1775 return ""
1777 def printUsage(commands):
1778 print "usage: %s <command> [options]" % sys.argv[0]
1779 print ""
1780 print "valid commands: %s" % ", ".join(commands)
1781 print ""
1782 print "Try %s <command> --help for command specific help." % sys.argv[0]
1783 print ""
1785 commands = {
1786 "debug" : P4Debug,
1787 "submit" : P4Submit,
1788 "commit" : P4Submit,
1789 "sync" : P4Sync,
1790 "rebase" : P4Rebase,
1791 "clone" : P4Clone,
1792 "rollback" : P4RollBack,
1793 "branches" : P4Branches
1797 def main():
1798 if len(sys.argv[1:]) == 0:
1799 printUsage(commands.keys())
1800 sys.exit(2)
1802 cmd = ""
1803 cmdName = sys.argv[1]
1804 try:
1805 klass = commands[cmdName]
1806 cmd = klass()
1807 except KeyError:
1808 print "unknown command %s" % cmdName
1809 print ""
1810 printUsage(commands.keys())
1811 sys.exit(2)
1813 options = cmd.options
1814 cmd.gitdir = os.environ.get("GIT_DIR", None)
1816 args = sys.argv[2:]
1818 if len(options) > 0:
1819 options.append(optparse.make_option("--git-dir", dest="gitdir"))
1821 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1822 options,
1823 description = cmd.description,
1824 formatter = HelpFormatter())
1826 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1827 global verbose
1828 verbose = cmd.verbose
1829 if cmd.needsGit:
1830 if cmd.gitdir == None:
1831 cmd.gitdir = os.path.abspath(".git")
1832 if not isValidGitDir(cmd.gitdir):
1833 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1834 if os.path.exists(cmd.gitdir):
1835 cdup = read_pipe("git rev-parse --show-cdup").strip()
1836 if len(cdup) > 0:
1837 os.chdir(cdup);
1839 if not isValidGitDir(cmd.gitdir):
1840 if isValidGitDir(cmd.gitdir + "/.git"):
1841 cmd.gitdir += "/.git"
1842 else:
1843 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1845 os.environ["GIT_DIR"] = cmd.gitdir
1847 if not cmd.run(args):
1848 parser.print_help()
1851 if __name__ == '__main__':
1852 main()