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