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 if os.environ.has_key("P4EDITOR"):
656 editor = os.environ.get("P4EDITOR")
657 else:
658 editor = os.environ.get("EDITOR", defaultEditor);
659 system(editor + " " + fileName)
660 tmpFile = open(fileName, "rb")
661 message = tmpFile.read()
662 tmpFile.close()
663 os.remove(fileName)
664 submitTemplate = message[:message.index(separatorLine)]
665 if self.isWindows:
666 submitTemplate = submitTemplate.replace("\r\n", "\n")
667
668 write_pipe("p4 submit -i", submitTemplate)
669 else:
670 fileName = "submit.txt"
671 file = open(fileName, "w+")
672 file.write(self.prepareLogMessage(template, logMessage))
673 file.close()
674 print ("Perforce submit template written as %s. "
675 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
676 % (fileName, fileName))
677
678 def run(self, args):
679 if len(args) == 0:
680 self.master = currentGitBranch()
681 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
682 die("Detecting current git branch failed!")
683 elif len(args) == 1:
684 self.master = args[0]
685 else:
686 return False
687
688 [upstream, settings] = findUpstreamBranchPoint()
689 self.depotPath = settings['depot-paths'][0]
690 if len(self.origin) == 0:
691 self.origin = upstream
692
693 if self.verbose:
694 print "Origin branch is " + self.origin
695
696 if len(self.depotPath) == 0:
697 print "Internal error: cannot locate perforce depot path from existing branches"
698 sys.exit(128)
699
700 self.clientPath = p4Where(self.depotPath)
701
702 if len(self.clientPath) == 0:
703 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
704 sys.exit(128)
705
706 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
707 self.oldWorkingDirectory = os.getcwd()
708
709 os.chdir(self.clientPath)
710 print "Syncronizing p4 checkout..."
711 system("p4 sync ...")
712
713 self.check()
714
715 commits = []
716 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
717 commits.append(line.strip())
718 commits.reverse()
719
720 while len(commits) > 0:
721 commit = commits[0]
722 commits = commits[1:]
723 self.applyCommit(commit)
724 if not self.interactive:
725 break
726
727 if len(commits) == 0:
728 print "All changes applied!"
729 os.chdir(self.oldWorkingDirectory)
730
731 sync = P4Sync()
732 sync.run([])
733
734 rebase = P4Rebase()
735 rebase.rebase()
736
737 return True
738
739class P4Sync(Command):
740 def __init__(self):
741 Command.__init__(self)
742 self.options = [
743 optparse.make_option("--branch", dest="branch"),
744 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
745 optparse.make_option("--changesfile", dest="changesFile"),
746 optparse.make_option("--silent", dest="silent", action="store_true"),
747 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
748 optparse.make_option("--verbose", dest="verbose", action="store_true"),
749 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
750 help="Import into refs/heads/ , not refs/remotes"),
751 optparse.make_option("--max-changes", dest="maxChanges"),
752 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
753 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
754 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
755 help="Only sync files that are included in the Perforce Client Spec")
756 ]
757 self.description = """Imports from Perforce into a git repository.\n
758 example:
759 //depot/my/project/ -- to import the current head
760 //depot/my/project/@all -- to import everything
761 //depot/my/project/@1,6 -- to import only from revision 1 to 6
762
763 (a ... is not needed in the path p4 specification, it's added implicitly)"""
764
765 self.usage += " //depot/path[@revRange]"
766 self.silent = False
767 self.createdBranches = Set()
768 self.committedChanges = Set()
769 self.branch = ""
770 self.detectBranches = False
771 self.detectLabels = False
772 self.changesFile = ""
773 self.syncWithOrigin = True
774 self.verbose = False
775 self.importIntoRemotes = True
776 self.maxChanges = ""
777 self.isWindows = (platform.system() == "Windows")
778 self.keepRepoPath = False
779 self.depotPaths = None
780 self.p4BranchesInGit = []
781 self.cloneExclude = []
782 self.useClientSpec = False
783 self.clientSpecDirs = []
784
785 if gitConfig("git-p4.syncFromOrigin") == "false":
786 self.syncWithOrigin = False
787
788 def extractFilesFromCommit(self, commit):
789 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
790 for path in self.cloneExclude]
791 files = []
792 fnum = 0
793 while commit.has_key("depotFile%s" % fnum):
794 path = commit["depotFile%s" % fnum]
795
796 if [p for p in self.cloneExclude
797 if path.startswith (p)]:
798 found = False
799 else:
800 found = [p for p in self.depotPaths
801 if path.startswith (p)]
802 if not found:
803 fnum = fnum + 1
804 continue
805
806 file = {}
807 file["path"] = path
808 file["rev"] = commit["rev%s" % fnum]
809 file["action"] = commit["action%s" % fnum]
810 file["type"] = commit["type%s" % fnum]
811 files.append(file)
812 fnum = fnum + 1
813 return files
814
815 def stripRepoPath(self, path, prefixes):
816 if self.keepRepoPath:
817 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
818
819 for p in prefixes:
820 if path.startswith(p):
821 path = path[len(p):]
822
823 return path
824
825 def splitFilesIntoBranches(self, commit):
826 branches = {}
827 fnum = 0
828 while commit.has_key("depotFile%s" % fnum):
829 path = commit["depotFile%s" % fnum]
830 found = [p for p in self.depotPaths
831 if path.startswith (p)]
832 if not found:
833 fnum = fnum + 1
834 continue
835
836 file = {}
837 file["path"] = path
838 file["rev"] = commit["rev%s" % fnum]
839 file["action"] = commit["action%s" % fnum]
840 file["type"] = commit["type%s" % fnum]
841 fnum = fnum + 1
842
843 relPath = self.stripRepoPath(path, self.depotPaths)
844
845 for branch in self.knownBranches.keys():
846
847 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
848 if relPath.startswith(branch + "/"):
849 if branch not in branches:
850 branches[branch] = []
851 branches[branch].append(file)
852 break
853
854 return branches
855
856 ## Should move this out, doesn't use SELF.
857 def readP4Files(self, files):
858 filesForCommit = []
859 filesToRead = []
860
861 for f in files:
862 includeFile = True
863 for val in self.clientSpecDirs:
864 if f['path'].startswith(val[0]):
865 if val[1] <= 0:
866 includeFile = False
867 break
868
869 if includeFile:
870 filesForCommit.append(f)
871 if f['action'] != 'delete':
872 filesToRead.append(f)
873
874 filedata = []
875 if len(filesToRead) > 0:
876 filedata = p4CmdList('-x - print',
877 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
878 for f in filesToRead]),
879 stdin_mode='w+')
880
881 if "p4ExitCode" in filedata[0]:
882 die("Problems executing p4. Error: [%d]."
883 % (filedata[0]['p4ExitCode']));
884
885 j = 0;
886 contents = {}
887 while j < len(filedata):
888 stat = filedata[j]
889 j += 1
890 text = [];
891 while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
892 text.append(filedata[j]['data'])
893 j += 1
894 text = ''.join(text)
895
896 if not stat.has_key('depotFile'):
897 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
898 continue
899
900 if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
901 text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
902 elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
903 text = re.sub(r'(?i)\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', text)
904
905 contents[stat['depotFile']] = text
906
907 for f in filesForCommit:
908 path = f['path']
909 if contents.has_key(path):
910 f['data'] = contents[path]
911
912 return filesForCommit
913
914 def commit(self, details, files, branch, branchPrefixes, parent = ""):
915 epoch = details["time"]
916 author = details["user"]
917
918 if self.verbose:
919 print "commit into %s" % branch
920
921 # start with reading files; if that fails, we should not
922 # create a commit.
923 new_files = []
924 for f in files:
925 if [p for p in branchPrefixes if f['path'].startswith(p)]:
926 new_files.append (f)
927 else:
928 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
929 files = self.readP4Files(new_files)
930
931 self.gitStream.write("commit %s\n" % branch)
932# gitStream.write("mark :%s\n" % details["change"])
933 self.committedChanges.add(int(details["change"]))
934 committer = ""
935 if author not in self.users:
936 self.getUserMapFromPerforceServer()
937 if author in self.users:
938 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
939 else:
940 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
941
942 self.gitStream.write("committer %s\n" % committer)
943
944 self.gitStream.write("data <<EOT\n")
945 self.gitStream.write(details["desc"])
946 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
947 % (','.join (branchPrefixes), details["change"]))
948 if len(details['options']) > 0:
949 self.gitStream.write(": options = %s" % details['options'])
950 self.gitStream.write("]\nEOT\n\n")
951
952 if len(parent) > 0:
953 if self.verbose:
954 print "parent %s" % parent
955 self.gitStream.write("from %s\n" % parent)
956
957 for file in files:
958 if file["type"] == "apple":
959 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
960 continue
961
962 relPath = self.stripRepoPath(file['path'], branchPrefixes)
963 if file["action"] == "delete":
964 self.gitStream.write("D %s\n" % relPath)
965 else:
966 data = file['data']
967
968 mode = "644"
969 if isP4Exec(file["type"]):
970 mode = "755"
971 elif file["type"] == "symlink":
972 mode = "120000"
973 # p4 print on a symlink contains "target\n", so strip it off
974 data = data[:-1]
975
976 if self.isWindows and file["type"].endswith("text"):
977 data = data.replace("\r\n", "\n")
978
979 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
980 self.gitStream.write("data %s\n" % len(data))
981 self.gitStream.write(data)
982 self.gitStream.write("\n")
983
984 self.gitStream.write("\n")
985
986 change = int(details["change"])
987
988 if self.labels.has_key(change):
989 label = self.labels[change]
990 labelDetails = label[0]
991 labelRevisions = label[1]
992 if self.verbose:
993 print "Change %s is labelled %s" % (change, labelDetails)
994
995 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
996 for p in branchPrefixes]))
997
998 if len(files) == len(labelRevisions):
999
1000 cleanedFiles = {}
1001 for info in files:
1002 if info["action"] == "delete":
1003 continue
1004 cleanedFiles[info["depotFile"]] = info["rev"]
1005
1006 if cleanedFiles == labelRevisions:
1007 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1008 self.gitStream.write("from %s\n" % branch)
1009
1010 owner = labelDetails["Owner"]
1011 tagger = ""
1012 if author in self.users:
1013 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1014 else:
1015 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1016 self.gitStream.write("tagger %s\n" % tagger)
1017 self.gitStream.write("data <<EOT\n")
1018 self.gitStream.write(labelDetails["Description"])
1019 self.gitStream.write("EOT\n\n")
1020
1021 else:
1022 if not self.silent:
1023 print ("Tag %s does not match with change %s: files do not match."
1024 % (labelDetails["label"], change))
1025
1026 else:
1027 if not self.silent:
1028 print ("Tag %s does not match with change %s: file count is different."
1029 % (labelDetails["label"], change))
1030
1031 def getUserCacheFilename(self):
1032 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1033 return home + "/.gitp4-usercache.txt"
1034
1035 def getUserMapFromPerforceServer(self):
1036 if self.userMapFromPerforceServer:
1037 return
1038 self.users = {}
1039
1040 for output in p4CmdList("users"):
1041 if not output.has_key("User"):
1042 continue
1043 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1044
1045
1046 s = ''
1047 for (key, val) in self.users.items():
1048 s += "%s\t%s\n" % (key, val)
1049
1050 open(self.getUserCacheFilename(), "wb").write(s)
1051 self.userMapFromPerforceServer = True
1052
1053 def loadUserMapFromCache(self):
1054 self.users = {}
1055 self.userMapFromPerforceServer = False
1056 try:
1057 cache = open(self.getUserCacheFilename(), "rb")
1058 lines = cache.readlines()
1059 cache.close()
1060 for line in lines:
1061 entry = line.strip().split("\t")
1062 self.users[entry[0]] = entry[1]
1063 except IOError:
1064 self.getUserMapFromPerforceServer()
1065
1066 def getLabels(self):
1067 self.labels = {}
1068
1069 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1070 if len(l) > 0 and not self.silent:
1071 print "Finding files belonging to labels in %s" % `self.depotPaths`
1072
1073 for output in l:
1074 label = output["label"]
1075 revisions = {}
1076 newestChange = 0
1077 if self.verbose:
1078 print "Querying files for label %s" % label
1079 for file in p4CmdList("files "
1080 + ' '.join (["%s...@%s" % (p, label)
1081 for p in self.depotPaths])):
1082 revisions[file["depotFile"]] = file["rev"]
1083 change = int(file["change"])
1084 if change > newestChange:
1085 newestChange = change
1086
1087 self.labels[newestChange] = [output, revisions]
1088
1089 if self.verbose:
1090 print "Label changes: %s" % self.labels.keys()
1091
1092 def guessProjectName(self):
1093 for p in self.depotPaths:
1094 if p.endswith("/"):
1095 p = p[:-1]
1096 p = p[p.strip().rfind("/") + 1:]
1097 if not p.endswith("/"):
1098 p += "/"
1099 return p
1100
1101 def getBranchMapping(self):
1102 lostAndFoundBranches = set()
1103
1104 for info in p4CmdList("branches"):
1105 details = p4Cmd("branch -o %s" % info["branch"])
1106 viewIdx = 0
1107 while details.has_key("View%s" % viewIdx):
1108 paths = details["View%s" % viewIdx].split(" ")
1109 viewIdx = viewIdx + 1
1110 # require standard //depot/foo/... //depot/bar/... mapping
1111 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1112 continue
1113 source = paths[0]
1114 destination = paths[1]
1115 ## HACK
1116 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1117 source = source[len(self.depotPaths[0]):-4]
1118 destination = destination[len(self.depotPaths[0]):-4]
1119
1120 if destination in self.knownBranches:
1121 if not self.silent:
1122 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1123 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1124 continue
1125
1126 self.knownBranches[destination] = source
1127
1128 lostAndFoundBranches.discard(destination)
1129
1130 if source not in self.knownBranches:
1131 lostAndFoundBranches.add(source)
1132
1133
1134 for branch in lostAndFoundBranches:
1135 self.knownBranches[branch] = branch
1136
1137 def getBranchMappingFromGitBranches(self):
1138 branches = p4BranchesInGit(self.importIntoRemotes)
1139 for branch in branches.keys():
1140 if branch == "master":
1141 branch = "main"
1142 else:
1143 branch = branch[len(self.projectName):]
1144 self.knownBranches[branch] = branch
1145
1146 def listExistingP4GitBranches(self):
1147 # branches holds mapping from name to commit
1148 branches = p4BranchesInGit(self.importIntoRemotes)
1149 self.p4BranchesInGit = branches.keys()
1150 for branch in branches.keys():
1151 self.initialParents[self.refPrefix + branch] = branches[branch]
1152
1153 def updateOptionDict(self, d):
1154 option_keys = {}
1155 if self.keepRepoPath:
1156 option_keys['keepRepoPath'] = 1
1157
1158 d["options"] = ' '.join(sorted(option_keys.keys()))
1159
1160 def readOptions(self, d):
1161 self.keepRepoPath = (d.has_key('options')
1162 and ('keepRepoPath' in d['options']))
1163
1164 def gitRefForBranch(self, branch):
1165 if branch == "main":
1166 return self.refPrefix + "master"
1167
1168 if len(branch) <= 0:
1169 return branch
1170
1171 return self.refPrefix + self.projectName + branch
1172
1173 def gitCommitByP4Change(self, ref, change):
1174 if self.verbose:
1175 print "looking in ref " + ref + " for change %s using bisect..." % change
1176
1177 earliestCommit = ""
1178 latestCommit = parseRevision(ref)
1179
1180 while True:
1181 if self.verbose:
1182 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1183 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1184 if len(next) == 0:
1185 if self.verbose:
1186 print "argh"
1187 return ""
1188 log = extractLogMessageFromGitCommit(next)
1189 settings = extractSettingsGitLog(log)
1190 currentChange = int(settings['change'])
1191 if self.verbose:
1192 print "current change %s" % currentChange
1193
1194 if currentChange == change:
1195 if self.verbose:
1196 print "found %s" % next
1197 return next
1198
1199 if currentChange < change:
1200 earliestCommit = "^%s" % next
1201 else:
1202 latestCommit = "%s" % next
1203
1204 return ""
1205
1206 def importNewBranch(self, branch, maxChange):
1207 # make fast-import flush all changes to disk and update the refs using the checkpoint
1208 # command so that we can try to find the branch parent in the git history
1209 self.gitStream.write("checkpoint\n\n");
1210 self.gitStream.flush();
1211 branchPrefix = self.depotPaths[0] + branch + "/"
1212 range = "@1,%s" % maxChange
1213 #print "prefix" + branchPrefix
1214 changes = p4ChangesForPaths([branchPrefix], range)
1215 if len(changes) <= 0:
1216 return False
1217 firstChange = changes[0]
1218 #print "first change in branch: %s" % firstChange
1219 sourceBranch = self.knownBranches[branch]
1220 sourceDepotPath = self.depotPaths[0] + sourceBranch
1221 sourceRef = self.gitRefForBranch(sourceBranch)
1222 #print "source " + sourceBranch
1223
1224 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1225 #print "branch parent: %s" % branchParentChange
1226 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1227 if len(gitParent) > 0:
1228 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1229 #print "parent git commit: %s" % gitParent
1230
1231 self.importChanges(changes)
1232 return True
1233
1234 def importChanges(self, changes):
1235 cnt = 1
1236 for change in changes:
1237 description = p4Cmd("describe %s" % change)
1238 self.updateOptionDict(description)
1239
1240 if not self.silent:
1241 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1242 sys.stdout.flush()
1243 cnt = cnt + 1
1244
1245 try:
1246 if self.detectBranches:
1247 branches = self.splitFilesIntoBranches(description)
1248 for branch in branches.keys():
1249 ## HACK --hwn
1250 branchPrefix = self.depotPaths[0] + branch + "/"
1251
1252 parent = ""
1253
1254 filesForCommit = branches[branch]
1255
1256 if self.verbose:
1257 print "branch is %s" % branch
1258
1259 self.updatedBranches.add(branch)
1260
1261 if branch not in self.createdBranches:
1262 self.createdBranches.add(branch)
1263 parent = self.knownBranches[branch]
1264 if parent == branch:
1265 parent = ""
1266 else:
1267 fullBranch = self.projectName + branch
1268 if fullBranch not in self.p4BranchesInGit:
1269 if not self.silent:
1270 print("\n Importing new branch %s" % fullBranch);
1271 if self.importNewBranch(branch, change - 1):
1272 parent = ""
1273 self.p4BranchesInGit.append(fullBranch)
1274 if not self.silent:
1275 print("\n Resuming with change %s" % change);
1276
1277 if self.verbose:
1278 print "parent determined through known branches: %s" % parent
1279
1280 branch = self.gitRefForBranch(branch)
1281 parent = self.gitRefForBranch(parent)
1282
1283 if self.verbose:
1284 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1285
1286 if len(parent) == 0 and branch in self.initialParents:
1287 parent = self.initialParents[branch]
1288 del self.initialParents[branch]
1289
1290 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1291 else:
1292 files = self.extractFilesFromCommit(description)
1293 self.commit(description, files, self.branch, self.depotPaths,
1294 self.initialParent)
1295 self.initialParent = ""
1296 except IOError:
1297 print self.gitError.read()
1298 sys.exit(1)
1299
1300 def importHeadRevision(self, revision):
1301 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1302
1303 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1304 details["desc"] = ("Initial import of %s from the state at revision %s"
1305 % (' '.join(self.depotPaths), revision))
1306 details["change"] = revision
1307 newestRevision = 0
1308
1309 fileCnt = 0
1310 for info in p4CmdList("files "
1311 + ' '.join(["%s...%s"
1312 % (p, revision)
1313 for p in self.depotPaths])):
1314
1315 if info['code'] == 'error':
1316 sys.stderr.write("p4 returned an error: %s\n"
1317 % info['data'])
1318 sys.exit(1)
1319
1320
1321 change = int(info["change"])
1322 if change > newestRevision:
1323 newestRevision = change
1324
1325 if info["action"] == "delete":
1326 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1327 #fileCnt = fileCnt + 1
1328 continue
1329
1330 for prop in ["depotFile", "rev", "action", "type" ]:
1331 details["%s%s" % (prop, fileCnt)] = info[prop]
1332
1333 fileCnt = fileCnt + 1
1334
1335 details["change"] = newestRevision
1336 self.updateOptionDict(details)
1337 try:
1338 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1339 except IOError:
1340 print "IO error with git fast-import. Is your git version recent enough?"
1341 print self.gitError.read()
1342
1343
1344 def getClientSpec(self):
1345 specList = p4CmdList( "client -o" )
1346 temp = {}
1347 for entry in specList:
1348 for k,v in entry.iteritems():
1349 if k.startswith("View"):
1350 if v.startswith('"'):
1351 start = 1
1352 else:
1353 start = 0
1354 index = v.find("...")
1355 v = v[start:index]
1356 if v.startswith("-"):
1357 v = v[1:]
1358 temp[v] = -len(v)
1359 else:
1360 temp[v] = len(v)
1361 self.clientSpecDirs = temp.items()
1362 self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1363
1364 def run(self, args):
1365 self.depotPaths = []
1366 self.changeRange = ""
1367 self.initialParent = ""
1368 self.previousDepotPaths = []
1369
1370 # map from branch depot path to parent branch
1371 self.knownBranches = {}
1372 self.initialParents = {}
1373 self.hasOrigin = originP4BranchesExist()
1374 if not self.syncWithOrigin:
1375 self.hasOrigin = False
1376
1377 if self.importIntoRemotes:
1378 self.refPrefix = "refs/remotes/p4/"
1379 else:
1380 self.refPrefix = "refs/heads/p4/"
1381
1382 if self.syncWithOrigin and self.hasOrigin:
1383 if not self.silent:
1384 print "Syncing with origin first by calling git fetch origin"
1385 system("git fetch origin")
1386
1387 if len(self.branch) == 0:
1388 self.branch = self.refPrefix + "master"
1389 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1390 system("git update-ref %s refs/heads/p4" % self.branch)
1391 system("git branch -D p4");
1392 # create it /after/ importing, when master exists
1393 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1394 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1395
1396 if self.useClientSpec or gitConfig("p4.useclientspec") == "true":
1397 self.getClientSpec()
1398
1399 # TODO: should always look at previous commits,
1400 # merge with previous imports, if possible.
1401 if args == []:
1402 if self.hasOrigin:
1403 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1404 self.listExistingP4GitBranches()
1405
1406 if len(self.p4BranchesInGit) > 1:
1407 if not self.silent:
1408 print "Importing from/into multiple branches"
1409 self.detectBranches = True
1410
1411 if self.verbose:
1412 print "branches: %s" % self.p4BranchesInGit
1413
1414 p4Change = 0
1415 for branch in self.p4BranchesInGit:
1416 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1417
1418 settings = extractSettingsGitLog(logMsg)
1419
1420 self.readOptions(settings)
1421 if (settings.has_key('depot-paths')
1422 and settings.has_key ('change')):
1423 change = int(settings['change']) + 1
1424 p4Change = max(p4Change, change)
1425
1426 depotPaths = sorted(settings['depot-paths'])
1427 if self.previousDepotPaths == []:
1428 self.previousDepotPaths = depotPaths
1429 else:
1430 paths = []
1431 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1432 for i in range(0, min(len(cur), len(prev))):
1433 if cur[i] <> prev[i]:
1434 i = i - 1
1435 break
1436
1437 paths.append (cur[:i + 1])
1438
1439 self.previousDepotPaths = paths
1440
1441 if p4Change > 0:
1442 self.depotPaths = sorted(self.previousDepotPaths)
1443 self.changeRange = "@%s,#head" % p4Change
1444 if not self.detectBranches:
1445 self.initialParent = parseRevision(self.branch)
1446 if not self.silent and not self.detectBranches:
1447 print "Performing incremental import into %s git branch" % self.branch
1448
1449 if not self.branch.startswith("refs/"):
1450 self.branch = "refs/heads/" + self.branch
1451
1452 if len(args) == 0 and self.depotPaths:
1453 if not self.silent:
1454 print "Depot paths: %s" % ' '.join(self.depotPaths)
1455 else:
1456 if self.depotPaths and self.depotPaths != args:
1457 print ("previous import used depot path %s and now %s was specified. "
1458 "This doesn't work!" % (' '.join (self.depotPaths),
1459 ' '.join (args)))
1460 sys.exit(1)
1461
1462 self.depotPaths = sorted(args)
1463
1464 revision = ""
1465 self.users = {}
1466
1467 newPaths = []
1468 for p in self.depotPaths:
1469 if p.find("@") != -1:
1470 atIdx = p.index("@")
1471 self.changeRange = p[atIdx:]
1472 if self.changeRange == "@all":
1473 self.changeRange = ""
1474 elif ',' not in self.changeRange:
1475 revision = self.changeRange
1476 self.changeRange = ""
1477 p = p[:atIdx]
1478 elif p.find("#") != -1:
1479 hashIdx = p.index("#")
1480 revision = p[hashIdx:]
1481 p = p[:hashIdx]
1482 elif self.previousDepotPaths == []:
1483 revision = "#head"
1484
1485 p = re.sub ("\.\.\.$", "", p)
1486 if not p.endswith("/"):
1487 p += "/"
1488
1489 newPaths.append(p)
1490
1491 self.depotPaths = newPaths
1492
1493
1494 self.loadUserMapFromCache()
1495 self.labels = {}
1496 if self.detectLabels:
1497 self.getLabels();
1498
1499 if self.detectBranches:
1500 ## FIXME - what's a P4 projectName ?
1501 self.projectName = self.guessProjectName()
1502
1503 if self.hasOrigin:
1504 self.getBranchMappingFromGitBranches()
1505 else:
1506 self.getBranchMapping()
1507 if self.verbose:
1508 print "p4-git branches: %s" % self.p4BranchesInGit
1509 print "initial parents: %s" % self.initialParents
1510 for b in self.p4BranchesInGit:
1511 if b != "master":
1512
1513 ## FIXME
1514 b = b[len(self.projectName):]
1515 self.createdBranches.add(b)
1516
1517 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1518
1519 importProcess = subprocess.Popen(["git", "fast-import"],
1520 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1521 stderr=subprocess.PIPE);
1522 self.gitOutput = importProcess.stdout
1523 self.gitStream = importProcess.stdin
1524 self.gitError = importProcess.stderr
1525
1526 if revision:
1527 self.importHeadRevision(revision)
1528 else:
1529 changes = []
1530
1531 if len(self.changesFile) > 0:
1532 output = open(self.changesFile).readlines()
1533 changeSet = Set()
1534 for line in output:
1535 changeSet.add(int(line))
1536
1537 for change in changeSet:
1538 changes.append(change)
1539
1540 changes.sort()
1541 else:
1542 if self.verbose:
1543 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1544 self.changeRange)
1545 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1546
1547 if len(self.maxChanges) > 0:
1548 changes = changes[:min(int(self.maxChanges), len(changes))]
1549
1550 if len(changes) == 0:
1551 if not self.silent:
1552 print "No changes to import!"
1553 return True
1554
1555 if not self.silent and not self.detectBranches:
1556 print "Import destination: %s" % self.branch
1557
1558 self.updatedBranches = set()
1559
1560 self.importChanges(changes)
1561
1562 if not self.silent:
1563 print ""
1564 if len(self.updatedBranches) > 0:
1565 sys.stdout.write("Updated branches: ")
1566 for b in self.updatedBranches:
1567 sys.stdout.write("%s " % b)
1568 sys.stdout.write("\n")
1569
1570 self.gitStream.close()
1571 if importProcess.wait() != 0:
1572 die("fast-import failed: %s" % self.gitError.read())
1573 self.gitOutput.close()
1574 self.gitError.close()
1575
1576 return True
1577
1578class P4Rebase(Command):
1579 def __init__(self):
1580 Command.__init__(self)
1581 self.options = [ ]
1582 self.description = ("Fetches the latest revision from perforce and "
1583 + "rebases the current work (branch) against it")
1584 self.verbose = False
1585
1586 def run(self, args):
1587 sync = P4Sync()
1588 sync.run([])
1589
1590 return self.rebase()
1591
1592 def rebase(self):
1593 if os.system("git update-index --refresh") != 0:
1594 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.");
1595 if len(read_pipe("git diff-index HEAD --")) > 0:
1596 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1597
1598 [upstream, settings] = findUpstreamBranchPoint()
1599 if len(upstream) == 0:
1600 die("Cannot find upstream branchpoint for rebase")
1601
1602 # the branchpoint may be p4/foo~3, so strip off the parent
1603 upstream = re.sub("~[0-9]+$", "", upstream)
1604
1605 print "Rebasing the current branch onto %s" % upstream
1606 oldHead = read_pipe("git rev-parse HEAD").strip()
1607 system("git rebase %s" % upstream)
1608 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1609 return True
1610
1611class P4Clone(P4Sync):
1612 def __init__(self):
1613 P4Sync.__init__(self)
1614 self.description = "Creates a new git repository and imports from Perforce into it"
1615 self.usage = "usage: %prog [options] //depot/path[@revRange]"
1616 self.options += [
1617 optparse.make_option("--destination", dest="cloneDestination",
1618 action='store', default=None,
1619 help="where to leave result of the clone"),
1620 optparse.make_option("-/", dest="cloneExclude",
1621 action="append", type="string",
1622 help="exclude depot path")
1623 ]
1624 self.cloneDestination = None
1625 self.needsGit = False
1626
1627 # This is required for the "append" cloneExclude action
1628 def ensure_value(self, attr, value):
1629 if not hasattr(self, attr) or getattr(self, attr) is None:
1630 setattr(self, attr, value)
1631 return getattr(self, attr)
1632
1633 def defaultDestination(self, args):
1634 ## TODO: use common prefix of args?
1635 depotPath = args[0]
1636 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1637 depotDir = re.sub("(#[^#]*)$", "", depotDir)
1638 depotDir = re.sub(r"\.\.\.$", "", depotDir)
1639 depotDir = re.sub(r"/$", "", depotDir)
1640 return os.path.split(depotDir)[1]
1641
1642 def run(self, args):
1643 if len(args) < 1:
1644 return False
1645
1646 if self.keepRepoPath and not self.cloneDestination:
1647 sys.stderr.write("Must specify destination for --keep-path\n")
1648 sys.exit(1)
1649
1650 depotPaths = args
1651
1652 if not self.cloneDestination and len(depotPaths) > 1:
1653 self.cloneDestination = depotPaths[-1]
1654 depotPaths = depotPaths[:-1]
1655
1656 self.cloneExclude = ["/"+p for p in self.cloneExclude]
1657 for p in depotPaths:
1658 if not p.startswith("//"):
1659 return False
1660
1661 if not self.cloneDestination:
1662 self.cloneDestination = self.defaultDestination(args)
1663
1664 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1665 if not os.path.exists(self.cloneDestination):
1666 os.makedirs(self.cloneDestination)
1667 os.chdir(self.cloneDestination)
1668 system("git init")
1669 self.gitdir = os.getcwd() + "/.git"
1670 if not P4Sync.run(self, depotPaths):
1671 return False
1672 if self.branch != "master":
1673 if gitBranchExists("refs/remotes/p4/master"):
1674 system("git branch master refs/remotes/p4/master")
1675 system("git checkout -f")
1676 else:
1677 print "Could not detect main branch. No checkout/master branch created."
1678
1679 return True
1680
1681class P4Branches(Command):
1682 def __init__(self):
1683 Command.__init__(self)
1684 self.options = [ ]
1685 self.description = ("Shows the git branches that hold imports and their "
1686 + "corresponding perforce depot paths")
1687 self.verbose = False
1688
1689 def run(self, args):
1690 if originP4BranchesExist():
1691 createOrUpdateBranchesFromOrigin()
1692
1693 cmdline = "git rev-parse --symbolic "
1694 cmdline += " --remotes"
1695
1696 for line in read_pipe_lines(cmdline):
1697 line = line.strip()
1698
1699 if not line.startswith('p4/') or line == "p4/HEAD":
1700 continue
1701 branch = line
1702
1703 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1704 settings = extractSettingsGitLog(log)
1705
1706 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1707 return True
1708
1709class HelpFormatter(optparse.IndentedHelpFormatter):
1710 def __init__(self):
1711 optparse.IndentedHelpFormatter.__init__(self)
1712
1713 def format_description(self, description):
1714 if description:
1715 return description + "\n"
1716 else:
1717 return ""
1718
1719def printUsage(commands):
1720 print "usage: %s <command> [options]" % sys.argv[0]
1721 print ""
1722 print "valid commands: %s" % ", ".join(commands)
1723 print ""
1724 print "Try %s <command> --help for command specific help." % sys.argv[0]
1725 print ""
1726
1727commands = {
1728 "debug" : P4Debug,
1729 "submit" : P4Submit,
1730 "commit" : P4Submit,
1731 "sync" : P4Sync,
1732 "rebase" : P4Rebase,
1733 "clone" : P4Clone,
1734 "rollback" : P4RollBack,
1735 "branches" : P4Branches
1736}
1737
1738
1739def main():
1740 if len(sys.argv[1:]) == 0:
1741 printUsage(commands.keys())
1742 sys.exit(2)
1743
1744 cmd = ""
1745 cmdName = sys.argv[1]
1746 try:
1747 klass = commands[cmdName]
1748 cmd = klass()
1749 except KeyError:
1750 print "unknown command %s" % cmdName
1751 print ""
1752 printUsage(commands.keys())
1753 sys.exit(2)
1754
1755 options = cmd.options
1756 cmd.gitdir = os.environ.get("GIT_DIR", None)
1757
1758 args = sys.argv[2:]
1759
1760 if len(options) > 0:
1761 options.append(optparse.make_option("--git-dir", dest="gitdir"))
1762
1763 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1764 options,
1765 description = cmd.description,
1766 formatter = HelpFormatter())
1767
1768 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1769 global verbose
1770 verbose = cmd.verbose
1771 if cmd.needsGit:
1772 if cmd.gitdir == None:
1773 cmd.gitdir = os.path.abspath(".git")
1774 if not isValidGitDir(cmd.gitdir):
1775 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1776 if os.path.exists(cmd.gitdir):
1777 cdup = read_pipe("git rev-parse --show-cdup").strip()
1778 if len(cdup) > 0:
1779 os.chdir(cdup);
1780
1781 if not isValidGitDir(cmd.gitdir):
1782 if isValidGitDir(cmd.gitdir + "/.git"):
1783 cmd.gitdir += "/.git"
1784 else:
1785 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1786
1787 os.environ["GIT_DIR"] = cmd.gitdir
1788
1789 if not cmd.run(args):
1790 parser.print_help()
1791
1792
1793if __name__ == '__main__':
1794 main()