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