git-p4: small improvements to user-preservation
[git/debian.git] / contrib / fast-import / git-p4
blobe66a7df90839e785e555b82c6235bee4def9b636
1 #!/usr/bin/env python
3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
5 # Author: Simon Hausmann <simon@lst.de>
6 # Copyright: 2007 Simon Hausmann <simon@lst.de>
7 # 2007 Trolltech ASA
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
11 import optparse, sys, os, marshal, subprocess, shelve
12 import tempfile, getopt, os.path, time, platform
13 import re
15 verbose = False
18 def p4_build_cmd(cmd):
19 """Build a suitable p4 command line.
21 This consolidates building and returning a p4 command line into one
22 location. It means that hooking into the environment, or other configuration
23 can be done more easily.
24 """
25 real_cmd = "%s " % "p4"
27 user = gitConfig("git-p4.user")
28 if len(user) > 0:
29 real_cmd += "-u %s " % user
31 password = gitConfig("git-p4.password")
32 if len(password) > 0:
33 real_cmd += "-P %s " % password
35 port = gitConfig("git-p4.port")
36 if len(port) > 0:
37 real_cmd += "-p %s " % port
39 host = gitConfig("git-p4.host")
40 if len(host) > 0:
41 real_cmd += "-h %s " % host
43 client = gitConfig("git-p4.client")
44 if len(client) > 0:
45 real_cmd += "-c %s " % client
47 real_cmd += "%s" % (cmd)
48 if verbose:
49 print real_cmd
50 return real_cmd
52 def chdir(dir):
53 if os.name == 'nt':
54 os.environ['PWD']=dir
55 os.chdir(dir)
57 def die(msg):
58 if verbose:
59 raise Exception(msg)
60 else:
61 sys.stderr.write(msg + "\n")
62 sys.exit(1)
64 def write_pipe(c, str):
65 if verbose:
66 sys.stderr.write('Writing pipe: %s\n' % c)
68 pipe = os.popen(c, 'w')
69 val = pipe.write(str)
70 if pipe.close():
71 die('Command failed: %s' % c)
73 return val
75 def p4_write_pipe(c, str):
76 real_cmd = p4_build_cmd(c)
77 return write_pipe(real_cmd, str)
79 def read_pipe(c, ignore_error=False):
80 if verbose:
81 sys.stderr.write('Reading pipe: %s\n' % c)
83 pipe = os.popen(c, 'rb')
84 val = pipe.read()
85 if pipe.close() and not ignore_error:
86 die('Command failed: %s' % c)
88 return val
90 def p4_read_pipe(c, ignore_error=False):
91 real_cmd = p4_build_cmd(c)
92 return read_pipe(real_cmd, ignore_error)
94 def read_pipe_lines(c):
95 if verbose:
96 sys.stderr.write('Reading pipe: %s\n' % c)
97 ## todo: check return status
98 pipe = os.popen(c, 'rb')
99 val = pipe.readlines()
100 if pipe.close():
101 die('Command failed: %s' % c)
103 return val
105 def p4_read_pipe_lines(c):
106 """Specifically invoke p4 on the command supplied. """
107 real_cmd = p4_build_cmd(c)
108 return read_pipe_lines(real_cmd)
110 def system(cmd):
111 if verbose:
112 sys.stderr.write("executing %s\n" % cmd)
113 if os.system(cmd) != 0:
114 die("command failed: %s" % cmd)
116 def p4_system(cmd):
117 """Specifically invoke p4 as the system command. """
118 real_cmd = p4_build_cmd(cmd)
119 return system(real_cmd)
121 def isP4Exec(kind):
122 """Determine if a Perforce 'kind' should have execute permission
124 'p4 help filetypes' gives a list of the types. If it starts with 'x',
125 or x follows one of a few letters. Otherwise, if there is an 'x' after
126 a plus sign, it is also executable"""
127 return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
129 def setP4ExecBit(file, mode):
130 # Reopens an already open file and changes the execute bit to match
131 # the execute bit setting in the passed in mode.
133 p4Type = "+x"
135 if not isModeExec(mode):
136 p4Type = getP4OpenedType(file)
137 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
138 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
139 if p4Type[-1] == "+":
140 p4Type = p4Type[0:-1]
142 p4_system("reopen -t %s %s" % (p4Type, file))
144 def getP4OpenedType(file):
145 # Returns the perforce file type for the given file.
147 result = p4_read_pipe("opened %s" % file)
148 match = re.match(".*\((.+)\)\r?$", result)
149 if match:
150 return match.group(1)
151 else:
152 die("Could not determine file type for %s (result: '%s')" % (file, result))
154 def diffTreePattern():
155 # This is a simple generator for the diff tree regex pattern. This could be
156 # a class variable if this and parseDiffTreeEntry were a part of a class.
157 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
158 while True:
159 yield pattern
161 def parseDiffTreeEntry(entry):
162 """Parses a single diff tree entry into its component elements.
164 See git-diff-tree(1) manpage for details about the format of the diff
165 output. This method returns a dictionary with the following elements:
167 src_mode - The mode of the source file
168 dst_mode - The mode of the destination file
169 src_sha1 - The sha1 for the source file
170 dst_sha1 - The sha1 fr the destination file
171 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
172 status_score - The score for the status (applicable for 'C' and 'R'
173 statuses). This is None if there is no score.
174 src - The path for the source file.
175 dst - The path for the destination file. This is only present for
176 copy or renames. If it is not present, this is None.
178 If the pattern is not matched, None is returned."""
180 match = diffTreePattern().next().match(entry)
181 if match:
182 return {
183 'src_mode': match.group(1),
184 'dst_mode': match.group(2),
185 'src_sha1': match.group(3),
186 'dst_sha1': match.group(4),
187 'status': match.group(5),
188 'status_score': match.group(6),
189 'src': match.group(7),
190 'dst': match.group(10)
192 return None
194 def isModeExec(mode):
195 # Returns True if the given git mode represents an executable file,
196 # otherwise False.
197 return mode[-3:] == "755"
199 def isModeExecChanged(src_mode, dst_mode):
200 return isModeExec(src_mode) != isModeExec(dst_mode)
202 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
203 cmd = p4_build_cmd("-G %s" % (cmd))
204 if verbose:
205 sys.stderr.write("Opening pipe: %s\n" % cmd)
207 # Use a temporary file to avoid deadlocks without
208 # subprocess.communicate(), which would put another copy
209 # of stdout into memory.
210 stdin_file = None
211 if stdin is not None:
212 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
213 stdin_file.write(stdin)
214 stdin_file.flush()
215 stdin_file.seek(0)
217 p4 = subprocess.Popen(cmd, shell=True,
218 stdin=stdin_file,
219 stdout=subprocess.PIPE)
221 result = []
222 try:
223 while True:
224 entry = marshal.load(p4.stdout)
225 if cb is not None:
226 cb(entry)
227 else:
228 result.append(entry)
229 except EOFError:
230 pass
231 exitCode = p4.wait()
232 if exitCode != 0:
233 entry = {}
234 entry["p4ExitCode"] = exitCode
235 result.append(entry)
237 return result
239 def p4Cmd(cmd):
240 list = p4CmdList(cmd)
241 result = {}
242 for entry in list:
243 result.update(entry)
244 return result;
246 def p4Where(depotPath):
247 if not depotPath.endswith("/"):
248 depotPath += "/"
249 depotPath = depotPath + "..."
250 outputList = p4CmdList("where %s" % depotPath)
251 output = None
252 for entry in outputList:
253 if "depotFile" in entry:
254 if entry["depotFile"] == depotPath:
255 output = entry
256 break
257 elif "data" in entry:
258 data = entry.get("data")
259 space = data.find(" ")
260 if data[:space] == depotPath:
261 output = entry
262 break
263 if output == None:
264 return ""
265 if output["code"] == "error":
266 return ""
267 clientPath = ""
268 if "path" in output:
269 clientPath = output.get("path")
270 elif "data" in output:
271 data = output.get("data")
272 lastSpace = data.rfind(" ")
273 clientPath = data[lastSpace + 1:]
275 if clientPath.endswith("..."):
276 clientPath = clientPath[:-3]
277 return clientPath
279 def currentGitBranch():
280 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
282 def isValidGitDir(path):
283 if (os.path.exists(path + "/HEAD")
284 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
285 return True;
286 return False
288 def parseRevision(ref):
289 return read_pipe("git rev-parse %s" % ref).strip()
291 def extractLogMessageFromGitCommit(commit):
292 logMessage = ""
294 ## fixme: title is first line of commit, not 1st paragraph.
295 foundTitle = False
296 for log in read_pipe_lines("git cat-file commit %s" % commit):
297 if not foundTitle:
298 if len(log) == 1:
299 foundTitle = True
300 continue
302 logMessage += log
303 return logMessage
305 def extractSettingsGitLog(log):
306 values = {}
307 for line in log.split("\n"):
308 line = line.strip()
309 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
310 if not m:
311 continue
313 assignments = m.group(1).split (':')
314 for a in assignments:
315 vals = a.split ('=')
316 key = vals[0].strip()
317 val = ('='.join (vals[1:])).strip()
318 if val.endswith ('\"') and val.startswith('"'):
319 val = val[1:-1]
321 values[key] = val
323 paths = values.get("depot-paths")
324 if not paths:
325 paths = values.get("depot-path")
326 if paths:
327 values['depot-paths'] = paths.split(',')
328 return values
330 def gitBranchExists(branch):
331 proc = subprocess.Popen(["git", "rev-parse", branch],
332 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
333 return proc.wait() == 0;
335 _gitConfig = {}
336 def gitConfig(key, args = None): # set args to "--bool", for instance
337 if not _gitConfig.has_key(key):
338 argsFilter = ""
339 if args != None:
340 argsFilter = "%s " % args
341 cmd = "git config %s%s" % (argsFilter, key)
342 _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
343 return _gitConfig[key]
345 def p4BranchesInGit(branchesAreInRemotes = True):
346 branches = {}
348 cmdline = "git rev-parse --symbolic "
349 if branchesAreInRemotes:
350 cmdline += " --remotes"
351 else:
352 cmdline += " --branches"
354 for line in read_pipe_lines(cmdline):
355 line = line.strip()
357 ## only import to p4/
358 if not line.startswith('p4/') or line == "p4/HEAD":
359 continue
360 branch = line
362 # strip off p4
363 branch = re.sub ("^p4/", "", line)
365 branches[branch] = parseRevision(line)
366 return branches
368 def findUpstreamBranchPoint(head = "HEAD"):
369 branches = p4BranchesInGit()
370 # map from depot-path to branch name
371 branchByDepotPath = {}
372 for branch in branches.keys():
373 tip = branches[branch]
374 log = extractLogMessageFromGitCommit(tip)
375 settings = extractSettingsGitLog(log)
376 if settings.has_key("depot-paths"):
377 paths = ",".join(settings["depot-paths"])
378 branchByDepotPath[paths] = "remotes/p4/" + branch
380 settings = None
381 parent = 0
382 while parent < 65535:
383 commit = head + "~%s" % parent
384 log = extractLogMessageFromGitCommit(commit)
385 settings = extractSettingsGitLog(log)
386 if settings.has_key("depot-paths"):
387 paths = ",".join(settings["depot-paths"])
388 if branchByDepotPath.has_key(paths):
389 return [branchByDepotPath[paths], settings]
391 parent = parent + 1
393 return ["", settings]
395 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
396 if not silent:
397 print ("Creating/updating branch(es) in %s based on origin branch(es)"
398 % localRefPrefix)
400 originPrefix = "origin/p4/"
402 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
403 line = line.strip()
404 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
405 continue
407 headName = line[len(originPrefix):]
408 remoteHead = localRefPrefix + headName
409 originHead = line
411 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
412 if (not original.has_key('depot-paths')
413 or not original.has_key('change')):
414 continue
416 update = False
417 if not gitBranchExists(remoteHead):
418 if verbose:
419 print "creating %s" % remoteHead
420 update = True
421 else:
422 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
423 if settings.has_key('change') > 0:
424 if settings['depot-paths'] == original['depot-paths']:
425 originP4Change = int(original['change'])
426 p4Change = int(settings['change'])
427 if originP4Change > p4Change:
428 print ("%s (%s) is newer than %s (%s). "
429 "Updating p4 branch from origin."
430 % (originHead, originP4Change,
431 remoteHead, p4Change))
432 update = True
433 else:
434 print ("Ignoring: %s was imported from %s while "
435 "%s was imported from %s"
436 % (originHead, ','.join(original['depot-paths']),
437 remoteHead, ','.join(settings['depot-paths'])))
439 if update:
440 system("git update-ref %s %s" % (remoteHead, originHead))
442 def originP4BranchesExist():
443 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
445 def p4ChangesForPaths(depotPaths, changeRange):
446 assert depotPaths
447 output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
448 for p in depotPaths]))
450 changes = {}
451 for line in output:
452 changeNum = int(line.split(" ")[1])
453 changes[changeNum] = True
455 changelist = changes.keys()
456 changelist.sort()
457 return changelist
459 def p4PathStartsWith(path, prefix):
460 # This method tries to remedy a potential mixed-case issue:
462 # If UserA adds //depot/DirA/file1
463 # and UserB adds //depot/dira/file2
465 # we may or may not have a problem. If you have core.ignorecase=true,
466 # we treat DirA and dira as the same directory
467 ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
468 if ignorecase:
469 return path.lower().startswith(prefix.lower())
470 return path.startswith(prefix)
472 class Command:
473 def __init__(self):
474 self.usage = "usage: %prog [options]"
475 self.needsGit = True
477 class P4UserMap:
478 def __init__(self):
479 self.userMapFromPerforceServer = False
481 def getUserCacheFilename(self):
482 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
483 return home + "/.gitp4-usercache.txt"
485 def getUserMapFromPerforceServer(self):
486 if self.userMapFromPerforceServer:
487 return
488 self.users = {}
489 self.emails = {}
491 for output in p4CmdList("users"):
492 if not output.has_key("User"):
493 continue
494 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
495 self.emails[output["Email"]] = output["User"]
498 s = ''
499 for (key, val) in self.users.items():
500 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
502 open(self.getUserCacheFilename(), "wb").write(s)
503 self.userMapFromPerforceServer = True
505 def loadUserMapFromCache(self):
506 self.users = {}
507 self.userMapFromPerforceServer = False
508 try:
509 cache = open(self.getUserCacheFilename(), "rb")
510 lines = cache.readlines()
511 cache.close()
512 for line in lines:
513 entry = line.strip().split("\t")
514 self.users[entry[0]] = entry[1]
515 except IOError:
516 self.getUserMapFromPerforceServer()
518 class P4Debug(Command):
519 def __init__(self):
520 Command.__init__(self)
521 self.options = [
522 optparse.make_option("--verbose", dest="verbose", action="store_true",
523 default=False),
525 self.description = "A tool to debug the output of p4 -G."
526 self.needsGit = False
527 self.verbose = False
529 def run(self, args):
530 j = 0
531 for output in p4CmdList(" ".join(args)):
532 print 'Element: %d' % j
533 j += 1
534 print output
535 return True
537 class P4RollBack(Command):
538 def __init__(self):
539 Command.__init__(self)
540 self.options = [
541 optparse.make_option("--verbose", dest="verbose", action="store_true"),
542 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
544 self.description = "A tool to debug the multi-branch import. Don't use :)"
545 self.verbose = False
546 self.rollbackLocalBranches = False
548 def run(self, args):
549 if len(args) != 1:
550 return False
551 maxChange = int(args[0])
553 if "p4ExitCode" in p4Cmd("changes -m 1"):
554 die("Problems executing p4");
556 if self.rollbackLocalBranches:
557 refPrefix = "refs/heads/"
558 lines = read_pipe_lines("git rev-parse --symbolic --branches")
559 else:
560 refPrefix = "refs/remotes/"
561 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
563 for line in lines:
564 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
565 line = line.strip()
566 ref = refPrefix + line
567 log = extractLogMessageFromGitCommit(ref)
568 settings = extractSettingsGitLog(log)
570 depotPaths = settings['depot-paths']
571 change = settings['change']
573 changed = False
575 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
576 for p in depotPaths]))) == 0:
577 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
578 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
579 continue
581 while change and int(change) > maxChange:
582 changed = True
583 if self.verbose:
584 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
585 system("git update-ref %s \"%s^\"" % (ref, ref))
586 log = extractLogMessageFromGitCommit(ref)
587 settings = extractSettingsGitLog(log)
590 depotPaths = settings['depot-paths']
591 change = settings['change']
593 if changed:
594 print "%s rewound to %s" % (ref, change)
596 return True
598 class P4Submit(Command, P4UserMap):
599 def __init__(self):
600 Command.__init__(self)
601 P4UserMap.__init__(self)
602 self.options = [
603 optparse.make_option("--verbose", dest="verbose", action="store_true"),
604 optparse.make_option("--origin", dest="origin"),
605 optparse.make_option("-M", dest="detectRenames", action="store_true"),
606 # preserve the user, requires relevant p4 permissions
607 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
609 self.description = "Submit changes from git to the perforce depot."
610 self.usage += " [name of git branch to submit into perforce depot]"
611 self.interactive = True
612 self.origin = ""
613 self.detectRenames = False
614 self.verbose = False
615 self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
616 self.isWindows = (platform.system() == "Windows")
618 def check(self):
619 if len(p4CmdList("opened ...")) > 0:
620 die("You have files opened with perforce! Close them before starting the sync.")
622 # replaces everything between 'Description:' and the next P4 submit template field with the
623 # commit message
624 def prepareLogMessage(self, template, message):
625 result = ""
627 inDescriptionSection = False
629 for line in template.split("\n"):
630 if line.startswith("#"):
631 result += line + "\n"
632 continue
634 if inDescriptionSection:
635 if line.startswith("Files:") or line.startswith("Jobs:"):
636 inDescriptionSection = False
637 else:
638 continue
639 else:
640 if line.startswith("Description:"):
641 inDescriptionSection = True
642 line += "\n"
643 for messageLine in message.split("\n"):
644 line += "\t" + messageLine + "\n"
646 result += line + "\n"
648 return result
650 def p4UserForCommit(self,id):
651 # Return the tuple (perforce user,git email) for a given git commit id
652 self.getUserMapFromPerforceServer()
653 gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
654 gitEmail = gitEmail.strip()
655 if not self.emails.has_key(gitEmail):
656 return (None,gitEmail)
657 else:
658 return (self.emails[gitEmail],gitEmail)
660 def checkValidP4Users(self,commits):
661 # check if any git authors cannot be mapped to p4 users
662 for id in commits:
663 (user,email) = self.p4UserForCommit(id)
664 if not user:
665 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
666 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
667 print "%s" % msg
668 else:
669 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
671 def lastP4Changelist(self):
672 # Get back the last changelist number submitted in this client spec. This
673 # then gets used to patch up the username in the change. If the same
674 # client spec is being used by multiple processes then this might go
675 # wrong.
676 results = p4CmdList("client -o") # find the current client
677 client = None
678 for r in results:
679 if r.has_key('Client'):
680 client = r['Client']
681 break
682 if not client:
683 die("could not get client spec")
684 results = p4CmdList("changes -c %s -m 1" % client)
685 for r in results:
686 if r.has_key('change'):
687 return r['change']
688 die("Could not get changelist number for last submit - cannot patch up user details")
690 def modifyChangelistUser(self, changelist, newUser):
691 # fixup the user field of a changelist after it has been submitted.
692 changes = p4CmdList("change -o %s" % changelist)
693 if len(changes) != 1:
694 die("Bad output from p4 change modifying %s to user %s" %
695 (changelist, newUser))
697 c = changes[0]
698 if c['User'] == newUser: return # nothing to do
699 c['User'] = newUser
700 input = marshal.dumps(c)
702 result = p4CmdList("change -f -i", stdin=input)
703 for r in result:
704 if r.has_key('code'):
705 if r['code'] == 'error':
706 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
707 if r.has_key('data'):
708 print("Updated user field for changelist %s to %s" % (changelist, newUser))
709 return
710 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
712 def canChangeChangelists(self):
713 # check to see if we have p4 admin or super-user permissions, either of
714 # which are required to modify changelists.
715 results = p4CmdList("protects %s" % self.depotPath)
716 for r in results:
717 if r.has_key('perm'):
718 if r['perm'] == 'admin':
719 return 1
720 if r['perm'] == 'super':
721 return 1
722 return 0
724 def prepareSubmitTemplate(self):
725 # remove lines in the Files section that show changes to files outside the depot path we're committing into
726 template = ""
727 inFilesSection = False
728 for line in p4_read_pipe_lines("change -o"):
729 if line.endswith("\r\n"):
730 line = line[:-2] + "\n"
731 if inFilesSection:
732 if line.startswith("\t"):
733 # path starts and ends with a tab
734 path = line[1:]
735 lastTab = path.rfind("\t")
736 if lastTab != -1:
737 path = path[:lastTab]
738 if not p4PathStartsWith(path, self.depotPath):
739 continue
740 else:
741 inFilesSection = False
742 else:
743 if line.startswith("Files:"):
744 inFilesSection = True
746 template += line
748 return template
750 def applyCommit(self, id):
751 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
753 if self.preserveUser:
754 (p4User, gitEmail) = self.p4UserForCommit(id)
756 if not self.detectRenames:
757 # If not explicitly set check the config variable
758 self.detectRenames = gitConfig("git-p4.detectRenames").lower() == "true"
760 if self.detectRenames:
761 diffOpts = "-M"
762 else:
763 diffOpts = ""
765 if gitConfig("git-p4.detectCopies").lower() == "true":
766 diffOpts += " -C"
768 if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
769 diffOpts += " --find-copies-harder"
771 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
772 filesToAdd = set()
773 filesToDelete = set()
774 editedFiles = set()
775 filesToChangeExecBit = {}
776 for line in diff:
777 diff = parseDiffTreeEntry(line)
778 modifier = diff['status']
779 path = diff['src']
780 if modifier == "M":
781 p4_system("edit \"%s\"" % path)
782 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
783 filesToChangeExecBit[path] = diff['dst_mode']
784 editedFiles.add(path)
785 elif modifier == "A":
786 filesToAdd.add(path)
787 filesToChangeExecBit[path] = diff['dst_mode']
788 if path in filesToDelete:
789 filesToDelete.remove(path)
790 elif modifier == "D":
791 filesToDelete.add(path)
792 if path in filesToAdd:
793 filesToAdd.remove(path)
794 elif modifier == "C":
795 src, dest = diff['src'], diff['dst']
796 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
797 if diff['src_sha1'] != diff['dst_sha1']:
798 p4_system("edit \"%s\"" % (dest))
799 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
800 p4_system("edit \"%s\"" % (dest))
801 filesToChangeExecBit[dest] = diff['dst_mode']
802 os.unlink(dest)
803 editedFiles.add(dest)
804 elif modifier == "R":
805 src, dest = diff['src'], diff['dst']
806 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
807 if diff['src_sha1'] != diff['dst_sha1']:
808 p4_system("edit \"%s\"" % (dest))
809 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
810 p4_system("edit \"%s\"" % (dest))
811 filesToChangeExecBit[dest] = diff['dst_mode']
812 os.unlink(dest)
813 editedFiles.add(dest)
814 filesToDelete.add(src)
815 else:
816 die("unknown modifier %s for %s" % (modifier, path))
818 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
819 patchcmd = diffcmd + " | git apply "
820 tryPatchCmd = patchcmd + "--check -"
821 applyPatchCmd = patchcmd + "--check --apply -"
823 if os.system(tryPatchCmd) != 0:
824 print "Unfortunately applying the change failed!"
825 print "What do you want to do?"
826 response = "x"
827 while response != "s" and response != "a" and response != "w":
828 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
829 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
830 if response == "s":
831 print "Skipping! Good luck with the next patches..."
832 for f in editedFiles:
833 p4_system("revert \"%s\"" % f);
834 for f in filesToAdd:
835 system("rm %s" %f)
836 return
837 elif response == "a":
838 os.system(applyPatchCmd)
839 if len(filesToAdd) > 0:
840 print "You may also want to call p4 add on the following files:"
841 print " ".join(filesToAdd)
842 if len(filesToDelete):
843 print "The following files should be scheduled for deletion with p4 delete:"
844 print " ".join(filesToDelete)
845 die("Please resolve and submit the conflict manually and "
846 + "continue afterwards with git-p4 submit --continue")
847 elif response == "w":
848 system(diffcmd + " > patch.txt")
849 print "Patch saved to patch.txt in %s !" % self.clientPath
850 die("Please resolve and submit the conflict manually and "
851 "continue afterwards with git-p4 submit --continue")
853 system(applyPatchCmd)
855 for f in filesToAdd:
856 p4_system("add \"%s\"" % f)
857 for f in filesToDelete:
858 p4_system("revert \"%s\"" % f)
859 p4_system("delete \"%s\"" % f)
861 # Set/clear executable bits
862 for f in filesToChangeExecBit.keys():
863 mode = filesToChangeExecBit[f]
864 setP4ExecBit(f, mode)
866 logMessage = extractLogMessageFromGitCommit(id)
867 logMessage = logMessage.strip()
869 template = self.prepareSubmitTemplate()
871 if self.interactive:
872 submitTemplate = self.prepareLogMessage(template, logMessage)
874 if self.preserveUser:
875 submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
877 if os.environ.has_key("P4DIFF"):
878 del(os.environ["P4DIFF"])
879 diff = ""
880 for editedFile in editedFiles:
881 diff += p4_read_pipe("diff -du %r" % editedFile)
883 newdiff = ""
884 for newFile in filesToAdd:
885 newdiff += "==== new file ====\n"
886 newdiff += "--- /dev/null\n"
887 newdiff += "+++ %s\n" % newFile
888 f = open(newFile, "r")
889 for line in f.readlines():
890 newdiff += "+" + line
891 f.close()
893 separatorLine = "######## everything below this line is just the diff #######\n"
895 [handle, fileName] = tempfile.mkstemp()
896 tmpFile = os.fdopen(handle, "w+")
897 if self.isWindows:
898 submitTemplate = submitTemplate.replace("\n", "\r\n")
899 separatorLine = separatorLine.replace("\n", "\r\n")
900 newdiff = newdiff.replace("\n", "\r\n")
901 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
902 tmpFile.close()
903 mtime = os.stat(fileName).st_mtime
904 if os.environ.has_key("P4EDITOR"):
905 editor = os.environ.get("P4EDITOR")
906 else:
907 editor = read_pipe("git var GIT_EDITOR").strip()
908 system(editor + " " + fileName)
910 if gitConfig("git-p4.skipSubmitEditCheck") == "true":
911 checkModTime = False
912 else:
913 checkModTime = True
915 response = "y"
916 if checkModTime and (os.stat(fileName).st_mtime <= mtime):
917 response = "x"
918 while response != "y" and response != "n":
919 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
921 if response == "y":
922 tmpFile = open(fileName, "rb")
923 message = tmpFile.read()
924 tmpFile.close()
925 submitTemplate = message[:message.index(separatorLine)]
926 if self.isWindows:
927 submitTemplate = submitTemplate.replace("\r\n", "\n")
928 p4_write_pipe("submit -i", submitTemplate)
930 if self.preserveUser:
931 if p4User:
932 # Get last changelist number. Cannot easily get it from
933 # the submit command output as the output is unmarshalled.
934 changelist = self.lastP4Changelist()
935 self.modifyChangelistUser(changelist, p4User)
937 else:
938 for f in editedFiles:
939 p4_system("revert \"%s\"" % f);
940 for f in filesToAdd:
941 p4_system("revert \"%s\"" % f);
942 system("rm %s" %f)
944 os.remove(fileName)
945 else:
946 fileName = "submit.txt"
947 file = open(fileName, "w+")
948 file.write(self.prepareLogMessage(template, logMessage))
949 file.close()
950 print ("Perforce submit template written as %s. "
951 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
952 % (fileName, fileName))
954 def run(self, args):
955 if len(args) == 0:
956 self.master = currentGitBranch()
957 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
958 die("Detecting current git branch failed!")
959 elif len(args) == 1:
960 self.master = args[0]
961 else:
962 return False
964 allowSubmit = gitConfig("git-p4.allowSubmit")
965 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
966 die("%s is not in git-p4.allowSubmit" % self.master)
968 [upstream, settings] = findUpstreamBranchPoint()
969 self.depotPath = settings['depot-paths'][0]
970 if len(self.origin) == 0:
971 self.origin = upstream
973 if self.preserveUser:
974 if not self.canChangeChangelists():
975 die("Cannot preserve user names without p4 super-user or admin permissions")
977 if self.verbose:
978 print "Origin branch is " + self.origin
980 if len(self.depotPath) == 0:
981 print "Internal error: cannot locate perforce depot path from existing branches"
982 sys.exit(128)
984 self.clientPath = p4Where(self.depotPath)
986 if len(self.clientPath) == 0:
987 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
988 sys.exit(128)
990 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
991 self.oldWorkingDirectory = os.getcwd()
993 chdir(self.clientPath)
994 print "Synchronizing p4 checkout..."
995 p4_system("sync ...")
997 self.check()
999 commits = []
1000 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1001 commits.append(line.strip())
1002 commits.reverse()
1004 if self.preserveUser:
1005 self.checkValidP4Users(commits)
1007 while len(commits) > 0:
1008 commit = commits[0]
1009 commits = commits[1:]
1010 self.applyCommit(commit)
1011 if not self.interactive:
1012 break
1014 if len(commits) == 0:
1015 print "All changes applied!"
1016 chdir(self.oldWorkingDirectory)
1018 sync = P4Sync()
1019 sync.run([])
1021 rebase = P4Rebase()
1022 rebase.rebase()
1024 return True
1026 class P4Sync(Command, P4UserMap):
1027 delete_actions = ( "delete", "move/delete", "purge" )
1029 def __init__(self):
1030 Command.__init__(self)
1031 P4UserMap.__init__(self)
1032 self.options = [
1033 optparse.make_option("--branch", dest="branch"),
1034 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1035 optparse.make_option("--changesfile", dest="changesFile"),
1036 optparse.make_option("--silent", dest="silent", action="store_true"),
1037 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1038 optparse.make_option("--verbose", dest="verbose", action="store_true"),
1039 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1040 help="Import into refs/heads/ , not refs/remotes"),
1041 optparse.make_option("--max-changes", dest="maxChanges"),
1042 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1043 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1044 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1045 help="Only sync files that are included in the Perforce Client Spec")
1047 self.description = """Imports from Perforce into a git repository.\n
1048 example:
1049 //depot/my/project/ -- to import the current head
1050 //depot/my/project/@all -- to import everything
1051 //depot/my/project/@1,6 -- to import only from revision 1 to 6
1053 (a ... is not needed in the path p4 specification, it's added implicitly)"""
1055 self.usage += " //depot/path[@revRange]"
1056 self.silent = False
1057 self.createdBranches = set()
1058 self.committedChanges = set()
1059 self.branch = ""
1060 self.detectBranches = False
1061 self.detectLabels = False
1062 self.changesFile = ""
1063 self.syncWithOrigin = True
1064 self.verbose = False
1065 self.importIntoRemotes = True
1066 self.maxChanges = ""
1067 self.isWindows = (platform.system() == "Windows")
1068 self.keepRepoPath = False
1069 self.depotPaths = None
1070 self.p4BranchesInGit = []
1071 self.cloneExclude = []
1072 self.useClientSpec = False
1073 self.clientSpecDirs = []
1075 if gitConfig("git-p4.syncFromOrigin") == "false":
1076 self.syncWithOrigin = False
1079 # P4 wildcards are not allowed in filenames. P4 complains
1080 # if you simply add them, but you can force it with "-f", in
1081 # which case it translates them into %xx encoding internally.
1082 # Search for and fix just these four characters. Do % last so
1083 # that fixing it does not inadvertently create new %-escapes.
1085 def wildcard_decode(self, path):
1086 # Cannot have * in a filename in windows; untested as to
1087 # what p4 would do in such a case.
1088 if not self.isWindows:
1089 path = path.replace("%2A", "*")
1090 path = path.replace("%23", "#") \
1091 .replace("%40", "@") \
1092 .replace("%25", "%")
1093 return path
1095 def extractFilesFromCommit(self, commit):
1096 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1097 for path in self.cloneExclude]
1098 files = []
1099 fnum = 0
1100 while commit.has_key("depotFile%s" % fnum):
1101 path = commit["depotFile%s" % fnum]
1103 if [p for p in self.cloneExclude
1104 if p4PathStartsWith(path, p)]:
1105 found = False
1106 else:
1107 found = [p for p in self.depotPaths
1108 if p4PathStartsWith(path, p)]
1109 if not found:
1110 fnum = fnum + 1
1111 continue
1113 file = {}
1114 file["path"] = path
1115 file["rev"] = commit["rev%s" % fnum]
1116 file["action"] = commit["action%s" % fnum]
1117 file["type"] = commit["type%s" % fnum]
1118 files.append(file)
1119 fnum = fnum + 1
1120 return files
1122 def stripRepoPath(self, path, prefixes):
1123 if self.useClientSpec:
1125 # if using the client spec, we use the output directory
1126 # specified in the client. For example, a view
1127 # //depot/foo/branch/... //client/branch/foo/...
1128 # will end up putting all foo/branch files into
1129 # branch/foo/
1130 for val in self.clientSpecDirs:
1131 if path.startswith(val[0]):
1132 # replace the depot path with the client path
1133 path = path.replace(val[0], val[1][1])
1134 # now strip out the client (//client/...)
1135 path = re.sub("^(//[^/]+/)", '', path)
1136 # the rest is all path
1137 return path
1139 if self.keepRepoPath:
1140 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1142 for p in prefixes:
1143 if p4PathStartsWith(path, p):
1144 path = path[len(p):]
1146 return path
1148 def splitFilesIntoBranches(self, commit):
1149 branches = {}
1150 fnum = 0
1151 while commit.has_key("depotFile%s" % fnum):
1152 path = commit["depotFile%s" % fnum]
1153 found = [p for p in self.depotPaths
1154 if p4PathStartsWith(path, p)]
1155 if not found:
1156 fnum = fnum + 1
1157 continue
1159 file = {}
1160 file["path"] = path
1161 file["rev"] = commit["rev%s" % fnum]
1162 file["action"] = commit["action%s" % fnum]
1163 file["type"] = commit["type%s" % fnum]
1164 fnum = fnum + 1
1166 relPath = self.stripRepoPath(path, self.depotPaths)
1168 for branch in self.knownBranches.keys():
1170 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1171 if relPath.startswith(branch + "/"):
1172 if branch not in branches:
1173 branches[branch] = []
1174 branches[branch].append(file)
1175 break
1177 return branches
1179 # output one file from the P4 stream
1180 # - helper for streamP4Files
1182 def streamOneP4File(self, file, contents):
1183 if file["type"] == "apple":
1184 print "\nfile %s is a strange apple file that forks. Ignoring" % \
1185 file['depotFile']
1186 return
1188 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1189 relPath = self.wildcard_decode(relPath)
1190 if verbose:
1191 sys.stderr.write("%s\n" % relPath)
1193 mode = "644"
1194 if isP4Exec(file["type"]):
1195 mode = "755"
1196 elif file["type"] == "symlink":
1197 mode = "120000"
1198 # p4 print on a symlink contains "target\n", so strip it off
1199 data = ''.join(contents)
1200 contents = [data[:-1]]
1202 if self.isWindows and file["type"].endswith("text"):
1203 mangled = []
1204 for data in contents:
1205 data = data.replace("\r\n", "\n")
1206 mangled.append(data)
1207 contents = mangled
1209 if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1210 contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1211 elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1212 contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1214 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1216 # total length...
1217 length = 0
1218 for d in contents:
1219 length = length + len(d)
1221 self.gitStream.write("data %d\n" % length)
1222 for d in contents:
1223 self.gitStream.write(d)
1224 self.gitStream.write("\n")
1226 def streamOneP4Deletion(self, file):
1227 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1228 if verbose:
1229 sys.stderr.write("delete %s\n" % relPath)
1230 self.gitStream.write("D %s\n" % relPath)
1232 # handle another chunk of streaming data
1233 def streamP4FilesCb(self, marshalled):
1235 if marshalled.has_key('depotFile') and self.stream_have_file_info:
1236 # start of a new file - output the old one first
1237 self.streamOneP4File(self.stream_file, self.stream_contents)
1238 self.stream_file = {}
1239 self.stream_contents = []
1240 self.stream_have_file_info = False
1242 # pick up the new file information... for the
1243 # 'data' field we need to append to our array
1244 for k in marshalled.keys():
1245 if k == 'data':
1246 self.stream_contents.append(marshalled['data'])
1247 else:
1248 self.stream_file[k] = marshalled[k]
1250 self.stream_have_file_info = True
1252 # Stream directly from "p4 files" into "git fast-import"
1253 def streamP4Files(self, files):
1254 filesForCommit = []
1255 filesToRead = []
1256 filesToDelete = []
1258 for f in files:
1259 includeFile = True
1260 for val in self.clientSpecDirs:
1261 if f['path'].startswith(val[0]):
1262 if val[1][0] <= 0:
1263 includeFile = False
1264 break
1266 if includeFile:
1267 filesForCommit.append(f)
1268 if f['action'] in self.delete_actions:
1269 filesToDelete.append(f)
1270 else:
1271 filesToRead.append(f)
1273 # deleted files...
1274 for f in filesToDelete:
1275 self.streamOneP4Deletion(f)
1277 if len(filesToRead) > 0:
1278 self.stream_file = {}
1279 self.stream_contents = []
1280 self.stream_have_file_info = False
1282 # curry self argument
1283 def streamP4FilesCbSelf(entry):
1284 self.streamP4FilesCb(entry)
1286 p4CmdList("-x - print",
1287 '\n'.join(['%s#%s' % (f['path'], f['rev'])
1288 for f in filesToRead]),
1289 cb=streamP4FilesCbSelf)
1291 # do the last chunk
1292 if self.stream_file.has_key('depotFile'):
1293 self.streamOneP4File(self.stream_file, self.stream_contents)
1295 def commit(self, details, files, branch, branchPrefixes, parent = ""):
1296 epoch = details["time"]
1297 author = details["user"]
1298 self.branchPrefixes = branchPrefixes
1300 if self.verbose:
1301 print "commit into %s" % branch
1303 # start with reading files; if that fails, we should not
1304 # create a commit.
1305 new_files = []
1306 for f in files:
1307 if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1308 new_files.append (f)
1309 else:
1310 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1312 self.gitStream.write("commit %s\n" % branch)
1313 # gitStream.write("mark :%s\n" % details["change"])
1314 self.committedChanges.add(int(details["change"]))
1315 committer = ""
1316 if author not in self.users:
1317 self.getUserMapFromPerforceServer()
1318 if author in self.users:
1319 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1320 else:
1321 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1323 self.gitStream.write("committer %s\n" % committer)
1325 self.gitStream.write("data <<EOT\n")
1326 self.gitStream.write(details["desc"])
1327 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1328 % (','.join (branchPrefixes), details["change"]))
1329 if len(details['options']) > 0:
1330 self.gitStream.write(": options = %s" % details['options'])
1331 self.gitStream.write("]\nEOT\n\n")
1333 if len(parent) > 0:
1334 if self.verbose:
1335 print "parent %s" % parent
1336 self.gitStream.write("from %s\n" % parent)
1338 self.streamP4Files(new_files)
1339 self.gitStream.write("\n")
1341 change = int(details["change"])
1343 if self.labels.has_key(change):
1344 label = self.labels[change]
1345 labelDetails = label[0]
1346 labelRevisions = label[1]
1347 if self.verbose:
1348 print "Change %s is labelled %s" % (change, labelDetails)
1350 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1351 for p in branchPrefixes]))
1353 if len(files) == len(labelRevisions):
1355 cleanedFiles = {}
1356 for info in files:
1357 if info["action"] in self.delete_actions:
1358 continue
1359 cleanedFiles[info["depotFile"]] = info["rev"]
1361 if cleanedFiles == labelRevisions:
1362 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1363 self.gitStream.write("from %s\n" % branch)
1365 owner = labelDetails["Owner"]
1366 tagger = ""
1367 if author in self.users:
1368 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1369 else:
1370 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1371 self.gitStream.write("tagger %s\n" % tagger)
1372 self.gitStream.write("data <<EOT\n")
1373 self.gitStream.write(labelDetails["Description"])
1374 self.gitStream.write("EOT\n\n")
1376 else:
1377 if not self.silent:
1378 print ("Tag %s does not match with change %s: files do not match."
1379 % (labelDetails["label"], change))
1381 else:
1382 if not self.silent:
1383 print ("Tag %s does not match with change %s: file count is different."
1384 % (labelDetails["label"], change))
1386 def getLabels(self):
1387 self.labels = {}
1389 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1390 if len(l) > 0 and not self.silent:
1391 print "Finding files belonging to labels in %s" % `self.depotPaths`
1393 for output in l:
1394 label = output["label"]
1395 revisions = {}
1396 newestChange = 0
1397 if self.verbose:
1398 print "Querying files for label %s" % label
1399 for file in p4CmdList("files "
1400 + ' '.join (["%s...@%s" % (p, label)
1401 for p in self.depotPaths])):
1402 revisions[file["depotFile"]] = file["rev"]
1403 change = int(file["change"])
1404 if change > newestChange:
1405 newestChange = change
1407 self.labels[newestChange] = [output, revisions]
1409 if self.verbose:
1410 print "Label changes: %s" % self.labels.keys()
1412 def guessProjectName(self):
1413 for p in self.depotPaths:
1414 if p.endswith("/"):
1415 p = p[:-1]
1416 p = p[p.strip().rfind("/") + 1:]
1417 if not p.endswith("/"):
1418 p += "/"
1419 return p
1421 def getBranchMapping(self):
1422 lostAndFoundBranches = set()
1424 for info in p4CmdList("branches"):
1425 details = p4Cmd("branch -o %s" % info["branch"])
1426 viewIdx = 0
1427 while details.has_key("View%s" % viewIdx):
1428 paths = details["View%s" % viewIdx].split(" ")
1429 viewIdx = viewIdx + 1
1430 # require standard //depot/foo/... //depot/bar/... mapping
1431 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1432 continue
1433 source = paths[0]
1434 destination = paths[1]
1435 ## HACK
1436 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
1437 source = source[len(self.depotPaths[0]):-4]
1438 destination = destination[len(self.depotPaths[0]):-4]
1440 if destination in self.knownBranches:
1441 if not self.silent:
1442 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1443 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1444 continue
1446 self.knownBranches[destination] = source
1448 lostAndFoundBranches.discard(destination)
1450 if source not in self.knownBranches:
1451 lostAndFoundBranches.add(source)
1454 for branch in lostAndFoundBranches:
1455 self.knownBranches[branch] = branch
1457 def getBranchMappingFromGitBranches(self):
1458 branches = p4BranchesInGit(self.importIntoRemotes)
1459 for branch in branches.keys():
1460 if branch == "master":
1461 branch = "main"
1462 else:
1463 branch = branch[len(self.projectName):]
1464 self.knownBranches[branch] = branch
1466 def listExistingP4GitBranches(self):
1467 # branches holds mapping from name to commit
1468 branches = p4BranchesInGit(self.importIntoRemotes)
1469 self.p4BranchesInGit = branches.keys()
1470 for branch in branches.keys():
1471 self.initialParents[self.refPrefix + branch] = branches[branch]
1473 def updateOptionDict(self, d):
1474 option_keys = {}
1475 if self.keepRepoPath:
1476 option_keys['keepRepoPath'] = 1
1478 d["options"] = ' '.join(sorted(option_keys.keys()))
1480 def readOptions(self, d):
1481 self.keepRepoPath = (d.has_key('options')
1482 and ('keepRepoPath' in d['options']))
1484 def gitRefForBranch(self, branch):
1485 if branch == "main":
1486 return self.refPrefix + "master"
1488 if len(branch) <= 0:
1489 return branch
1491 return self.refPrefix + self.projectName + branch
1493 def gitCommitByP4Change(self, ref, change):
1494 if self.verbose:
1495 print "looking in ref " + ref + " for change %s using bisect..." % change
1497 earliestCommit = ""
1498 latestCommit = parseRevision(ref)
1500 while True:
1501 if self.verbose:
1502 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1503 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1504 if len(next) == 0:
1505 if self.verbose:
1506 print "argh"
1507 return ""
1508 log = extractLogMessageFromGitCommit(next)
1509 settings = extractSettingsGitLog(log)
1510 currentChange = int(settings['change'])
1511 if self.verbose:
1512 print "current change %s" % currentChange
1514 if currentChange == change:
1515 if self.verbose:
1516 print "found %s" % next
1517 return next
1519 if currentChange < change:
1520 earliestCommit = "^%s" % next
1521 else:
1522 latestCommit = "%s" % next
1524 return ""
1526 def importNewBranch(self, branch, maxChange):
1527 # make fast-import flush all changes to disk and update the refs using the checkpoint
1528 # command so that we can try to find the branch parent in the git history
1529 self.gitStream.write("checkpoint\n\n");
1530 self.gitStream.flush();
1531 branchPrefix = self.depotPaths[0] + branch + "/"
1532 range = "@1,%s" % maxChange
1533 #print "prefix" + branchPrefix
1534 changes = p4ChangesForPaths([branchPrefix], range)
1535 if len(changes) <= 0:
1536 return False
1537 firstChange = changes[0]
1538 #print "first change in branch: %s" % firstChange
1539 sourceBranch = self.knownBranches[branch]
1540 sourceDepotPath = self.depotPaths[0] + sourceBranch
1541 sourceRef = self.gitRefForBranch(sourceBranch)
1542 #print "source " + sourceBranch
1544 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1545 #print "branch parent: %s" % branchParentChange
1546 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1547 if len(gitParent) > 0:
1548 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1549 #print "parent git commit: %s" % gitParent
1551 self.importChanges(changes)
1552 return True
1554 def importChanges(self, changes):
1555 cnt = 1
1556 for change in changes:
1557 description = p4Cmd("describe %s" % change)
1558 self.updateOptionDict(description)
1560 if not self.silent:
1561 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1562 sys.stdout.flush()
1563 cnt = cnt + 1
1565 try:
1566 if self.detectBranches:
1567 branches = self.splitFilesIntoBranches(description)
1568 for branch in branches.keys():
1569 ## HACK --hwn
1570 branchPrefix = self.depotPaths[0] + branch + "/"
1572 parent = ""
1574 filesForCommit = branches[branch]
1576 if self.verbose:
1577 print "branch is %s" % branch
1579 self.updatedBranches.add(branch)
1581 if branch not in self.createdBranches:
1582 self.createdBranches.add(branch)
1583 parent = self.knownBranches[branch]
1584 if parent == branch:
1585 parent = ""
1586 else:
1587 fullBranch = self.projectName + branch
1588 if fullBranch not in self.p4BranchesInGit:
1589 if not self.silent:
1590 print("\n Importing new branch %s" % fullBranch);
1591 if self.importNewBranch(branch, change - 1):
1592 parent = ""
1593 self.p4BranchesInGit.append(fullBranch)
1594 if not self.silent:
1595 print("\n Resuming with change %s" % change);
1597 if self.verbose:
1598 print "parent determined through known branches: %s" % parent
1600 branch = self.gitRefForBranch(branch)
1601 parent = self.gitRefForBranch(parent)
1603 if self.verbose:
1604 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1606 if len(parent) == 0 and branch in self.initialParents:
1607 parent = self.initialParents[branch]
1608 del self.initialParents[branch]
1610 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1611 else:
1612 files = self.extractFilesFromCommit(description)
1613 self.commit(description, files, self.branch, self.depotPaths,
1614 self.initialParent)
1615 self.initialParent = ""
1616 except IOError:
1617 print self.gitError.read()
1618 sys.exit(1)
1620 def importHeadRevision(self, revision):
1621 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1623 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1624 details["desc"] = ("Initial import of %s from the state at revision %s\n"
1625 % (' '.join(self.depotPaths), revision))
1626 details["change"] = revision
1627 newestRevision = 0
1629 fileCnt = 0
1630 for info in p4CmdList("files "
1631 + ' '.join(["%s...%s"
1632 % (p, revision)
1633 for p in self.depotPaths])):
1635 if 'code' in info and info['code'] == 'error':
1636 sys.stderr.write("p4 returned an error: %s\n"
1637 % info['data'])
1638 if info['data'].find("must refer to client") >= 0:
1639 sys.stderr.write("This particular p4 error is misleading.\n")
1640 sys.stderr.write("Perhaps the depot path was misspelled.\n");
1641 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
1642 sys.exit(1)
1643 if 'p4ExitCode' in info:
1644 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
1645 sys.exit(1)
1648 change = int(info["change"])
1649 if change > newestRevision:
1650 newestRevision = change
1652 if info["action"] in self.delete_actions:
1653 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1654 #fileCnt = fileCnt + 1
1655 continue
1657 for prop in ["depotFile", "rev", "action", "type" ]:
1658 details["%s%s" % (prop, fileCnt)] = info[prop]
1660 fileCnt = fileCnt + 1
1662 details["change"] = newestRevision
1663 self.updateOptionDict(details)
1664 try:
1665 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1666 except IOError:
1667 print "IO error with git fast-import. Is your git version recent enough?"
1668 print self.gitError.read()
1671 def getClientSpec(self):
1672 specList = p4CmdList( "client -o" )
1673 temp = {}
1674 for entry in specList:
1675 for k,v in entry.iteritems():
1676 if k.startswith("View"):
1678 # p4 has these %%1 to %%9 arguments in specs to
1679 # reorder paths; which we can't handle (yet :)
1680 if re.match('%%\d', v) != None:
1681 print "Sorry, can't handle %%n arguments in client specs"
1682 sys.exit(1)
1684 if v.startswith('"'):
1685 start = 1
1686 else:
1687 start = 0
1688 index = v.find("...")
1690 # save the "client view"; i.e the RHS of the view
1691 # line that tells the client where to put the
1692 # files for this view.
1693 cv = v[index+3:].strip() # +3 to remove previous '...'
1695 # if the client view doesn't end with a
1696 # ... wildcard, then we're going to mess up the
1697 # output directory, so fail gracefully.
1698 if not cv.endswith('...'):
1699 print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1700 sys.exit(1)
1701 cv=cv[:-3]
1703 # now save the view; +index means included, -index
1704 # means it should be filtered out.
1705 v = v[start:index]
1706 if v.startswith("-"):
1707 v = v[1:]
1708 include = -len(v)
1709 else:
1710 include = len(v)
1712 temp[v] = (include, cv)
1714 self.clientSpecDirs = temp.items()
1715 self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1717 def run(self, args):
1718 self.depotPaths = []
1719 self.changeRange = ""
1720 self.initialParent = ""
1721 self.previousDepotPaths = []
1723 # map from branch depot path to parent branch
1724 self.knownBranches = {}
1725 self.initialParents = {}
1726 self.hasOrigin = originP4BranchesExist()
1727 if not self.syncWithOrigin:
1728 self.hasOrigin = False
1730 if self.importIntoRemotes:
1731 self.refPrefix = "refs/remotes/p4/"
1732 else:
1733 self.refPrefix = "refs/heads/p4/"
1735 if self.syncWithOrigin and self.hasOrigin:
1736 if not self.silent:
1737 print "Syncing with origin first by calling git fetch origin"
1738 system("git fetch origin")
1740 if len(self.branch) == 0:
1741 self.branch = self.refPrefix + "master"
1742 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1743 system("git update-ref %s refs/heads/p4" % self.branch)
1744 system("git branch -D p4");
1745 # create it /after/ importing, when master exists
1746 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1747 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1749 if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1750 self.getClientSpec()
1752 # TODO: should always look at previous commits,
1753 # merge with previous imports, if possible.
1754 if args == []:
1755 if self.hasOrigin:
1756 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1757 self.listExistingP4GitBranches()
1759 if len(self.p4BranchesInGit) > 1:
1760 if not self.silent:
1761 print "Importing from/into multiple branches"
1762 self.detectBranches = True
1764 if self.verbose:
1765 print "branches: %s" % self.p4BranchesInGit
1767 p4Change = 0
1768 for branch in self.p4BranchesInGit:
1769 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1771 settings = extractSettingsGitLog(logMsg)
1773 self.readOptions(settings)
1774 if (settings.has_key('depot-paths')
1775 and settings.has_key ('change')):
1776 change = int(settings['change']) + 1
1777 p4Change = max(p4Change, change)
1779 depotPaths = sorted(settings['depot-paths'])
1780 if self.previousDepotPaths == []:
1781 self.previousDepotPaths = depotPaths
1782 else:
1783 paths = []
1784 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1785 for i in range(0, min(len(cur), len(prev))):
1786 if cur[i] <> prev[i]:
1787 i = i - 1
1788 break
1790 paths.append (cur[:i + 1])
1792 self.previousDepotPaths = paths
1794 if p4Change > 0:
1795 self.depotPaths = sorted(self.previousDepotPaths)
1796 self.changeRange = "@%s,#head" % p4Change
1797 if not self.detectBranches:
1798 self.initialParent = parseRevision(self.branch)
1799 if not self.silent and not self.detectBranches:
1800 print "Performing incremental import into %s git branch" % self.branch
1802 if not self.branch.startswith("refs/"):
1803 self.branch = "refs/heads/" + self.branch
1805 if len(args) == 0 and self.depotPaths:
1806 if not self.silent:
1807 print "Depot paths: %s" % ' '.join(self.depotPaths)
1808 else:
1809 if self.depotPaths and self.depotPaths != args:
1810 print ("previous import used depot path %s and now %s was specified. "
1811 "This doesn't work!" % (' '.join (self.depotPaths),
1812 ' '.join (args)))
1813 sys.exit(1)
1815 self.depotPaths = sorted(args)
1817 revision = ""
1818 self.users = {}
1820 newPaths = []
1821 for p in self.depotPaths:
1822 if p.find("@") != -1:
1823 atIdx = p.index("@")
1824 self.changeRange = p[atIdx:]
1825 if self.changeRange == "@all":
1826 self.changeRange = ""
1827 elif ',' not in self.changeRange:
1828 revision = self.changeRange
1829 self.changeRange = ""
1830 p = p[:atIdx]
1831 elif p.find("#") != -1:
1832 hashIdx = p.index("#")
1833 revision = p[hashIdx:]
1834 p = p[:hashIdx]
1835 elif self.previousDepotPaths == []:
1836 revision = "#head"
1838 p = re.sub ("\.\.\.$", "", p)
1839 if not p.endswith("/"):
1840 p += "/"
1842 newPaths.append(p)
1844 self.depotPaths = newPaths
1847 self.loadUserMapFromCache()
1848 self.labels = {}
1849 if self.detectLabels:
1850 self.getLabels();
1852 if self.detectBranches:
1853 ## FIXME - what's a P4 projectName ?
1854 self.projectName = self.guessProjectName()
1856 if self.hasOrigin:
1857 self.getBranchMappingFromGitBranches()
1858 else:
1859 self.getBranchMapping()
1860 if self.verbose:
1861 print "p4-git branches: %s" % self.p4BranchesInGit
1862 print "initial parents: %s" % self.initialParents
1863 for b in self.p4BranchesInGit:
1864 if b != "master":
1866 ## FIXME
1867 b = b[len(self.projectName):]
1868 self.createdBranches.add(b)
1870 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1872 importProcess = subprocess.Popen(["git", "fast-import"],
1873 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1874 stderr=subprocess.PIPE);
1875 self.gitOutput = importProcess.stdout
1876 self.gitStream = importProcess.stdin
1877 self.gitError = importProcess.stderr
1879 if revision:
1880 self.importHeadRevision(revision)
1881 else:
1882 changes = []
1884 if len(self.changesFile) > 0:
1885 output = open(self.changesFile).readlines()
1886 changeSet = set()
1887 for line in output:
1888 changeSet.add(int(line))
1890 for change in changeSet:
1891 changes.append(change)
1893 changes.sort()
1894 else:
1895 # catch "git-p4 sync" with no new branches, in a repo that
1896 # does not have any existing git-p4 branches
1897 if len(args) == 0 and not self.p4BranchesInGit:
1898 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
1899 if self.verbose:
1900 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1901 self.changeRange)
1902 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1904 if len(self.maxChanges) > 0:
1905 changes = changes[:min(int(self.maxChanges), len(changes))]
1907 if len(changes) == 0:
1908 if not self.silent:
1909 print "No changes to import!"
1910 return True
1912 if not self.silent and not self.detectBranches:
1913 print "Import destination: %s" % self.branch
1915 self.updatedBranches = set()
1917 self.importChanges(changes)
1919 if not self.silent:
1920 print ""
1921 if len(self.updatedBranches) > 0:
1922 sys.stdout.write("Updated branches: ")
1923 for b in self.updatedBranches:
1924 sys.stdout.write("%s " % b)
1925 sys.stdout.write("\n")
1927 self.gitStream.close()
1928 if importProcess.wait() != 0:
1929 die("fast-import failed: %s" % self.gitError.read())
1930 self.gitOutput.close()
1931 self.gitError.close()
1933 return True
1935 class P4Rebase(Command):
1936 def __init__(self):
1937 Command.__init__(self)
1938 self.options = [ ]
1939 self.description = ("Fetches the latest revision from perforce and "
1940 + "rebases the current work (branch) against it")
1941 self.verbose = False
1943 def run(self, args):
1944 sync = P4Sync()
1945 sync.run([])
1947 return self.rebase()
1949 def rebase(self):
1950 if os.system("git update-index --refresh") != 0:
1951 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.");
1952 if len(read_pipe("git diff-index HEAD --")) > 0:
1953 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1955 [upstream, settings] = findUpstreamBranchPoint()
1956 if len(upstream) == 0:
1957 die("Cannot find upstream branchpoint for rebase")
1959 # the branchpoint may be p4/foo~3, so strip off the parent
1960 upstream = re.sub("~[0-9]+$", "", upstream)
1962 print "Rebasing the current branch onto %s" % upstream
1963 oldHead = read_pipe("git rev-parse HEAD").strip()
1964 system("git rebase %s" % upstream)
1965 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1966 return True
1968 class P4Clone(P4Sync):
1969 def __init__(self):
1970 P4Sync.__init__(self)
1971 self.description = "Creates a new git repository and imports from Perforce into it"
1972 self.usage = "usage: %prog [options] //depot/path[@revRange]"
1973 self.options += [
1974 optparse.make_option("--destination", dest="cloneDestination",
1975 action='store', default=None,
1976 help="where to leave result of the clone"),
1977 optparse.make_option("-/", dest="cloneExclude",
1978 action="append", type="string",
1979 help="exclude depot path"),
1980 optparse.make_option("--bare", dest="cloneBare",
1981 action="store_true", default=False),
1983 self.cloneDestination = None
1984 self.needsGit = False
1985 self.cloneBare = False
1987 # This is required for the "append" cloneExclude action
1988 def ensure_value(self, attr, value):
1989 if not hasattr(self, attr) or getattr(self, attr) is None:
1990 setattr(self, attr, value)
1991 return getattr(self, attr)
1993 def defaultDestination(self, args):
1994 ## TODO: use common prefix of args?
1995 depotPath = args[0]
1996 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1997 depotDir = re.sub("(#[^#]*)$", "", depotDir)
1998 depotDir = re.sub(r"\.\.\.$", "", depotDir)
1999 depotDir = re.sub(r"/$", "", depotDir)
2000 return os.path.split(depotDir)[1]
2002 def run(self, args):
2003 if len(args) < 1:
2004 return False
2006 if self.keepRepoPath and not self.cloneDestination:
2007 sys.stderr.write("Must specify destination for --keep-path\n")
2008 sys.exit(1)
2010 depotPaths = args
2012 if not self.cloneDestination and len(depotPaths) > 1:
2013 self.cloneDestination = depotPaths[-1]
2014 depotPaths = depotPaths[:-1]
2016 self.cloneExclude = ["/"+p for p in self.cloneExclude]
2017 for p in depotPaths:
2018 if not p.startswith("//"):
2019 return False
2021 if not self.cloneDestination:
2022 self.cloneDestination = self.defaultDestination(args)
2024 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2026 if not os.path.exists(self.cloneDestination):
2027 os.makedirs(self.cloneDestination)
2028 chdir(self.cloneDestination)
2030 init_cmd = [ "git", "init" ]
2031 if self.cloneBare:
2032 init_cmd.append("--bare")
2033 subprocess.check_call(init_cmd)
2035 if not P4Sync.run(self, depotPaths):
2036 return False
2037 if self.branch != "master":
2038 if self.importIntoRemotes:
2039 masterbranch = "refs/remotes/p4/master"
2040 else:
2041 masterbranch = "refs/heads/p4/master"
2042 if gitBranchExists(masterbranch):
2043 system("git branch master %s" % masterbranch)
2044 if not self.cloneBare:
2045 system("git checkout -f")
2046 else:
2047 print "Could not detect main branch. No checkout/master branch created."
2049 return True
2051 class P4Branches(Command):
2052 def __init__(self):
2053 Command.__init__(self)
2054 self.options = [ ]
2055 self.description = ("Shows the git branches that hold imports and their "
2056 + "corresponding perforce depot paths")
2057 self.verbose = False
2059 def run(self, args):
2060 if originP4BranchesExist():
2061 createOrUpdateBranchesFromOrigin()
2063 cmdline = "git rev-parse --symbolic "
2064 cmdline += " --remotes"
2066 for line in read_pipe_lines(cmdline):
2067 line = line.strip()
2069 if not line.startswith('p4/') or line == "p4/HEAD":
2070 continue
2071 branch = line
2073 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2074 settings = extractSettingsGitLog(log)
2076 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2077 return True
2079 class HelpFormatter(optparse.IndentedHelpFormatter):
2080 def __init__(self):
2081 optparse.IndentedHelpFormatter.__init__(self)
2083 def format_description(self, description):
2084 if description:
2085 return description + "\n"
2086 else:
2087 return ""
2089 def printUsage(commands):
2090 print "usage: %s <command> [options]" % sys.argv[0]
2091 print ""
2092 print "valid commands: %s" % ", ".join(commands)
2093 print ""
2094 print "Try %s <command> --help for command specific help." % sys.argv[0]
2095 print ""
2097 commands = {
2098 "debug" : P4Debug,
2099 "submit" : P4Submit,
2100 "commit" : P4Submit,
2101 "sync" : P4Sync,
2102 "rebase" : P4Rebase,
2103 "clone" : P4Clone,
2104 "rollback" : P4RollBack,
2105 "branches" : P4Branches
2109 def main():
2110 if len(sys.argv[1:]) == 0:
2111 printUsage(commands.keys())
2112 sys.exit(2)
2114 cmd = ""
2115 cmdName = sys.argv[1]
2116 try:
2117 klass = commands[cmdName]
2118 cmd = klass()
2119 except KeyError:
2120 print "unknown command %s" % cmdName
2121 print ""
2122 printUsage(commands.keys())
2123 sys.exit(2)
2125 options = cmd.options
2126 cmd.gitdir = os.environ.get("GIT_DIR", None)
2128 args = sys.argv[2:]
2130 if len(options) > 0:
2131 options.append(optparse.make_option("--git-dir", dest="gitdir"))
2133 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2134 options,
2135 description = cmd.description,
2136 formatter = HelpFormatter())
2138 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2139 global verbose
2140 verbose = cmd.verbose
2141 if cmd.needsGit:
2142 if cmd.gitdir == None:
2143 cmd.gitdir = os.path.abspath(".git")
2144 if not isValidGitDir(cmd.gitdir):
2145 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2146 if os.path.exists(cmd.gitdir):
2147 cdup = read_pipe("git rev-parse --show-cdup").strip()
2148 if len(cdup) > 0:
2149 chdir(cdup);
2151 if not isValidGitDir(cmd.gitdir):
2152 if isValidGitDir(cmd.gitdir + "/.git"):
2153 cmd.gitdir += "/.git"
2154 else:
2155 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2157 os.environ["GIT_DIR"] = cmd.gitdir
2159 if not cmd.run(args):
2160 parser.print_help()
2163 if __name__ == '__main__':
2164 main()