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