rebase -i: remember merge options beyond continue actions
[git/debian.git] / git-p4.py
bloba79b6d82ab5ac8def38e7a213f0b990f4688379a
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>
10 import sys
11 if sys.hexversion < 0x02040000:
12 # The limiter is the subprocess module
13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n")
14 sys.exit(1)
15 import os
16 import optparse
17 import marshal
18 import subprocess
19 import tempfile
20 import time
21 import platform
22 import re
23 import shutil
24 import stat
26 try:
27 from subprocess import CalledProcessError
28 except ImportError:
29 # from python2.7:subprocess.py
30 # Exception classes used by this module.
31 class CalledProcessError(Exception):
32 """This exception is raised when a process run by check_call() returns
33 a non-zero exit status. The exit status will be stored in the
34 returncode attribute."""
35 def __init__(self, returncode, cmd):
36 self.returncode = returncode
37 self.cmd = cmd
38 def __str__(self):
39 return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
41 verbose = False
43 # Only labels/tags matching this will be imported/exported
44 defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
46 # Grab changes in blocks of this many revisions, unless otherwise requested
47 defaultBlockSize = 512
49 def p4_build_cmd(cmd):
50 """Build a suitable p4 command line.
52 This consolidates building and returning a p4 command line into one
53 location. It means that hooking into the environment, or other configuration
54 can be done more easily.
55 """
56 real_cmd = ["p4"]
58 user = gitConfig("git-p4.user")
59 if len(user) > 0:
60 real_cmd += ["-u",user]
62 password = gitConfig("git-p4.password")
63 if len(password) > 0:
64 real_cmd += ["-P", password]
66 port = gitConfig("git-p4.port")
67 if len(port) > 0:
68 real_cmd += ["-p", port]
70 host = gitConfig("git-p4.host")
71 if len(host) > 0:
72 real_cmd += ["-H", host]
74 client = gitConfig("git-p4.client")
75 if len(client) > 0:
76 real_cmd += ["-c", client]
79 if isinstance(cmd,basestring):
80 real_cmd = ' '.join(real_cmd) + ' ' + cmd
81 else:
82 real_cmd += cmd
83 return real_cmd
85 def chdir(path, is_client_path=False):
86 """Do chdir to the given path, and set the PWD environment
87 variable for use by P4. It does not look at getcwd() output.
88 Since we're not using the shell, it is necessary to set the
89 PWD environment variable explicitly.
91 Normally, expand the path to force it to be absolute. This
92 addresses the use of relative path names inside P4 settings,
93 e.g. P4CONFIG=.p4config. P4 does not simply open the filename
94 as given; it looks for .p4config using PWD.
96 If is_client_path, the path was handed to us directly by p4,
97 and may be a symbolic link. Do not call os.getcwd() in this
98 case, because it will cause p4 to think that PWD is not inside
99 the client path.
102 os.chdir(path)
103 if not is_client_path:
104 path = os.getcwd()
105 os.environ['PWD'] = path
107 def die(msg):
108 if verbose:
109 raise Exception(msg)
110 else:
111 sys.stderr.write(msg + "\n")
112 sys.exit(1)
114 def write_pipe(c, stdin):
115 if verbose:
116 sys.stderr.write('Writing pipe: %s\n' % str(c))
118 expand = isinstance(c,basestring)
119 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
120 pipe = p.stdin
121 val = pipe.write(stdin)
122 pipe.close()
123 if p.wait():
124 die('Command failed: %s' % str(c))
126 return val
128 def p4_write_pipe(c, stdin):
129 real_cmd = p4_build_cmd(c)
130 return write_pipe(real_cmd, stdin)
132 def read_pipe(c, ignore_error=False):
133 if verbose:
134 sys.stderr.write('Reading pipe: %s\n' % str(c))
136 expand = isinstance(c,basestring)
137 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
138 (out, err) = p.communicate()
139 if p.returncode != 0 and not ignore_error:
140 die('Command failed: %s\nError: %s' % (str(c), err))
141 return out
143 def p4_read_pipe(c, ignore_error=False):
144 real_cmd = p4_build_cmd(c)
145 return read_pipe(real_cmd, ignore_error)
147 def read_pipe_lines(c):
148 if verbose:
149 sys.stderr.write('Reading pipe: %s\n' % str(c))
151 expand = isinstance(c, basestring)
152 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
153 pipe = p.stdout
154 val = pipe.readlines()
155 if pipe.close() or p.wait():
156 die('Command failed: %s' % str(c))
158 return val
160 def p4_read_pipe_lines(c):
161 """Specifically invoke p4 on the command supplied. """
162 real_cmd = p4_build_cmd(c)
163 return read_pipe_lines(real_cmd)
165 def p4_has_command(cmd):
166 """Ask p4 for help on this command. If it returns an error, the
167 command does not exist in this version of p4."""
168 real_cmd = p4_build_cmd(["help", cmd])
169 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
170 stderr=subprocess.PIPE)
171 p.communicate()
172 return p.returncode == 0
174 def p4_has_move_command():
175 """See if the move command exists, that it supports -k, and that
176 it has not been administratively disabled. The arguments
177 must be correct, but the filenames do not have to exist. Use
178 ones with wildcards so even if they exist, it will fail."""
180 if not p4_has_command("move"):
181 return False
182 cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
183 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
184 (out, err) = p.communicate()
185 # return code will be 1 in either case
186 if err.find("Invalid option") >= 0:
187 return False
188 if err.find("disabled") >= 0:
189 return False
190 # assume it failed because @... was invalid changelist
191 return True
193 def system(cmd, ignore_error=False):
194 expand = isinstance(cmd,basestring)
195 if verbose:
196 sys.stderr.write("executing %s\n" % str(cmd))
197 retcode = subprocess.call(cmd, shell=expand)
198 if retcode and not ignore_error:
199 raise CalledProcessError(retcode, cmd)
201 return retcode
203 def p4_system(cmd):
204 """Specifically invoke p4 as the system command. """
205 real_cmd = p4_build_cmd(cmd)
206 expand = isinstance(real_cmd, basestring)
207 retcode = subprocess.call(real_cmd, shell=expand)
208 if retcode:
209 raise CalledProcessError(retcode, real_cmd)
211 _p4_version_string = None
212 def p4_version_string():
213 """Read the version string, showing just the last line, which
214 hopefully is the interesting version bit.
216 $ p4 -V
217 Perforce - The Fast Software Configuration Management System.
218 Copyright 1995-2011 Perforce Software. All rights reserved.
219 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
221 global _p4_version_string
222 if not _p4_version_string:
223 a = p4_read_pipe_lines(["-V"])
224 _p4_version_string = a[-1].rstrip()
225 return _p4_version_string
227 def p4_integrate(src, dest):
228 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
230 def p4_sync(f, *options):
231 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
233 def p4_add(f):
234 # forcibly add file names with wildcards
235 if wildcard_present(f):
236 p4_system(["add", "-f", f])
237 else:
238 p4_system(["add", f])
240 def p4_delete(f):
241 p4_system(["delete", wildcard_encode(f)])
243 def p4_edit(f):
244 p4_system(["edit", wildcard_encode(f)])
246 def p4_revert(f):
247 p4_system(["revert", wildcard_encode(f)])
249 def p4_reopen(type, f):
250 p4_system(["reopen", "-t", type, wildcard_encode(f)])
252 def p4_move(src, dest):
253 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
255 def p4_last_change():
256 results = p4CmdList(["changes", "-m", "1"])
257 return int(results[0]['change'])
259 def p4_describe(change):
260 """Make sure it returns a valid result by checking for
261 the presence of field "time". Return a dict of the
262 results."""
264 ds = p4CmdList(["describe", "-s", str(change)])
265 if len(ds) != 1:
266 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
268 d = ds[0]
270 if "p4ExitCode" in d:
271 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
272 str(d)))
273 if "code" in d:
274 if d["code"] == "error":
275 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
277 if "time" not in d:
278 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
280 return d
283 # Canonicalize the p4 type and return a tuple of the
284 # base type, plus any modifiers. See "p4 help filetypes"
285 # for a list and explanation.
287 def split_p4_type(p4type):
289 p4_filetypes_historical = {
290 "ctempobj": "binary+Sw",
291 "ctext": "text+C",
292 "cxtext": "text+Cx",
293 "ktext": "text+k",
294 "kxtext": "text+kx",
295 "ltext": "text+F",
296 "tempobj": "binary+FSw",
297 "ubinary": "binary+F",
298 "uresource": "resource+F",
299 "uxbinary": "binary+Fx",
300 "xbinary": "binary+x",
301 "xltext": "text+Fx",
302 "xtempobj": "binary+Swx",
303 "xtext": "text+x",
304 "xunicode": "unicode+x",
305 "xutf16": "utf16+x",
307 if p4type in p4_filetypes_historical:
308 p4type = p4_filetypes_historical[p4type]
309 mods = ""
310 s = p4type.split("+")
311 base = s[0]
312 mods = ""
313 if len(s) > 1:
314 mods = s[1]
315 return (base, mods)
318 # return the raw p4 type of a file (text, text+ko, etc)
320 def p4_type(f):
321 results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
322 return results[0]['headType']
325 # Given a type base and modifier, return a regexp matching
326 # the keywords that can be expanded in the file
328 def p4_keywords_regexp_for_type(base, type_mods):
329 if base in ("text", "unicode", "binary"):
330 kwords = None
331 if "ko" in type_mods:
332 kwords = 'Id|Header'
333 elif "k" in type_mods:
334 kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
335 else:
336 return None
337 pattern = r"""
338 \$ # Starts with a dollar, followed by...
339 (%s) # one of the keywords, followed by...
340 (:[^$\n]+)? # possibly an old expansion, followed by...
341 \$ # another dollar
342 """ % kwords
343 return pattern
344 else:
345 return None
348 # Given a file, return a regexp matching the possible
349 # RCS keywords that will be expanded, or None for files
350 # with kw expansion turned off.
352 def p4_keywords_regexp_for_file(file):
353 if not os.path.exists(file):
354 return None
355 else:
356 (type_base, type_mods) = split_p4_type(p4_type(file))
357 return p4_keywords_regexp_for_type(type_base, type_mods)
359 def setP4ExecBit(file, mode):
360 # Reopens an already open file and changes the execute bit to match
361 # the execute bit setting in the passed in mode.
363 p4Type = "+x"
365 if not isModeExec(mode):
366 p4Type = getP4OpenedType(file)
367 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
368 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
369 if p4Type[-1] == "+":
370 p4Type = p4Type[0:-1]
372 p4_reopen(p4Type, file)
374 def getP4OpenedType(file):
375 # Returns the perforce file type for the given file.
377 result = p4_read_pipe(["opened", wildcard_encode(file)])
378 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
379 if match:
380 return match.group(1)
381 else:
382 die("Could not determine file type for %s (result: '%s')" % (file, result))
384 # Return the set of all p4 labels
385 def getP4Labels(depotPaths):
386 labels = set()
387 if isinstance(depotPaths,basestring):
388 depotPaths = [depotPaths]
390 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
391 label = l['label']
392 labels.add(label)
394 return labels
396 # Return the set of all git tags
397 def getGitTags():
398 gitTags = set()
399 for line in read_pipe_lines(["git", "tag"]):
400 tag = line.strip()
401 gitTags.add(tag)
402 return gitTags
404 def diffTreePattern():
405 # This is a simple generator for the diff tree regex pattern. This could be
406 # a class variable if this and parseDiffTreeEntry were a part of a class.
407 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
408 while True:
409 yield pattern
411 def parseDiffTreeEntry(entry):
412 """Parses a single diff tree entry into its component elements.
414 See git-diff-tree(1) manpage for details about the format of the diff
415 output. This method returns a dictionary with the following elements:
417 src_mode - The mode of the source file
418 dst_mode - The mode of the destination file
419 src_sha1 - The sha1 for the source file
420 dst_sha1 - The sha1 fr the destination file
421 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
422 status_score - The score for the status (applicable for 'C' and 'R'
423 statuses). This is None if there is no score.
424 src - The path for the source file.
425 dst - The path for the destination file. This is only present for
426 copy or renames. If it is not present, this is None.
428 If the pattern is not matched, None is returned."""
430 match = diffTreePattern().next().match(entry)
431 if match:
432 return {
433 'src_mode': match.group(1),
434 'dst_mode': match.group(2),
435 'src_sha1': match.group(3),
436 'dst_sha1': match.group(4),
437 'status': match.group(5),
438 'status_score': match.group(6),
439 'src': match.group(7),
440 'dst': match.group(10)
442 return None
444 def isModeExec(mode):
445 # Returns True if the given git mode represents an executable file,
446 # otherwise False.
447 return mode[-3:] == "755"
449 def isModeExecChanged(src_mode, dst_mode):
450 return isModeExec(src_mode) != isModeExec(dst_mode)
452 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
454 if isinstance(cmd,basestring):
455 cmd = "-G " + cmd
456 expand = True
457 else:
458 cmd = ["-G"] + cmd
459 expand = False
461 cmd = p4_build_cmd(cmd)
462 if verbose:
463 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
465 # Use a temporary file to avoid deadlocks without
466 # subprocess.communicate(), which would put another copy
467 # of stdout into memory.
468 stdin_file = None
469 if stdin is not None:
470 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
471 if isinstance(stdin,basestring):
472 stdin_file.write(stdin)
473 else:
474 for i in stdin:
475 stdin_file.write(i + '\n')
476 stdin_file.flush()
477 stdin_file.seek(0)
479 p4 = subprocess.Popen(cmd,
480 shell=expand,
481 stdin=stdin_file,
482 stdout=subprocess.PIPE)
484 result = []
485 try:
486 while True:
487 entry = marshal.load(p4.stdout)
488 if cb is not None:
489 cb(entry)
490 else:
491 result.append(entry)
492 except EOFError:
493 pass
494 exitCode = p4.wait()
495 if exitCode != 0:
496 entry = {}
497 entry["p4ExitCode"] = exitCode
498 result.append(entry)
500 return result
502 def p4Cmd(cmd):
503 list = p4CmdList(cmd)
504 result = {}
505 for entry in list:
506 result.update(entry)
507 return result;
509 def p4Where(depotPath):
510 if not depotPath.endswith("/"):
511 depotPath += "/"
512 depotPathLong = depotPath + "..."
513 outputList = p4CmdList(["where", depotPathLong])
514 output = None
515 for entry in outputList:
516 if "depotFile" in entry:
517 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
518 # The base path always ends with "/...".
519 if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
520 output = entry
521 break
522 elif "data" in entry:
523 data = entry.get("data")
524 space = data.find(" ")
525 if data[:space] == depotPath:
526 output = entry
527 break
528 if output == None:
529 return ""
530 if output["code"] == "error":
531 return ""
532 clientPath = ""
533 if "path" in output:
534 clientPath = output.get("path")
535 elif "data" in output:
536 data = output.get("data")
537 lastSpace = data.rfind(" ")
538 clientPath = data[lastSpace + 1:]
540 if clientPath.endswith("..."):
541 clientPath = clientPath[:-3]
542 return clientPath
544 def currentGitBranch():
545 retcode = system(["git", "symbolic-ref", "-q", "HEAD"], ignore_error=True)
546 if retcode != 0:
547 # on a detached head
548 return None
549 else:
550 return read_pipe(["git", "name-rev", "HEAD"]).split(" ")[1].strip()
552 def isValidGitDir(path):
553 if (os.path.exists(path + "/HEAD")
554 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
555 return True;
556 return False
558 def parseRevision(ref):
559 return read_pipe("git rev-parse %s" % ref).strip()
561 def branchExists(ref):
562 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
563 ignore_error=True)
564 return len(rev) > 0
566 def extractLogMessageFromGitCommit(commit):
567 logMessage = ""
569 ## fixme: title is first line of commit, not 1st paragraph.
570 foundTitle = False
571 for log in read_pipe_lines("git cat-file commit %s" % commit):
572 if not foundTitle:
573 if len(log) == 1:
574 foundTitle = True
575 continue
577 logMessage += log
578 return logMessage
580 def extractSettingsGitLog(log):
581 values = {}
582 for line in log.split("\n"):
583 line = line.strip()
584 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
585 if not m:
586 continue
588 assignments = m.group(1).split (':')
589 for a in assignments:
590 vals = a.split ('=')
591 key = vals[0].strip()
592 val = ('='.join (vals[1:])).strip()
593 if val.endswith ('\"') and val.startswith('"'):
594 val = val[1:-1]
596 values[key] = val
598 paths = values.get("depot-paths")
599 if not paths:
600 paths = values.get("depot-path")
601 if paths:
602 values['depot-paths'] = paths.split(',')
603 return values
605 def gitBranchExists(branch):
606 proc = subprocess.Popen(["git", "rev-parse", branch],
607 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
608 return proc.wait() == 0;
610 _gitConfig = {}
612 def gitConfig(key):
613 if not _gitConfig.has_key(key):
614 cmd = [ "git", "config", key ]
615 s = read_pipe(cmd, ignore_error=True)
616 _gitConfig[key] = s.strip()
617 return _gitConfig[key]
619 def gitConfigBool(key):
620 """Return a bool, using git config --bool. It is True only if the
621 variable is set to true, and False if set to false or not present
622 in the config."""
624 if not _gitConfig.has_key(key):
625 cmd = [ "git", "config", "--bool", key ]
626 s = read_pipe(cmd, ignore_error=True)
627 v = s.strip()
628 _gitConfig[key] = v == "true"
629 return _gitConfig[key]
631 def gitConfigList(key):
632 if not _gitConfig.has_key(key):
633 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
634 _gitConfig[key] = s.strip().split(os.linesep)
635 return _gitConfig[key]
637 def p4BranchesInGit(branchesAreInRemotes=True):
638 """Find all the branches whose names start with "p4/", looking
639 in remotes or heads as specified by the argument. Return
640 a dictionary of { branch: revision } for each one found.
641 The branch names are the short names, without any
642 "p4/" prefix."""
644 branches = {}
646 cmdline = "git rev-parse --symbolic "
647 if branchesAreInRemotes:
648 cmdline += "--remotes"
649 else:
650 cmdline += "--branches"
652 for line in read_pipe_lines(cmdline):
653 line = line.strip()
655 # only import to p4/
656 if not line.startswith('p4/'):
657 continue
658 # special symbolic ref to p4/master
659 if line == "p4/HEAD":
660 continue
662 # strip off p4/ prefix
663 branch = line[len("p4/"):]
665 branches[branch] = parseRevision(line)
667 return branches
669 def branch_exists(branch):
670 """Make sure that the given ref name really exists."""
672 cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
673 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
674 out, _ = p.communicate()
675 if p.returncode:
676 return False
677 # expect exactly one line of output: the branch name
678 return out.rstrip() == branch
680 def findUpstreamBranchPoint(head = "HEAD"):
681 branches = p4BranchesInGit()
682 # map from depot-path to branch name
683 branchByDepotPath = {}
684 for branch in branches.keys():
685 tip = branches[branch]
686 log = extractLogMessageFromGitCommit(tip)
687 settings = extractSettingsGitLog(log)
688 if settings.has_key("depot-paths"):
689 paths = ",".join(settings["depot-paths"])
690 branchByDepotPath[paths] = "remotes/p4/" + branch
692 settings = None
693 parent = 0
694 while parent < 65535:
695 commit = head + "~%s" % parent
696 log = extractLogMessageFromGitCommit(commit)
697 settings = extractSettingsGitLog(log)
698 if settings.has_key("depot-paths"):
699 paths = ",".join(settings["depot-paths"])
700 if branchByDepotPath.has_key(paths):
701 return [branchByDepotPath[paths], settings]
703 parent = parent + 1
705 return ["", settings]
707 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
708 if not silent:
709 print ("Creating/updating branch(es) in %s based on origin branch(es)"
710 % localRefPrefix)
712 originPrefix = "origin/p4/"
714 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
715 line = line.strip()
716 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
717 continue
719 headName = line[len(originPrefix):]
720 remoteHead = localRefPrefix + headName
721 originHead = line
723 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
724 if (not original.has_key('depot-paths')
725 or not original.has_key('change')):
726 continue
728 update = False
729 if not gitBranchExists(remoteHead):
730 if verbose:
731 print "creating %s" % remoteHead
732 update = True
733 else:
734 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
735 if settings.has_key('change') > 0:
736 if settings['depot-paths'] == original['depot-paths']:
737 originP4Change = int(original['change'])
738 p4Change = int(settings['change'])
739 if originP4Change > p4Change:
740 print ("%s (%s) is newer than %s (%s). "
741 "Updating p4 branch from origin."
742 % (originHead, originP4Change,
743 remoteHead, p4Change))
744 update = True
745 else:
746 print ("Ignoring: %s was imported from %s while "
747 "%s was imported from %s"
748 % (originHead, ','.join(original['depot-paths']),
749 remoteHead, ','.join(settings['depot-paths'])))
751 if update:
752 system("git update-ref %s %s" % (remoteHead, originHead))
754 def originP4BranchesExist():
755 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
758 def p4ParseNumericChangeRange(parts):
759 changeStart = int(parts[0][1:])
760 if parts[1] == '#head':
761 changeEnd = p4_last_change()
762 else:
763 changeEnd = int(parts[1])
765 return (changeStart, changeEnd)
767 def chooseBlockSize(blockSize):
768 if blockSize:
769 return blockSize
770 else:
771 return defaultBlockSize
773 def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
774 assert depotPaths
776 # Parse the change range into start and end. Try to find integer
777 # revision ranges as these can be broken up into blocks to avoid
778 # hitting server-side limits (maxrows, maxscanresults). But if
779 # that doesn't work, fall back to using the raw revision specifier
780 # strings, without using block mode.
782 if changeRange is None or changeRange == '':
783 changeStart = 1
784 changeEnd = p4_last_change()
785 block_size = chooseBlockSize(requestedBlockSize)
786 else:
787 parts = changeRange.split(',')
788 assert len(parts) == 2
789 try:
790 (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
791 block_size = chooseBlockSize(requestedBlockSize)
792 except:
793 changeStart = parts[0][1:]
794 changeEnd = parts[1]
795 if requestedBlockSize:
796 die("cannot use --changes-block-size with non-numeric revisions")
797 block_size = None
799 # Accumulate change numbers in a dictionary to avoid duplicates
800 changes = {}
802 for p in depotPaths:
803 # Retrieve changes a block at a time, to prevent running
804 # into a MaxResults/MaxScanRows error from the server.
806 while True:
807 cmd = ['changes']
809 if block_size:
810 end = min(changeEnd, changeStart + block_size)
811 revisionRange = "%d,%d" % (changeStart, end)
812 else:
813 revisionRange = "%s,%s" % (changeStart, changeEnd)
815 cmd += ["%s...@%s" % (p, revisionRange)]
817 for line in p4_read_pipe_lines(cmd):
818 changeNum = int(line.split(" ")[1])
819 changes[changeNum] = True
821 if not block_size:
822 break
824 if end >= changeEnd:
825 break
827 changeStart = end + 1
829 changelist = changes.keys()
830 changelist.sort()
831 return changelist
833 def p4PathStartsWith(path, prefix):
834 # This method tries to remedy a potential mixed-case issue:
836 # If UserA adds //depot/DirA/file1
837 # and UserB adds //depot/dira/file2
839 # we may or may not have a problem. If you have core.ignorecase=true,
840 # we treat DirA and dira as the same directory
841 if gitConfigBool("core.ignorecase"):
842 return path.lower().startswith(prefix.lower())
843 return path.startswith(prefix)
845 def getClientSpec():
846 """Look at the p4 client spec, create a View() object that contains
847 all the mappings, and return it."""
849 specList = p4CmdList("client -o")
850 if len(specList) != 1:
851 die('Output from "client -o" is %d lines, expecting 1' %
852 len(specList))
854 # dictionary of all client parameters
855 entry = specList[0]
857 # the //client/ name
858 client_name = entry["Client"]
860 # just the keys that start with "View"
861 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
863 # hold this new View
864 view = View(client_name)
866 # append the lines, in order, to the view
867 for view_num in range(len(view_keys)):
868 k = "View%d" % view_num
869 if k not in view_keys:
870 die("Expected view key %s missing" % k)
871 view.append(entry[k])
873 return view
875 def getClientRoot():
876 """Grab the client directory."""
878 output = p4CmdList("client -o")
879 if len(output) != 1:
880 die('Output from "client -o" is %d lines, expecting 1' % len(output))
882 entry = output[0]
883 if "Root" not in entry:
884 die('Client has no "Root"')
886 return entry["Root"]
889 # P4 wildcards are not allowed in filenames. P4 complains
890 # if you simply add them, but you can force it with "-f", in
891 # which case it translates them into %xx encoding internally.
893 def wildcard_decode(path):
894 # Search for and fix just these four characters. Do % last so
895 # that fixing it does not inadvertently create new %-escapes.
896 # Cannot have * in a filename in windows; untested as to
897 # what p4 would do in such a case.
898 if not platform.system() == "Windows":
899 path = path.replace("%2A", "*")
900 path = path.replace("%23", "#") \
901 .replace("%40", "@") \
902 .replace("%25", "%")
903 return path
905 def wildcard_encode(path):
906 # do % first to avoid double-encoding the %s introduced here
907 path = path.replace("%", "%25") \
908 .replace("*", "%2A") \
909 .replace("#", "%23") \
910 .replace("@", "%40")
911 return path
913 def wildcard_present(path):
914 m = re.search("[*#@%]", path)
915 return m is not None
917 class Command:
918 def __init__(self):
919 self.usage = "usage: %prog [options]"
920 self.needsGit = True
921 self.verbose = False
923 class P4UserMap:
924 def __init__(self):
925 self.userMapFromPerforceServer = False
926 self.myP4UserId = None
928 def p4UserId(self):
929 if self.myP4UserId:
930 return self.myP4UserId
932 results = p4CmdList("user -o")
933 for r in results:
934 if r.has_key('User'):
935 self.myP4UserId = r['User']
936 return r['User']
937 die("Could not find your p4 user id")
939 def p4UserIsMe(self, p4User):
940 # return True if the given p4 user is actually me
941 me = self.p4UserId()
942 if not p4User or p4User != me:
943 return False
944 else:
945 return True
947 def getUserCacheFilename(self):
948 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
949 return home + "/.gitp4-usercache.txt"
951 def getUserMapFromPerforceServer(self):
952 if self.userMapFromPerforceServer:
953 return
954 self.users = {}
955 self.emails = {}
957 for output in p4CmdList("users"):
958 if not output.has_key("User"):
959 continue
960 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
961 self.emails[output["Email"]] = output["User"]
964 s = ''
965 for (key, val) in self.users.items():
966 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
968 open(self.getUserCacheFilename(), "wb").write(s)
969 self.userMapFromPerforceServer = True
971 def loadUserMapFromCache(self):
972 self.users = {}
973 self.userMapFromPerforceServer = False
974 try:
975 cache = open(self.getUserCacheFilename(), "rb")
976 lines = cache.readlines()
977 cache.close()
978 for line in lines:
979 entry = line.strip().split("\t")
980 self.users[entry[0]] = entry[1]
981 except IOError:
982 self.getUserMapFromPerforceServer()
984 class P4Debug(Command):
985 def __init__(self):
986 Command.__init__(self)
987 self.options = []
988 self.description = "A tool to debug the output of p4 -G."
989 self.needsGit = False
991 def run(self, args):
992 j = 0
993 for output in p4CmdList(args):
994 print 'Element: %d' % j
995 j += 1
996 print output
997 return True
999 class P4RollBack(Command):
1000 def __init__(self):
1001 Command.__init__(self)
1002 self.options = [
1003 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
1005 self.description = "A tool to debug the multi-branch import. Don't use :)"
1006 self.rollbackLocalBranches = False
1008 def run(self, args):
1009 if len(args) != 1:
1010 return False
1011 maxChange = int(args[0])
1013 if "p4ExitCode" in p4Cmd("changes -m 1"):
1014 die("Problems executing p4");
1016 if self.rollbackLocalBranches:
1017 refPrefix = "refs/heads/"
1018 lines = read_pipe_lines("git rev-parse --symbolic --branches")
1019 else:
1020 refPrefix = "refs/remotes/"
1021 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1023 for line in lines:
1024 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1025 line = line.strip()
1026 ref = refPrefix + line
1027 log = extractLogMessageFromGitCommit(ref)
1028 settings = extractSettingsGitLog(log)
1030 depotPaths = settings['depot-paths']
1031 change = settings['change']
1033 changed = False
1035 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
1036 for p in depotPaths]))) == 0:
1037 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
1038 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1039 continue
1041 while change and int(change) > maxChange:
1042 changed = True
1043 if self.verbose:
1044 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
1045 system("git update-ref %s \"%s^\"" % (ref, ref))
1046 log = extractLogMessageFromGitCommit(ref)
1047 settings = extractSettingsGitLog(log)
1050 depotPaths = settings['depot-paths']
1051 change = settings['change']
1053 if changed:
1054 print "%s rewound to %s" % (ref, change)
1056 return True
1058 class P4Submit(Command, P4UserMap):
1060 conflict_behavior_choices = ("ask", "skip", "quit")
1062 def __init__(self):
1063 Command.__init__(self)
1064 P4UserMap.__init__(self)
1065 self.options = [
1066 optparse.make_option("--origin", dest="origin"),
1067 optparse.make_option("-M", dest="detectRenames", action="store_true"),
1068 # preserve the user, requires relevant p4 permissions
1069 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1070 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1071 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1072 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1073 optparse.make_option("--conflict", dest="conflict_behavior",
1074 choices=self.conflict_behavior_choices),
1075 optparse.make_option("--branch", dest="branch"),
1077 self.description = "Submit changes from git to the perforce depot."
1078 self.usage += " [name of git branch to submit into perforce depot]"
1079 self.origin = ""
1080 self.detectRenames = False
1081 self.preserveUser = gitConfigBool("git-p4.preserveUser")
1082 self.dry_run = False
1083 self.prepare_p4_only = False
1084 self.conflict_behavior = None
1085 self.isWindows = (platform.system() == "Windows")
1086 self.exportLabels = False
1087 self.p4HasMoveCommand = p4_has_move_command()
1088 self.branch = None
1090 def check(self):
1091 if len(p4CmdList("opened ...")) > 0:
1092 die("You have files opened with perforce! Close them before starting the sync.")
1094 def separate_jobs_from_description(self, message):
1095 """Extract and return a possible Jobs field in the commit
1096 message. It goes into a separate section in the p4 change
1097 specification.
1099 A jobs line starts with "Jobs:" and looks like a new field
1100 in a form. Values are white-space separated on the same
1101 line or on following lines that start with a tab.
1103 This does not parse and extract the full git commit message
1104 like a p4 form. It just sees the Jobs: line as a marker
1105 to pass everything from then on directly into the p4 form,
1106 but outside the description section.
1108 Return a tuple (stripped log message, jobs string)."""
1110 m = re.search(r'^Jobs:', message, re.MULTILINE)
1111 if m is None:
1112 return (message, None)
1114 jobtext = message[m.start():]
1115 stripped_message = message[:m.start()].rstrip()
1116 return (stripped_message, jobtext)
1118 def prepareLogMessage(self, template, message, jobs):
1119 """Edits the template returned from "p4 change -o" to insert
1120 the message in the Description field, and the jobs text in
1121 the Jobs field."""
1122 result = ""
1124 inDescriptionSection = False
1126 for line in template.split("\n"):
1127 if line.startswith("#"):
1128 result += line + "\n"
1129 continue
1131 if inDescriptionSection:
1132 if line.startswith("Files:") or line.startswith("Jobs:"):
1133 inDescriptionSection = False
1134 # insert Jobs section
1135 if jobs:
1136 result += jobs + "\n"
1137 else:
1138 continue
1139 else:
1140 if line.startswith("Description:"):
1141 inDescriptionSection = True
1142 line += "\n"
1143 for messageLine in message.split("\n"):
1144 line += "\t" + messageLine + "\n"
1146 result += line + "\n"
1148 return result
1150 def patchRCSKeywords(self, file, pattern):
1151 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1152 (handle, outFileName) = tempfile.mkstemp(dir='.')
1153 try:
1154 outFile = os.fdopen(handle, "w+")
1155 inFile = open(file, "r")
1156 regexp = re.compile(pattern, re.VERBOSE)
1157 for line in inFile.readlines():
1158 line = regexp.sub(r'$\1$', line)
1159 outFile.write(line)
1160 inFile.close()
1161 outFile.close()
1162 # Forcibly overwrite the original file
1163 os.unlink(file)
1164 shutil.move(outFileName, file)
1165 except:
1166 # cleanup our temporary file
1167 os.unlink(outFileName)
1168 print "Failed to strip RCS keywords in %s" % file
1169 raise
1171 print "Patched up RCS keywords in %s" % file
1173 def p4UserForCommit(self,id):
1174 # Return the tuple (perforce user,git email) for a given git commit id
1175 self.getUserMapFromPerforceServer()
1176 gitEmail = read_pipe(["git", "log", "--max-count=1",
1177 "--format=%ae", id])
1178 gitEmail = gitEmail.strip()
1179 if not self.emails.has_key(gitEmail):
1180 return (None,gitEmail)
1181 else:
1182 return (self.emails[gitEmail],gitEmail)
1184 def checkValidP4Users(self,commits):
1185 # check if any git authors cannot be mapped to p4 users
1186 for id in commits:
1187 (user,email) = self.p4UserForCommit(id)
1188 if not user:
1189 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1190 if gitConfigBool("git-p4.allowMissingP4Users"):
1191 print "%s" % msg
1192 else:
1193 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1195 def lastP4Changelist(self):
1196 # Get back the last changelist number submitted in this client spec. This
1197 # then gets used to patch up the username in the change. If the same
1198 # client spec is being used by multiple processes then this might go
1199 # wrong.
1200 results = p4CmdList("client -o") # find the current client
1201 client = None
1202 for r in results:
1203 if r.has_key('Client'):
1204 client = r['Client']
1205 break
1206 if not client:
1207 die("could not get client spec")
1208 results = p4CmdList(["changes", "-c", client, "-m", "1"])
1209 for r in results:
1210 if r.has_key('change'):
1211 return r['change']
1212 die("Could not get changelist number for last submit - cannot patch up user details")
1214 def modifyChangelistUser(self, changelist, newUser):
1215 # fixup the user field of a changelist after it has been submitted.
1216 changes = p4CmdList("change -o %s" % changelist)
1217 if len(changes) != 1:
1218 die("Bad output from p4 change modifying %s to user %s" %
1219 (changelist, newUser))
1221 c = changes[0]
1222 if c['User'] == newUser: return # nothing to do
1223 c['User'] = newUser
1224 input = marshal.dumps(c)
1226 result = p4CmdList("change -f -i", stdin=input)
1227 for r in result:
1228 if r.has_key('code'):
1229 if r['code'] == 'error':
1230 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1231 if r.has_key('data'):
1232 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1233 return
1234 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1236 def canChangeChangelists(self):
1237 # check to see if we have p4 admin or super-user permissions, either of
1238 # which are required to modify changelists.
1239 results = p4CmdList(["protects", self.depotPath])
1240 for r in results:
1241 if r.has_key('perm'):
1242 if r['perm'] == 'admin':
1243 return 1
1244 if r['perm'] == 'super':
1245 return 1
1246 return 0
1248 def prepareSubmitTemplate(self):
1249 """Run "p4 change -o" to grab a change specification template.
1250 This does not use "p4 -G", as it is nice to keep the submission
1251 template in original order, since a human might edit it.
1253 Remove lines in the Files section that show changes to files
1254 outside the depot path we're committing into."""
1256 template = ""
1257 inFilesSection = False
1258 for line in p4_read_pipe_lines(['change', '-o']):
1259 if line.endswith("\r\n"):
1260 line = line[:-2] + "\n"
1261 if inFilesSection:
1262 if line.startswith("\t"):
1263 # path starts and ends with a tab
1264 path = line[1:]
1265 lastTab = path.rfind("\t")
1266 if lastTab != -1:
1267 path = path[:lastTab]
1268 if not p4PathStartsWith(path, self.depotPath):
1269 continue
1270 else:
1271 inFilesSection = False
1272 else:
1273 if line.startswith("Files:"):
1274 inFilesSection = True
1276 template += line
1278 return template
1280 def edit_template(self, template_file):
1281 """Invoke the editor to let the user change the submission
1282 message. Return true if okay to continue with the submit."""
1284 # if configured to skip the editing part, just submit
1285 if gitConfigBool("git-p4.skipSubmitEdit"):
1286 return True
1288 # look at the modification time, to check later if the user saved
1289 # the file
1290 mtime = os.stat(template_file).st_mtime
1292 # invoke the editor
1293 if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1294 editor = os.environ.get("P4EDITOR")
1295 else:
1296 editor = read_pipe("git var GIT_EDITOR").strip()
1297 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1299 # If the file was not saved, prompt to see if this patch should
1300 # be skipped. But skip this verification step if configured so.
1301 if gitConfigBool("git-p4.skipSubmitEditCheck"):
1302 return True
1304 # modification time updated means user saved the file
1305 if os.stat(template_file).st_mtime > mtime:
1306 return True
1308 while True:
1309 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1310 if response == 'y':
1311 return True
1312 if response == 'n':
1313 return False
1315 def get_diff_description(self, editedFiles, filesToAdd):
1316 # diff
1317 if os.environ.has_key("P4DIFF"):
1318 del(os.environ["P4DIFF"])
1319 diff = ""
1320 for editedFile in editedFiles:
1321 diff += p4_read_pipe(['diff', '-du',
1322 wildcard_encode(editedFile)])
1324 # new file diff
1325 newdiff = ""
1326 for newFile in filesToAdd:
1327 newdiff += "==== new file ====\n"
1328 newdiff += "--- /dev/null\n"
1329 newdiff += "+++ %s\n" % newFile
1330 f = open(newFile, "r")
1331 for line in f.readlines():
1332 newdiff += "+" + line
1333 f.close()
1335 return (diff + newdiff).replace('\r\n', '\n')
1337 def applyCommit(self, id):
1338 """Apply one commit, return True if it succeeded."""
1340 print "Applying", read_pipe(["git", "show", "-s",
1341 "--format=format:%h %s", id])
1343 (p4User, gitEmail) = self.p4UserForCommit(id)
1345 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1346 filesToAdd = set()
1347 filesToDelete = set()
1348 editedFiles = set()
1349 pureRenameCopy = set()
1350 filesToChangeExecBit = {}
1352 for line in diff:
1353 diff = parseDiffTreeEntry(line)
1354 modifier = diff['status']
1355 path = diff['src']
1356 if modifier == "M":
1357 p4_edit(path)
1358 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1359 filesToChangeExecBit[path] = diff['dst_mode']
1360 editedFiles.add(path)
1361 elif modifier == "A":
1362 filesToAdd.add(path)
1363 filesToChangeExecBit[path] = diff['dst_mode']
1364 if path in filesToDelete:
1365 filesToDelete.remove(path)
1366 elif modifier == "D":
1367 filesToDelete.add(path)
1368 if path in filesToAdd:
1369 filesToAdd.remove(path)
1370 elif modifier == "C":
1371 src, dest = diff['src'], diff['dst']
1372 p4_integrate(src, dest)
1373 pureRenameCopy.add(dest)
1374 if diff['src_sha1'] != diff['dst_sha1']:
1375 p4_edit(dest)
1376 pureRenameCopy.discard(dest)
1377 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1378 p4_edit(dest)
1379 pureRenameCopy.discard(dest)
1380 filesToChangeExecBit[dest] = diff['dst_mode']
1381 if self.isWindows:
1382 # turn off read-only attribute
1383 os.chmod(dest, stat.S_IWRITE)
1384 os.unlink(dest)
1385 editedFiles.add(dest)
1386 elif modifier == "R":
1387 src, dest = diff['src'], diff['dst']
1388 if self.p4HasMoveCommand:
1389 p4_edit(src) # src must be open before move
1390 p4_move(src, dest) # opens for (move/delete, move/add)
1391 else:
1392 p4_integrate(src, dest)
1393 if diff['src_sha1'] != diff['dst_sha1']:
1394 p4_edit(dest)
1395 else:
1396 pureRenameCopy.add(dest)
1397 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1398 if not self.p4HasMoveCommand:
1399 p4_edit(dest) # with move: already open, writable
1400 filesToChangeExecBit[dest] = diff['dst_mode']
1401 if not self.p4HasMoveCommand:
1402 if self.isWindows:
1403 os.chmod(dest, stat.S_IWRITE)
1404 os.unlink(dest)
1405 filesToDelete.add(src)
1406 editedFiles.add(dest)
1407 else:
1408 die("unknown modifier %s for %s" % (modifier, path))
1410 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1411 patchcmd = diffcmd + " | git apply "
1412 tryPatchCmd = patchcmd + "--check -"
1413 applyPatchCmd = patchcmd + "--check --apply -"
1414 patch_succeeded = True
1416 if os.system(tryPatchCmd) != 0:
1417 fixed_rcs_keywords = False
1418 patch_succeeded = False
1419 print "Unfortunately applying the change failed!"
1421 # Patch failed, maybe it's just RCS keyword woes. Look through
1422 # the patch to see if that's possible.
1423 if gitConfigBool("git-p4.attemptRCSCleanup"):
1424 file = None
1425 pattern = None
1426 kwfiles = {}
1427 for file in editedFiles | filesToDelete:
1428 # did this file's delta contain RCS keywords?
1429 pattern = p4_keywords_regexp_for_file(file)
1431 if pattern:
1432 # this file is a possibility...look for RCS keywords.
1433 regexp = re.compile(pattern, re.VERBOSE)
1434 for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1435 if regexp.search(line):
1436 if verbose:
1437 print "got keyword match on %s in %s in %s" % (pattern, line, file)
1438 kwfiles[file] = pattern
1439 break
1441 for file in kwfiles:
1442 if verbose:
1443 print "zapping %s with %s" % (line,pattern)
1444 # File is being deleted, so not open in p4. Must
1445 # disable the read-only bit on windows.
1446 if self.isWindows and file not in editedFiles:
1447 os.chmod(file, stat.S_IWRITE)
1448 self.patchRCSKeywords(file, kwfiles[file])
1449 fixed_rcs_keywords = True
1451 if fixed_rcs_keywords:
1452 print "Retrying the patch with RCS keywords cleaned up"
1453 if os.system(tryPatchCmd) == 0:
1454 patch_succeeded = True
1456 if not patch_succeeded:
1457 for f in editedFiles:
1458 p4_revert(f)
1459 return False
1462 # Apply the patch for real, and do add/delete/+x handling.
1464 system(applyPatchCmd)
1466 for f in filesToAdd:
1467 p4_add(f)
1468 for f in filesToDelete:
1469 p4_revert(f)
1470 p4_delete(f)
1472 # Set/clear executable bits
1473 for f in filesToChangeExecBit.keys():
1474 mode = filesToChangeExecBit[f]
1475 setP4ExecBit(f, mode)
1478 # Build p4 change description, starting with the contents
1479 # of the git commit message.
1481 logMessage = extractLogMessageFromGitCommit(id)
1482 logMessage = logMessage.strip()
1483 (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1485 template = self.prepareSubmitTemplate()
1486 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1488 if self.preserveUser:
1489 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1491 if self.checkAuthorship and not self.p4UserIsMe(p4User):
1492 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1493 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1494 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1496 separatorLine = "######## everything below this line is just the diff #######\n"
1497 if not self.prepare_p4_only:
1498 submitTemplate += separatorLine
1499 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)
1501 (handle, fileName) = tempfile.mkstemp()
1502 tmpFile = os.fdopen(handle, "w+b")
1503 if self.isWindows:
1504 submitTemplate = submitTemplate.replace("\n", "\r\n")
1505 tmpFile.write(submitTemplate)
1506 tmpFile.close()
1508 if self.prepare_p4_only:
1510 # Leave the p4 tree prepared, and the submit template around
1511 # and let the user decide what to do next
1513 print
1514 print "P4 workspace prepared for submission."
1515 print "To submit or revert, go to client workspace"
1516 print " " + self.clientPath
1517 print
1518 print "To submit, use \"p4 submit\" to write a new description,"
1519 print "or \"p4 submit -i <%s\" to use the one prepared by" \
1520 " \"git p4\"." % fileName
1521 print "You can delete the file \"%s\" when finished." % fileName
1523 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1524 print "To preserve change ownership by user %s, you must\n" \
1525 "do \"p4 change -f <change>\" after submitting and\n" \
1526 "edit the User field."
1527 if pureRenameCopy:
1528 print "After submitting, renamed files must be re-synced."
1529 print "Invoke \"p4 sync -f\" on each of these files:"
1530 for f in pureRenameCopy:
1531 print " " + f
1533 print
1534 print "To revert the changes, use \"p4 revert ...\", and delete"
1535 print "the submit template file \"%s\"" % fileName
1536 if filesToAdd:
1537 print "Since the commit adds new files, they must be deleted:"
1538 for f in filesToAdd:
1539 print " " + f
1540 print
1541 return True
1544 # Let the user edit the change description, then submit it.
1546 submitted = False
1548 try:
1549 if self.edit_template(fileName):
1550 # read the edited message and submit
1551 tmpFile = open(fileName, "rb")
1552 message = tmpFile.read()
1553 tmpFile.close()
1554 if self.isWindows:
1555 message = message.replace("\r\n", "\n")
1556 submitTemplate = message[:message.index(separatorLine)]
1557 p4_write_pipe(['submit', '-i'], submitTemplate)
1559 if self.preserveUser:
1560 if p4User:
1561 # Get last changelist number. Cannot easily get it from
1562 # the submit command output as the output is
1563 # unmarshalled.
1564 changelist = self.lastP4Changelist()
1565 self.modifyChangelistUser(changelist, p4User)
1567 # The rename/copy happened by applying a patch that created a
1568 # new file. This leaves it writable, which confuses p4.
1569 for f in pureRenameCopy:
1570 p4_sync(f, "-f")
1571 submitted = True
1573 finally:
1574 # skip this patch
1575 if not submitted:
1576 print "Submission cancelled, undoing p4 changes."
1577 for f in editedFiles:
1578 p4_revert(f)
1579 for f in filesToAdd:
1580 p4_revert(f)
1581 os.remove(f)
1582 for f in filesToDelete:
1583 p4_revert(f)
1585 os.remove(fileName)
1586 return submitted
1588 # Export git tags as p4 labels. Create a p4 label and then tag
1589 # with that.
1590 def exportGitTags(self, gitTags):
1591 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1592 if len(validLabelRegexp) == 0:
1593 validLabelRegexp = defaultLabelRegexp
1594 m = re.compile(validLabelRegexp)
1596 for name in gitTags:
1598 if not m.match(name):
1599 if verbose:
1600 print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1601 continue
1603 # Get the p4 commit this corresponds to
1604 logMessage = extractLogMessageFromGitCommit(name)
1605 values = extractSettingsGitLog(logMessage)
1607 if not values.has_key('change'):
1608 # a tag pointing to something not sent to p4; ignore
1609 if verbose:
1610 print "git tag %s does not give a p4 commit" % name
1611 continue
1612 else:
1613 changelist = values['change']
1615 # Get the tag details.
1616 inHeader = True
1617 isAnnotated = False
1618 body = []
1619 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1620 l = l.strip()
1621 if inHeader:
1622 if re.match(r'tag\s+', l):
1623 isAnnotated = True
1624 elif re.match(r'\s*$', l):
1625 inHeader = False
1626 continue
1627 else:
1628 body.append(l)
1630 if not isAnnotated:
1631 body = ["lightweight tag imported by git p4\n"]
1633 # Create the label - use the same view as the client spec we are using
1634 clientSpec = getClientSpec()
1636 labelTemplate = "Label: %s\n" % name
1637 labelTemplate += "Description:\n"
1638 for b in body:
1639 labelTemplate += "\t" + b + "\n"
1640 labelTemplate += "View:\n"
1641 for depot_side in clientSpec.mappings:
1642 labelTemplate += "\t%s\n" % depot_side
1644 if self.dry_run:
1645 print "Would create p4 label %s for tag" % name
1646 elif self.prepare_p4_only:
1647 print "Not creating p4 label %s for tag due to option" \
1648 " --prepare-p4-only" % name
1649 else:
1650 p4_write_pipe(["label", "-i"], labelTemplate)
1652 # Use the label
1653 p4_system(["tag", "-l", name] +
1654 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1656 if verbose:
1657 print "created p4 label for tag %s" % name
1659 def run(self, args):
1660 if len(args) == 0:
1661 self.master = currentGitBranch()
1662 elif len(args) == 1:
1663 self.master = args[0]
1664 if not branchExists(self.master):
1665 die("Branch %s does not exist" % self.master)
1666 else:
1667 return False
1669 if self.master:
1670 allowSubmit = gitConfig("git-p4.allowSubmit")
1671 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1672 die("%s is not in git-p4.allowSubmit" % self.master)
1674 [upstream, settings] = findUpstreamBranchPoint()
1675 self.depotPath = settings['depot-paths'][0]
1676 if len(self.origin) == 0:
1677 self.origin = upstream
1679 if self.preserveUser:
1680 if not self.canChangeChangelists():
1681 die("Cannot preserve user names without p4 super-user or admin permissions")
1683 # if not set from the command line, try the config file
1684 if self.conflict_behavior is None:
1685 val = gitConfig("git-p4.conflict")
1686 if val:
1687 if val not in self.conflict_behavior_choices:
1688 die("Invalid value '%s' for config git-p4.conflict" % val)
1689 else:
1690 val = "ask"
1691 self.conflict_behavior = val
1693 if self.verbose:
1694 print "Origin branch is " + self.origin
1696 if len(self.depotPath) == 0:
1697 print "Internal error: cannot locate perforce depot path from existing branches"
1698 sys.exit(128)
1700 self.useClientSpec = False
1701 if gitConfigBool("git-p4.useclientspec"):
1702 self.useClientSpec = True
1703 if self.useClientSpec:
1704 self.clientSpecDirs = getClientSpec()
1706 # Check for the existance of P4 branches
1707 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
1709 if self.useClientSpec and not branchesDetected:
1710 # all files are relative to the client spec
1711 self.clientPath = getClientRoot()
1712 else:
1713 self.clientPath = p4Where(self.depotPath)
1715 if self.clientPath == "":
1716 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1718 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1719 self.oldWorkingDirectory = os.getcwd()
1721 # ensure the clientPath exists
1722 new_client_dir = False
1723 if not os.path.exists(self.clientPath):
1724 new_client_dir = True
1725 os.makedirs(self.clientPath)
1727 chdir(self.clientPath, is_client_path=True)
1728 if self.dry_run:
1729 print "Would synchronize p4 checkout in %s" % self.clientPath
1730 else:
1731 print "Synchronizing p4 checkout..."
1732 if new_client_dir:
1733 # old one was destroyed, and maybe nobody told p4
1734 p4_sync("...", "-f")
1735 else:
1736 p4_sync("...")
1737 self.check()
1739 commits = []
1740 if self.master:
1741 commitish = self.master
1742 else:
1743 commitish = 'HEAD'
1745 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, commitish)]):
1746 commits.append(line.strip())
1747 commits.reverse()
1749 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1750 self.checkAuthorship = False
1751 else:
1752 self.checkAuthorship = True
1754 if self.preserveUser:
1755 self.checkValidP4Users(commits)
1758 # Build up a set of options to be passed to diff when
1759 # submitting each commit to p4.
1761 if self.detectRenames:
1762 # command-line -M arg
1763 self.diffOpts = "-M"
1764 else:
1765 # If not explicitly set check the config variable
1766 detectRenames = gitConfig("git-p4.detectRenames")
1768 if detectRenames.lower() == "false" or detectRenames == "":
1769 self.diffOpts = ""
1770 elif detectRenames.lower() == "true":
1771 self.diffOpts = "-M"
1772 else:
1773 self.diffOpts = "-M%s" % detectRenames
1775 # no command-line arg for -C or --find-copies-harder, just
1776 # config variables
1777 detectCopies = gitConfig("git-p4.detectCopies")
1778 if detectCopies.lower() == "false" or detectCopies == "":
1779 pass
1780 elif detectCopies.lower() == "true":
1781 self.diffOpts += " -C"
1782 else:
1783 self.diffOpts += " -C%s" % detectCopies
1785 if gitConfigBool("git-p4.detectCopiesHarder"):
1786 self.diffOpts += " --find-copies-harder"
1789 # Apply the commits, one at a time. On failure, ask if should
1790 # continue to try the rest of the patches, or quit.
1792 if self.dry_run:
1793 print "Would apply"
1794 applied = []
1795 last = len(commits) - 1
1796 for i, commit in enumerate(commits):
1797 if self.dry_run:
1798 print " ", read_pipe(["git", "show", "-s",
1799 "--format=format:%h %s", commit])
1800 ok = True
1801 else:
1802 ok = self.applyCommit(commit)
1803 if ok:
1804 applied.append(commit)
1805 else:
1806 if self.prepare_p4_only and i < last:
1807 print "Processing only the first commit due to option" \
1808 " --prepare-p4-only"
1809 break
1810 if i < last:
1811 quit = False
1812 while True:
1813 # prompt for what to do, or use the option/variable
1814 if self.conflict_behavior == "ask":
1815 print "What do you want to do?"
1816 response = raw_input("[s]kip this commit but apply"
1817 " the rest, or [q]uit? ")
1818 if not response:
1819 continue
1820 elif self.conflict_behavior == "skip":
1821 response = "s"
1822 elif self.conflict_behavior == "quit":
1823 response = "q"
1824 else:
1825 die("Unknown conflict_behavior '%s'" %
1826 self.conflict_behavior)
1828 if response[0] == "s":
1829 print "Skipping this commit, but applying the rest"
1830 break
1831 if response[0] == "q":
1832 print "Quitting"
1833 quit = True
1834 break
1835 if quit:
1836 break
1838 chdir(self.oldWorkingDirectory)
1840 if self.dry_run:
1841 pass
1842 elif self.prepare_p4_only:
1843 pass
1844 elif len(commits) == len(applied):
1845 print "All commits applied!"
1847 sync = P4Sync()
1848 if self.branch:
1849 sync.branch = self.branch
1850 sync.run([])
1852 rebase = P4Rebase()
1853 rebase.rebase()
1855 else:
1856 if len(applied) == 0:
1857 print "No commits applied."
1858 else:
1859 print "Applied only the commits marked with '*':"
1860 for c in commits:
1861 if c in applied:
1862 star = "*"
1863 else:
1864 star = " "
1865 print star, read_pipe(["git", "show", "-s",
1866 "--format=format:%h %s", c])
1867 print "You will have to do 'git p4 sync' and rebase."
1869 if gitConfigBool("git-p4.exportLabels"):
1870 self.exportLabels = True
1872 if self.exportLabels:
1873 p4Labels = getP4Labels(self.depotPath)
1874 gitTags = getGitTags()
1876 missingGitTags = gitTags - p4Labels
1877 self.exportGitTags(missingGitTags)
1879 # exit with error unless everything applied perfectly
1880 if len(commits) != len(applied):
1881 sys.exit(1)
1883 return True
1885 class View(object):
1886 """Represent a p4 view ("p4 help views"), and map files in a
1887 repo according to the view."""
1889 def __init__(self, client_name):
1890 self.mappings = []
1891 self.client_prefix = "//%s/" % client_name
1892 # cache results of "p4 where" to lookup client file locations
1893 self.client_spec_path_cache = {}
1895 def append(self, view_line):
1896 """Parse a view line, splitting it into depot and client
1897 sides. Append to self.mappings, preserving order. This
1898 is only needed for tag creation."""
1900 # Split the view line into exactly two words. P4 enforces
1901 # structure on these lines that simplifies this quite a bit.
1903 # Either or both words may be double-quoted.
1904 # Single quotes do not matter.
1905 # Double-quote marks cannot occur inside the words.
1906 # A + or - prefix is also inside the quotes.
1907 # There are no quotes unless they contain a space.
1908 # The line is already white-space stripped.
1909 # The two words are separated by a single space.
1911 if view_line[0] == '"':
1912 # First word is double quoted. Find its end.
1913 close_quote_index = view_line.find('"', 1)
1914 if close_quote_index <= 0:
1915 die("No first-word closing quote found: %s" % view_line)
1916 depot_side = view_line[1:close_quote_index]
1917 # skip closing quote and space
1918 rhs_index = close_quote_index + 1 + 1
1919 else:
1920 space_index = view_line.find(" ")
1921 if space_index <= 0:
1922 die("No word-splitting space found: %s" % view_line)
1923 depot_side = view_line[0:space_index]
1924 rhs_index = space_index + 1
1926 # prefix + means overlay on previous mapping
1927 if depot_side.startswith("+"):
1928 depot_side = depot_side[1:]
1930 # prefix - means exclude this path, leave out of mappings
1931 exclude = False
1932 if depot_side.startswith("-"):
1933 exclude = True
1934 depot_side = depot_side[1:]
1936 if not exclude:
1937 self.mappings.append(depot_side)
1939 def convert_client_path(self, clientFile):
1940 # chop off //client/ part to make it relative
1941 if not clientFile.startswith(self.client_prefix):
1942 die("No prefix '%s' on clientFile '%s'" %
1943 (self.client_prefix, clientFile))
1944 return clientFile[len(self.client_prefix):]
1946 def update_client_spec_path_cache(self, files):
1947 """ Caching file paths by "p4 where" batch query """
1949 # List depot file paths exclude that already cached
1950 fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
1952 if len(fileArgs) == 0:
1953 return # All files in cache
1955 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
1956 for res in where_result:
1957 if "code" in res and res["code"] == "error":
1958 # assume error is "... file(s) not in client view"
1959 continue
1960 if "clientFile" not in res:
1961 die("No clientFile in 'p4 where' output")
1962 if "unmap" in res:
1963 # it will list all of them, but only one not unmap-ped
1964 continue
1965 if gitConfigBool("core.ignorecase"):
1966 res['depotFile'] = res['depotFile'].lower()
1967 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
1969 # not found files or unmap files set to ""
1970 for depotFile in fileArgs:
1971 if gitConfigBool("core.ignorecase"):
1972 depotFile = depotFile.lower()
1973 if depotFile not in self.client_spec_path_cache:
1974 self.client_spec_path_cache[depotFile] = ""
1976 def map_in_client(self, depot_path):
1977 """Return the relative location in the client where this
1978 depot file should live. Returns "" if the file should
1979 not be mapped in the client."""
1981 if gitConfigBool("core.ignorecase"):
1982 depot_path = depot_path.lower()
1984 if depot_path in self.client_spec_path_cache:
1985 return self.client_spec_path_cache[depot_path]
1987 die( "Error: %s is not found in client spec path" % depot_path )
1988 return ""
1990 class P4Sync(Command, P4UserMap):
1991 delete_actions = ( "delete", "move/delete", "purge" )
1993 def __init__(self):
1994 Command.__init__(self)
1995 P4UserMap.__init__(self)
1996 self.options = [
1997 optparse.make_option("--branch", dest="branch"),
1998 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1999 optparse.make_option("--changesfile", dest="changesFile"),
2000 optparse.make_option("--silent", dest="silent", action="store_true"),
2001 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2002 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2003 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2004 help="Import into refs/heads/ , not refs/remotes"),
2005 optparse.make_option("--max-changes", dest="maxChanges",
2006 help="Maximum number of changes to import"),
2007 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2008 help="Internal block size to use when iteratively calling p4 changes"),
2009 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2010 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2011 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2012 help="Only sync files that are included in the Perforce Client Spec"),
2013 optparse.make_option("-/", dest="cloneExclude",
2014 action="append", type="string",
2015 help="exclude depot path"),
2017 self.description = """Imports from Perforce into a git repository.\n
2018 example:
2019 //depot/my/project/ -- to import the current head
2020 //depot/my/project/@all -- to import everything
2021 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2023 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2025 self.usage += " //depot/path[@revRange]"
2026 self.silent = False
2027 self.createdBranches = set()
2028 self.committedChanges = set()
2029 self.branch = ""
2030 self.detectBranches = False
2031 self.detectLabels = False
2032 self.importLabels = False
2033 self.changesFile = ""
2034 self.syncWithOrigin = True
2035 self.importIntoRemotes = True
2036 self.maxChanges = ""
2037 self.changes_block_size = None
2038 self.keepRepoPath = False
2039 self.depotPaths = None
2040 self.p4BranchesInGit = []
2041 self.cloneExclude = []
2042 self.useClientSpec = False
2043 self.useClientSpec_from_options = False
2044 self.clientSpecDirs = None
2045 self.tempBranches = []
2046 self.tempBranchLocation = "git-p4-tmp"
2048 if gitConfig("git-p4.syncFromOrigin") == "false":
2049 self.syncWithOrigin = False
2051 # This is required for the "append" cloneExclude action
2052 def ensure_value(self, attr, value):
2053 if not hasattr(self, attr) or getattr(self, attr) is None:
2054 setattr(self, attr, value)
2055 return getattr(self, attr)
2057 # Force a checkpoint in fast-import and wait for it to finish
2058 def checkpoint(self):
2059 self.gitStream.write("checkpoint\n\n")
2060 self.gitStream.write("progress checkpoint\n\n")
2061 out = self.gitOutput.readline()
2062 if self.verbose:
2063 print "checkpoint finished: " + out
2065 def extractFilesFromCommit(self, commit):
2066 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2067 for path in self.cloneExclude]
2068 files = []
2069 fnum = 0
2070 while commit.has_key("depotFile%s" % fnum):
2071 path = commit["depotFile%s" % fnum]
2073 if [p for p in self.cloneExclude
2074 if p4PathStartsWith(path, p)]:
2075 found = False
2076 else:
2077 found = [p for p in self.depotPaths
2078 if p4PathStartsWith(path, p)]
2079 if not found:
2080 fnum = fnum + 1
2081 continue
2083 file = {}
2084 file["path"] = path
2085 file["rev"] = commit["rev%s" % fnum]
2086 file["action"] = commit["action%s" % fnum]
2087 file["type"] = commit["type%s" % fnum]
2088 files.append(file)
2089 fnum = fnum + 1
2090 return files
2092 def stripRepoPath(self, path, prefixes):
2093 """When streaming files, this is called to map a p4 depot path
2094 to where it should go in git. The prefixes are either
2095 self.depotPaths, or self.branchPrefixes in the case of
2096 branch detection."""
2098 if self.useClientSpec:
2099 # branch detection moves files up a level (the branch name)
2100 # from what client spec interpretation gives
2101 path = self.clientSpecDirs.map_in_client(path)
2102 if self.detectBranches:
2103 for b in self.knownBranches:
2104 if path.startswith(b + "/"):
2105 path = path[len(b)+1:]
2107 elif self.keepRepoPath:
2108 # Preserve everything in relative path name except leading
2109 # //depot/; just look at first prefix as they all should
2110 # be in the same depot.
2111 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2112 if p4PathStartsWith(path, depot):
2113 path = path[len(depot):]
2115 else:
2116 for p in prefixes:
2117 if p4PathStartsWith(path, p):
2118 path = path[len(p):]
2119 break
2121 path = wildcard_decode(path)
2122 return path
2124 def splitFilesIntoBranches(self, commit):
2125 """Look at each depotFile in the commit to figure out to what
2126 branch it belongs."""
2128 if self.clientSpecDirs:
2129 files = self.extractFilesFromCommit(commit)
2130 self.clientSpecDirs.update_client_spec_path_cache(files)
2132 branches = {}
2133 fnum = 0
2134 while commit.has_key("depotFile%s" % fnum):
2135 path = commit["depotFile%s" % fnum]
2136 found = [p for p in self.depotPaths
2137 if p4PathStartsWith(path, p)]
2138 if not found:
2139 fnum = fnum + 1
2140 continue
2142 file = {}
2143 file["path"] = path
2144 file["rev"] = commit["rev%s" % fnum]
2145 file["action"] = commit["action%s" % fnum]
2146 file["type"] = commit["type%s" % fnum]
2147 fnum = fnum + 1
2149 # start with the full relative path where this file would
2150 # go in a p4 client
2151 if self.useClientSpec:
2152 relPath = self.clientSpecDirs.map_in_client(path)
2153 else:
2154 relPath = self.stripRepoPath(path, self.depotPaths)
2156 for branch in self.knownBranches.keys():
2157 # add a trailing slash so that a commit into qt/4.2foo
2158 # doesn't end up in qt/4.2, e.g.
2159 if relPath.startswith(branch + "/"):
2160 if branch not in branches:
2161 branches[branch] = []
2162 branches[branch].append(file)
2163 break
2165 return branches
2167 # output one file from the P4 stream
2168 # - helper for streamP4Files
2170 def streamOneP4File(self, file, contents):
2171 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2172 if verbose:
2173 sys.stderr.write("%s\n" % relPath)
2175 (type_base, type_mods) = split_p4_type(file["type"])
2177 git_mode = "100644"
2178 if "x" in type_mods:
2179 git_mode = "100755"
2180 if type_base == "symlink":
2181 git_mode = "120000"
2182 # p4 print on a symlink sometimes contains "target\n";
2183 # if it does, remove the newline
2184 data = ''.join(contents)
2185 if not data:
2186 # Some version of p4 allowed creating a symlink that pointed
2187 # to nothing. This causes p4 errors when checking out such
2188 # a change, and errors here too. Work around it by ignoring
2189 # the bad symlink; hopefully a future change fixes it.
2190 print "\nIgnoring empty symlink in %s" % file['depotFile']
2191 return
2192 elif data[-1] == '\n':
2193 contents = [data[:-1]]
2194 else:
2195 contents = [data]
2197 if type_base == "utf16":
2198 # p4 delivers different text in the python output to -G
2199 # than it does when using "print -o", or normal p4 client
2200 # operations. utf16 is converted to ascii or utf8, perhaps.
2201 # But ascii text saved as -t utf16 is completely mangled.
2202 # Invoke print -o to get the real contents.
2204 # On windows, the newlines will always be mangled by print, so put
2205 # them back too. This is not needed to the cygwin windows version,
2206 # just the native "NT" type.
2208 try:
2209 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2210 except Exception as e:
2211 if 'Translation of file content failed' in str(e):
2212 type_base = 'binary'
2213 else:
2214 raise e
2215 else:
2216 if p4_version_string().find('/NT') >= 0:
2217 text = text.replace('\r\n', '\n')
2218 contents = [ text ]
2220 if type_base == "apple":
2221 # Apple filetype files will be streamed as a concatenation of
2222 # its appledouble header and the contents. This is useless
2223 # on both macs and non-macs. If using "print -q -o xx", it
2224 # will create "xx" with the data, and "%xx" with the header.
2225 # This is also not very useful.
2227 # Ideally, someday, this script can learn how to generate
2228 # appledouble files directly and import those to git, but
2229 # non-mac machines can never find a use for apple filetype.
2230 print "\nIgnoring apple filetype file %s" % file['depotFile']
2231 return
2233 # Note that we do not try to de-mangle keywords on utf16 files,
2234 # even though in theory somebody may want that.
2235 pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2236 if pattern:
2237 regexp = re.compile(pattern, re.VERBOSE)
2238 text = ''.join(contents)
2239 text = regexp.sub(r'$\1$', text)
2240 contents = [ text ]
2242 self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2244 # total length...
2245 length = 0
2246 for d in contents:
2247 length = length + len(d)
2249 self.gitStream.write("data %d\n" % length)
2250 for d in contents:
2251 self.gitStream.write(d)
2252 self.gitStream.write("\n")
2254 def streamOneP4Deletion(self, file):
2255 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2256 if verbose:
2257 sys.stderr.write("delete %s\n" % relPath)
2258 self.gitStream.write("D %s\n" % relPath)
2260 # handle another chunk of streaming data
2261 def streamP4FilesCb(self, marshalled):
2263 # catch p4 errors and complain
2264 err = None
2265 if "code" in marshalled:
2266 if marshalled["code"] == "error":
2267 if "data" in marshalled:
2268 err = marshalled["data"].rstrip()
2269 if err:
2270 f = None
2271 if self.stream_have_file_info:
2272 if "depotFile" in self.stream_file:
2273 f = self.stream_file["depotFile"]
2274 # force a failure in fast-import, else an empty
2275 # commit will be made
2276 self.gitStream.write("\n")
2277 self.gitStream.write("die-now\n")
2278 self.gitStream.close()
2279 # ignore errors, but make sure it exits first
2280 self.importProcess.wait()
2281 if f:
2282 die("Error from p4 print for %s: %s" % (f, err))
2283 else:
2284 die("Error from p4 print: %s" % err)
2286 if marshalled.has_key('depotFile') and self.stream_have_file_info:
2287 # start of a new file - output the old one first
2288 self.streamOneP4File(self.stream_file, self.stream_contents)
2289 self.stream_file = {}
2290 self.stream_contents = []
2291 self.stream_have_file_info = False
2293 # pick up the new file information... for the
2294 # 'data' field we need to append to our array
2295 for k in marshalled.keys():
2296 if k == 'data':
2297 self.stream_contents.append(marshalled['data'])
2298 else:
2299 self.stream_file[k] = marshalled[k]
2301 self.stream_have_file_info = True
2303 # Stream directly from "p4 files" into "git fast-import"
2304 def streamP4Files(self, files):
2305 filesForCommit = []
2306 filesToRead = []
2307 filesToDelete = []
2309 for f in files:
2310 # if using a client spec, only add the files that have
2311 # a path in the client
2312 if self.clientSpecDirs:
2313 if self.clientSpecDirs.map_in_client(f['path']) == "":
2314 continue
2316 filesForCommit.append(f)
2317 if f['action'] in self.delete_actions:
2318 filesToDelete.append(f)
2319 else:
2320 filesToRead.append(f)
2322 # deleted files...
2323 for f in filesToDelete:
2324 self.streamOneP4Deletion(f)
2326 if len(filesToRead) > 0:
2327 self.stream_file = {}
2328 self.stream_contents = []
2329 self.stream_have_file_info = False
2331 # curry self argument
2332 def streamP4FilesCbSelf(entry):
2333 self.streamP4FilesCb(entry)
2335 fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2337 p4CmdList(["-x", "-", "print"],
2338 stdin=fileArgs,
2339 cb=streamP4FilesCbSelf)
2341 # do the last chunk
2342 if self.stream_file.has_key('depotFile'):
2343 self.streamOneP4File(self.stream_file, self.stream_contents)
2345 def make_email(self, userid):
2346 if userid in self.users:
2347 return self.users[userid]
2348 else:
2349 return "%s <a@b>" % userid
2351 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2352 """ Stream a p4 tag.
2353 commit is either a git commit, or a fast-import mark, ":<p4commit>"
2356 if verbose:
2357 print "writing tag %s for commit %s" % (labelName, commit)
2358 gitStream.write("tag %s\n" % labelName)
2359 gitStream.write("from %s\n" % commit)
2361 if labelDetails.has_key('Owner'):
2362 owner = labelDetails["Owner"]
2363 else:
2364 owner = None
2366 # Try to use the owner of the p4 label, or failing that,
2367 # the current p4 user id.
2368 if owner:
2369 email = self.make_email(owner)
2370 else:
2371 email = self.make_email(self.p4UserId())
2372 tagger = "%s %s %s" % (email, epoch, self.tz)
2374 gitStream.write("tagger %s\n" % tagger)
2376 print "labelDetails=",labelDetails
2377 if labelDetails.has_key('Description'):
2378 description = labelDetails['Description']
2379 else:
2380 description = 'Label from git p4'
2382 gitStream.write("data %d\n" % len(description))
2383 gitStream.write(description)
2384 gitStream.write("\n")
2386 def commit(self, details, files, branch, parent = ""):
2387 epoch = details["time"]
2388 author = details["user"]
2390 if self.verbose:
2391 print "commit into %s" % branch
2393 # start with reading files; if that fails, we should not
2394 # create a commit.
2395 new_files = []
2396 for f in files:
2397 if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2398 new_files.append (f)
2399 else:
2400 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2402 if self.clientSpecDirs:
2403 self.clientSpecDirs.update_client_spec_path_cache(files)
2405 self.gitStream.write("commit %s\n" % branch)
2406 self.gitStream.write("mark :%s\n" % details["change"])
2407 self.committedChanges.add(int(details["change"]))
2408 committer = ""
2409 if author not in self.users:
2410 self.getUserMapFromPerforceServer()
2411 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2413 self.gitStream.write("committer %s\n" % committer)
2415 self.gitStream.write("data <<EOT\n")
2416 self.gitStream.write(details["desc"])
2417 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2418 (','.join(self.branchPrefixes), details["change"]))
2419 if len(details['options']) > 0:
2420 self.gitStream.write(": options = %s" % details['options'])
2421 self.gitStream.write("]\nEOT\n\n")
2423 if len(parent) > 0:
2424 if self.verbose:
2425 print "parent %s" % parent
2426 self.gitStream.write("from %s\n" % parent)
2428 self.streamP4Files(new_files)
2429 self.gitStream.write("\n")
2431 change = int(details["change"])
2433 if self.labels.has_key(change):
2434 label = self.labels[change]
2435 labelDetails = label[0]
2436 labelRevisions = label[1]
2437 if self.verbose:
2438 print "Change %s is labelled %s" % (change, labelDetails)
2440 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2441 for p in self.branchPrefixes])
2443 if len(files) == len(labelRevisions):
2445 cleanedFiles = {}
2446 for info in files:
2447 if info["action"] in self.delete_actions:
2448 continue
2449 cleanedFiles[info["depotFile"]] = info["rev"]
2451 if cleanedFiles == labelRevisions:
2452 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2454 else:
2455 if not self.silent:
2456 print ("Tag %s does not match with change %s: files do not match."
2457 % (labelDetails["label"], change))
2459 else:
2460 if not self.silent:
2461 print ("Tag %s does not match with change %s: file count is different."
2462 % (labelDetails["label"], change))
2464 # Build a dictionary of changelists and labels, for "detect-labels" option.
2465 def getLabels(self):
2466 self.labels = {}
2468 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2469 if len(l) > 0 and not self.silent:
2470 print "Finding files belonging to labels in %s" % `self.depotPaths`
2472 for output in l:
2473 label = output["label"]
2474 revisions = {}
2475 newestChange = 0
2476 if self.verbose:
2477 print "Querying files for label %s" % label
2478 for file in p4CmdList(["files"] +
2479 ["%s...@%s" % (p, label)
2480 for p in self.depotPaths]):
2481 revisions[file["depotFile"]] = file["rev"]
2482 change = int(file["change"])
2483 if change > newestChange:
2484 newestChange = change
2486 self.labels[newestChange] = [output, revisions]
2488 if self.verbose:
2489 print "Label changes: %s" % self.labels.keys()
2491 # Import p4 labels as git tags. A direct mapping does not
2492 # exist, so assume that if all the files are at the same revision
2493 # then we can use that, or it's something more complicated we should
2494 # just ignore.
2495 def importP4Labels(self, stream, p4Labels):
2496 if verbose:
2497 print "import p4 labels: " + ' '.join(p4Labels)
2499 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2500 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2501 if len(validLabelRegexp) == 0:
2502 validLabelRegexp = defaultLabelRegexp
2503 m = re.compile(validLabelRegexp)
2505 for name in p4Labels:
2506 commitFound = False
2508 if not m.match(name):
2509 if verbose:
2510 print "label %s does not match regexp %s" % (name,validLabelRegexp)
2511 continue
2513 if name in ignoredP4Labels:
2514 continue
2516 labelDetails = p4CmdList(['label', "-o", name])[0]
2518 # get the most recent changelist for each file in this label
2519 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2520 for p in self.depotPaths])
2522 if change.has_key('change'):
2523 # find the corresponding git commit; take the oldest commit
2524 changelist = int(change['change'])
2525 if changelist in self.committedChanges:
2526 gitCommit = ":%d" % changelist # use a fast-import mark
2527 commitFound = True
2528 else:
2529 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2530 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
2531 if len(gitCommit) == 0:
2532 print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
2533 else:
2534 commitFound = True
2535 gitCommit = gitCommit.strip()
2537 if commitFound:
2538 # Convert from p4 time format
2539 try:
2540 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2541 except ValueError:
2542 print "Could not convert label time %s" % labelDetails['Update']
2543 tmwhen = 1
2545 when = int(time.mktime(tmwhen))
2546 self.streamTag(stream, name, labelDetails, gitCommit, when)
2547 if verbose:
2548 print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2549 else:
2550 if verbose:
2551 print "Label %s has no changelists - possibly deleted?" % name
2553 if not commitFound:
2554 # We can't import this label; don't try again as it will get very
2555 # expensive repeatedly fetching all the files for labels that will
2556 # never be imported. If the label is moved in the future, the
2557 # ignore will need to be removed manually.
2558 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2560 def guessProjectName(self):
2561 for p in self.depotPaths:
2562 if p.endswith("/"):
2563 p = p[:-1]
2564 p = p[p.strip().rfind("/") + 1:]
2565 if not p.endswith("/"):
2566 p += "/"
2567 return p
2569 def getBranchMapping(self):
2570 lostAndFoundBranches = set()
2572 user = gitConfig("git-p4.branchUser")
2573 if len(user) > 0:
2574 command = "branches -u %s" % user
2575 else:
2576 command = "branches"
2578 for info in p4CmdList(command):
2579 details = p4Cmd(["branch", "-o", info["branch"]])
2580 viewIdx = 0
2581 while details.has_key("View%s" % viewIdx):
2582 paths = details["View%s" % viewIdx].split(" ")
2583 viewIdx = viewIdx + 1
2584 # require standard //depot/foo/... //depot/bar/... mapping
2585 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2586 continue
2587 source = paths[0]
2588 destination = paths[1]
2589 ## HACK
2590 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2591 source = source[len(self.depotPaths[0]):-4]
2592 destination = destination[len(self.depotPaths[0]):-4]
2594 if destination in self.knownBranches:
2595 if not self.silent:
2596 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2597 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2598 continue
2600 self.knownBranches[destination] = source
2602 lostAndFoundBranches.discard(destination)
2604 if source not in self.knownBranches:
2605 lostAndFoundBranches.add(source)
2607 # Perforce does not strictly require branches to be defined, so we also
2608 # check git config for a branch list.
2610 # Example of branch definition in git config file:
2611 # [git-p4]
2612 # branchList=main:branchA
2613 # branchList=main:branchB
2614 # branchList=branchA:branchC
2615 configBranches = gitConfigList("git-p4.branchList")
2616 for branch in configBranches:
2617 if branch:
2618 (source, destination) = branch.split(":")
2619 self.knownBranches[destination] = source
2621 lostAndFoundBranches.discard(destination)
2623 if source not in self.knownBranches:
2624 lostAndFoundBranches.add(source)
2627 for branch in lostAndFoundBranches:
2628 self.knownBranches[branch] = branch
2630 def getBranchMappingFromGitBranches(self):
2631 branches = p4BranchesInGit(self.importIntoRemotes)
2632 for branch in branches.keys():
2633 if branch == "master":
2634 branch = "main"
2635 else:
2636 branch = branch[len(self.projectName):]
2637 self.knownBranches[branch] = branch
2639 def updateOptionDict(self, d):
2640 option_keys = {}
2641 if self.keepRepoPath:
2642 option_keys['keepRepoPath'] = 1
2644 d["options"] = ' '.join(sorted(option_keys.keys()))
2646 def readOptions(self, d):
2647 self.keepRepoPath = (d.has_key('options')
2648 and ('keepRepoPath' in d['options']))
2650 def gitRefForBranch(self, branch):
2651 if branch == "main":
2652 return self.refPrefix + "master"
2654 if len(branch) <= 0:
2655 return branch
2657 return self.refPrefix + self.projectName + branch
2659 def gitCommitByP4Change(self, ref, change):
2660 if self.verbose:
2661 print "looking in ref " + ref + " for change %s using bisect..." % change
2663 earliestCommit = ""
2664 latestCommit = parseRevision(ref)
2666 while True:
2667 if self.verbose:
2668 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2669 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2670 if len(next) == 0:
2671 if self.verbose:
2672 print "argh"
2673 return ""
2674 log = extractLogMessageFromGitCommit(next)
2675 settings = extractSettingsGitLog(log)
2676 currentChange = int(settings['change'])
2677 if self.verbose:
2678 print "current change %s" % currentChange
2680 if currentChange == change:
2681 if self.verbose:
2682 print "found %s" % next
2683 return next
2685 if currentChange < change:
2686 earliestCommit = "^%s" % next
2687 else:
2688 latestCommit = "%s" % next
2690 return ""
2692 def importNewBranch(self, branch, maxChange):
2693 # make fast-import flush all changes to disk and update the refs using the checkpoint
2694 # command so that we can try to find the branch parent in the git history
2695 self.gitStream.write("checkpoint\n\n");
2696 self.gitStream.flush();
2697 branchPrefix = self.depotPaths[0] + branch + "/"
2698 range = "@1,%s" % maxChange
2699 #print "prefix" + branchPrefix
2700 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
2701 if len(changes) <= 0:
2702 return False
2703 firstChange = changes[0]
2704 #print "first change in branch: %s" % firstChange
2705 sourceBranch = self.knownBranches[branch]
2706 sourceDepotPath = self.depotPaths[0] + sourceBranch
2707 sourceRef = self.gitRefForBranch(sourceBranch)
2708 #print "source " + sourceBranch
2710 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2711 #print "branch parent: %s" % branchParentChange
2712 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2713 if len(gitParent) > 0:
2714 self.initialParents[self.gitRefForBranch(branch)] = gitParent
2715 #print "parent git commit: %s" % gitParent
2717 self.importChanges(changes)
2718 return True
2720 def searchParent(self, parent, branch, target):
2721 parentFound = False
2722 for blob in read_pipe_lines(["git", "rev-list", "--reverse",
2723 "--no-merges", parent]):
2724 blob = blob.strip()
2725 if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2726 parentFound = True
2727 if self.verbose:
2728 print "Found parent of %s in commit %s" % (branch, blob)
2729 break
2730 if parentFound:
2731 return blob
2732 else:
2733 return None
2735 def importChanges(self, changes):
2736 cnt = 1
2737 for change in changes:
2738 description = p4_describe(change)
2739 self.updateOptionDict(description)
2741 if not self.silent:
2742 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2743 sys.stdout.flush()
2744 cnt = cnt + 1
2746 try:
2747 if self.detectBranches:
2748 branches = self.splitFilesIntoBranches(description)
2749 for branch in branches.keys():
2750 ## HACK --hwn
2751 branchPrefix = self.depotPaths[0] + branch + "/"
2752 self.branchPrefixes = [ branchPrefix ]
2754 parent = ""
2756 filesForCommit = branches[branch]
2758 if self.verbose:
2759 print "branch is %s" % branch
2761 self.updatedBranches.add(branch)
2763 if branch not in self.createdBranches:
2764 self.createdBranches.add(branch)
2765 parent = self.knownBranches[branch]
2766 if parent == branch:
2767 parent = ""
2768 else:
2769 fullBranch = self.projectName + branch
2770 if fullBranch not in self.p4BranchesInGit:
2771 if not self.silent:
2772 print("\n Importing new branch %s" % fullBranch);
2773 if self.importNewBranch(branch, change - 1):
2774 parent = ""
2775 self.p4BranchesInGit.append(fullBranch)
2776 if not self.silent:
2777 print("\n Resuming with change %s" % change);
2779 if self.verbose:
2780 print "parent determined through known branches: %s" % parent
2782 branch = self.gitRefForBranch(branch)
2783 parent = self.gitRefForBranch(parent)
2785 if self.verbose:
2786 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2788 if len(parent) == 0 and branch in self.initialParents:
2789 parent = self.initialParents[branch]
2790 del self.initialParents[branch]
2792 blob = None
2793 if len(parent) > 0:
2794 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2795 if self.verbose:
2796 print "Creating temporary branch: " + tempBranch
2797 self.commit(description, filesForCommit, tempBranch)
2798 self.tempBranches.append(tempBranch)
2799 self.checkpoint()
2800 blob = self.searchParent(parent, branch, tempBranch)
2801 if blob:
2802 self.commit(description, filesForCommit, branch, blob)
2803 else:
2804 if self.verbose:
2805 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2806 self.commit(description, filesForCommit, branch, parent)
2807 else:
2808 files = self.extractFilesFromCommit(description)
2809 self.commit(description, files, self.branch,
2810 self.initialParent)
2811 # only needed once, to connect to the previous commit
2812 self.initialParent = ""
2813 except IOError:
2814 print self.gitError.read()
2815 sys.exit(1)
2817 def importHeadRevision(self, revision):
2818 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2820 details = {}
2821 details["user"] = "git perforce import user"
2822 details["desc"] = ("Initial import of %s from the state at revision %s\n"
2823 % (' '.join(self.depotPaths), revision))
2824 details["change"] = revision
2825 newestRevision = 0
2827 fileCnt = 0
2828 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2830 for info in p4CmdList(["files"] + fileArgs):
2832 if 'code' in info and info['code'] == 'error':
2833 sys.stderr.write("p4 returned an error: %s\n"
2834 % info['data'])
2835 if info['data'].find("must refer to client") >= 0:
2836 sys.stderr.write("This particular p4 error is misleading.\n")
2837 sys.stderr.write("Perhaps the depot path was misspelled.\n");
2838 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
2839 sys.exit(1)
2840 if 'p4ExitCode' in info:
2841 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2842 sys.exit(1)
2845 change = int(info["change"])
2846 if change > newestRevision:
2847 newestRevision = change
2849 if info["action"] in self.delete_actions:
2850 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2851 #fileCnt = fileCnt + 1
2852 continue
2854 for prop in ["depotFile", "rev", "action", "type" ]:
2855 details["%s%s" % (prop, fileCnt)] = info[prop]
2857 fileCnt = fileCnt + 1
2859 details["change"] = newestRevision
2861 # Use time from top-most change so that all git p4 clones of
2862 # the same p4 repo have the same commit SHA1s.
2863 res = p4_describe(newestRevision)
2864 details["time"] = res["time"]
2866 self.updateOptionDict(details)
2867 try:
2868 self.commit(details, self.extractFilesFromCommit(details), self.branch)
2869 except IOError:
2870 print "IO error with git fast-import. Is your git version recent enough?"
2871 print self.gitError.read()
2874 def run(self, args):
2875 self.depotPaths = []
2876 self.changeRange = ""
2877 self.previousDepotPaths = []
2878 self.hasOrigin = False
2880 # map from branch depot path to parent branch
2881 self.knownBranches = {}
2882 self.initialParents = {}
2884 if self.importIntoRemotes:
2885 self.refPrefix = "refs/remotes/p4/"
2886 else:
2887 self.refPrefix = "refs/heads/p4/"
2889 if self.syncWithOrigin:
2890 self.hasOrigin = originP4BranchesExist()
2891 if self.hasOrigin:
2892 if not self.silent:
2893 print 'Syncing with origin first, using "git fetch origin"'
2894 system("git fetch origin")
2896 branch_arg_given = bool(self.branch)
2897 if len(self.branch) == 0:
2898 self.branch = self.refPrefix + "master"
2899 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2900 system("git update-ref %s refs/heads/p4" % self.branch)
2901 system("git branch -D p4")
2903 # accept either the command-line option, or the configuration variable
2904 if self.useClientSpec:
2905 # will use this after clone to set the variable
2906 self.useClientSpec_from_options = True
2907 else:
2908 if gitConfigBool("git-p4.useclientspec"):
2909 self.useClientSpec = True
2910 if self.useClientSpec:
2911 self.clientSpecDirs = getClientSpec()
2913 # TODO: should always look at previous commits,
2914 # merge with previous imports, if possible.
2915 if args == []:
2916 if self.hasOrigin:
2917 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2919 # branches holds mapping from branch name to sha1
2920 branches = p4BranchesInGit(self.importIntoRemotes)
2922 # restrict to just this one, disabling detect-branches
2923 if branch_arg_given:
2924 short = self.branch.split("/")[-1]
2925 if short in branches:
2926 self.p4BranchesInGit = [ short ]
2927 else:
2928 self.p4BranchesInGit = branches.keys()
2930 if len(self.p4BranchesInGit) > 1:
2931 if not self.silent:
2932 print "Importing from/into multiple branches"
2933 self.detectBranches = True
2934 for branch in branches.keys():
2935 self.initialParents[self.refPrefix + branch] = \
2936 branches[branch]
2938 if self.verbose:
2939 print "branches: %s" % self.p4BranchesInGit
2941 p4Change = 0
2942 for branch in self.p4BranchesInGit:
2943 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
2945 settings = extractSettingsGitLog(logMsg)
2947 self.readOptions(settings)
2948 if (settings.has_key('depot-paths')
2949 and settings.has_key ('change')):
2950 change = int(settings['change']) + 1
2951 p4Change = max(p4Change, change)
2953 depotPaths = sorted(settings['depot-paths'])
2954 if self.previousDepotPaths == []:
2955 self.previousDepotPaths = depotPaths
2956 else:
2957 paths = []
2958 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2959 prev_list = prev.split("/")
2960 cur_list = cur.split("/")
2961 for i in range(0, min(len(cur_list), len(prev_list))):
2962 if cur_list[i] <> prev_list[i]:
2963 i = i - 1
2964 break
2966 paths.append ("/".join(cur_list[:i + 1]))
2968 self.previousDepotPaths = paths
2970 if p4Change > 0:
2971 self.depotPaths = sorted(self.previousDepotPaths)
2972 self.changeRange = "@%s,#head" % p4Change
2973 if not self.silent and not self.detectBranches:
2974 print "Performing incremental import into %s git branch" % self.branch
2976 # accept multiple ref name abbreviations:
2977 # refs/foo/bar/branch -> use it exactly
2978 # p4/branch -> prepend refs/remotes/ or refs/heads/
2979 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2980 if not self.branch.startswith("refs/"):
2981 if self.importIntoRemotes:
2982 prepend = "refs/remotes/"
2983 else:
2984 prepend = "refs/heads/"
2985 if not self.branch.startswith("p4/"):
2986 prepend += "p4/"
2987 self.branch = prepend + self.branch
2989 if len(args) == 0 and self.depotPaths:
2990 if not self.silent:
2991 print "Depot paths: %s" % ' '.join(self.depotPaths)
2992 else:
2993 if self.depotPaths and self.depotPaths != args:
2994 print ("previous import used depot path %s and now %s was specified. "
2995 "This doesn't work!" % (' '.join (self.depotPaths),
2996 ' '.join (args)))
2997 sys.exit(1)
2999 self.depotPaths = sorted(args)
3001 revision = ""
3002 self.users = {}
3004 # Make sure no revision specifiers are used when --changesfile
3005 # is specified.
3006 bad_changesfile = False
3007 if len(self.changesFile) > 0:
3008 for p in self.depotPaths:
3009 if p.find("@") >= 0 or p.find("#") >= 0:
3010 bad_changesfile = True
3011 break
3012 if bad_changesfile:
3013 die("Option --changesfile is incompatible with revision specifiers")
3015 newPaths = []
3016 for p in self.depotPaths:
3017 if p.find("@") != -1:
3018 atIdx = p.index("@")
3019 self.changeRange = p[atIdx:]
3020 if self.changeRange == "@all":
3021 self.changeRange = ""
3022 elif ',' not in self.changeRange:
3023 revision = self.changeRange
3024 self.changeRange = ""
3025 p = p[:atIdx]
3026 elif p.find("#") != -1:
3027 hashIdx = p.index("#")
3028 revision = p[hashIdx:]
3029 p = p[:hashIdx]
3030 elif self.previousDepotPaths == []:
3031 # pay attention to changesfile, if given, else import
3032 # the entire p4 tree at the head revision
3033 if len(self.changesFile) == 0:
3034 revision = "#head"
3036 p = re.sub ("\.\.\.$", "", p)
3037 if not p.endswith("/"):
3038 p += "/"
3040 newPaths.append(p)
3042 self.depotPaths = newPaths
3044 # --detect-branches may change this for each branch
3045 self.branchPrefixes = self.depotPaths
3047 self.loadUserMapFromCache()
3048 self.labels = {}
3049 if self.detectLabels:
3050 self.getLabels();
3052 if self.detectBranches:
3053 ## FIXME - what's a P4 projectName ?
3054 self.projectName = self.guessProjectName()
3056 if self.hasOrigin:
3057 self.getBranchMappingFromGitBranches()
3058 else:
3059 self.getBranchMapping()
3060 if self.verbose:
3061 print "p4-git branches: %s" % self.p4BranchesInGit
3062 print "initial parents: %s" % self.initialParents
3063 for b in self.p4BranchesInGit:
3064 if b != "master":
3066 ## FIXME
3067 b = b[len(self.projectName):]
3068 self.createdBranches.add(b)
3070 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
3072 self.importProcess = subprocess.Popen(["git", "fast-import"],
3073 stdin=subprocess.PIPE,
3074 stdout=subprocess.PIPE,
3075 stderr=subprocess.PIPE);
3076 self.gitOutput = self.importProcess.stdout
3077 self.gitStream = self.importProcess.stdin
3078 self.gitError = self.importProcess.stderr
3080 if revision:
3081 self.importHeadRevision(revision)
3082 else:
3083 changes = []
3085 if len(self.changesFile) > 0:
3086 output = open(self.changesFile).readlines()
3087 changeSet = set()
3088 for line in output:
3089 changeSet.add(int(line))
3091 for change in changeSet:
3092 changes.append(change)
3094 changes.sort()
3095 else:
3096 # catch "git p4 sync" with no new branches, in a repo that
3097 # does not have any existing p4 branches
3098 if len(args) == 0:
3099 if not self.p4BranchesInGit:
3100 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3102 # The default branch is master, unless --branch is used to
3103 # specify something else. Make sure it exists, or complain
3104 # nicely about how to use --branch.
3105 if not self.detectBranches:
3106 if not branch_exists(self.branch):
3107 if branch_arg_given:
3108 die("Error: branch %s does not exist." % self.branch)
3109 else:
3110 die("Error: no branch %s; perhaps specify one with --branch." %
3111 self.branch)
3113 if self.verbose:
3114 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3115 self.changeRange)
3116 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3118 if len(self.maxChanges) > 0:
3119 changes = changes[:min(int(self.maxChanges), len(changes))]
3121 if len(changes) == 0:
3122 if not self.silent:
3123 print "No changes to import!"
3124 else:
3125 if not self.silent and not self.detectBranches:
3126 print "Import destination: %s" % self.branch
3128 self.updatedBranches = set()
3130 if not self.detectBranches:
3131 if args:
3132 # start a new branch
3133 self.initialParent = ""
3134 else:
3135 # build on a previous revision
3136 self.initialParent = parseRevision(self.branch)
3138 self.importChanges(changes)
3140 if not self.silent:
3141 print ""
3142 if len(self.updatedBranches) > 0:
3143 sys.stdout.write("Updated branches: ")
3144 for b in self.updatedBranches:
3145 sys.stdout.write("%s " % b)
3146 sys.stdout.write("\n")
3148 if gitConfigBool("git-p4.importLabels"):
3149 self.importLabels = True
3151 if self.importLabels:
3152 p4Labels = getP4Labels(self.depotPaths)
3153 gitTags = getGitTags()
3155 missingP4Labels = p4Labels - gitTags
3156 self.importP4Labels(self.gitStream, missingP4Labels)
3158 self.gitStream.close()
3159 if self.importProcess.wait() != 0:
3160 die("fast-import failed: %s" % self.gitError.read())
3161 self.gitOutput.close()
3162 self.gitError.close()
3164 # Cleanup temporary branches created during import
3165 if self.tempBranches != []:
3166 for branch in self.tempBranches:
3167 read_pipe("git update-ref -d %s" % branch)
3168 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3170 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3171 # a convenient shortcut refname "p4".
3172 if self.importIntoRemotes:
3173 head_ref = self.refPrefix + "HEAD"
3174 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3175 system(["git", "symbolic-ref", head_ref, self.branch])
3177 return True
3179 class P4Rebase(Command):
3180 def __init__(self):
3181 Command.__init__(self)
3182 self.options = [
3183 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3185 self.importLabels = False
3186 self.description = ("Fetches the latest revision from perforce and "
3187 + "rebases the current work (branch) against it")
3189 def run(self, args):
3190 sync = P4Sync()
3191 sync.importLabels = self.importLabels
3192 sync.run([])
3194 return self.rebase()
3196 def rebase(self):
3197 if os.system("git update-index --refresh") != 0:
3198 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.");
3199 if len(read_pipe("git diff-index HEAD --")) > 0:
3200 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3202 [upstream, settings] = findUpstreamBranchPoint()
3203 if len(upstream) == 0:
3204 die("Cannot find upstream branchpoint for rebase")
3206 # the branchpoint may be p4/foo~3, so strip off the parent
3207 upstream = re.sub("~[0-9]+$", "", upstream)
3209 print "Rebasing the current branch onto %s" % upstream
3210 oldHead = read_pipe("git rev-parse HEAD").strip()
3211 system("git rebase %s" % upstream)
3212 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3213 return True
3215 class P4Clone(P4Sync):
3216 def __init__(self):
3217 P4Sync.__init__(self)
3218 self.description = "Creates a new git repository and imports from Perforce into it"
3219 self.usage = "usage: %prog [options] //depot/path[@revRange]"
3220 self.options += [
3221 optparse.make_option("--destination", dest="cloneDestination",
3222 action='store', default=None,
3223 help="where to leave result of the clone"),
3224 optparse.make_option("--bare", dest="cloneBare",
3225 action="store_true", default=False),
3227 self.cloneDestination = None
3228 self.needsGit = False
3229 self.cloneBare = False
3231 def defaultDestination(self, args):
3232 ## TODO: use common prefix of args?
3233 depotPath = args[0]
3234 depotDir = re.sub("(@[^@]*)$", "", depotPath)
3235 depotDir = re.sub("(#[^#]*)$", "", depotDir)
3236 depotDir = re.sub(r"\.\.\.$", "", depotDir)
3237 depotDir = re.sub(r"/$", "", depotDir)
3238 return os.path.split(depotDir)[1]
3240 def run(self, args):
3241 if len(args) < 1:
3242 return False
3244 if self.keepRepoPath and not self.cloneDestination:
3245 sys.stderr.write("Must specify destination for --keep-path\n")
3246 sys.exit(1)
3248 depotPaths = args
3250 if not self.cloneDestination and len(depotPaths) > 1:
3251 self.cloneDestination = depotPaths[-1]
3252 depotPaths = depotPaths[:-1]
3254 self.cloneExclude = ["/"+p for p in self.cloneExclude]
3255 for p in depotPaths:
3256 if not p.startswith("//"):
3257 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3258 return False
3260 if not self.cloneDestination:
3261 self.cloneDestination = self.defaultDestination(args)
3263 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3265 if not os.path.exists(self.cloneDestination):
3266 os.makedirs(self.cloneDestination)
3267 chdir(self.cloneDestination)
3269 init_cmd = [ "git", "init" ]
3270 if self.cloneBare:
3271 init_cmd.append("--bare")
3272 retcode = subprocess.call(init_cmd)
3273 if retcode:
3274 raise CalledProcessError(retcode, init_cmd)
3276 if not P4Sync.run(self, depotPaths):
3277 return False
3279 # create a master branch and check out a work tree
3280 if gitBranchExists(self.branch):
3281 system([ "git", "branch", "master", self.branch ])
3282 if not self.cloneBare:
3283 system([ "git", "checkout", "-f" ])
3284 else:
3285 print 'Not checking out any branch, use ' \
3286 '"git checkout -q -b master <branch>"'
3288 # auto-set this variable if invoked with --use-client-spec
3289 if self.useClientSpec_from_options:
3290 system("git config --bool git-p4.useclientspec true")
3292 return True
3294 class P4Branches(Command):
3295 def __init__(self):
3296 Command.__init__(self)
3297 self.options = [ ]
3298 self.description = ("Shows the git branches that hold imports and their "
3299 + "corresponding perforce depot paths")
3300 self.verbose = False
3302 def run(self, args):
3303 if originP4BranchesExist():
3304 createOrUpdateBranchesFromOrigin()
3306 cmdline = "git rev-parse --symbolic "
3307 cmdline += " --remotes"
3309 for line in read_pipe_lines(cmdline):
3310 line = line.strip()
3312 if not line.startswith('p4/') or line == "p4/HEAD":
3313 continue
3314 branch = line
3316 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3317 settings = extractSettingsGitLog(log)
3319 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3320 return True
3322 class HelpFormatter(optparse.IndentedHelpFormatter):
3323 def __init__(self):
3324 optparse.IndentedHelpFormatter.__init__(self)
3326 def format_description(self, description):
3327 if description:
3328 return description + "\n"
3329 else:
3330 return ""
3332 def printUsage(commands):
3333 print "usage: %s <command> [options]" % sys.argv[0]
3334 print ""
3335 print "valid commands: %s" % ", ".join(commands)
3336 print ""
3337 print "Try %s <command> --help for command specific help." % sys.argv[0]
3338 print ""
3340 commands = {
3341 "debug" : P4Debug,
3342 "submit" : P4Submit,
3343 "commit" : P4Submit,
3344 "sync" : P4Sync,
3345 "rebase" : P4Rebase,
3346 "clone" : P4Clone,
3347 "rollback" : P4RollBack,
3348 "branches" : P4Branches
3352 def main():
3353 if len(sys.argv[1:]) == 0:
3354 printUsage(commands.keys())
3355 sys.exit(2)
3357 cmdName = sys.argv[1]
3358 try:
3359 klass = commands[cmdName]
3360 cmd = klass()
3361 except KeyError:
3362 print "unknown command %s" % cmdName
3363 print ""
3364 printUsage(commands.keys())
3365 sys.exit(2)
3367 options = cmd.options
3368 cmd.gitdir = os.environ.get("GIT_DIR", None)
3370 args = sys.argv[2:]
3372 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3373 if cmd.needsGit:
3374 options.append(optparse.make_option("--git-dir", dest="gitdir"))
3376 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3377 options,
3378 description = cmd.description,
3379 formatter = HelpFormatter())
3381 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3382 global verbose
3383 verbose = cmd.verbose
3384 if cmd.needsGit:
3385 if cmd.gitdir == None:
3386 cmd.gitdir = os.path.abspath(".git")
3387 if not isValidGitDir(cmd.gitdir):
3388 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3389 if os.path.exists(cmd.gitdir):
3390 cdup = read_pipe("git rev-parse --show-cdup").strip()
3391 if len(cdup) > 0:
3392 chdir(cdup);
3394 if not isValidGitDir(cmd.gitdir):
3395 if isValidGitDir(cmd.gitdir + "/.git"):
3396 cmd.gitdir += "/.git"
3397 else:
3398 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3400 os.environ["GIT_DIR"] = cmd.gitdir
3402 if not cmd.run(args):
3403 parser.print_help()
3404 sys.exit(2)
3407 if __name__ == '__main__':
3408 main()