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