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 %s" % ("p4", cmd
)
36 sys
.stderr
.write(msg
+ "\n")
39 def write_pipe(c
, str):
41 sys
.stderr
.write('Writing pipe: %s\n' % c
)
43 pipe
= os
.popen(c
, 'w')
46 die('Command failed: %s' % c
)
50 def read_pipe(c
, ignore_error
=False):
52 sys
.stderr
.write('Reading pipe: %s\n' % c
)
54 pipe
= os
.popen(c
, 'rb')
56 if pipe
.close() and not ignore_error
:
57 die('Command failed: %s' % c
)
62 def read_pipe_lines(c
):
64 sys
.stderr
.write('Reading pipe: %s\n' % c
)
65 ## todo: check return status
66 pipe
= os
.popen(c
, 'rb')
67 val
= pipe
.readlines()
69 die('Command failed: %s' % c
)
73 def p4_read_pipe_lines(c
):
74 """Specifically invoke p4 on the command supplied. """
75 real_cmd
= "%s %s" % ("p4", c
)
78 return read_pipe_lines(real_cmd
)
82 sys
.stderr
.write("executing %s\n" % cmd
)
83 if os
.system(cmd
) != 0:
84 die("command failed: %s" % cmd
)
87 """Specifically invoke p4 as the system command. """
88 real_cmd
= "%s %s" % ("p4", cmd
)
91 return system(real_cmd
)
94 """Determine if a Perforce 'kind' should have execute permission
96 'p4 help filetypes' gives a list of the types. If it starts with 'x',
97 or x follows one of a few letters. Otherwise, if there is an 'x' after
98 a plus sign, it is also executable"""
99 return (re
.search(r
"(^[cku]?x)|\+.*x", kind
) != None)
101 def setP4ExecBit(file, mode
):
102 # Reopens an already open file and changes the execute bit to match
103 # the execute bit setting in the passed in mode.
107 if not isModeExec(mode
):
108 p4Type
= getP4OpenedType(file)
109 p4Type
= re
.sub('^([cku]?)x(.*)', '\\1\\2', p4Type
)
110 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
111 if p4Type
[-1] == "+":
112 p4Type
= p4Type
[0:-1]
114 p4_system("reopen -t %s %s" % (p4Type
, file))
116 def getP4OpenedType(file):
117 # Returns the perforce file type for the given file.
119 result
= read_pipe("p4 opened %s" % file)
120 match
= re
.match(".*\((.+)\)\r?$", result
)
122 return match
.group(1)
124 die("Could not determine file type for %s (result: '%s')" % (file, result
))
126 def diffTreePattern():
127 # This is a simple generator for the diff tree regex pattern. This could be
128 # a class variable if this and parseDiffTreeEntry were a part of a class.
129 pattern
= re
.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
133 def parseDiffTreeEntry(entry
):
134 """Parses a single diff tree entry into its component elements.
136 See git-diff-tree(1) manpage for details about the format of the diff
137 output. This method returns a dictionary with the following elements:
139 src_mode - The mode of the source file
140 dst_mode - The mode of the destination file
141 src_sha1 - The sha1 for the source file
142 dst_sha1 - The sha1 fr the destination file
143 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
144 status_score - The score for the status (applicable for 'C' and 'R'
145 statuses). This is None if there is no score.
146 src - The path for the source file.
147 dst - The path for the destination file. This is only present for
148 copy or renames. If it is not present, this is None.
150 If the pattern is not matched, None is returned."""
152 match
= diffTreePattern().next().match(entry
)
155 'src_mode': match
.group(1),
156 'dst_mode': match
.group(2),
157 'src_sha1': match
.group(3),
158 'dst_sha1': match
.group(4),
159 'status': match
.group(5),
160 'status_score': match
.group(6),
161 'src': match
.group(7),
162 'dst': match
.group(10)
166 def isModeExec(mode
):
167 # Returns True if the given git mode represents an executable file,
169 return mode
[-3:] == "755"
171 def isModeExecChanged(src_mode
, dst_mode
):
172 return isModeExec(src_mode
) != isModeExec(dst_mode
)
174 def p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b'):
175 cmd
= "p4 -G %s" % cmd
177 sys
.stderr
.write("Opening pipe: %s\n" % cmd
)
179 # Use a temporary file to avoid deadlocks without
180 # subprocess.communicate(), which would put another copy
181 # of stdout into memory.
183 if stdin
is not None:
184 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
185 stdin_file
.write(stdin
)
189 p4
= subprocess
.Popen(cmd
, shell
=True,
191 stdout
=subprocess
.PIPE
)
196 entry
= marshal
.load(p4
.stdout
)
203 entry
["p4ExitCode"] = exitCode
209 list = p4CmdList(cmd
)
215 def p4Where(depotPath
):
216 if not depotPath
.endswith("/"):
218 output
= p4Cmd("where %s..." % depotPath
)
219 if output
["code"] == "error":
223 clientPath
= output
.get("path")
224 elif "data" in output
:
225 data
= output
.get("data")
226 lastSpace
= data
.rfind(" ")
227 clientPath
= data
[lastSpace
+ 1:]
229 if clientPath
.endswith("..."):
230 clientPath
= clientPath
[:-3]
233 def currentGitBranch():
234 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
236 def isValidGitDir(path
):
237 if (os
.path
.exists(path
+ "/HEAD")
238 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
242 def parseRevision(ref
):
243 return read_pipe("git rev-parse %s" % ref
).strip()
245 def extractLogMessageFromGitCommit(commit
):
248 ## fixme: title is first line of commit, not 1st paragraph.
250 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
259 def extractSettingsGitLog(log
):
261 for line
in log
.split("\n"):
263 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
267 assignments
= m
.group(1).split (':')
268 for a
in assignments
:
270 key
= vals
[0].strip()
271 val
= ('='.join (vals
[1:])).strip()
272 if val
.endswith ('\"') and val
.startswith('"'):
277 paths
= values
.get("depot-paths")
279 paths
= values
.get("depot-path")
281 values
['depot-paths'] = paths
.split(',')
284 def gitBranchExists(branch
):
285 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
286 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
287 return proc
.wait() == 0;
290 return read_pipe("git config %s" % key
, ignore_error
=True).strip()
292 def p4BranchesInGit(branchesAreInRemotes
= True):
295 cmdline
= "git rev-parse --symbolic "
296 if branchesAreInRemotes
:
297 cmdline
+= " --remotes"
299 cmdline
+= " --branches"
301 for line
in read_pipe_lines(cmdline
):
304 ## only import to p4/
305 if not line
.startswith('p4/') or line
== "p4/HEAD":
310 branch
= re
.sub ("^p4/", "", line
)
312 branches
[branch
] = parseRevision(line
)
315 def findUpstreamBranchPoint(head
= "HEAD"):
316 branches
= p4BranchesInGit()
317 # map from depot-path to branch name
318 branchByDepotPath
= {}
319 for branch
in branches
.keys():
320 tip
= branches
[branch
]
321 log
= extractLogMessageFromGitCommit(tip
)
322 settings
= extractSettingsGitLog(log
)
323 if settings
.has_key("depot-paths"):
324 paths
= ",".join(settings
["depot-paths"])
325 branchByDepotPath
[paths
] = "remotes/p4/" + branch
329 while parent
< 65535:
330 commit
= head
+ "~%s" % parent
331 log
= extractLogMessageFromGitCommit(commit
)
332 settings
= extractSettingsGitLog(log
)
333 if settings
.has_key("depot-paths"):
334 paths
= ",".join(settings
["depot-paths"])
335 if branchByDepotPath
.has_key(paths
):
336 return [branchByDepotPath
[paths
], settings
]
340 return ["", settings
]
342 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
344 print ("Creating/updating branch(es) in %s based on origin branch(es)"
347 originPrefix
= "origin/p4/"
349 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
351 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
354 headName
= line
[len(originPrefix
):]
355 remoteHead
= localRefPrefix
+ headName
358 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
359 if (not original
.has_key('depot-paths')
360 or not original
.has_key('change')):
364 if not gitBranchExists(remoteHead
):
366 print "creating %s" % remoteHead
369 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
370 if settings
.has_key('change') > 0:
371 if settings
['depot-paths'] == original
['depot-paths']:
372 originP4Change
= int(original
['change'])
373 p4Change
= int(settings
['change'])
374 if originP4Change
> p4Change
:
375 print ("%s (%s) is newer than %s (%s). "
376 "Updating p4 branch from origin."
377 % (originHead
, originP4Change
,
378 remoteHead
, p4Change
))
381 print ("Ignoring: %s was imported from %s while "
382 "%s was imported from %s"
383 % (originHead
, ','.join(original
['depot-paths']),
384 remoteHead
, ','.join(settings
['depot-paths'])))
387 system("git update-ref %s %s" % (remoteHead
, originHead
))
389 def originP4BranchesExist():
390 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
392 def p4ChangesForPaths(depotPaths
, changeRange
):
394 output
= p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p
, changeRange
)
395 for p
in depotPaths
]))
399 changeNum
= line
.split(" ")[1]
400 changes
.append(int(changeNum
))
407 self
.usage
= "usage: %prog [options]"
410 class P4Debug(Command
):
412 Command
.__init
__(self
)
414 optparse
.make_option("--verbose", dest
="verbose", action
="store_true",
417 self
.description
= "A tool to debug the output of p4 -G."
418 self
.needsGit
= False
423 for output
in p4CmdList(" ".join(args
)):
424 print 'Element: %d' % j
429 class P4RollBack(Command
):
431 Command
.__init
__(self
)
433 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
434 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
436 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
438 self
.rollbackLocalBranches
= False
443 maxChange
= int(args
[0])
445 if "p4ExitCode" in p4Cmd("changes -m 1"):
446 die("Problems executing p4");
448 if self
.rollbackLocalBranches
:
449 refPrefix
= "refs/heads/"
450 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
452 refPrefix
= "refs/remotes/"
453 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
456 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
458 ref
= refPrefix
+ line
459 log
= extractLogMessageFromGitCommit(ref
)
460 settings
= extractSettingsGitLog(log
)
462 depotPaths
= settings
['depot-paths']
463 change
= settings
['change']
467 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
468 for p
in depotPaths
]))) == 0:
469 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
470 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
473 while change
and int(change
) > maxChange
:
476 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
477 system("git update-ref %s \"%s^\"" % (ref
, ref
))
478 log
= extractLogMessageFromGitCommit(ref
)
479 settings
= extractSettingsGitLog(log
)
482 depotPaths
= settings
['depot-paths']
483 change
= settings
['change']
486 print "%s rewound to %s" % (ref
, change
)
490 class P4Submit(Command
):
492 Command
.__init
__(self
)
494 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
495 optparse
.make_option("--origin", dest
="origin"),
496 optparse
.make_option("-M", dest
="detectRename", action
="store_true"),
498 self
.description
= "Submit changes from git to the perforce depot."
499 self
.usage
+= " [name of git branch to submit into perforce depot]"
500 self
.interactive
= True
502 self
.detectRename
= False
504 self
.isWindows
= (platform
.system() == "Windows")
507 if len(p4CmdList("opened ...")) > 0:
508 die("You have files opened with perforce! Close them before starting the sync.")
510 # replaces everything between 'Description:' and the next P4 submit template field with the
512 def prepareLogMessage(self
, template
, message
):
515 inDescriptionSection
= False
517 for line
in template
.split("\n"):
518 if line
.startswith("#"):
519 result
+= line
+ "\n"
522 if inDescriptionSection
:
523 if line
.startswith("Files:"):
524 inDescriptionSection
= False
528 if line
.startswith("Description:"):
529 inDescriptionSection
= True
531 for messageLine
in message
.split("\n"):
532 line
+= "\t" + messageLine
+ "\n"
534 result
+= line
+ "\n"
538 def prepareSubmitTemplate(self
):
539 # remove lines in the Files section that show changes to files outside the depot path we're committing into
541 inFilesSection
= False
542 for line
in p4_read_pipe_lines("change -o"):
543 if line
.endswith("\r\n"):
544 line
= line
[:-2] + "\n"
546 if line
.startswith("\t"):
547 # path starts and ends with a tab
549 lastTab
= path
.rfind("\t")
551 path
= path
[:lastTab
]
552 if not path
.startswith(self
.depotPath
):
555 inFilesSection
= False
557 if line
.startswith("Files:"):
558 inFilesSection
= True
564 def applyCommit(self
, id):
565 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
566 diffOpts
= ("", "-M")[self
.detectRename
]
567 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts
, id, id))
569 filesToDelete
= set()
571 filesToChangeExecBit
= {}
573 diff
= parseDiffTreeEntry(line
)
574 modifier
= diff
['status']
577 p4_system("edit \"%s\"" % path
)
578 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
579 filesToChangeExecBit
[path
] = diff
['dst_mode']
580 editedFiles
.add(path
)
581 elif modifier
== "A":
583 filesToChangeExecBit
[path
] = diff
['dst_mode']
584 if path
in filesToDelete
:
585 filesToDelete
.remove(path
)
586 elif modifier
== "D":
587 filesToDelete
.add(path
)
588 if path
in filesToAdd
:
589 filesToAdd
.remove(path
)
590 elif modifier
== "R":
591 src
, dest
= diff
['src'], diff
['dst']
592 p4_system("integrate -Dt \"%s\" \"%s\"" % (src
, dest
))
593 p4_system("edit \"%s\"" % (dest
))
594 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
595 filesToChangeExecBit
[dest
] = diff
['dst_mode']
597 editedFiles
.add(dest
)
598 filesToDelete
.add(src
)
600 die("unknown modifier %s for %s" % (modifier
, path
))
602 diffcmd
= "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
603 patchcmd
= diffcmd
+ " | git apply "
604 tryPatchCmd
= patchcmd
+ "--check -"
605 applyPatchCmd
= patchcmd
+ "--check --apply -"
607 if os
.system(tryPatchCmd
) != 0:
608 print "Unfortunately applying the change failed!"
609 print "What do you want to do?"
611 while response
!= "s" and response
!= "a" and response
!= "w":
612 response
= raw_input("[s]kip this patch / [a]pply the patch forcibly "
613 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
615 print "Skipping! Good luck with the next patches..."
616 for f
in editedFiles
:
617 p4_system("revert \"%s\"" % f
);
621 elif response
== "a":
622 os
.system(applyPatchCmd
)
623 if len(filesToAdd
) > 0:
624 print "You may also want to call p4 add on the following files:"
625 print " ".join(filesToAdd
)
626 if len(filesToDelete
):
627 print "The following files should be scheduled for deletion with p4 delete:"
628 print " ".join(filesToDelete
)
629 die("Please resolve and submit the conflict manually and "
630 + "continue afterwards with git-p4 submit --continue")
631 elif response
== "w":
632 system(diffcmd
+ " > patch.txt")
633 print "Patch saved to patch.txt in %s !" % self
.clientPath
634 die("Please resolve and submit the conflict manually and "
635 "continue afterwards with git-p4 submit --continue")
637 system(applyPatchCmd
)
640 p4_system("add \"%s\"" % f
)
641 for f
in filesToDelete
:
642 p4_system("revert \"%s\"" % f
)
643 p4_system("delete \"%s\"" % f
)
645 # Set/clear executable bits
646 for f
in filesToChangeExecBit
.keys():
647 mode
= filesToChangeExecBit
[f
]
648 setP4ExecBit(f
, mode
)
650 logMessage
= extractLogMessageFromGitCommit(id)
651 logMessage
= logMessage
.strip()
653 template
= self
.prepareSubmitTemplate()
656 submitTemplate
= self
.prepareLogMessage(template
, logMessage
)
657 if os
.environ
.has_key("P4DIFF"):
658 del(os
.environ
["P4DIFF"])
659 diff
= read_pipe("p4 diff -du ...")
662 for newFile
in filesToAdd
:
663 newdiff
+= "==== new file ====\n"
664 newdiff
+= "--- /dev/null\n"
665 newdiff
+= "+++ %s\n" % newFile
666 f
= open(newFile
, "r")
667 for line
in f
.readlines():
668 newdiff
+= "+" + line
671 separatorLine
= "######## everything below this line is just the diff #######\n"
673 [handle
, fileName
] = tempfile
.mkstemp()
674 tmpFile
= os
.fdopen(handle
, "w+")
676 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
677 separatorLine
= separatorLine
.replace("\n", "\r\n")
678 newdiff
= newdiff
.replace("\n", "\r\n")
679 tmpFile
.write(submitTemplate
+ separatorLine
+ diff
+ newdiff
)
682 if platform
.system() == "Windows":
683 defaultEditor
= "notepad"
684 if os
.environ
.has_key("P4EDITOR"):
685 editor
= os
.environ
.get("P4EDITOR")
687 editor
= os
.environ
.get("EDITOR", defaultEditor
);
688 system(editor
+ " " + fileName
)
689 tmpFile
= open(fileName
, "rb")
690 message
= tmpFile
.read()
693 submitTemplate
= message
[:message
.index(separatorLine
)]
695 submitTemplate
= submitTemplate
.replace("\r\n", "\n")
697 write_pipe("p4 submit -i", submitTemplate
)
699 fileName
= "submit.txt"
700 file = open(fileName
, "w+")
701 file.write(self
.prepareLogMessage(template
, logMessage
))
703 print ("Perforce submit template written as %s. "
704 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
705 % (fileName
, fileName
))
709 self
.master
= currentGitBranch()
710 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
711 die("Detecting current git branch failed!")
713 self
.master
= args
[0]
717 allowSubmit
= gitConfig("git-p4.allowSubmit")
718 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
719 die("%s is not in git-p4.allowSubmit" % self
.master
)
721 [upstream
, settings
] = findUpstreamBranchPoint()
722 self
.depotPath
= settings
['depot-paths'][0]
723 if len(self
.origin
) == 0:
724 self
.origin
= upstream
727 print "Origin branch is " + self
.origin
729 if len(self
.depotPath
) == 0:
730 print "Internal error: cannot locate perforce depot path from existing branches"
733 self
.clientPath
= p4Where(self
.depotPath
)
735 if len(self
.clientPath
) == 0:
736 print "Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
739 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
740 self
.oldWorkingDirectory
= os
.getcwd()
742 os
.chdir(self
.clientPath
)
743 print "Syncronizing p4 checkout..."
744 p4_system("sync ...")
749 for line
in read_pipe_lines("git rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)):
750 commits
.append(line
.strip())
753 while len(commits
) > 0:
755 commits
= commits
[1:]
756 self
.applyCommit(commit
)
757 if not self
.interactive
:
760 if len(commits
) == 0:
761 print "All changes applied!"
762 os
.chdir(self
.oldWorkingDirectory
)
772 class P4Sync(Command
):
774 Command
.__init
__(self
)
776 optparse
.make_option("--branch", dest
="branch"),
777 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
778 optparse
.make_option("--changesfile", dest
="changesFile"),
779 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
780 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
781 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
782 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
783 help="Import into refs/heads/ , not refs/remotes"),
784 optparse
.make_option("--max-changes", dest
="maxChanges"),
785 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
786 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
787 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
788 help="Only sync files that are included in the Perforce Client Spec")
790 self
.description
= """Imports from Perforce into a git repository.\n
792 //depot/my/project/ -- to import the current head
793 //depot/my/project/@all -- to import everything
794 //depot/my/project/@1,6 -- to import only from revision 1 to 6
796 (a ... is not needed in the path p4 specification, it's added implicitly)"""
798 self
.usage
+= " //depot/path[@revRange]"
800 self
.createdBranches
= Set()
801 self
.committedChanges
= Set()
803 self
.detectBranches
= False
804 self
.detectLabels
= False
805 self
.changesFile
= ""
806 self
.syncWithOrigin
= True
808 self
.importIntoRemotes
= True
810 self
.isWindows
= (platform
.system() == "Windows")
811 self
.keepRepoPath
= False
812 self
.depotPaths
= None
813 self
.p4BranchesInGit
= []
814 self
.cloneExclude
= []
815 self
.useClientSpec
= False
816 self
.clientSpecDirs
= []
818 if gitConfig("git-p4.syncFromOrigin") == "false":
819 self
.syncWithOrigin
= False
821 def extractFilesFromCommit(self
, commit
):
822 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
823 for path
in self
.cloneExclude
]
826 while commit
.has_key("depotFile%s" % fnum
):
827 path
= commit
["depotFile%s" % fnum
]
829 if [p
for p
in self
.cloneExclude
830 if path
.startswith (p
)]:
833 found
= [p
for p
in self
.depotPaths
834 if path
.startswith (p
)]
841 file["rev"] = commit
["rev%s" % fnum
]
842 file["action"] = commit
["action%s" % fnum
]
843 file["type"] = commit
["type%s" % fnum
]
848 def stripRepoPath(self
, path
, prefixes
):
849 if self
.keepRepoPath
:
850 prefixes
= [re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])]
853 if path
.startswith(p
):
858 def splitFilesIntoBranches(self
, commit
):
861 while commit
.has_key("depotFile%s" % fnum
):
862 path
= commit
["depotFile%s" % fnum
]
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
]
876 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
878 for branch
in self
.knownBranches
.keys():
880 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
881 if relPath
.startswith(branch
+ "/"):
882 if branch
not in branches
:
883 branches
[branch
] = []
884 branches
[branch
].append(file)
889 ## Should move this out, doesn't use SELF.
890 def readP4Files(self
, files
):
896 for val
in self
.clientSpecDirs
:
897 if f
['path'].startswith(val
[0]):
903 filesForCommit
.append(f
)
904 if f
['action'] != 'delete':
905 filesToRead
.append(f
)
908 if len(filesToRead
) > 0:
909 filedata
= p4CmdList('-x - print',
910 stdin
='\n'.join(['%s#%s' % (f
['path'], f
['rev'])
911 for f
in filesToRead
]),
914 if "p4ExitCode" in filedata
[0]:
915 die("Problems executing p4. Error: [%d]."
916 % (filedata
[0]['p4ExitCode']));
920 while j
< len(filedata
):
924 while j
< len(filedata
) and filedata
[j
]['code'] in ('text', 'unicode', 'binary'):
925 text
.append(filedata
[j
]['data'])
929 if not stat
.has_key('depotFile'):
930 sys
.stderr
.write("p4 print fails with: %s\n" % repr(stat
))
933 if stat
['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
934 text
= re
.sub(r
'(?i)\$(Id|Header):[^$]*\$',r
'$\1$', text
)
935 elif stat
['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
936 text
= re
.sub(r
'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r
'$\1$', text
)
938 contents
[stat
['depotFile']] = text
940 for f
in filesForCommit
:
942 if contents
.has_key(path
):
943 f
['data'] = contents
[path
]
945 return filesForCommit
947 def commit(self
, details
, files
, branch
, branchPrefixes
, parent
= ""):
948 epoch
= details
["time"]
949 author
= details
["user"]
952 print "commit into %s" % branch
954 # start with reading files; if that fails, we should not
958 if [p
for p
in branchPrefixes
if f
['path'].startswith(p
)]:
961 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % path
)
962 files
= self
.readP4Files(new_files
)
964 self
.gitStream
.write("commit %s\n" % branch
)
965 # gitStream.write("mark :%s\n" % details["change"])
966 self
.committedChanges
.add(int(details
["change"]))
968 if author
not in self
.users
:
969 self
.getUserMapFromPerforceServer()
970 if author
in self
.users
:
971 committer
= "%s %s %s" % (self
.users
[author
], epoch
, self
.tz
)
973 committer
= "%s <a@b> %s %s" % (author
, epoch
, self
.tz
)
975 self
.gitStream
.write("committer %s\n" % committer
)
977 self
.gitStream
.write("data <<EOT\n")
978 self
.gitStream
.write(details
["desc"])
979 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s"
980 % (','.join (branchPrefixes
), details
["change"]))
981 if len(details
['options']) > 0:
982 self
.gitStream
.write(": options = %s" % details
['options'])
983 self
.gitStream
.write("]\nEOT\n\n")
987 print "parent %s" % parent
988 self
.gitStream
.write("from %s\n" % parent
)
991 if file["type"] == "apple":
992 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
995 relPath
= self
.stripRepoPath(file['path'], branchPrefixes
)
996 if file["action"] == "delete":
997 self
.gitStream
.write("D %s\n" % relPath
)
1002 if isP4Exec(file["type"]):
1004 elif file["type"] == "symlink":
1006 # p4 print on a symlink contains "target\n", so strip it off
1009 if self
.isWindows
and file["type"].endswith("text"):
1010 data
= data
.replace("\r\n", "\n")
1012 self
.gitStream
.write("M %s inline %s\n" % (mode
, relPath
))
1013 self
.gitStream
.write("data %s\n" % len(data
))
1014 self
.gitStream
.write(data
)
1015 self
.gitStream
.write("\n")
1017 self
.gitStream
.write("\n")
1019 change
= int(details
["change"])
1021 if self
.labels
.has_key(change
):
1022 label
= self
.labels
[change
]
1023 labelDetails
= label
[0]
1024 labelRevisions
= label
[1]
1026 print "Change %s is labelled %s" % (change
, labelDetails
)
1028 files
= p4CmdList("files " + ' '.join (["%s...@%s" % (p
, change
)
1029 for p
in branchPrefixes
]))
1031 if len(files
) == len(labelRevisions
):
1035 if info
["action"] == "delete":
1037 cleanedFiles
[info
["depotFile"]] = info
["rev"]
1039 if cleanedFiles
== labelRevisions
:
1040 self
.gitStream
.write("tag tag_%s\n" % labelDetails
["label"])
1041 self
.gitStream
.write("from %s\n" % branch
)
1043 owner
= labelDetails
["Owner"]
1045 if author
in self
.users
:
1046 tagger
= "%s %s %s" % (self
.users
[owner
], epoch
, self
.tz
)
1048 tagger
= "%s <a@b> %s %s" % (owner
, epoch
, self
.tz
)
1049 self
.gitStream
.write("tagger %s\n" % tagger
)
1050 self
.gitStream
.write("data <<EOT\n")
1051 self
.gitStream
.write(labelDetails
["Description"])
1052 self
.gitStream
.write("EOT\n\n")
1056 print ("Tag %s does not match with change %s: files do not match."
1057 % (labelDetails
["label"], change
))
1061 print ("Tag %s does not match with change %s: file count is different."
1062 % (labelDetails
["label"], change
))
1064 def getUserCacheFilename(self
):
1065 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
1066 return home
+ "/.gitp4-usercache.txt"
1068 def getUserMapFromPerforceServer(self
):
1069 if self
.userMapFromPerforceServer
:
1073 for output
in p4CmdList("users"):
1074 if not output
.has_key("User"):
1076 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
1080 for (key
, val
) in self
.users
.items():
1081 s
+= "%s\t%s\n" % (key
, val
)
1083 open(self
.getUserCacheFilename(), "wb").write(s
)
1084 self
.userMapFromPerforceServer
= True
1086 def loadUserMapFromCache(self
):
1088 self
.userMapFromPerforceServer
= False
1090 cache
= open(self
.getUserCacheFilename(), "rb")
1091 lines
= cache
.readlines()
1094 entry
= line
.strip().split("\t")
1095 self
.users
[entry
[0]] = entry
[1]
1097 self
.getUserMapFromPerforceServer()
1099 def getLabels(self
):
1102 l
= p4CmdList("labels %s..." % ' '.join (self
.depotPaths
))
1103 if len(l
) > 0 and not self
.silent
:
1104 print "Finding files belonging to labels in %s" % `self
.depotPaths`
1107 label
= output
["label"]
1111 print "Querying files for label %s" % label
1112 for file in p4CmdList("files "
1113 + ' '.join (["%s...@%s" % (p
, label
)
1114 for p
in self
.depotPaths
])):
1115 revisions
[file["depotFile"]] = file["rev"]
1116 change
= int(file["change"])
1117 if change
> newestChange
:
1118 newestChange
= change
1120 self
.labels
[newestChange
] = [output
, revisions
]
1123 print "Label changes: %s" % self
.labels
.keys()
1125 def guessProjectName(self
):
1126 for p
in self
.depotPaths
:
1129 p
= p
[p
.strip().rfind("/") + 1:]
1130 if not p
.endswith("/"):
1134 def getBranchMapping(self
):
1135 lostAndFoundBranches
= set()
1137 for info
in p4CmdList("branches"):
1138 details
= p4Cmd("branch -o %s" % info
["branch"])
1140 while details
.has_key("View%s" % viewIdx
):
1141 paths
= details
["View%s" % viewIdx
].split(" ")
1142 viewIdx
= viewIdx
+ 1
1143 # require standard //depot/foo/... //depot/bar/... mapping
1144 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
1147 destination
= paths
[1]
1149 if source
.startswith(self
.depotPaths
[0]) and destination
.startswith(self
.depotPaths
[0]):
1150 source
= source
[len(self
.depotPaths
[0]):-4]
1151 destination
= destination
[len(self
.depotPaths
[0]):-4]
1153 if destination
in self
.knownBranches
:
1155 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
1156 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
1159 self
.knownBranches
[destination
] = source
1161 lostAndFoundBranches
.discard(destination
)
1163 if source
not in self
.knownBranches
:
1164 lostAndFoundBranches
.add(source
)
1167 for branch
in lostAndFoundBranches
:
1168 self
.knownBranches
[branch
] = branch
1170 def getBranchMappingFromGitBranches(self
):
1171 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1172 for branch
in branches
.keys():
1173 if branch
== "master":
1176 branch
= branch
[len(self
.projectName
):]
1177 self
.knownBranches
[branch
] = branch
1179 def listExistingP4GitBranches(self
):
1180 # branches holds mapping from name to commit
1181 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1182 self
.p4BranchesInGit
= branches
.keys()
1183 for branch
in branches
.keys():
1184 self
.initialParents
[self
.refPrefix
+ branch
] = branches
[branch
]
1186 def updateOptionDict(self
, d
):
1188 if self
.keepRepoPath
:
1189 option_keys
['keepRepoPath'] = 1
1191 d
["options"] = ' '.join(sorted(option_keys
.keys()))
1193 def readOptions(self
, d
):
1194 self
.keepRepoPath
= (d
.has_key('options')
1195 and ('keepRepoPath' in d
['options']))
1197 def gitRefForBranch(self
, branch
):
1198 if branch
== "main":
1199 return self
.refPrefix
+ "master"
1201 if len(branch
) <= 0:
1204 return self
.refPrefix
+ self
.projectName
+ branch
1206 def gitCommitByP4Change(self
, ref
, change
):
1208 print "looking in ref " + ref
+ " for change %s using bisect..." % change
1211 latestCommit
= parseRevision(ref
)
1215 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
1216 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
1221 log
= extractLogMessageFromGitCommit(next
)
1222 settings
= extractSettingsGitLog(log
)
1223 currentChange
= int(settings
['change'])
1225 print "current change %s" % currentChange
1227 if currentChange
== change
:
1229 print "found %s" % next
1232 if currentChange
< change
:
1233 earliestCommit
= "^%s" % next
1235 latestCommit
= "%s" % next
1239 def importNewBranch(self
, branch
, maxChange
):
1240 # make fast-import flush all changes to disk and update the refs using the checkpoint
1241 # command so that we can try to find the branch parent in the git history
1242 self
.gitStream
.write("checkpoint\n\n");
1243 self
.gitStream
.flush();
1244 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1245 range = "@1,%s" % maxChange
1246 #print "prefix" + branchPrefix
1247 changes
= p4ChangesForPaths([branchPrefix
], range)
1248 if len(changes
) <= 0:
1250 firstChange
= changes
[0]
1251 #print "first change in branch: %s" % firstChange
1252 sourceBranch
= self
.knownBranches
[branch
]
1253 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
1254 sourceRef
= self
.gitRefForBranch(sourceBranch
)
1255 #print "source " + sourceBranch
1257 branchParentChange
= int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath
, firstChange
))["change"])
1258 #print "branch parent: %s" % branchParentChange
1259 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
1260 if len(gitParent
) > 0:
1261 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
1262 #print "parent git commit: %s" % gitParent
1264 self
.importChanges(changes
)
1267 def importChanges(self
, changes
):
1269 for change
in changes
:
1270 description
= p4Cmd("describe %s" % change
)
1271 self
.updateOptionDict(description
)
1274 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
1279 if self
.detectBranches
:
1280 branches
= self
.splitFilesIntoBranches(description
)
1281 for branch
in branches
.keys():
1283 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1287 filesForCommit
= branches
[branch
]
1290 print "branch is %s" % branch
1292 self
.updatedBranches
.add(branch
)
1294 if branch
not in self
.createdBranches
:
1295 self
.createdBranches
.add(branch
)
1296 parent
= self
.knownBranches
[branch
]
1297 if parent
== branch
:
1300 fullBranch
= self
.projectName
+ branch
1301 if fullBranch
not in self
.p4BranchesInGit
:
1303 print("\n Importing new branch %s" % fullBranch
);
1304 if self
.importNewBranch(branch
, change
- 1):
1306 self
.p4BranchesInGit
.append(fullBranch
)
1308 print("\n Resuming with change %s" % change
);
1311 print "parent determined through known branches: %s" % parent
1313 branch
= self
.gitRefForBranch(branch
)
1314 parent
= self
.gitRefForBranch(parent
)
1317 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
1319 if len(parent
) == 0 and branch
in self
.initialParents
:
1320 parent
= self
.initialParents
[branch
]
1321 del self
.initialParents
[branch
]
1323 self
.commit(description
, filesForCommit
, branch
, [branchPrefix
], parent
)
1325 files
= self
.extractFilesFromCommit(description
)
1326 self
.commit(description
, files
, self
.branch
, self
.depotPaths
,
1328 self
.initialParent
= ""
1330 print self
.gitError
.read()
1333 def importHeadRevision(self
, revision
):
1334 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
1336 details
= { "user" : "git perforce import user", "time" : int(time
.time()) }
1337 details
["desc"] = ("Initial import of %s from the state at revision %s"
1338 % (' '.join(self
.depotPaths
), revision
))
1339 details
["change"] = revision
1343 for info
in p4CmdList("files "
1344 + ' '.join(["%s...%s"
1346 for p
in self
.depotPaths
])):
1348 if info
['code'] == 'error':
1349 sys
.stderr
.write("p4 returned an error: %s\n"
1354 change
= int(info
["change"])
1355 if change
> newestRevision
:
1356 newestRevision
= change
1358 if info
["action"] == "delete":
1359 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1360 #fileCnt = fileCnt + 1
1363 for prop
in ["depotFile", "rev", "action", "type" ]:
1364 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
1366 fileCnt
= fileCnt
+ 1
1368 details
["change"] = newestRevision
1369 self
.updateOptionDict(details
)
1371 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
, self
.depotPaths
)
1373 print "IO error with git fast-import. Is your git version recent enough?"
1374 print self
.gitError
.read()
1377 def getClientSpec(self
):
1378 specList
= p4CmdList( "client -o" )
1380 for entry
in specList
:
1381 for k
,v
in entry
.iteritems():
1382 if k
.startswith("View"):
1383 if v
.startswith('"'):
1387 index
= v
.find("...")
1389 if v
.startswith("-"):
1394 self
.clientSpecDirs
= temp
.items()
1395 self
.clientSpecDirs
.sort( lambda x
, y
: abs( y
[1] ) - abs( x
[1] ) )
1397 def run(self
, args
):
1398 self
.depotPaths
= []
1399 self
.changeRange
= ""
1400 self
.initialParent
= ""
1401 self
.previousDepotPaths
= []
1403 # map from branch depot path to parent branch
1404 self
.knownBranches
= {}
1405 self
.initialParents
= {}
1406 self
.hasOrigin
= originP4BranchesExist()
1407 if not self
.syncWithOrigin
:
1408 self
.hasOrigin
= False
1410 if self
.importIntoRemotes
:
1411 self
.refPrefix
= "refs/remotes/p4/"
1413 self
.refPrefix
= "refs/heads/p4/"
1415 if self
.syncWithOrigin
and self
.hasOrigin
:
1417 print "Syncing with origin first by calling git fetch origin"
1418 system("git fetch origin")
1420 if len(self
.branch
) == 0:
1421 self
.branch
= self
.refPrefix
+ "master"
1422 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
1423 system("git update-ref %s refs/heads/p4" % self
.branch
)
1424 system("git branch -D p4");
1425 # create it /after/ importing, when master exists
1426 if not gitBranchExists(self
.refPrefix
+ "HEAD") and self
.importIntoRemotes
and gitBranchExists(self
.branch
):
1427 system("git symbolic-ref %sHEAD %s" % (self
.refPrefix
, self
.branch
))
1429 if self
.useClientSpec
or gitConfig("p4.useclientspec") == "true":
1430 self
.getClientSpec()
1432 # TODO: should always look at previous commits,
1433 # merge with previous imports, if possible.
1436 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
1437 self
.listExistingP4GitBranches()
1439 if len(self
.p4BranchesInGit
) > 1:
1441 print "Importing from/into multiple branches"
1442 self
.detectBranches
= True
1445 print "branches: %s" % self
.p4BranchesInGit
1448 for branch
in self
.p4BranchesInGit
:
1449 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
1451 settings
= extractSettingsGitLog(logMsg
)
1453 self
.readOptions(settings
)
1454 if (settings
.has_key('depot-paths')
1455 and settings
.has_key ('change')):
1456 change
= int(settings
['change']) + 1
1457 p4Change
= max(p4Change
, change
)
1459 depotPaths
= sorted(settings
['depot-paths'])
1460 if self
.previousDepotPaths
== []:
1461 self
.previousDepotPaths
= depotPaths
1464 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
1465 for i
in range(0, min(len(cur
), len(prev
))):
1466 if cur
[i
] <> prev
[i
]:
1470 paths
.append (cur
[:i
+ 1])
1472 self
.previousDepotPaths
= paths
1475 self
.depotPaths
= sorted(self
.previousDepotPaths
)
1476 self
.changeRange
= "@%s,#head" % p4Change
1477 if not self
.detectBranches
:
1478 self
.initialParent
= parseRevision(self
.branch
)
1479 if not self
.silent
and not self
.detectBranches
:
1480 print "Performing incremental import into %s git branch" % self
.branch
1482 if not self
.branch
.startswith("refs/"):
1483 self
.branch
= "refs/heads/" + self
.branch
1485 if len(args
) == 0 and self
.depotPaths
:
1487 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
1489 if self
.depotPaths
and self
.depotPaths
!= args
:
1490 print ("previous import used depot path %s and now %s was specified. "
1491 "This doesn't work!" % (' '.join (self
.depotPaths
),
1495 self
.depotPaths
= sorted(args
)
1501 for p
in self
.depotPaths
:
1502 if p
.find("@") != -1:
1503 atIdx
= p
.index("@")
1504 self
.changeRange
= p
[atIdx
:]
1505 if self
.changeRange
== "@all":
1506 self
.changeRange
= ""
1507 elif ',' not in self
.changeRange
:
1508 revision
= self
.changeRange
1509 self
.changeRange
= ""
1511 elif p
.find("#") != -1:
1512 hashIdx
= p
.index("#")
1513 revision
= p
[hashIdx
:]
1515 elif self
.previousDepotPaths
== []:
1518 p
= re
.sub ("\.\.\.$", "", p
)
1519 if not p
.endswith("/"):
1524 self
.depotPaths
= newPaths
1527 self
.loadUserMapFromCache()
1529 if self
.detectLabels
:
1532 if self
.detectBranches
:
1533 ## FIXME - what's a P4 projectName ?
1534 self
.projectName
= self
.guessProjectName()
1537 self
.getBranchMappingFromGitBranches()
1539 self
.getBranchMapping()
1541 print "p4-git branches: %s" % self
.p4BranchesInGit
1542 print "initial parents: %s" % self
.initialParents
1543 for b
in self
.p4BranchesInGit
:
1547 b
= b
[len(self
.projectName
):]
1548 self
.createdBranches
.add(b
)
1550 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
1552 importProcess
= subprocess
.Popen(["git", "fast-import"],
1553 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
1554 stderr
=subprocess
.PIPE
);
1555 self
.gitOutput
= importProcess
.stdout
1556 self
.gitStream
= importProcess
.stdin
1557 self
.gitError
= importProcess
.stderr
1560 self
.importHeadRevision(revision
)
1564 if len(self
.changesFile
) > 0:
1565 output
= open(self
.changesFile
).readlines()
1568 changeSet
.add(int(line
))
1570 for change
in changeSet
:
1571 changes
.append(change
)
1576 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
1578 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
)
1580 if len(self
.maxChanges
) > 0:
1581 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
1583 if len(changes
) == 0:
1585 print "No changes to import!"
1588 if not self
.silent
and not self
.detectBranches
:
1589 print "Import destination: %s" % self
.branch
1591 self
.updatedBranches
= set()
1593 self
.importChanges(changes
)
1597 if len(self
.updatedBranches
) > 0:
1598 sys
.stdout
.write("Updated branches: ")
1599 for b
in self
.updatedBranches
:
1600 sys
.stdout
.write("%s " % b
)
1601 sys
.stdout
.write("\n")
1603 self
.gitStream
.close()
1604 if importProcess
.wait() != 0:
1605 die("fast-import failed: %s" % self
.gitError
.read())
1606 self
.gitOutput
.close()
1607 self
.gitError
.close()
1611 class P4Rebase(Command
):
1613 Command
.__init
__(self
)
1615 self
.description
= ("Fetches the latest revision from perforce and "
1616 + "rebases the current work (branch) against it")
1617 self
.verbose
= False
1619 def run(self
, args
):
1623 return self
.rebase()
1626 if os
.system("git update-index --refresh") != 0:
1627 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.");
1628 if len(read_pipe("git diff-index HEAD --")) > 0:
1629 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1631 [upstream
, settings
] = findUpstreamBranchPoint()
1632 if len(upstream
) == 0:
1633 die("Cannot find upstream branchpoint for rebase")
1635 # the branchpoint may be p4/foo~3, so strip off the parent
1636 upstream
= re
.sub("~[0-9]+$", "", upstream
)
1638 print "Rebasing the current branch onto %s" % upstream
1639 oldHead
= read_pipe("git rev-parse HEAD").strip()
1640 system("git rebase %s" % upstream
)
1641 system("git diff-tree --stat --summary -M %s HEAD" % oldHead
)
1644 class P4Clone(P4Sync
):
1646 P4Sync
.__init
__(self
)
1647 self
.description
= "Creates a new git repository and imports from Perforce into it"
1648 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
1650 optparse
.make_option("--destination", dest
="cloneDestination",
1651 action
='store', default
=None,
1652 help="where to leave result of the clone"),
1653 optparse
.make_option("-/", dest
="cloneExclude",
1654 action
="append", type="string",
1655 help="exclude depot path")
1657 self
.cloneDestination
= None
1658 self
.needsGit
= False
1660 # This is required for the "append" cloneExclude action
1661 def ensure_value(self
, attr
, value
):
1662 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
1663 setattr(self
, attr
, value
)
1664 return getattr(self
, attr
)
1666 def defaultDestination(self
, args
):
1667 ## TODO: use common prefix of args?
1669 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
1670 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
1671 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
1672 depotDir
= re
.sub(r
"/$", "", depotDir
)
1673 return os
.path
.split(depotDir
)[1]
1675 def run(self
, args
):
1679 if self
.keepRepoPath
and not self
.cloneDestination
:
1680 sys
.stderr
.write("Must specify destination for --keep-path\n")
1685 if not self
.cloneDestination
and len(depotPaths
) > 1:
1686 self
.cloneDestination
= depotPaths
[-1]
1687 depotPaths
= depotPaths
[:-1]
1689 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
1690 for p
in depotPaths
:
1691 if not p
.startswith("//"):
1694 if not self
.cloneDestination
:
1695 self
.cloneDestination
= self
.defaultDestination(args
)
1697 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
1698 if not os
.path
.exists(self
.cloneDestination
):
1699 os
.makedirs(self
.cloneDestination
)
1700 os
.chdir(self
.cloneDestination
)
1702 self
.gitdir
= os
.getcwd() + "/.git"
1703 if not P4Sync
.run(self
, depotPaths
):
1705 if self
.branch
!= "master":
1706 if gitBranchExists("refs/remotes/p4/master"):
1707 system("git branch master refs/remotes/p4/master")
1708 system("git checkout -f")
1710 print "Could not detect main branch. No checkout/master branch created."
1714 class P4Branches(Command
):
1716 Command
.__init
__(self
)
1718 self
.description
= ("Shows the git branches that hold imports and their "
1719 + "corresponding perforce depot paths")
1720 self
.verbose
= False
1722 def run(self
, args
):
1723 if originP4BranchesExist():
1724 createOrUpdateBranchesFromOrigin()
1726 cmdline
= "git rev-parse --symbolic "
1727 cmdline
+= " --remotes"
1729 for line
in read_pipe_lines(cmdline
):
1732 if not line
.startswith('p4/') or line
== "p4/HEAD":
1736 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
1737 settings
= extractSettingsGitLog(log
)
1739 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
1742 class HelpFormatter(optparse
.IndentedHelpFormatter
):
1744 optparse
.IndentedHelpFormatter
.__init
__(self
)
1746 def format_description(self
, description
):
1748 return description
+ "\n"
1752 def printUsage(commands
):
1753 print "usage: %s <command> [options]" % sys
.argv
[0]
1755 print "valid commands: %s" % ", ".join(commands
)
1757 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
1762 "submit" : P4Submit
,
1763 "commit" : P4Submit
,
1765 "rebase" : P4Rebase
,
1767 "rollback" : P4RollBack
,
1768 "branches" : P4Branches
1773 if len(sys
.argv
[1:]) == 0:
1774 printUsage(commands
.keys())
1778 cmdName
= sys
.argv
[1]
1780 klass
= commands
[cmdName
]
1783 print "unknown command %s" % cmdName
1785 printUsage(commands
.keys())
1788 options
= cmd
.options
1789 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
1793 if len(options
) > 0:
1794 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
1796 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
1798 description
= cmd
.description
,
1799 formatter
= HelpFormatter())
1801 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
1803 verbose
= cmd
.verbose
1805 if cmd
.gitdir
== None:
1806 cmd
.gitdir
= os
.path
.abspath(".git")
1807 if not isValidGitDir(cmd
.gitdir
):
1808 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
1809 if os
.path
.exists(cmd
.gitdir
):
1810 cdup
= read_pipe("git rev-parse --show-cdup").strip()
1814 if not isValidGitDir(cmd
.gitdir
):
1815 if isValidGitDir(cmd
.gitdir
+ "/.git"):
1816 cmd
.gitdir
+= "/.git"
1818 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
1820 os
.environ
["GIT_DIR"] = cmd
.gitdir
1822 if not cmd
.run(args
):
1826 if __name__
== '__main__':