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, args = None): # set args to "--bool", for instance
337 if not _gitConfig.has_key(key):
338 argsFilter = ""
339 if args != None:
340 argsFilter = "%s " % args
341 cmd = "git config %s%s" % (argsFilter, key)
342 _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
343 return _gitConfig[key]
344
345def p4BranchesInGit(branchesAreInRemotes = True):
346 branches = {}
347
348 cmdline = "git rev-parse --symbolic "
349 if branchesAreInRemotes:
350 cmdline += " --remotes"
351 else:
352 cmdline += " --branches"
353
354 for line in read_pipe_lines(cmdline):
355 line = line.strip()
356
357 ## only import to p4/
358 if not line.startswith('p4/') or line == "p4/HEAD":
359 continue
360 branch = line
361
362 # strip off p4
363 branch = re.sub ("^p4/", "", line)
364
365 branches[branch] = parseRevision(line)
366 return branches
367
368def findUpstreamBranchPoint(head = "HEAD"):
369 branches = p4BranchesInGit()
370 # map from depot-path to branch name
371 branchByDepotPath = {}
372 for branch in branches.keys():
373 tip = branches[branch]
374 log = extractLogMessageFromGitCommit(tip)
375 settings = extractSettingsGitLog(log)
376 if settings.has_key("depot-paths"):
377 paths = ",".join(settings["depot-paths"])
378 branchByDepotPath[paths] = "remotes/p4/" + branch
379
380 settings = None
381 parent = 0
382 while parent < 65535:
383 commit = head + "~%s" % parent
384 log = extractLogMessageFromGitCommit(commit)
385 settings = extractSettingsGitLog(log)
386 if settings.has_key("depot-paths"):
387 paths = ",".join(settings["depot-paths"])
388 if branchByDepotPath.has_key(paths):
389 return [branchByDepotPath[paths], settings]
390
391 parent = parent + 1
392
393 return ["", settings]
394
395def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
396 if not silent:
397 print ("Creating/updating branch(es) in %s based on origin branch(es)"
398 % localRefPrefix)
399
400 originPrefix = "origin/p4/"
401
402 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
403 line = line.strip()
404 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
405 continue
406
407 headName = line[len(originPrefix):]
408 remoteHead = localRefPrefix + headName
409 originHead = line
410
411 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
412 if (not original.has_key('depot-paths')
413 or not original.has_key('change')):
414 continue
415
416 update = False
417 if not gitBranchExists(remoteHead):
418 if verbose:
419 print "creating %s" % remoteHead
420 update = True
421 else:
422 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
423 if settings.has_key('change') > 0:
424 if settings['depot-paths'] == original['depot-paths']:
425 originP4Change = int(original['change'])
426 p4Change = int(settings['change'])
427 if originP4Change > p4Change:
428 print ("%s (%s) is newer than %s (%s). "
429 "Updating p4 branch from origin."
430 % (originHead, originP4Change,
431 remoteHead, p4Change))
432 update = True
433 else:
434 print ("Ignoring: %s was imported from %s while "
435 "%s was imported from %s"
436 % (originHead, ','.join(original['depot-paths']),
437 remoteHead, ','.join(settings['depot-paths'])))
438
439 if update:
440 system("git update-ref %s %s" % (remoteHead, originHead))
441
442def originP4BranchesExist():
443 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
444
445def p4ChangesForPaths(depotPaths, changeRange):
446 assert depotPaths
447 output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
448 for p in depotPaths]))
449
450 changes = {}
451 for line in output:
452 changeNum = int(line.split(" ")[1])
453 changes[changeNum] = True
454
455 changelist = changes.keys()
456 changelist.sort()
457 return changelist
458
459def p4PathStartsWith(path, prefix):
460 # This method tries to remedy a potential mixed-case issue:
461 #
462 # If UserA adds //depot/DirA/file1
463 # and UserB adds //depot/dira/file2
464 #
465 # we may or may not have a problem. If you have core.ignorecase=true,
466 # we treat DirA and dira as the same directory
467 ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
468 if ignorecase:
469 return path.lower().startswith(prefix.lower())
470 return path.startswith(prefix)
471
472class Command:
473 def __init__(self):
474 self.usage = "usage: %prog [options]"
475 self.needsGit = True
476
477class P4Debug(Command):
478 def __init__(self):
479 Command.__init__(self)
480 self.options = [
481 optparse.make_option("--verbose", dest="verbose", action="store_true",
482 default=False),
483 ]
484 self.description = "A tool to debug the output of p4 -G."
485 self.needsGit = False
486 self.verbose = False
487
488 def run(self, args):
489 j = 0
490 for output in p4CmdList(" ".join(args)):
491 print 'Element: %d' % j
492 j += 1
493 print output
494 return True
495
496class P4RollBack(Command):
497 def __init__(self):
498 Command.__init__(self)
499 self.options = [
500 optparse.make_option("--verbose", dest="verbose", action="store_true"),
501 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
502 ]
503 self.description = "A tool to debug the multi-branch import. Don't use :)"
504 self.verbose = False
505 self.rollbackLocalBranches = False
506
507 def run(self, args):
508 if len(args) != 1:
509 return False
510 maxChange = int(args[0])
511
512 if "p4ExitCode" in p4Cmd("changes -m 1"):
513 die("Problems executing p4");
514
515 if self.rollbackLocalBranches:
516 refPrefix = "refs/heads/"
517 lines = read_pipe_lines("git rev-parse --symbolic --branches")
518 else:
519 refPrefix = "refs/remotes/"
520 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
521
522 for line in lines:
523 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
524 line = line.strip()
525 ref = refPrefix + line
526 log = extractLogMessageFromGitCommit(ref)
527 settings = extractSettingsGitLog(log)
528
529 depotPaths = settings['depot-paths']
530 change = settings['change']
531
532 changed = False
533
534 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
535 for p in depotPaths]))) == 0:
536 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
537 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
538 continue
539
540 while change and int(change) > maxChange:
541 changed = True
542 if self.verbose:
543 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
544 system("git update-ref %s \"%s^\"" % (ref, ref))
545 log = extractLogMessageFromGitCommit(ref)
546 settings = extractSettingsGitLog(log)
547
548
549 depotPaths = settings['depot-paths']
550 change = settings['change']
551
552 if changed:
553 print "%s rewound to %s" % (ref, change)
554
555 return True
556
557class P4Submit(Command):
558 def __init__(self):
559 Command.__init__(self)
560 self.options = [
561 optparse.make_option("--verbose", dest="verbose", action="store_true"),
562 optparse.make_option("--origin", dest="origin"),
563 optparse.make_option("-M", dest="detectRenames", action="store_true"),
564 ]
565 self.description = "Submit changes from git to the perforce depot."
566 self.usage += " [name of git branch to submit into perforce depot]"
567 self.interactive = True
568 self.origin = ""
569 self.detectRenames = False
570 self.verbose = False
571 self.isWindows = (platform.system() == "Windows")
572
573 def check(self):
574 if len(p4CmdList("opened ...")) > 0:
575 die("You have files opened with perforce! Close them before starting the sync.")
576
577 # replaces everything between 'Description:' and the next P4 submit template field with the
578 # commit message
579 def prepareLogMessage(self, template, message):
580 result = ""
581
582 inDescriptionSection = False
583
584 for line in template.split("\n"):
585 if line.startswith("#"):
586 result += line + "\n"
587 continue
588
589 if inDescriptionSection:
590 if line.startswith("Files:") or line.startswith("Jobs:"):
591 inDescriptionSection = False
592 else:
593 continue
594 else:
595 if line.startswith("Description:"):
596 inDescriptionSection = True
597 line += "\n"
598 for messageLine in message.split("\n"):
599 line += "\t" + messageLine + "\n"
600
601 result += line + "\n"
602
603 return result
604
605 def prepareSubmitTemplate(self):
606 # remove lines in the Files section that show changes to files outside the depot path we're committing into
607 template = ""
608 inFilesSection = False
609 for line in p4_read_pipe_lines("change -o"):
610 if line.endswith("\r\n"):
611 line = line[:-2] + "\n"
612 if inFilesSection:
613 if line.startswith("\t"):
614 # path starts and ends with a tab
615 path = line[1:]
616 lastTab = path.rfind("\t")
617 if lastTab != -1:
618 path = path[:lastTab]
619 if not p4PathStartsWith(path, self.depotPath):
620 continue
621 else:
622 inFilesSection = False
623 else:
624 if line.startswith("Files:"):
625 inFilesSection = True
626
627 template += line
628
629 return template
630
631 def applyCommit(self, id):
632 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
633
634 if not self.detectRenames:
635 # If not explicitly set check the config variable
636 self.detectRenames = gitConfig("git-p4.detectRenames").lower() == "true"
637
638 if self.detectRenames:
639 diffOpts = "-M"
640 else:
641 diffOpts = ""
642
643 if gitConfig("git-p4.detectCopies").lower() == "true":
644 diffOpts += " -C"
645
646 if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
647 diffOpts += " --find-copies-harder"
648
649 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
650 filesToAdd = set()
651 filesToDelete = set()
652 editedFiles = set()
653 filesToChangeExecBit = {}
654 for line in diff:
655 diff = parseDiffTreeEntry(line)
656 modifier = diff['status']
657 path = diff['src']
658 if modifier == "M":
659 p4_system("edit \"%s\"" % path)
660 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
661 filesToChangeExecBit[path] = diff['dst_mode']
662 editedFiles.add(path)
663 elif modifier == "A":
664 filesToAdd.add(path)
665 filesToChangeExecBit[path] = diff['dst_mode']
666 if path in filesToDelete:
667 filesToDelete.remove(path)
668 elif modifier == "D":
669 filesToDelete.add(path)
670 if path in filesToAdd:
671 filesToAdd.remove(path)
672 elif modifier == "C":
673 src, dest = diff['src'], diff['dst']
674 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
675 if diff['src_sha1'] != diff['dst_sha1']:
676 p4_system("edit \"%s\"" % (dest))
677 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
678 p4_system("edit \"%s\"" % (dest))
679 filesToChangeExecBit[dest] = diff['dst_mode']
680 os.unlink(dest)
681 editedFiles.add(dest)
682 elif modifier == "R":
683 src, dest = diff['src'], diff['dst']
684 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
685 if diff['src_sha1'] != diff['dst_sha1']:
686 p4_system("edit \"%s\"" % (dest))
687 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
688 p4_system("edit \"%s\"" % (dest))
689 filesToChangeExecBit[dest] = diff['dst_mode']
690 os.unlink(dest)
691 editedFiles.add(dest)
692 filesToDelete.add(src)
693 else:
694 die("unknown modifier %s for %s" % (modifier, path))
695
696 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
697 patchcmd = diffcmd + " | git apply "
698 tryPatchCmd = patchcmd + "--check -"
699 applyPatchCmd = patchcmd + "--check --apply -"
700
701 if os.system(tryPatchCmd) != 0:
702 print "Unfortunately applying the change failed!"
703 print "What do you want to do?"
704 response = "x"
705 while response != "s" and response != "a" and response != "w":
706 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
707 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
708 if response == "s":
709 print "Skipping! Good luck with the next patches..."
710 for f in editedFiles:
711 p4_system("revert \"%s\"" % f);
712 for f in filesToAdd:
713 system("rm %s" %f)
714 return
715 elif response == "a":
716 os.system(applyPatchCmd)
717 if len(filesToAdd) > 0:
718 print "You may also want to call p4 add on the following files:"
719 print " ".join(filesToAdd)
720 if len(filesToDelete):
721 print "The following files should be scheduled for deletion with p4 delete:"
722 print " ".join(filesToDelete)
723 die("Please resolve and submit the conflict manually and "
724 + "continue afterwards with git-p4 submit --continue")
725 elif response == "w":
726 system(diffcmd + " > patch.txt")
727 print "Patch saved to patch.txt in %s !" % self.clientPath
728 die("Please resolve and submit the conflict manually and "
729 "continue afterwards with git-p4 submit --continue")
730
731 system(applyPatchCmd)
732
733 for f in filesToAdd:
734 p4_system("add \"%s\"" % f)
735 for f in filesToDelete:
736 p4_system("revert \"%s\"" % f)
737 p4_system("delete \"%s\"" % f)
738
739 # Set/clear executable bits
740 for f in filesToChangeExecBit.keys():
741 mode = filesToChangeExecBit[f]
742 setP4ExecBit(f, mode)
743
744 logMessage = extractLogMessageFromGitCommit(id)
745 logMessage = logMessage.strip()
746
747 template = self.prepareSubmitTemplate()
748
749 if self.interactive:
750 submitTemplate = self.prepareLogMessage(template, logMessage)
751 if os.environ.has_key("P4DIFF"):
752 del(os.environ["P4DIFF"])
753 diff = ""
754 for editedFile in editedFiles:
755 diff += p4_read_pipe("diff -du %r" % editedFile)
756
757 newdiff = ""
758 for newFile in filesToAdd:
759 newdiff += "==== new file ====\n"
760 newdiff += "--- /dev/null\n"
761 newdiff += "+++ %s\n" % newFile
762 f = open(newFile, "r")
763 for line in f.readlines():
764 newdiff += "+" + line
765 f.close()
766
767 separatorLine = "######## everything below this line is just the diff #######\n"
768
769 [handle, fileName] = tempfile.mkstemp()
770 tmpFile = os.fdopen(handle, "w+")
771 if self.isWindows:
772 submitTemplate = submitTemplate.replace("\n", "\r\n")
773 separatorLine = separatorLine.replace("\n", "\r\n")
774 newdiff = newdiff.replace("\n", "\r\n")
775 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
776 tmpFile.close()
777 mtime = os.stat(fileName).st_mtime
778 if os.environ.has_key("P4EDITOR"):
779 editor = os.environ.get("P4EDITOR")
780 else:
781 editor = read_pipe("git var GIT_EDITOR").strip()
782 system(editor + " " + fileName)
783
784 response = "y"
785 if os.stat(fileName).st_mtime <= mtime:
786 response = "x"
787 while response != "y" and response != "n":
788 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
789
790 if response == "y":
791 tmpFile = open(fileName, "rb")
792 message = tmpFile.read()
793 tmpFile.close()
794 submitTemplate = message[:message.index(separatorLine)]
795 if self.isWindows:
796 submitTemplate = submitTemplate.replace("\r\n", "\n")
797 p4_write_pipe("submit -i", submitTemplate)
798 else:
799 for f in editedFiles:
800 p4_system("revert \"%s\"" % f);
801 for f in filesToAdd:
802 p4_system("revert \"%s\"" % f);
803 system("rm %s" %f)
804
805 os.remove(fileName)
806 else:
807 fileName = "submit.txt"
808 file = open(fileName, "w+")
809 file.write(self.prepareLogMessage(template, logMessage))
810 file.close()
811 print ("Perforce submit template written as %s. "
812 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
813 % (fileName, fileName))
814
815 def run(self, args):
816 if len(args) == 0:
817 self.master = currentGitBranch()
818 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
819 die("Detecting current git branch failed!")
820 elif len(args) == 1:
821 self.master = args[0]
822 else:
823 return False
824
825 allowSubmit = gitConfig("git-p4.allowSubmit")
826 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
827 die("%s is not in git-p4.allowSubmit" % self.master)
828
829 [upstream, settings] = findUpstreamBranchPoint()
830 self.depotPath = settings['depot-paths'][0]
831 if len(self.origin) == 0:
832 self.origin = upstream
833
834 if self.verbose:
835 print "Origin branch is " + self.origin
836
837 if len(self.depotPath) == 0:
838 print "Internal error: cannot locate perforce depot path from existing branches"
839 sys.exit(128)
840
841 self.clientPath = p4Where(self.depotPath)
842
843 if len(self.clientPath) == 0:
844 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
845 sys.exit(128)
846
847 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
848 self.oldWorkingDirectory = os.getcwd()
849
850 chdir(self.clientPath)
851 print "Synchronizing p4 checkout..."
852 p4_system("sync ...")
853
854 self.check()
855
856 commits = []
857 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
858 commits.append(line.strip())
859 commits.reverse()
860
861 while len(commits) > 0:
862 commit = commits[0]
863 commits = commits[1:]
864 self.applyCommit(commit)
865 if not self.interactive:
866 break
867
868 if len(commits) == 0:
869 print "All changes applied!"
870 chdir(self.oldWorkingDirectory)
871
872 sync = P4Sync()
873 sync.run([])
874
875 rebase = P4Rebase()
876 rebase.rebase()
877
878 return True
879
880class P4Sync(Command):
881 delete_actions = ( "delete", "move/delete", "purge" )
882
883 def __init__(self):
884 Command.__init__(self)
885 self.options = [
886 optparse.make_option("--branch", dest="branch"),
887 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
888 optparse.make_option("--changesfile", dest="changesFile"),
889 optparse.make_option("--silent", dest="silent", action="store_true"),
890 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
891 optparse.make_option("--verbose", dest="verbose", action="store_true"),
892 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
893 help="Import into refs/heads/ , not refs/remotes"),
894 optparse.make_option("--max-changes", dest="maxChanges"),
895 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
896 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
897 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
898 help="Only sync files that are included in the Perforce Client Spec")
899 ]
900 self.description = """Imports from Perforce into a git repository.\n
901 example:
902 //depot/my/project/ -- to import the current head
903 //depot/my/project/@all -- to import everything
904 //depot/my/project/@1,6 -- to import only from revision 1 to 6
905
906 (a ... is not needed in the path p4 specification, it's added implicitly)"""
907
908 self.usage += " //depot/path[@revRange]"
909 self.silent = False
910 self.createdBranches = set()
911 self.committedChanges = set()
912 self.branch = ""
913 self.detectBranches = False
914 self.detectLabels = False
915 self.changesFile = ""
916 self.syncWithOrigin = True
917 self.verbose = False
918 self.importIntoRemotes = True
919 self.maxChanges = ""
920 self.isWindows = (platform.system() == "Windows")
921 self.keepRepoPath = False
922 self.depotPaths = None
923 self.p4BranchesInGit = []
924 self.cloneExclude = []
925 self.useClientSpec = False
926 self.clientSpecDirs = []
927
928 if gitConfig("git-p4.syncFromOrigin") == "false":
929 self.syncWithOrigin = False
930
931 #
932 # P4 wildcards are not allowed in filenames. P4 complains
933 # if you simply add them, but you can force it with "-f", in
934 # which case it translates them into %xx encoding internally.
935 # Search for and fix just these four characters. Do % last so
936 # that fixing it does not inadvertently create new %-escapes.
937 #
938 def wildcard_decode(self, path):
939 # Cannot have * in a filename in windows; untested as to
940 # what p4 would do in such a case.
941 if not self.isWindows:
942 path = path.replace("%2A", "*")
943 path = path.replace("%23", "#") \
944 .replace("%40", "@") \
945 .replace("%25", "%")
946 return path
947
948 def extractFilesFromCommit(self, commit):
949 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
950 for path in self.cloneExclude]
951 files = []
952 fnum = 0
953 while commit.has_key("depotFile%s" % fnum):
954 path = commit["depotFile%s" % fnum]
955
956 if [p for p in self.cloneExclude
957 if p4PathStartsWith(path, p)]:
958 found = False
959 else:
960 found = [p for p in self.depotPaths
961 if p4PathStartsWith(path, p)]
962 if not found:
963 fnum = fnum + 1
964 continue
965
966 file = {}
967 file["path"] = path
968 file["rev"] = commit["rev%s" % fnum]
969 file["action"] = commit["action%s" % fnum]
970 file["type"] = commit["type%s" % fnum]
971 files.append(file)
972 fnum = fnum + 1
973 return files
974
975 def stripRepoPath(self, path, prefixes):
976 if self.useClientSpec:
977
978 # if using the client spec, we use the output directory
979 # specified in the client. For example, a view
980 # //depot/foo/branch/... //client/branch/foo/...
981 # will end up putting all foo/branch files into
982 # branch/foo/
983 for val in self.clientSpecDirs:
984 if path.startswith(val[0]):
985 # replace the depot path with the client path
986 path = path.replace(val[0], val[1][1])
987 # now strip out the client (//client/...)
988 path = re.sub("^(//[^/]+/)", '', path)
989 # the rest is all path
990 return path
991
992 if self.keepRepoPath:
993 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
994
995 for p in prefixes:
996 if p4PathStartsWith(path, p):
997 path = path[len(p):]
998
999 return path
1000
1001 def splitFilesIntoBranches(self, commit):
1002 branches = {}
1003 fnum = 0
1004 while commit.has_key("depotFile%s" % fnum):
1005 path = commit["depotFile%s" % fnum]
1006 found = [p for p in self.depotPaths
1007 if p4PathStartsWith(path, p)]
1008 if not found:
1009 fnum = fnum + 1
1010 continue
1011
1012 file = {}
1013 file["path"] = path
1014 file["rev"] = commit["rev%s" % fnum]
1015 file["action"] = commit["action%s" % fnum]
1016 file["type"] = commit["type%s" % fnum]
1017 fnum = fnum + 1
1018
1019 relPath = self.stripRepoPath(path, self.depotPaths)
1020
1021 for branch in self.knownBranches.keys():
1022
1023 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1024 if relPath.startswith(branch + "/"):
1025 if branch not in branches:
1026 branches[branch] = []
1027 branches[branch].append(file)
1028 break
1029
1030 return branches
1031
1032 # output one file from the P4 stream
1033 # - helper for streamP4Files
1034
1035 def streamOneP4File(self, file, contents):
1036 if file["type"] == "apple":
1037 print "\nfile %s is a strange apple file that forks. Ignoring" % \
1038 file['depotFile']
1039 return
1040
1041 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1042 relPath = self.wildcard_decode(relPath)
1043 if verbose:
1044 sys.stderr.write("%s\n" % relPath)
1045
1046 mode = "644"
1047 if isP4Exec(file["type"]):
1048 mode = "755"
1049 elif file["type"] == "symlink":
1050 mode = "120000"
1051 # p4 print on a symlink contains "target\n", so strip it off
1052 data = ''.join(contents)
1053 contents = [data[:-1]]
1054
1055 if self.isWindows and file["type"].endswith("text"):
1056 mangled = []
1057 for data in contents:
1058 data = data.replace("\r\n", "\n")
1059 mangled.append(data)
1060 contents = mangled
1061
1062 if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1063 contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1064 elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1065 contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1066
1067 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1068
1069 # total length...
1070 length = 0
1071 for d in contents:
1072 length = length + len(d)
1073
1074 self.gitStream.write("data %d\n" % length)
1075 for d in contents:
1076 self.gitStream.write(d)
1077 self.gitStream.write("\n")
1078
1079 def streamOneP4Deletion(self, file):
1080 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1081 if verbose:
1082 sys.stderr.write("delete %s\n" % relPath)
1083 self.gitStream.write("D %s\n" % relPath)
1084
1085 # handle another chunk of streaming data
1086 def streamP4FilesCb(self, marshalled):
1087
1088 if marshalled.has_key('depotFile') and self.stream_have_file_info:
1089 # start of a new file - output the old one first
1090 self.streamOneP4File(self.stream_file, self.stream_contents)
1091 self.stream_file = {}
1092 self.stream_contents = []
1093 self.stream_have_file_info = False
1094
1095 # pick up the new file information... for the
1096 # 'data' field we need to append to our array
1097 for k in marshalled.keys():
1098 if k == 'data':
1099 self.stream_contents.append(marshalled['data'])
1100 else:
1101 self.stream_file[k] = marshalled[k]
1102
1103 self.stream_have_file_info = True
1104
1105 # Stream directly from "p4 files" into "git fast-import"
1106 def streamP4Files(self, files):
1107 filesForCommit = []
1108 filesToRead = []
1109 filesToDelete = []
1110
1111 for f in files:
1112 includeFile = True
1113 for val in self.clientSpecDirs:
1114 if f['path'].startswith(val[0]):
1115 if val[1][0] <= 0:
1116 includeFile = False
1117 break
1118
1119 if includeFile:
1120 filesForCommit.append(f)
1121 if f['action'] in self.delete_actions:
1122 filesToDelete.append(f)
1123 else:
1124 filesToRead.append(f)
1125
1126 # deleted files...
1127 for f in filesToDelete:
1128 self.streamOneP4Deletion(f)
1129
1130 if len(filesToRead) > 0:
1131 self.stream_file = {}
1132 self.stream_contents = []
1133 self.stream_have_file_info = False
1134
1135 # curry self argument
1136 def streamP4FilesCbSelf(entry):
1137 self.streamP4FilesCb(entry)
1138
1139 p4CmdList("-x - print",
1140 '\n'.join(['%s#%s' % (f['path'], f['rev'])
1141 for f in filesToRead]),
1142 cb=streamP4FilesCbSelf)
1143
1144 # do the last chunk
1145 if self.stream_file.has_key('depotFile'):
1146 self.streamOneP4File(self.stream_file, self.stream_contents)
1147
1148 def commit(self, details, files, branch, branchPrefixes, parent = ""):
1149 epoch = details["time"]
1150 author = details["user"]
1151 self.branchPrefixes = branchPrefixes
1152
1153 if self.verbose:
1154 print "commit into %s" % branch
1155
1156 # start with reading files; if that fails, we should not
1157 # create a commit.
1158 new_files = []
1159 for f in files:
1160 if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1161 new_files.append (f)
1162 else:
1163 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1164
1165 self.gitStream.write("commit %s\n" % branch)
1166# gitStream.write("mark :%s\n" % details["change"])
1167 self.committedChanges.add(int(details["change"]))
1168 committer = ""
1169 if author not in self.users:
1170 self.getUserMapFromPerforceServer()
1171 if author in self.users:
1172 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1173 else:
1174 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1175
1176 self.gitStream.write("committer %s\n" % committer)
1177
1178 self.gitStream.write("data <<EOT\n")
1179 self.gitStream.write(details["desc"])
1180 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1181 % (','.join (branchPrefixes), details["change"]))
1182 if len(details['options']) > 0:
1183 self.gitStream.write(": options = %s" % details['options'])
1184 self.gitStream.write("]\nEOT\n\n")
1185
1186 if len(parent) > 0:
1187 if self.verbose:
1188 print "parent %s" % parent
1189 self.gitStream.write("from %s\n" % parent)
1190
1191 self.streamP4Files(new_files)
1192 self.gitStream.write("\n")
1193
1194 change = int(details["change"])
1195
1196 if self.labels.has_key(change):
1197 label = self.labels[change]
1198 labelDetails = label[0]
1199 labelRevisions = label[1]
1200 if self.verbose:
1201 print "Change %s is labelled %s" % (change, labelDetails)
1202
1203 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1204 for p in branchPrefixes]))
1205
1206 if len(files) == len(labelRevisions):
1207
1208 cleanedFiles = {}
1209 for info in files:
1210 if info["action"] in self.delete_actions:
1211 continue
1212 cleanedFiles[info["depotFile"]] = info["rev"]
1213
1214 if cleanedFiles == labelRevisions:
1215 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1216 self.gitStream.write("from %s\n" % branch)
1217
1218 owner = labelDetails["Owner"]
1219 tagger = ""
1220 if author in self.users:
1221 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1222 else:
1223 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1224 self.gitStream.write("tagger %s\n" % tagger)
1225 self.gitStream.write("data <<EOT\n")
1226 self.gitStream.write(labelDetails["Description"])
1227 self.gitStream.write("EOT\n\n")
1228
1229 else:
1230 if not self.silent:
1231 print ("Tag %s does not match with change %s: files do not match."
1232 % (labelDetails["label"], change))
1233
1234 else:
1235 if not self.silent:
1236 print ("Tag %s does not match with change %s: file count is different."
1237 % (labelDetails["label"], change))
1238
1239 def getUserCacheFilename(self):
1240 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1241 return home + "/.gitp4-usercache.txt"
1242
1243 def getUserMapFromPerforceServer(self):
1244 if self.userMapFromPerforceServer:
1245 return
1246 self.users = {}
1247
1248 for output in p4CmdList("users"):
1249 if not output.has_key("User"):
1250 continue
1251 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1252
1253
1254 s = ''
1255 for (key, val) in self.users.items():
1256 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1257
1258 open(self.getUserCacheFilename(), "wb").write(s)
1259 self.userMapFromPerforceServer = True
1260
1261 def loadUserMapFromCache(self):
1262 self.users = {}
1263 self.userMapFromPerforceServer = False
1264 try:
1265 cache = open(self.getUserCacheFilename(), "rb")
1266 lines = cache.readlines()
1267 cache.close()
1268 for line in lines:
1269 entry = line.strip().split("\t")
1270 self.users[entry[0]] = entry[1]
1271 except IOError:
1272 self.getUserMapFromPerforceServer()
1273
1274 def getLabels(self):
1275 self.labels = {}
1276
1277 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1278 if len(l) > 0 and not self.silent:
1279 print "Finding files belonging to labels in %s" % `self.depotPaths`
1280
1281 for output in l:
1282 label = output["label"]
1283 revisions = {}
1284 newestChange = 0
1285 if self.verbose:
1286 print "Querying files for label %s" % label
1287 for file in p4CmdList("files "
1288 + ' '.join (["%s...@%s" % (p, label)
1289 for p in self.depotPaths])):
1290 revisions[file["depotFile"]] = file["rev"]
1291 change = int(file["change"])
1292 if change > newestChange:
1293 newestChange = change
1294
1295 self.labels[newestChange] = [output, revisions]
1296
1297 if self.verbose:
1298 print "Label changes: %s" % self.labels.keys()
1299
1300 def guessProjectName(self):
1301 for p in self.depotPaths:
1302 if p.endswith("/"):
1303 p = p[:-1]
1304 p = p[p.strip().rfind("/") + 1:]
1305 if not p.endswith("/"):
1306 p += "/"
1307 return p
1308
1309 def getBranchMapping(self):
1310 lostAndFoundBranches = set()
1311
1312 for info in p4CmdList("branches"):
1313 details = p4Cmd("branch -o %s" % info["branch"])
1314 viewIdx = 0
1315 while details.has_key("View%s" % viewIdx):
1316 paths = details["View%s" % viewIdx].split(" ")
1317 viewIdx = viewIdx + 1
1318 # require standard //depot/foo/... //depot/bar/... mapping
1319 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1320 continue
1321 source = paths[0]
1322 destination = paths[1]
1323 ## HACK
1324 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
1325 source = source[len(self.depotPaths[0]):-4]
1326 destination = destination[len(self.depotPaths[0]):-4]
1327
1328 if destination in self.knownBranches:
1329 if not self.silent:
1330 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1331 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1332 continue
1333
1334 self.knownBranches[destination] = source
1335
1336 lostAndFoundBranches.discard(destination)
1337
1338 if source not in self.knownBranches:
1339 lostAndFoundBranches.add(source)
1340
1341
1342 for branch in lostAndFoundBranches:
1343 self.knownBranches[branch] = branch
1344
1345 def getBranchMappingFromGitBranches(self):
1346 branches = p4BranchesInGit(self.importIntoRemotes)
1347 for branch in branches.keys():
1348 if branch == "master":
1349 branch = "main"
1350 else:
1351 branch = branch[len(self.projectName):]
1352 self.knownBranches[branch] = branch
1353
1354 def listExistingP4GitBranches(self):
1355 # branches holds mapping from name to commit
1356 branches = p4BranchesInGit(self.importIntoRemotes)
1357 self.p4BranchesInGit = branches.keys()
1358 for branch in branches.keys():
1359 self.initialParents[self.refPrefix + branch] = branches[branch]
1360
1361 def updateOptionDict(self, d):
1362 option_keys = {}
1363 if self.keepRepoPath:
1364 option_keys['keepRepoPath'] = 1
1365
1366 d["options"] = ' '.join(sorted(option_keys.keys()))
1367
1368 def readOptions(self, d):
1369 self.keepRepoPath = (d.has_key('options')
1370 and ('keepRepoPath' in d['options']))
1371
1372 def gitRefForBranch(self, branch):
1373 if branch == "main":
1374 return self.refPrefix + "master"
1375
1376 if len(branch) <= 0:
1377 return branch
1378
1379 return self.refPrefix + self.projectName + branch
1380
1381 def gitCommitByP4Change(self, ref, change):
1382 if self.verbose:
1383 print "looking in ref " + ref + " for change %s using bisect..." % change
1384
1385 earliestCommit = ""
1386 latestCommit = parseRevision(ref)
1387
1388 while True:
1389 if self.verbose:
1390 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1391 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1392 if len(next) == 0:
1393 if self.verbose:
1394 print "argh"
1395 return ""
1396 log = extractLogMessageFromGitCommit(next)
1397 settings = extractSettingsGitLog(log)
1398 currentChange = int(settings['change'])
1399 if self.verbose:
1400 print "current change %s" % currentChange
1401
1402 if currentChange == change:
1403 if self.verbose:
1404 print "found %s" % next
1405 return next
1406
1407 if currentChange < change:
1408 earliestCommit = "^%s" % next
1409 else:
1410 latestCommit = "%s" % next
1411
1412 return ""
1413
1414 def importNewBranch(self, branch, maxChange):
1415 # make fast-import flush all changes to disk and update the refs using the checkpoint
1416 # command so that we can try to find the branch parent in the git history
1417 self.gitStream.write("checkpoint\n\n");
1418 self.gitStream.flush();
1419 branchPrefix = self.depotPaths[0] + branch + "/"
1420 range = "@1,%s" % maxChange
1421 #print "prefix" + branchPrefix
1422 changes = p4ChangesForPaths([branchPrefix], range)
1423 if len(changes) <= 0:
1424 return False
1425 firstChange = changes[0]
1426 #print "first change in branch: %s" % firstChange
1427 sourceBranch = self.knownBranches[branch]
1428 sourceDepotPath = self.depotPaths[0] + sourceBranch
1429 sourceRef = self.gitRefForBranch(sourceBranch)
1430 #print "source " + sourceBranch
1431
1432 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1433 #print "branch parent: %s" % branchParentChange
1434 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1435 if len(gitParent) > 0:
1436 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1437 #print "parent git commit: %s" % gitParent
1438
1439 self.importChanges(changes)
1440 return True
1441
1442 def importChanges(self, changes):
1443 cnt = 1
1444 for change in changes:
1445 description = p4Cmd("describe %s" % change)
1446 self.updateOptionDict(description)
1447
1448 if not self.silent:
1449 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1450 sys.stdout.flush()
1451 cnt = cnt + 1
1452
1453 try:
1454 if self.detectBranches:
1455 branches = self.splitFilesIntoBranches(description)
1456 for branch in branches.keys():
1457 ## HACK --hwn
1458 branchPrefix = self.depotPaths[0] + branch + "/"
1459
1460 parent = ""
1461
1462 filesForCommit = branches[branch]
1463
1464 if self.verbose:
1465 print "branch is %s" % branch
1466
1467 self.updatedBranches.add(branch)
1468
1469 if branch not in self.createdBranches:
1470 self.createdBranches.add(branch)
1471 parent = self.knownBranches[branch]
1472 if parent == branch:
1473 parent = ""
1474 else:
1475 fullBranch = self.projectName + branch
1476 if fullBranch not in self.p4BranchesInGit:
1477 if not self.silent:
1478 print("\n Importing new branch %s" % fullBranch);
1479 if self.importNewBranch(branch, change - 1):
1480 parent = ""
1481 self.p4BranchesInGit.append(fullBranch)
1482 if not self.silent:
1483 print("\n Resuming with change %s" % change);
1484
1485 if self.verbose:
1486 print "parent determined through known branches: %s" % parent
1487
1488 branch = self.gitRefForBranch(branch)
1489 parent = self.gitRefForBranch(parent)
1490
1491 if self.verbose:
1492 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1493
1494 if len(parent) == 0 and branch in self.initialParents:
1495 parent = self.initialParents[branch]
1496 del self.initialParents[branch]
1497
1498 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1499 else:
1500 files = self.extractFilesFromCommit(description)
1501 self.commit(description, files, self.branch, self.depotPaths,
1502 self.initialParent)
1503 self.initialParent = ""
1504 except IOError:
1505 print self.gitError.read()
1506 sys.exit(1)
1507
1508 def importHeadRevision(self, revision):
1509 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1510
1511 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1512 details["desc"] = ("Initial import of %s from the state at revision %s\n"
1513 % (' '.join(self.depotPaths), revision))
1514 details["change"] = revision
1515 newestRevision = 0
1516
1517 fileCnt = 0
1518 for info in p4CmdList("files "
1519 + ' '.join(["%s...%s"
1520 % (p, revision)
1521 for p in self.depotPaths])):
1522
1523 if 'code' in info and info['code'] == 'error':
1524 sys.stderr.write("p4 returned an error: %s\n"
1525 % info['data'])
1526 if info['data'].find("must refer to client") >= 0:
1527 sys.stderr.write("This particular p4 error is misleading.\n")
1528 sys.stderr.write("Perhaps the depot path was misspelled.\n");
1529 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
1530 sys.exit(1)
1531 if 'p4ExitCode' in info:
1532 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
1533 sys.exit(1)
1534
1535
1536 change = int(info["change"])
1537 if change > newestRevision:
1538 newestRevision = change
1539
1540 if info["action"] in self.delete_actions:
1541 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1542 #fileCnt = fileCnt + 1
1543 continue
1544
1545 for prop in ["depotFile", "rev", "action", "type" ]:
1546 details["%s%s" % (prop, fileCnt)] = info[prop]
1547
1548 fileCnt = fileCnt + 1
1549
1550 details["change"] = newestRevision
1551 self.updateOptionDict(details)
1552 try:
1553 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1554 except IOError:
1555 print "IO error with git fast-import. Is your git version recent enough?"
1556 print self.gitError.read()
1557
1558
1559 def getClientSpec(self):
1560 specList = p4CmdList( "client -o" )
1561 temp = {}
1562 for entry in specList:
1563 for k,v in entry.iteritems():
1564 if k.startswith("View"):
1565
1566 # p4 has these %%1 to %%9 arguments in specs to
1567 # reorder paths; which we can't handle (yet :)
1568 if re.match('%%\d', v) != None:
1569 print "Sorry, can't handle %%n arguments in client specs"
1570 sys.exit(1)
1571
1572 if v.startswith('"'):
1573 start = 1
1574 else:
1575 start = 0
1576 index = v.find("...")
1577
1578 # save the "client view"; i.e the RHS of the view
1579 # line that tells the client where to put the
1580 # files for this view.
1581 cv = v[index+3:].strip() # +3 to remove previous '...'
1582
1583 # if the client view doesn't end with a
1584 # ... wildcard, then we're going to mess up the
1585 # output directory, so fail gracefully.
1586 if not cv.endswith('...'):
1587 print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1588 sys.exit(1)
1589 cv=cv[:-3]
1590
1591 # now save the view; +index means included, -index
1592 # means it should be filtered out.
1593 v = v[start:index]
1594 if v.startswith("-"):
1595 v = v[1:]
1596 include = -len(v)
1597 else:
1598 include = len(v)
1599
1600 temp[v] = (include, cv)
1601
1602 self.clientSpecDirs = temp.items()
1603 self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1604
1605 def run(self, args):
1606 self.depotPaths = []
1607 self.changeRange = ""
1608 self.initialParent = ""
1609 self.previousDepotPaths = []
1610
1611 # map from branch depot path to parent branch
1612 self.knownBranches = {}
1613 self.initialParents = {}
1614 self.hasOrigin = originP4BranchesExist()
1615 if not self.syncWithOrigin:
1616 self.hasOrigin = False
1617
1618 if self.importIntoRemotes:
1619 self.refPrefix = "refs/remotes/p4/"
1620 else:
1621 self.refPrefix = "refs/heads/p4/"
1622
1623 if self.syncWithOrigin and self.hasOrigin:
1624 if not self.silent:
1625 print "Syncing with origin first by calling git fetch origin"
1626 system("git fetch origin")
1627
1628 if len(self.branch) == 0:
1629 self.branch = self.refPrefix + "master"
1630 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1631 system("git update-ref %s refs/heads/p4" % self.branch)
1632 system("git branch -D p4");
1633 # create it /after/ importing, when master exists
1634 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1635 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1636
1637 if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1638 self.getClientSpec()
1639
1640 # TODO: should always look at previous commits,
1641 # merge with previous imports, if possible.
1642 if args == []:
1643 if self.hasOrigin:
1644 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1645 self.listExistingP4GitBranches()
1646
1647 if len(self.p4BranchesInGit) > 1:
1648 if not self.silent:
1649 print "Importing from/into multiple branches"
1650 self.detectBranches = True
1651
1652 if self.verbose:
1653 print "branches: %s" % self.p4BranchesInGit
1654
1655 p4Change = 0
1656 for branch in self.p4BranchesInGit:
1657 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1658
1659 settings = extractSettingsGitLog(logMsg)
1660
1661 self.readOptions(settings)
1662 if (settings.has_key('depot-paths')
1663 and settings.has_key ('change')):
1664 change = int(settings['change']) + 1
1665 p4Change = max(p4Change, change)
1666
1667 depotPaths = sorted(settings['depot-paths'])
1668 if self.previousDepotPaths == []:
1669 self.previousDepotPaths = depotPaths
1670 else:
1671 paths = []
1672 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1673 for i in range(0, min(len(cur), len(prev))):
1674 if cur[i] <> prev[i]:
1675 i = i - 1
1676 break
1677
1678 paths.append (cur[:i + 1])
1679
1680 self.previousDepotPaths = paths
1681
1682 if p4Change > 0:
1683 self.depotPaths = sorted(self.previousDepotPaths)
1684 self.changeRange = "@%s,#head" % p4Change
1685 if not self.detectBranches:
1686 self.initialParent = parseRevision(self.branch)
1687 if not self.silent and not self.detectBranches:
1688 print "Performing incremental import into %s git branch" % self.branch
1689
1690 if not self.branch.startswith("refs/"):
1691 self.branch = "refs/heads/" + self.branch
1692
1693 if len(args) == 0 and self.depotPaths:
1694 if not self.silent:
1695 print "Depot paths: %s" % ' '.join(self.depotPaths)
1696 else:
1697 if self.depotPaths and self.depotPaths != args:
1698 print ("previous import used depot path %s and now %s was specified. "
1699 "This doesn't work!" % (' '.join (self.depotPaths),
1700 ' '.join (args)))
1701 sys.exit(1)
1702
1703 self.depotPaths = sorted(args)
1704
1705 revision = ""
1706 self.users = {}
1707
1708 newPaths = []
1709 for p in self.depotPaths:
1710 if p.find("@") != -1:
1711 atIdx = p.index("@")
1712 self.changeRange = p[atIdx:]
1713 if self.changeRange == "@all":
1714 self.changeRange = ""
1715 elif ',' not in self.changeRange:
1716 revision = self.changeRange
1717 self.changeRange = ""
1718 p = p[:atIdx]
1719 elif p.find("#") != -1:
1720 hashIdx = p.index("#")
1721 revision = p[hashIdx:]
1722 p = p[:hashIdx]
1723 elif self.previousDepotPaths == []:
1724 revision = "#head"
1725
1726 p = re.sub ("\.\.\.$", "", p)
1727 if not p.endswith("/"):
1728 p += "/"
1729
1730 newPaths.append(p)
1731
1732 self.depotPaths = newPaths
1733
1734
1735 self.loadUserMapFromCache()
1736 self.labels = {}
1737 if self.detectLabels:
1738 self.getLabels();
1739
1740 if self.detectBranches:
1741 ## FIXME - what's a P4 projectName ?
1742 self.projectName = self.guessProjectName()
1743
1744 if self.hasOrigin:
1745 self.getBranchMappingFromGitBranches()
1746 else:
1747 self.getBranchMapping()
1748 if self.verbose:
1749 print "p4-git branches: %s" % self.p4BranchesInGit
1750 print "initial parents: %s" % self.initialParents
1751 for b in self.p4BranchesInGit:
1752 if b != "master":
1753
1754 ## FIXME
1755 b = b[len(self.projectName):]
1756 self.createdBranches.add(b)
1757
1758 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1759
1760 importProcess = subprocess.Popen(["git", "fast-import"],
1761 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1762 stderr=subprocess.PIPE);
1763 self.gitOutput = importProcess.stdout
1764 self.gitStream = importProcess.stdin
1765 self.gitError = importProcess.stderr
1766
1767 if revision:
1768 self.importHeadRevision(revision)
1769 else:
1770 changes = []
1771
1772 if len(self.changesFile) > 0:
1773 output = open(self.changesFile).readlines()
1774 changeSet = set()
1775 for line in output:
1776 changeSet.add(int(line))
1777
1778 for change in changeSet:
1779 changes.append(change)
1780
1781 changes.sort()
1782 else:
1783 # catch "git-p4 sync" with no new branches, in a repo that
1784 # does not have any existing git-p4 branches
1785 if len(args) == 0 and not self.p4BranchesInGit:
1786 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
1787 if self.verbose:
1788 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1789 self.changeRange)
1790 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1791
1792 if len(self.maxChanges) > 0:
1793 changes = changes[:min(int(self.maxChanges), len(changes))]
1794
1795 if len(changes) == 0:
1796 if not self.silent:
1797 print "No changes to import!"
1798 return True
1799
1800 if not self.silent and not self.detectBranches:
1801 print "Import destination: %s" % self.branch
1802
1803 self.updatedBranches = set()
1804
1805 self.importChanges(changes)
1806
1807 if not self.silent:
1808 print ""
1809 if len(self.updatedBranches) > 0:
1810 sys.stdout.write("Updated branches: ")
1811 for b in self.updatedBranches:
1812 sys.stdout.write("%s " % b)
1813 sys.stdout.write("\n")
1814
1815 self.gitStream.close()
1816 if importProcess.wait() != 0:
1817 die("fast-import failed: %s" % self.gitError.read())
1818 self.gitOutput.close()
1819 self.gitError.close()
1820
1821 return True
1822
1823class P4Rebase(Command):
1824 def __init__(self):
1825 Command.__init__(self)
1826 self.options = [ ]
1827 self.description = ("Fetches the latest revision from perforce and "
1828 + "rebases the current work (branch) against it")
1829 self.verbose = False
1830
1831 def run(self, args):
1832 sync = P4Sync()
1833 sync.run([])
1834
1835 return self.rebase()
1836
1837 def rebase(self):
1838 if os.system("git update-index --refresh") != 0:
1839 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.");
1840 if len(read_pipe("git diff-index HEAD --")) > 0:
1841 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1842
1843 [upstream, settings] = findUpstreamBranchPoint()
1844 if len(upstream) == 0:
1845 die("Cannot find upstream branchpoint for rebase")
1846
1847 # the branchpoint may be p4/foo~3, so strip off the parent
1848 upstream = re.sub("~[0-9]+$", "", upstream)
1849
1850 print "Rebasing the current branch onto %s" % upstream
1851 oldHead = read_pipe("git rev-parse HEAD").strip()
1852 system("git rebase %s" % upstream)
1853 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1854 return True
1855
1856class P4Clone(P4Sync):
1857 def __init__(self):
1858 P4Sync.__init__(self)
1859 self.description = "Creates a new git repository and imports from Perforce into it"
1860 self.usage = "usage: %prog [options] //depot/path[@revRange]"
1861 self.options += [
1862 optparse.make_option("--destination", dest="cloneDestination",
1863 action='store', default=None,
1864 help="where to leave result of the clone"),
1865 optparse.make_option("-/", dest="cloneExclude",
1866 action="append", type="string",
1867 help="exclude depot path"),
1868 optparse.make_option("--bare", dest="cloneBare",
1869 action="store_true", default=False),
1870 ]
1871 self.cloneDestination = None
1872 self.needsGit = False
1873 self.cloneBare = False
1874
1875 # This is required for the "append" cloneExclude action
1876 def ensure_value(self, attr, value):
1877 if not hasattr(self, attr) or getattr(self, attr) is None:
1878 setattr(self, attr, value)
1879 return getattr(self, attr)
1880
1881 def defaultDestination(self, args):
1882 ## TODO: use common prefix of args?
1883 depotPath = args[0]
1884 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1885 depotDir = re.sub("(#[^#]*)$", "", depotDir)
1886 depotDir = re.sub(r"\.\.\.$", "", depotDir)
1887 depotDir = re.sub(r"/$", "", depotDir)
1888 return os.path.split(depotDir)[1]
1889
1890 def run(self, args):
1891 if len(args) < 1:
1892 return False
1893
1894 if self.keepRepoPath and not self.cloneDestination:
1895 sys.stderr.write("Must specify destination for --keep-path\n")
1896 sys.exit(1)
1897
1898 depotPaths = args
1899
1900 if not self.cloneDestination and len(depotPaths) > 1:
1901 self.cloneDestination = depotPaths[-1]
1902 depotPaths = depotPaths[:-1]
1903
1904 self.cloneExclude = ["/"+p for p in self.cloneExclude]
1905 for p in depotPaths:
1906 if not p.startswith("//"):
1907 return False
1908
1909 if not self.cloneDestination:
1910 self.cloneDestination = self.defaultDestination(args)
1911
1912 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1913
1914 if not os.path.exists(self.cloneDestination):
1915 os.makedirs(self.cloneDestination)
1916 chdir(self.cloneDestination)
1917
1918 init_cmd = [ "git", "init" ]
1919 if self.cloneBare:
1920 init_cmd.append("--bare")
1921 subprocess.check_call(init_cmd)
1922
1923 if not P4Sync.run(self, depotPaths):
1924 return False
1925 if self.branch != "master":
1926 if self.importIntoRemotes:
1927 masterbranch = "refs/remotes/p4/master"
1928 else:
1929 masterbranch = "refs/heads/p4/master"
1930 if gitBranchExists(masterbranch):
1931 system("git branch master %s" % masterbranch)
1932 if not self.cloneBare:
1933 system("git checkout -f")
1934 else:
1935 print "Could not detect main branch. No checkout/master branch created."
1936
1937 return True
1938
1939class P4Branches(Command):
1940 def __init__(self):
1941 Command.__init__(self)
1942 self.options = [ ]
1943 self.description = ("Shows the git branches that hold imports and their "
1944 + "corresponding perforce depot paths")
1945 self.verbose = False
1946
1947 def run(self, args):
1948 if originP4BranchesExist():
1949 createOrUpdateBranchesFromOrigin()
1950
1951 cmdline = "git rev-parse --symbolic "
1952 cmdline += " --remotes"
1953
1954 for line in read_pipe_lines(cmdline):
1955 line = line.strip()
1956
1957 if not line.startswith('p4/') or line == "p4/HEAD":
1958 continue
1959 branch = line
1960
1961 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1962 settings = extractSettingsGitLog(log)
1963
1964 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1965 return True
1966
1967class HelpFormatter(optparse.IndentedHelpFormatter):
1968 def __init__(self):
1969 optparse.IndentedHelpFormatter.__init__(self)
1970
1971 def format_description(self, description):
1972 if description:
1973 return description + "\n"
1974 else:
1975 return ""
1976
1977def printUsage(commands):
1978 print "usage: %s <command> [options]" % sys.argv[0]
1979 print ""
1980 print "valid commands: %s" % ", ".join(commands)
1981 print ""
1982 print "Try %s <command> --help for command specific help." % sys.argv[0]
1983 print ""
1984
1985commands = {
1986 "debug" : P4Debug,
1987 "submit" : P4Submit,
1988 "commit" : P4Submit,
1989 "sync" : P4Sync,
1990 "rebase" : P4Rebase,
1991 "clone" : P4Clone,
1992 "rollback" : P4RollBack,
1993 "branches" : P4Branches
1994}
1995
1996
1997def main():
1998 if len(sys.argv[1:]) == 0:
1999 printUsage(commands.keys())
2000 sys.exit(2)
2001
2002 cmd = ""
2003 cmdName = sys.argv[1]
2004 try:
2005 klass = commands[cmdName]
2006 cmd = klass()
2007 except KeyError:
2008 print "unknown command %s" % cmdName
2009 print ""
2010 printUsage(commands.keys())
2011 sys.exit(2)
2012
2013 options = cmd.options
2014 cmd.gitdir = os.environ.get("GIT_DIR", None)
2015
2016 args = sys.argv[2:]
2017
2018 if len(options) > 0:
2019 options.append(optparse.make_option("--git-dir", dest="gitdir"))
2020
2021 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2022 options,
2023 description = cmd.description,
2024 formatter = HelpFormatter())
2025
2026 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2027 global verbose
2028 verbose = cmd.verbose
2029 if cmd.needsGit:
2030 if cmd.gitdir == None:
2031 cmd.gitdir = os.path.abspath(".git")
2032 if not isValidGitDir(cmd.gitdir):
2033 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2034 if os.path.exists(cmd.gitdir):
2035 cdup = read_pipe("git rev-parse --show-cdup").strip()
2036 if len(cdup) > 0:
2037 chdir(cdup);
2038
2039 if not isValidGitDir(cmd.gitdir):
2040 if isValidGitDir(cmd.gitdir + "/.git"):
2041 cmd.gitdir += "/.git"
2042 else:
2043 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2044
2045 os.environ["GIT_DIR"] = cmd.gitdir
2046
2047 if not cmd.run(args):
2048 parser.print_help()
2049
2050
2051if __name__ == '__main__':
2052 main()