git p4: catch p4 describe errors
[git/gitweb.git] / git-p4.py
blobcd68e04a4551809527366bbbf9b7b71c064c2edf
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 p4_has_command(cmd):
124 """Ask p4 for help on this command. If it returns an error, the
125 command does not exist in this version of p4."""
126 real_cmd = p4_build_cmd(["help", cmd])
127 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
128 stderr=subprocess.PIPE)
129 p.communicate()
130 return p.returncode == 0
132 def system(cmd):
133 expand = isinstance(cmd,basestring)
134 if verbose:
135 sys.stderr.write("executing %s\n" % str(cmd))
136 subprocess.check_call(cmd, shell=expand)
138 def p4_system(cmd):
139 """Specifically invoke p4 as the system command. """
140 real_cmd = p4_build_cmd(cmd)
141 expand = isinstance(real_cmd, basestring)
142 subprocess.check_call(real_cmd, shell=expand)
144 def p4_integrate(src, dest):
145 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
147 def p4_sync(f, *options):
148 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
150 def p4_add(f):
151 # forcibly add file names with wildcards
152 if wildcard_present(f):
153 p4_system(["add", "-f", f])
154 else:
155 p4_system(["add", f])
157 def p4_delete(f):
158 p4_system(["delete", wildcard_encode(f)])
160 def p4_edit(f):
161 p4_system(["edit", wildcard_encode(f)])
163 def p4_revert(f):
164 p4_system(["revert", wildcard_encode(f)])
166 def p4_reopen(type, f):
167 p4_system(["reopen", "-t", type, wildcard_encode(f)])
169 def p4_move(src, dest):
170 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
172 def p4_describe(change):
173 """Make sure it returns a valid result by checking for
174 the presence of field "time". Return a dict of the
175 results."""
177 ds = p4CmdList(["describe", "-s", str(change)])
178 if len(ds) != 1:
179 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
181 d = ds[0]
183 if "p4ExitCode" in d:
184 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
185 str(d)))
186 if "code" in d:
187 if d["code"] == "error":
188 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
190 if "time" not in d:
191 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
193 return d
196 # Canonicalize the p4 type and return a tuple of the
197 # base type, plus any modifiers. See "p4 help filetypes"
198 # for a list and explanation.
200 def split_p4_type(p4type):
202 p4_filetypes_historical = {
203 "ctempobj": "binary+Sw",
204 "ctext": "text+C",
205 "cxtext": "text+Cx",
206 "ktext": "text+k",
207 "kxtext": "text+kx",
208 "ltext": "text+F",
209 "tempobj": "binary+FSw",
210 "ubinary": "binary+F",
211 "uresource": "resource+F",
212 "uxbinary": "binary+Fx",
213 "xbinary": "binary+x",
214 "xltext": "text+Fx",
215 "xtempobj": "binary+Swx",
216 "xtext": "text+x",
217 "xunicode": "unicode+x",
218 "xutf16": "utf16+x",
220 if p4type in p4_filetypes_historical:
221 p4type = p4_filetypes_historical[p4type]
222 mods = ""
223 s = p4type.split("+")
224 base = s[0]
225 mods = ""
226 if len(s) > 1:
227 mods = s[1]
228 return (base, mods)
231 # return the raw p4 type of a file (text, text+ko, etc)
233 def p4_type(file):
234 results = p4CmdList(["fstat", "-T", "headType", file])
235 return results[0]['headType']
238 # Given a type base and modifier, return a regexp matching
239 # the keywords that can be expanded in the file
241 def p4_keywords_regexp_for_type(base, type_mods):
242 if base in ("text", "unicode", "binary"):
243 kwords = None
244 if "ko" in type_mods:
245 kwords = 'Id|Header'
246 elif "k" in type_mods:
247 kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
248 else:
249 return None
250 pattern = r"""
251 \$ # Starts with a dollar, followed by...
252 (%s) # one of the keywords, followed by...
253 (:[^$\n]+)? # possibly an old expansion, followed by...
254 \$ # another dollar
255 """ % kwords
256 return pattern
257 else:
258 return None
261 # Given a file, return a regexp matching the possible
262 # RCS keywords that will be expanded, or None for files
263 # with kw expansion turned off.
265 def p4_keywords_regexp_for_file(file):
266 if not os.path.exists(file):
267 return None
268 else:
269 (type_base, type_mods) = split_p4_type(p4_type(file))
270 return p4_keywords_regexp_for_type(type_base, type_mods)
272 def setP4ExecBit(file, mode):
273 # Reopens an already open file and changes the execute bit to match
274 # the execute bit setting in the passed in mode.
276 p4Type = "+x"
278 if not isModeExec(mode):
279 p4Type = getP4OpenedType(file)
280 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
281 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
282 if p4Type[-1] == "+":
283 p4Type = p4Type[0:-1]
285 p4_reopen(p4Type, file)
287 def getP4OpenedType(file):
288 # Returns the perforce file type for the given file.
290 result = p4_read_pipe(["opened", wildcard_encode(file)])
291 match = re.match(".*\((.+)\)\r?$", result)
292 if match:
293 return match.group(1)
294 else:
295 die("Could not determine file type for %s (result: '%s')" % (file, result))
297 # Return the set of all p4 labels
298 def getP4Labels(depotPaths):
299 labels = set()
300 if isinstance(depotPaths,basestring):
301 depotPaths = [depotPaths]
303 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
304 label = l['label']
305 labels.add(label)
307 return labels
309 # Return the set of all git tags
310 def getGitTags():
311 gitTags = set()
312 for line in read_pipe_lines(["git", "tag"]):
313 tag = line.strip()
314 gitTags.add(tag)
315 return gitTags
317 def diffTreePattern():
318 # This is a simple generator for the diff tree regex pattern. This could be
319 # a class variable if this and parseDiffTreeEntry were a part of a class.
320 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
321 while True:
322 yield pattern
324 def parseDiffTreeEntry(entry):
325 """Parses a single diff tree entry into its component elements.
327 See git-diff-tree(1) manpage for details about the format of the diff
328 output. This method returns a dictionary with the following elements:
330 src_mode - The mode of the source file
331 dst_mode - The mode of the destination file
332 src_sha1 - The sha1 for the source file
333 dst_sha1 - The sha1 fr the destination file
334 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
335 status_score - The score for the status (applicable for 'C' and 'R'
336 statuses). This is None if there is no score.
337 src - The path for the source file.
338 dst - The path for the destination file. This is only present for
339 copy or renames. If it is not present, this is None.
341 If the pattern is not matched, None is returned."""
343 match = diffTreePattern().next().match(entry)
344 if match:
345 return {
346 'src_mode': match.group(1),
347 'dst_mode': match.group(2),
348 'src_sha1': match.group(3),
349 'dst_sha1': match.group(4),
350 'status': match.group(5),
351 'status_score': match.group(6),
352 'src': match.group(7),
353 'dst': match.group(10)
355 return None
357 def isModeExec(mode):
358 # Returns True if the given git mode represents an executable file,
359 # otherwise False.
360 return mode[-3:] == "755"
362 def isModeExecChanged(src_mode, dst_mode):
363 return isModeExec(src_mode) != isModeExec(dst_mode)
365 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
367 if isinstance(cmd,basestring):
368 cmd = "-G " + cmd
369 expand = True
370 else:
371 cmd = ["-G"] + cmd
372 expand = False
374 cmd = p4_build_cmd(cmd)
375 if verbose:
376 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
378 # Use a temporary file to avoid deadlocks without
379 # subprocess.communicate(), which would put another copy
380 # of stdout into memory.
381 stdin_file = None
382 if stdin is not None:
383 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
384 if isinstance(stdin,basestring):
385 stdin_file.write(stdin)
386 else:
387 for i in stdin:
388 stdin_file.write(i + '\n')
389 stdin_file.flush()
390 stdin_file.seek(0)
392 p4 = subprocess.Popen(cmd,
393 shell=expand,
394 stdin=stdin_file,
395 stdout=subprocess.PIPE)
397 result = []
398 try:
399 while True:
400 entry = marshal.load(p4.stdout)
401 if cb is not None:
402 cb(entry)
403 else:
404 result.append(entry)
405 except EOFError:
406 pass
407 exitCode = p4.wait()
408 if exitCode != 0:
409 entry = {}
410 entry["p4ExitCode"] = exitCode
411 result.append(entry)
413 return result
415 def p4Cmd(cmd):
416 list = p4CmdList(cmd)
417 result = {}
418 for entry in list:
419 result.update(entry)
420 return result;
422 def p4Where(depotPath):
423 if not depotPath.endswith("/"):
424 depotPath += "/"
425 depotPath = depotPath + "..."
426 outputList = p4CmdList(["where", depotPath])
427 output = None
428 for entry in outputList:
429 if "depotFile" in entry:
430 if entry["depotFile"] == depotPath:
431 output = entry
432 break
433 elif "data" in entry:
434 data = entry.get("data")
435 space = data.find(" ")
436 if data[:space] == depotPath:
437 output = entry
438 break
439 if output == None:
440 return ""
441 if output["code"] == "error":
442 return ""
443 clientPath = ""
444 if "path" in output:
445 clientPath = output.get("path")
446 elif "data" in output:
447 data = output.get("data")
448 lastSpace = data.rfind(" ")
449 clientPath = data[lastSpace + 1:]
451 if clientPath.endswith("..."):
452 clientPath = clientPath[:-3]
453 return clientPath
455 def currentGitBranch():
456 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
458 def isValidGitDir(path):
459 if (os.path.exists(path + "/HEAD")
460 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
461 return True;
462 return False
464 def parseRevision(ref):
465 return read_pipe("git rev-parse %s" % ref).strip()
467 def branchExists(ref):
468 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
469 ignore_error=True)
470 return len(rev) > 0
472 def extractLogMessageFromGitCommit(commit):
473 logMessage = ""
475 ## fixme: title is first line of commit, not 1st paragraph.
476 foundTitle = False
477 for log in read_pipe_lines("git cat-file commit %s" % commit):
478 if not foundTitle:
479 if len(log) == 1:
480 foundTitle = True
481 continue
483 logMessage += log
484 return logMessage
486 def extractSettingsGitLog(log):
487 values = {}
488 for line in log.split("\n"):
489 line = line.strip()
490 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
491 if not m:
492 continue
494 assignments = m.group(1).split (':')
495 for a in assignments:
496 vals = a.split ('=')
497 key = vals[0].strip()
498 val = ('='.join (vals[1:])).strip()
499 if val.endswith ('\"') and val.startswith('"'):
500 val = val[1:-1]
502 values[key] = val
504 paths = values.get("depot-paths")
505 if not paths:
506 paths = values.get("depot-path")
507 if paths:
508 values['depot-paths'] = paths.split(',')
509 return values
511 def gitBranchExists(branch):
512 proc = subprocess.Popen(["git", "rev-parse", branch],
513 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
514 return proc.wait() == 0;
516 _gitConfig = {}
517 def gitConfig(key, args = None): # set args to "--bool", for instance
518 if not _gitConfig.has_key(key):
519 argsFilter = ""
520 if args != None:
521 argsFilter = "%s " % args
522 cmd = "git config %s%s" % (argsFilter, key)
523 _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
524 return _gitConfig[key]
526 def gitConfigList(key):
527 if not _gitConfig.has_key(key):
528 _gitConfig[key] = read_pipe("git config --get-all %s" % key, ignore_error=True).strip().split(os.linesep)
529 return _gitConfig[key]
531 def p4BranchesInGit(branchesAreInRemotes = True):
532 branches = {}
534 cmdline = "git rev-parse --symbolic "
535 if branchesAreInRemotes:
536 cmdline += " --remotes"
537 else:
538 cmdline += " --branches"
540 for line in read_pipe_lines(cmdline):
541 line = line.strip()
543 ## only import to p4/
544 if not line.startswith('p4/') or line == "p4/HEAD":
545 continue
546 branch = line
548 # strip off p4
549 branch = re.sub ("^p4/", "", line)
551 branches[branch] = parseRevision(line)
552 return branches
554 def findUpstreamBranchPoint(head = "HEAD"):
555 branches = p4BranchesInGit()
556 # map from depot-path to branch name
557 branchByDepotPath = {}
558 for branch in branches.keys():
559 tip = branches[branch]
560 log = extractLogMessageFromGitCommit(tip)
561 settings = extractSettingsGitLog(log)
562 if settings.has_key("depot-paths"):
563 paths = ",".join(settings["depot-paths"])
564 branchByDepotPath[paths] = "remotes/p4/" + branch
566 settings = None
567 parent = 0
568 while parent < 65535:
569 commit = head + "~%s" % parent
570 log = extractLogMessageFromGitCommit(commit)
571 settings = extractSettingsGitLog(log)
572 if settings.has_key("depot-paths"):
573 paths = ",".join(settings["depot-paths"])
574 if branchByDepotPath.has_key(paths):
575 return [branchByDepotPath[paths], settings]
577 parent = parent + 1
579 return ["", settings]
581 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
582 if not silent:
583 print ("Creating/updating branch(es) in %s based on origin branch(es)"
584 % localRefPrefix)
586 originPrefix = "origin/p4/"
588 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
589 line = line.strip()
590 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
591 continue
593 headName = line[len(originPrefix):]
594 remoteHead = localRefPrefix + headName
595 originHead = line
597 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
598 if (not original.has_key('depot-paths')
599 or not original.has_key('change')):
600 continue
602 update = False
603 if not gitBranchExists(remoteHead):
604 if verbose:
605 print "creating %s" % remoteHead
606 update = True
607 else:
608 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
609 if settings.has_key('change') > 0:
610 if settings['depot-paths'] == original['depot-paths']:
611 originP4Change = int(original['change'])
612 p4Change = int(settings['change'])
613 if originP4Change > p4Change:
614 print ("%s (%s) is newer than %s (%s). "
615 "Updating p4 branch from origin."
616 % (originHead, originP4Change,
617 remoteHead, p4Change))
618 update = True
619 else:
620 print ("Ignoring: %s was imported from %s while "
621 "%s was imported from %s"
622 % (originHead, ','.join(original['depot-paths']),
623 remoteHead, ','.join(settings['depot-paths'])))
625 if update:
626 system("git update-ref %s %s" % (remoteHead, originHead))
628 def originP4BranchesExist():
629 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
631 def p4ChangesForPaths(depotPaths, changeRange):
632 assert depotPaths
633 cmd = ['changes']
634 for p in depotPaths:
635 cmd += ["%s...%s" % (p, changeRange)]
636 output = p4_read_pipe_lines(cmd)
638 changes = {}
639 for line in output:
640 changeNum = int(line.split(" ")[1])
641 changes[changeNum] = True
643 changelist = changes.keys()
644 changelist.sort()
645 return changelist
647 def p4PathStartsWith(path, prefix):
648 # This method tries to remedy a potential mixed-case issue:
650 # If UserA adds //depot/DirA/file1
651 # and UserB adds //depot/dira/file2
653 # we may or may not have a problem. If you have core.ignorecase=true,
654 # we treat DirA and dira as the same directory
655 ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
656 if ignorecase:
657 return path.lower().startswith(prefix.lower())
658 return path.startswith(prefix)
660 def getClientSpec():
661 """Look at the p4 client spec, create a View() object that contains
662 all the mappings, and return it."""
664 specList = p4CmdList("client -o")
665 if len(specList) != 1:
666 die('Output from "client -o" is %d lines, expecting 1' %
667 len(specList))
669 # dictionary of all client parameters
670 entry = specList[0]
672 # just the keys that start with "View"
673 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
675 # hold this new View
676 view = View()
678 # append the lines, in order, to the view
679 for view_num in range(len(view_keys)):
680 k = "View%d" % view_num
681 if k not in view_keys:
682 die("Expected view key %s missing" % k)
683 view.append(entry[k])
685 return view
687 def getClientRoot():
688 """Grab the client directory."""
690 output = p4CmdList("client -o")
691 if len(output) != 1:
692 die('Output from "client -o" is %d lines, expecting 1' % len(output))
694 entry = output[0]
695 if "Root" not in entry:
696 die('Client has no "Root"')
698 return entry["Root"]
701 # P4 wildcards are not allowed in filenames. P4 complains
702 # if you simply add them, but you can force it with "-f", in
703 # which case it translates them into %xx encoding internally.
705 def wildcard_decode(path):
706 # Search for and fix just these four characters. Do % last so
707 # that fixing it does not inadvertently create new %-escapes.
708 # Cannot have * in a filename in windows; untested as to
709 # what p4 would do in such a case.
710 if not platform.system() == "Windows":
711 path = path.replace("%2A", "*")
712 path = path.replace("%23", "#") \
713 .replace("%40", "@") \
714 .replace("%25", "%")
715 return path
717 def wildcard_encode(path):
718 # do % first to avoid double-encoding the %s introduced here
719 path = path.replace("%", "%25") \
720 .replace("*", "%2A") \
721 .replace("#", "%23") \
722 .replace("@", "%40")
723 return path
725 def wildcard_present(path):
726 return path.translate(None, "*#@%") != path
728 class Command:
729 def __init__(self):
730 self.usage = "usage: %prog [options]"
731 self.needsGit = True
732 self.verbose = False
734 class P4UserMap:
735 def __init__(self):
736 self.userMapFromPerforceServer = False
737 self.myP4UserId = None
739 def p4UserId(self):
740 if self.myP4UserId:
741 return self.myP4UserId
743 results = p4CmdList("user -o")
744 for r in results:
745 if r.has_key('User'):
746 self.myP4UserId = r['User']
747 return r['User']
748 die("Could not find your p4 user id")
750 def p4UserIsMe(self, p4User):
751 # return True if the given p4 user is actually me
752 me = self.p4UserId()
753 if not p4User or p4User != me:
754 return False
755 else:
756 return True
758 def getUserCacheFilename(self):
759 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
760 return home + "/.gitp4-usercache.txt"
762 def getUserMapFromPerforceServer(self):
763 if self.userMapFromPerforceServer:
764 return
765 self.users = {}
766 self.emails = {}
768 for output in p4CmdList("users"):
769 if not output.has_key("User"):
770 continue
771 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
772 self.emails[output["Email"]] = output["User"]
775 s = ''
776 for (key, val) in self.users.items():
777 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
779 open(self.getUserCacheFilename(), "wb").write(s)
780 self.userMapFromPerforceServer = True
782 def loadUserMapFromCache(self):
783 self.users = {}
784 self.userMapFromPerforceServer = False
785 try:
786 cache = open(self.getUserCacheFilename(), "rb")
787 lines = cache.readlines()
788 cache.close()
789 for line in lines:
790 entry = line.strip().split("\t")
791 self.users[entry[0]] = entry[1]
792 except IOError:
793 self.getUserMapFromPerforceServer()
795 class P4Debug(Command):
796 def __init__(self):
797 Command.__init__(self)
798 self.options = []
799 self.description = "A tool to debug the output of p4 -G."
800 self.needsGit = False
802 def run(self, args):
803 j = 0
804 for output in p4CmdList(args):
805 print 'Element: %d' % j
806 j += 1
807 print output
808 return True
810 class P4RollBack(Command):
811 def __init__(self):
812 Command.__init__(self)
813 self.options = [
814 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
816 self.description = "A tool to debug the multi-branch import. Don't use :)"
817 self.rollbackLocalBranches = False
819 def run(self, args):
820 if len(args) != 1:
821 return False
822 maxChange = int(args[0])
824 if "p4ExitCode" in p4Cmd("changes -m 1"):
825 die("Problems executing p4");
827 if self.rollbackLocalBranches:
828 refPrefix = "refs/heads/"
829 lines = read_pipe_lines("git rev-parse --symbolic --branches")
830 else:
831 refPrefix = "refs/remotes/"
832 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
834 for line in lines:
835 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
836 line = line.strip()
837 ref = refPrefix + line
838 log = extractLogMessageFromGitCommit(ref)
839 settings = extractSettingsGitLog(log)
841 depotPaths = settings['depot-paths']
842 change = settings['change']
844 changed = False
846 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
847 for p in depotPaths]))) == 0:
848 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
849 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
850 continue
852 while change and int(change) > maxChange:
853 changed = True
854 if self.verbose:
855 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
856 system("git update-ref %s \"%s^\"" % (ref, ref))
857 log = extractLogMessageFromGitCommit(ref)
858 settings = extractSettingsGitLog(log)
861 depotPaths = settings['depot-paths']
862 change = settings['change']
864 if changed:
865 print "%s rewound to %s" % (ref, change)
867 return True
869 class P4Submit(Command, P4UserMap):
871 conflict_behavior_choices = ("ask", "skip", "quit")
873 def __init__(self):
874 Command.__init__(self)
875 P4UserMap.__init__(self)
876 self.options = [
877 optparse.make_option("--origin", dest="origin"),
878 optparse.make_option("-M", dest="detectRenames", action="store_true"),
879 # preserve the user, requires relevant p4 permissions
880 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
881 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
882 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
883 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
884 optparse.make_option("--conflict", dest="conflict_behavior",
885 choices=self.conflict_behavior_choices)
887 self.description = "Submit changes from git to the perforce depot."
888 self.usage += " [name of git branch to submit into perforce depot]"
889 self.origin = ""
890 self.detectRenames = False
891 self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
892 self.dry_run = False
893 self.prepare_p4_only = False
894 self.conflict_behavior = None
895 self.isWindows = (platform.system() == "Windows")
896 self.exportLabels = False
897 self.p4HasMoveCommand = p4_has_command("move")
899 def check(self):
900 if len(p4CmdList("opened ...")) > 0:
901 die("You have files opened with perforce! Close them before starting the sync.")
903 def separate_jobs_from_description(self, message):
904 """Extract and return a possible Jobs field in the commit
905 message. It goes into a separate section in the p4 change
906 specification.
908 A jobs line starts with "Jobs:" and looks like a new field
909 in a form. Values are white-space separated on the same
910 line or on following lines that start with a tab.
912 This does not parse and extract the full git commit message
913 like a p4 form. It just sees the Jobs: line as a marker
914 to pass everything from then on directly into the p4 form,
915 but outside the description section.
917 Return a tuple (stripped log message, jobs string)."""
919 m = re.search(r'^Jobs:', message, re.MULTILINE)
920 if m is None:
921 return (message, None)
923 jobtext = message[m.start():]
924 stripped_message = message[:m.start()].rstrip()
925 return (stripped_message, jobtext)
927 def prepareLogMessage(self, template, message, jobs):
928 """Edits the template returned from "p4 change -o" to insert
929 the message in the Description field, and the jobs text in
930 the Jobs field."""
931 result = ""
933 inDescriptionSection = False
935 for line in template.split("\n"):
936 if line.startswith("#"):
937 result += line + "\n"
938 continue
940 if inDescriptionSection:
941 if line.startswith("Files:") or line.startswith("Jobs:"):
942 inDescriptionSection = False
943 # insert Jobs section
944 if jobs:
945 result += jobs + "\n"
946 else:
947 continue
948 else:
949 if line.startswith("Description:"):
950 inDescriptionSection = True
951 line += "\n"
952 for messageLine in message.split("\n"):
953 line += "\t" + messageLine + "\n"
955 result += line + "\n"
957 return result
959 def patchRCSKeywords(self, file, pattern):
960 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
961 (handle, outFileName) = tempfile.mkstemp(dir='.')
962 try:
963 outFile = os.fdopen(handle, "w+")
964 inFile = open(file, "r")
965 regexp = re.compile(pattern, re.VERBOSE)
966 for line in inFile.readlines():
967 line = regexp.sub(r'$\1$', line)
968 outFile.write(line)
969 inFile.close()
970 outFile.close()
971 # Forcibly overwrite the original file
972 os.unlink(file)
973 shutil.move(outFileName, file)
974 except:
975 # cleanup our temporary file
976 os.unlink(outFileName)
977 print "Failed to strip RCS keywords in %s" % file
978 raise
980 print "Patched up RCS keywords in %s" % file
982 def p4UserForCommit(self,id):
983 # Return the tuple (perforce user,git email) for a given git commit id
984 self.getUserMapFromPerforceServer()
985 gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
986 gitEmail = gitEmail.strip()
987 if not self.emails.has_key(gitEmail):
988 return (None,gitEmail)
989 else:
990 return (self.emails[gitEmail],gitEmail)
992 def checkValidP4Users(self,commits):
993 # check if any git authors cannot be mapped to p4 users
994 for id in commits:
995 (user,email) = self.p4UserForCommit(id)
996 if not user:
997 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
998 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
999 print "%s" % msg
1000 else:
1001 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1003 def lastP4Changelist(self):
1004 # Get back the last changelist number submitted in this client spec. This
1005 # then gets used to patch up the username in the change. If the same
1006 # client spec is being used by multiple processes then this might go
1007 # wrong.
1008 results = p4CmdList("client -o") # find the current client
1009 client = None
1010 for r in results:
1011 if r.has_key('Client'):
1012 client = r['Client']
1013 break
1014 if not client:
1015 die("could not get client spec")
1016 results = p4CmdList(["changes", "-c", client, "-m", "1"])
1017 for r in results:
1018 if r.has_key('change'):
1019 return r['change']
1020 die("Could not get changelist number for last submit - cannot patch up user details")
1022 def modifyChangelistUser(self, changelist, newUser):
1023 # fixup the user field of a changelist after it has been submitted.
1024 changes = p4CmdList("change -o %s" % changelist)
1025 if len(changes) != 1:
1026 die("Bad output from p4 change modifying %s to user %s" %
1027 (changelist, newUser))
1029 c = changes[0]
1030 if c['User'] == newUser: return # nothing to do
1031 c['User'] = newUser
1032 input = marshal.dumps(c)
1034 result = p4CmdList("change -f -i", stdin=input)
1035 for r in result:
1036 if r.has_key('code'):
1037 if r['code'] == 'error':
1038 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1039 if r.has_key('data'):
1040 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1041 return
1042 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1044 def canChangeChangelists(self):
1045 # check to see if we have p4 admin or super-user permissions, either of
1046 # which are required to modify changelists.
1047 results = p4CmdList(["protects", self.depotPath])
1048 for r in results:
1049 if r.has_key('perm'):
1050 if r['perm'] == 'admin':
1051 return 1
1052 if r['perm'] == 'super':
1053 return 1
1054 return 0
1056 def prepareSubmitTemplate(self):
1057 """Run "p4 change -o" to grab a change specification template.
1058 This does not use "p4 -G", as it is nice to keep the submission
1059 template in original order, since a human might edit it.
1061 Remove lines in the Files section that show changes to files
1062 outside the depot path we're committing into."""
1064 template = ""
1065 inFilesSection = False
1066 for line in p4_read_pipe_lines(['change', '-o']):
1067 if line.endswith("\r\n"):
1068 line = line[:-2] + "\n"
1069 if inFilesSection:
1070 if line.startswith("\t"):
1071 # path starts and ends with a tab
1072 path = line[1:]
1073 lastTab = path.rfind("\t")
1074 if lastTab != -1:
1075 path = path[:lastTab]
1076 if not p4PathStartsWith(path, self.depotPath):
1077 continue
1078 else:
1079 inFilesSection = False
1080 else:
1081 if line.startswith("Files:"):
1082 inFilesSection = True
1084 template += line
1086 return template
1088 def edit_template(self, template_file):
1089 """Invoke the editor to let the user change the submission
1090 message. Return true if okay to continue with the submit."""
1092 # if configured to skip the editing part, just submit
1093 if gitConfig("git-p4.skipSubmitEdit") == "true":
1094 return True
1096 # look at the modification time, to check later if the user saved
1097 # the file
1098 mtime = os.stat(template_file).st_mtime
1100 # invoke the editor
1101 if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1102 editor = os.environ.get("P4EDITOR")
1103 else:
1104 editor = read_pipe("git var GIT_EDITOR").strip()
1105 system(editor + " " + template_file)
1107 # If the file was not saved, prompt to see if this patch should
1108 # be skipped. But skip this verification step if configured so.
1109 if gitConfig("git-p4.skipSubmitEditCheck") == "true":
1110 return True
1112 # modification time updated means user saved the file
1113 if os.stat(template_file).st_mtime > mtime:
1114 return True
1116 while True:
1117 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1118 if response == 'y':
1119 return True
1120 if response == 'n':
1121 return False
1123 def applyCommit(self, id):
1124 """Apply one commit, return True if it succeeded."""
1126 print "Applying", read_pipe(["git", "show", "-s",
1127 "--format=format:%h %s", id])
1129 (p4User, gitEmail) = self.p4UserForCommit(id)
1131 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1132 filesToAdd = set()
1133 filesToDelete = set()
1134 editedFiles = set()
1135 pureRenameCopy = set()
1136 filesToChangeExecBit = {}
1138 for line in diff:
1139 diff = parseDiffTreeEntry(line)
1140 modifier = diff['status']
1141 path = diff['src']
1142 if modifier == "M":
1143 p4_edit(path)
1144 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1145 filesToChangeExecBit[path] = diff['dst_mode']
1146 editedFiles.add(path)
1147 elif modifier == "A":
1148 filesToAdd.add(path)
1149 filesToChangeExecBit[path] = diff['dst_mode']
1150 if path in filesToDelete:
1151 filesToDelete.remove(path)
1152 elif modifier == "D":
1153 filesToDelete.add(path)
1154 if path in filesToAdd:
1155 filesToAdd.remove(path)
1156 elif modifier == "C":
1157 src, dest = diff['src'], diff['dst']
1158 p4_integrate(src, dest)
1159 pureRenameCopy.add(dest)
1160 if diff['src_sha1'] != diff['dst_sha1']:
1161 p4_edit(dest)
1162 pureRenameCopy.discard(dest)
1163 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1164 p4_edit(dest)
1165 pureRenameCopy.discard(dest)
1166 filesToChangeExecBit[dest] = diff['dst_mode']
1167 os.unlink(dest)
1168 editedFiles.add(dest)
1169 elif modifier == "R":
1170 src, dest = diff['src'], diff['dst']
1171 if self.p4HasMoveCommand:
1172 p4_edit(src) # src must be open before move
1173 p4_move(src, dest) # opens for (move/delete, move/add)
1174 else:
1175 p4_integrate(src, dest)
1176 if diff['src_sha1'] != diff['dst_sha1']:
1177 p4_edit(dest)
1178 else:
1179 pureRenameCopy.add(dest)
1180 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1181 if not self.p4HasMoveCommand:
1182 p4_edit(dest) # with move: already open, writable
1183 filesToChangeExecBit[dest] = diff['dst_mode']
1184 if not self.p4HasMoveCommand:
1185 os.unlink(dest)
1186 filesToDelete.add(src)
1187 editedFiles.add(dest)
1188 else:
1189 die("unknown modifier %s for %s" % (modifier, path))
1191 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
1192 patchcmd = diffcmd + " | git apply "
1193 tryPatchCmd = patchcmd + "--check -"
1194 applyPatchCmd = patchcmd + "--check --apply -"
1195 patch_succeeded = True
1197 if os.system(tryPatchCmd) != 0:
1198 fixed_rcs_keywords = False
1199 patch_succeeded = False
1200 print "Unfortunately applying the change failed!"
1202 # Patch failed, maybe it's just RCS keyword woes. Look through
1203 # the patch to see if that's possible.
1204 if gitConfig("git-p4.attemptRCSCleanup","--bool") == "true":
1205 file = None
1206 pattern = None
1207 kwfiles = {}
1208 for file in editedFiles | filesToDelete:
1209 # did this file's delta contain RCS keywords?
1210 pattern = p4_keywords_regexp_for_file(file)
1212 if pattern:
1213 # this file is a possibility...look for RCS keywords.
1214 regexp = re.compile(pattern, re.VERBOSE)
1215 for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1216 if regexp.search(line):
1217 if verbose:
1218 print "got keyword match on %s in %s in %s" % (pattern, line, file)
1219 kwfiles[file] = pattern
1220 break
1222 for file in kwfiles:
1223 if verbose:
1224 print "zapping %s with %s" % (line,pattern)
1225 self.patchRCSKeywords(file, kwfiles[file])
1226 fixed_rcs_keywords = True
1228 if fixed_rcs_keywords:
1229 print "Retrying the patch with RCS keywords cleaned up"
1230 if os.system(tryPatchCmd) == 0:
1231 patch_succeeded = True
1233 if not patch_succeeded:
1234 for f in editedFiles:
1235 p4_revert(f)
1236 return False
1239 # Apply the patch for real, and do add/delete/+x handling.
1241 system(applyPatchCmd)
1243 for f in filesToAdd:
1244 p4_add(f)
1245 for f in filesToDelete:
1246 p4_revert(f)
1247 p4_delete(f)
1249 # Set/clear executable bits
1250 for f in filesToChangeExecBit.keys():
1251 mode = filesToChangeExecBit[f]
1252 setP4ExecBit(f, mode)
1255 # Build p4 change description, starting with the contents
1256 # of the git commit message.
1258 logMessage = extractLogMessageFromGitCommit(id)
1259 logMessage = logMessage.strip()
1260 (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1262 template = self.prepareSubmitTemplate()
1263 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1265 if self.preserveUser:
1266 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1268 if self.checkAuthorship and not self.p4UserIsMe(p4User):
1269 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1270 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1271 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1273 separatorLine = "######## everything below this line is just the diff #######\n"
1275 # diff
1276 if os.environ.has_key("P4DIFF"):
1277 del(os.environ["P4DIFF"])
1278 diff = ""
1279 for editedFile in editedFiles:
1280 diff += p4_read_pipe(['diff', '-du',
1281 wildcard_encode(editedFile)])
1283 # new file diff
1284 newdiff = ""
1285 for newFile in filesToAdd:
1286 newdiff += "==== new file ====\n"
1287 newdiff += "--- /dev/null\n"
1288 newdiff += "+++ %s\n" % newFile
1289 f = open(newFile, "r")
1290 for line in f.readlines():
1291 newdiff += "+" + line
1292 f.close()
1294 # change description file: submitTemplate, separatorLine, diff, newdiff
1295 (handle, fileName) = tempfile.mkstemp()
1296 tmpFile = os.fdopen(handle, "w+")
1297 if self.isWindows:
1298 submitTemplate = submitTemplate.replace("\n", "\r\n")
1299 separatorLine = separatorLine.replace("\n", "\r\n")
1300 newdiff = newdiff.replace("\n", "\r\n")
1301 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
1302 tmpFile.close()
1304 if self.prepare_p4_only:
1306 # Leave the p4 tree prepared, and the submit template around
1307 # and let the user decide what to do next
1309 print
1310 print "P4 workspace prepared for submission."
1311 print "To submit or revert, go to client workspace"
1312 print " " + self.clientPath
1313 print
1314 print "To submit, use \"p4 submit\" to write a new description,"
1315 print "or \"p4 submit -i %s\" to use the one prepared by" \
1316 " \"git p4\"." % fileName
1317 print "You can delete the file \"%s\" when finished." % fileName
1319 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1320 print "To preserve change ownership by user %s, you must\n" \
1321 "do \"p4 change -f <change>\" after submitting and\n" \
1322 "edit the User field."
1323 if pureRenameCopy:
1324 print "After submitting, renamed files must be re-synced."
1325 print "Invoke \"p4 sync -f\" on each of these files:"
1326 for f in pureRenameCopy:
1327 print " " + f
1329 print
1330 print "To revert the changes, use \"p4 revert ...\", and delete"
1331 print "the submit template file \"%s\"" % fileName
1332 if filesToAdd:
1333 print "Since the commit adds new files, they must be deleted:"
1334 for f in filesToAdd:
1335 print " " + f
1336 print
1337 return True
1340 # Let the user edit the change description, then submit it.
1342 if self.edit_template(fileName):
1343 # read the edited message and submit
1344 ret = True
1345 tmpFile = open(fileName, "rb")
1346 message = tmpFile.read()
1347 tmpFile.close()
1348 submitTemplate = message[:message.index(separatorLine)]
1349 if self.isWindows:
1350 submitTemplate = submitTemplate.replace("\r\n", "\n")
1351 p4_write_pipe(['submit', '-i'], submitTemplate)
1353 if self.preserveUser:
1354 if p4User:
1355 # Get last changelist number. Cannot easily get it from
1356 # the submit command output as the output is
1357 # unmarshalled.
1358 changelist = self.lastP4Changelist()
1359 self.modifyChangelistUser(changelist, p4User)
1361 # The rename/copy happened by applying a patch that created a
1362 # new file. This leaves it writable, which confuses p4.
1363 for f in pureRenameCopy:
1364 p4_sync(f, "-f")
1366 else:
1367 # skip this patch
1368 ret = False
1369 print "Submission cancelled, undoing p4 changes."
1370 for f in editedFiles:
1371 p4_revert(f)
1372 for f in filesToAdd:
1373 p4_revert(f)
1374 os.remove(f)
1375 for f in filesToDelete:
1376 p4_revert(f)
1378 os.remove(fileName)
1379 return ret
1381 # Export git tags as p4 labels. Create a p4 label and then tag
1382 # with that.
1383 def exportGitTags(self, gitTags):
1384 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1385 if len(validLabelRegexp) == 0:
1386 validLabelRegexp = defaultLabelRegexp
1387 m = re.compile(validLabelRegexp)
1389 for name in gitTags:
1391 if not m.match(name):
1392 if verbose:
1393 print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1394 continue
1396 # Get the p4 commit this corresponds to
1397 logMessage = extractLogMessageFromGitCommit(name)
1398 values = extractSettingsGitLog(logMessage)
1400 if not values.has_key('change'):
1401 # a tag pointing to something not sent to p4; ignore
1402 if verbose:
1403 print "git tag %s does not give a p4 commit" % name
1404 continue
1405 else:
1406 changelist = values['change']
1408 # Get the tag details.
1409 inHeader = True
1410 isAnnotated = False
1411 body = []
1412 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1413 l = l.strip()
1414 if inHeader:
1415 if re.match(r'tag\s+', l):
1416 isAnnotated = True
1417 elif re.match(r'\s*$', l):
1418 inHeader = False
1419 continue
1420 else:
1421 body.append(l)
1423 if not isAnnotated:
1424 body = ["lightweight tag imported by git p4\n"]
1426 # Create the label - use the same view as the client spec we are using
1427 clientSpec = getClientSpec()
1429 labelTemplate = "Label: %s\n" % name
1430 labelTemplate += "Description:\n"
1431 for b in body:
1432 labelTemplate += "\t" + b + "\n"
1433 labelTemplate += "View:\n"
1434 for mapping in clientSpec.mappings:
1435 labelTemplate += "\t%s\n" % mapping.depot_side.path
1437 if self.dry_run:
1438 print "Would create p4 label %s for tag" % name
1439 elif self.prepare_p4_only:
1440 print "Not creating p4 label %s for tag due to option" \
1441 " --prepare-p4-only" % name
1442 else:
1443 p4_write_pipe(["label", "-i"], labelTemplate)
1445 # Use the label
1446 p4_system(["tag", "-l", name] +
1447 ["%s@%s" % (mapping.depot_side.path, changelist) for mapping in clientSpec.mappings])
1449 if verbose:
1450 print "created p4 label for tag %s" % name
1452 def run(self, args):
1453 if len(args) == 0:
1454 self.master = currentGitBranch()
1455 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1456 die("Detecting current git branch failed!")
1457 elif len(args) == 1:
1458 self.master = args[0]
1459 if not branchExists(self.master):
1460 die("Branch %s does not exist" % self.master)
1461 else:
1462 return False
1464 allowSubmit = gitConfig("git-p4.allowSubmit")
1465 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1466 die("%s is not in git-p4.allowSubmit" % self.master)
1468 [upstream, settings] = findUpstreamBranchPoint()
1469 self.depotPath = settings['depot-paths'][0]
1470 if len(self.origin) == 0:
1471 self.origin = upstream
1473 if self.preserveUser:
1474 if not self.canChangeChangelists():
1475 die("Cannot preserve user names without p4 super-user or admin permissions")
1477 # if not set from the command line, try the config file
1478 if self.conflict_behavior is None:
1479 val = gitConfig("git-p4.conflict")
1480 if val:
1481 if val not in self.conflict_behavior_choices:
1482 die("Invalid value '%s' for config git-p4.conflict" % val)
1483 else:
1484 val = "ask"
1485 self.conflict_behavior = val
1487 if self.verbose:
1488 print "Origin branch is " + self.origin
1490 if len(self.depotPath) == 0:
1491 print "Internal error: cannot locate perforce depot path from existing branches"
1492 sys.exit(128)
1494 self.useClientSpec = False
1495 if gitConfig("git-p4.useclientspec", "--bool") == "true":
1496 self.useClientSpec = True
1497 if self.useClientSpec:
1498 self.clientSpecDirs = getClientSpec()
1500 if self.useClientSpec:
1501 # all files are relative to the client spec
1502 self.clientPath = getClientRoot()
1503 else:
1504 self.clientPath = p4Where(self.depotPath)
1506 if self.clientPath == "":
1507 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1509 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1510 self.oldWorkingDirectory = os.getcwd()
1512 # ensure the clientPath exists
1513 new_client_dir = False
1514 if not os.path.exists(self.clientPath):
1515 new_client_dir = True
1516 os.makedirs(self.clientPath)
1518 chdir(self.clientPath)
1519 if self.dry_run:
1520 print "Would synchronize p4 checkout in %s" % self.clientPath
1521 else:
1522 print "Synchronizing p4 checkout..."
1523 if new_client_dir:
1524 # old one was destroyed, and maybe nobody told p4
1525 p4_sync("...", "-f")
1526 else:
1527 p4_sync("...")
1528 self.check()
1530 commits = []
1531 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1532 commits.append(line.strip())
1533 commits.reverse()
1535 if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1536 self.checkAuthorship = False
1537 else:
1538 self.checkAuthorship = True
1540 if self.preserveUser:
1541 self.checkValidP4Users(commits)
1544 # Build up a set of options to be passed to diff when
1545 # submitting each commit to p4.
1547 if self.detectRenames:
1548 # command-line -M arg
1549 self.diffOpts = "-M"
1550 else:
1551 # If not explicitly set check the config variable
1552 detectRenames = gitConfig("git-p4.detectRenames")
1554 if detectRenames.lower() == "false" or detectRenames == "":
1555 self.diffOpts = ""
1556 elif detectRenames.lower() == "true":
1557 self.diffOpts = "-M"
1558 else:
1559 self.diffOpts = "-M%s" % detectRenames
1561 # no command-line arg for -C or --find-copies-harder, just
1562 # config variables
1563 detectCopies = gitConfig("git-p4.detectCopies")
1564 if detectCopies.lower() == "false" or detectCopies == "":
1565 pass
1566 elif detectCopies.lower() == "true":
1567 self.diffOpts += " -C"
1568 else:
1569 self.diffOpts += " -C%s" % detectCopies
1571 if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
1572 self.diffOpts += " --find-copies-harder"
1575 # Apply the commits, one at a time. On failure, ask if should
1576 # continue to try the rest of the patches, or quit.
1578 if self.dry_run:
1579 print "Would apply"
1580 applied = []
1581 last = len(commits) - 1
1582 for i, commit in enumerate(commits):
1583 if self.dry_run:
1584 print " ", read_pipe(["git", "show", "-s",
1585 "--format=format:%h %s", commit])
1586 ok = True
1587 else:
1588 ok = self.applyCommit(commit)
1589 if ok:
1590 applied.append(commit)
1591 else:
1592 if self.prepare_p4_only and i < last:
1593 print "Processing only the first commit due to option" \
1594 " --prepare-p4-only"
1595 break
1596 if i < last:
1597 quit = False
1598 while True:
1599 # prompt for what to do, or use the option/variable
1600 if self.conflict_behavior == "ask":
1601 print "What do you want to do?"
1602 response = raw_input("[s]kip this commit but apply"
1603 " the rest, or [q]uit? ")
1604 if not response:
1605 continue
1606 elif self.conflict_behavior == "skip":
1607 response = "s"
1608 elif self.conflict_behavior == "quit":
1609 response = "q"
1610 else:
1611 die("Unknown conflict_behavior '%s'" %
1612 self.conflict_behavior)
1614 if response[0] == "s":
1615 print "Skipping this commit, but applying the rest"
1616 break
1617 if response[0] == "q":
1618 print "Quitting"
1619 quit = True
1620 break
1621 if quit:
1622 break
1624 chdir(self.oldWorkingDirectory)
1626 if self.dry_run:
1627 pass
1628 elif self.prepare_p4_only:
1629 pass
1630 elif len(commits) == len(applied):
1631 print "All commits applied!"
1633 sync = P4Sync()
1634 sync.run([])
1636 rebase = P4Rebase()
1637 rebase.rebase()
1639 else:
1640 if len(applied) == 0:
1641 print "No commits applied."
1642 else:
1643 print "Applied only the commits marked with '*':"
1644 for c in commits:
1645 if c in applied:
1646 star = "*"
1647 else:
1648 star = " "
1649 print star, read_pipe(["git", "show", "-s",
1650 "--format=format:%h %s", c])
1651 print "You will have to do 'git p4 sync' and rebase."
1653 if gitConfig("git-p4.exportLabels", "--bool") == "true":
1654 self.exportLabels = True
1656 if self.exportLabels:
1657 p4Labels = getP4Labels(self.depotPath)
1658 gitTags = getGitTags()
1660 missingGitTags = gitTags - p4Labels
1661 self.exportGitTags(missingGitTags)
1663 # exit with error unless everything applied perfecly
1664 if len(commits) != len(applied):
1665 sys.exit(1)
1667 return True
1669 class View(object):
1670 """Represent a p4 view ("p4 help views"), and map files in a
1671 repo according to the view."""
1673 class Path(object):
1674 """A depot or client path, possibly containing wildcards.
1675 The only one supported is ... at the end, currently.
1676 Initialize with the full path, with //depot or //client."""
1678 def __init__(self, path, is_depot):
1679 self.path = path
1680 self.is_depot = is_depot
1681 self.find_wildcards()
1682 # remember the prefix bit, useful for relative mappings
1683 m = re.match("(//[^/]+/)", self.path)
1684 if not m:
1685 die("Path %s does not start with //prefix/" % self.path)
1686 prefix = m.group(1)
1687 if not self.is_depot:
1688 # strip //client/ on client paths
1689 self.path = self.path[len(prefix):]
1691 def find_wildcards(self):
1692 """Make sure wildcards are valid, and set up internal
1693 variables."""
1695 self.ends_triple_dot = False
1696 # There are three wildcards allowed in p4 views
1697 # (see "p4 help views"). This code knows how to
1698 # handle "..." (only at the end), but cannot deal with
1699 # "%%n" or "*". Only check the depot_side, as p4 should
1700 # validate that the client_side matches too.
1701 if re.search(r'%%[1-9]', self.path):
1702 die("Can't handle %%n wildcards in view: %s" % self.path)
1703 if self.path.find("*") >= 0:
1704 die("Can't handle * wildcards in view: %s" % self.path)
1705 triple_dot_index = self.path.find("...")
1706 if triple_dot_index >= 0:
1707 if triple_dot_index != len(self.path) - 3:
1708 die("Can handle only single ... wildcard, at end: %s" %
1709 self.path)
1710 self.ends_triple_dot = True
1712 def ensure_compatible(self, other_path):
1713 """Make sure the wildcards agree."""
1714 if self.ends_triple_dot != other_path.ends_triple_dot:
1715 die("Both paths must end with ... if either does;\n" +
1716 "paths: %s %s" % (self.path, other_path.path))
1718 def match_wildcards(self, test_path):
1719 """See if this test_path matches us, and fill in the value
1720 of the wildcards if so. Returns a tuple of
1721 (True|False, wildcards[]). For now, only the ... at end
1722 is supported, so at most one wildcard."""
1723 if self.ends_triple_dot:
1724 dotless = self.path[:-3]
1725 if test_path.startswith(dotless):
1726 wildcard = test_path[len(dotless):]
1727 return (True, [ wildcard ])
1728 else:
1729 if test_path == self.path:
1730 return (True, [])
1731 return (False, [])
1733 def match(self, test_path):
1734 """Just return if it matches; don't bother with the wildcards."""
1735 b, _ = self.match_wildcards(test_path)
1736 return b
1738 def fill_in_wildcards(self, wildcards):
1739 """Return the relative path, with the wildcards filled in
1740 if there are any."""
1741 if self.ends_triple_dot:
1742 return self.path[:-3] + wildcards[0]
1743 else:
1744 return self.path
1746 class Mapping(object):
1747 def __init__(self, depot_side, client_side, overlay, exclude):
1748 # depot_side is without the trailing /... if it had one
1749 self.depot_side = View.Path(depot_side, is_depot=True)
1750 self.client_side = View.Path(client_side, is_depot=False)
1751 self.overlay = overlay # started with "+"
1752 self.exclude = exclude # started with "-"
1753 assert not (self.overlay and self.exclude)
1754 self.depot_side.ensure_compatible(self.client_side)
1756 def __str__(self):
1757 c = " "
1758 if self.overlay:
1759 c = "+"
1760 if self.exclude:
1761 c = "-"
1762 return "View.Mapping: %s%s -> %s" % \
1763 (c, self.depot_side.path, self.client_side.path)
1765 def map_depot_to_client(self, depot_path):
1766 """Calculate the client path if using this mapping on the
1767 given depot path; does not consider the effect of other
1768 mappings in a view. Even excluded mappings are returned."""
1769 matches, wildcards = self.depot_side.match_wildcards(depot_path)
1770 if not matches:
1771 return ""
1772 client_path = self.client_side.fill_in_wildcards(wildcards)
1773 return client_path
1776 # View methods
1778 def __init__(self):
1779 self.mappings = []
1781 def append(self, view_line):
1782 """Parse a view line, splitting it into depot and client
1783 sides. Append to self.mappings, preserving order."""
1785 # Split the view line into exactly two words. P4 enforces
1786 # structure on these lines that simplifies this quite a bit.
1788 # Either or both words may be double-quoted.
1789 # Single quotes do not matter.
1790 # Double-quote marks cannot occur inside the words.
1791 # A + or - prefix is also inside the quotes.
1792 # There are no quotes unless they contain a space.
1793 # The line is already white-space stripped.
1794 # The two words are separated by a single space.
1796 if view_line[0] == '"':
1797 # First word is double quoted. Find its end.
1798 close_quote_index = view_line.find('"', 1)
1799 if close_quote_index <= 0:
1800 die("No first-word closing quote found: %s" % view_line)
1801 depot_side = view_line[1:close_quote_index]
1802 # skip closing quote and space
1803 rhs_index = close_quote_index + 1 + 1
1804 else:
1805 space_index = view_line.find(" ")
1806 if space_index <= 0:
1807 die("No word-splitting space found: %s" % view_line)
1808 depot_side = view_line[0:space_index]
1809 rhs_index = space_index + 1
1811 if view_line[rhs_index] == '"':
1812 # Second word is double quoted. Make sure there is a
1813 # double quote at the end too.
1814 if not view_line.endswith('"'):
1815 die("View line with rhs quote should end with one: %s" %
1816 view_line)
1817 # skip the quotes
1818 client_side = view_line[rhs_index+1:-1]
1819 else:
1820 client_side = view_line[rhs_index:]
1822 # prefix + means overlay on previous mapping
1823 overlay = False
1824 if depot_side.startswith("+"):
1825 overlay = True
1826 depot_side = depot_side[1:]
1828 # prefix - means exclude this path
1829 exclude = False
1830 if depot_side.startswith("-"):
1831 exclude = True
1832 depot_side = depot_side[1:]
1834 m = View.Mapping(depot_side, client_side, overlay, exclude)
1835 self.mappings.append(m)
1837 def map_in_client(self, depot_path):
1838 """Return the relative location in the client where this
1839 depot file should live. Returns "" if the file should
1840 not be mapped in the client."""
1842 paths_filled = []
1843 client_path = ""
1845 # look at later entries first
1846 for m in self.mappings[::-1]:
1848 # see where will this path end up in the client
1849 p = m.map_depot_to_client(depot_path)
1851 if p == "":
1852 # Depot path does not belong in client. Must remember
1853 # this, as previous items should not cause files to
1854 # exist in this path either. Remember that the list is
1855 # being walked from the end, which has higher precedence.
1856 # Overlap mappings do not exclude previous mappings.
1857 if not m.overlay:
1858 paths_filled.append(m.client_side)
1860 else:
1861 # This mapping matched; no need to search any further.
1862 # But, the mapping could be rejected if the client path
1863 # has already been claimed by an earlier mapping (i.e.
1864 # one later in the list, which we are walking backwards).
1865 already_mapped_in_client = False
1866 for f in paths_filled:
1867 # this is View.Path.match
1868 if f.match(p):
1869 already_mapped_in_client = True
1870 break
1871 if not already_mapped_in_client:
1872 # Include this file, unless it is from a line that
1873 # explicitly said to exclude it.
1874 if not m.exclude:
1875 client_path = p
1877 # a match, even if rejected, always stops the search
1878 break
1880 return client_path
1882 class P4Sync(Command, P4UserMap):
1883 delete_actions = ( "delete", "move/delete", "purge" )
1885 def __init__(self):
1886 Command.__init__(self)
1887 P4UserMap.__init__(self)
1888 self.options = [
1889 optparse.make_option("--branch", dest="branch"),
1890 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1891 optparse.make_option("--changesfile", dest="changesFile"),
1892 optparse.make_option("--silent", dest="silent", action="store_true"),
1893 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1894 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1895 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1896 help="Import into refs/heads/ , not refs/remotes"),
1897 optparse.make_option("--max-changes", dest="maxChanges"),
1898 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1899 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1900 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1901 help="Only sync files that are included in the Perforce Client Spec")
1903 self.description = """Imports from Perforce into a git repository.\n
1904 example:
1905 //depot/my/project/ -- to import the current head
1906 //depot/my/project/@all -- to import everything
1907 //depot/my/project/@1,6 -- to import only from revision 1 to 6
1909 (a ... is not needed in the path p4 specification, it's added implicitly)"""
1911 self.usage += " //depot/path[@revRange]"
1912 self.silent = False
1913 self.createdBranches = set()
1914 self.committedChanges = set()
1915 self.branch = ""
1916 self.detectBranches = False
1917 self.detectLabels = False
1918 self.importLabels = False
1919 self.changesFile = ""
1920 self.syncWithOrigin = True
1921 self.importIntoRemotes = True
1922 self.maxChanges = ""
1923 self.isWindows = (platform.system() == "Windows")
1924 self.keepRepoPath = False
1925 self.depotPaths = None
1926 self.p4BranchesInGit = []
1927 self.cloneExclude = []
1928 self.useClientSpec = False
1929 self.useClientSpec_from_options = False
1930 self.clientSpecDirs = None
1931 self.tempBranches = []
1932 self.tempBranchLocation = "git-p4-tmp"
1934 if gitConfig("git-p4.syncFromOrigin") == "false":
1935 self.syncWithOrigin = False
1937 # Force a checkpoint in fast-import and wait for it to finish
1938 def checkpoint(self):
1939 self.gitStream.write("checkpoint\n\n")
1940 self.gitStream.write("progress checkpoint\n\n")
1941 out = self.gitOutput.readline()
1942 if self.verbose:
1943 print "checkpoint finished: " + out
1945 def extractFilesFromCommit(self, commit):
1946 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1947 for path in self.cloneExclude]
1948 files = []
1949 fnum = 0
1950 while commit.has_key("depotFile%s" % fnum):
1951 path = commit["depotFile%s" % fnum]
1953 if [p for p in self.cloneExclude
1954 if p4PathStartsWith(path, p)]:
1955 found = False
1956 else:
1957 found = [p for p in self.depotPaths
1958 if p4PathStartsWith(path, p)]
1959 if not found:
1960 fnum = fnum + 1
1961 continue
1963 file = {}
1964 file["path"] = path
1965 file["rev"] = commit["rev%s" % fnum]
1966 file["action"] = commit["action%s" % fnum]
1967 file["type"] = commit["type%s" % fnum]
1968 files.append(file)
1969 fnum = fnum + 1
1970 return files
1972 def stripRepoPath(self, path, prefixes):
1973 """When streaming files, this is called to map a p4 depot path
1974 to where it should go in git. The prefixes are either
1975 self.depotPaths, or self.branchPrefixes in the case of
1976 branch detection."""
1978 if self.useClientSpec:
1979 # branch detection moves files up a level (the branch name)
1980 # from what client spec interpretation gives
1981 path = self.clientSpecDirs.map_in_client(path)
1982 if self.detectBranches:
1983 for b in self.knownBranches:
1984 if path.startswith(b + "/"):
1985 path = path[len(b)+1:]
1987 elif self.keepRepoPath:
1988 # Preserve everything in relative path name except leading
1989 # //depot/; just look at first prefix as they all should
1990 # be in the same depot.
1991 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
1992 if p4PathStartsWith(path, depot):
1993 path = path[len(depot):]
1995 else:
1996 for p in prefixes:
1997 if p4PathStartsWith(path, p):
1998 path = path[len(p):]
1999 break
2001 path = wildcard_decode(path)
2002 return path
2004 def splitFilesIntoBranches(self, commit):
2005 """Look at each depotFile in the commit to figure out to what
2006 branch it belongs."""
2008 branches = {}
2009 fnum = 0
2010 while commit.has_key("depotFile%s" % fnum):
2011 path = commit["depotFile%s" % fnum]
2012 found = [p for p in self.depotPaths
2013 if p4PathStartsWith(path, p)]
2014 if not found:
2015 fnum = fnum + 1
2016 continue
2018 file = {}
2019 file["path"] = path
2020 file["rev"] = commit["rev%s" % fnum]
2021 file["action"] = commit["action%s" % fnum]
2022 file["type"] = commit["type%s" % fnum]
2023 fnum = fnum + 1
2025 # start with the full relative path where this file would
2026 # go in a p4 client
2027 if self.useClientSpec:
2028 relPath = self.clientSpecDirs.map_in_client(path)
2029 else:
2030 relPath = self.stripRepoPath(path, self.depotPaths)
2032 for branch in self.knownBranches.keys():
2033 # add a trailing slash so that a commit into qt/4.2foo
2034 # doesn't end up in qt/4.2, e.g.
2035 if relPath.startswith(branch + "/"):
2036 if branch not in branches:
2037 branches[branch] = []
2038 branches[branch].append(file)
2039 break
2041 return branches
2043 # output one file from the P4 stream
2044 # - helper for streamP4Files
2046 def streamOneP4File(self, file, contents):
2047 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2048 if verbose:
2049 sys.stderr.write("%s\n" % relPath)
2051 (type_base, type_mods) = split_p4_type(file["type"])
2053 git_mode = "100644"
2054 if "x" in type_mods:
2055 git_mode = "100755"
2056 if type_base == "symlink":
2057 git_mode = "120000"
2058 # p4 print on a symlink contains "target\n"; remove the newline
2059 data = ''.join(contents)
2060 contents = [data[:-1]]
2062 if type_base == "utf16":
2063 # p4 delivers different text in the python output to -G
2064 # than it does when using "print -o", or normal p4 client
2065 # operations. utf16 is converted to ascii or utf8, perhaps.
2066 # But ascii text saved as -t utf16 is completely mangled.
2067 # Invoke print -o to get the real contents.
2068 text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
2069 contents = [ text ]
2071 if type_base == "apple":
2072 # Apple filetype files will be streamed as a concatenation of
2073 # its appledouble header and the contents. This is useless
2074 # on both macs and non-macs. If using "print -q -o xx", it
2075 # will create "xx" with the data, and "%xx" with the header.
2076 # This is also not very useful.
2078 # Ideally, someday, this script can learn how to generate
2079 # appledouble files directly and import those to git, but
2080 # non-mac machines can never find a use for apple filetype.
2081 print "\nIgnoring apple filetype file %s" % file['depotFile']
2082 return
2084 # Perhaps windows wants unicode, utf16 newlines translated too;
2085 # but this is not doing it.
2086 if self.isWindows and type_base == "text":
2087 mangled = []
2088 for data in contents:
2089 data = data.replace("\r\n", "\n")
2090 mangled.append(data)
2091 contents = mangled
2093 # Note that we do not try to de-mangle keywords on utf16 files,
2094 # even though in theory somebody may want that.
2095 pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2096 if pattern:
2097 regexp = re.compile(pattern, re.VERBOSE)
2098 text = ''.join(contents)
2099 text = regexp.sub(r'$\1$', text)
2100 contents = [ text ]
2102 self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2104 # total length...
2105 length = 0
2106 for d in contents:
2107 length = length + len(d)
2109 self.gitStream.write("data %d\n" % length)
2110 for d in contents:
2111 self.gitStream.write(d)
2112 self.gitStream.write("\n")
2114 def streamOneP4Deletion(self, file):
2115 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2116 if verbose:
2117 sys.stderr.write("delete %s\n" % relPath)
2118 self.gitStream.write("D %s\n" % relPath)
2120 # handle another chunk of streaming data
2121 def streamP4FilesCb(self, marshalled):
2123 if marshalled.has_key('depotFile') and self.stream_have_file_info:
2124 # start of a new file - output the old one first
2125 self.streamOneP4File(self.stream_file, self.stream_contents)
2126 self.stream_file = {}
2127 self.stream_contents = []
2128 self.stream_have_file_info = False
2130 # pick up the new file information... for the
2131 # 'data' field we need to append to our array
2132 for k in marshalled.keys():
2133 if k == 'data':
2134 self.stream_contents.append(marshalled['data'])
2135 else:
2136 self.stream_file[k] = marshalled[k]
2138 self.stream_have_file_info = True
2140 # Stream directly from "p4 files" into "git fast-import"
2141 def streamP4Files(self, files):
2142 filesForCommit = []
2143 filesToRead = []
2144 filesToDelete = []
2146 for f in files:
2147 # if using a client spec, only add the files that have
2148 # a path in the client
2149 if self.clientSpecDirs:
2150 if self.clientSpecDirs.map_in_client(f['path']) == "":
2151 continue
2153 filesForCommit.append(f)
2154 if f['action'] in self.delete_actions:
2155 filesToDelete.append(f)
2156 else:
2157 filesToRead.append(f)
2159 # deleted files...
2160 for f in filesToDelete:
2161 self.streamOneP4Deletion(f)
2163 if len(filesToRead) > 0:
2164 self.stream_file = {}
2165 self.stream_contents = []
2166 self.stream_have_file_info = False
2168 # curry self argument
2169 def streamP4FilesCbSelf(entry):
2170 self.streamP4FilesCb(entry)
2172 fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2174 p4CmdList(["-x", "-", "print"],
2175 stdin=fileArgs,
2176 cb=streamP4FilesCbSelf)
2178 # do the last chunk
2179 if self.stream_file.has_key('depotFile'):
2180 self.streamOneP4File(self.stream_file, self.stream_contents)
2182 def make_email(self, userid):
2183 if userid in self.users:
2184 return self.users[userid]
2185 else:
2186 return "%s <a@b>" % userid
2188 # Stream a p4 tag
2189 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2190 if verbose:
2191 print "writing tag %s for commit %s" % (labelName, commit)
2192 gitStream.write("tag %s\n" % labelName)
2193 gitStream.write("from %s\n" % commit)
2195 if labelDetails.has_key('Owner'):
2196 owner = labelDetails["Owner"]
2197 else:
2198 owner = None
2200 # Try to use the owner of the p4 label, or failing that,
2201 # the current p4 user id.
2202 if owner:
2203 email = self.make_email(owner)
2204 else:
2205 email = self.make_email(self.p4UserId())
2206 tagger = "%s %s %s" % (email, epoch, self.tz)
2208 gitStream.write("tagger %s\n" % tagger)
2210 print "labelDetails=",labelDetails
2211 if labelDetails.has_key('Description'):
2212 description = labelDetails['Description']
2213 else:
2214 description = 'Label from git p4'
2216 gitStream.write("data %d\n" % len(description))
2217 gitStream.write(description)
2218 gitStream.write("\n")
2220 def commit(self, details, files, branch, parent = ""):
2221 epoch = details["time"]
2222 author = details["user"]
2224 if self.verbose:
2225 print "commit into %s" % branch
2227 # start with reading files; if that fails, we should not
2228 # create a commit.
2229 new_files = []
2230 for f in files:
2231 if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2232 new_files.append (f)
2233 else:
2234 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2236 self.gitStream.write("commit %s\n" % branch)
2237 # gitStream.write("mark :%s\n" % details["change"])
2238 self.committedChanges.add(int(details["change"]))
2239 committer = ""
2240 if author not in self.users:
2241 self.getUserMapFromPerforceServer()
2242 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2244 self.gitStream.write("committer %s\n" % committer)
2246 self.gitStream.write("data <<EOT\n")
2247 self.gitStream.write(details["desc"])
2248 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2249 (','.join(self.branchPrefixes), details["change"]))
2250 if len(details['options']) > 0:
2251 self.gitStream.write(": options = %s" % details['options'])
2252 self.gitStream.write("]\nEOT\n\n")
2254 if len(parent) > 0:
2255 if self.verbose:
2256 print "parent %s" % parent
2257 self.gitStream.write("from %s\n" % parent)
2259 self.streamP4Files(new_files)
2260 self.gitStream.write("\n")
2262 change = int(details["change"])
2264 if self.labels.has_key(change):
2265 label = self.labels[change]
2266 labelDetails = label[0]
2267 labelRevisions = label[1]
2268 if self.verbose:
2269 print "Change %s is labelled %s" % (change, labelDetails)
2271 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2272 for p in self.branchPrefixes])
2274 if len(files) == len(labelRevisions):
2276 cleanedFiles = {}
2277 for info in files:
2278 if info["action"] in self.delete_actions:
2279 continue
2280 cleanedFiles[info["depotFile"]] = info["rev"]
2282 if cleanedFiles == labelRevisions:
2283 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2285 else:
2286 if not self.silent:
2287 print ("Tag %s does not match with change %s: files do not match."
2288 % (labelDetails["label"], change))
2290 else:
2291 if not self.silent:
2292 print ("Tag %s does not match with change %s: file count is different."
2293 % (labelDetails["label"], change))
2295 # Build a dictionary of changelists and labels, for "detect-labels" option.
2296 def getLabels(self):
2297 self.labels = {}
2299 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2300 if len(l) > 0 and not self.silent:
2301 print "Finding files belonging to labels in %s" % `self.depotPaths`
2303 for output in l:
2304 label = output["label"]
2305 revisions = {}
2306 newestChange = 0
2307 if self.verbose:
2308 print "Querying files for label %s" % label
2309 for file in p4CmdList(["files"] +
2310 ["%s...@%s" % (p, label)
2311 for p in self.depotPaths]):
2312 revisions[file["depotFile"]] = file["rev"]
2313 change = int(file["change"])
2314 if change > newestChange:
2315 newestChange = change
2317 self.labels[newestChange] = [output, revisions]
2319 if self.verbose:
2320 print "Label changes: %s" % self.labels.keys()
2322 # Import p4 labels as git tags. A direct mapping does not
2323 # exist, so assume that if all the files are at the same revision
2324 # then we can use that, or it's something more complicated we should
2325 # just ignore.
2326 def importP4Labels(self, stream, p4Labels):
2327 if verbose:
2328 print "import p4 labels: " + ' '.join(p4Labels)
2330 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2331 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2332 if len(validLabelRegexp) == 0:
2333 validLabelRegexp = defaultLabelRegexp
2334 m = re.compile(validLabelRegexp)
2336 for name in p4Labels:
2337 commitFound = False
2339 if not m.match(name):
2340 if verbose:
2341 print "label %s does not match regexp %s" % (name,validLabelRegexp)
2342 continue
2344 if name in ignoredP4Labels:
2345 continue
2347 labelDetails = p4CmdList(['label', "-o", name])[0]
2349 # get the most recent changelist for each file in this label
2350 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2351 for p in self.depotPaths])
2353 if change.has_key('change'):
2354 # find the corresponding git commit; take the oldest commit
2355 changelist = int(change['change'])
2356 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2357 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2358 if len(gitCommit) == 0:
2359 print "could not find git commit for changelist %d" % changelist
2360 else:
2361 gitCommit = gitCommit.strip()
2362 commitFound = True
2363 # Convert from p4 time format
2364 try:
2365 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2366 except ValueError:
2367 print "Could not convert label time %s" % labelDetail['Update']
2368 tmwhen = 1
2370 when = int(time.mktime(tmwhen))
2371 self.streamTag(stream, name, labelDetails, gitCommit, when)
2372 if verbose:
2373 print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2374 else:
2375 if verbose:
2376 print "Label %s has no changelists - possibly deleted?" % name
2378 if not commitFound:
2379 # We can't import this label; don't try again as it will get very
2380 # expensive repeatedly fetching all the files for labels that will
2381 # never be imported. If the label is moved in the future, the
2382 # ignore will need to be removed manually.
2383 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2385 def guessProjectName(self):
2386 for p in self.depotPaths:
2387 if p.endswith("/"):
2388 p = p[:-1]
2389 p = p[p.strip().rfind("/") + 1:]
2390 if not p.endswith("/"):
2391 p += "/"
2392 return p
2394 def getBranchMapping(self):
2395 lostAndFoundBranches = set()
2397 user = gitConfig("git-p4.branchUser")
2398 if len(user) > 0:
2399 command = "branches -u %s" % user
2400 else:
2401 command = "branches"
2403 for info in p4CmdList(command):
2404 details = p4Cmd(["branch", "-o", info["branch"]])
2405 viewIdx = 0
2406 while details.has_key("View%s" % viewIdx):
2407 paths = details["View%s" % viewIdx].split(" ")
2408 viewIdx = viewIdx + 1
2409 # require standard //depot/foo/... //depot/bar/... mapping
2410 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2411 continue
2412 source = paths[0]
2413 destination = paths[1]
2414 ## HACK
2415 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2416 source = source[len(self.depotPaths[0]):-4]
2417 destination = destination[len(self.depotPaths[0]):-4]
2419 if destination in self.knownBranches:
2420 if not self.silent:
2421 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2422 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2423 continue
2425 self.knownBranches[destination] = source
2427 lostAndFoundBranches.discard(destination)
2429 if source not in self.knownBranches:
2430 lostAndFoundBranches.add(source)
2432 # Perforce does not strictly require branches to be defined, so we also
2433 # check git config for a branch list.
2435 # Example of branch definition in git config file:
2436 # [git-p4]
2437 # branchList=main:branchA
2438 # branchList=main:branchB
2439 # branchList=branchA:branchC
2440 configBranches = gitConfigList("git-p4.branchList")
2441 for branch in configBranches:
2442 if branch:
2443 (source, destination) = branch.split(":")
2444 self.knownBranches[destination] = source
2446 lostAndFoundBranches.discard(destination)
2448 if source not in self.knownBranches:
2449 lostAndFoundBranches.add(source)
2452 for branch in lostAndFoundBranches:
2453 self.knownBranches[branch] = branch
2455 def getBranchMappingFromGitBranches(self):
2456 branches = p4BranchesInGit(self.importIntoRemotes)
2457 for branch in branches.keys():
2458 if branch == "master":
2459 branch = "main"
2460 else:
2461 branch = branch[len(self.projectName):]
2462 self.knownBranches[branch] = branch
2464 def listExistingP4GitBranches(self):
2465 # branches holds mapping from name to commit
2466 branches = p4BranchesInGit(self.importIntoRemotes)
2467 self.p4BranchesInGit = branches.keys()
2468 for branch in branches.keys():
2469 self.initialParents[self.refPrefix + branch] = branches[branch]
2471 def updateOptionDict(self, d):
2472 option_keys = {}
2473 if self.keepRepoPath:
2474 option_keys['keepRepoPath'] = 1
2476 d["options"] = ' '.join(sorted(option_keys.keys()))
2478 def readOptions(self, d):
2479 self.keepRepoPath = (d.has_key('options')
2480 and ('keepRepoPath' in d['options']))
2482 def gitRefForBranch(self, branch):
2483 if branch == "main":
2484 return self.refPrefix + "master"
2486 if len(branch) <= 0:
2487 return branch
2489 return self.refPrefix + self.projectName + branch
2491 def gitCommitByP4Change(self, ref, change):
2492 if self.verbose:
2493 print "looking in ref " + ref + " for change %s using bisect..." % change
2495 earliestCommit = ""
2496 latestCommit = parseRevision(ref)
2498 while True:
2499 if self.verbose:
2500 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2501 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2502 if len(next) == 0:
2503 if self.verbose:
2504 print "argh"
2505 return ""
2506 log = extractLogMessageFromGitCommit(next)
2507 settings = extractSettingsGitLog(log)
2508 currentChange = int(settings['change'])
2509 if self.verbose:
2510 print "current change %s" % currentChange
2512 if currentChange == change:
2513 if self.verbose:
2514 print "found %s" % next
2515 return next
2517 if currentChange < change:
2518 earliestCommit = "^%s" % next
2519 else:
2520 latestCommit = "%s" % next
2522 return ""
2524 def importNewBranch(self, branch, maxChange):
2525 # make fast-import flush all changes to disk and update the refs using the checkpoint
2526 # command so that we can try to find the branch parent in the git history
2527 self.gitStream.write("checkpoint\n\n");
2528 self.gitStream.flush();
2529 branchPrefix = self.depotPaths[0] + branch + "/"
2530 range = "@1,%s" % maxChange
2531 #print "prefix" + branchPrefix
2532 changes = p4ChangesForPaths([branchPrefix], range)
2533 if len(changes) <= 0:
2534 return False
2535 firstChange = changes[0]
2536 #print "first change in branch: %s" % firstChange
2537 sourceBranch = self.knownBranches[branch]
2538 sourceDepotPath = self.depotPaths[0] + sourceBranch
2539 sourceRef = self.gitRefForBranch(sourceBranch)
2540 #print "source " + sourceBranch
2542 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2543 #print "branch parent: %s" % branchParentChange
2544 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2545 if len(gitParent) > 0:
2546 self.initialParents[self.gitRefForBranch(branch)] = gitParent
2547 #print "parent git commit: %s" % gitParent
2549 self.importChanges(changes)
2550 return True
2552 def searchParent(self, parent, branch, target):
2553 parentFound = False
2554 for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]):
2555 blob = blob.strip()
2556 if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2557 parentFound = True
2558 if self.verbose:
2559 print "Found parent of %s in commit %s" % (branch, blob)
2560 break
2561 if parentFound:
2562 return blob
2563 else:
2564 return None
2566 def importChanges(self, changes):
2567 cnt = 1
2568 for change in changes:
2569 description = p4_describe(change)
2570 self.updateOptionDict(description)
2572 if not self.silent:
2573 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2574 sys.stdout.flush()
2575 cnt = cnt + 1
2577 try:
2578 if self.detectBranches:
2579 branches = self.splitFilesIntoBranches(description)
2580 for branch in branches.keys():
2581 ## HACK --hwn
2582 branchPrefix = self.depotPaths[0] + branch + "/"
2583 self.branchPrefixes = [ branchPrefix ]
2585 parent = ""
2587 filesForCommit = branches[branch]
2589 if self.verbose:
2590 print "branch is %s" % branch
2592 self.updatedBranches.add(branch)
2594 if branch not in self.createdBranches:
2595 self.createdBranches.add(branch)
2596 parent = self.knownBranches[branch]
2597 if parent == branch:
2598 parent = ""
2599 else:
2600 fullBranch = self.projectName + branch
2601 if fullBranch not in self.p4BranchesInGit:
2602 if not self.silent:
2603 print("\n Importing new branch %s" % fullBranch);
2604 if self.importNewBranch(branch, change - 1):
2605 parent = ""
2606 self.p4BranchesInGit.append(fullBranch)
2607 if not self.silent:
2608 print("\n Resuming with change %s" % change);
2610 if self.verbose:
2611 print "parent determined through known branches: %s" % parent
2613 branch = self.gitRefForBranch(branch)
2614 parent = self.gitRefForBranch(parent)
2616 if self.verbose:
2617 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2619 if len(parent) == 0 and branch in self.initialParents:
2620 parent = self.initialParents[branch]
2621 del self.initialParents[branch]
2623 blob = None
2624 if len(parent) > 0:
2625 tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change))
2626 if self.verbose:
2627 print "Creating temporary branch: " + tempBranch
2628 self.commit(description, filesForCommit, tempBranch)
2629 self.tempBranches.append(tempBranch)
2630 self.checkpoint()
2631 blob = self.searchParent(parent, branch, tempBranch)
2632 if blob:
2633 self.commit(description, filesForCommit, branch, blob)
2634 else:
2635 if self.verbose:
2636 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2637 self.commit(description, filesForCommit, branch, parent)
2638 else:
2639 files = self.extractFilesFromCommit(description)
2640 self.commit(description, files, self.branch,
2641 self.initialParent)
2642 self.initialParent = ""
2643 except IOError:
2644 print self.gitError.read()
2645 sys.exit(1)
2647 def importHeadRevision(self, revision):
2648 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2650 details = {}
2651 details["user"] = "git perforce import user"
2652 details["desc"] = ("Initial import of %s from the state at revision %s\n"
2653 % (' '.join(self.depotPaths), revision))
2654 details["change"] = revision
2655 newestRevision = 0
2657 fileCnt = 0
2658 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2660 for info in p4CmdList(["files"] + fileArgs):
2662 if 'code' in info and info['code'] == 'error':
2663 sys.stderr.write("p4 returned an error: %s\n"
2664 % info['data'])
2665 if info['data'].find("must refer to client") >= 0:
2666 sys.stderr.write("This particular p4 error is misleading.\n")
2667 sys.stderr.write("Perhaps the depot path was misspelled.\n");
2668 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
2669 sys.exit(1)
2670 if 'p4ExitCode' in info:
2671 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2672 sys.exit(1)
2675 change = int(info["change"])
2676 if change > newestRevision:
2677 newestRevision = change
2679 if info["action"] in self.delete_actions:
2680 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2681 #fileCnt = fileCnt + 1
2682 continue
2684 for prop in ["depotFile", "rev", "action", "type" ]:
2685 details["%s%s" % (prop, fileCnt)] = info[prop]
2687 fileCnt = fileCnt + 1
2689 details["change"] = newestRevision
2691 # Use time from top-most change so that all git p4 clones of
2692 # the same p4 repo have the same commit SHA1s.
2693 res = p4_describe(newestRevision)
2694 details["time"] = res["time"]
2696 self.updateOptionDict(details)
2697 try:
2698 self.commit(details, self.extractFilesFromCommit(details), self.branch)
2699 except IOError:
2700 print "IO error with git fast-import. Is your git version recent enough?"
2701 print self.gitError.read()
2704 def run(self, args):
2705 self.depotPaths = []
2706 self.changeRange = ""
2707 self.initialParent = ""
2708 self.previousDepotPaths = []
2710 # map from branch depot path to parent branch
2711 self.knownBranches = {}
2712 self.initialParents = {}
2713 self.hasOrigin = originP4BranchesExist()
2714 if not self.syncWithOrigin:
2715 self.hasOrigin = False
2717 if self.importIntoRemotes:
2718 self.refPrefix = "refs/remotes/p4/"
2719 else:
2720 self.refPrefix = "refs/heads/p4/"
2722 if self.syncWithOrigin and self.hasOrigin:
2723 if not self.silent:
2724 print "Syncing with origin first by calling git fetch origin"
2725 system("git fetch origin")
2727 if len(self.branch) == 0:
2728 self.branch = self.refPrefix + "master"
2729 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2730 system("git update-ref %s refs/heads/p4" % self.branch)
2731 system("git branch -D p4");
2732 # create it /after/ importing, when master exists
2733 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
2734 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
2736 # accept either the command-line option, or the configuration variable
2737 if self.useClientSpec:
2738 # will use this after clone to set the variable
2739 self.useClientSpec_from_options = True
2740 else:
2741 if gitConfig("git-p4.useclientspec", "--bool") == "true":
2742 self.useClientSpec = True
2743 if self.useClientSpec:
2744 self.clientSpecDirs = getClientSpec()
2746 # TODO: should always look at previous commits,
2747 # merge with previous imports, if possible.
2748 if args == []:
2749 if self.hasOrigin:
2750 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2751 self.listExistingP4GitBranches()
2753 if len(self.p4BranchesInGit) > 1:
2754 if not self.silent:
2755 print "Importing from/into multiple branches"
2756 self.detectBranches = True
2758 if self.verbose:
2759 print "branches: %s" % self.p4BranchesInGit
2761 p4Change = 0
2762 for branch in self.p4BranchesInGit:
2763 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
2765 settings = extractSettingsGitLog(logMsg)
2767 self.readOptions(settings)
2768 if (settings.has_key('depot-paths')
2769 and settings.has_key ('change')):
2770 change = int(settings['change']) + 1
2771 p4Change = max(p4Change, change)
2773 depotPaths = sorted(settings['depot-paths'])
2774 if self.previousDepotPaths == []:
2775 self.previousDepotPaths = depotPaths
2776 else:
2777 paths = []
2778 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2779 prev_list = prev.split("/")
2780 cur_list = cur.split("/")
2781 for i in range(0, min(len(cur_list), len(prev_list))):
2782 if cur_list[i] <> prev_list[i]:
2783 i = i - 1
2784 break
2786 paths.append ("/".join(cur_list[:i + 1]))
2788 self.previousDepotPaths = paths
2790 if p4Change > 0:
2791 self.depotPaths = sorted(self.previousDepotPaths)
2792 self.changeRange = "@%s,#head" % p4Change
2793 if not self.detectBranches:
2794 self.initialParent = parseRevision(self.branch)
2795 if not self.silent and not self.detectBranches:
2796 print "Performing incremental import into %s git branch" % self.branch
2798 if not self.branch.startswith("refs/"):
2799 self.branch = "refs/heads/" + self.branch
2801 if len(args) == 0 and self.depotPaths:
2802 if not self.silent:
2803 print "Depot paths: %s" % ' '.join(self.depotPaths)
2804 else:
2805 if self.depotPaths and self.depotPaths != args:
2806 print ("previous import used depot path %s and now %s was specified. "
2807 "This doesn't work!" % (' '.join (self.depotPaths),
2808 ' '.join (args)))
2809 sys.exit(1)
2811 self.depotPaths = sorted(args)
2813 revision = ""
2814 self.users = {}
2816 # Make sure no revision specifiers are used when --changesfile
2817 # is specified.
2818 bad_changesfile = False
2819 if len(self.changesFile) > 0:
2820 for p in self.depotPaths:
2821 if p.find("@") >= 0 or p.find("#") >= 0:
2822 bad_changesfile = True
2823 break
2824 if bad_changesfile:
2825 die("Option --changesfile is incompatible with revision specifiers")
2827 newPaths = []
2828 for p in self.depotPaths:
2829 if p.find("@") != -1:
2830 atIdx = p.index("@")
2831 self.changeRange = p[atIdx:]
2832 if self.changeRange == "@all":
2833 self.changeRange = ""
2834 elif ',' not in self.changeRange:
2835 revision = self.changeRange
2836 self.changeRange = ""
2837 p = p[:atIdx]
2838 elif p.find("#") != -1:
2839 hashIdx = p.index("#")
2840 revision = p[hashIdx:]
2841 p = p[:hashIdx]
2842 elif self.previousDepotPaths == []:
2843 # pay attention to changesfile, if given, else import
2844 # the entire p4 tree at the head revision
2845 if len(self.changesFile) == 0:
2846 revision = "#head"
2848 p = re.sub ("\.\.\.$", "", p)
2849 if not p.endswith("/"):
2850 p += "/"
2852 newPaths.append(p)
2854 self.depotPaths = newPaths
2856 # --detect-branches may change this for each branch
2857 self.branchPrefixes = self.depotPaths
2859 self.loadUserMapFromCache()
2860 self.labels = {}
2861 if self.detectLabels:
2862 self.getLabels();
2864 if self.detectBranches:
2865 ## FIXME - what's a P4 projectName ?
2866 self.projectName = self.guessProjectName()
2868 if self.hasOrigin:
2869 self.getBranchMappingFromGitBranches()
2870 else:
2871 self.getBranchMapping()
2872 if self.verbose:
2873 print "p4-git branches: %s" % self.p4BranchesInGit
2874 print "initial parents: %s" % self.initialParents
2875 for b in self.p4BranchesInGit:
2876 if b != "master":
2878 ## FIXME
2879 b = b[len(self.projectName):]
2880 self.createdBranches.add(b)
2882 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2884 importProcess = subprocess.Popen(["git", "fast-import"],
2885 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2886 stderr=subprocess.PIPE);
2887 self.gitOutput = importProcess.stdout
2888 self.gitStream = importProcess.stdin
2889 self.gitError = importProcess.stderr
2891 if revision:
2892 self.importHeadRevision(revision)
2893 else:
2894 changes = []
2896 if len(self.changesFile) > 0:
2897 output = open(self.changesFile).readlines()
2898 changeSet = set()
2899 for line in output:
2900 changeSet.add(int(line))
2902 for change in changeSet:
2903 changes.append(change)
2905 changes.sort()
2906 else:
2907 # catch "git p4 sync" with no new branches, in a repo that
2908 # does not have any existing p4 branches
2909 if len(args) == 0 and not self.p4BranchesInGit:
2910 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
2911 if self.verbose:
2912 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2913 self.changeRange)
2914 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2916 if len(self.maxChanges) > 0:
2917 changes = changes[:min(int(self.maxChanges), len(changes))]
2919 if len(changes) == 0:
2920 if not self.silent:
2921 print "No changes to import!"
2922 else:
2923 if not self.silent and not self.detectBranches:
2924 print "Import destination: %s" % self.branch
2926 self.updatedBranches = set()
2928 self.importChanges(changes)
2930 if not self.silent:
2931 print ""
2932 if len(self.updatedBranches) > 0:
2933 sys.stdout.write("Updated branches: ")
2934 for b in self.updatedBranches:
2935 sys.stdout.write("%s " % b)
2936 sys.stdout.write("\n")
2938 if gitConfig("git-p4.importLabels", "--bool") == "true":
2939 self.importLabels = True
2941 if self.importLabels:
2942 p4Labels = getP4Labels(self.depotPaths)
2943 gitTags = getGitTags()
2945 missingP4Labels = p4Labels - gitTags
2946 self.importP4Labels(self.gitStream, missingP4Labels)
2948 self.gitStream.close()
2949 if importProcess.wait() != 0:
2950 die("fast-import failed: %s" % self.gitError.read())
2951 self.gitOutput.close()
2952 self.gitError.close()
2954 # Cleanup temporary branches created during import
2955 if self.tempBranches != []:
2956 for branch in self.tempBranches:
2957 read_pipe("git update-ref -d %s" % branch)
2958 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
2960 return True
2962 class P4Rebase(Command):
2963 def __init__(self):
2964 Command.__init__(self)
2965 self.options = [
2966 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2968 self.importLabels = False
2969 self.description = ("Fetches the latest revision from perforce and "
2970 + "rebases the current work (branch) against it")
2972 def run(self, args):
2973 sync = P4Sync()
2974 sync.importLabels = self.importLabels
2975 sync.run([])
2977 return self.rebase()
2979 def rebase(self):
2980 if os.system("git update-index --refresh") != 0:
2981 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.");
2982 if len(read_pipe("git diff-index HEAD --")) > 0:
2983 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2985 [upstream, settings] = findUpstreamBranchPoint()
2986 if len(upstream) == 0:
2987 die("Cannot find upstream branchpoint for rebase")
2989 # the branchpoint may be p4/foo~3, so strip off the parent
2990 upstream = re.sub("~[0-9]+$", "", upstream)
2992 print "Rebasing the current branch onto %s" % upstream
2993 oldHead = read_pipe("git rev-parse HEAD").strip()
2994 system("git rebase %s" % upstream)
2995 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
2996 return True
2998 class P4Clone(P4Sync):
2999 def __init__(self):
3000 P4Sync.__init__(self)
3001 self.description = "Creates a new git repository and imports from Perforce into it"
3002 self.usage = "usage: %prog [options] //depot/path[@revRange]"
3003 self.options += [
3004 optparse.make_option("--destination", dest="cloneDestination",
3005 action='store', default=None,
3006 help="where to leave result of the clone"),
3007 optparse.make_option("-/", dest="cloneExclude",
3008 action="append", type="string",
3009 help="exclude depot path"),
3010 optparse.make_option("--bare", dest="cloneBare",
3011 action="store_true", default=False),
3013 self.cloneDestination = None
3014 self.needsGit = False
3015 self.cloneBare = False
3017 # This is required for the "append" cloneExclude action
3018 def ensure_value(self, attr, value):
3019 if not hasattr(self, attr) or getattr(self, attr) is None:
3020 setattr(self, attr, value)
3021 return getattr(self, attr)
3023 def defaultDestination(self, args):
3024 ## TODO: use common prefix of args?
3025 depotPath = args[0]
3026 depotDir = re.sub("(@[^@]*)$", "", depotPath)
3027 depotDir = re.sub("(#[^#]*)$", "", depotDir)
3028 depotDir = re.sub(r"\.\.\.$", "", depotDir)
3029 depotDir = re.sub(r"/$", "", depotDir)
3030 return os.path.split(depotDir)[1]
3032 def run(self, args):
3033 if len(args) < 1:
3034 return False
3036 if self.keepRepoPath and not self.cloneDestination:
3037 sys.stderr.write("Must specify destination for --keep-path\n")
3038 sys.exit(1)
3040 depotPaths = args
3042 if not self.cloneDestination and len(depotPaths) > 1:
3043 self.cloneDestination = depotPaths[-1]
3044 depotPaths = depotPaths[:-1]
3046 self.cloneExclude = ["/"+p for p in self.cloneExclude]
3047 for p in depotPaths:
3048 if not p.startswith("//"):
3049 return False
3051 if not self.cloneDestination:
3052 self.cloneDestination = self.defaultDestination(args)
3054 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3056 if not os.path.exists(self.cloneDestination):
3057 os.makedirs(self.cloneDestination)
3058 chdir(self.cloneDestination)
3060 init_cmd = [ "git", "init" ]
3061 if self.cloneBare:
3062 init_cmd.append("--bare")
3063 subprocess.check_call(init_cmd)
3065 if not P4Sync.run(self, depotPaths):
3066 return False
3067 if self.branch != "master":
3068 if self.importIntoRemotes:
3069 masterbranch = "refs/remotes/p4/master"
3070 else:
3071 masterbranch = "refs/heads/p4/master"
3072 if gitBranchExists(masterbranch):
3073 system("git branch master %s" % masterbranch)
3074 if not self.cloneBare:
3075 system("git checkout -f")
3076 else:
3077 print "Could not detect main branch. No checkout/master branch created."
3079 # auto-set this variable if invoked with --use-client-spec
3080 if self.useClientSpec_from_options:
3081 system("git config --bool git-p4.useclientspec true")
3083 return True
3085 class P4Branches(Command):
3086 def __init__(self):
3087 Command.__init__(self)
3088 self.options = [ ]
3089 self.description = ("Shows the git branches that hold imports and their "
3090 + "corresponding perforce depot paths")
3091 self.verbose = False
3093 def run(self, args):
3094 if originP4BranchesExist():
3095 createOrUpdateBranchesFromOrigin()
3097 cmdline = "git rev-parse --symbolic "
3098 cmdline += " --remotes"
3100 for line in read_pipe_lines(cmdline):
3101 line = line.strip()
3103 if not line.startswith('p4/') or line == "p4/HEAD":
3104 continue
3105 branch = line
3107 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3108 settings = extractSettingsGitLog(log)
3110 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3111 return True
3113 class HelpFormatter(optparse.IndentedHelpFormatter):
3114 def __init__(self):
3115 optparse.IndentedHelpFormatter.__init__(self)
3117 def format_description(self, description):
3118 if description:
3119 return description + "\n"
3120 else:
3121 return ""
3123 def printUsage(commands):
3124 print "usage: %s <command> [options]" % sys.argv[0]
3125 print ""
3126 print "valid commands: %s" % ", ".join(commands)
3127 print ""
3128 print "Try %s <command> --help for command specific help." % sys.argv[0]
3129 print ""
3131 commands = {
3132 "debug" : P4Debug,
3133 "submit" : P4Submit,
3134 "commit" : P4Submit,
3135 "sync" : P4Sync,
3136 "rebase" : P4Rebase,
3137 "clone" : P4Clone,
3138 "rollback" : P4RollBack,
3139 "branches" : P4Branches
3143 def main():
3144 if len(sys.argv[1:]) == 0:
3145 printUsage(commands.keys())
3146 sys.exit(2)
3148 cmd = ""
3149 cmdName = sys.argv[1]
3150 try:
3151 klass = commands[cmdName]
3152 cmd = klass()
3153 except KeyError:
3154 print "unknown command %s" % cmdName
3155 print ""
3156 printUsage(commands.keys())
3157 sys.exit(2)
3159 options = cmd.options
3160 cmd.gitdir = os.environ.get("GIT_DIR", None)
3162 args = sys.argv[2:]
3164 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3165 if cmd.needsGit:
3166 options.append(optparse.make_option("--git-dir", dest="gitdir"))
3168 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3169 options,
3170 description = cmd.description,
3171 formatter = HelpFormatter())
3173 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3174 global verbose
3175 verbose = cmd.verbose
3176 if cmd.needsGit:
3177 if cmd.gitdir == None:
3178 cmd.gitdir = os.path.abspath(".git")
3179 if not isValidGitDir(cmd.gitdir):
3180 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3181 if os.path.exists(cmd.gitdir):
3182 cdup = read_pipe("git rev-parse --show-cdup").strip()
3183 if len(cdup) > 0:
3184 chdir(cdup);
3186 if not isValidGitDir(cmd.gitdir):
3187 if isValidGitDir(cmd.gitdir + "/.git"):
3188 cmd.gitdir += "/.git"
3189 else:
3190 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3192 os.environ["GIT_DIR"] = cmd.gitdir
3194 if not cmd.run(args):
3195 parser.print_help()
3196 sys.exit(2)
3199 if __name__ == '__main__':
3200 main()