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 output
= p4Cmd("where %s..." % depotPath
)
249 if output
["code"] == "error":
253 clientPath
= output
.get("path")
254 elif "data" in output
:
255 data
= output
.get("data")
256 lastSpace
= data
.rfind(" ")
257 clientPath
= data
[lastSpace
+ 1:]
259 if clientPath
.endswith("..."):
260 clientPath
= clientPath
[:-3]
263 def currentGitBranch():
264 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
266 def isValidGitDir(path
):
267 if (os
.path
.exists(path
+ "/HEAD")
268 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
272 def parseRevision(ref
):
273 return read_pipe("git rev-parse %s" % ref
).strip()
275 def extractLogMessageFromGitCommit(commit
):
278 ## fixme: title is first line of commit, not 1st paragraph.
280 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
289 def extractSettingsGitLog(log
):
291 for line
in log
.split("\n"):
293 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
297 assignments
= m
.group(1).split (':')
298 for a
in assignments
:
300 key
= vals
[0].strip()
301 val
= ('='.join (vals
[1:])).strip()
302 if val
.endswith ('\"') and val
.startswith('"'):
307 paths
= values
.get("depot-paths")
309 paths
= values
.get("depot-path")
311 values
['depot-paths'] = paths
.split(',')
314 def gitBranchExists(branch
):
315 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
316 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
317 return proc
.wait() == 0;
320 return read_pipe("git config %s" % key
, ignore_error
=True).strip()
322 def p4BranchesInGit(branchesAreInRemotes
= True):
325 cmdline
= "git rev-parse --symbolic "
326 if branchesAreInRemotes
:
327 cmdline
+= " --remotes"
329 cmdline
+= " --branches"
331 for line
in read_pipe_lines(cmdline
):
334 ## only import to p4/
335 if not line
.startswith('p4/') or line
== "p4/HEAD":
340 branch
= re
.sub ("^p4/", "", line
)
342 branches
[branch
] = parseRevision(line
)
345 def findUpstreamBranchPoint(head
= "HEAD"):
346 branches
= p4BranchesInGit()
347 # map from depot-path to branch name
348 branchByDepotPath
= {}
349 for branch
in branches
.keys():
350 tip
= branches
[branch
]
351 log
= extractLogMessageFromGitCommit(tip
)
352 settings
= extractSettingsGitLog(log
)
353 if settings
.has_key("depot-paths"):
354 paths
= ",".join(settings
["depot-paths"])
355 branchByDepotPath
[paths
] = "remotes/p4/" + branch
359 while parent
< 65535:
360 commit
= head
+ "~%s" % parent
361 log
= extractLogMessageFromGitCommit(commit
)
362 settings
= extractSettingsGitLog(log
)
363 if settings
.has_key("depot-paths"):
364 paths
= ",".join(settings
["depot-paths"])
365 if branchByDepotPath
.has_key(paths
):
366 return [branchByDepotPath
[paths
], settings
]
370 return ["", settings
]
372 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
374 print ("Creating/updating branch(es) in %s based on origin branch(es)"
377 originPrefix
= "origin/p4/"
379 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
381 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
384 headName
= line
[len(originPrefix
):]
385 remoteHead
= localRefPrefix
+ headName
388 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
389 if (not original
.has_key('depot-paths')
390 or not original
.has_key('change')):
394 if not gitBranchExists(remoteHead
):
396 print "creating %s" % remoteHead
399 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
400 if settings
.has_key('change') > 0:
401 if settings
['depot-paths'] == original
['depot-paths']:
402 originP4Change
= int(original
['change'])
403 p4Change
= int(settings
['change'])
404 if originP4Change
> p4Change
:
405 print ("%s (%s) is newer than %s (%s). "
406 "Updating p4 branch from origin."
407 % (originHead
, originP4Change
,
408 remoteHead
, p4Change
))
411 print ("Ignoring: %s was imported from %s while "
412 "%s was imported from %s"
413 % (originHead
, ','.join(original
['depot-paths']),
414 remoteHead
, ','.join(settings
['depot-paths'])))
417 system("git update-ref %s %s" % (remoteHead
, originHead
))
419 def originP4BranchesExist():
420 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
422 def p4ChangesForPaths(depotPaths
, changeRange
):
424 output
= p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p
, changeRange
)
425 for p
in depotPaths
]))
429 changeNum
= line
.split(" ")[1]
430 changes
.append(int(changeNum
))
437 self
.usage
= "usage: %prog [options]"
440 class P4Debug(Command
):
442 Command
.__init
__(self
)
444 optparse
.make_option("--verbose", dest
="verbose", action
="store_true",
447 self
.description
= "A tool to debug the output of p4 -G."
448 self
.needsGit
= False
453 for output
in p4CmdList(" ".join(args
)):
454 print 'Element: %d' % j
459 class P4RollBack(Command
):
461 Command
.__init
__(self
)
463 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
464 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
466 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
468 self
.rollbackLocalBranches
= False
473 maxChange
= int(args
[0])
475 if "p4ExitCode" in p4Cmd("changes -m 1"):
476 die("Problems executing p4");
478 if self
.rollbackLocalBranches
:
479 refPrefix
= "refs/heads/"
480 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
482 refPrefix
= "refs/remotes/"
483 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
486 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
488 ref
= refPrefix
+ line
489 log
= extractLogMessageFromGitCommit(ref
)
490 settings
= extractSettingsGitLog(log
)
492 depotPaths
= settings
['depot-paths']
493 change
= settings
['change']
497 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
498 for p
in depotPaths
]))) == 0:
499 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
500 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
503 while change
and int(change
) > maxChange
:
506 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
507 system("git update-ref %s \"%s^\"" % (ref
, ref
))
508 log
= extractLogMessageFromGitCommit(ref
)
509 settings
= extractSettingsGitLog(log
)
512 depotPaths
= settings
['depot-paths']
513 change
= settings
['change']
516 print "%s rewound to %s" % (ref
, change
)
520 class P4Submit(Command
):
522 Command
.__init
__(self
)
524 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
525 optparse
.make_option("--origin", dest
="origin"),
526 optparse
.make_option("-M", dest
="detectRename", action
="store_true"),
528 self
.description
= "Submit changes from git to the perforce depot."
529 self
.usage
+= " [name of git branch to submit into perforce depot]"
530 self
.interactive
= True
532 self
.detectRename
= False
534 self
.isWindows
= (platform
.system() == "Windows")
537 if len(p4CmdList("opened ...")) > 0:
538 die("You have files opened with perforce! Close them before starting the sync.")
540 # replaces everything between 'Description:' and the next P4 submit template field with the
542 def prepareLogMessage(self
, template
, message
):
545 inDescriptionSection
= False
547 for line
in template
.split("\n"):
548 if line
.startswith("#"):
549 result
+= line
+ "\n"
552 if inDescriptionSection
:
553 if line
.startswith("Files:"):
554 inDescriptionSection
= False
558 if line
.startswith("Description:"):
559 inDescriptionSection
= True
561 for messageLine
in message
.split("\n"):
562 line
+= "\t" + messageLine
+ "\n"
564 result
+= line
+ "\n"
568 def prepareSubmitTemplate(self
):
569 # remove lines in the Files section that show changes to files outside the depot path we're committing into
571 inFilesSection
= False
572 for line
in p4_read_pipe_lines("change -o"):
573 if line
.endswith("\r\n"):
574 line
= line
[:-2] + "\n"
576 if line
.startswith("\t"):
577 # path starts and ends with a tab
579 lastTab
= path
.rfind("\t")
581 path
= path
[:lastTab
]
582 if not path
.startswith(self
.depotPath
):
585 inFilesSection
= False
587 if line
.startswith("Files:"):
588 inFilesSection
= True
594 def applyCommit(self
, id):
595 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
596 diffOpts
= ("", "-M")[self
.detectRename
]
597 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts
, id, id))
599 filesToDelete
= set()
601 filesToChangeExecBit
= {}
603 diff
= parseDiffTreeEntry(line
)
604 modifier
= diff
['status']
607 p4_system("edit \"%s\"" % path
)
608 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
609 filesToChangeExecBit
[path
] = diff
['dst_mode']
610 editedFiles
.add(path
)
611 elif modifier
== "A":
613 filesToChangeExecBit
[path
] = diff
['dst_mode']
614 if path
in filesToDelete
:
615 filesToDelete
.remove(path
)
616 elif modifier
== "D":
617 filesToDelete
.add(path
)
618 if path
in filesToAdd
:
619 filesToAdd
.remove(path
)
620 elif modifier
== "R":
621 src
, dest
= diff
['src'], diff
['dst']
622 p4_system("integrate -Dt \"%s\" \"%s\"" % (src
, dest
))
623 p4_system("edit \"%s\"" % (dest
))
624 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
625 filesToChangeExecBit
[dest
] = diff
['dst_mode']
627 editedFiles
.add(dest
)
628 filesToDelete
.add(src
)
630 die("unknown modifier %s for %s" % (modifier
, path
))
632 diffcmd
= "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
633 patchcmd
= diffcmd
+ " | git apply "
634 tryPatchCmd
= patchcmd
+ "--check -"
635 applyPatchCmd
= patchcmd
+ "--check --apply -"
637 if os
.system(tryPatchCmd
) != 0:
638 print "Unfortunately applying the change failed!"
639 print "What do you want to do?"
641 while response
!= "s" and response
!= "a" and response
!= "w":
642 response
= raw_input("[s]kip this patch / [a]pply the patch forcibly "
643 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
645 print "Skipping! Good luck with the next patches..."
646 for f
in editedFiles
:
647 p4_system("revert \"%s\"" % f
);
651 elif response
== "a":
652 os
.system(applyPatchCmd
)
653 if len(filesToAdd
) > 0:
654 print "You may also want to call p4 add on the following files:"
655 print " ".join(filesToAdd
)
656 if len(filesToDelete
):
657 print "The following files should be scheduled for deletion with p4 delete:"
658 print " ".join(filesToDelete
)
659 die("Please resolve and submit the conflict manually and "
660 + "continue afterwards with git-p4 submit --continue")
661 elif response
== "w":
662 system(diffcmd
+ " > patch.txt")
663 print "Patch saved to patch.txt in %s !" % self
.clientPath
664 die("Please resolve and submit the conflict manually and "
665 "continue afterwards with git-p4 submit --continue")
667 system(applyPatchCmd
)
670 p4_system("add \"%s\"" % f
)
671 for f
in filesToDelete
:
672 p4_system("revert \"%s\"" % f
)
673 p4_system("delete \"%s\"" % f
)
675 # Set/clear executable bits
676 for f
in filesToChangeExecBit
.keys():
677 mode
= filesToChangeExecBit
[f
]
678 setP4ExecBit(f
, mode
)
680 logMessage
= extractLogMessageFromGitCommit(id)
681 logMessage
= logMessage
.strip()
683 template
= self
.prepareSubmitTemplate()
686 submitTemplate
= self
.prepareLogMessage(template
, logMessage
)
687 if os
.environ
.has_key("P4DIFF"):
688 del(os
.environ
["P4DIFF"])
689 diff
= p4_read_pipe("diff -du ...")
692 for newFile
in filesToAdd
:
693 newdiff
+= "==== new file ====\n"
694 newdiff
+= "--- /dev/null\n"
695 newdiff
+= "+++ %s\n" % newFile
696 f
= open(newFile
, "r")
697 for line
in f
.readlines():
698 newdiff
+= "+" + line
701 separatorLine
= "######## everything below this line is just the diff #######\n"
703 [handle
, fileName
] = tempfile
.mkstemp()
704 tmpFile
= os
.fdopen(handle
, "w+")
706 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
707 separatorLine
= separatorLine
.replace("\n", "\r\n")
708 newdiff
= newdiff
.replace("\n", "\r\n")
709 tmpFile
.write(submitTemplate
+ separatorLine
+ diff
+ newdiff
)
712 if platform
.system() == "Windows":
713 defaultEditor
= "notepad"
714 if os
.environ
.has_key("P4EDITOR"):
715 editor
= os
.environ
.get("P4EDITOR")
717 editor
= os
.environ
.get("EDITOR", defaultEditor
);
718 system(editor
+ " " + fileName
)
719 tmpFile
= open(fileName
, "rb")
720 message
= tmpFile
.read()
723 submitTemplate
= message
[:message
.index(separatorLine
)]
725 submitTemplate
= submitTemplate
.replace("\r\n", "\n")
727 p4_write_pipe("submit -i", submitTemplate
)
729 fileName
= "submit.txt"
730 file = open(fileName
, "w+")
731 file.write(self
.prepareLogMessage(template
, logMessage
))
733 print ("Perforce submit template written as %s. "
734 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
735 % (fileName
, fileName
))
739 self
.master
= currentGitBranch()
740 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
741 die("Detecting current git branch failed!")
743 self
.master
= args
[0]
747 allowSubmit
= gitConfig("git-p4.allowSubmit")
748 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
749 die("%s is not in git-p4.allowSubmit" % self
.master
)
751 [upstream
, settings
] = findUpstreamBranchPoint()
752 self
.depotPath
= settings
['depot-paths'][0]
753 if len(self
.origin
) == 0:
754 self
.origin
= upstream
757 print "Origin branch is " + self
.origin
759 if len(self
.depotPath
) == 0:
760 print "Internal error: cannot locate perforce depot path from existing branches"
763 self
.clientPath
= p4Where(self
.depotPath
)
765 if len(self
.clientPath
) == 0:
766 print "Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
769 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
770 self
.oldWorkingDirectory
= os
.getcwd()
772 chdir(self
.clientPath
)
773 print "Syncronizing p4 checkout..."
774 p4_system("sync ...")
779 for line
in read_pipe_lines("git rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)):
780 commits
.append(line
.strip())
783 while len(commits
) > 0:
785 commits
= commits
[1:]
786 self
.applyCommit(commit
)
787 if not self
.interactive
:
790 if len(commits
) == 0:
791 print "All changes applied!"
792 chdir(self
.oldWorkingDirectory
)
802 class P4Sync(Command
):
804 Command
.__init
__(self
)
806 optparse
.make_option("--branch", dest
="branch"),
807 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
808 optparse
.make_option("--changesfile", dest
="changesFile"),
809 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
810 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
811 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
812 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
813 help="Import into refs/heads/ , not refs/remotes"),
814 optparse
.make_option("--max-changes", dest
="maxChanges"),
815 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
816 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
817 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
818 help="Only sync files that are included in the Perforce Client Spec")
820 self
.description
= """Imports from Perforce into a git repository.\n
822 //depot/my/project/ -- to import the current head
823 //depot/my/project/@all -- to import everything
824 //depot/my/project/@1,6 -- to import only from revision 1 to 6
826 (a ... is not needed in the path p4 specification, it's added implicitly)"""
828 self
.usage
+= " //depot/path[@revRange]"
830 self
.createdBranches
= Set()
831 self
.committedChanges
= Set()
833 self
.detectBranches
= False
834 self
.detectLabels
= False
835 self
.changesFile
= ""
836 self
.syncWithOrigin
= True
838 self
.importIntoRemotes
= True
840 self
.isWindows
= (platform
.system() == "Windows")
841 self
.keepRepoPath
= False
842 self
.depotPaths
= None
843 self
.p4BranchesInGit
= []
844 self
.cloneExclude
= []
845 self
.useClientSpec
= False
846 self
.clientSpecDirs
= []
848 if gitConfig("git-p4.syncFromOrigin") == "false":
849 self
.syncWithOrigin
= False
851 def extractFilesFromCommit(self
, commit
):
852 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
853 for path
in self
.cloneExclude
]
856 while commit
.has_key("depotFile%s" % fnum
):
857 path
= commit
["depotFile%s" % fnum
]
859 if [p
for p
in self
.cloneExclude
860 if path
.startswith (p
)]:
863 found
= [p
for p
in self
.depotPaths
864 if path
.startswith (p
)]
871 file["rev"] = commit
["rev%s" % fnum
]
872 file["action"] = commit
["action%s" % fnum
]
873 file["type"] = commit
["type%s" % fnum
]
878 def stripRepoPath(self
, path
, prefixes
):
879 if self
.keepRepoPath
:
880 prefixes
= [re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])]
883 if path
.startswith(p
):
888 def splitFilesIntoBranches(self
, commit
):
891 while commit
.has_key("depotFile%s" % fnum
):
892 path
= commit
["depotFile%s" % fnum
]
893 found
= [p
for p
in self
.depotPaths
894 if path
.startswith (p
)]
901 file["rev"] = commit
["rev%s" % fnum
]
902 file["action"] = commit
["action%s" % fnum
]
903 file["type"] = commit
["type%s" % fnum
]
906 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
908 for branch
in self
.knownBranches
.keys():
910 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
911 if relPath
.startswith(branch
+ "/"):
912 if branch
not in branches
:
913 branches
[branch
] = []
914 branches
[branch
].append(file)
919 ## Should move this out, doesn't use SELF.
920 def readP4Files(self
, files
):
926 for val
in self
.clientSpecDirs
:
927 if f
['path'].startswith(val
[0]):
933 filesForCommit
.append(f
)
934 if f
['action'] != 'delete':
935 filesToRead
.append(f
)
938 if len(filesToRead
) > 0:
939 filedata
= p4CmdList('-x - print',
940 stdin
='\n'.join(['%s#%s' % (f
['path'], f
['rev'])
941 for f
in filesToRead
]),
944 if "p4ExitCode" in filedata
[0]:
945 die("Problems executing p4. Error: [%d]."
946 % (filedata
[0]['p4ExitCode']));
950 while j
< len(filedata
):
954 while j
< len(filedata
) and filedata
[j
]['code'] in ('text', 'unicode', 'binary'):
955 text
.append(filedata
[j
]['data'])
959 if not stat
.has_key('depotFile'):
960 sys
.stderr
.write("p4 print fails with: %s\n" % repr(stat
))
963 if stat
['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
964 text
= re
.sub(r
'(?i)\$(Id|Header):[^$]*\$',r
'$\1$', text
)
965 elif stat
['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
966 text
= re
.sub(r
'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r
'$\1$', text
)
968 contents
[stat
['depotFile']] = text
970 for f
in filesForCommit
:
972 if contents
.has_key(path
):
973 f
['data'] = contents
[path
]
975 return filesForCommit
977 def commit(self
, details
, files
, branch
, branchPrefixes
, parent
= ""):
978 epoch
= details
["time"]
979 author
= details
["user"]
982 print "commit into %s" % branch
984 # start with reading files; if that fails, we should not
988 if [p
for p
in branchPrefixes
if f
['path'].startswith(p
)]:
991 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % path
)
992 files
= self
.readP4Files(new_files
)
994 self
.gitStream
.write("commit %s\n" % branch
)
995 # gitStream.write("mark :%s\n" % details["change"])
996 self
.committedChanges
.add(int(details
["change"]))
998 if author
not in self
.users
:
999 self
.getUserMapFromPerforceServer()
1000 if author
in self
.users
:
1001 committer
= "%s %s %s" % (self
.users
[author
], epoch
, self
.tz
)
1003 committer
= "%s <a@b> %s %s" % (author
, epoch
, self
.tz
)
1005 self
.gitStream
.write("committer %s\n" % committer
)
1007 self
.gitStream
.write("data <<EOT\n")
1008 self
.gitStream
.write(details
["desc"])
1009 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1010 % (','.join (branchPrefixes
), details
["change"]))
1011 if len(details
['options']) > 0:
1012 self
.gitStream
.write(": options = %s" % details
['options'])
1013 self
.gitStream
.write("]\nEOT\n\n")
1017 print "parent %s" % parent
1018 self
.gitStream
.write("from %s\n" % parent
)
1021 if file["type"] == "apple":
1022 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1025 relPath
= self
.stripRepoPath(file['path'], branchPrefixes
)
1026 if file["action"] == "delete":
1027 self
.gitStream
.write("D %s\n" % relPath
)
1032 if isP4Exec(file["type"]):
1034 elif file["type"] == "symlink":
1036 # p4 print on a symlink contains "target\n", so strip it off
1039 if self
.isWindows
and file["type"].endswith("text"):
1040 data
= data
.replace("\r\n", "\n")
1042 self
.gitStream
.write("M %s inline %s\n" % (mode
, relPath
))
1043 self
.gitStream
.write("data %s\n" % len(data
))
1044 self
.gitStream
.write(data
)
1045 self
.gitStream
.write("\n")
1047 self
.gitStream
.write("\n")
1049 change
= int(details
["change"])
1051 if self
.labels
.has_key(change
):
1052 label
= self
.labels
[change
]
1053 labelDetails
= label
[0]
1054 labelRevisions
= label
[1]
1056 print "Change %s is labelled %s" % (change
, labelDetails
)
1058 files
= p4CmdList("files " + ' '.join (["%s...@%s" % (p
, change
)
1059 for p
in branchPrefixes
]))
1061 if len(files
) == len(labelRevisions
):
1065 if info
["action"] == "delete":
1067 cleanedFiles
[info
["depotFile"]] = info
["rev"]
1069 if cleanedFiles
== labelRevisions
:
1070 self
.gitStream
.write("tag tag_%s\n" % labelDetails
["label"])
1071 self
.gitStream
.write("from %s\n" % branch
)
1073 owner
= labelDetails
["Owner"]
1075 if author
in self
.users
:
1076 tagger
= "%s %s %s" % (self
.users
[owner
], epoch
, self
.tz
)
1078 tagger
= "%s <a@b> %s %s" % (owner
, epoch
, self
.tz
)
1079 self
.gitStream
.write("tagger %s\n" % tagger
)
1080 self
.gitStream
.write("data <<EOT\n")
1081 self
.gitStream
.write(labelDetails
["Description"])
1082 self
.gitStream
.write("EOT\n\n")
1086 print ("Tag %s does not match with change %s: files do not match."
1087 % (labelDetails
["label"], change
))
1091 print ("Tag %s does not match with change %s: file count is different."
1092 % (labelDetails
["label"], change
))
1094 def getUserCacheFilename(self
):
1095 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
1096 return home
+ "/.gitp4-usercache.txt"
1098 def getUserMapFromPerforceServer(self
):
1099 if self
.userMapFromPerforceServer
:
1103 for output
in p4CmdList("users"):
1104 if not output
.has_key("User"):
1106 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
1110 for (key
, val
) in self
.users
.items():
1111 s
+= "%s\t%s\n" % (key
, val
)
1113 open(self
.getUserCacheFilename(), "wb").write(s
)
1114 self
.userMapFromPerforceServer
= True
1116 def loadUserMapFromCache(self
):
1118 self
.userMapFromPerforceServer
= False
1120 cache
= open(self
.getUserCacheFilename(), "rb")
1121 lines
= cache
.readlines()
1124 entry
= line
.strip().split("\t")
1125 self
.users
[entry
[0]] = entry
[1]
1127 self
.getUserMapFromPerforceServer()
1129 def getLabels(self
):
1132 l
= p4CmdList("labels %s..." % ' '.join (self
.depotPaths
))
1133 if len(l
) > 0 and not self
.silent
:
1134 print "Finding files belonging to labels in %s" % `self
.depotPaths`
1137 label
= output
["label"]
1141 print "Querying files for label %s" % label
1142 for file in p4CmdList("files "
1143 + ' '.join (["%s...@%s" % (p
, label
)
1144 for p
in self
.depotPaths
])):
1145 revisions
[file["depotFile"]] = file["rev"]
1146 change
= int(file["change"])
1147 if change
> newestChange
:
1148 newestChange
= change
1150 self
.labels
[newestChange
] = [output
, revisions
]
1153 print "Label changes: %s" % self
.labels
.keys()
1155 def guessProjectName(self
):
1156 for p
in self
.depotPaths
:
1159 p
= p
[p
.strip().rfind("/") + 1:]
1160 if not p
.endswith("/"):
1164 def getBranchMapping(self
):
1165 lostAndFoundBranches
= set()
1167 for info
in p4CmdList("branches"):
1168 details
= p4Cmd("branch -o %s" % info
["branch"])
1170 while details
.has_key("View%s" % viewIdx
):
1171 paths
= details
["View%s" % viewIdx
].split(" ")
1172 viewIdx
= viewIdx
+ 1
1173 # require standard //depot/foo/... //depot/bar/... mapping
1174 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
1177 destination
= paths
[1]
1179 if source
.startswith(self
.depotPaths
[0]) and destination
.startswith(self
.depotPaths
[0]):
1180 source
= source
[len(self
.depotPaths
[0]):-4]
1181 destination
= destination
[len(self
.depotPaths
[0]):-4]
1183 if destination
in self
.knownBranches
:
1185 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
1186 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
1189 self
.knownBranches
[destination
] = source
1191 lostAndFoundBranches
.discard(destination
)
1193 if source
not in self
.knownBranches
:
1194 lostAndFoundBranches
.add(source
)
1197 for branch
in lostAndFoundBranches
:
1198 self
.knownBranches
[branch
] = branch
1200 def getBranchMappingFromGitBranches(self
):
1201 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1202 for branch
in branches
.keys():
1203 if branch
== "master":
1206 branch
= branch
[len(self
.projectName
):]
1207 self
.knownBranches
[branch
] = branch
1209 def listExistingP4GitBranches(self
):
1210 # branches holds mapping from name to commit
1211 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1212 self
.p4BranchesInGit
= branches
.keys()
1213 for branch
in branches
.keys():
1214 self
.initialParents
[self
.refPrefix
+ branch
] = branches
[branch
]
1216 def updateOptionDict(self
, d
):
1218 if self
.keepRepoPath
:
1219 option_keys
['keepRepoPath'] = 1
1221 d
["options"] = ' '.join(sorted(option_keys
.keys()))
1223 def readOptions(self
, d
):
1224 self
.keepRepoPath
= (d
.has_key('options')
1225 and ('keepRepoPath' in d
['options']))
1227 def gitRefForBranch(self
, branch
):
1228 if branch
== "main":
1229 return self
.refPrefix
+ "master"
1231 if len(branch
) <= 0:
1234 return self
.refPrefix
+ self
.projectName
+ branch
1236 def gitCommitByP4Change(self
, ref
, change
):
1238 print "looking in ref " + ref
+ " for change %s using bisect..." % change
1241 latestCommit
= parseRevision(ref
)
1245 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
1246 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
1251 log
= extractLogMessageFromGitCommit(next
)
1252 settings
= extractSettingsGitLog(log
)
1253 currentChange
= int(settings
['change'])
1255 print "current change %s" % currentChange
1257 if currentChange
== change
:
1259 print "found %s" % next
1262 if currentChange
< change
:
1263 earliestCommit
= "^%s" % next
1265 latestCommit
= "%s" % next
1269 def importNewBranch(self
, branch
, maxChange
):
1270 # make fast-import flush all changes to disk and update the refs using the checkpoint
1271 # command so that we can try to find the branch parent in the git history
1272 self
.gitStream
.write("checkpoint\n\n");
1273 self
.gitStream
.flush();
1274 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1275 range = "@1,%s" % maxChange
1276 #print "prefix" + branchPrefix
1277 changes
= p4ChangesForPaths([branchPrefix
], range)
1278 if len(changes
) <= 0:
1280 firstChange
= changes
[0]
1281 #print "first change in branch: %s" % firstChange
1282 sourceBranch
= self
.knownBranches
[branch
]
1283 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
1284 sourceRef
= self
.gitRefForBranch(sourceBranch
)
1285 #print "source " + sourceBranch
1287 branchParentChange
= int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath
, firstChange
))["change"])
1288 #print "branch parent: %s" % branchParentChange
1289 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
1290 if len(gitParent
) > 0:
1291 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
1292 #print "parent git commit: %s" % gitParent
1294 self
.importChanges(changes
)
1297 def importChanges(self
, changes
):
1299 for change
in changes
:
1300 description
= p4Cmd("describe %s" % change
)
1301 self
.updateOptionDict(description
)
1304 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
1309 if self
.detectBranches
:
1310 branches
= self
.splitFilesIntoBranches(description
)
1311 for branch
in branches
.keys():
1313 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1317 filesForCommit
= branches
[branch
]
1320 print "branch is %s" % branch
1322 self
.updatedBranches
.add(branch
)
1324 if branch
not in self
.createdBranches
:
1325 self
.createdBranches
.add(branch
)
1326 parent
= self
.knownBranches
[branch
]
1327 if parent
== branch
:
1330 fullBranch
= self
.projectName
+ branch
1331 if fullBranch
not in self
.p4BranchesInGit
:
1333 print("\n Importing new branch %s" % fullBranch
);
1334 if self
.importNewBranch(branch
, change
- 1):
1336 self
.p4BranchesInGit
.append(fullBranch
)
1338 print("\n Resuming with change %s" % change
);
1341 print "parent determined through known branches: %s" % parent
1343 branch
= self
.gitRefForBranch(branch
)
1344 parent
= self
.gitRefForBranch(parent
)
1347 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
1349 if len(parent
) == 0 and branch
in self
.initialParents
:
1350 parent
= self
.initialParents
[branch
]
1351 del self
.initialParents
[branch
]
1353 self
.commit(description
, filesForCommit
, branch
, [branchPrefix
], parent
)
1355 files
= self
.extractFilesFromCommit(description
)
1356 self
.commit(description
, files
, self
.branch
, self
.depotPaths
,
1358 self
.initialParent
= ""
1360 print self
.gitError
.read()
1363 def importHeadRevision(self
, revision
):
1364 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
1366 details
= { "user" : "git perforce import user", "time" : int(time
.time()) }
1367 details
["desc"] = ("Initial import of %s from the state at revision %s"
1368 % (' '.join(self
.depotPaths
), revision
))
1369 details
["change"] = revision
1373 for info
in p4CmdList("files "
1374 + ' '.join(["%s...%s"
1376 for p
in self
.depotPaths
])):
1378 if info
['code'] == 'error':
1379 sys
.stderr
.write("p4 returned an error: %s\n"
1384 change
= int(info
["change"])
1385 if change
> newestRevision
:
1386 newestRevision
= change
1388 if info
["action"] == "delete":
1389 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1390 #fileCnt = fileCnt + 1
1393 for prop
in ["depotFile", "rev", "action", "type" ]:
1394 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
1396 fileCnt
= fileCnt
+ 1
1398 details
["change"] = newestRevision
1399 self
.updateOptionDict(details
)
1401 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
, self
.depotPaths
)
1403 print "IO error with git fast-import. Is your git version recent enough?"
1404 print self
.gitError
.read()
1407 def getClientSpec(self
):
1408 specList
= p4CmdList( "client -o" )
1410 for entry
in specList
:
1411 for k
,v
in entry
.iteritems():
1412 if k
.startswith("View"):
1413 if v
.startswith('"'):
1417 index
= v
.find("...")
1419 if v
.startswith("-"):
1424 self
.clientSpecDirs
= temp
.items()
1425 self
.clientSpecDirs
.sort( lambda x
, y
: abs( y
[1] ) - abs( x
[1] ) )
1427 def run(self
, args
):
1428 self
.depotPaths
= []
1429 self
.changeRange
= ""
1430 self
.initialParent
= ""
1431 self
.previousDepotPaths
= []
1433 # map from branch depot path to parent branch
1434 self
.knownBranches
= {}
1435 self
.initialParents
= {}
1436 self
.hasOrigin
= originP4BranchesExist()
1437 if not self
.syncWithOrigin
:
1438 self
.hasOrigin
= False
1440 if self
.importIntoRemotes
:
1441 self
.refPrefix
= "refs/remotes/p4/"
1443 self
.refPrefix
= "refs/heads/p4/"
1445 if self
.syncWithOrigin
and self
.hasOrigin
:
1447 print "Syncing with origin first by calling git fetch origin"
1448 system("git fetch origin")
1450 if len(self
.branch
) == 0:
1451 self
.branch
= self
.refPrefix
+ "master"
1452 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
1453 system("git update-ref %s refs/heads/p4" % self
.branch
)
1454 system("git branch -D p4");
1455 # create it /after/ importing, when master exists
1456 if not gitBranchExists(self
.refPrefix
+ "HEAD") and self
.importIntoRemotes
and gitBranchExists(self
.branch
):
1457 system("git symbolic-ref %sHEAD %s" % (self
.refPrefix
, self
.branch
))
1459 if self
.useClientSpec
or gitConfig("git-p4.useclientspec") == "true":
1460 self
.getClientSpec()
1462 # TODO: should always look at previous commits,
1463 # merge with previous imports, if possible.
1466 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
1467 self
.listExistingP4GitBranches()
1469 if len(self
.p4BranchesInGit
) > 1:
1471 print "Importing from/into multiple branches"
1472 self
.detectBranches
= True
1475 print "branches: %s" % self
.p4BranchesInGit
1478 for branch
in self
.p4BranchesInGit
:
1479 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
1481 settings
= extractSettingsGitLog(logMsg
)
1483 self
.readOptions(settings
)
1484 if (settings
.has_key('depot-paths')
1485 and settings
.has_key ('change')):
1486 change
= int(settings
['change']) + 1
1487 p4Change
= max(p4Change
, change
)
1489 depotPaths
= sorted(settings
['depot-paths'])
1490 if self
.previousDepotPaths
== []:
1491 self
.previousDepotPaths
= depotPaths
1494 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
1495 for i
in range(0, min(len(cur
), len(prev
))):
1496 if cur
[i
] <> prev
[i
]:
1500 paths
.append (cur
[:i
+ 1])
1502 self
.previousDepotPaths
= paths
1505 self
.depotPaths
= sorted(self
.previousDepotPaths
)
1506 self
.changeRange
= "@%s,#head" % p4Change
1507 if not self
.detectBranches
:
1508 self
.initialParent
= parseRevision(self
.branch
)
1509 if not self
.silent
and not self
.detectBranches
:
1510 print "Performing incremental import into %s git branch" % self
.branch
1512 if not self
.branch
.startswith("refs/"):
1513 self
.branch
= "refs/heads/" + self
.branch
1515 if len(args
) == 0 and self
.depotPaths
:
1517 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
1519 if self
.depotPaths
and self
.depotPaths
!= args
:
1520 print ("previous import used depot path %s and now %s was specified. "
1521 "This doesn't work!" % (' '.join (self
.depotPaths
),
1525 self
.depotPaths
= sorted(args
)
1531 for p
in self
.depotPaths
:
1532 if p
.find("@") != -1:
1533 atIdx
= p
.index("@")
1534 self
.changeRange
= p
[atIdx
:]
1535 if self
.changeRange
== "@all":
1536 self
.changeRange
= ""
1537 elif ',' not in self
.changeRange
:
1538 revision
= self
.changeRange
1539 self
.changeRange
= ""
1541 elif p
.find("#") != -1:
1542 hashIdx
= p
.index("#")
1543 revision
= p
[hashIdx
:]
1545 elif self
.previousDepotPaths
== []:
1548 p
= re
.sub ("\.\.\.$", "", p
)
1549 if not p
.endswith("/"):
1554 self
.depotPaths
= newPaths
1557 self
.loadUserMapFromCache()
1559 if self
.detectLabels
:
1562 if self
.detectBranches
:
1563 ## FIXME - what's a P4 projectName ?
1564 self
.projectName
= self
.guessProjectName()
1567 self
.getBranchMappingFromGitBranches()
1569 self
.getBranchMapping()
1571 print "p4-git branches: %s" % self
.p4BranchesInGit
1572 print "initial parents: %s" % self
.initialParents
1573 for b
in self
.p4BranchesInGit
:
1577 b
= b
[len(self
.projectName
):]
1578 self
.createdBranches
.add(b
)
1580 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
1582 importProcess
= subprocess
.Popen(["git", "fast-import"],
1583 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
1584 stderr
=subprocess
.PIPE
);
1585 self
.gitOutput
= importProcess
.stdout
1586 self
.gitStream
= importProcess
.stdin
1587 self
.gitError
= importProcess
.stderr
1590 self
.importHeadRevision(revision
)
1594 if len(self
.changesFile
) > 0:
1595 output
= open(self
.changesFile
).readlines()
1598 changeSet
.add(int(line
))
1600 for change
in changeSet
:
1601 changes
.append(change
)
1606 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
1608 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
)
1610 if len(self
.maxChanges
) > 0:
1611 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
1613 if len(changes
) == 0:
1615 print "No changes to import!"
1618 if not self
.silent
and not self
.detectBranches
:
1619 print "Import destination: %s" % self
.branch
1621 self
.updatedBranches
= set()
1623 self
.importChanges(changes
)
1627 if len(self
.updatedBranches
) > 0:
1628 sys
.stdout
.write("Updated branches: ")
1629 for b
in self
.updatedBranches
:
1630 sys
.stdout
.write("%s " % b
)
1631 sys
.stdout
.write("\n")
1633 self
.gitStream
.close()
1634 if importProcess
.wait() != 0:
1635 die("fast-import failed: %s" % self
.gitError
.read())
1636 self
.gitOutput
.close()
1637 self
.gitError
.close()
1641 class P4Rebase(Command
):
1643 Command
.__init
__(self
)
1645 self
.description
= ("Fetches the latest revision from perforce and "
1646 + "rebases the current work (branch) against it")
1647 self
.verbose
= False
1649 def run(self
, args
):
1653 return self
.rebase()
1656 if os
.system("git update-index --refresh") != 0:
1657 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.");
1658 if len(read_pipe("git diff-index HEAD --")) > 0:
1659 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1661 [upstream
, settings
] = findUpstreamBranchPoint()
1662 if len(upstream
) == 0:
1663 die("Cannot find upstream branchpoint for rebase")
1665 # the branchpoint may be p4/foo~3, so strip off the parent
1666 upstream
= re
.sub("~[0-9]+$", "", upstream
)
1668 print "Rebasing the current branch onto %s" % upstream
1669 oldHead
= read_pipe("git rev-parse HEAD").strip()
1670 system("git rebase %s" % upstream
)
1671 system("git diff-tree --stat --summary -M %s HEAD" % oldHead
)
1674 class P4Clone(P4Sync
):
1676 P4Sync
.__init
__(self
)
1677 self
.description
= "Creates a new git repository and imports from Perforce into it"
1678 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
1680 optparse
.make_option("--destination", dest
="cloneDestination",
1681 action
='store', default
=None,
1682 help="where to leave result of the clone"),
1683 optparse
.make_option("-/", dest
="cloneExclude",
1684 action
="append", type="string",
1685 help="exclude depot path")
1687 self
.cloneDestination
= None
1688 self
.needsGit
= False
1690 # This is required for the "append" cloneExclude action
1691 def ensure_value(self
, attr
, value
):
1692 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
1693 setattr(self
, attr
, value
)
1694 return getattr(self
, attr
)
1696 def defaultDestination(self
, args
):
1697 ## TODO: use common prefix of args?
1699 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
1700 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
1701 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
1702 depotDir
= re
.sub(r
"/$", "", depotDir
)
1703 return os
.path
.split(depotDir
)[1]
1705 def run(self
, args
):
1709 if self
.keepRepoPath
and not self
.cloneDestination
:
1710 sys
.stderr
.write("Must specify destination for --keep-path\n")
1715 if not self
.cloneDestination
and len(depotPaths
) > 1:
1716 self
.cloneDestination
= depotPaths
[-1]
1717 depotPaths
= depotPaths
[:-1]
1719 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
1720 for p
in depotPaths
:
1721 if not p
.startswith("//"):
1724 if not self
.cloneDestination
:
1725 self
.cloneDestination
= self
.defaultDestination(args
)
1727 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
1728 if not os
.path
.exists(self
.cloneDestination
):
1729 os
.makedirs(self
.cloneDestination
)
1730 chdir(self
.cloneDestination
)
1732 self
.gitdir
= os
.getcwd() + "/.git"
1733 if not P4Sync
.run(self
, depotPaths
):
1735 if self
.branch
!= "master":
1736 if gitBranchExists("refs/remotes/p4/master"):
1737 system("git branch master refs/remotes/p4/master")
1738 system("git checkout -f")
1740 print "Could not detect main branch. No checkout/master branch created."
1744 class P4Branches(Command
):
1746 Command
.__init
__(self
)
1748 self
.description
= ("Shows the git branches that hold imports and their "
1749 + "corresponding perforce depot paths")
1750 self
.verbose
= False
1752 def run(self
, args
):
1753 if originP4BranchesExist():
1754 createOrUpdateBranchesFromOrigin()
1756 cmdline
= "git rev-parse --symbolic "
1757 cmdline
+= " --remotes"
1759 for line
in read_pipe_lines(cmdline
):
1762 if not line
.startswith('p4/') or line
== "p4/HEAD":
1766 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
1767 settings
= extractSettingsGitLog(log
)
1769 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
1772 class HelpFormatter(optparse
.IndentedHelpFormatter
):
1774 optparse
.IndentedHelpFormatter
.__init
__(self
)
1776 def format_description(self
, description
):
1778 return description
+ "\n"
1782 def printUsage(commands
):
1783 print "usage: %s <command> [options]" % sys
.argv
[0]
1785 print "valid commands: %s" % ", ".join(commands
)
1787 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
1792 "submit" : P4Submit
,
1793 "commit" : P4Submit
,
1795 "rebase" : P4Rebase
,
1797 "rollback" : P4RollBack
,
1798 "branches" : P4Branches
1803 if len(sys
.argv
[1:]) == 0:
1804 printUsage(commands
.keys())
1808 cmdName
= sys
.argv
[1]
1810 klass
= commands
[cmdName
]
1813 print "unknown command %s" % cmdName
1815 printUsage(commands
.keys())
1818 options
= cmd
.options
1819 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
1823 if len(options
) > 0:
1824 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
1826 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
1828 description
= cmd
.description
,
1829 formatter
= HelpFormatter())
1831 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
1833 verbose
= cmd
.verbose
1835 if cmd
.gitdir
== None:
1836 cmd
.gitdir
= os
.path
.abspath(".git")
1837 if not isValidGitDir(cmd
.gitdir
):
1838 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
1839 if os
.path
.exists(cmd
.gitdir
):
1840 cdup
= read_pipe("git rev-parse --show-cdup").strip()
1844 if not isValidGitDir(cmd
.gitdir
):
1845 if isValidGitDir(cmd
.gitdir
+ "/.git"):
1846 cmd
.gitdir
+= "/.git"
1848 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
1850 os
.environ
["GIT_DIR"] = cmd
.gitdir
1852 if not cmd
.run(args
):
1856 if __name__
== '__main__':