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
23 sys
.stderr
.write(msg
+ "\n")
26 def write_pipe(c
, str):
28 sys
.stderr
.write('Writing pipe: %s\n' % c
)
30 pipe
= os
.popen(c
, 'w')
33 die('Command failed: %s' % c
)
37 def read_pipe(c
, ignore_error
=False):
39 sys
.stderr
.write('Reading pipe: %s\n' % c
)
41 pipe
= os
.popen(c
, 'rb')
43 if pipe
.close() and not ignore_error
:
44 die('Command failed: %s' % c
)
49 def read_pipe_lines(c
):
51 sys
.stderr
.write('Reading pipe: %s\n' % c
)
52 ## todo: check return status
53 pipe
= os
.popen(c
, 'rb')
54 val
= pipe
.readlines()
56 die('Command failed: %s' % c
)
60 def p4_read_pipe_lines(c
):
61 """Specifically invoke p4 on the command supplied. """
62 real_cmd
= "%s %s" % ("p4", c
)
65 return read_pipe_lines(real_cmd
)
69 sys
.stderr
.write("executing %s\n" % cmd
)
70 if os
.system(cmd
) != 0:
71 die("command failed: %s" % cmd
)
74 """Determine if a Perforce 'kind' should have execute permission
76 'p4 help filetypes' gives a list of the types. If it starts with 'x',
77 or x follows one of a few letters. Otherwise, if there is an 'x' after
78 a plus sign, it is also executable"""
79 return (re
.search(r
"(^[cku]?x)|\+.*x", kind
) != None)
81 def setP4ExecBit(file, mode
):
82 # Reopens an already open file and changes the execute bit to match
83 # the execute bit setting in the passed in mode.
87 if not isModeExec(mode
):
88 p4Type
= getP4OpenedType(file)
89 p4Type
= re
.sub('^([cku]?)x(.*)', '\\1\\2', p4Type
)
90 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
94 system("p4 reopen -t %s %s" % (p4Type
, file))
96 def getP4OpenedType(file):
97 # Returns the perforce file type for the given file.
99 result
= read_pipe("p4 opened %s" % file)
100 match
= re
.match(".*\((.+)\)\r?$", result
)
102 return match
.group(1)
104 die("Could not determine file type for %s (result: '%s')" % (file, result
))
106 def diffTreePattern():
107 # This is a simple generator for the diff tree regex pattern. This could be
108 # a class variable if this and parseDiffTreeEntry were a part of a class.
109 pattern
= re
.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
113 def parseDiffTreeEntry(entry
):
114 """Parses a single diff tree entry into its component elements.
116 See git-diff-tree(1) manpage for details about the format of the diff
117 output. This method returns a dictionary with the following elements:
119 src_mode - The mode of the source file
120 dst_mode - The mode of the destination file
121 src_sha1 - The sha1 for the source file
122 dst_sha1 - The sha1 fr the destination file
123 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
124 status_score - The score for the status (applicable for 'C' and 'R'
125 statuses). This is None if there is no score.
126 src - The path for the source file.
127 dst - The path for the destination file. This is only present for
128 copy or renames. If it is not present, this is None.
130 If the pattern is not matched, None is returned."""
132 match
= diffTreePattern().next().match(entry
)
135 'src_mode': match
.group(1),
136 'dst_mode': match
.group(2),
137 'src_sha1': match
.group(3),
138 'dst_sha1': match
.group(4),
139 'status': match
.group(5),
140 'status_score': match
.group(6),
141 'src': match
.group(7),
142 'dst': match
.group(10)
146 def isModeExec(mode
):
147 # Returns True if the given git mode represents an executable file,
149 return mode
[-3:] == "755"
151 def isModeExecChanged(src_mode
, dst_mode
):
152 return isModeExec(src_mode
) != isModeExec(dst_mode
)
154 def p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b'):
155 cmd
= "p4 -G %s" % cmd
157 sys
.stderr
.write("Opening pipe: %s\n" % cmd
)
159 # Use a temporary file to avoid deadlocks without
160 # subprocess.communicate(), which would put another copy
161 # of stdout into memory.
163 if stdin
is not None:
164 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
165 stdin_file
.write(stdin
)
169 p4
= subprocess
.Popen(cmd
, shell
=True,
171 stdout
=subprocess
.PIPE
)
176 entry
= marshal
.load(p4
.stdout
)
183 entry
["p4ExitCode"] = exitCode
189 list = p4CmdList(cmd
)
195 def p4Where(depotPath
):
196 if not depotPath
.endswith("/"):
198 output
= p4Cmd("where %s..." % depotPath
)
199 if output
["code"] == "error":
203 clientPath
= output
.get("path")
204 elif "data" in output
:
205 data
= output
.get("data")
206 lastSpace
= data
.rfind(" ")
207 clientPath
= data
[lastSpace
+ 1:]
209 if clientPath
.endswith("..."):
210 clientPath
= clientPath
[:-3]
213 def currentGitBranch():
214 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
216 def isValidGitDir(path
):
217 if (os
.path
.exists(path
+ "/HEAD")
218 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
222 def parseRevision(ref
):
223 return read_pipe("git rev-parse %s" % ref
).strip()
225 def extractLogMessageFromGitCommit(commit
):
228 ## fixme: title is first line of commit, not 1st paragraph.
230 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
239 def extractSettingsGitLog(log
):
241 for line
in log
.split("\n"):
243 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
247 assignments
= m
.group(1).split (':')
248 for a
in assignments
:
250 key
= vals
[0].strip()
251 val
= ('='.join (vals
[1:])).strip()
252 if val
.endswith ('\"') and val
.startswith('"'):
257 paths
= values
.get("depot-paths")
259 paths
= values
.get("depot-path")
261 values
['depot-paths'] = paths
.split(',')
264 def gitBranchExists(branch
):
265 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
266 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
267 return proc
.wait() == 0;
270 return read_pipe("git config %s" % key
, ignore_error
=True).strip()
272 def p4BranchesInGit(branchesAreInRemotes
= True):
275 cmdline
= "git rev-parse --symbolic "
276 if branchesAreInRemotes
:
277 cmdline
+= " --remotes"
279 cmdline
+= " --branches"
281 for line
in read_pipe_lines(cmdline
):
284 ## only import to p4/
285 if not line
.startswith('p4/') or line
== "p4/HEAD":
290 branch
= re
.sub ("^p4/", "", line
)
292 branches
[branch
] = parseRevision(line
)
295 def findUpstreamBranchPoint(head
= "HEAD"):
296 branches
= p4BranchesInGit()
297 # map from depot-path to branch name
298 branchByDepotPath
= {}
299 for branch
in branches
.keys():
300 tip
= branches
[branch
]
301 log
= extractLogMessageFromGitCommit(tip
)
302 settings
= extractSettingsGitLog(log
)
303 if settings
.has_key("depot-paths"):
304 paths
= ",".join(settings
["depot-paths"])
305 branchByDepotPath
[paths
] = "remotes/p4/" + branch
309 while parent
< 65535:
310 commit
= head
+ "~%s" % parent
311 log
= extractLogMessageFromGitCommit(commit
)
312 settings
= extractSettingsGitLog(log
)
313 if settings
.has_key("depot-paths"):
314 paths
= ",".join(settings
["depot-paths"])
315 if branchByDepotPath
.has_key(paths
):
316 return [branchByDepotPath
[paths
], settings
]
320 return ["", settings
]
322 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
324 print ("Creating/updating branch(es) in %s based on origin branch(es)"
327 originPrefix
= "origin/p4/"
329 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
331 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
334 headName
= line
[len(originPrefix
):]
335 remoteHead
= localRefPrefix
+ headName
338 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
339 if (not original
.has_key('depot-paths')
340 or not original
.has_key('change')):
344 if not gitBranchExists(remoteHead
):
346 print "creating %s" % remoteHead
349 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
350 if settings
.has_key('change') > 0:
351 if settings
['depot-paths'] == original
['depot-paths']:
352 originP4Change
= int(original
['change'])
353 p4Change
= int(settings
['change'])
354 if originP4Change
> p4Change
:
355 print ("%s (%s) is newer than %s (%s). "
356 "Updating p4 branch from origin."
357 % (originHead
, originP4Change
,
358 remoteHead
, p4Change
))
361 print ("Ignoring: %s was imported from %s while "
362 "%s was imported from %s"
363 % (originHead
, ','.join(original
['depot-paths']),
364 remoteHead
, ','.join(settings
['depot-paths'])))
367 system("git update-ref %s %s" % (remoteHead
, originHead
))
369 def originP4BranchesExist():
370 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
372 def p4ChangesForPaths(depotPaths
, changeRange
):
374 output
= read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p
, changeRange
)
375 for p
in depotPaths
]))
379 changeNum
= line
.split(" ")[1]
380 changes
.append(int(changeNum
))
387 self
.usage
= "usage: %prog [options]"
390 class P4Debug(Command
):
392 Command
.__init
__(self
)
394 optparse
.make_option("--verbose", dest
="verbose", action
="store_true",
397 self
.description
= "A tool to debug the output of p4 -G."
398 self
.needsGit
= False
403 for output
in p4CmdList(" ".join(args
)):
404 print 'Element: %d' % j
409 class P4RollBack(Command
):
411 Command
.__init
__(self
)
413 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
414 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
416 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
418 self
.rollbackLocalBranches
= False
423 maxChange
= int(args
[0])
425 if "p4ExitCode" in p4Cmd("changes -m 1"):
426 die("Problems executing p4");
428 if self
.rollbackLocalBranches
:
429 refPrefix
= "refs/heads/"
430 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
432 refPrefix
= "refs/remotes/"
433 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
436 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
438 ref
= refPrefix
+ line
439 log
= extractLogMessageFromGitCommit(ref
)
440 settings
= extractSettingsGitLog(log
)
442 depotPaths
= settings
['depot-paths']
443 change
= settings
['change']
447 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
448 for p
in depotPaths
]))) == 0:
449 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
450 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
453 while change
and int(change
) > maxChange
:
456 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
457 system("git update-ref %s \"%s^\"" % (ref
, ref
))
458 log
= extractLogMessageFromGitCommit(ref
)
459 settings
= extractSettingsGitLog(log
)
462 depotPaths
= settings
['depot-paths']
463 change
= settings
['change']
466 print "%s rewound to %s" % (ref
, change
)
470 class P4Submit(Command
):
472 Command
.__init
__(self
)
474 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
475 optparse
.make_option("--origin", dest
="origin"),
476 optparse
.make_option("-M", dest
="detectRename", action
="store_true"),
478 self
.description
= "Submit changes from git to the perforce depot."
479 self
.usage
+= " [name of git branch to submit into perforce depot]"
480 self
.interactive
= True
482 self
.detectRename
= False
484 self
.isWindows
= (platform
.system() == "Windows")
487 if len(p4CmdList("opened ...")) > 0:
488 die("You have files opened with perforce! Close them before starting the sync.")
490 # replaces everything between 'Description:' and the next P4 submit template field with the
492 def prepareLogMessage(self
, template
, message
):
495 inDescriptionSection
= False
497 for line
in template
.split("\n"):
498 if line
.startswith("#"):
499 result
+= line
+ "\n"
502 if inDescriptionSection
:
503 if line
.startswith("Files:"):
504 inDescriptionSection
= False
508 if line
.startswith("Description:"):
509 inDescriptionSection
= True
511 for messageLine
in message
.split("\n"):
512 line
+= "\t" + messageLine
+ "\n"
514 result
+= line
+ "\n"
518 def prepareSubmitTemplate(self
):
519 # remove lines in the Files section that show changes to files outside the depot path we're committing into
521 inFilesSection
= False
522 for line
in read_pipe_lines("p4 change -o"):
523 if line
.endswith("\r\n"):
524 line
= line
[:-2] + "\n"
526 if line
.startswith("\t"):
527 # path starts and ends with a tab
529 lastTab
= path
.rfind("\t")
531 path
= path
[:lastTab
]
532 if not path
.startswith(self
.depotPath
):
535 inFilesSection
= False
537 if line
.startswith("Files:"):
538 inFilesSection
= True
544 def applyCommit(self
, id):
545 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
546 diffOpts
= ("", "-M")[self
.detectRename
]
547 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts
, id, id))
549 filesToDelete
= set()
551 filesToChangeExecBit
= {}
553 diff
= parseDiffTreeEntry(line
)
554 modifier
= diff
['status']
557 system("p4 edit \"%s\"" % path
)
558 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
559 filesToChangeExecBit
[path
] = diff
['dst_mode']
560 editedFiles
.add(path
)
561 elif modifier
== "A":
563 filesToChangeExecBit
[path
] = diff
['dst_mode']
564 if path
in filesToDelete
:
565 filesToDelete
.remove(path
)
566 elif modifier
== "D":
567 filesToDelete
.add(path
)
568 if path
in filesToAdd
:
569 filesToAdd
.remove(path
)
570 elif modifier
== "R":
571 src
, dest
= diff
['src'], diff
['dst']
572 system("p4 integrate -Dt \"%s\" \"%s\"" % (src
, dest
))
573 system("p4 edit \"%s\"" % (dest
))
574 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
575 filesToChangeExecBit
[dest
] = diff
['dst_mode']
577 editedFiles
.add(dest
)
578 filesToDelete
.add(src
)
580 die("unknown modifier %s for %s" % (modifier
, path
))
582 diffcmd
= "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
583 patchcmd
= diffcmd
+ " | git apply "
584 tryPatchCmd
= patchcmd
+ "--check -"
585 applyPatchCmd
= patchcmd
+ "--check --apply -"
587 if os
.system(tryPatchCmd
) != 0:
588 print "Unfortunately applying the change failed!"
589 print "What do you want to do?"
591 while response
!= "s" and response
!= "a" and response
!= "w":
592 response
= raw_input("[s]kip this patch / [a]pply the patch forcibly "
593 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
595 print "Skipping! Good luck with the next patches..."
596 for f
in editedFiles
:
597 system("p4 revert \"%s\"" % f
);
601 elif response
== "a":
602 os
.system(applyPatchCmd
)
603 if len(filesToAdd
) > 0:
604 print "You may also want to call p4 add on the following files:"
605 print " ".join(filesToAdd
)
606 if len(filesToDelete
):
607 print "The following files should be scheduled for deletion with p4 delete:"
608 print " ".join(filesToDelete
)
609 die("Please resolve and submit the conflict manually and "
610 + "continue afterwards with git-p4 submit --continue")
611 elif response
== "w":
612 system(diffcmd
+ " > patch.txt")
613 print "Patch saved to patch.txt in %s !" % self
.clientPath
614 die("Please resolve and submit the conflict manually and "
615 "continue afterwards with git-p4 submit --continue")
617 system(applyPatchCmd
)
620 system("p4 add \"%s\"" % f
)
621 for f
in filesToDelete
:
622 system("p4 revert \"%s\"" % f
)
623 system("p4 delete \"%s\"" % f
)
625 # Set/clear executable bits
626 for f
in filesToChangeExecBit
.keys():
627 mode
= filesToChangeExecBit
[f
]
628 setP4ExecBit(f
, mode
)
630 logMessage
= extractLogMessageFromGitCommit(id)
631 logMessage
= logMessage
.strip()
633 template
= self
.prepareSubmitTemplate()
636 submitTemplate
= self
.prepareLogMessage(template
, logMessage
)
637 if os
.environ
.has_key("P4DIFF"):
638 del(os
.environ
["P4DIFF"])
639 diff
= read_pipe("p4 diff -du ...")
642 for newFile
in filesToAdd
:
643 newdiff
+= "==== new file ====\n"
644 newdiff
+= "--- /dev/null\n"
645 newdiff
+= "+++ %s\n" % newFile
646 f
= open(newFile
, "r")
647 for line
in f
.readlines():
648 newdiff
+= "+" + line
651 separatorLine
= "######## everything below this line is just the diff #######\n"
653 [handle
, fileName
] = tempfile
.mkstemp()
654 tmpFile
= os
.fdopen(handle
, "w+")
656 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
657 separatorLine
= separatorLine
.replace("\n", "\r\n")
658 newdiff
= newdiff
.replace("\n", "\r\n")
659 tmpFile
.write(submitTemplate
+ separatorLine
+ diff
+ newdiff
)
662 if platform
.system() == "Windows":
663 defaultEditor
= "notepad"
664 if os
.environ
.has_key("P4EDITOR"):
665 editor
= os
.environ
.get("P4EDITOR")
667 editor
= os
.environ
.get("EDITOR", defaultEditor
);
668 system(editor
+ " " + fileName
)
669 tmpFile
= open(fileName
, "rb")
670 message
= tmpFile
.read()
673 submitTemplate
= message
[:message
.index(separatorLine
)]
675 submitTemplate
= submitTemplate
.replace("\r\n", "\n")
677 write_pipe("p4 submit -i", submitTemplate
)
679 fileName
= "submit.txt"
680 file = open(fileName
, "w+")
681 file.write(self
.prepareLogMessage(template
, logMessage
))
683 print ("Perforce submit template written as %s. "
684 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
685 % (fileName
, fileName
))
689 self
.master
= currentGitBranch()
690 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
691 die("Detecting current git branch failed!")
693 self
.master
= args
[0]
697 allowSubmit
= gitConfig("git-p4.allowSubmit")
698 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
699 die("%s is not in git-p4.allowSubmit" % self
.master
)
701 [upstream
, settings
] = findUpstreamBranchPoint()
702 self
.depotPath
= settings
['depot-paths'][0]
703 if len(self
.origin
) == 0:
704 self
.origin
= upstream
707 print "Origin branch is " + self
.origin
709 if len(self
.depotPath
) == 0:
710 print "Internal error: cannot locate perforce depot path from existing branches"
713 self
.clientPath
= p4Where(self
.depotPath
)
715 if len(self
.clientPath
) == 0:
716 print "Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
719 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
720 self
.oldWorkingDirectory
= os
.getcwd()
722 os
.chdir(self
.clientPath
)
723 print "Syncronizing p4 checkout..."
724 system("p4 sync ...")
729 for line
in read_pipe_lines("git rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)):
730 commits
.append(line
.strip())
733 while len(commits
) > 0:
735 commits
= commits
[1:]
736 self
.applyCommit(commit
)
737 if not self
.interactive
:
740 if len(commits
) == 0:
741 print "All changes applied!"
742 os
.chdir(self
.oldWorkingDirectory
)
752 class P4Sync(Command
):
754 Command
.__init
__(self
)
756 optparse
.make_option("--branch", dest
="branch"),
757 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
758 optparse
.make_option("--changesfile", dest
="changesFile"),
759 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
760 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
761 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
762 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
763 help="Import into refs/heads/ , not refs/remotes"),
764 optparse
.make_option("--max-changes", dest
="maxChanges"),
765 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
766 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
767 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
768 help="Only sync files that are included in the Perforce Client Spec")
770 self
.description
= """Imports from Perforce into a git repository.\n
772 //depot/my/project/ -- to import the current head
773 //depot/my/project/@all -- to import everything
774 //depot/my/project/@1,6 -- to import only from revision 1 to 6
776 (a ... is not needed in the path p4 specification, it's added implicitly)"""
778 self
.usage
+= " //depot/path[@revRange]"
780 self
.createdBranches
= Set()
781 self
.committedChanges
= Set()
783 self
.detectBranches
= False
784 self
.detectLabels
= False
785 self
.changesFile
= ""
786 self
.syncWithOrigin
= True
788 self
.importIntoRemotes
= True
790 self
.isWindows
= (platform
.system() == "Windows")
791 self
.keepRepoPath
= False
792 self
.depotPaths
= None
793 self
.p4BranchesInGit
= []
794 self
.cloneExclude
= []
795 self
.useClientSpec
= False
796 self
.clientSpecDirs
= []
798 if gitConfig("git-p4.syncFromOrigin") == "false":
799 self
.syncWithOrigin
= False
801 def extractFilesFromCommit(self
, commit
):
802 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
803 for path
in self
.cloneExclude
]
806 while commit
.has_key("depotFile%s" % fnum
):
807 path
= commit
["depotFile%s" % fnum
]
809 if [p
for p
in self
.cloneExclude
810 if path
.startswith (p
)]:
813 found
= [p
for p
in self
.depotPaths
814 if path
.startswith (p
)]
821 file["rev"] = commit
["rev%s" % fnum
]
822 file["action"] = commit
["action%s" % fnum
]
823 file["type"] = commit
["type%s" % fnum
]
828 def stripRepoPath(self
, path
, prefixes
):
829 if self
.keepRepoPath
:
830 prefixes
= [re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])]
833 if path
.startswith(p
):
838 def splitFilesIntoBranches(self
, commit
):
841 while commit
.has_key("depotFile%s" % fnum
):
842 path
= commit
["depotFile%s" % fnum
]
843 found
= [p
for p
in self
.depotPaths
844 if path
.startswith (p
)]
851 file["rev"] = commit
["rev%s" % fnum
]
852 file["action"] = commit
["action%s" % fnum
]
853 file["type"] = commit
["type%s" % fnum
]
856 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
858 for branch
in self
.knownBranches
.keys():
860 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
861 if relPath
.startswith(branch
+ "/"):
862 if branch
not in branches
:
863 branches
[branch
] = []
864 branches
[branch
].append(file)
869 ## Should move this out, doesn't use SELF.
870 def readP4Files(self
, files
):
876 for val
in self
.clientSpecDirs
:
877 if f
['path'].startswith(val
[0]):
883 filesForCommit
.append(f
)
884 if f
['action'] != 'delete':
885 filesToRead
.append(f
)
888 if len(filesToRead
) > 0:
889 filedata
= p4CmdList('-x - print',
890 stdin
='\n'.join(['%s#%s' % (f
['path'], f
['rev'])
891 for f
in filesToRead
]),
894 if "p4ExitCode" in filedata
[0]:
895 die("Problems executing p4. Error: [%d]."
896 % (filedata
[0]['p4ExitCode']));
900 while j
< len(filedata
):
904 while j
< len(filedata
) and filedata
[j
]['code'] in ('text', 'unicode', 'binary'):
905 text
.append(filedata
[j
]['data'])
909 if not stat
.has_key('depotFile'):
910 sys
.stderr
.write("p4 print fails with: %s\n" % repr(stat
))
913 if stat
['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
914 text
= re
.sub(r
'(?i)\$(Id|Header):[^$]*\$',r
'$\1$', text
)
915 elif stat
['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
916 text
= re
.sub(r
'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r
'$\1$', text
)
918 contents
[stat
['depotFile']] = text
920 for f
in filesForCommit
:
922 if contents
.has_key(path
):
923 f
['data'] = contents
[path
]
925 return filesForCommit
927 def commit(self
, details
, files
, branch
, branchPrefixes
, parent
= ""):
928 epoch
= details
["time"]
929 author
= details
["user"]
932 print "commit into %s" % branch
934 # start with reading files; if that fails, we should not
938 if [p
for p
in branchPrefixes
if f
['path'].startswith(p
)]:
941 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % path
)
942 files
= self
.readP4Files(new_files
)
944 self
.gitStream
.write("commit %s\n" % branch
)
945 # gitStream.write("mark :%s\n" % details["change"])
946 self
.committedChanges
.add(int(details
["change"]))
948 if author
not in self
.users
:
949 self
.getUserMapFromPerforceServer()
950 if author
in self
.users
:
951 committer
= "%s %s %s" % (self
.users
[author
], epoch
, self
.tz
)
953 committer
= "%s <a@b> %s %s" % (author
, epoch
, self
.tz
)
955 self
.gitStream
.write("committer %s\n" % committer
)
957 self
.gitStream
.write("data <<EOT\n")
958 self
.gitStream
.write(details
["desc"])
959 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s"
960 % (','.join (branchPrefixes
), details
["change"]))
961 if len(details
['options']) > 0:
962 self
.gitStream
.write(": options = %s" % details
['options'])
963 self
.gitStream
.write("]\nEOT\n\n")
967 print "parent %s" % parent
968 self
.gitStream
.write("from %s\n" % parent
)
971 if file["type"] == "apple":
972 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
975 relPath
= self
.stripRepoPath(file['path'], branchPrefixes
)
976 if file["action"] == "delete":
977 self
.gitStream
.write("D %s\n" % relPath
)
982 if isP4Exec(file["type"]):
984 elif file["type"] == "symlink":
986 # p4 print on a symlink contains "target\n", so strip it off
989 if self
.isWindows
and file["type"].endswith("text"):
990 data
= data
.replace("\r\n", "\n")
992 self
.gitStream
.write("M %s inline %s\n" % (mode
, relPath
))
993 self
.gitStream
.write("data %s\n" % len(data
))
994 self
.gitStream
.write(data
)
995 self
.gitStream
.write("\n")
997 self
.gitStream
.write("\n")
999 change
= int(details
["change"])
1001 if self
.labels
.has_key(change
):
1002 label
= self
.labels
[change
]
1003 labelDetails
= label
[0]
1004 labelRevisions
= label
[1]
1006 print "Change %s is labelled %s" % (change
, labelDetails
)
1008 files
= p4CmdList("files " + ' '.join (["%s...@%s" % (p
, change
)
1009 for p
in branchPrefixes
]))
1011 if len(files
) == len(labelRevisions
):
1015 if info
["action"] == "delete":
1017 cleanedFiles
[info
["depotFile"]] = info
["rev"]
1019 if cleanedFiles
== labelRevisions
:
1020 self
.gitStream
.write("tag tag_%s\n" % labelDetails
["label"])
1021 self
.gitStream
.write("from %s\n" % branch
)
1023 owner
= labelDetails
["Owner"]
1025 if author
in self
.users
:
1026 tagger
= "%s %s %s" % (self
.users
[owner
], epoch
, self
.tz
)
1028 tagger
= "%s <a@b> %s %s" % (owner
, epoch
, self
.tz
)
1029 self
.gitStream
.write("tagger %s\n" % tagger
)
1030 self
.gitStream
.write("data <<EOT\n")
1031 self
.gitStream
.write(labelDetails
["Description"])
1032 self
.gitStream
.write("EOT\n\n")
1036 print ("Tag %s does not match with change %s: files do not match."
1037 % (labelDetails
["label"], change
))
1041 print ("Tag %s does not match with change %s: file count is different."
1042 % (labelDetails
["label"], change
))
1044 def getUserCacheFilename(self
):
1045 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
1046 return home
+ "/.gitp4-usercache.txt"
1048 def getUserMapFromPerforceServer(self
):
1049 if self
.userMapFromPerforceServer
:
1053 for output
in p4CmdList("users"):
1054 if not output
.has_key("User"):
1056 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
1060 for (key
, val
) in self
.users
.items():
1061 s
+= "%s\t%s\n" % (key
, val
)
1063 open(self
.getUserCacheFilename(), "wb").write(s
)
1064 self
.userMapFromPerforceServer
= True
1066 def loadUserMapFromCache(self
):
1068 self
.userMapFromPerforceServer
= False
1070 cache
= open(self
.getUserCacheFilename(), "rb")
1071 lines
= cache
.readlines()
1074 entry
= line
.strip().split("\t")
1075 self
.users
[entry
[0]] = entry
[1]
1077 self
.getUserMapFromPerforceServer()
1079 def getLabels(self
):
1082 l
= p4CmdList("labels %s..." % ' '.join (self
.depotPaths
))
1083 if len(l
) > 0 and not self
.silent
:
1084 print "Finding files belonging to labels in %s" % `self
.depotPaths`
1087 label
= output
["label"]
1091 print "Querying files for label %s" % label
1092 for file in p4CmdList("files "
1093 + ' '.join (["%s...@%s" % (p
, label
)
1094 for p
in self
.depotPaths
])):
1095 revisions
[file["depotFile"]] = file["rev"]
1096 change
= int(file["change"])
1097 if change
> newestChange
:
1098 newestChange
= change
1100 self
.labels
[newestChange
] = [output
, revisions
]
1103 print "Label changes: %s" % self
.labels
.keys()
1105 def guessProjectName(self
):
1106 for p
in self
.depotPaths
:
1109 p
= p
[p
.strip().rfind("/") + 1:]
1110 if not p
.endswith("/"):
1114 def getBranchMapping(self
):
1115 lostAndFoundBranches
= set()
1117 for info
in p4CmdList("branches"):
1118 details
= p4Cmd("branch -o %s" % info
["branch"])
1120 while details
.has_key("View%s" % viewIdx
):
1121 paths
= details
["View%s" % viewIdx
].split(" ")
1122 viewIdx
= viewIdx
+ 1
1123 # require standard //depot/foo/... //depot/bar/... mapping
1124 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
1127 destination
= paths
[1]
1129 if source
.startswith(self
.depotPaths
[0]) and destination
.startswith(self
.depotPaths
[0]):
1130 source
= source
[len(self
.depotPaths
[0]):-4]
1131 destination
= destination
[len(self
.depotPaths
[0]):-4]
1133 if destination
in self
.knownBranches
:
1135 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
1136 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
1139 self
.knownBranches
[destination
] = source
1141 lostAndFoundBranches
.discard(destination
)
1143 if source
not in self
.knownBranches
:
1144 lostAndFoundBranches
.add(source
)
1147 for branch
in lostAndFoundBranches
:
1148 self
.knownBranches
[branch
] = branch
1150 def getBranchMappingFromGitBranches(self
):
1151 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1152 for branch
in branches
.keys():
1153 if branch
== "master":
1156 branch
= branch
[len(self
.projectName
):]
1157 self
.knownBranches
[branch
] = branch
1159 def listExistingP4GitBranches(self
):
1160 # branches holds mapping from name to commit
1161 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1162 self
.p4BranchesInGit
= branches
.keys()
1163 for branch
in branches
.keys():
1164 self
.initialParents
[self
.refPrefix
+ branch
] = branches
[branch
]
1166 def updateOptionDict(self
, d
):
1168 if self
.keepRepoPath
:
1169 option_keys
['keepRepoPath'] = 1
1171 d
["options"] = ' '.join(sorted(option_keys
.keys()))
1173 def readOptions(self
, d
):
1174 self
.keepRepoPath
= (d
.has_key('options')
1175 and ('keepRepoPath' in d
['options']))
1177 def gitRefForBranch(self
, branch
):
1178 if branch
== "main":
1179 return self
.refPrefix
+ "master"
1181 if len(branch
) <= 0:
1184 return self
.refPrefix
+ self
.projectName
+ branch
1186 def gitCommitByP4Change(self
, ref
, change
):
1188 print "looking in ref " + ref
+ " for change %s using bisect..." % change
1191 latestCommit
= parseRevision(ref
)
1195 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
1196 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
1201 log
= extractLogMessageFromGitCommit(next
)
1202 settings
= extractSettingsGitLog(log
)
1203 currentChange
= int(settings
['change'])
1205 print "current change %s" % currentChange
1207 if currentChange
== change
:
1209 print "found %s" % next
1212 if currentChange
< change
:
1213 earliestCommit
= "^%s" % next
1215 latestCommit
= "%s" % next
1219 def importNewBranch(self
, branch
, maxChange
):
1220 # make fast-import flush all changes to disk and update the refs using the checkpoint
1221 # command so that we can try to find the branch parent in the git history
1222 self
.gitStream
.write("checkpoint\n\n");
1223 self
.gitStream
.flush();
1224 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1225 range = "@1,%s" % maxChange
1226 #print "prefix" + branchPrefix
1227 changes
= p4ChangesForPaths([branchPrefix
], range)
1228 if len(changes
) <= 0:
1230 firstChange
= changes
[0]
1231 #print "first change in branch: %s" % firstChange
1232 sourceBranch
= self
.knownBranches
[branch
]
1233 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
1234 sourceRef
= self
.gitRefForBranch(sourceBranch
)
1235 #print "source " + sourceBranch
1237 branchParentChange
= int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath
, firstChange
))["change"])
1238 #print "branch parent: %s" % branchParentChange
1239 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
1240 if len(gitParent
) > 0:
1241 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
1242 #print "parent git commit: %s" % gitParent
1244 self
.importChanges(changes
)
1247 def importChanges(self
, changes
):
1249 for change
in changes
:
1250 description
= p4Cmd("describe %s" % change
)
1251 self
.updateOptionDict(description
)
1254 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
1259 if self
.detectBranches
:
1260 branches
= self
.splitFilesIntoBranches(description
)
1261 for branch
in branches
.keys():
1263 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1267 filesForCommit
= branches
[branch
]
1270 print "branch is %s" % branch
1272 self
.updatedBranches
.add(branch
)
1274 if branch
not in self
.createdBranches
:
1275 self
.createdBranches
.add(branch
)
1276 parent
= self
.knownBranches
[branch
]
1277 if parent
== branch
:
1280 fullBranch
= self
.projectName
+ branch
1281 if fullBranch
not in self
.p4BranchesInGit
:
1283 print("\n Importing new branch %s" % fullBranch
);
1284 if self
.importNewBranch(branch
, change
- 1):
1286 self
.p4BranchesInGit
.append(fullBranch
)
1288 print("\n Resuming with change %s" % change
);
1291 print "parent determined through known branches: %s" % parent
1293 branch
= self
.gitRefForBranch(branch
)
1294 parent
= self
.gitRefForBranch(parent
)
1297 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
1299 if len(parent
) == 0 and branch
in self
.initialParents
:
1300 parent
= self
.initialParents
[branch
]
1301 del self
.initialParents
[branch
]
1303 self
.commit(description
, filesForCommit
, branch
, [branchPrefix
], parent
)
1305 files
= self
.extractFilesFromCommit(description
)
1306 self
.commit(description
, files
, self
.branch
, self
.depotPaths
,
1308 self
.initialParent
= ""
1310 print self
.gitError
.read()
1313 def importHeadRevision(self
, revision
):
1314 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
1316 details
= { "user" : "git perforce import user", "time" : int(time
.time()) }
1317 details
["desc"] = ("Initial import of %s from the state at revision %s"
1318 % (' '.join(self
.depotPaths
), revision
))
1319 details
["change"] = revision
1323 for info
in p4CmdList("files "
1324 + ' '.join(["%s...%s"
1326 for p
in self
.depotPaths
])):
1328 if info
['code'] == 'error':
1329 sys
.stderr
.write("p4 returned an error: %s\n"
1334 change
= int(info
["change"])
1335 if change
> newestRevision
:
1336 newestRevision
= change
1338 if info
["action"] == "delete":
1339 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1340 #fileCnt = fileCnt + 1
1343 for prop
in ["depotFile", "rev", "action", "type" ]:
1344 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
1346 fileCnt
= fileCnt
+ 1
1348 details
["change"] = newestRevision
1349 self
.updateOptionDict(details
)
1351 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
, self
.depotPaths
)
1353 print "IO error with git fast-import. Is your git version recent enough?"
1354 print self
.gitError
.read()
1357 def getClientSpec(self
):
1358 specList
= p4CmdList( "client -o" )
1360 for entry
in specList
:
1361 for k
,v
in entry
.iteritems():
1362 if k
.startswith("View"):
1363 if v
.startswith('"'):
1367 index
= v
.find("...")
1369 if v
.startswith("-"):
1374 self
.clientSpecDirs
= temp
.items()
1375 self
.clientSpecDirs
.sort( lambda x
, y
: abs( y
[1] ) - abs( x
[1] ) )
1377 def run(self
, args
):
1378 self
.depotPaths
= []
1379 self
.changeRange
= ""
1380 self
.initialParent
= ""
1381 self
.previousDepotPaths
= []
1383 # map from branch depot path to parent branch
1384 self
.knownBranches
= {}
1385 self
.initialParents
= {}
1386 self
.hasOrigin
= originP4BranchesExist()
1387 if not self
.syncWithOrigin
:
1388 self
.hasOrigin
= False
1390 if self
.importIntoRemotes
:
1391 self
.refPrefix
= "refs/remotes/p4/"
1393 self
.refPrefix
= "refs/heads/p4/"
1395 if self
.syncWithOrigin
and self
.hasOrigin
:
1397 print "Syncing with origin first by calling git fetch origin"
1398 system("git fetch origin")
1400 if len(self
.branch
) == 0:
1401 self
.branch
= self
.refPrefix
+ "master"
1402 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
1403 system("git update-ref %s refs/heads/p4" % self
.branch
)
1404 system("git branch -D p4");
1405 # create it /after/ importing, when master exists
1406 if not gitBranchExists(self
.refPrefix
+ "HEAD") and self
.importIntoRemotes
and gitBranchExists(self
.branch
):
1407 system("git symbolic-ref %sHEAD %s" % (self
.refPrefix
, self
.branch
))
1409 if self
.useClientSpec
or gitConfig("p4.useclientspec") == "true":
1410 self
.getClientSpec()
1412 # TODO: should always look at previous commits,
1413 # merge with previous imports, if possible.
1416 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
1417 self
.listExistingP4GitBranches()
1419 if len(self
.p4BranchesInGit
) > 1:
1421 print "Importing from/into multiple branches"
1422 self
.detectBranches
= True
1425 print "branches: %s" % self
.p4BranchesInGit
1428 for branch
in self
.p4BranchesInGit
:
1429 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
1431 settings
= extractSettingsGitLog(logMsg
)
1433 self
.readOptions(settings
)
1434 if (settings
.has_key('depot-paths')
1435 and settings
.has_key ('change')):
1436 change
= int(settings
['change']) + 1
1437 p4Change
= max(p4Change
, change
)
1439 depotPaths
= sorted(settings
['depot-paths'])
1440 if self
.previousDepotPaths
== []:
1441 self
.previousDepotPaths
= depotPaths
1444 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
1445 for i
in range(0, min(len(cur
), len(prev
))):
1446 if cur
[i
] <> prev
[i
]:
1450 paths
.append (cur
[:i
+ 1])
1452 self
.previousDepotPaths
= paths
1455 self
.depotPaths
= sorted(self
.previousDepotPaths
)
1456 self
.changeRange
= "@%s,#head" % p4Change
1457 if not self
.detectBranches
:
1458 self
.initialParent
= parseRevision(self
.branch
)
1459 if not self
.silent
and not self
.detectBranches
:
1460 print "Performing incremental import into %s git branch" % self
.branch
1462 if not self
.branch
.startswith("refs/"):
1463 self
.branch
= "refs/heads/" + self
.branch
1465 if len(args
) == 0 and self
.depotPaths
:
1467 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
1469 if self
.depotPaths
and self
.depotPaths
!= args
:
1470 print ("previous import used depot path %s and now %s was specified. "
1471 "This doesn't work!" % (' '.join (self
.depotPaths
),
1475 self
.depotPaths
= sorted(args
)
1481 for p
in self
.depotPaths
:
1482 if p
.find("@") != -1:
1483 atIdx
= p
.index("@")
1484 self
.changeRange
= p
[atIdx
:]
1485 if self
.changeRange
== "@all":
1486 self
.changeRange
= ""
1487 elif ',' not in self
.changeRange
:
1488 revision
= self
.changeRange
1489 self
.changeRange
= ""
1491 elif p
.find("#") != -1:
1492 hashIdx
= p
.index("#")
1493 revision
= p
[hashIdx
:]
1495 elif self
.previousDepotPaths
== []:
1498 p
= re
.sub ("\.\.\.$", "", p
)
1499 if not p
.endswith("/"):
1504 self
.depotPaths
= newPaths
1507 self
.loadUserMapFromCache()
1509 if self
.detectLabels
:
1512 if self
.detectBranches
:
1513 ## FIXME - what's a P4 projectName ?
1514 self
.projectName
= self
.guessProjectName()
1517 self
.getBranchMappingFromGitBranches()
1519 self
.getBranchMapping()
1521 print "p4-git branches: %s" % self
.p4BranchesInGit
1522 print "initial parents: %s" % self
.initialParents
1523 for b
in self
.p4BranchesInGit
:
1527 b
= b
[len(self
.projectName
):]
1528 self
.createdBranches
.add(b
)
1530 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
1532 importProcess
= subprocess
.Popen(["git", "fast-import"],
1533 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
1534 stderr
=subprocess
.PIPE
);
1535 self
.gitOutput
= importProcess
.stdout
1536 self
.gitStream
= importProcess
.stdin
1537 self
.gitError
= importProcess
.stderr
1540 self
.importHeadRevision(revision
)
1544 if len(self
.changesFile
) > 0:
1545 output
= open(self
.changesFile
).readlines()
1548 changeSet
.add(int(line
))
1550 for change
in changeSet
:
1551 changes
.append(change
)
1556 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
1558 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
)
1560 if len(self
.maxChanges
) > 0:
1561 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
1563 if len(changes
) == 0:
1565 print "No changes to import!"
1568 if not self
.silent
and not self
.detectBranches
:
1569 print "Import destination: %s" % self
.branch
1571 self
.updatedBranches
= set()
1573 self
.importChanges(changes
)
1577 if len(self
.updatedBranches
) > 0:
1578 sys
.stdout
.write("Updated branches: ")
1579 for b
in self
.updatedBranches
:
1580 sys
.stdout
.write("%s " % b
)
1581 sys
.stdout
.write("\n")
1583 self
.gitStream
.close()
1584 if importProcess
.wait() != 0:
1585 die("fast-import failed: %s" % self
.gitError
.read())
1586 self
.gitOutput
.close()
1587 self
.gitError
.close()
1591 class P4Rebase(Command
):
1593 Command
.__init
__(self
)
1595 self
.description
= ("Fetches the latest revision from perforce and "
1596 + "rebases the current work (branch) against it")
1597 self
.verbose
= False
1599 def run(self
, args
):
1603 return self
.rebase()
1606 if os
.system("git update-index --refresh") != 0:
1607 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.");
1608 if len(read_pipe("git diff-index HEAD --")) > 0:
1609 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1611 [upstream
, settings
] = findUpstreamBranchPoint()
1612 if len(upstream
) == 0:
1613 die("Cannot find upstream branchpoint for rebase")
1615 # the branchpoint may be p4/foo~3, so strip off the parent
1616 upstream
= re
.sub("~[0-9]+$", "", upstream
)
1618 print "Rebasing the current branch onto %s" % upstream
1619 oldHead
= read_pipe("git rev-parse HEAD").strip()
1620 system("git rebase %s" % upstream
)
1621 system("git diff-tree --stat --summary -M %s HEAD" % oldHead
)
1624 class P4Clone(P4Sync
):
1626 P4Sync
.__init
__(self
)
1627 self
.description
= "Creates a new git repository and imports from Perforce into it"
1628 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
1630 optparse
.make_option("--destination", dest
="cloneDestination",
1631 action
='store', default
=None,
1632 help="where to leave result of the clone"),
1633 optparse
.make_option("-/", dest
="cloneExclude",
1634 action
="append", type="string",
1635 help="exclude depot path")
1637 self
.cloneDestination
= None
1638 self
.needsGit
= False
1640 # This is required for the "append" cloneExclude action
1641 def ensure_value(self
, attr
, value
):
1642 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
1643 setattr(self
, attr
, value
)
1644 return getattr(self
, attr
)
1646 def defaultDestination(self
, args
):
1647 ## TODO: use common prefix of args?
1649 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
1650 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
1651 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
1652 depotDir
= re
.sub(r
"/$", "", depotDir
)
1653 return os
.path
.split(depotDir
)[1]
1655 def run(self
, args
):
1659 if self
.keepRepoPath
and not self
.cloneDestination
:
1660 sys
.stderr
.write("Must specify destination for --keep-path\n")
1665 if not self
.cloneDestination
and len(depotPaths
) > 1:
1666 self
.cloneDestination
= depotPaths
[-1]
1667 depotPaths
= depotPaths
[:-1]
1669 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
1670 for p
in depotPaths
:
1671 if not p
.startswith("//"):
1674 if not self
.cloneDestination
:
1675 self
.cloneDestination
= self
.defaultDestination(args
)
1677 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
1678 if not os
.path
.exists(self
.cloneDestination
):
1679 os
.makedirs(self
.cloneDestination
)
1680 os
.chdir(self
.cloneDestination
)
1682 self
.gitdir
= os
.getcwd() + "/.git"
1683 if not P4Sync
.run(self
, depotPaths
):
1685 if self
.branch
!= "master":
1686 if gitBranchExists("refs/remotes/p4/master"):
1687 system("git branch master refs/remotes/p4/master")
1688 system("git checkout -f")
1690 print "Could not detect main branch. No checkout/master branch created."
1694 class P4Branches(Command
):
1696 Command
.__init
__(self
)
1698 self
.description
= ("Shows the git branches that hold imports and their "
1699 + "corresponding perforce depot paths")
1700 self
.verbose
= False
1702 def run(self
, args
):
1703 if originP4BranchesExist():
1704 createOrUpdateBranchesFromOrigin()
1706 cmdline
= "git rev-parse --symbolic "
1707 cmdline
+= " --remotes"
1709 for line
in read_pipe_lines(cmdline
):
1712 if not line
.startswith('p4/') or line
== "p4/HEAD":
1716 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
1717 settings
= extractSettingsGitLog(log
)
1719 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
1722 class HelpFormatter(optparse
.IndentedHelpFormatter
):
1724 optparse
.IndentedHelpFormatter
.__init
__(self
)
1726 def format_description(self
, description
):
1728 return description
+ "\n"
1732 def printUsage(commands
):
1733 print "usage: %s <command> [options]" % sys
.argv
[0]
1735 print "valid commands: %s" % ", ".join(commands
)
1737 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
1742 "submit" : P4Submit
,
1743 "commit" : P4Submit
,
1745 "rebase" : P4Rebase
,
1747 "rollback" : P4RollBack
,
1748 "branches" : P4Branches
1753 if len(sys
.argv
[1:]) == 0:
1754 printUsage(commands
.keys())
1758 cmdName
= sys
.argv
[1]
1760 klass
= commands
[cmdName
]
1763 print "unknown command %s" % cmdName
1765 printUsage(commands
.keys())
1768 options
= cmd
.options
1769 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
1773 if len(options
) > 0:
1774 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
1776 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
1778 description
= cmd
.description
,
1779 formatter
= HelpFormatter())
1781 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
1783 verbose
= cmd
.verbose
1785 if cmd
.gitdir
== None:
1786 cmd
.gitdir
= os
.path
.abspath(".git")
1787 if not isValidGitDir(cmd
.gitdir
):
1788 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
1789 if os
.path
.exists(cmd
.gitdir
):
1790 cdup
= read_pipe("git rev-parse --show-cdup").strip()
1794 if not isValidGitDir(cmd
.gitdir
):
1795 if isValidGitDir(cmd
.gitdir
+ "/.git"):
1796 cmd
.gitdir
+= "/.git"
1798 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
1800 os
.environ
["GIT_DIR"] = cmd
.gitdir
1802 if not cmd
.run(args
):
1806 if __name__
== '__main__':