doc: revisions: extra clarification of <rev>^! notation effects
[git/debian.git] / git-p4.py
bloba7ec1187066b90675f55e44103ba54e03d132caa
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 filesForCommit.append(f)
2311 if f['action'] in self.delete_actions:
2312 filesToDelete.append(f)
2313 else:
2314 filesToRead.append(f)
2316 # deleted files...
2317 for f in filesToDelete:
2318 self.streamOneP4Deletion(f)
2320 if len(filesToRead) > 0:
2321 self.stream_file = {}
2322 self.stream_contents = []
2323 self.stream_have_file_info = False
2325 # curry self argument
2326 def streamP4FilesCbSelf(entry):
2327 self.streamP4FilesCb(entry)
2329 fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2331 p4CmdList(["-x", "-", "print"],
2332 stdin=fileArgs,
2333 cb=streamP4FilesCbSelf)
2335 # do the last chunk
2336 if self.stream_file.has_key('depotFile'):
2337 self.streamOneP4File(self.stream_file, self.stream_contents)
2339 def make_email(self, userid):
2340 if userid in self.users:
2341 return self.users[userid]
2342 else:
2343 return "%s <a@b>" % userid
2345 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2346 """ Stream a p4 tag.
2347 commit is either a git commit, or a fast-import mark, ":<p4commit>"
2350 if verbose:
2351 print "writing tag %s for commit %s" % (labelName, commit)
2352 gitStream.write("tag %s\n" % labelName)
2353 gitStream.write("from %s\n" % commit)
2355 if labelDetails.has_key('Owner'):
2356 owner = labelDetails["Owner"]
2357 else:
2358 owner = None
2360 # Try to use the owner of the p4 label, or failing that,
2361 # the current p4 user id.
2362 if owner:
2363 email = self.make_email(owner)
2364 else:
2365 email = self.make_email(self.p4UserId())
2366 tagger = "%s %s %s" % (email, epoch, self.tz)
2368 gitStream.write("tagger %s\n" % tagger)
2370 print "labelDetails=",labelDetails
2371 if labelDetails.has_key('Description'):
2372 description = labelDetails['Description']
2373 else:
2374 description = 'Label from git p4'
2376 gitStream.write("data %d\n" % len(description))
2377 gitStream.write(description)
2378 gitStream.write("\n")
2380 def inClientSpec(self, path):
2381 if not self.clientSpecDirs:
2382 return True
2383 inClientSpec = self.clientSpecDirs.map_in_client(path)
2384 if not inClientSpec and self.verbose:
2385 print('Ignoring file outside of client spec: {0}'.format(path))
2386 return inClientSpec
2388 def hasBranchPrefix(self, path):
2389 if not self.branchPrefixes:
2390 return True
2391 hasPrefix = [p for p in self.branchPrefixes
2392 if p4PathStartsWith(path, p)]
2393 if hasPrefix and self.verbose:
2394 print('Ignoring file outside of prefix: {0}'.format(path))
2395 return hasPrefix
2397 def commit(self, details, files, branch, parent = ""):
2398 epoch = details["time"]
2399 author = details["user"]
2401 if self.verbose:
2402 print('commit into {0}'.format(branch))
2404 if self.clientSpecDirs:
2405 self.clientSpecDirs.update_client_spec_path_cache(files)
2407 files = [f for f in files
2408 if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]
2410 if not files and not gitConfigBool('git-p4.keepEmptyCommits'):
2411 print('Ignoring revision {0} as it would produce an empty commit.'
2412 .format(details['change']))
2413 return
2415 self.gitStream.write("commit %s\n" % branch)
2416 self.gitStream.write("mark :%s\n" % details["change"])
2417 self.committedChanges.add(int(details["change"]))
2418 committer = ""
2419 if author not in self.users:
2420 self.getUserMapFromPerforceServer()
2421 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2423 self.gitStream.write("committer %s\n" % committer)
2425 self.gitStream.write("data <<EOT\n")
2426 self.gitStream.write(details["desc"])
2427 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2428 (','.join(self.branchPrefixes), details["change"]))
2429 if len(details['options']) > 0:
2430 self.gitStream.write(": options = %s" % details['options'])
2431 self.gitStream.write("]\nEOT\n\n")
2433 if len(parent) > 0:
2434 if self.verbose:
2435 print "parent %s" % parent
2436 self.gitStream.write("from %s\n" % parent)
2438 self.streamP4Files(files)
2439 self.gitStream.write("\n")
2441 change = int(details["change"])
2443 if self.labels.has_key(change):
2444 label = self.labels[change]
2445 labelDetails = label[0]
2446 labelRevisions = label[1]
2447 if self.verbose:
2448 print "Change %s is labelled %s" % (change, labelDetails)
2450 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2451 for p in self.branchPrefixes])
2453 if len(files) == len(labelRevisions):
2455 cleanedFiles = {}
2456 for info in files:
2457 if info["action"] in self.delete_actions:
2458 continue
2459 cleanedFiles[info["depotFile"]] = info["rev"]
2461 if cleanedFiles == labelRevisions:
2462 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2464 else:
2465 if not self.silent:
2466 print ("Tag %s does not match with change %s: files do not match."
2467 % (labelDetails["label"], change))
2469 else:
2470 if not self.silent:
2471 print ("Tag %s does not match with change %s: file count is different."
2472 % (labelDetails["label"], change))
2474 # Build a dictionary of changelists and labels, for "detect-labels" option.
2475 def getLabels(self):
2476 self.labels = {}
2478 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2479 if len(l) > 0 and not self.silent:
2480 print "Finding files belonging to labels in %s" % `self.depotPaths`
2482 for output in l:
2483 label = output["label"]
2484 revisions = {}
2485 newestChange = 0
2486 if self.verbose:
2487 print "Querying files for label %s" % label
2488 for file in p4CmdList(["files"] +
2489 ["%s...@%s" % (p, label)
2490 for p in self.depotPaths]):
2491 revisions[file["depotFile"]] = file["rev"]
2492 change = int(file["change"])
2493 if change > newestChange:
2494 newestChange = change
2496 self.labels[newestChange] = [output, revisions]
2498 if self.verbose:
2499 print "Label changes: %s" % self.labels.keys()
2501 # Import p4 labels as git tags. A direct mapping does not
2502 # exist, so assume that if all the files are at the same revision
2503 # then we can use that, or it's something more complicated we should
2504 # just ignore.
2505 def importP4Labels(self, stream, p4Labels):
2506 if verbose:
2507 print "import p4 labels: " + ' '.join(p4Labels)
2509 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2510 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2511 if len(validLabelRegexp) == 0:
2512 validLabelRegexp = defaultLabelRegexp
2513 m = re.compile(validLabelRegexp)
2515 for name in p4Labels:
2516 commitFound = False
2518 if not m.match(name):
2519 if verbose:
2520 print "label %s does not match regexp %s" % (name,validLabelRegexp)
2521 continue
2523 if name in ignoredP4Labels:
2524 continue
2526 labelDetails = p4CmdList(['label', "-o", name])[0]
2528 # get the most recent changelist for each file in this label
2529 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2530 for p in self.depotPaths])
2532 if change.has_key('change'):
2533 # find the corresponding git commit; take the oldest commit
2534 changelist = int(change['change'])
2535 if changelist in self.committedChanges:
2536 gitCommit = ":%d" % changelist # use a fast-import mark
2537 commitFound = True
2538 else:
2539 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2540 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
2541 if len(gitCommit) == 0:
2542 print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
2543 else:
2544 commitFound = True
2545 gitCommit = gitCommit.strip()
2547 if commitFound:
2548 # Convert from p4 time format
2549 try:
2550 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2551 except ValueError:
2552 print "Could not convert label time %s" % labelDetails['Update']
2553 tmwhen = 1
2555 when = int(time.mktime(tmwhen))
2556 self.streamTag(stream, name, labelDetails, gitCommit, when)
2557 if verbose:
2558 print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2559 else:
2560 if verbose:
2561 print "Label %s has no changelists - possibly deleted?" % name
2563 if not commitFound:
2564 # We can't import this label; don't try again as it will get very
2565 # expensive repeatedly fetching all the files for labels that will
2566 # never be imported. If the label is moved in the future, the
2567 # ignore will need to be removed manually.
2568 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2570 def guessProjectName(self):
2571 for p in self.depotPaths:
2572 if p.endswith("/"):
2573 p = p[:-1]
2574 p = p[p.strip().rfind("/") + 1:]
2575 if not p.endswith("/"):
2576 p += "/"
2577 return p
2579 def getBranchMapping(self):
2580 lostAndFoundBranches = set()
2582 user = gitConfig("git-p4.branchUser")
2583 if len(user) > 0:
2584 command = "branches -u %s" % user
2585 else:
2586 command = "branches"
2588 for info in p4CmdList(command):
2589 details = p4Cmd(["branch", "-o", info["branch"]])
2590 viewIdx = 0
2591 while details.has_key("View%s" % viewIdx):
2592 paths = details["View%s" % viewIdx].split(" ")
2593 viewIdx = viewIdx + 1
2594 # require standard //depot/foo/... //depot/bar/... mapping
2595 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2596 continue
2597 source = paths[0]
2598 destination = paths[1]
2599 ## HACK
2600 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2601 source = source[len(self.depotPaths[0]):-4]
2602 destination = destination[len(self.depotPaths[0]):-4]
2604 if destination in self.knownBranches:
2605 if not self.silent:
2606 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2607 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2608 continue
2610 self.knownBranches[destination] = source
2612 lostAndFoundBranches.discard(destination)
2614 if source not in self.knownBranches:
2615 lostAndFoundBranches.add(source)
2617 # Perforce does not strictly require branches to be defined, so we also
2618 # check git config for a branch list.
2620 # Example of branch definition in git config file:
2621 # [git-p4]
2622 # branchList=main:branchA
2623 # branchList=main:branchB
2624 # branchList=branchA:branchC
2625 configBranches = gitConfigList("git-p4.branchList")
2626 for branch in configBranches:
2627 if branch:
2628 (source, destination) = branch.split(":")
2629 self.knownBranches[destination] = source
2631 lostAndFoundBranches.discard(destination)
2633 if source not in self.knownBranches:
2634 lostAndFoundBranches.add(source)
2637 for branch in lostAndFoundBranches:
2638 self.knownBranches[branch] = branch
2640 def getBranchMappingFromGitBranches(self):
2641 branches = p4BranchesInGit(self.importIntoRemotes)
2642 for branch in branches.keys():
2643 if branch == "master":
2644 branch = "main"
2645 else:
2646 branch = branch[len(self.projectName):]
2647 self.knownBranches[branch] = branch
2649 def updateOptionDict(self, d):
2650 option_keys = {}
2651 if self.keepRepoPath:
2652 option_keys['keepRepoPath'] = 1
2654 d["options"] = ' '.join(sorted(option_keys.keys()))
2656 def readOptions(self, d):
2657 self.keepRepoPath = (d.has_key('options')
2658 and ('keepRepoPath' in d['options']))
2660 def gitRefForBranch(self, branch):
2661 if branch == "main":
2662 return self.refPrefix + "master"
2664 if len(branch) <= 0:
2665 return branch
2667 return self.refPrefix + self.projectName + branch
2669 def gitCommitByP4Change(self, ref, change):
2670 if self.verbose:
2671 print "looking in ref " + ref + " for change %s using bisect..." % change
2673 earliestCommit = ""
2674 latestCommit = parseRevision(ref)
2676 while True:
2677 if self.verbose:
2678 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2679 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2680 if len(next) == 0:
2681 if self.verbose:
2682 print "argh"
2683 return ""
2684 log = extractLogMessageFromGitCommit(next)
2685 settings = extractSettingsGitLog(log)
2686 currentChange = int(settings['change'])
2687 if self.verbose:
2688 print "current change %s" % currentChange
2690 if currentChange == change:
2691 if self.verbose:
2692 print "found %s" % next
2693 return next
2695 if currentChange < change:
2696 earliestCommit = "^%s" % next
2697 else:
2698 latestCommit = "%s" % next
2700 return ""
2702 def importNewBranch(self, branch, maxChange):
2703 # make fast-import flush all changes to disk and update the refs using the checkpoint
2704 # command so that we can try to find the branch parent in the git history
2705 self.gitStream.write("checkpoint\n\n");
2706 self.gitStream.flush();
2707 branchPrefix = self.depotPaths[0] + branch + "/"
2708 range = "@1,%s" % maxChange
2709 #print "prefix" + branchPrefix
2710 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
2711 if len(changes) <= 0:
2712 return False
2713 firstChange = changes[0]
2714 #print "first change in branch: %s" % firstChange
2715 sourceBranch = self.knownBranches[branch]
2716 sourceDepotPath = self.depotPaths[0] + sourceBranch
2717 sourceRef = self.gitRefForBranch(sourceBranch)
2718 #print "source " + sourceBranch
2720 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2721 #print "branch parent: %s" % branchParentChange
2722 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2723 if len(gitParent) > 0:
2724 self.initialParents[self.gitRefForBranch(branch)] = gitParent
2725 #print "parent git commit: %s" % gitParent
2727 self.importChanges(changes)
2728 return True
2730 def searchParent(self, parent, branch, target):
2731 parentFound = False
2732 for blob in read_pipe_lines(["git", "rev-list", "--reverse",
2733 "--no-merges", parent]):
2734 blob = blob.strip()
2735 if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2736 parentFound = True
2737 if self.verbose:
2738 print "Found parent of %s in commit %s" % (branch, blob)
2739 break
2740 if parentFound:
2741 return blob
2742 else:
2743 return None
2745 def importChanges(self, changes):
2746 cnt = 1
2747 for change in changes:
2748 description = p4_describe(change)
2749 self.updateOptionDict(description)
2751 if not self.silent:
2752 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2753 sys.stdout.flush()
2754 cnt = cnt + 1
2756 try:
2757 if self.detectBranches:
2758 branches = self.splitFilesIntoBranches(description)
2759 for branch in branches.keys():
2760 ## HACK --hwn
2761 branchPrefix = self.depotPaths[0] + branch + "/"
2762 self.branchPrefixes = [ branchPrefix ]
2764 parent = ""
2766 filesForCommit = branches[branch]
2768 if self.verbose:
2769 print "branch is %s" % branch
2771 self.updatedBranches.add(branch)
2773 if branch not in self.createdBranches:
2774 self.createdBranches.add(branch)
2775 parent = self.knownBranches[branch]
2776 if parent == branch:
2777 parent = ""
2778 else:
2779 fullBranch = self.projectName + branch
2780 if fullBranch not in self.p4BranchesInGit:
2781 if not self.silent:
2782 print("\n Importing new branch %s" % fullBranch);
2783 if self.importNewBranch(branch, change - 1):
2784 parent = ""
2785 self.p4BranchesInGit.append(fullBranch)
2786 if not self.silent:
2787 print("\n Resuming with change %s" % change);
2789 if self.verbose:
2790 print "parent determined through known branches: %s" % parent
2792 branch = self.gitRefForBranch(branch)
2793 parent = self.gitRefForBranch(parent)
2795 if self.verbose:
2796 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2798 if len(parent) == 0 and branch in self.initialParents:
2799 parent = self.initialParents[branch]
2800 del self.initialParents[branch]
2802 blob = None
2803 if len(parent) > 0:
2804 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2805 if self.verbose:
2806 print "Creating temporary branch: " + tempBranch
2807 self.commit(description, filesForCommit, tempBranch)
2808 self.tempBranches.append(tempBranch)
2809 self.checkpoint()
2810 blob = self.searchParent(parent, branch, tempBranch)
2811 if blob:
2812 self.commit(description, filesForCommit, branch, blob)
2813 else:
2814 if self.verbose:
2815 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2816 self.commit(description, filesForCommit, branch, parent)
2817 else:
2818 files = self.extractFilesFromCommit(description)
2819 self.commit(description, files, self.branch,
2820 self.initialParent)
2821 # only needed once, to connect to the previous commit
2822 self.initialParent = ""
2823 except IOError:
2824 print self.gitError.read()
2825 sys.exit(1)
2827 def importHeadRevision(self, revision):
2828 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2830 details = {}
2831 details["user"] = "git perforce import user"
2832 details["desc"] = ("Initial import of %s from the state at revision %s\n"
2833 % (' '.join(self.depotPaths), revision))
2834 details["change"] = revision
2835 newestRevision = 0
2837 fileCnt = 0
2838 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2840 for info in p4CmdList(["files"] + fileArgs):
2842 if 'code' in info and info['code'] == 'error':
2843 sys.stderr.write("p4 returned an error: %s\n"
2844 % info['data'])
2845 if info['data'].find("must refer to client") >= 0:
2846 sys.stderr.write("This particular p4 error is misleading.\n")
2847 sys.stderr.write("Perhaps the depot path was misspelled.\n");
2848 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
2849 sys.exit(1)
2850 if 'p4ExitCode' in info:
2851 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2852 sys.exit(1)
2855 change = int(info["change"])
2856 if change > newestRevision:
2857 newestRevision = change
2859 if info["action"] in self.delete_actions:
2860 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2861 #fileCnt = fileCnt + 1
2862 continue
2864 for prop in ["depotFile", "rev", "action", "type" ]:
2865 details["%s%s" % (prop, fileCnt)] = info[prop]
2867 fileCnt = fileCnt + 1
2869 details["change"] = newestRevision
2871 # Use time from top-most change so that all git p4 clones of
2872 # the same p4 repo have the same commit SHA1s.
2873 res = p4_describe(newestRevision)
2874 details["time"] = res["time"]
2876 self.updateOptionDict(details)
2877 try:
2878 self.commit(details, self.extractFilesFromCommit(details), self.branch)
2879 except IOError:
2880 print "IO error with git fast-import. Is your git version recent enough?"
2881 print self.gitError.read()
2884 def run(self, args):
2885 self.depotPaths = []
2886 self.changeRange = ""
2887 self.previousDepotPaths = []
2888 self.hasOrigin = False
2890 # map from branch depot path to parent branch
2891 self.knownBranches = {}
2892 self.initialParents = {}
2894 if self.importIntoRemotes:
2895 self.refPrefix = "refs/remotes/p4/"
2896 else:
2897 self.refPrefix = "refs/heads/p4/"
2899 if self.syncWithOrigin:
2900 self.hasOrigin = originP4BranchesExist()
2901 if self.hasOrigin:
2902 if not self.silent:
2903 print 'Syncing with origin first, using "git fetch origin"'
2904 system("git fetch origin")
2906 branch_arg_given = bool(self.branch)
2907 if len(self.branch) == 0:
2908 self.branch = self.refPrefix + "master"
2909 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2910 system("git update-ref %s refs/heads/p4" % self.branch)
2911 system("git branch -D p4")
2913 # accept either the command-line option, or the configuration variable
2914 if self.useClientSpec:
2915 # will use this after clone to set the variable
2916 self.useClientSpec_from_options = True
2917 else:
2918 if gitConfigBool("git-p4.useclientspec"):
2919 self.useClientSpec = True
2920 if self.useClientSpec:
2921 self.clientSpecDirs = getClientSpec()
2923 # TODO: should always look at previous commits,
2924 # merge with previous imports, if possible.
2925 if args == []:
2926 if self.hasOrigin:
2927 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2929 # branches holds mapping from branch name to sha1
2930 branches = p4BranchesInGit(self.importIntoRemotes)
2932 # restrict to just this one, disabling detect-branches
2933 if branch_arg_given:
2934 short = self.branch.split("/")[-1]
2935 if short in branches:
2936 self.p4BranchesInGit = [ short ]
2937 else:
2938 self.p4BranchesInGit = branches.keys()
2940 if len(self.p4BranchesInGit) > 1:
2941 if not self.silent:
2942 print "Importing from/into multiple branches"
2943 self.detectBranches = True
2944 for branch in branches.keys():
2945 self.initialParents[self.refPrefix + branch] = \
2946 branches[branch]
2948 if self.verbose:
2949 print "branches: %s" % self.p4BranchesInGit
2951 p4Change = 0
2952 for branch in self.p4BranchesInGit:
2953 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
2955 settings = extractSettingsGitLog(logMsg)
2957 self.readOptions(settings)
2958 if (settings.has_key('depot-paths')
2959 and settings.has_key ('change')):
2960 change = int(settings['change']) + 1
2961 p4Change = max(p4Change, change)
2963 depotPaths = sorted(settings['depot-paths'])
2964 if self.previousDepotPaths == []:
2965 self.previousDepotPaths = depotPaths
2966 else:
2967 paths = []
2968 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2969 prev_list = prev.split("/")
2970 cur_list = cur.split("/")
2971 for i in range(0, min(len(cur_list), len(prev_list))):
2972 if cur_list[i] <> prev_list[i]:
2973 i = i - 1
2974 break
2976 paths.append ("/".join(cur_list[:i + 1]))
2978 self.previousDepotPaths = paths
2980 if p4Change > 0:
2981 self.depotPaths = sorted(self.previousDepotPaths)
2982 self.changeRange = "@%s,#head" % p4Change
2983 if not self.silent and not self.detectBranches:
2984 print "Performing incremental import into %s git branch" % self.branch
2986 # accept multiple ref name abbreviations:
2987 # refs/foo/bar/branch -> use it exactly
2988 # p4/branch -> prepend refs/remotes/ or refs/heads/
2989 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2990 if not self.branch.startswith("refs/"):
2991 if self.importIntoRemotes:
2992 prepend = "refs/remotes/"
2993 else:
2994 prepend = "refs/heads/"
2995 if not self.branch.startswith("p4/"):
2996 prepend += "p4/"
2997 self.branch = prepend + self.branch
2999 if len(args) == 0 and self.depotPaths:
3000 if not self.silent:
3001 print "Depot paths: %s" % ' '.join(self.depotPaths)
3002 else:
3003 if self.depotPaths and self.depotPaths != args:
3004 print ("previous import used depot path %s and now %s was specified. "
3005 "This doesn't work!" % (' '.join (self.depotPaths),
3006 ' '.join (args)))
3007 sys.exit(1)
3009 self.depotPaths = sorted(args)
3011 revision = ""
3012 self.users = {}
3014 # Make sure no revision specifiers are used when --changesfile
3015 # is specified.
3016 bad_changesfile = False
3017 if len(self.changesFile) > 0:
3018 for p in self.depotPaths:
3019 if p.find("@") >= 0 or p.find("#") >= 0:
3020 bad_changesfile = True
3021 break
3022 if bad_changesfile:
3023 die("Option --changesfile is incompatible with revision specifiers")
3025 newPaths = []
3026 for p in self.depotPaths:
3027 if p.find("@") != -1:
3028 atIdx = p.index("@")
3029 self.changeRange = p[atIdx:]
3030 if self.changeRange == "@all":
3031 self.changeRange = ""
3032 elif ',' not in self.changeRange:
3033 revision = self.changeRange
3034 self.changeRange = ""
3035 p = p[:atIdx]
3036 elif p.find("#") != -1:
3037 hashIdx = p.index("#")
3038 revision = p[hashIdx:]
3039 p = p[:hashIdx]
3040 elif self.previousDepotPaths == []:
3041 # pay attention to changesfile, if given, else import
3042 # the entire p4 tree at the head revision
3043 if len(self.changesFile) == 0:
3044 revision = "#head"
3046 p = re.sub ("\.\.\.$", "", p)
3047 if not p.endswith("/"):
3048 p += "/"
3050 newPaths.append(p)
3052 self.depotPaths = newPaths
3054 # --detect-branches may change this for each branch
3055 self.branchPrefixes = self.depotPaths
3057 self.loadUserMapFromCache()
3058 self.labels = {}
3059 if self.detectLabels:
3060 self.getLabels();
3062 if self.detectBranches:
3063 ## FIXME - what's a P4 projectName ?
3064 self.projectName = self.guessProjectName()
3066 if self.hasOrigin:
3067 self.getBranchMappingFromGitBranches()
3068 else:
3069 self.getBranchMapping()
3070 if self.verbose:
3071 print "p4-git branches: %s" % self.p4BranchesInGit
3072 print "initial parents: %s" % self.initialParents
3073 for b in self.p4BranchesInGit:
3074 if b != "master":
3076 ## FIXME
3077 b = b[len(self.projectName):]
3078 self.createdBranches.add(b)
3080 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
3082 self.importProcess = subprocess.Popen(["git", "fast-import"],
3083 stdin=subprocess.PIPE,
3084 stdout=subprocess.PIPE,
3085 stderr=subprocess.PIPE);
3086 self.gitOutput = self.importProcess.stdout
3087 self.gitStream = self.importProcess.stdin
3088 self.gitError = self.importProcess.stderr
3090 if revision:
3091 self.importHeadRevision(revision)
3092 else:
3093 changes = []
3095 if len(self.changesFile) > 0:
3096 output = open(self.changesFile).readlines()
3097 changeSet = set()
3098 for line in output:
3099 changeSet.add(int(line))
3101 for change in changeSet:
3102 changes.append(change)
3104 changes.sort()
3105 else:
3106 # catch "git p4 sync" with no new branches, in a repo that
3107 # does not have any existing p4 branches
3108 if len(args) == 0:
3109 if not self.p4BranchesInGit:
3110 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3112 # The default branch is master, unless --branch is used to
3113 # specify something else. Make sure it exists, or complain
3114 # nicely about how to use --branch.
3115 if not self.detectBranches:
3116 if not branch_exists(self.branch):
3117 if branch_arg_given:
3118 die("Error: branch %s does not exist." % self.branch)
3119 else:
3120 die("Error: no branch %s; perhaps specify one with --branch." %
3121 self.branch)
3123 if self.verbose:
3124 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3125 self.changeRange)
3126 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3128 if len(self.maxChanges) > 0:
3129 changes = changes[:min(int(self.maxChanges), len(changes))]
3131 if len(changes) == 0:
3132 if not self.silent:
3133 print "No changes to import!"
3134 else:
3135 if not self.silent and not self.detectBranches:
3136 print "Import destination: %s" % self.branch
3138 self.updatedBranches = set()
3140 if not self.detectBranches:
3141 if args:
3142 # start a new branch
3143 self.initialParent = ""
3144 else:
3145 # build on a previous revision
3146 self.initialParent = parseRevision(self.branch)
3148 self.importChanges(changes)
3150 if not self.silent:
3151 print ""
3152 if len(self.updatedBranches) > 0:
3153 sys.stdout.write("Updated branches: ")
3154 for b in self.updatedBranches:
3155 sys.stdout.write("%s " % b)
3156 sys.stdout.write("\n")
3158 if gitConfigBool("git-p4.importLabels"):
3159 self.importLabels = True
3161 if self.importLabels:
3162 p4Labels = getP4Labels(self.depotPaths)
3163 gitTags = getGitTags()
3165 missingP4Labels = p4Labels - gitTags
3166 self.importP4Labels(self.gitStream, missingP4Labels)
3168 self.gitStream.close()
3169 if self.importProcess.wait() != 0:
3170 die("fast-import failed: %s" % self.gitError.read())
3171 self.gitOutput.close()
3172 self.gitError.close()
3174 # Cleanup temporary branches created during import
3175 if self.tempBranches != []:
3176 for branch in self.tempBranches:
3177 read_pipe("git update-ref -d %s" % branch)
3178 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3180 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3181 # a convenient shortcut refname "p4".
3182 if self.importIntoRemotes:
3183 head_ref = self.refPrefix + "HEAD"
3184 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3185 system(["git", "symbolic-ref", head_ref, self.branch])
3187 return True
3189 class P4Rebase(Command):
3190 def __init__(self):
3191 Command.__init__(self)
3192 self.options = [
3193 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3195 self.importLabels = False
3196 self.description = ("Fetches the latest revision from perforce and "
3197 + "rebases the current work (branch) against it")
3199 def run(self, args):
3200 sync = P4Sync()
3201 sync.importLabels = self.importLabels
3202 sync.run([])
3204 return self.rebase()
3206 def rebase(self):
3207 if os.system("git update-index --refresh") != 0:
3208 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.");
3209 if len(read_pipe("git diff-index HEAD --")) > 0:
3210 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3212 [upstream, settings] = findUpstreamBranchPoint()
3213 if len(upstream) == 0:
3214 die("Cannot find upstream branchpoint for rebase")
3216 # the branchpoint may be p4/foo~3, so strip off the parent
3217 upstream = re.sub("~[0-9]+$", "", upstream)
3219 print "Rebasing the current branch onto %s" % upstream
3220 oldHead = read_pipe("git rev-parse HEAD").strip()
3221 system("git rebase %s" % upstream)
3222 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3223 return True
3225 class P4Clone(P4Sync):
3226 def __init__(self):
3227 P4Sync.__init__(self)
3228 self.description = "Creates a new git repository and imports from Perforce into it"
3229 self.usage = "usage: %prog [options] //depot/path[@revRange]"
3230 self.options += [
3231 optparse.make_option("--destination", dest="cloneDestination",
3232 action='store', default=None,
3233 help="where to leave result of the clone"),
3234 optparse.make_option("--bare", dest="cloneBare",
3235 action="store_true", default=False),
3237 self.cloneDestination = None
3238 self.needsGit = False
3239 self.cloneBare = False
3241 def defaultDestination(self, args):
3242 ## TODO: use common prefix of args?
3243 depotPath = args[0]
3244 depotDir = re.sub("(@[^@]*)$", "", depotPath)
3245 depotDir = re.sub("(#[^#]*)$", "", depotDir)
3246 depotDir = re.sub(r"\.\.\.$", "", depotDir)
3247 depotDir = re.sub(r"/$", "", depotDir)
3248 return os.path.split(depotDir)[1]
3250 def run(self, args):
3251 if len(args) < 1:
3252 return False
3254 if self.keepRepoPath and not self.cloneDestination:
3255 sys.stderr.write("Must specify destination for --keep-path\n")
3256 sys.exit(1)
3258 depotPaths = args
3260 if not self.cloneDestination and len(depotPaths) > 1:
3261 self.cloneDestination = depotPaths[-1]
3262 depotPaths = depotPaths[:-1]
3264 self.cloneExclude = ["/"+p for p in self.cloneExclude]
3265 for p in depotPaths:
3266 if not p.startswith("//"):
3267 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3268 return False
3270 if not self.cloneDestination:
3271 self.cloneDestination = self.defaultDestination(args)
3273 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3275 if not os.path.exists(self.cloneDestination):
3276 os.makedirs(self.cloneDestination)
3277 chdir(self.cloneDestination)
3279 init_cmd = [ "git", "init" ]
3280 if self.cloneBare:
3281 init_cmd.append("--bare")
3282 retcode = subprocess.call(init_cmd)
3283 if retcode:
3284 raise CalledProcessError(retcode, init_cmd)
3286 if not P4Sync.run(self, depotPaths):
3287 return False
3289 # create a master branch and check out a work tree
3290 if gitBranchExists(self.branch):
3291 system([ "git", "branch", "master", self.branch ])
3292 if not self.cloneBare:
3293 system([ "git", "checkout", "-f" ])
3294 else:
3295 print 'Not checking out any branch, use ' \
3296 '"git checkout -q -b master <branch>"'
3298 # auto-set this variable if invoked with --use-client-spec
3299 if self.useClientSpec_from_options:
3300 system("git config --bool git-p4.useclientspec true")
3302 return True
3304 class P4Branches(Command):
3305 def __init__(self):
3306 Command.__init__(self)
3307 self.options = [ ]
3308 self.description = ("Shows the git branches that hold imports and their "
3309 + "corresponding perforce depot paths")
3310 self.verbose = False
3312 def run(self, args):
3313 if originP4BranchesExist():
3314 createOrUpdateBranchesFromOrigin()
3316 cmdline = "git rev-parse --symbolic "
3317 cmdline += " --remotes"
3319 for line in read_pipe_lines(cmdline):
3320 line = line.strip()
3322 if not line.startswith('p4/') or line == "p4/HEAD":
3323 continue
3324 branch = line
3326 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3327 settings = extractSettingsGitLog(log)
3329 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3330 return True
3332 class HelpFormatter(optparse.IndentedHelpFormatter):
3333 def __init__(self):
3334 optparse.IndentedHelpFormatter.__init__(self)
3336 def format_description(self, description):
3337 if description:
3338 return description + "\n"
3339 else:
3340 return ""
3342 def printUsage(commands):
3343 print "usage: %s <command> [options]" % sys.argv[0]
3344 print ""
3345 print "valid commands: %s" % ", ".join(commands)
3346 print ""
3347 print "Try %s <command> --help for command specific help." % sys.argv[0]
3348 print ""
3350 commands = {
3351 "debug" : P4Debug,
3352 "submit" : P4Submit,
3353 "commit" : P4Submit,
3354 "sync" : P4Sync,
3355 "rebase" : P4Rebase,
3356 "clone" : P4Clone,
3357 "rollback" : P4RollBack,
3358 "branches" : P4Branches
3362 def main():
3363 if len(sys.argv[1:]) == 0:
3364 printUsage(commands.keys())
3365 sys.exit(2)
3367 cmdName = sys.argv[1]
3368 try:
3369 klass = commands[cmdName]
3370 cmd = klass()
3371 except KeyError:
3372 print "unknown command %s" % cmdName
3373 print ""
3374 printUsage(commands.keys())
3375 sys.exit(2)
3377 options = cmd.options
3378 cmd.gitdir = os.environ.get("GIT_DIR", None)
3380 args = sys.argv[2:]
3382 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3383 if cmd.needsGit:
3384 options.append(optparse.make_option("--git-dir", dest="gitdir"))
3386 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3387 options,
3388 description = cmd.description,
3389 formatter = HelpFormatter())
3391 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3392 global verbose
3393 verbose = cmd.verbose
3394 if cmd.needsGit:
3395 if cmd.gitdir == None:
3396 cmd.gitdir = os.path.abspath(".git")
3397 if not isValidGitDir(cmd.gitdir):
3398 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3399 if os.path.exists(cmd.gitdir):
3400 cdup = read_pipe("git rev-parse --show-cdup").strip()
3401 if len(cdup) > 0:
3402 chdir(cdup);
3404 if not isValidGitDir(cmd.gitdir):
3405 if isValidGitDir(cmd.gitdir + "/.git"):
3406 cmd.gitdir += "/.git"
3407 else:
3408 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3410 os.environ["GIT_DIR"] = cmd.gitdir
3412 if not cmd.run(args):
3413 parser.print_help()
3414 sys.exit(2)
3417 if __name__ == '__main__':
3418 main()