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