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>
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
11 import optparse
, sys
, os
, marshal
, popen2
, subprocess
, shelve
12 import tempfile
, getopt
, sha
, os
.path
, time
, platform
20 def p4_build_cmd(cmd
):
21 """Build a suitable p4 command line.
23 This consolidates building and returning a p4 command line into one
24 location. It means that hooking into the environment, or other configuration
25 can be done more easily.
27 real_cmd
= "%s " % "p4"
29 user
= gitConfig("git-p4.user")
31 real_cmd
+= "-u %s " % user
33 password
= gitConfig("git-p4.password")
35 real_cmd
+= "-P %s " % password
37 port
= gitConfig("git-p4.port")
39 real_cmd
+= "-p %s " % port
41 host
= gitConfig("git-p4.host")
43 real_cmd
+= "-h %s " % host
45 client
= gitConfig("git-p4.client")
47 real_cmd
+= "-c %s " % client
49 real_cmd
+= "%s" % (cmd
)
63 sys
.stderr
.write(msg
+ "\n")
66 def write_pipe(c
, str):
68 sys
.stderr
.write('Writing pipe: %s\n' % c
)
70 pipe
= os
.popen(c
, 'w')
73 die('Command failed: %s' % c
)
77 def p4_write_pipe(c
, str):
78 real_cmd
= p4_build_cmd(c
)
79 return write_pipe(real_cmd
, str)
81 def read_pipe(c
, ignore_error
=False):
83 sys
.stderr
.write('Reading pipe: %s\n' % c
)
85 pipe
= os
.popen(c
, 'rb')
87 if pipe
.close() and not ignore_error
:
88 die('Command failed: %s' % c
)
92 def p4_read_pipe(c
, ignore_error
=False):
93 real_cmd
= p4_build_cmd(c
)
94 return read_pipe(real_cmd
, ignore_error
)
96 def read_pipe_lines(c
):
98 sys
.stderr
.write('Reading pipe: %s\n' % c
)
99 ## todo: check return status
100 pipe
= os
.popen(c
, 'rb')
101 val
= pipe
.readlines()
103 die('Command failed: %s' % c
)
107 def p4_read_pipe_lines(c
):
108 """Specifically invoke p4 on the command supplied. """
109 real_cmd
= p4_build_cmd(c
)
110 return read_pipe_lines(real_cmd
)
114 sys
.stderr
.write("executing %s\n" % cmd
)
115 if os
.system(cmd
) != 0:
116 die("command failed: %s" % cmd
)
119 """Specifically invoke p4 as the system command. """
120 real_cmd
= p4_build_cmd(cmd
)
121 return system(real_cmd
)
124 """Determine if a Perforce 'kind' should have execute permission
126 'p4 help filetypes' gives a list of the types. If it starts with 'x',
127 or x follows one of a few letters. Otherwise, if there is an 'x' after
128 a plus sign, it is also executable"""
129 return (re
.search(r
"(^[cku]?x)|\+.*x", kind
) != None)
131 def setP4ExecBit(file, mode
):
132 # Reopens an already open file and changes the execute bit to match
133 # the execute bit setting in the passed in mode.
137 if not isModeExec(mode
):
138 p4Type
= getP4OpenedType(file)
139 p4Type
= re
.sub('^([cku]?)x(.*)', '\\1\\2', p4Type
)
140 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
141 if p4Type
[-1] == "+":
142 p4Type
= p4Type
[0:-1]
144 p4_system("reopen -t %s %s" % (p4Type
, file))
146 def getP4OpenedType(file):
147 # Returns the perforce file type for the given file.
149 result
= p4_read_pipe("opened %s" % file)
150 match
= re
.match(".*\((.+)\)\r?$", result
)
152 return match
.group(1)
154 die("Could not determine file type for %s (result: '%s')" % (file, result
))
156 def diffTreePattern():
157 # This is a simple generator for the diff tree regex pattern. This could be
158 # a class variable if this and parseDiffTreeEntry were a part of a class.
159 pattern
= re
.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
163 def parseDiffTreeEntry(entry
):
164 """Parses a single diff tree entry into its component elements.
166 See git-diff-tree(1) manpage for details about the format of the diff
167 output. This method returns a dictionary with the following elements:
169 src_mode - The mode of the source file
170 dst_mode - The mode of the destination file
171 src_sha1 - The sha1 for the source file
172 dst_sha1 - The sha1 fr the destination file
173 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
174 status_score - The score for the status (applicable for 'C' and 'R'
175 statuses). This is None if there is no score.
176 src - The path for the source file.
177 dst - The path for the destination file. This is only present for
178 copy or renames. If it is not present, this is None.
180 If the pattern is not matched, None is returned."""
182 match
= diffTreePattern().next().match(entry
)
185 'src_mode': match
.group(1),
186 'dst_mode': match
.group(2),
187 'src_sha1': match
.group(3),
188 'dst_sha1': match
.group(4),
189 'status': match
.group(5),
190 'status_score': match
.group(6),
191 'src': match
.group(7),
192 'dst': match
.group(10)
196 def isModeExec(mode
):
197 # Returns True if the given git mode represents an executable file,
199 return mode
[-3:] == "755"
201 def isModeExecChanged(src_mode
, dst_mode
):
202 return isModeExec(src_mode
) != isModeExec(dst_mode
)
204 def p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b'):
205 cmd
= p4_build_cmd("-G %s" % (cmd
))
207 sys
.stderr
.write("Opening pipe: %s\n" % cmd
)
209 # Use a temporary file to avoid deadlocks without
210 # subprocess.communicate(), which would put another copy
211 # of stdout into memory.
213 if stdin
is not None:
214 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
215 stdin_file
.write(stdin
)
219 p4
= subprocess
.Popen(cmd
, shell
=True,
221 stdout
=subprocess
.PIPE
)
226 entry
= marshal
.load(p4
.stdout
)
233 entry
["p4ExitCode"] = exitCode
239 list = p4CmdList(cmd
)
245 def p4Where(depotPath
):
246 if not depotPath
.endswith("/"):
248 depotPath
= depotPath
+ "..."
249 outputList
= p4CmdList("where %s" % depotPath
)
251 for entry
in outputList
:
252 if "depotFile" in entry
:
253 if entry
["depotFile"] == depotPath
:
256 elif "data" in entry
:
257 data
= entry
.get("data")
258 space
= data
.find(" ")
259 if data
[:space
] == depotPath
:
264 if output
["code"] == "error":
268 clientPath
= output
.get("path")
269 elif "data" in output
:
270 data
= output
.get("data")
271 lastSpace
= data
.rfind(" ")
272 clientPath
= data
[lastSpace
+ 1:]
274 if clientPath
.endswith("..."):
275 clientPath
= clientPath
[:-3]
278 def currentGitBranch():
279 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
281 def isValidGitDir(path
):
282 if (os
.path
.exists(path
+ "/HEAD")
283 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
287 def parseRevision(ref
):
288 return read_pipe("git rev-parse %s" % ref
).strip()
290 def extractLogMessageFromGitCommit(commit
):
293 ## fixme: title is first line of commit, not 1st paragraph.
295 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
304 def extractSettingsGitLog(log
):
306 for line
in log
.split("\n"):
308 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
312 assignments
= m
.group(1).split (':')
313 for a
in assignments
:
315 key
= vals
[0].strip()
316 val
= ('='.join (vals
[1:])).strip()
317 if val
.endswith ('\"') and val
.startswith('"'):
322 paths
= values
.get("depot-paths")
324 paths
= values
.get("depot-path")
326 values
['depot-paths'] = paths
.split(',')
329 def gitBranchExists(branch
):
330 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
331 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
332 return proc
.wait() == 0;
336 if not _gitConfig
.has_key(key
):
337 _gitConfig
[key
] = read_pipe("git config %s" % key
, ignore_error
=True).strip()
338 return _gitConfig
[key
]
340 def p4BranchesInGit(branchesAreInRemotes
= True):
343 cmdline
= "git rev-parse --symbolic "
344 if branchesAreInRemotes
:
345 cmdline
+= " --remotes"
347 cmdline
+= " --branches"
349 for line
in read_pipe_lines(cmdline
):
352 ## only import to p4/
353 if not line
.startswith('p4/') or line
== "p4/HEAD":
358 branch
= re
.sub ("^p4/", "", line
)
360 branches
[branch
] = parseRevision(line
)
363 def findUpstreamBranchPoint(head
= "HEAD"):
364 branches
= p4BranchesInGit()
365 # map from depot-path to branch name
366 branchByDepotPath
= {}
367 for branch
in branches
.keys():
368 tip
= branches
[branch
]
369 log
= extractLogMessageFromGitCommit(tip
)
370 settings
= extractSettingsGitLog(log
)
371 if settings
.has_key("depot-paths"):
372 paths
= ",".join(settings
["depot-paths"])
373 branchByDepotPath
[paths
] = "remotes/p4/" + branch
377 while parent
< 65535:
378 commit
= head
+ "~%s" % parent
379 log
= extractLogMessageFromGitCommit(commit
)
380 settings
= extractSettingsGitLog(log
)
381 if settings
.has_key("depot-paths"):
382 paths
= ",".join(settings
["depot-paths"])
383 if branchByDepotPath
.has_key(paths
):
384 return [branchByDepotPath
[paths
], settings
]
388 return ["", settings
]
390 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
392 print ("Creating/updating branch(es) in %s based on origin branch(es)"
395 originPrefix
= "origin/p4/"
397 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
399 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
402 headName
= line
[len(originPrefix
):]
403 remoteHead
= localRefPrefix
+ headName
406 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
407 if (not original
.has_key('depot-paths')
408 or not original
.has_key('change')):
412 if not gitBranchExists(remoteHead
):
414 print "creating %s" % remoteHead
417 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
418 if settings
.has_key('change') > 0:
419 if settings
['depot-paths'] == original
['depot-paths']:
420 originP4Change
= int(original
['change'])
421 p4Change
= int(settings
['change'])
422 if originP4Change
> p4Change
:
423 print ("%s (%s) is newer than %s (%s). "
424 "Updating p4 branch from origin."
425 % (originHead
, originP4Change
,
426 remoteHead
, p4Change
))
429 print ("Ignoring: %s was imported from %s while "
430 "%s was imported from %s"
431 % (originHead
, ','.join(original
['depot-paths']),
432 remoteHead
, ','.join(settings
['depot-paths'])))
435 system("git update-ref %s %s" % (remoteHead
, originHead
))
437 def originP4BranchesExist():
438 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
440 def p4ChangesForPaths(depotPaths
, changeRange
):
442 output
= p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p
, changeRange
)
443 for p
in depotPaths
]))
447 changeNum
= int(line
.split(" ")[1])
448 changes
[changeNum
] = True
450 changelist
= changes
.keys()
456 self
.usage
= "usage: %prog [options]"
459 class P4Debug(Command
):
461 Command
.__init
__(self
)
463 optparse
.make_option("--verbose", dest
="verbose", action
="store_true",
466 self
.description
= "A tool to debug the output of p4 -G."
467 self
.needsGit
= False
472 for output
in p4CmdList(" ".join(args
)):
473 print 'Element: %d' % j
478 class P4RollBack(Command
):
480 Command
.__init
__(self
)
482 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
483 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
485 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
487 self
.rollbackLocalBranches
= False
492 maxChange
= int(args
[0])
494 if "p4ExitCode" in p4Cmd("changes -m 1"):
495 die("Problems executing p4");
497 if self
.rollbackLocalBranches
:
498 refPrefix
= "refs/heads/"
499 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
501 refPrefix
= "refs/remotes/"
502 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
505 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
507 ref
= refPrefix
+ line
508 log
= extractLogMessageFromGitCommit(ref
)
509 settings
= extractSettingsGitLog(log
)
511 depotPaths
= settings
['depot-paths']
512 change
= settings
['change']
516 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
517 for p
in depotPaths
]))) == 0:
518 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
519 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
522 while change
and int(change
) > maxChange
:
525 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
526 system("git update-ref %s \"%s^\"" % (ref
, ref
))
527 log
= extractLogMessageFromGitCommit(ref
)
528 settings
= extractSettingsGitLog(log
)
531 depotPaths
= settings
['depot-paths']
532 change
= settings
['change']
535 print "%s rewound to %s" % (ref
, change
)
539 class P4Submit(Command
):
541 Command
.__init
__(self
)
543 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
544 optparse
.make_option("--origin", dest
="origin"),
545 optparse
.make_option("-M", dest
="detectRename", action
="store_true"),
547 self
.description
= "Submit changes from git to the perforce depot."
548 self
.usage
+= " [name of git branch to submit into perforce depot]"
549 self
.interactive
= True
551 self
.detectRename
= False
553 self
.isWindows
= (platform
.system() == "Windows")
556 if len(p4CmdList("opened ...")) > 0:
557 die("You have files opened with perforce! Close them before starting the sync.")
559 # replaces everything between 'Description:' and the next P4 submit template field with the
561 def prepareLogMessage(self
, template
, message
):
564 inDescriptionSection
= False
566 for line
in template
.split("\n"):
567 if line
.startswith("#"):
568 result
+= line
+ "\n"
571 if inDescriptionSection
:
572 if line
.startswith("Files:"):
573 inDescriptionSection
= False
577 if line
.startswith("Description:"):
578 inDescriptionSection
= True
580 for messageLine
in message
.split("\n"):
581 line
+= "\t" + messageLine
+ "\n"
583 result
+= line
+ "\n"
587 def prepareSubmitTemplate(self
):
588 # remove lines in the Files section that show changes to files outside the depot path we're committing into
590 inFilesSection
= False
591 for line
in p4_read_pipe_lines("change -o"):
592 if line
.endswith("\r\n"):
593 line
= line
[:-2] + "\n"
595 if line
.startswith("\t"):
596 # path starts and ends with a tab
598 lastTab
= path
.rfind("\t")
600 path
= path
[:lastTab
]
601 if not path
.startswith(self
.depotPath
):
604 inFilesSection
= False
606 if line
.startswith("Files:"):
607 inFilesSection
= True
613 def applyCommit(self
, id):
614 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
615 diffOpts
= ("", "-M")[self
.detectRename
]
616 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts
, id, id))
618 filesToDelete
= set()
620 filesToChangeExecBit
= {}
622 diff
= parseDiffTreeEntry(line
)
623 modifier
= diff
['status']
626 p4_system("edit \"%s\"" % path
)
627 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
628 filesToChangeExecBit
[path
] = diff
['dst_mode']
629 editedFiles
.add(path
)
630 elif modifier
== "A":
632 filesToChangeExecBit
[path
] = diff
['dst_mode']
633 if path
in filesToDelete
:
634 filesToDelete
.remove(path
)
635 elif modifier
== "D":
636 filesToDelete
.add(path
)
637 if path
in filesToAdd
:
638 filesToAdd
.remove(path
)
639 elif modifier
== "R":
640 src
, dest
= diff
['src'], diff
['dst']
641 p4_system("integrate -Dt \"%s\" \"%s\"" % (src
, dest
))
642 p4_system("edit \"%s\"" % (dest
))
643 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
644 filesToChangeExecBit
[dest
] = diff
['dst_mode']
646 editedFiles
.add(dest
)
647 filesToDelete
.add(src
)
649 die("unknown modifier %s for %s" % (modifier
, path
))
651 diffcmd
= "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
652 patchcmd
= diffcmd
+ " | git apply "
653 tryPatchCmd
= patchcmd
+ "--check -"
654 applyPatchCmd
= patchcmd
+ "--check --apply -"
656 if os
.system(tryPatchCmd
) != 0:
657 print "Unfortunately applying the change failed!"
658 print "What do you want to do?"
660 while response
!= "s" and response
!= "a" and response
!= "w":
661 response
= raw_input("[s]kip this patch / [a]pply the patch forcibly "
662 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
664 print "Skipping! Good luck with the next patches..."
665 for f
in editedFiles
:
666 p4_system("revert \"%s\"" % f
);
670 elif response
== "a":
671 os
.system(applyPatchCmd
)
672 if len(filesToAdd
) > 0:
673 print "You may also want to call p4 add on the following files:"
674 print " ".join(filesToAdd
)
675 if len(filesToDelete
):
676 print "The following files should be scheduled for deletion with p4 delete:"
677 print " ".join(filesToDelete
)
678 die("Please resolve and submit the conflict manually and "
679 + "continue afterwards with git-p4 submit --continue")
680 elif response
== "w":
681 system(diffcmd
+ " > patch.txt")
682 print "Patch saved to patch.txt in %s !" % self
.clientPath
683 die("Please resolve and submit the conflict manually and "
684 "continue afterwards with git-p4 submit --continue")
686 system(applyPatchCmd
)
689 p4_system("add \"%s\"" % f
)
690 for f
in filesToDelete
:
691 p4_system("revert \"%s\"" % f
)
692 p4_system("delete \"%s\"" % f
)
694 # Set/clear executable bits
695 for f
in filesToChangeExecBit
.keys():
696 mode
= filesToChangeExecBit
[f
]
697 setP4ExecBit(f
, mode
)
699 logMessage
= extractLogMessageFromGitCommit(id)
700 logMessage
= logMessage
.strip()
702 template
= self
.prepareSubmitTemplate()
705 submitTemplate
= self
.prepareLogMessage(template
, logMessage
)
706 if os
.environ
.has_key("P4DIFF"):
707 del(os
.environ
["P4DIFF"])
708 diff
= p4_read_pipe("diff -du ...")
711 for newFile
in filesToAdd
:
712 newdiff
+= "==== new file ====\n"
713 newdiff
+= "--- /dev/null\n"
714 newdiff
+= "+++ %s\n" % newFile
715 f
= open(newFile
, "r")
716 for line
in f
.readlines():
717 newdiff
+= "+" + line
720 separatorLine
= "######## everything below this line is just the diff #######\n"
722 [handle
, fileName
] = tempfile
.mkstemp()
723 tmpFile
= os
.fdopen(handle
, "w+")
725 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
726 separatorLine
= separatorLine
.replace("\n", "\r\n")
727 newdiff
= newdiff
.replace("\n", "\r\n")
728 tmpFile
.write(submitTemplate
+ separatorLine
+ diff
+ newdiff
)
730 mtime
= os
.stat(fileName
).st_mtime
732 if platform
.system() == "Windows":
733 defaultEditor
= "notepad"
734 if os
.environ
.has_key("P4EDITOR"):
735 editor
= os
.environ
.get("P4EDITOR")
737 editor
= os
.environ
.get("EDITOR", defaultEditor
);
738 system(editor
+ " " + fileName
)
741 if os
.stat(fileName
).st_mtime
<= mtime
:
743 while response
!= "y" and response
!= "n":
744 response
= raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
747 tmpFile
= open(fileName
, "rb")
748 message
= tmpFile
.read()
750 submitTemplate
= message
[:message
.index(separatorLine
)]
752 submitTemplate
= submitTemplate
.replace("\r\n", "\n")
753 p4_write_pipe("submit -i", submitTemplate
)
755 for f
in editedFiles
:
756 p4_system("revert \"%s\"" % f
);
758 p4_system("revert \"%s\"" % f
);
763 fileName
= "submit.txt"
764 file = open(fileName
, "w+")
765 file.write(self
.prepareLogMessage(template
, logMessage
))
767 print ("Perforce submit template written as %s. "
768 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
769 % (fileName
, fileName
))
773 self
.master
= currentGitBranch()
774 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
775 die("Detecting current git branch failed!")
777 self
.master
= args
[0]
781 allowSubmit
= gitConfig("git-p4.allowSubmit")
782 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
783 die("%s is not in git-p4.allowSubmit" % self
.master
)
785 [upstream
, settings
] = findUpstreamBranchPoint()
786 self
.depotPath
= settings
['depot-paths'][0]
787 if len(self
.origin
) == 0:
788 self
.origin
= upstream
791 print "Origin branch is " + self
.origin
793 if len(self
.depotPath
) == 0:
794 print "Internal error: cannot locate perforce depot path from existing branches"
797 self
.clientPath
= p4Where(self
.depotPath
)
799 if len(self
.clientPath
) == 0:
800 print "Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
803 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
804 self
.oldWorkingDirectory
= os
.getcwd()
806 chdir(self
.clientPath
)
807 print "Syncronizing p4 checkout..."
808 p4_system("sync ...")
813 for line
in read_pipe_lines("git rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)):
814 commits
.append(line
.strip())
817 while len(commits
) > 0:
819 commits
= commits
[1:]
820 self
.applyCommit(commit
)
821 if not self
.interactive
:
824 if len(commits
) == 0:
825 print "All changes applied!"
826 chdir(self
.oldWorkingDirectory
)
836 class P4Sync(Command
):
838 Command
.__init
__(self
)
840 optparse
.make_option("--branch", dest
="branch"),
841 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
842 optparse
.make_option("--changesfile", dest
="changesFile"),
843 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
844 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
845 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
846 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
847 help="Import into refs/heads/ , not refs/remotes"),
848 optparse
.make_option("--max-changes", dest
="maxChanges"),
849 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
850 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
851 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
852 help="Only sync files that are included in the Perforce Client Spec")
854 self
.description
= """Imports from Perforce into a git repository.\n
856 //depot/my/project/ -- to import the current head
857 //depot/my/project/@all -- to import everything
858 //depot/my/project/@1,6 -- to import only from revision 1 to 6
860 (a ... is not needed in the path p4 specification, it's added implicitly)"""
862 self
.usage
+= " //depot/path[@revRange]"
864 self
.createdBranches
= Set()
865 self
.committedChanges
= Set()
867 self
.detectBranches
= False
868 self
.detectLabels
= False
869 self
.changesFile
= ""
870 self
.syncWithOrigin
= True
872 self
.importIntoRemotes
= True
874 self
.isWindows
= (platform
.system() == "Windows")
875 self
.keepRepoPath
= False
876 self
.depotPaths
= None
877 self
.p4BranchesInGit
= []
878 self
.cloneExclude
= []
879 self
.useClientSpec
= False
880 self
.clientSpecDirs
= []
882 if gitConfig("git-p4.syncFromOrigin") == "false":
883 self
.syncWithOrigin
= False
885 def extractFilesFromCommit(self
, commit
):
886 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
887 for path
in self
.cloneExclude
]
890 while commit
.has_key("depotFile%s" % fnum
):
891 path
= commit
["depotFile%s" % fnum
]
893 if [p
for p
in self
.cloneExclude
894 if path
.startswith (p
)]:
897 found
= [p
for p
in self
.depotPaths
898 if path
.startswith (p
)]
905 file["rev"] = commit
["rev%s" % fnum
]
906 file["action"] = commit
["action%s" % fnum
]
907 file["type"] = commit
["type%s" % fnum
]
912 def stripRepoPath(self
, path
, prefixes
):
913 if self
.keepRepoPath
:
914 prefixes
= [re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])]
917 if path
.startswith(p
):
922 def splitFilesIntoBranches(self
, commit
):
925 while commit
.has_key("depotFile%s" % fnum
):
926 path
= commit
["depotFile%s" % fnum
]
927 found
= [p
for p
in self
.depotPaths
928 if path
.startswith (p
)]
935 file["rev"] = commit
["rev%s" % fnum
]
936 file["action"] = commit
["action%s" % fnum
]
937 file["type"] = commit
["type%s" % fnum
]
940 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
942 for branch
in self
.knownBranches
.keys():
944 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
945 if relPath
.startswith(branch
+ "/"):
946 if branch
not in branches
:
947 branches
[branch
] = []
948 branches
[branch
].append(file)
953 ## Should move this out, doesn't use SELF.
954 def readP4Files(self
, files
):
960 for val
in self
.clientSpecDirs
:
961 if f
['path'].startswith(val
[0]):
967 filesForCommit
.append(f
)
968 if f
['action'] not in ('delete', 'purge'):
969 filesToRead
.append(f
)
972 if len(filesToRead
) > 0:
973 filedata
= p4CmdList('-x - print',
974 stdin
='\n'.join(['%s#%s' % (f
['path'], f
['rev'])
975 for f
in filesToRead
]),
978 if "p4ExitCode" in filedata
[0]:
979 die("Problems executing p4. Error: [%d]."
980 % (filedata
[0]['p4ExitCode']));
984 while j
< len(filedata
):
988 while j
< len(filedata
) and filedata
[j
]['code'] in ('text', 'unicode', 'binary'):
989 text
+= filedata
[j
]['data']
990 del filedata
[j
]['data']
993 if not stat
.has_key('depotFile'):
994 sys
.stderr
.write("p4 print fails with: %s\n" % repr(stat
))
997 if stat
['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
998 text
= re
.sub(r
'(?i)\$(Id|Header):[^$]*\$',r
'$\1$', text
)
999 elif stat
['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1000 text
= re
.sub(r
'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r
'$\1$', text
)
1002 contents
[stat
['depotFile']] = text
1004 for f
in filesForCommit
:
1006 if contents
.has_key(path
):
1007 f
['data'] = contents
[path
]
1009 return filesForCommit
1011 def commit(self
, details
, files
, branch
, branchPrefixes
, parent
= ""):
1012 epoch
= details
["time"]
1013 author
= details
["user"]
1016 print "commit into %s" % branch
1018 # start with reading files; if that fails, we should not
1022 if [p
for p
in branchPrefixes
if f
['path'].startswith(p
)]:
1023 new_files
.append (f
)
1025 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % path
)
1026 files
= self
.readP4Files(new_files
)
1028 self
.gitStream
.write("commit %s\n" % branch
)
1029 # gitStream.write("mark :%s\n" % details["change"])
1030 self
.committedChanges
.add(int(details
["change"]))
1032 if author
not in self
.users
:
1033 self
.getUserMapFromPerforceServer()
1034 if author
in self
.users
:
1035 committer
= "%s %s %s" % (self
.users
[author
], epoch
, self
.tz
)
1037 committer
= "%s <a@b> %s %s" % (author
, epoch
, self
.tz
)
1039 self
.gitStream
.write("committer %s\n" % committer
)
1041 self
.gitStream
.write("data <<EOT\n")
1042 self
.gitStream
.write(details
["desc"])
1043 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1044 % (','.join (branchPrefixes
), details
["change"]))
1045 if len(details
['options']) > 0:
1046 self
.gitStream
.write(": options = %s" % details
['options'])
1047 self
.gitStream
.write("]\nEOT\n\n")
1051 print "parent %s" % parent
1052 self
.gitStream
.write("from %s\n" % parent
)
1055 if file["type"] == "apple":
1056 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1059 relPath
= self
.stripRepoPath(file['path'], branchPrefixes
)
1060 if file["action"] in ("delete", "purge"):
1061 self
.gitStream
.write("D %s\n" % relPath
)
1066 if isP4Exec(file["type"]):
1068 elif file["type"] == "symlink":
1070 # p4 print on a symlink contains "target\n", so strip it off
1073 if self
.isWindows
and file["type"].endswith("text"):
1074 data
= data
.replace("\r\n", "\n")
1076 self
.gitStream
.write("M %s inline %s\n" % (mode
, relPath
))
1077 self
.gitStream
.write("data %s\n" % len(data
))
1078 self
.gitStream
.write(data
)
1079 self
.gitStream
.write("\n")
1081 self
.gitStream
.write("\n")
1083 change
= int(details
["change"])
1085 if self
.labels
.has_key(change
):
1086 label
= self
.labels
[change
]
1087 labelDetails
= label
[0]
1088 labelRevisions
= label
[1]
1090 print "Change %s is labelled %s" % (change
, labelDetails
)
1092 files
= p4CmdList("files " + ' '.join (["%s...@%s" % (p
, change
)
1093 for p
in branchPrefixes
]))
1095 if len(files
) == len(labelRevisions
):
1099 if info
["action"] in ("delete", "purge"):
1101 cleanedFiles
[info
["depotFile"]] = info
["rev"]
1103 if cleanedFiles
== labelRevisions
:
1104 self
.gitStream
.write("tag tag_%s\n" % labelDetails
["label"])
1105 self
.gitStream
.write("from %s\n" % branch
)
1107 owner
= labelDetails
["Owner"]
1109 if author
in self
.users
:
1110 tagger
= "%s %s %s" % (self
.users
[owner
], epoch
, self
.tz
)
1112 tagger
= "%s <a@b> %s %s" % (owner
, epoch
, self
.tz
)
1113 self
.gitStream
.write("tagger %s\n" % tagger
)
1114 self
.gitStream
.write("data <<EOT\n")
1115 self
.gitStream
.write(labelDetails
["Description"])
1116 self
.gitStream
.write("EOT\n\n")
1120 print ("Tag %s does not match with change %s: files do not match."
1121 % (labelDetails
["label"], change
))
1125 print ("Tag %s does not match with change %s: file count is different."
1126 % (labelDetails
["label"], change
))
1128 def getUserCacheFilename(self
):
1129 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
1130 return home
+ "/.gitp4-usercache.txt"
1132 def getUserMapFromPerforceServer(self
):
1133 if self
.userMapFromPerforceServer
:
1137 for output
in p4CmdList("users"):
1138 if not output
.has_key("User"):
1140 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
1144 for (key
, val
) in self
.users
.items():
1145 s
+= "%s\t%s\n" % (key
, val
)
1147 open(self
.getUserCacheFilename(), "wb").write(s
)
1148 self
.userMapFromPerforceServer
= True
1150 def loadUserMapFromCache(self
):
1152 self
.userMapFromPerforceServer
= False
1154 cache
= open(self
.getUserCacheFilename(), "rb")
1155 lines
= cache
.readlines()
1158 entry
= line
.strip().split("\t")
1159 self
.users
[entry
[0]] = entry
[1]
1161 self
.getUserMapFromPerforceServer()
1163 def getLabels(self
):
1166 l
= p4CmdList("labels %s..." % ' '.join (self
.depotPaths
))
1167 if len(l
) > 0 and not self
.silent
:
1168 print "Finding files belonging to labels in %s" % `self
.depotPaths`
1171 label
= output
["label"]
1175 print "Querying files for label %s" % label
1176 for file in p4CmdList("files "
1177 + ' '.join (["%s...@%s" % (p
, label
)
1178 for p
in self
.depotPaths
])):
1179 revisions
[file["depotFile"]] = file["rev"]
1180 change
= int(file["change"])
1181 if change
> newestChange
:
1182 newestChange
= change
1184 self
.labels
[newestChange
] = [output
, revisions
]
1187 print "Label changes: %s" % self
.labels
.keys()
1189 def guessProjectName(self
):
1190 for p
in self
.depotPaths
:
1193 p
= p
[p
.strip().rfind("/") + 1:]
1194 if not p
.endswith("/"):
1198 def getBranchMapping(self
):
1199 lostAndFoundBranches
= set()
1201 for info
in p4CmdList("branches"):
1202 details
= p4Cmd("branch -o %s" % info
["branch"])
1204 while details
.has_key("View%s" % viewIdx
):
1205 paths
= details
["View%s" % viewIdx
].split(" ")
1206 viewIdx
= viewIdx
+ 1
1207 # require standard //depot/foo/... //depot/bar/... mapping
1208 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
1211 destination
= paths
[1]
1213 if source
.startswith(self
.depotPaths
[0]) and destination
.startswith(self
.depotPaths
[0]):
1214 source
= source
[len(self
.depotPaths
[0]):-4]
1215 destination
= destination
[len(self
.depotPaths
[0]):-4]
1217 if destination
in self
.knownBranches
:
1219 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
1220 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
1223 self
.knownBranches
[destination
] = source
1225 lostAndFoundBranches
.discard(destination
)
1227 if source
not in self
.knownBranches
:
1228 lostAndFoundBranches
.add(source
)
1231 for branch
in lostAndFoundBranches
:
1232 self
.knownBranches
[branch
] = branch
1234 def getBranchMappingFromGitBranches(self
):
1235 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1236 for branch
in branches
.keys():
1237 if branch
== "master":
1240 branch
= branch
[len(self
.projectName
):]
1241 self
.knownBranches
[branch
] = branch
1243 def listExistingP4GitBranches(self
):
1244 # branches holds mapping from name to commit
1245 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1246 self
.p4BranchesInGit
= branches
.keys()
1247 for branch
in branches
.keys():
1248 self
.initialParents
[self
.refPrefix
+ branch
] = branches
[branch
]
1250 def updateOptionDict(self
, d
):
1252 if self
.keepRepoPath
:
1253 option_keys
['keepRepoPath'] = 1
1255 d
["options"] = ' '.join(sorted(option_keys
.keys()))
1257 def readOptions(self
, d
):
1258 self
.keepRepoPath
= (d
.has_key('options')
1259 and ('keepRepoPath' in d
['options']))
1261 def gitRefForBranch(self
, branch
):
1262 if branch
== "main":
1263 return self
.refPrefix
+ "master"
1265 if len(branch
) <= 0:
1268 return self
.refPrefix
+ self
.projectName
+ branch
1270 def gitCommitByP4Change(self
, ref
, change
):
1272 print "looking in ref " + ref
+ " for change %s using bisect..." % change
1275 latestCommit
= parseRevision(ref
)
1279 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
1280 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
1285 log
= extractLogMessageFromGitCommit(next
)
1286 settings
= extractSettingsGitLog(log
)
1287 currentChange
= int(settings
['change'])
1289 print "current change %s" % currentChange
1291 if currentChange
== change
:
1293 print "found %s" % next
1296 if currentChange
< change
:
1297 earliestCommit
= "^%s" % next
1299 latestCommit
= "%s" % next
1303 def importNewBranch(self
, branch
, maxChange
):
1304 # make fast-import flush all changes to disk and update the refs using the checkpoint
1305 # command so that we can try to find the branch parent in the git history
1306 self
.gitStream
.write("checkpoint\n\n");
1307 self
.gitStream
.flush();
1308 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1309 range = "@1,%s" % maxChange
1310 #print "prefix" + branchPrefix
1311 changes
= p4ChangesForPaths([branchPrefix
], range)
1312 if len(changes
) <= 0:
1314 firstChange
= changes
[0]
1315 #print "first change in branch: %s" % firstChange
1316 sourceBranch
= self
.knownBranches
[branch
]
1317 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
1318 sourceRef
= self
.gitRefForBranch(sourceBranch
)
1319 #print "source " + sourceBranch
1321 branchParentChange
= int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath
, firstChange
))["change"])
1322 #print "branch parent: %s" % branchParentChange
1323 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
1324 if len(gitParent
) > 0:
1325 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
1326 #print "parent git commit: %s" % gitParent
1328 self
.importChanges(changes
)
1331 def importChanges(self
, changes
):
1333 for change
in changes
:
1334 description
= p4Cmd("describe %s" % change
)
1335 self
.updateOptionDict(description
)
1338 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
1343 if self
.detectBranches
:
1344 branches
= self
.splitFilesIntoBranches(description
)
1345 for branch
in branches
.keys():
1347 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1351 filesForCommit
= branches
[branch
]
1354 print "branch is %s" % branch
1356 self
.updatedBranches
.add(branch
)
1358 if branch
not in self
.createdBranches
:
1359 self
.createdBranches
.add(branch
)
1360 parent
= self
.knownBranches
[branch
]
1361 if parent
== branch
:
1364 fullBranch
= self
.projectName
+ branch
1365 if fullBranch
not in self
.p4BranchesInGit
:
1367 print("\n Importing new branch %s" % fullBranch
);
1368 if self
.importNewBranch(branch
, change
- 1):
1370 self
.p4BranchesInGit
.append(fullBranch
)
1372 print("\n Resuming with change %s" % change
);
1375 print "parent determined through known branches: %s" % parent
1377 branch
= self
.gitRefForBranch(branch
)
1378 parent
= self
.gitRefForBranch(parent
)
1381 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
1383 if len(parent
) == 0 and branch
in self
.initialParents
:
1384 parent
= self
.initialParents
[branch
]
1385 del self
.initialParents
[branch
]
1387 self
.commit(description
, filesForCommit
, branch
, [branchPrefix
], parent
)
1389 files
= self
.extractFilesFromCommit(description
)
1390 self
.commit(description
, files
, self
.branch
, self
.depotPaths
,
1392 self
.initialParent
= ""
1394 print self
.gitError
.read()
1397 def importHeadRevision(self
, revision
):
1398 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
1400 details
= { "user" : "git perforce import user", "time" : int(time
.time()) }
1401 details
["desc"] = ("Initial import of %s from the state at revision %s"
1402 % (' '.join(self
.depotPaths
), revision
))
1403 details
["change"] = revision
1407 for info
in p4CmdList("files "
1408 + ' '.join(["%s...%s"
1410 for p
in self
.depotPaths
])):
1412 if info
['code'] == 'error':
1413 sys
.stderr
.write("p4 returned an error: %s\n"
1418 change
= int(info
["change"])
1419 if change
> newestRevision
:
1420 newestRevision
= change
1422 if info
["action"] in ("delete", "purge"):
1423 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1424 #fileCnt = fileCnt + 1
1427 for prop
in ["depotFile", "rev", "action", "type" ]:
1428 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
1430 fileCnt
= fileCnt
+ 1
1432 details
["change"] = newestRevision
1433 self
.updateOptionDict(details
)
1435 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
, self
.depotPaths
)
1437 print "IO error with git fast-import. Is your git version recent enough?"
1438 print self
.gitError
.read()
1441 def getClientSpec(self
):
1442 specList
= p4CmdList( "client -o" )
1444 for entry
in specList
:
1445 for k
,v
in entry
.iteritems():
1446 if k
.startswith("View"):
1447 if v
.startswith('"'):
1451 index
= v
.find("...")
1453 if v
.startswith("-"):
1458 self
.clientSpecDirs
= temp
.items()
1459 self
.clientSpecDirs
.sort( lambda x
, y
: abs( y
[1] ) - abs( x
[1] ) )
1461 def run(self
, args
):
1462 self
.depotPaths
= []
1463 self
.changeRange
= ""
1464 self
.initialParent
= ""
1465 self
.previousDepotPaths
= []
1467 # map from branch depot path to parent branch
1468 self
.knownBranches
= {}
1469 self
.initialParents
= {}
1470 self
.hasOrigin
= originP4BranchesExist()
1471 if not self
.syncWithOrigin
:
1472 self
.hasOrigin
= False
1474 if self
.importIntoRemotes
:
1475 self
.refPrefix
= "refs/remotes/p4/"
1477 self
.refPrefix
= "refs/heads/p4/"
1479 if self
.syncWithOrigin
and self
.hasOrigin
:
1481 print "Syncing with origin first by calling git fetch origin"
1482 system("git fetch origin")
1484 if len(self
.branch
) == 0:
1485 self
.branch
= self
.refPrefix
+ "master"
1486 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
1487 system("git update-ref %s refs/heads/p4" % self
.branch
)
1488 system("git branch -D p4");
1489 # create it /after/ importing, when master exists
1490 if not gitBranchExists(self
.refPrefix
+ "HEAD") and self
.importIntoRemotes
and gitBranchExists(self
.branch
):
1491 system("git symbolic-ref %sHEAD %s" % (self
.refPrefix
, self
.branch
))
1493 if self
.useClientSpec
or gitConfig("git-p4.useclientspec") == "true":
1494 self
.getClientSpec()
1496 # TODO: should always look at previous commits,
1497 # merge with previous imports, if possible.
1500 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
1501 self
.listExistingP4GitBranches()
1503 if len(self
.p4BranchesInGit
) > 1:
1505 print "Importing from/into multiple branches"
1506 self
.detectBranches
= True
1509 print "branches: %s" % self
.p4BranchesInGit
1512 for branch
in self
.p4BranchesInGit
:
1513 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
1515 settings
= extractSettingsGitLog(logMsg
)
1517 self
.readOptions(settings
)
1518 if (settings
.has_key('depot-paths')
1519 and settings
.has_key ('change')):
1520 change
= int(settings
['change']) + 1
1521 p4Change
= max(p4Change
, change
)
1523 depotPaths
= sorted(settings
['depot-paths'])
1524 if self
.previousDepotPaths
== []:
1525 self
.previousDepotPaths
= depotPaths
1528 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
1529 for i
in range(0, min(len(cur
), len(prev
))):
1530 if cur
[i
] <> prev
[i
]:
1534 paths
.append (cur
[:i
+ 1])
1536 self
.previousDepotPaths
= paths
1539 self
.depotPaths
= sorted(self
.previousDepotPaths
)
1540 self
.changeRange
= "@%s,#head" % p4Change
1541 if not self
.detectBranches
:
1542 self
.initialParent
= parseRevision(self
.branch
)
1543 if not self
.silent
and not self
.detectBranches
:
1544 print "Performing incremental import into %s git branch" % self
.branch
1546 if not self
.branch
.startswith("refs/"):
1547 self
.branch
= "refs/heads/" + self
.branch
1549 if len(args
) == 0 and self
.depotPaths
:
1551 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
1553 if self
.depotPaths
and self
.depotPaths
!= args
:
1554 print ("previous import used depot path %s and now %s was specified. "
1555 "This doesn't work!" % (' '.join (self
.depotPaths
),
1559 self
.depotPaths
= sorted(args
)
1565 for p
in self
.depotPaths
:
1566 if p
.find("@") != -1:
1567 atIdx
= p
.index("@")
1568 self
.changeRange
= p
[atIdx
:]
1569 if self
.changeRange
== "@all":
1570 self
.changeRange
= ""
1571 elif ',' not in self
.changeRange
:
1572 revision
= self
.changeRange
1573 self
.changeRange
= ""
1575 elif p
.find("#") != -1:
1576 hashIdx
= p
.index("#")
1577 revision
= p
[hashIdx
:]
1579 elif self
.previousDepotPaths
== []:
1582 p
= re
.sub ("\.\.\.$", "", p
)
1583 if not p
.endswith("/"):
1588 self
.depotPaths
= newPaths
1591 self
.loadUserMapFromCache()
1593 if self
.detectLabels
:
1596 if self
.detectBranches
:
1597 ## FIXME - what's a P4 projectName ?
1598 self
.projectName
= self
.guessProjectName()
1601 self
.getBranchMappingFromGitBranches()
1603 self
.getBranchMapping()
1605 print "p4-git branches: %s" % self
.p4BranchesInGit
1606 print "initial parents: %s" % self
.initialParents
1607 for b
in self
.p4BranchesInGit
:
1611 b
= b
[len(self
.projectName
):]
1612 self
.createdBranches
.add(b
)
1614 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
1616 importProcess
= subprocess
.Popen(["git", "fast-import"],
1617 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
1618 stderr
=subprocess
.PIPE
);
1619 self
.gitOutput
= importProcess
.stdout
1620 self
.gitStream
= importProcess
.stdin
1621 self
.gitError
= importProcess
.stderr
1624 self
.importHeadRevision(revision
)
1628 if len(self
.changesFile
) > 0:
1629 output
= open(self
.changesFile
).readlines()
1632 changeSet
.add(int(line
))
1634 for change
in changeSet
:
1635 changes
.append(change
)
1640 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
1642 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
)
1644 if len(self
.maxChanges
) > 0:
1645 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
1647 if len(changes
) == 0:
1649 print "No changes to import!"
1652 if not self
.silent
and not self
.detectBranches
:
1653 print "Import destination: %s" % self
.branch
1655 self
.updatedBranches
= set()
1657 self
.importChanges(changes
)
1661 if len(self
.updatedBranches
) > 0:
1662 sys
.stdout
.write("Updated branches: ")
1663 for b
in self
.updatedBranches
:
1664 sys
.stdout
.write("%s " % b
)
1665 sys
.stdout
.write("\n")
1667 self
.gitStream
.close()
1668 if importProcess
.wait() != 0:
1669 die("fast-import failed: %s" % self
.gitError
.read())
1670 self
.gitOutput
.close()
1671 self
.gitError
.close()
1675 class P4Rebase(Command
):
1677 Command
.__init
__(self
)
1679 self
.description
= ("Fetches the latest revision from perforce and "
1680 + "rebases the current work (branch) against it")
1681 self
.verbose
= False
1683 def run(self
, args
):
1687 return self
.rebase()
1690 if os
.system("git update-index --refresh") != 0:
1691 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.");
1692 if len(read_pipe("git diff-index HEAD --")) > 0:
1693 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1695 [upstream
, settings
] = findUpstreamBranchPoint()
1696 if len(upstream
) == 0:
1697 die("Cannot find upstream branchpoint for rebase")
1699 # the branchpoint may be p4/foo~3, so strip off the parent
1700 upstream
= re
.sub("~[0-9]+$", "", upstream
)
1702 print "Rebasing the current branch onto %s" % upstream
1703 oldHead
= read_pipe("git rev-parse HEAD").strip()
1704 system("git rebase %s" % upstream
)
1705 system("git diff-tree --stat --summary -M %s HEAD" % oldHead
)
1708 class P4Clone(P4Sync
):
1710 P4Sync
.__init
__(self
)
1711 self
.description
= "Creates a new git repository and imports from Perforce into it"
1712 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
1714 optparse
.make_option("--destination", dest
="cloneDestination",
1715 action
='store', default
=None,
1716 help="where to leave result of the clone"),
1717 optparse
.make_option("-/", dest
="cloneExclude",
1718 action
="append", type="string",
1719 help="exclude depot path")
1721 self
.cloneDestination
= None
1722 self
.needsGit
= False
1724 # This is required for the "append" cloneExclude action
1725 def ensure_value(self
, attr
, value
):
1726 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
1727 setattr(self
, attr
, value
)
1728 return getattr(self
, attr
)
1730 def defaultDestination(self
, args
):
1731 ## TODO: use common prefix of args?
1733 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
1734 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
1735 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
1736 depotDir
= re
.sub(r
"/$", "", depotDir
)
1737 return os
.path
.split(depotDir
)[1]
1739 def run(self
, args
):
1743 if self
.keepRepoPath
and not self
.cloneDestination
:
1744 sys
.stderr
.write("Must specify destination for --keep-path\n")
1749 if not self
.cloneDestination
and len(depotPaths
) > 1:
1750 self
.cloneDestination
= depotPaths
[-1]
1751 depotPaths
= depotPaths
[:-1]
1753 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
1754 for p
in depotPaths
:
1755 if not p
.startswith("//"):
1758 if not self
.cloneDestination
:
1759 self
.cloneDestination
= self
.defaultDestination(args
)
1761 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
1762 if not os
.path
.exists(self
.cloneDestination
):
1763 os
.makedirs(self
.cloneDestination
)
1764 chdir(self
.cloneDestination
)
1766 self
.gitdir
= os
.getcwd() + "/.git"
1767 if not P4Sync
.run(self
, depotPaths
):
1769 if self
.branch
!= "master":
1770 if self
.importIntoRemotes
:
1771 masterbranch
= "refs/remotes/p4/master"
1773 masterbranch
= "refs/heads/p4/master"
1774 if gitBranchExists(masterbranch
):
1775 system("git branch master %s" % masterbranch
)
1776 system("git checkout -f")
1778 print "Could not detect main branch. No checkout/master branch created."
1782 class P4Branches(Command
):
1784 Command
.__init
__(self
)
1786 self
.description
= ("Shows the git branches that hold imports and their "
1787 + "corresponding perforce depot paths")
1788 self
.verbose
= False
1790 def run(self
, args
):
1791 if originP4BranchesExist():
1792 createOrUpdateBranchesFromOrigin()
1794 cmdline
= "git rev-parse --symbolic "
1795 cmdline
+= " --remotes"
1797 for line
in read_pipe_lines(cmdline
):
1800 if not line
.startswith('p4/') or line
== "p4/HEAD":
1804 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
1805 settings
= extractSettingsGitLog(log
)
1807 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
1810 class HelpFormatter(optparse
.IndentedHelpFormatter
):
1812 optparse
.IndentedHelpFormatter
.__init
__(self
)
1814 def format_description(self
, description
):
1816 return description
+ "\n"
1820 def printUsage(commands
):
1821 print "usage: %s <command> [options]" % sys
.argv
[0]
1823 print "valid commands: %s" % ", ".join(commands
)
1825 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
1830 "submit" : P4Submit
,
1831 "commit" : P4Submit
,
1833 "rebase" : P4Rebase
,
1835 "rollback" : P4RollBack
,
1836 "branches" : P4Branches
1841 if len(sys
.argv
[1:]) == 0:
1842 printUsage(commands
.keys())
1846 cmdName
= sys
.argv
[1]
1848 klass
= commands
[cmdName
]
1851 print "unknown command %s" % cmdName
1853 printUsage(commands
.keys())
1856 options
= cmd
.options
1857 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
1861 if len(options
) > 0:
1862 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
1864 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
1866 description
= cmd
.description
,
1867 formatter
= HelpFormatter())
1869 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
1871 verbose
= cmd
.verbose
1873 if cmd
.gitdir
== None:
1874 cmd
.gitdir
= os
.path
.abspath(".git")
1875 if not isValidGitDir(cmd
.gitdir
):
1876 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
1877 if os
.path
.exists(cmd
.gitdir
):
1878 cdup
= read_pipe("git rev-parse --show-cdup").strip()
1882 if not isValidGitDir(cmd
.gitdir
):
1883 if isValidGitDir(cmd
.gitdir
+ "/.git"):
1884 cmd
.gitdir
+= "/.git"
1886 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
1888 os
.environ
["GIT_DIR"] = cmd
.gitdir
1890 if not cmd
.run(args
):
1894 if __name__
== '__main__':