git p4: refactor diffOpts calculation
[git.git] / git-p4.py
blob5fe509f6f8655affacca304dcacfb3f77d7ac7cf
1 #!/usr/bin/env python
3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
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>
11 import optparse, sys, os, marshal, subprocess, shelve
12 import tempfile, getopt, os.path, time, platform
13 import re, shutil
15 verbose = False
17 # Only labels/tags matching this will be imported/exported
18 defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
20 def p4_build_cmd(cmd):
21 """Build a suitable p4 command line.
23 This consolidates building and returning a p4 command line into one
24 location. It means that hooking into the environment, or other configuration
25 can be done more easily.
26 """
27 real_cmd = ["p4"]
29 user = gitConfig("git-p4.user")
30 if len(user) > 0:
31 real_cmd += ["-u",user]
33 password = gitConfig("git-p4.password")
34 if len(password) > 0:
35 real_cmd += ["-P", password]
37 port = gitConfig("git-p4.port")
38 if len(port) > 0:
39 real_cmd += ["-p", port]
41 host = gitConfig("git-p4.host")
42 if len(host) > 0:
43 real_cmd += ["-H", host]
45 client = gitConfig("git-p4.client")
46 if len(client) > 0:
47 real_cmd += ["-c", client]
50 if isinstance(cmd,basestring):
51 real_cmd = ' '.join(real_cmd) + ' ' + cmd
52 else:
53 real_cmd += cmd
54 return real_cmd
56 def chdir(dir):
57 # P4 uses the PWD environment variable rather than getcwd(). Since we're
58 # not using the shell, we have to set it ourselves. This path could
59 # be relative, so go there first, then figure out where we ended up.
60 os.chdir(dir)
61 os.environ['PWD'] = os.getcwd()
63 def die(msg):
64 if verbose:
65 raise Exception(msg)
66 else:
67 sys.stderr.write(msg + "\n")
68 sys.exit(1)
70 def write_pipe(c, stdin):
71 if verbose:
72 sys.stderr.write('Writing pipe: %s\n' % str(c))
74 expand = isinstance(c,basestring)
75 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
76 pipe = p.stdin
77 val = pipe.write(stdin)
78 pipe.close()
79 if p.wait():
80 die('Command failed: %s' % str(c))
82 return val
84 def p4_write_pipe(c, stdin):
85 real_cmd = p4_build_cmd(c)
86 return write_pipe(real_cmd, stdin)
88 def read_pipe(c, ignore_error=False):
89 if verbose:
90 sys.stderr.write('Reading pipe: %s\n' % str(c))
92 expand = isinstance(c,basestring)
93 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
94 pipe = p.stdout
95 val = pipe.read()
96 if p.wait() and not ignore_error:
97 die('Command failed: %s' % str(c))
99 return val
101 def p4_read_pipe(c, ignore_error=False):
102 real_cmd = p4_build_cmd(c)
103 return read_pipe(real_cmd, ignore_error)
105 def read_pipe_lines(c):
106 if verbose:
107 sys.stderr.write('Reading pipe: %s\n' % str(c))
109 expand = isinstance(c, basestring)
110 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
111 pipe = p.stdout
112 val = pipe.readlines()
113 if pipe.close() or p.wait():
114 die('Command failed: %s' % str(c))
116 return val
118 def p4_read_pipe_lines(c):
119 """Specifically invoke p4 on the command supplied. """
120 real_cmd = p4_build_cmd(c)
121 return read_pipe_lines(real_cmd)
123 def system(cmd):
124 expand = isinstance(cmd,basestring)
125 if verbose:
126 sys.stderr.write("executing %s\n" % str(cmd))
127 subprocess.check_call(cmd, shell=expand)
129 def p4_system(cmd):
130 """Specifically invoke p4 as the system command. """
131 real_cmd = p4_build_cmd(cmd)
132 expand = isinstance(real_cmd, basestring)
133 subprocess.check_call(real_cmd, shell=expand)
135 def p4_integrate(src, dest):
136 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
138 def p4_sync(f, *options):
139 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
141 def p4_add(f):
142 # forcibly add file names with wildcards
143 if wildcard_present(f):
144 p4_system(["add", "-f", f])
145 else:
146 p4_system(["add", f])
148 def p4_delete(f):
149 p4_system(["delete", wildcard_encode(f)])
151 def p4_edit(f):
152 p4_system(["edit", wildcard_encode(f)])
154 def p4_revert(f):
155 p4_system(["revert", wildcard_encode(f)])
157 def p4_reopen(type, f):
158 p4_system(["reopen", "-t", type, wildcard_encode(f)])
161 # Canonicalize the p4 type and return a tuple of the
162 # base type, plus any modifiers. See "p4 help filetypes"
163 # for a list and explanation.
165 def split_p4_type(p4type):
167 p4_filetypes_historical = {
168 "ctempobj": "binary+Sw",
169 "ctext": "text+C",
170 "cxtext": "text+Cx",
171 "ktext": "text+k",
172 "kxtext": "text+kx",
173 "ltext": "text+F",
174 "tempobj": "binary+FSw",
175 "ubinary": "binary+F",
176 "uresource": "resource+F",
177 "uxbinary": "binary+Fx",
178 "xbinary": "binary+x",
179 "xltext": "text+Fx",
180 "xtempobj": "binary+Swx",
181 "xtext": "text+x",
182 "xunicode": "unicode+x",
183 "xutf16": "utf16+x",
185 if p4type in p4_filetypes_historical:
186 p4type = p4_filetypes_historical[p4type]
187 mods = ""
188 s = p4type.split("+")
189 base = s[0]
190 mods = ""
191 if len(s) > 1:
192 mods = s[1]
193 return (base, mods)
196 # return the raw p4 type of a file (text, text+ko, etc)
198 def p4_type(file):
199 results = p4CmdList(["fstat", "-T", "headType", file])
200 return results[0]['headType']
203 # Given a type base and modifier, return a regexp matching
204 # the keywords that can be expanded in the file
206 def p4_keywords_regexp_for_type(base, type_mods):
207 if base in ("text", "unicode", "binary"):
208 kwords = None
209 if "ko" in type_mods:
210 kwords = 'Id|Header'
211 elif "k" in type_mods:
212 kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
213 else:
214 return None
215 pattern = r"""
216 \$ # Starts with a dollar, followed by...
217 (%s) # one of the keywords, followed by...
218 (:[^$]+)? # possibly an old expansion, followed by...
219 \$ # another dollar
220 """ % kwords
221 return pattern
222 else:
223 return None
226 # Given a file, return a regexp matching the possible
227 # RCS keywords that will be expanded, or None for files
228 # with kw expansion turned off.
230 def p4_keywords_regexp_for_file(file):
231 if not os.path.exists(file):
232 return None
233 else:
234 (type_base, type_mods) = split_p4_type(p4_type(file))
235 return p4_keywords_regexp_for_type(type_base, type_mods)
237 def setP4ExecBit(file, mode):
238 # Reopens an already open file and changes the execute bit to match
239 # the execute bit setting in the passed in mode.
241 p4Type = "+x"
243 if not isModeExec(mode):
244 p4Type = getP4OpenedType(file)
245 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
246 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
247 if p4Type[-1] == "+":
248 p4Type = p4Type[0:-1]
250 p4_reopen(p4Type, file)
252 def getP4OpenedType(file):
253 # Returns the perforce file type for the given file.
255 result = p4_read_pipe(["opened", wildcard_encode(file)])
256 match = re.match(".*\((.+)\)\r?$", result)
257 if match:
258 return match.group(1)
259 else:
260 die("Could not determine file type for %s (result: '%s')" % (file, result))
262 # Return the set of all p4 labels
263 def getP4Labels(depotPaths):
264 labels = set()
265 if isinstance(depotPaths,basestring):
266 depotPaths = [depotPaths]
268 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
269 label = l['label']
270 labels.add(label)
272 return labels
274 # Return the set of all git tags
275 def getGitTags():
276 gitTags = set()
277 for line in read_pipe_lines(["git", "tag"]):
278 tag = line.strip()
279 gitTags.add(tag)
280 return gitTags
282 def diffTreePattern():
283 # This is a simple generator for the diff tree regex pattern. This could be
284 # a class variable if this and parseDiffTreeEntry were a part of a class.
285 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
286 while True:
287 yield pattern
289 def parseDiffTreeEntry(entry):
290 """Parses a single diff tree entry into its component elements.
292 See git-diff-tree(1) manpage for details about the format of the diff
293 output. This method returns a dictionary with the following elements:
295 src_mode - The mode of the source file
296 dst_mode - The mode of the destination file
297 src_sha1 - The sha1 for the source file
298 dst_sha1 - The sha1 fr the destination file
299 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
300 status_score - The score for the status (applicable for 'C' and 'R'
301 statuses). This is None if there is no score.
302 src - The path for the source file.
303 dst - The path for the destination file. This is only present for
304 copy or renames. If it is not present, this is None.
306 If the pattern is not matched, None is returned."""
308 match = diffTreePattern().next().match(entry)
309 if match:
310 return {
311 'src_mode': match.group(1),
312 'dst_mode': match.group(2),
313 'src_sha1': match.group(3),
314 'dst_sha1': match.group(4),
315 'status': match.group(5),
316 'status_score': match.group(6),
317 'src': match.group(7),
318 'dst': match.group(10)
320 return None
322 def isModeExec(mode):
323 # Returns True if the given git mode represents an executable file,
324 # otherwise False.
325 return mode[-3:] == "755"
327 def isModeExecChanged(src_mode, dst_mode):
328 return isModeExec(src_mode) != isModeExec(dst_mode)
330 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
332 if isinstance(cmd,basestring):
333 cmd = "-G " + cmd
334 expand = True
335 else:
336 cmd = ["-G"] + cmd
337 expand = False
339 cmd = p4_build_cmd(cmd)
340 if verbose:
341 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
343 # Use a temporary file to avoid deadlocks without
344 # subprocess.communicate(), which would put another copy
345 # of stdout into memory.
346 stdin_file = None
347 if stdin is not None:
348 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
349 if isinstance(stdin,basestring):
350 stdin_file.write(stdin)
351 else:
352 for i in stdin:
353 stdin_file.write(i + '\n')
354 stdin_file.flush()
355 stdin_file.seek(0)
357 p4 = subprocess.Popen(cmd,
358 shell=expand,
359 stdin=stdin_file,
360 stdout=subprocess.PIPE)
362 result = []
363 try:
364 while True:
365 entry = marshal.load(p4.stdout)
366 if cb is not None:
367 cb(entry)
368 else:
369 result.append(entry)
370 except EOFError:
371 pass
372 exitCode = p4.wait()
373 if exitCode != 0:
374 entry = {}
375 entry["p4ExitCode"] = exitCode
376 result.append(entry)
378 return result
380 def p4Cmd(cmd):
381 list = p4CmdList(cmd)
382 result = {}
383 for entry in list:
384 result.update(entry)
385 return result;
387 def p4Where(depotPath):
388 if not depotPath.endswith("/"):
389 depotPath += "/"
390 depotPath = depotPath + "..."
391 outputList = p4CmdList(["where", depotPath])
392 output = None
393 for entry in outputList:
394 if "depotFile" in entry:
395 if entry["depotFile"] == depotPath:
396 output = entry
397 break
398 elif "data" in entry:
399 data = entry.get("data")
400 space = data.find(" ")
401 if data[:space] == depotPath:
402 output = entry
403 break
404 if output == None:
405 return ""
406 if output["code"] == "error":
407 return ""
408 clientPath = ""
409 if "path" in output:
410 clientPath = output.get("path")
411 elif "data" in output:
412 data = output.get("data")
413 lastSpace = data.rfind(" ")
414 clientPath = data[lastSpace + 1:]
416 if clientPath.endswith("..."):
417 clientPath = clientPath[:-3]
418 return clientPath
420 def currentGitBranch():
421 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
423 def isValidGitDir(path):
424 if (os.path.exists(path + "/HEAD")
425 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
426 return True;
427 return False
429 def parseRevision(ref):
430 return read_pipe("git rev-parse %s" % ref).strip()
432 def branchExists(ref):
433 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
434 ignore_error=True)
435 return len(rev) > 0
437 def extractLogMessageFromGitCommit(commit):
438 logMessage = ""
440 ## fixme: title is first line of commit, not 1st paragraph.
441 foundTitle = False
442 for log in read_pipe_lines("git cat-file commit %s" % commit):
443 if not foundTitle:
444 if len(log) == 1:
445 foundTitle = True
446 continue
448 logMessage += log
449 return logMessage
451 def extractSettingsGitLog(log):
452 values = {}
453 for line in log.split("\n"):
454 line = line.strip()
455 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
456 if not m:
457 continue
459 assignments = m.group(1).split (':')
460 for a in assignments:
461 vals = a.split ('=')
462 key = vals[0].strip()
463 val = ('='.join (vals[1:])).strip()
464 if val.endswith ('\"') and val.startswith('"'):
465 val = val[1:-1]
467 values[key] = val
469 paths = values.get("depot-paths")
470 if not paths:
471 paths = values.get("depot-path")
472 if paths:
473 values['depot-paths'] = paths.split(',')
474 return values
476 def gitBranchExists(branch):
477 proc = subprocess.Popen(["git", "rev-parse", branch],
478 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
479 return proc.wait() == 0;
481 _gitConfig = {}
482 def gitConfig(key, args = None): # set args to "--bool", for instance
483 if not _gitConfig.has_key(key):
484 argsFilter = ""
485 if args != None:
486 argsFilter = "%s " % args
487 cmd = "git config %s%s" % (argsFilter, key)
488 _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
489 return _gitConfig[key]
491 def gitConfigList(key):
492 if not _gitConfig.has_key(key):
493 _gitConfig[key] = read_pipe("git config --get-all %s" % key, ignore_error=True).strip().split(os.linesep)
494 return _gitConfig[key]
496 def p4BranchesInGit(branchesAreInRemotes = True):
497 branches = {}
499 cmdline = "git rev-parse --symbolic "
500 if branchesAreInRemotes:
501 cmdline += " --remotes"
502 else:
503 cmdline += " --branches"
505 for line in read_pipe_lines(cmdline):
506 line = line.strip()
508 ## only import to p4/
509 if not line.startswith('p4/') or line == "p4/HEAD":
510 continue
511 branch = line
513 # strip off p4
514 branch = re.sub ("^p4/", "", line)
516 branches[branch] = parseRevision(line)
517 return branches
519 def findUpstreamBranchPoint(head = "HEAD"):
520 branches = p4BranchesInGit()
521 # map from depot-path to branch name
522 branchByDepotPath = {}
523 for branch in branches.keys():
524 tip = branches[branch]
525 log = extractLogMessageFromGitCommit(tip)
526 settings = extractSettingsGitLog(log)
527 if settings.has_key("depot-paths"):
528 paths = ",".join(settings["depot-paths"])
529 branchByDepotPath[paths] = "remotes/p4/" + branch
531 settings = None
532 parent = 0
533 while parent < 65535:
534 commit = head + "~%s" % parent
535 log = extractLogMessageFromGitCommit(commit)
536 settings = extractSettingsGitLog(log)
537 if settings.has_key("depot-paths"):
538 paths = ",".join(settings["depot-paths"])
539 if branchByDepotPath.has_key(paths):
540 return [branchByDepotPath[paths], settings]
542 parent = parent + 1
544 return ["", settings]
546 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
547 if not silent:
548 print ("Creating/updating branch(es) in %s based on origin branch(es)"
549 % localRefPrefix)
551 originPrefix = "origin/p4/"
553 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
554 line = line.strip()
555 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
556 continue
558 headName = line[len(originPrefix):]
559 remoteHead = localRefPrefix + headName
560 originHead = line
562 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
563 if (not original.has_key('depot-paths')
564 or not original.has_key('change')):
565 continue
567 update = False
568 if not gitBranchExists(remoteHead):
569 if verbose:
570 print "creating %s" % remoteHead
571 update = True
572 else:
573 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
574 if settings.has_key('change') > 0:
575 if settings['depot-paths'] == original['depot-paths']:
576 originP4Change = int(original['change'])
577 p4Change = int(settings['change'])
578 if originP4Change > p4Change:
579 print ("%s (%s) is newer than %s (%s). "
580 "Updating p4 branch from origin."
581 % (originHead, originP4Change,
582 remoteHead, p4Change))
583 update = True
584 else:
585 print ("Ignoring: %s was imported from %s while "
586 "%s was imported from %s"
587 % (originHead, ','.join(original['depot-paths']),
588 remoteHead, ','.join(settings['depot-paths'])))
590 if update:
591 system("git update-ref %s %s" % (remoteHead, originHead))
593 def originP4BranchesExist():
594 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
596 def p4ChangesForPaths(depotPaths, changeRange):
597 assert depotPaths
598 cmd = ['changes']
599 for p in depotPaths:
600 cmd += ["%s...%s" % (p, changeRange)]
601 output = p4_read_pipe_lines(cmd)
603 changes = {}
604 for line in output:
605 changeNum = int(line.split(" ")[1])
606 changes[changeNum] = True
608 changelist = changes.keys()
609 changelist.sort()
610 return changelist
612 def p4PathStartsWith(path, prefix):
613 # This method tries to remedy a potential mixed-case issue:
615 # If UserA adds //depot/DirA/file1
616 # and UserB adds //depot/dira/file2
618 # we may or may not have a problem. If you have core.ignorecase=true,
619 # we treat DirA and dira as the same directory
620 ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
621 if ignorecase:
622 return path.lower().startswith(prefix.lower())
623 return path.startswith(prefix)
625 def getClientSpec():
626 """Look at the p4 client spec, create a View() object that contains
627 all the mappings, and return it."""
629 specList = p4CmdList("client -o")
630 if len(specList) != 1:
631 die('Output from "client -o" is %d lines, expecting 1' %
632 len(specList))
634 # dictionary of all client parameters
635 entry = specList[0]
637 # just the keys that start with "View"
638 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
640 # hold this new View
641 view = View()
643 # append the lines, in order, to the view
644 for view_num in range(len(view_keys)):
645 k = "View%d" % view_num
646 if k not in view_keys:
647 die("Expected view key %s missing" % k)
648 view.append(entry[k])
650 return view
652 def getClientRoot():
653 """Grab the client directory."""
655 output = p4CmdList("client -o")
656 if len(output) != 1:
657 die('Output from "client -o" is %d lines, expecting 1' % len(output))
659 entry = output[0]
660 if "Root" not in entry:
661 die('Client has no "Root"')
663 return entry["Root"]
666 # P4 wildcards are not allowed in filenames. P4 complains
667 # if you simply add them, but you can force it with "-f", in
668 # which case it translates them into %xx encoding internally.
670 def wildcard_decode(path):
671 # Search for and fix just these four characters. Do % last so
672 # that fixing it does not inadvertently create new %-escapes.
673 # Cannot have * in a filename in windows; untested as to
674 # what p4 would do in such a case.
675 if not platform.system() == "Windows":
676 path = path.replace("%2A", "*")
677 path = path.replace("%23", "#") \
678 .replace("%40", "@") \
679 .replace("%25", "%")
680 return path
682 def wildcard_encode(path):
683 # do % first to avoid double-encoding the %s introduced here
684 path = path.replace("%", "%25") \
685 .replace("*", "%2A") \
686 .replace("#", "%23") \
687 .replace("@", "%40")
688 return path
690 def wildcard_present(path):
691 return path.translate(None, "*#@%") != path
693 class Command:
694 def __init__(self):
695 self.usage = "usage: %prog [options]"
696 self.needsGit = True
697 self.verbose = False
699 class P4UserMap:
700 def __init__(self):
701 self.userMapFromPerforceServer = False
702 self.myP4UserId = None
704 def p4UserId(self):
705 if self.myP4UserId:
706 return self.myP4UserId
708 results = p4CmdList("user -o")
709 for r in results:
710 if r.has_key('User'):
711 self.myP4UserId = r['User']
712 return r['User']
713 die("Could not find your p4 user id")
715 def p4UserIsMe(self, p4User):
716 # return True if the given p4 user is actually me
717 me = self.p4UserId()
718 if not p4User or p4User != me:
719 return False
720 else:
721 return True
723 def getUserCacheFilename(self):
724 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
725 return home + "/.gitp4-usercache.txt"
727 def getUserMapFromPerforceServer(self):
728 if self.userMapFromPerforceServer:
729 return
730 self.users = {}
731 self.emails = {}
733 for output in p4CmdList("users"):
734 if not output.has_key("User"):
735 continue
736 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
737 self.emails[output["Email"]] = output["User"]
740 s = ''
741 for (key, val) in self.users.items():
742 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
744 open(self.getUserCacheFilename(), "wb").write(s)
745 self.userMapFromPerforceServer = True
747 def loadUserMapFromCache(self):
748 self.users = {}
749 self.userMapFromPerforceServer = False
750 try:
751 cache = open(self.getUserCacheFilename(), "rb")
752 lines = cache.readlines()
753 cache.close()
754 for line in lines:
755 entry = line.strip().split("\t")
756 self.users[entry[0]] = entry[1]
757 except IOError:
758 self.getUserMapFromPerforceServer()
760 class P4Debug(Command):
761 def __init__(self):
762 Command.__init__(self)
763 self.options = []
764 self.description = "A tool to debug the output of p4 -G."
765 self.needsGit = False
767 def run(self, args):
768 j = 0
769 for output in p4CmdList(args):
770 print 'Element: %d' % j
771 j += 1
772 print output
773 return True
775 class P4RollBack(Command):
776 def __init__(self):
777 Command.__init__(self)
778 self.options = [
779 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
781 self.description = "A tool to debug the multi-branch import. Don't use :)"
782 self.rollbackLocalBranches = False
784 def run(self, args):
785 if len(args) != 1:
786 return False
787 maxChange = int(args[0])
789 if "p4ExitCode" in p4Cmd("changes -m 1"):
790 die("Problems executing p4");
792 if self.rollbackLocalBranches:
793 refPrefix = "refs/heads/"
794 lines = read_pipe_lines("git rev-parse --symbolic --branches")
795 else:
796 refPrefix = "refs/remotes/"
797 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
799 for line in lines:
800 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
801 line = line.strip()
802 ref = refPrefix + line
803 log = extractLogMessageFromGitCommit(ref)
804 settings = extractSettingsGitLog(log)
806 depotPaths = settings['depot-paths']
807 change = settings['change']
809 changed = False
811 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
812 for p in depotPaths]))) == 0:
813 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
814 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
815 continue
817 while change and int(change) > maxChange:
818 changed = True
819 if self.verbose:
820 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
821 system("git update-ref %s \"%s^\"" % (ref, ref))
822 log = extractLogMessageFromGitCommit(ref)
823 settings = extractSettingsGitLog(log)
826 depotPaths = settings['depot-paths']
827 change = settings['change']
829 if changed:
830 print "%s rewound to %s" % (ref, change)
832 return True
834 class P4Submit(Command, P4UserMap):
835 def __init__(self):
836 Command.__init__(self)
837 P4UserMap.__init__(self)
838 self.options = [
839 optparse.make_option("--origin", dest="origin"),
840 optparse.make_option("-M", dest="detectRenames", action="store_true"),
841 # preserve the user, requires relevant p4 permissions
842 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
843 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
845 self.description = "Submit changes from git to the perforce depot."
846 self.usage += " [name of git branch to submit into perforce depot]"
847 self.interactive = True
848 self.origin = ""
849 self.detectRenames = False
850 self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
851 self.isWindows = (platform.system() == "Windows")
852 self.exportLabels = False
854 def check(self):
855 if len(p4CmdList("opened ...")) > 0:
856 die("You have files opened with perforce! Close them before starting the sync.")
858 # replaces everything between 'Description:' and the next P4 submit template field with the
859 # commit message
860 def prepareLogMessage(self, template, message):
861 result = ""
863 inDescriptionSection = False
865 for line in template.split("\n"):
866 if line.startswith("#"):
867 result += line + "\n"
868 continue
870 if inDescriptionSection:
871 if line.startswith("Files:") or line.startswith("Jobs:"):
872 inDescriptionSection = False
873 else:
874 continue
875 else:
876 if line.startswith("Description:"):
877 inDescriptionSection = True
878 line += "\n"
879 for messageLine in message.split("\n"):
880 line += "\t" + messageLine + "\n"
882 result += line + "\n"
884 return result
886 def patchRCSKeywords(self, file, pattern):
887 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
888 (handle, outFileName) = tempfile.mkstemp(dir='.')
889 try:
890 outFile = os.fdopen(handle, "w+")
891 inFile = open(file, "r")
892 regexp = re.compile(pattern, re.VERBOSE)
893 for line in inFile.readlines():
894 line = regexp.sub(r'$\1$', line)
895 outFile.write(line)
896 inFile.close()
897 outFile.close()
898 # Forcibly overwrite the original file
899 os.unlink(file)
900 shutil.move(outFileName, file)
901 except:
902 # cleanup our temporary file
903 os.unlink(outFileName)
904 print "Failed to strip RCS keywords in %s" % file
905 raise
907 print "Patched up RCS keywords in %s" % file
909 def p4UserForCommit(self,id):
910 # Return the tuple (perforce user,git email) for a given git commit id
911 self.getUserMapFromPerforceServer()
912 gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
913 gitEmail = gitEmail.strip()
914 if not self.emails.has_key(gitEmail):
915 return (None,gitEmail)
916 else:
917 return (self.emails[gitEmail],gitEmail)
919 def checkValidP4Users(self,commits):
920 # check if any git authors cannot be mapped to p4 users
921 for id in commits:
922 (user,email) = self.p4UserForCommit(id)
923 if not user:
924 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
925 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
926 print "%s" % msg
927 else:
928 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
930 def lastP4Changelist(self):
931 # Get back the last changelist number submitted in this client spec. This
932 # then gets used to patch up the username in the change. If the same
933 # client spec is being used by multiple processes then this might go
934 # wrong.
935 results = p4CmdList("client -o") # find the current client
936 client = None
937 for r in results:
938 if r.has_key('Client'):
939 client = r['Client']
940 break
941 if not client:
942 die("could not get client spec")
943 results = p4CmdList(["changes", "-c", client, "-m", "1"])
944 for r in results:
945 if r.has_key('change'):
946 return r['change']
947 die("Could not get changelist number for last submit - cannot patch up user details")
949 def modifyChangelistUser(self, changelist, newUser):
950 # fixup the user field of a changelist after it has been submitted.
951 changes = p4CmdList("change -o %s" % changelist)
952 if len(changes) != 1:
953 die("Bad output from p4 change modifying %s to user %s" %
954 (changelist, newUser))
956 c = changes[0]
957 if c['User'] == newUser: return # nothing to do
958 c['User'] = newUser
959 input = marshal.dumps(c)
961 result = p4CmdList("change -f -i", stdin=input)
962 for r in result:
963 if r.has_key('code'):
964 if r['code'] == 'error':
965 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
966 if r.has_key('data'):
967 print("Updated user field for changelist %s to %s" % (changelist, newUser))
968 return
969 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
971 def canChangeChangelists(self):
972 # check to see if we have p4 admin or super-user permissions, either of
973 # which are required to modify changelists.
974 results = p4CmdList(["protects", self.depotPath])
975 for r in results:
976 if r.has_key('perm'):
977 if r['perm'] == 'admin':
978 return 1
979 if r['perm'] == 'super':
980 return 1
981 return 0
983 def prepareSubmitTemplate(self):
984 # remove lines in the Files section that show changes to files outside the depot path we're committing into
985 template = ""
986 inFilesSection = False
987 for line in p4_read_pipe_lines(['change', '-o']):
988 if line.endswith("\r\n"):
989 line = line[:-2] + "\n"
990 if inFilesSection:
991 if line.startswith("\t"):
992 # path starts and ends with a tab
993 path = line[1:]
994 lastTab = path.rfind("\t")
995 if lastTab != -1:
996 path = path[:lastTab]
997 if not p4PathStartsWith(path, self.depotPath):
998 continue
999 else:
1000 inFilesSection = False
1001 else:
1002 if line.startswith("Files:"):
1003 inFilesSection = True
1005 template += line
1007 return template
1009 def edit_template(self, template_file):
1010 """Invoke the editor to let the user change the submission
1011 message. Return true if okay to continue with the submit."""
1013 # if configured to skip the editing part, just submit
1014 if gitConfig("git-p4.skipSubmitEdit") == "true":
1015 return True
1017 # look at the modification time, to check later if the user saved
1018 # the file
1019 mtime = os.stat(template_file).st_mtime
1021 # invoke the editor
1022 if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1023 editor = os.environ.get("P4EDITOR")
1024 else:
1025 editor = read_pipe("git var GIT_EDITOR").strip()
1026 system(editor + " " + template_file)
1028 # If the file was not saved, prompt to see if this patch should
1029 # be skipped. But skip this verification step if configured so.
1030 if gitConfig("git-p4.skipSubmitEditCheck") == "true":
1031 return True
1033 # modification time updated means user saved the file
1034 if os.stat(template_file).st_mtime > mtime:
1035 return True
1037 while True:
1038 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1039 if response == 'y':
1040 return True
1041 if response == 'n':
1042 return False
1044 def applyCommit(self, id):
1045 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
1047 (p4User, gitEmail) = self.p4UserForCommit(id)
1050 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1051 filesToAdd = set()
1052 filesToDelete = set()
1053 editedFiles = set()
1054 pureRenameCopy = set()
1055 filesToChangeExecBit = {}
1057 for line in diff:
1058 diff = parseDiffTreeEntry(line)
1059 modifier = diff['status']
1060 path = diff['src']
1061 if modifier == "M":
1062 p4_edit(path)
1063 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1064 filesToChangeExecBit[path] = diff['dst_mode']
1065 editedFiles.add(path)
1066 elif modifier == "A":
1067 filesToAdd.add(path)
1068 filesToChangeExecBit[path] = diff['dst_mode']
1069 if path in filesToDelete:
1070 filesToDelete.remove(path)
1071 elif modifier == "D":
1072 filesToDelete.add(path)
1073 if path in filesToAdd:
1074 filesToAdd.remove(path)
1075 elif modifier == "C":
1076 src, dest = diff['src'], diff['dst']
1077 p4_integrate(src, dest)
1078 pureRenameCopy.add(dest)
1079 if diff['src_sha1'] != diff['dst_sha1']:
1080 p4_edit(dest)
1081 pureRenameCopy.discard(dest)
1082 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1083 p4_edit(dest)
1084 pureRenameCopy.discard(dest)
1085 filesToChangeExecBit[dest] = diff['dst_mode']
1086 os.unlink(dest)
1087 editedFiles.add(dest)
1088 elif modifier == "R":
1089 src, dest = diff['src'], diff['dst']
1090 p4_integrate(src, dest)
1091 if diff['src_sha1'] != diff['dst_sha1']:
1092 p4_edit(dest)
1093 else:
1094 pureRenameCopy.add(dest)
1095 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1096 p4_edit(dest)
1097 filesToChangeExecBit[dest] = diff['dst_mode']
1098 os.unlink(dest)
1099 editedFiles.add(dest)
1100 filesToDelete.add(src)
1101 else:
1102 die("unknown modifier %s for %s" % (modifier, path))
1104 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
1105 patchcmd = diffcmd + " | git apply "
1106 tryPatchCmd = patchcmd + "--check -"
1107 applyPatchCmd = patchcmd + "--check --apply -"
1108 patch_succeeded = True
1110 if os.system(tryPatchCmd) != 0:
1111 fixed_rcs_keywords = False
1112 patch_succeeded = False
1113 print "Unfortunately applying the change failed!"
1115 # Patch failed, maybe it's just RCS keyword woes. Look through
1116 # the patch to see if that's possible.
1117 if gitConfig("git-p4.attemptRCSCleanup","--bool") == "true":
1118 file = None
1119 pattern = None
1120 kwfiles = {}
1121 for file in editedFiles | filesToDelete:
1122 # did this file's delta contain RCS keywords?
1123 pattern = p4_keywords_regexp_for_file(file)
1125 if pattern:
1126 # this file is a possibility...look for RCS keywords.
1127 regexp = re.compile(pattern, re.VERBOSE)
1128 for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1129 if regexp.search(line):
1130 if verbose:
1131 print "got keyword match on %s in %s in %s" % (pattern, line, file)
1132 kwfiles[file] = pattern
1133 break
1135 for file in kwfiles:
1136 if verbose:
1137 print "zapping %s with %s" % (line,pattern)
1138 self.patchRCSKeywords(file, kwfiles[file])
1139 fixed_rcs_keywords = True
1141 if fixed_rcs_keywords:
1142 print "Retrying the patch with RCS keywords cleaned up"
1143 if os.system(tryPatchCmd) == 0:
1144 patch_succeeded = True
1146 if not patch_succeeded:
1147 print "What do you want to do?"
1148 response = "x"
1149 while response != "s" and response != "a" and response != "w":
1150 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
1151 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
1152 if response == "s":
1153 print "Skipping! Good luck with the next patches..."
1154 for f in editedFiles:
1155 p4_revert(f)
1156 for f in filesToAdd:
1157 os.remove(f)
1158 return
1159 elif response == "a":
1160 os.system(applyPatchCmd)
1161 if len(filesToAdd) > 0:
1162 print "You may also want to call p4 add on the following files:"
1163 print " ".join(filesToAdd)
1164 if len(filesToDelete):
1165 print "The following files should be scheduled for deletion with p4 delete:"
1166 print " ".join(filesToDelete)
1167 die("Please resolve and submit the conflict manually and "
1168 + "continue afterwards with git p4 submit --continue")
1169 elif response == "w":
1170 system(diffcmd + " > patch.txt")
1171 print "Patch saved to patch.txt in %s !" % self.clientPath
1172 die("Please resolve and submit the conflict manually and "
1173 "continue afterwards with git p4 submit --continue")
1175 system(applyPatchCmd)
1177 for f in filesToAdd:
1178 p4_add(f)
1179 for f in filesToDelete:
1180 p4_revert(f)
1181 p4_delete(f)
1183 # Set/clear executable bits
1184 for f in filesToChangeExecBit.keys():
1185 mode = filesToChangeExecBit[f]
1186 setP4ExecBit(f, mode)
1188 logMessage = extractLogMessageFromGitCommit(id)
1189 logMessage = logMessage.strip()
1191 template = self.prepareSubmitTemplate()
1193 if self.interactive:
1194 submitTemplate = self.prepareLogMessage(template, logMessage)
1196 if self.preserveUser:
1197 submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
1199 if os.environ.has_key("P4DIFF"):
1200 del(os.environ["P4DIFF"])
1201 diff = ""
1202 for editedFile in editedFiles:
1203 diff += p4_read_pipe(['diff', '-du',
1204 wildcard_encode(editedFile)])
1206 newdiff = ""
1207 for newFile in filesToAdd:
1208 newdiff += "==== new file ====\n"
1209 newdiff += "--- /dev/null\n"
1210 newdiff += "+++ %s\n" % newFile
1211 f = open(newFile, "r")
1212 for line in f.readlines():
1213 newdiff += "+" + line
1214 f.close()
1216 if self.checkAuthorship and not self.p4UserIsMe(p4User):
1217 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1218 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1219 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1221 separatorLine = "######## everything below this line is just the diff #######\n"
1223 (handle, fileName) = tempfile.mkstemp()
1224 tmpFile = os.fdopen(handle, "w+")
1225 if self.isWindows:
1226 submitTemplate = submitTemplate.replace("\n", "\r\n")
1227 separatorLine = separatorLine.replace("\n", "\r\n")
1228 newdiff = newdiff.replace("\n", "\r\n")
1229 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
1230 tmpFile.close()
1232 if self.edit_template(fileName):
1233 # read the edited message and submit
1234 tmpFile = open(fileName, "rb")
1235 message = tmpFile.read()
1236 tmpFile.close()
1237 submitTemplate = message[:message.index(separatorLine)]
1238 if self.isWindows:
1239 submitTemplate = submitTemplate.replace("\r\n", "\n")
1240 p4_write_pipe(['submit', '-i'], submitTemplate)
1242 if self.preserveUser:
1243 if p4User:
1244 # Get last changelist number. Cannot easily get it from
1245 # the submit command output as the output is
1246 # unmarshalled.
1247 changelist = self.lastP4Changelist()
1248 self.modifyChangelistUser(changelist, p4User)
1250 # The rename/copy happened by applying a patch that created a
1251 # new file. This leaves it writable, which confuses p4.
1252 for f in pureRenameCopy:
1253 p4_sync(f, "-f")
1255 else:
1256 # skip this patch
1257 print "Submission cancelled, undoing p4 changes."
1258 for f in editedFiles:
1259 p4_revert(f)
1260 for f in filesToAdd:
1261 p4_revert(f)
1262 os.remove(f)
1264 os.remove(fileName)
1265 else:
1266 fileName = "submit.txt"
1267 file = open(fileName, "w+")
1268 file.write(self.prepareLogMessage(template, logMessage))
1269 file.close()
1270 print ("Perforce submit template written as %s. "
1271 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
1272 % (fileName, fileName))
1274 # Export git tags as p4 labels. Create a p4 label and then tag
1275 # with that.
1276 def exportGitTags(self, gitTags):
1277 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1278 if len(validLabelRegexp) == 0:
1279 validLabelRegexp = defaultLabelRegexp
1280 m = re.compile(validLabelRegexp)
1282 for name in gitTags:
1284 if not m.match(name):
1285 if verbose:
1286 print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1287 continue
1289 # Get the p4 commit this corresponds to
1290 logMessage = extractLogMessageFromGitCommit(name)
1291 values = extractSettingsGitLog(logMessage)
1293 if not values.has_key('change'):
1294 # a tag pointing to something not sent to p4; ignore
1295 if verbose:
1296 print "git tag %s does not give a p4 commit" % name
1297 continue
1298 else:
1299 changelist = values['change']
1301 # Get the tag details.
1302 inHeader = True
1303 isAnnotated = False
1304 body = []
1305 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1306 l = l.strip()
1307 if inHeader:
1308 if re.match(r'tag\s+', l):
1309 isAnnotated = True
1310 elif re.match(r'\s*$', l):
1311 inHeader = False
1312 continue
1313 else:
1314 body.append(l)
1316 if not isAnnotated:
1317 body = ["lightweight tag imported by git p4\n"]
1319 # Create the label - use the same view as the client spec we are using
1320 clientSpec = getClientSpec()
1322 labelTemplate = "Label: %s\n" % name
1323 labelTemplate += "Description:\n"
1324 for b in body:
1325 labelTemplate += "\t" + b + "\n"
1326 labelTemplate += "View:\n"
1327 for mapping in clientSpec.mappings:
1328 labelTemplate += "\t%s\n" % mapping.depot_side.path
1330 p4_write_pipe(["label", "-i"], labelTemplate)
1332 # Use the label
1333 p4_system(["tag", "-l", name] +
1334 ["%s@%s" % (mapping.depot_side.path, changelist) for mapping in clientSpec.mappings])
1336 if verbose:
1337 print "created p4 label for tag %s" % name
1339 def run(self, args):
1340 if len(args) == 0:
1341 self.master = currentGitBranch()
1342 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1343 die("Detecting current git branch failed!")
1344 elif len(args) == 1:
1345 self.master = args[0]
1346 if not branchExists(self.master):
1347 die("Branch %s does not exist" % self.master)
1348 else:
1349 return False
1351 allowSubmit = gitConfig("git-p4.allowSubmit")
1352 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1353 die("%s is not in git-p4.allowSubmit" % self.master)
1355 [upstream, settings] = findUpstreamBranchPoint()
1356 self.depotPath = settings['depot-paths'][0]
1357 if len(self.origin) == 0:
1358 self.origin = upstream
1360 if self.preserveUser:
1361 if not self.canChangeChangelists():
1362 die("Cannot preserve user names without p4 super-user or admin permissions")
1364 if self.verbose:
1365 print "Origin branch is " + self.origin
1367 if len(self.depotPath) == 0:
1368 print "Internal error: cannot locate perforce depot path from existing branches"
1369 sys.exit(128)
1371 self.useClientSpec = False
1372 if gitConfig("git-p4.useclientspec", "--bool") == "true":
1373 self.useClientSpec = True
1374 if self.useClientSpec:
1375 self.clientSpecDirs = getClientSpec()
1377 if self.useClientSpec:
1378 # all files are relative to the client spec
1379 self.clientPath = getClientRoot()
1380 else:
1381 self.clientPath = p4Where(self.depotPath)
1383 if self.clientPath == "":
1384 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1386 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1387 self.oldWorkingDirectory = os.getcwd()
1389 # ensure the clientPath exists
1390 new_client_dir = False
1391 if not os.path.exists(self.clientPath):
1392 new_client_dir = True
1393 os.makedirs(self.clientPath)
1395 chdir(self.clientPath)
1396 print "Synchronizing p4 checkout..."
1397 if new_client_dir:
1398 # old one was destroyed, and maybe nobody told p4
1399 p4_sync("...", "-f")
1400 else:
1401 p4_sync("...")
1402 self.check()
1404 commits = []
1405 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1406 commits.append(line.strip())
1407 commits.reverse()
1409 if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1410 self.checkAuthorship = False
1411 else:
1412 self.checkAuthorship = True
1414 if self.preserveUser:
1415 self.checkValidP4Users(commits)
1418 # Build up a set of options to be passed to diff when
1419 # submitting each commit to p4.
1421 if self.detectRenames:
1422 # command-line -M arg
1423 self.diffOpts = "-M"
1424 else:
1425 # If not explicitly set check the config variable
1426 detectRenames = gitConfig("git-p4.detectRenames")
1428 if detectRenames.lower() == "false" or detectRenames == "":
1429 self.diffOpts = ""
1430 elif detectRenames.lower() == "true":
1431 self.diffOpts = "-M"
1432 else:
1433 self.diffOpts = "-M%s" % detectRenames
1435 # no command-line arg for -C or --find-copies-harder, just
1436 # config variables
1437 detectCopies = gitConfig("git-p4.detectCopies")
1438 if detectCopies.lower() == "false" or detectCopies == "":
1439 pass
1440 elif detectCopies.lower() == "true":
1441 self.diffOpts += " -C"
1442 else:
1443 self.diffOpts += " -C%s" % detectCopies
1445 if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
1446 self.diffOpts += " --find-copies-harder"
1448 while len(commits) > 0:
1449 commit = commits[0]
1450 commits = commits[1:]
1451 self.applyCommit(commit)
1452 if not self.interactive:
1453 break
1455 if len(commits) == 0:
1456 print "All changes applied!"
1457 chdir(self.oldWorkingDirectory)
1459 sync = P4Sync()
1460 sync.run([])
1462 rebase = P4Rebase()
1463 rebase.rebase()
1465 if gitConfig("git-p4.exportLabels", "--bool") == "true":
1466 self.exportLabels = True
1468 if self.exportLabels:
1469 p4Labels = getP4Labels(self.depotPath)
1470 gitTags = getGitTags()
1472 missingGitTags = gitTags - p4Labels
1473 self.exportGitTags(missingGitTags)
1475 return True
1477 class View(object):
1478 """Represent a p4 view ("p4 help views"), and map files in a
1479 repo according to the view."""
1481 class Path(object):
1482 """A depot or client path, possibly containing wildcards.
1483 The only one supported is ... at the end, currently.
1484 Initialize with the full path, with //depot or //client."""
1486 def __init__(self, path, is_depot):
1487 self.path = path
1488 self.is_depot = is_depot
1489 self.find_wildcards()
1490 # remember the prefix bit, useful for relative mappings
1491 m = re.match("(//[^/]+/)", self.path)
1492 if not m:
1493 die("Path %s does not start with //prefix/" % self.path)
1494 prefix = m.group(1)
1495 if not self.is_depot:
1496 # strip //client/ on client paths
1497 self.path = self.path[len(prefix):]
1499 def find_wildcards(self):
1500 """Make sure wildcards are valid, and set up internal
1501 variables."""
1503 self.ends_triple_dot = False
1504 # There are three wildcards allowed in p4 views
1505 # (see "p4 help views"). This code knows how to
1506 # handle "..." (only at the end), but cannot deal with
1507 # "%%n" or "*". Only check the depot_side, as p4 should
1508 # validate that the client_side matches too.
1509 if re.search(r'%%[1-9]', self.path):
1510 die("Can't handle %%n wildcards in view: %s" % self.path)
1511 if self.path.find("*") >= 0:
1512 die("Can't handle * wildcards in view: %s" % self.path)
1513 triple_dot_index = self.path.find("...")
1514 if triple_dot_index >= 0:
1515 if triple_dot_index != len(self.path) - 3:
1516 die("Can handle only single ... wildcard, at end: %s" %
1517 self.path)
1518 self.ends_triple_dot = True
1520 def ensure_compatible(self, other_path):
1521 """Make sure the wildcards agree."""
1522 if self.ends_triple_dot != other_path.ends_triple_dot:
1523 die("Both paths must end with ... if either does;\n" +
1524 "paths: %s %s" % (self.path, other_path.path))
1526 def match_wildcards(self, test_path):
1527 """See if this test_path matches us, and fill in the value
1528 of the wildcards if so. Returns a tuple of
1529 (True|False, wildcards[]). For now, only the ... at end
1530 is supported, so at most one wildcard."""
1531 if self.ends_triple_dot:
1532 dotless = self.path[:-3]
1533 if test_path.startswith(dotless):
1534 wildcard = test_path[len(dotless):]
1535 return (True, [ wildcard ])
1536 else:
1537 if test_path == self.path:
1538 return (True, [])
1539 return (False, [])
1541 def match(self, test_path):
1542 """Just return if it matches; don't bother with the wildcards."""
1543 b, _ = self.match_wildcards(test_path)
1544 return b
1546 def fill_in_wildcards(self, wildcards):
1547 """Return the relative path, with the wildcards filled in
1548 if there are any."""
1549 if self.ends_triple_dot:
1550 return self.path[:-3] + wildcards[0]
1551 else:
1552 return self.path
1554 class Mapping(object):
1555 def __init__(self, depot_side, client_side, overlay, exclude):
1556 # depot_side is without the trailing /... if it had one
1557 self.depot_side = View.Path(depot_side, is_depot=True)
1558 self.client_side = View.Path(client_side, is_depot=False)
1559 self.overlay = overlay # started with "+"
1560 self.exclude = exclude # started with "-"
1561 assert not (self.overlay and self.exclude)
1562 self.depot_side.ensure_compatible(self.client_side)
1564 def __str__(self):
1565 c = " "
1566 if self.overlay:
1567 c = "+"
1568 if self.exclude:
1569 c = "-"
1570 return "View.Mapping: %s%s -> %s" % \
1571 (c, self.depot_side.path, self.client_side.path)
1573 def map_depot_to_client(self, depot_path):
1574 """Calculate the client path if using this mapping on the
1575 given depot path; does not consider the effect of other
1576 mappings in a view. Even excluded mappings are returned."""
1577 matches, wildcards = self.depot_side.match_wildcards(depot_path)
1578 if not matches:
1579 return ""
1580 client_path = self.client_side.fill_in_wildcards(wildcards)
1581 return client_path
1584 # View methods
1586 def __init__(self):
1587 self.mappings = []
1589 def append(self, view_line):
1590 """Parse a view line, splitting it into depot and client
1591 sides. Append to self.mappings, preserving order."""
1593 # Split the view line into exactly two words. P4 enforces
1594 # structure on these lines that simplifies this quite a bit.
1596 # Either or both words may be double-quoted.
1597 # Single quotes do not matter.
1598 # Double-quote marks cannot occur inside the words.
1599 # A + or - prefix is also inside the quotes.
1600 # There are no quotes unless they contain a space.
1601 # The line is already white-space stripped.
1602 # The two words are separated by a single space.
1604 if view_line[0] == '"':
1605 # First word is double quoted. Find its end.
1606 close_quote_index = view_line.find('"', 1)
1607 if close_quote_index <= 0:
1608 die("No first-word closing quote found: %s" % view_line)
1609 depot_side = view_line[1:close_quote_index]
1610 # skip closing quote and space
1611 rhs_index = close_quote_index + 1 + 1
1612 else:
1613 space_index = view_line.find(" ")
1614 if space_index <= 0:
1615 die("No word-splitting space found: %s" % view_line)
1616 depot_side = view_line[0:space_index]
1617 rhs_index = space_index + 1
1619 if view_line[rhs_index] == '"':
1620 # Second word is double quoted. Make sure there is a
1621 # double quote at the end too.
1622 if not view_line.endswith('"'):
1623 die("View line with rhs quote should end with one: %s" %
1624 view_line)
1625 # skip the quotes
1626 client_side = view_line[rhs_index+1:-1]
1627 else:
1628 client_side = view_line[rhs_index:]
1630 # prefix + means overlay on previous mapping
1631 overlay = False
1632 if depot_side.startswith("+"):
1633 overlay = True
1634 depot_side = depot_side[1:]
1636 # prefix - means exclude this path
1637 exclude = False
1638 if depot_side.startswith("-"):
1639 exclude = True
1640 depot_side = depot_side[1:]
1642 m = View.Mapping(depot_side, client_side, overlay, exclude)
1643 self.mappings.append(m)
1645 def map_in_client(self, depot_path):
1646 """Return the relative location in the client where this
1647 depot file should live. Returns "" if the file should
1648 not be mapped in the client."""
1650 paths_filled = []
1651 client_path = ""
1653 # look at later entries first
1654 for m in self.mappings[::-1]:
1656 # see where will this path end up in the client
1657 p = m.map_depot_to_client(depot_path)
1659 if p == "":
1660 # Depot path does not belong in client. Must remember
1661 # this, as previous items should not cause files to
1662 # exist in this path either. Remember that the list is
1663 # being walked from the end, which has higher precedence.
1664 # Overlap mappings do not exclude previous mappings.
1665 if not m.overlay:
1666 paths_filled.append(m.client_side)
1668 else:
1669 # This mapping matched; no need to search any further.
1670 # But, the mapping could be rejected if the client path
1671 # has already been claimed by an earlier mapping (i.e.
1672 # one later in the list, which we are walking backwards).
1673 already_mapped_in_client = False
1674 for f in paths_filled:
1675 # this is View.Path.match
1676 if f.match(p):
1677 already_mapped_in_client = True
1678 break
1679 if not already_mapped_in_client:
1680 # Include this file, unless it is from a line that
1681 # explicitly said to exclude it.
1682 if not m.exclude:
1683 client_path = p
1685 # a match, even if rejected, always stops the search
1686 break
1688 return client_path
1690 class P4Sync(Command, P4UserMap):
1691 delete_actions = ( "delete", "move/delete", "purge" )
1693 def __init__(self):
1694 Command.__init__(self)
1695 P4UserMap.__init__(self)
1696 self.options = [
1697 optparse.make_option("--branch", dest="branch"),
1698 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1699 optparse.make_option("--changesfile", dest="changesFile"),
1700 optparse.make_option("--silent", dest="silent", action="store_true"),
1701 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1702 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1703 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1704 help="Import into refs/heads/ , not refs/remotes"),
1705 optparse.make_option("--max-changes", dest="maxChanges"),
1706 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1707 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1708 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1709 help="Only sync files that are included in the Perforce Client Spec")
1711 self.description = """Imports from Perforce into a git repository.\n
1712 example:
1713 //depot/my/project/ -- to import the current head
1714 //depot/my/project/@all -- to import everything
1715 //depot/my/project/@1,6 -- to import only from revision 1 to 6
1717 (a ... is not needed in the path p4 specification, it's added implicitly)"""
1719 self.usage += " //depot/path[@revRange]"
1720 self.silent = False
1721 self.createdBranches = set()
1722 self.committedChanges = set()
1723 self.branch = ""
1724 self.detectBranches = False
1725 self.detectLabels = False
1726 self.importLabels = False
1727 self.changesFile = ""
1728 self.syncWithOrigin = True
1729 self.importIntoRemotes = True
1730 self.maxChanges = ""
1731 self.isWindows = (platform.system() == "Windows")
1732 self.keepRepoPath = False
1733 self.depotPaths = None
1734 self.p4BranchesInGit = []
1735 self.cloneExclude = []
1736 self.useClientSpec = False
1737 self.useClientSpec_from_options = False
1738 self.clientSpecDirs = None
1739 self.tempBranches = []
1740 self.tempBranchLocation = "git-p4-tmp"
1742 if gitConfig("git-p4.syncFromOrigin") == "false":
1743 self.syncWithOrigin = False
1745 # Force a checkpoint in fast-import and wait for it to finish
1746 def checkpoint(self):
1747 self.gitStream.write("checkpoint\n\n")
1748 self.gitStream.write("progress checkpoint\n\n")
1749 out = self.gitOutput.readline()
1750 if self.verbose:
1751 print "checkpoint finished: " + out
1753 def extractFilesFromCommit(self, commit):
1754 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1755 for path in self.cloneExclude]
1756 files = []
1757 fnum = 0
1758 while commit.has_key("depotFile%s" % fnum):
1759 path = commit["depotFile%s" % fnum]
1761 if [p for p in self.cloneExclude
1762 if p4PathStartsWith(path, p)]:
1763 found = False
1764 else:
1765 found = [p for p in self.depotPaths
1766 if p4PathStartsWith(path, p)]
1767 if not found:
1768 fnum = fnum + 1
1769 continue
1771 file = {}
1772 file["path"] = path
1773 file["rev"] = commit["rev%s" % fnum]
1774 file["action"] = commit["action%s" % fnum]
1775 file["type"] = commit["type%s" % fnum]
1776 files.append(file)
1777 fnum = fnum + 1
1778 return files
1780 def stripRepoPath(self, path, prefixes):
1781 if self.useClientSpec:
1782 return self.clientSpecDirs.map_in_client(path)
1784 if self.keepRepoPath:
1785 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1787 for p in prefixes:
1788 if p4PathStartsWith(path, p):
1789 path = path[len(p):]
1791 return path
1793 def splitFilesIntoBranches(self, commit):
1794 branches = {}
1795 fnum = 0
1796 while commit.has_key("depotFile%s" % fnum):
1797 path = commit["depotFile%s" % fnum]
1798 found = [p for p in self.depotPaths
1799 if p4PathStartsWith(path, p)]
1800 if not found:
1801 fnum = fnum + 1
1802 continue
1804 file = {}
1805 file["path"] = path
1806 file["rev"] = commit["rev%s" % fnum]
1807 file["action"] = commit["action%s" % fnum]
1808 file["type"] = commit["type%s" % fnum]
1809 fnum = fnum + 1
1811 relPath = self.stripRepoPath(path, self.depotPaths)
1812 relPath = wildcard_decode(relPath)
1814 for branch in self.knownBranches.keys():
1816 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1817 if relPath.startswith(branch + "/"):
1818 if branch not in branches:
1819 branches[branch] = []
1820 branches[branch].append(file)
1821 break
1823 return branches
1825 # output one file from the P4 stream
1826 # - helper for streamP4Files
1828 def streamOneP4File(self, file, contents):
1829 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1830 relPath = wildcard_decode(relPath)
1831 if verbose:
1832 sys.stderr.write("%s\n" % relPath)
1834 (type_base, type_mods) = split_p4_type(file["type"])
1836 git_mode = "100644"
1837 if "x" in type_mods:
1838 git_mode = "100755"
1839 if type_base == "symlink":
1840 git_mode = "120000"
1841 # p4 print on a symlink contains "target\n"; remove the newline
1842 data = ''.join(contents)
1843 contents = [data[:-1]]
1845 if type_base == "utf16":
1846 # p4 delivers different text in the python output to -G
1847 # than it does when using "print -o", or normal p4 client
1848 # operations. utf16 is converted to ascii or utf8, perhaps.
1849 # But ascii text saved as -t utf16 is completely mangled.
1850 # Invoke print -o to get the real contents.
1851 text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
1852 contents = [ text ]
1854 if type_base == "apple":
1855 # Apple filetype files will be streamed as a concatenation of
1856 # its appledouble header and the contents. This is useless
1857 # on both macs and non-macs. If using "print -q -o xx", it
1858 # will create "xx" with the data, and "%xx" with the header.
1859 # This is also not very useful.
1861 # Ideally, someday, this script can learn how to generate
1862 # appledouble files directly and import those to git, but
1863 # non-mac machines can never find a use for apple filetype.
1864 print "\nIgnoring apple filetype file %s" % file['depotFile']
1865 return
1867 # Perhaps windows wants unicode, utf16 newlines translated too;
1868 # but this is not doing it.
1869 if self.isWindows and type_base == "text":
1870 mangled = []
1871 for data in contents:
1872 data = data.replace("\r\n", "\n")
1873 mangled.append(data)
1874 contents = mangled
1876 # Note that we do not try to de-mangle keywords on utf16 files,
1877 # even though in theory somebody may want that.
1878 pattern = p4_keywords_regexp_for_type(type_base, type_mods)
1879 if pattern:
1880 regexp = re.compile(pattern, re.VERBOSE)
1881 text = ''.join(contents)
1882 text = regexp.sub(r'$\1$', text)
1883 contents = [ text ]
1885 self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
1887 # total length...
1888 length = 0
1889 for d in contents:
1890 length = length + len(d)
1892 self.gitStream.write("data %d\n" % length)
1893 for d in contents:
1894 self.gitStream.write(d)
1895 self.gitStream.write("\n")
1897 def streamOneP4Deletion(self, file):
1898 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1899 relPath = wildcard_decode(relPath)
1900 if verbose:
1901 sys.stderr.write("delete %s\n" % relPath)
1902 self.gitStream.write("D %s\n" % relPath)
1904 # handle another chunk of streaming data
1905 def streamP4FilesCb(self, marshalled):
1907 if marshalled.has_key('depotFile') and self.stream_have_file_info:
1908 # start of a new file - output the old one first
1909 self.streamOneP4File(self.stream_file, self.stream_contents)
1910 self.stream_file = {}
1911 self.stream_contents = []
1912 self.stream_have_file_info = False
1914 # pick up the new file information... for the
1915 # 'data' field we need to append to our array
1916 for k in marshalled.keys():
1917 if k == 'data':
1918 self.stream_contents.append(marshalled['data'])
1919 else:
1920 self.stream_file[k] = marshalled[k]
1922 self.stream_have_file_info = True
1924 # Stream directly from "p4 files" into "git fast-import"
1925 def streamP4Files(self, files):
1926 filesForCommit = []
1927 filesToRead = []
1928 filesToDelete = []
1930 for f in files:
1931 # if using a client spec, only add the files that have
1932 # a path in the client
1933 if self.clientSpecDirs:
1934 if self.clientSpecDirs.map_in_client(f['path']) == "":
1935 continue
1937 filesForCommit.append(f)
1938 if f['action'] in self.delete_actions:
1939 filesToDelete.append(f)
1940 else:
1941 filesToRead.append(f)
1943 # deleted files...
1944 for f in filesToDelete:
1945 self.streamOneP4Deletion(f)
1947 if len(filesToRead) > 0:
1948 self.stream_file = {}
1949 self.stream_contents = []
1950 self.stream_have_file_info = False
1952 # curry self argument
1953 def streamP4FilesCbSelf(entry):
1954 self.streamP4FilesCb(entry)
1956 fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
1958 p4CmdList(["-x", "-", "print"],
1959 stdin=fileArgs,
1960 cb=streamP4FilesCbSelf)
1962 # do the last chunk
1963 if self.stream_file.has_key('depotFile'):
1964 self.streamOneP4File(self.stream_file, self.stream_contents)
1966 def make_email(self, userid):
1967 if userid in self.users:
1968 return self.users[userid]
1969 else:
1970 return "%s <a@b>" % userid
1972 # Stream a p4 tag
1973 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
1974 if verbose:
1975 print "writing tag %s for commit %s" % (labelName, commit)
1976 gitStream.write("tag %s\n" % labelName)
1977 gitStream.write("from %s\n" % commit)
1979 if labelDetails.has_key('Owner'):
1980 owner = labelDetails["Owner"]
1981 else:
1982 owner = None
1984 # Try to use the owner of the p4 label, or failing that,
1985 # the current p4 user id.
1986 if owner:
1987 email = self.make_email(owner)
1988 else:
1989 email = self.make_email(self.p4UserId())
1990 tagger = "%s %s %s" % (email, epoch, self.tz)
1992 gitStream.write("tagger %s\n" % tagger)
1994 print "labelDetails=",labelDetails
1995 if labelDetails.has_key('Description'):
1996 description = labelDetails['Description']
1997 else:
1998 description = 'Label from git p4'
2000 gitStream.write("data %d\n" % len(description))
2001 gitStream.write(description)
2002 gitStream.write("\n")
2004 def commit(self, details, files, branch, branchPrefixes, parent = ""):
2005 epoch = details["time"]
2006 author = details["user"]
2007 self.branchPrefixes = branchPrefixes
2009 if self.verbose:
2010 print "commit into %s" % branch
2012 # start with reading files; if that fails, we should not
2013 # create a commit.
2014 new_files = []
2015 for f in files:
2016 if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
2017 new_files.append (f)
2018 else:
2019 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2021 self.gitStream.write("commit %s\n" % branch)
2022 # gitStream.write("mark :%s\n" % details["change"])
2023 self.committedChanges.add(int(details["change"]))
2024 committer = ""
2025 if author not in self.users:
2026 self.getUserMapFromPerforceServer()
2027 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2029 self.gitStream.write("committer %s\n" % committer)
2031 self.gitStream.write("data <<EOT\n")
2032 self.gitStream.write(details["desc"])
2033 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
2034 % (','.join (branchPrefixes), details["change"]))
2035 if len(details['options']) > 0:
2036 self.gitStream.write(": options = %s" % details['options'])
2037 self.gitStream.write("]\nEOT\n\n")
2039 if len(parent) > 0:
2040 if self.verbose:
2041 print "parent %s" % parent
2042 self.gitStream.write("from %s\n" % parent)
2044 self.streamP4Files(new_files)
2045 self.gitStream.write("\n")
2047 change = int(details["change"])
2049 if self.labels.has_key(change):
2050 label = self.labels[change]
2051 labelDetails = label[0]
2052 labelRevisions = label[1]
2053 if self.verbose:
2054 print "Change %s is labelled %s" % (change, labelDetails)
2056 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2057 for p in branchPrefixes])
2059 if len(files) == len(labelRevisions):
2061 cleanedFiles = {}
2062 for info in files:
2063 if info["action"] in self.delete_actions:
2064 continue
2065 cleanedFiles[info["depotFile"]] = info["rev"]
2067 if cleanedFiles == labelRevisions:
2068 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2070 else:
2071 if not self.silent:
2072 print ("Tag %s does not match with change %s: files do not match."
2073 % (labelDetails["label"], change))
2075 else:
2076 if not self.silent:
2077 print ("Tag %s does not match with change %s: file count is different."
2078 % (labelDetails["label"], change))
2080 # Build a dictionary of changelists and labels, for "detect-labels" option.
2081 def getLabels(self):
2082 self.labels = {}
2084 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2085 if len(l) > 0 and not self.silent:
2086 print "Finding files belonging to labels in %s" % `self.depotPaths`
2088 for output in l:
2089 label = output["label"]
2090 revisions = {}
2091 newestChange = 0
2092 if self.verbose:
2093 print "Querying files for label %s" % label
2094 for file in p4CmdList(["files"] +
2095 ["%s...@%s" % (p, label)
2096 for p in self.depotPaths]):
2097 revisions[file["depotFile"]] = file["rev"]
2098 change = int(file["change"])
2099 if change > newestChange:
2100 newestChange = change
2102 self.labels[newestChange] = [output, revisions]
2104 if self.verbose:
2105 print "Label changes: %s" % self.labels.keys()
2107 # Import p4 labels as git tags. A direct mapping does not
2108 # exist, so assume that if all the files are at the same revision
2109 # then we can use that, or it's something more complicated we should
2110 # just ignore.
2111 def importP4Labels(self, stream, p4Labels):
2112 if verbose:
2113 print "import p4 labels: " + ' '.join(p4Labels)
2115 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2116 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2117 if len(validLabelRegexp) == 0:
2118 validLabelRegexp = defaultLabelRegexp
2119 m = re.compile(validLabelRegexp)
2121 for name in p4Labels:
2122 commitFound = False
2124 if not m.match(name):
2125 if verbose:
2126 print "label %s does not match regexp %s" % (name,validLabelRegexp)
2127 continue
2129 if name in ignoredP4Labels:
2130 continue
2132 labelDetails = p4CmdList(['label', "-o", name])[0]
2134 # get the most recent changelist for each file in this label
2135 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2136 for p in self.depotPaths])
2138 if change.has_key('change'):
2139 # find the corresponding git commit; take the oldest commit
2140 changelist = int(change['change'])
2141 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2142 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2143 if len(gitCommit) == 0:
2144 print "could not find git commit for changelist %d" % changelist
2145 else:
2146 gitCommit = gitCommit.strip()
2147 commitFound = True
2148 # Convert from p4 time format
2149 try:
2150 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2151 except ValueError:
2152 print "Could not convert label time %s" % labelDetail['Update']
2153 tmwhen = 1
2155 when = int(time.mktime(tmwhen))
2156 self.streamTag(stream, name, labelDetails, gitCommit, when)
2157 if verbose:
2158 print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2159 else:
2160 if verbose:
2161 print "Label %s has no changelists - possibly deleted?" % name
2163 if not commitFound:
2164 # We can't import this label; don't try again as it will get very
2165 # expensive repeatedly fetching all the files for labels that will
2166 # never be imported. If the label is moved in the future, the
2167 # ignore will need to be removed manually.
2168 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2170 def guessProjectName(self):
2171 for p in self.depotPaths:
2172 if p.endswith("/"):
2173 p = p[:-1]
2174 p = p[p.strip().rfind("/") + 1:]
2175 if not p.endswith("/"):
2176 p += "/"
2177 return p
2179 def getBranchMapping(self):
2180 lostAndFoundBranches = set()
2182 user = gitConfig("git-p4.branchUser")
2183 if len(user) > 0:
2184 command = "branches -u %s" % user
2185 else:
2186 command = "branches"
2188 for info in p4CmdList(command):
2189 details = p4Cmd(["branch", "-o", info["branch"]])
2190 viewIdx = 0
2191 while details.has_key("View%s" % viewIdx):
2192 paths = details["View%s" % viewIdx].split(" ")
2193 viewIdx = viewIdx + 1
2194 # require standard //depot/foo/... //depot/bar/... mapping
2195 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2196 continue
2197 source = paths[0]
2198 destination = paths[1]
2199 ## HACK
2200 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2201 source = source[len(self.depotPaths[0]):-4]
2202 destination = destination[len(self.depotPaths[0]):-4]
2204 if destination in self.knownBranches:
2205 if not self.silent:
2206 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2207 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2208 continue
2210 self.knownBranches[destination] = source
2212 lostAndFoundBranches.discard(destination)
2214 if source not in self.knownBranches:
2215 lostAndFoundBranches.add(source)
2217 # Perforce does not strictly require branches to be defined, so we also
2218 # check git config for a branch list.
2220 # Example of branch definition in git config file:
2221 # [git-p4]
2222 # branchList=main:branchA
2223 # branchList=main:branchB
2224 # branchList=branchA:branchC
2225 configBranches = gitConfigList("git-p4.branchList")
2226 for branch in configBranches:
2227 if branch:
2228 (source, destination) = branch.split(":")
2229 self.knownBranches[destination] = source
2231 lostAndFoundBranches.discard(destination)
2233 if source not in self.knownBranches:
2234 lostAndFoundBranches.add(source)
2237 for branch in lostAndFoundBranches:
2238 self.knownBranches[branch] = branch
2240 def getBranchMappingFromGitBranches(self):
2241 branches = p4BranchesInGit(self.importIntoRemotes)
2242 for branch in branches.keys():
2243 if branch == "master":
2244 branch = "main"
2245 else:
2246 branch = branch[len(self.projectName):]
2247 self.knownBranches[branch] = branch
2249 def listExistingP4GitBranches(self):
2250 # branches holds mapping from name to commit
2251 branches = p4BranchesInGit(self.importIntoRemotes)
2252 self.p4BranchesInGit = branches.keys()
2253 for branch in branches.keys():
2254 self.initialParents[self.refPrefix + branch] = branches[branch]
2256 def updateOptionDict(self, d):
2257 option_keys = {}
2258 if self.keepRepoPath:
2259 option_keys['keepRepoPath'] = 1
2261 d["options"] = ' '.join(sorted(option_keys.keys()))
2263 def readOptions(self, d):
2264 self.keepRepoPath = (d.has_key('options')
2265 and ('keepRepoPath' in d['options']))
2267 def gitRefForBranch(self, branch):
2268 if branch == "main":
2269 return self.refPrefix + "master"
2271 if len(branch) <= 0:
2272 return branch
2274 return self.refPrefix + self.projectName + branch
2276 def gitCommitByP4Change(self, ref, change):
2277 if self.verbose:
2278 print "looking in ref " + ref + " for change %s using bisect..." % change
2280 earliestCommit = ""
2281 latestCommit = parseRevision(ref)
2283 while True:
2284 if self.verbose:
2285 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2286 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2287 if len(next) == 0:
2288 if self.verbose:
2289 print "argh"
2290 return ""
2291 log = extractLogMessageFromGitCommit(next)
2292 settings = extractSettingsGitLog(log)
2293 currentChange = int(settings['change'])
2294 if self.verbose:
2295 print "current change %s" % currentChange
2297 if currentChange == change:
2298 if self.verbose:
2299 print "found %s" % next
2300 return next
2302 if currentChange < change:
2303 earliestCommit = "^%s" % next
2304 else:
2305 latestCommit = "%s" % next
2307 return ""
2309 def importNewBranch(self, branch, maxChange):
2310 # make fast-import flush all changes to disk and update the refs using the checkpoint
2311 # command so that we can try to find the branch parent in the git history
2312 self.gitStream.write("checkpoint\n\n");
2313 self.gitStream.flush();
2314 branchPrefix = self.depotPaths[0] + branch + "/"
2315 range = "@1,%s" % maxChange
2316 #print "prefix" + branchPrefix
2317 changes = p4ChangesForPaths([branchPrefix], range)
2318 if len(changes) <= 0:
2319 return False
2320 firstChange = changes[0]
2321 #print "first change in branch: %s" % firstChange
2322 sourceBranch = self.knownBranches[branch]
2323 sourceDepotPath = self.depotPaths[0] + sourceBranch
2324 sourceRef = self.gitRefForBranch(sourceBranch)
2325 #print "source " + sourceBranch
2327 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2328 #print "branch parent: %s" % branchParentChange
2329 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2330 if len(gitParent) > 0:
2331 self.initialParents[self.gitRefForBranch(branch)] = gitParent
2332 #print "parent git commit: %s" % gitParent
2334 self.importChanges(changes)
2335 return True
2337 def searchParent(self, parent, branch, target):
2338 parentFound = False
2339 for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]):
2340 blob = blob.strip()
2341 if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2342 parentFound = True
2343 if self.verbose:
2344 print "Found parent of %s in commit %s" % (branch, blob)
2345 break
2346 if parentFound:
2347 return blob
2348 else:
2349 return None
2351 def importChanges(self, changes):
2352 cnt = 1
2353 for change in changes:
2354 description = p4Cmd(["describe", str(change)])
2355 self.updateOptionDict(description)
2357 if not self.silent:
2358 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2359 sys.stdout.flush()
2360 cnt = cnt + 1
2362 try:
2363 if self.detectBranches:
2364 branches = self.splitFilesIntoBranches(description)
2365 for branch in branches.keys():
2366 ## HACK --hwn
2367 branchPrefix = self.depotPaths[0] + branch + "/"
2369 parent = ""
2371 filesForCommit = branches[branch]
2373 if self.verbose:
2374 print "branch is %s" % branch
2376 self.updatedBranches.add(branch)
2378 if branch not in self.createdBranches:
2379 self.createdBranches.add(branch)
2380 parent = self.knownBranches[branch]
2381 if parent == branch:
2382 parent = ""
2383 else:
2384 fullBranch = self.projectName + branch
2385 if fullBranch not in self.p4BranchesInGit:
2386 if not self.silent:
2387 print("\n Importing new branch %s" % fullBranch);
2388 if self.importNewBranch(branch, change - 1):
2389 parent = ""
2390 self.p4BranchesInGit.append(fullBranch)
2391 if not self.silent:
2392 print("\n Resuming with change %s" % change);
2394 if self.verbose:
2395 print "parent determined through known branches: %s" % parent
2397 branch = self.gitRefForBranch(branch)
2398 parent = self.gitRefForBranch(parent)
2400 if self.verbose:
2401 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2403 if len(parent) == 0 and branch in self.initialParents:
2404 parent = self.initialParents[branch]
2405 del self.initialParents[branch]
2407 blob = None
2408 if len(parent) > 0:
2409 tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change))
2410 if self.verbose:
2411 print "Creating temporary branch: " + tempBranch
2412 self.commit(description, filesForCommit, tempBranch, [branchPrefix])
2413 self.tempBranches.append(tempBranch)
2414 self.checkpoint()
2415 blob = self.searchParent(parent, branch, tempBranch)
2416 if blob:
2417 self.commit(description, filesForCommit, branch, [branchPrefix], blob)
2418 else:
2419 if self.verbose:
2420 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2421 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
2422 else:
2423 files = self.extractFilesFromCommit(description)
2424 self.commit(description, files, self.branch, self.depotPaths,
2425 self.initialParent)
2426 self.initialParent = ""
2427 except IOError:
2428 print self.gitError.read()
2429 sys.exit(1)
2431 def importHeadRevision(self, revision):
2432 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2434 details = {}
2435 details["user"] = "git perforce import user"
2436 details["desc"] = ("Initial import of %s from the state at revision %s\n"
2437 % (' '.join(self.depotPaths), revision))
2438 details["change"] = revision
2439 newestRevision = 0
2441 fileCnt = 0
2442 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2444 for info in p4CmdList(["files"] + fileArgs):
2446 if 'code' in info and info['code'] == 'error':
2447 sys.stderr.write("p4 returned an error: %s\n"
2448 % info['data'])
2449 if info['data'].find("must refer to client") >= 0:
2450 sys.stderr.write("This particular p4 error is misleading.\n")
2451 sys.stderr.write("Perhaps the depot path was misspelled.\n");
2452 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
2453 sys.exit(1)
2454 if 'p4ExitCode' in info:
2455 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2456 sys.exit(1)
2459 change = int(info["change"])
2460 if change > newestRevision:
2461 newestRevision = change
2463 if info["action"] in self.delete_actions:
2464 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2465 #fileCnt = fileCnt + 1
2466 continue
2468 for prop in ["depotFile", "rev", "action", "type" ]:
2469 details["%s%s" % (prop, fileCnt)] = info[prop]
2471 fileCnt = fileCnt + 1
2473 details["change"] = newestRevision
2475 # Use time from top-most change so that all git p4 clones of
2476 # the same p4 repo have the same commit SHA1s.
2477 res = p4CmdList("describe -s %d" % newestRevision)
2478 newestTime = None
2479 for r in res:
2480 if r.has_key('time'):
2481 newestTime = int(r['time'])
2482 if newestTime is None:
2483 die("\"describe -s\" on newest change %d did not give a time")
2484 details["time"] = newestTime
2486 self.updateOptionDict(details)
2487 try:
2488 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
2489 except IOError:
2490 print "IO error with git fast-import. Is your git version recent enough?"
2491 print self.gitError.read()
2494 def run(self, args):
2495 self.depotPaths = []
2496 self.changeRange = ""
2497 self.initialParent = ""
2498 self.previousDepotPaths = []
2500 # map from branch depot path to parent branch
2501 self.knownBranches = {}
2502 self.initialParents = {}
2503 self.hasOrigin = originP4BranchesExist()
2504 if not self.syncWithOrigin:
2505 self.hasOrigin = False
2507 if self.importIntoRemotes:
2508 self.refPrefix = "refs/remotes/p4/"
2509 else:
2510 self.refPrefix = "refs/heads/p4/"
2512 if self.syncWithOrigin and self.hasOrigin:
2513 if not self.silent:
2514 print "Syncing with origin first by calling git fetch origin"
2515 system("git fetch origin")
2517 if len(self.branch) == 0:
2518 self.branch = self.refPrefix + "master"
2519 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2520 system("git update-ref %s refs/heads/p4" % self.branch)
2521 system("git branch -D p4");
2522 # create it /after/ importing, when master exists
2523 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
2524 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
2526 # accept either the command-line option, or the configuration variable
2527 if self.useClientSpec:
2528 # will use this after clone to set the variable
2529 self.useClientSpec_from_options = True
2530 else:
2531 if gitConfig("git-p4.useclientspec", "--bool") == "true":
2532 self.useClientSpec = True
2533 if self.useClientSpec:
2534 self.clientSpecDirs = getClientSpec()
2536 # TODO: should always look at previous commits,
2537 # merge with previous imports, if possible.
2538 if args == []:
2539 if self.hasOrigin:
2540 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2541 self.listExistingP4GitBranches()
2543 if len(self.p4BranchesInGit) > 1:
2544 if not self.silent:
2545 print "Importing from/into multiple branches"
2546 self.detectBranches = True
2548 if self.verbose:
2549 print "branches: %s" % self.p4BranchesInGit
2551 p4Change = 0
2552 for branch in self.p4BranchesInGit:
2553 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
2555 settings = extractSettingsGitLog(logMsg)
2557 self.readOptions(settings)
2558 if (settings.has_key('depot-paths')
2559 and settings.has_key ('change')):
2560 change = int(settings['change']) + 1
2561 p4Change = max(p4Change, change)
2563 depotPaths = sorted(settings['depot-paths'])
2564 if self.previousDepotPaths == []:
2565 self.previousDepotPaths = depotPaths
2566 else:
2567 paths = []
2568 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2569 prev_list = prev.split("/")
2570 cur_list = cur.split("/")
2571 for i in range(0, min(len(cur_list), len(prev_list))):
2572 if cur_list[i] <> prev_list[i]:
2573 i = i - 1
2574 break
2576 paths.append ("/".join(cur_list[:i + 1]))
2578 self.previousDepotPaths = paths
2580 if p4Change > 0:
2581 self.depotPaths = sorted(self.previousDepotPaths)
2582 self.changeRange = "@%s,#head" % p4Change
2583 if not self.detectBranches:
2584 self.initialParent = parseRevision(self.branch)
2585 if not self.silent and not self.detectBranches:
2586 print "Performing incremental import into %s git branch" % self.branch
2588 if not self.branch.startswith("refs/"):
2589 self.branch = "refs/heads/" + self.branch
2591 if len(args) == 0 and self.depotPaths:
2592 if not self.silent:
2593 print "Depot paths: %s" % ' '.join(self.depotPaths)
2594 else:
2595 if self.depotPaths and self.depotPaths != args:
2596 print ("previous import used depot path %s and now %s was specified. "
2597 "This doesn't work!" % (' '.join (self.depotPaths),
2598 ' '.join (args)))
2599 sys.exit(1)
2601 self.depotPaths = sorted(args)
2603 revision = ""
2604 self.users = {}
2606 # Make sure no revision specifiers are used when --changesfile
2607 # is specified.
2608 bad_changesfile = False
2609 if len(self.changesFile) > 0:
2610 for p in self.depotPaths:
2611 if p.find("@") >= 0 or p.find("#") >= 0:
2612 bad_changesfile = True
2613 break
2614 if bad_changesfile:
2615 die("Option --changesfile is incompatible with revision specifiers")
2617 newPaths = []
2618 for p in self.depotPaths:
2619 if p.find("@") != -1:
2620 atIdx = p.index("@")
2621 self.changeRange = p[atIdx:]
2622 if self.changeRange == "@all":
2623 self.changeRange = ""
2624 elif ',' not in self.changeRange:
2625 revision = self.changeRange
2626 self.changeRange = ""
2627 p = p[:atIdx]
2628 elif p.find("#") != -1:
2629 hashIdx = p.index("#")
2630 revision = p[hashIdx:]
2631 p = p[:hashIdx]
2632 elif self.previousDepotPaths == []:
2633 # pay attention to changesfile, if given, else import
2634 # the entire p4 tree at the head revision
2635 if len(self.changesFile) == 0:
2636 revision = "#head"
2638 p = re.sub ("\.\.\.$", "", p)
2639 if not p.endswith("/"):
2640 p += "/"
2642 newPaths.append(p)
2644 self.depotPaths = newPaths
2646 self.loadUserMapFromCache()
2647 self.labels = {}
2648 if self.detectLabels:
2649 self.getLabels();
2651 if self.detectBranches:
2652 ## FIXME - what's a P4 projectName ?
2653 self.projectName = self.guessProjectName()
2655 if self.hasOrigin:
2656 self.getBranchMappingFromGitBranches()
2657 else:
2658 self.getBranchMapping()
2659 if self.verbose:
2660 print "p4-git branches: %s" % self.p4BranchesInGit
2661 print "initial parents: %s" % self.initialParents
2662 for b in self.p4BranchesInGit:
2663 if b != "master":
2665 ## FIXME
2666 b = b[len(self.projectName):]
2667 self.createdBranches.add(b)
2669 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2671 importProcess = subprocess.Popen(["git", "fast-import"],
2672 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2673 stderr=subprocess.PIPE);
2674 self.gitOutput = importProcess.stdout
2675 self.gitStream = importProcess.stdin
2676 self.gitError = importProcess.stderr
2678 if revision:
2679 self.importHeadRevision(revision)
2680 else:
2681 changes = []
2683 if len(self.changesFile) > 0:
2684 output = open(self.changesFile).readlines()
2685 changeSet = set()
2686 for line in output:
2687 changeSet.add(int(line))
2689 for change in changeSet:
2690 changes.append(change)
2692 changes.sort()
2693 else:
2694 # catch "git p4 sync" with no new branches, in a repo that
2695 # does not have any existing p4 branches
2696 if len(args) == 0 and not self.p4BranchesInGit:
2697 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
2698 if self.verbose:
2699 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2700 self.changeRange)
2701 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2703 if len(self.maxChanges) > 0:
2704 changes = changes[:min(int(self.maxChanges), len(changes))]
2706 if len(changes) == 0:
2707 if not self.silent:
2708 print "No changes to import!"
2709 else:
2710 if not self.silent and not self.detectBranches:
2711 print "Import destination: %s" % self.branch
2713 self.updatedBranches = set()
2715 self.importChanges(changes)
2717 if not self.silent:
2718 print ""
2719 if len(self.updatedBranches) > 0:
2720 sys.stdout.write("Updated branches: ")
2721 for b in self.updatedBranches:
2722 sys.stdout.write("%s " % b)
2723 sys.stdout.write("\n")
2725 if gitConfig("git-p4.importLabels", "--bool") == "true":
2726 self.importLabels = True
2728 if self.importLabels:
2729 p4Labels = getP4Labels(self.depotPaths)
2730 gitTags = getGitTags()
2732 missingP4Labels = p4Labels - gitTags
2733 self.importP4Labels(self.gitStream, missingP4Labels)
2735 self.gitStream.close()
2736 if importProcess.wait() != 0:
2737 die("fast-import failed: %s" % self.gitError.read())
2738 self.gitOutput.close()
2739 self.gitError.close()
2741 # Cleanup temporary branches created during import
2742 if self.tempBranches != []:
2743 for branch in self.tempBranches:
2744 read_pipe("git update-ref -d %s" % branch)
2745 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
2747 return True
2749 class P4Rebase(Command):
2750 def __init__(self):
2751 Command.__init__(self)
2752 self.options = [
2753 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2755 self.importLabels = False
2756 self.description = ("Fetches the latest revision from perforce and "
2757 + "rebases the current work (branch) against it")
2759 def run(self, args):
2760 sync = P4Sync()
2761 sync.importLabels = self.importLabels
2762 sync.run([])
2764 return self.rebase()
2766 def rebase(self):
2767 if os.system("git update-index --refresh") != 0:
2768 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.");
2769 if len(read_pipe("git diff-index HEAD --")) > 0:
2770 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2772 [upstream, settings] = findUpstreamBranchPoint()
2773 if len(upstream) == 0:
2774 die("Cannot find upstream branchpoint for rebase")
2776 # the branchpoint may be p4/foo~3, so strip off the parent
2777 upstream = re.sub("~[0-9]+$", "", upstream)
2779 print "Rebasing the current branch onto %s" % upstream
2780 oldHead = read_pipe("git rev-parse HEAD").strip()
2781 system("git rebase %s" % upstream)
2782 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
2783 return True
2785 class P4Clone(P4Sync):
2786 def __init__(self):
2787 P4Sync.__init__(self)
2788 self.description = "Creates a new git repository and imports from Perforce into it"
2789 self.usage = "usage: %prog [options] //depot/path[@revRange]"
2790 self.options += [
2791 optparse.make_option("--destination", dest="cloneDestination",
2792 action='store', default=None,
2793 help="where to leave result of the clone"),
2794 optparse.make_option("-/", dest="cloneExclude",
2795 action="append", type="string",
2796 help="exclude depot path"),
2797 optparse.make_option("--bare", dest="cloneBare",
2798 action="store_true", default=False),
2800 self.cloneDestination = None
2801 self.needsGit = False
2802 self.cloneBare = False
2804 # This is required for the "append" cloneExclude action
2805 def ensure_value(self, attr, value):
2806 if not hasattr(self, attr) or getattr(self, attr) is None:
2807 setattr(self, attr, value)
2808 return getattr(self, attr)
2810 def defaultDestination(self, args):
2811 ## TODO: use common prefix of args?
2812 depotPath = args[0]
2813 depotDir = re.sub("(@[^@]*)$", "", depotPath)
2814 depotDir = re.sub("(#[^#]*)$", "", depotDir)
2815 depotDir = re.sub(r"\.\.\.$", "", depotDir)
2816 depotDir = re.sub(r"/$", "", depotDir)
2817 return os.path.split(depotDir)[1]
2819 def run(self, args):
2820 if len(args) < 1:
2821 return False
2823 if self.keepRepoPath and not self.cloneDestination:
2824 sys.stderr.write("Must specify destination for --keep-path\n")
2825 sys.exit(1)
2827 depotPaths = args
2829 if not self.cloneDestination and len(depotPaths) > 1:
2830 self.cloneDestination = depotPaths[-1]
2831 depotPaths = depotPaths[:-1]
2833 self.cloneExclude = ["/"+p for p in self.cloneExclude]
2834 for p in depotPaths:
2835 if not p.startswith("//"):
2836 return False
2838 if not self.cloneDestination:
2839 self.cloneDestination = self.defaultDestination(args)
2841 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2843 if not os.path.exists(self.cloneDestination):
2844 os.makedirs(self.cloneDestination)
2845 chdir(self.cloneDestination)
2847 init_cmd = [ "git", "init" ]
2848 if self.cloneBare:
2849 init_cmd.append("--bare")
2850 subprocess.check_call(init_cmd)
2852 if not P4Sync.run(self, depotPaths):
2853 return False
2854 if self.branch != "master":
2855 if self.importIntoRemotes:
2856 masterbranch = "refs/remotes/p4/master"
2857 else:
2858 masterbranch = "refs/heads/p4/master"
2859 if gitBranchExists(masterbranch):
2860 system("git branch master %s" % masterbranch)
2861 if not self.cloneBare:
2862 system("git checkout -f")
2863 else:
2864 print "Could not detect main branch. No checkout/master branch created."
2866 # auto-set this variable if invoked with --use-client-spec
2867 if self.useClientSpec_from_options:
2868 system("git config --bool git-p4.useclientspec true")
2870 return True
2872 class P4Branches(Command):
2873 def __init__(self):
2874 Command.__init__(self)
2875 self.options = [ ]
2876 self.description = ("Shows the git branches that hold imports and their "
2877 + "corresponding perforce depot paths")
2878 self.verbose = False
2880 def run(self, args):
2881 if originP4BranchesExist():
2882 createOrUpdateBranchesFromOrigin()
2884 cmdline = "git rev-parse --symbolic "
2885 cmdline += " --remotes"
2887 for line in read_pipe_lines(cmdline):
2888 line = line.strip()
2890 if not line.startswith('p4/') or line == "p4/HEAD":
2891 continue
2892 branch = line
2894 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2895 settings = extractSettingsGitLog(log)
2897 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2898 return True
2900 class HelpFormatter(optparse.IndentedHelpFormatter):
2901 def __init__(self):
2902 optparse.IndentedHelpFormatter.__init__(self)
2904 def format_description(self, description):
2905 if description:
2906 return description + "\n"
2907 else:
2908 return ""
2910 def printUsage(commands):
2911 print "usage: %s <command> [options]" % sys.argv[0]
2912 print ""
2913 print "valid commands: %s" % ", ".join(commands)
2914 print ""
2915 print "Try %s <command> --help for command specific help." % sys.argv[0]
2916 print ""
2918 commands = {
2919 "debug" : P4Debug,
2920 "submit" : P4Submit,
2921 "commit" : P4Submit,
2922 "sync" : P4Sync,
2923 "rebase" : P4Rebase,
2924 "clone" : P4Clone,
2925 "rollback" : P4RollBack,
2926 "branches" : P4Branches
2930 def main():
2931 if len(sys.argv[1:]) == 0:
2932 printUsage(commands.keys())
2933 sys.exit(2)
2935 cmd = ""
2936 cmdName = sys.argv[1]
2937 try:
2938 klass = commands[cmdName]
2939 cmd = klass()
2940 except KeyError:
2941 print "unknown command %s" % cmdName
2942 print ""
2943 printUsage(commands.keys())
2944 sys.exit(2)
2946 options = cmd.options
2947 cmd.gitdir = os.environ.get("GIT_DIR", None)
2949 args = sys.argv[2:]
2951 options.append(optparse.make_option("--verbose", dest="verbose", action="store_true"))
2952 if cmd.needsGit:
2953 options.append(optparse.make_option("--git-dir", dest="gitdir"))
2955 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2956 options,
2957 description = cmd.description,
2958 formatter = HelpFormatter())
2960 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2961 global verbose
2962 verbose = cmd.verbose
2963 if cmd.needsGit:
2964 if cmd.gitdir == None:
2965 cmd.gitdir = os.path.abspath(".git")
2966 if not isValidGitDir(cmd.gitdir):
2967 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2968 if os.path.exists(cmd.gitdir):
2969 cdup = read_pipe("git rev-parse --show-cdup").strip()
2970 if len(cdup) > 0:
2971 chdir(cdup);
2973 if not isValidGitDir(cmd.gitdir):
2974 if isValidGitDir(cmd.gitdir + "/.git"):
2975 cmd.gitdir += "/.git"
2976 else:
2977 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2979 os.environ["GIT_DIR"] = cmd.gitdir
2981 if not cmd.run(args):
2982 parser.print_help()
2983 sys.exit(2)
2986 if __name__ == '__main__':
2987 main()