3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
5 # Author: Simon Hausmann <simon@lst.de>
6 # Copyright: 2007 Simon Hausmann <simon@lst.de>
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
11 import optparse
, sys
, os
, marshal
, popen2
, subprocess
, shelve
12 import tempfile
, getopt
, sha
, os
.path
, time
, platform
20 def p4_build_cmd(cmd
):
21 """Build a suitable p4 command line.
23 This consolidates building and returning a p4 command line into one
24 location. It means that hooking into the environment, or other configuration
25 can be done more easily.
27 real_cmd
= "%s " % "p4"
29 user
= gitConfig("git-p4.user")
31 real_cmd
+= "-u %s " % user
33 password
= gitConfig("git-p4.password")
35 real_cmd
+= "-P %s " % password
37 port
= gitConfig("git-p4.port")
39 real_cmd
+= "-p %s " % port
41 host
= gitConfig("git-p4.host")
43 real_cmd
+= "-h %s " % host
45 client
= gitConfig("git-p4.client")
47 real_cmd
+= "-c %s " % client
49 real_cmd
+= "%s" % (cmd
)
58 sys
.stderr
.write(msg
+ "\n")
61 def write_pipe(c
, str):
63 sys
.stderr
.write('Writing pipe: %s\n' % c
)
65 pipe
= os
.popen(c
, 'w')
68 die('Command failed: %s' % c
)
72 def read_pipe(c
, ignore_error
=False):
74 sys
.stderr
.write('Reading pipe: %s\n' % c
)
76 pipe
= os
.popen(c
, 'rb')
78 if pipe
.close() and not ignore_error
:
79 die('Command failed: %s' % c
)
84 def read_pipe_lines(c
):
86 sys
.stderr
.write('Reading pipe: %s\n' % c
)
87 ## todo: check return status
88 pipe
= os
.popen(c
, 'rb')
89 val
= pipe
.readlines()
91 die('Command failed: %s' % c
)
95 def p4_read_pipe_lines(c
):
96 """Specifically invoke p4 on the command supplied. """
97 real_cmd
= p4_build_cmd(c
)
98 return read_pipe_lines(real_cmd
)
102 sys
.stderr
.write("executing %s\n" % cmd
)
103 if os
.system(cmd
) != 0:
104 die("command failed: %s" % cmd
)
107 """Specifically invoke p4 as the system command. """
108 real_cmd
= p4_build_cmd(cmd
)
109 return system(real_cmd
)
112 """Determine if a Perforce 'kind' should have execute permission
114 'p4 help filetypes' gives a list of the types. If it starts with 'x',
115 or x follows one of a few letters. Otherwise, if there is an 'x' after
116 a plus sign, it is also executable"""
117 return (re
.search(r
"(^[cku]?x)|\+.*x", kind
) != None)
119 def setP4ExecBit(file, mode
):
120 # Reopens an already open file and changes the execute bit to match
121 # the execute bit setting in the passed in mode.
125 if not isModeExec(mode
):
126 p4Type
= getP4OpenedType(file)
127 p4Type
= re
.sub('^([cku]?)x(.*)', '\\1\\2', p4Type
)
128 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
129 if p4Type
[-1] == "+":
130 p4Type
= p4Type
[0:-1]
132 p4_system("reopen -t %s %s" % (p4Type
, file))
134 def getP4OpenedType(file):
135 # Returns the perforce file type for the given file.
137 result
= read_pipe("p4 opened %s" % file)
138 match
= re
.match(".*\((.+)\)\r?$", result
)
140 return match
.group(1)
142 die("Could not determine file type for %s (result: '%s')" % (file, result
))
144 def diffTreePattern():
145 # This is a simple generator for the diff tree regex pattern. This could be
146 # a class variable if this and parseDiffTreeEntry were a part of a class.
147 pattern
= re
.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
151 def parseDiffTreeEntry(entry
):
152 """Parses a single diff tree entry into its component elements.
154 See git-diff-tree(1) manpage for details about the format of the diff
155 output. This method returns a dictionary with the following elements:
157 src_mode - The mode of the source file
158 dst_mode - The mode of the destination file
159 src_sha1 - The sha1 for the source file
160 dst_sha1 - The sha1 fr the destination file
161 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
162 status_score - The score for the status (applicable for 'C' and 'R'
163 statuses). This is None if there is no score.
164 src - The path for the source file.
165 dst - The path for the destination file. This is only present for
166 copy or renames. If it is not present, this is None.
168 If the pattern is not matched, None is returned."""
170 match
= diffTreePattern().next().match(entry
)
173 'src_mode': match
.group(1),
174 'dst_mode': match
.group(2),
175 'src_sha1': match
.group(3),
176 'dst_sha1': match
.group(4),
177 'status': match
.group(5),
178 'status_score': match
.group(6),
179 'src': match
.group(7),
180 'dst': match
.group(10)
184 def isModeExec(mode
):
185 # Returns True if the given git mode represents an executable file,
187 return mode
[-3:] == "755"
189 def isModeExecChanged(src_mode
, dst_mode
):
190 return isModeExec(src_mode
) != isModeExec(dst_mode
)
192 def p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b'):
193 cmd
= p4_build_cmd("-G %s" % (cmd
))
195 sys
.stderr
.write("Opening pipe: %s\n" % cmd
)
197 # Use a temporary file to avoid deadlocks without
198 # subprocess.communicate(), which would put another copy
199 # of stdout into memory.
201 if stdin
is not None:
202 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
203 stdin_file
.write(stdin
)
207 p4
= subprocess
.Popen(cmd
, shell
=True,
209 stdout
=subprocess
.PIPE
)
214 entry
= marshal
.load(p4
.stdout
)
221 entry
["p4ExitCode"] = exitCode
227 list = p4CmdList(cmd
)
233 def p4Where(depotPath
):
234 if not depotPath
.endswith("/"):
236 output
= p4Cmd("where %s..." % depotPath
)
237 if output
["code"] == "error":
241 clientPath
= output
.get("path")
242 elif "data" in output
:
243 data
= output
.get("data")
244 lastSpace
= data
.rfind(" ")
245 clientPath
= data
[lastSpace
+ 1:]
247 if clientPath
.endswith("..."):
248 clientPath
= clientPath
[:-3]
251 def currentGitBranch():
252 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
254 def isValidGitDir(path
):
255 if (os
.path
.exists(path
+ "/HEAD")
256 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
260 def parseRevision(ref
):
261 return read_pipe("git rev-parse %s" % ref
).strip()
263 def extractLogMessageFromGitCommit(commit
):
266 ## fixme: title is first line of commit, not 1st paragraph.
268 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
277 def extractSettingsGitLog(log
):
279 for line
in log
.split("\n"):
281 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
285 assignments
= m
.group(1).split (':')
286 for a
in assignments
:
288 key
= vals
[0].strip()
289 val
= ('='.join (vals
[1:])).strip()
290 if val
.endswith ('\"') and val
.startswith('"'):
295 paths
= values
.get("depot-paths")
297 paths
= values
.get("depot-path")
299 values
['depot-paths'] = paths
.split(',')
302 def gitBranchExists(branch
):
303 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
304 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
305 return proc
.wait() == 0;
308 return read_pipe("git config %s" % key
, ignore_error
=True).strip()
310 def p4BranchesInGit(branchesAreInRemotes
= True):
313 cmdline
= "git rev-parse --symbolic "
314 if branchesAreInRemotes
:
315 cmdline
+= " --remotes"
317 cmdline
+= " --branches"
319 for line
in read_pipe_lines(cmdline
):
322 ## only import to p4/
323 if not line
.startswith('p4/') or line
== "p4/HEAD":
328 branch
= re
.sub ("^p4/", "", line
)
330 branches
[branch
] = parseRevision(line
)
333 def findUpstreamBranchPoint(head
= "HEAD"):
334 branches
= p4BranchesInGit()
335 # map from depot-path to branch name
336 branchByDepotPath
= {}
337 for branch
in branches
.keys():
338 tip
= branches
[branch
]
339 log
= extractLogMessageFromGitCommit(tip
)
340 settings
= extractSettingsGitLog(log
)
341 if settings
.has_key("depot-paths"):
342 paths
= ",".join(settings
["depot-paths"])
343 branchByDepotPath
[paths
] = "remotes/p4/" + branch
347 while parent
< 65535:
348 commit
= head
+ "~%s" % parent
349 log
= extractLogMessageFromGitCommit(commit
)
350 settings
= extractSettingsGitLog(log
)
351 if settings
.has_key("depot-paths"):
352 paths
= ",".join(settings
["depot-paths"])
353 if branchByDepotPath
.has_key(paths
):
354 return [branchByDepotPath
[paths
], settings
]
358 return ["", settings
]
360 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
362 print ("Creating/updating branch(es) in %s based on origin branch(es)"
365 originPrefix
= "origin/p4/"
367 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
369 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
372 headName
= line
[len(originPrefix
):]
373 remoteHead
= localRefPrefix
+ headName
376 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
377 if (not original
.has_key('depot-paths')
378 or not original
.has_key('change')):
382 if not gitBranchExists(remoteHead
):
384 print "creating %s" % remoteHead
387 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
388 if settings
.has_key('change') > 0:
389 if settings
['depot-paths'] == original
['depot-paths']:
390 originP4Change
= int(original
['change'])
391 p4Change
= int(settings
['change'])
392 if originP4Change
> p4Change
:
393 print ("%s (%s) is newer than %s (%s). "
394 "Updating p4 branch from origin."
395 % (originHead
, originP4Change
,
396 remoteHead
, p4Change
))
399 print ("Ignoring: %s was imported from %s while "
400 "%s was imported from %s"
401 % (originHead
, ','.join(original
['depot-paths']),
402 remoteHead
, ','.join(settings
['depot-paths'])))
405 system("git update-ref %s %s" % (remoteHead
, originHead
))
407 def originP4BranchesExist():
408 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
410 def p4ChangesForPaths(depotPaths
, changeRange
):
412 output
= p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p
, changeRange
)
413 for p
in depotPaths
]))
417 changeNum
= line
.split(" ")[1]
418 changes
.append(int(changeNum
))
425 self
.usage
= "usage: %prog [options]"
428 class P4Debug(Command
):
430 Command
.__init
__(self
)
432 optparse
.make_option("--verbose", dest
="verbose", action
="store_true",
435 self
.description
= "A tool to debug the output of p4 -G."
436 self
.needsGit
= False
441 for output
in p4CmdList(" ".join(args
)):
442 print 'Element: %d' % j
447 class P4RollBack(Command
):
449 Command
.__init
__(self
)
451 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
452 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
454 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
456 self
.rollbackLocalBranches
= False
461 maxChange
= int(args
[0])
463 if "p4ExitCode" in p4Cmd("changes -m 1"):
464 die("Problems executing p4");
466 if self
.rollbackLocalBranches
:
467 refPrefix
= "refs/heads/"
468 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
470 refPrefix
= "refs/remotes/"
471 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
474 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
476 ref
= refPrefix
+ line
477 log
= extractLogMessageFromGitCommit(ref
)
478 settings
= extractSettingsGitLog(log
)
480 depotPaths
= settings
['depot-paths']
481 change
= settings
['change']
485 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
486 for p
in depotPaths
]))) == 0:
487 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
488 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
491 while change
and int(change
) > maxChange
:
494 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
495 system("git update-ref %s \"%s^\"" % (ref
, ref
))
496 log
= extractLogMessageFromGitCommit(ref
)
497 settings
= extractSettingsGitLog(log
)
500 depotPaths
= settings
['depot-paths']
501 change
= settings
['change']
504 print "%s rewound to %s" % (ref
, change
)
508 class P4Submit(Command
):
510 Command
.__init
__(self
)
512 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
513 optparse
.make_option("--origin", dest
="origin"),
514 optparse
.make_option("-M", dest
="detectRename", action
="store_true"),
516 self
.description
= "Submit changes from git to the perforce depot."
517 self
.usage
+= " [name of git branch to submit into perforce depot]"
518 self
.interactive
= True
520 self
.detectRename
= False
522 self
.isWindows
= (platform
.system() == "Windows")
525 if len(p4CmdList("opened ...")) > 0:
526 die("You have files opened with perforce! Close them before starting the sync.")
528 # replaces everything between 'Description:' and the next P4 submit template field with the
530 def prepareLogMessage(self
, template
, message
):
533 inDescriptionSection
= False
535 for line
in template
.split("\n"):
536 if line
.startswith("#"):
537 result
+= line
+ "\n"
540 if inDescriptionSection
:
541 if line
.startswith("Files:"):
542 inDescriptionSection
= False
546 if line
.startswith("Description:"):
547 inDescriptionSection
= True
549 for messageLine
in message
.split("\n"):
550 line
+= "\t" + messageLine
+ "\n"
552 result
+= line
+ "\n"
556 def prepareSubmitTemplate(self
):
557 # remove lines in the Files section that show changes to files outside the depot path we're committing into
559 inFilesSection
= False
560 for line
in p4_read_pipe_lines("change -o"):
561 if line
.endswith("\r\n"):
562 line
= line
[:-2] + "\n"
564 if line
.startswith("\t"):
565 # path starts and ends with a tab
567 lastTab
= path
.rfind("\t")
569 path
= path
[:lastTab
]
570 if not path
.startswith(self
.depotPath
):
573 inFilesSection
= False
575 if line
.startswith("Files:"):
576 inFilesSection
= True
582 def applyCommit(self
, id):
583 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
584 diffOpts
= ("", "-M")[self
.detectRename
]
585 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts
, id, id))
587 filesToDelete
= set()
589 filesToChangeExecBit
= {}
591 diff
= parseDiffTreeEntry(line
)
592 modifier
= diff
['status']
595 p4_system("edit \"%s\"" % path
)
596 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
597 filesToChangeExecBit
[path
] = diff
['dst_mode']
598 editedFiles
.add(path
)
599 elif modifier
== "A":
601 filesToChangeExecBit
[path
] = diff
['dst_mode']
602 if path
in filesToDelete
:
603 filesToDelete
.remove(path
)
604 elif modifier
== "D":
605 filesToDelete
.add(path
)
606 if path
in filesToAdd
:
607 filesToAdd
.remove(path
)
608 elif modifier
== "R":
609 src
, dest
= diff
['src'], diff
['dst']
610 p4_system("integrate -Dt \"%s\" \"%s\"" % (src
, dest
))
611 p4_system("edit \"%s\"" % (dest
))
612 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
613 filesToChangeExecBit
[dest
] = diff
['dst_mode']
615 editedFiles
.add(dest
)
616 filesToDelete
.add(src
)
618 die("unknown modifier %s for %s" % (modifier
, path
))
620 diffcmd
= "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
621 patchcmd
= diffcmd
+ " | git apply "
622 tryPatchCmd
= patchcmd
+ "--check -"
623 applyPatchCmd
= patchcmd
+ "--check --apply -"
625 if os
.system(tryPatchCmd
) != 0:
626 print "Unfortunately applying the change failed!"
627 print "What do you want to do?"
629 while response
!= "s" and response
!= "a" and response
!= "w":
630 response
= raw_input("[s]kip this patch / [a]pply the patch forcibly "
631 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
633 print "Skipping! Good luck with the next patches..."
634 for f
in editedFiles
:
635 p4_system("revert \"%s\"" % f
);
639 elif response
== "a":
640 os
.system(applyPatchCmd
)
641 if len(filesToAdd
) > 0:
642 print "You may also want to call p4 add on the following files:"
643 print " ".join(filesToAdd
)
644 if len(filesToDelete
):
645 print "The following files should be scheduled for deletion with p4 delete:"
646 print " ".join(filesToDelete
)
647 die("Please resolve and submit the conflict manually and "
648 + "continue afterwards with git-p4 submit --continue")
649 elif response
== "w":
650 system(diffcmd
+ " > patch.txt")
651 print "Patch saved to patch.txt in %s !" % self
.clientPath
652 die("Please resolve and submit the conflict manually and "
653 "continue afterwards with git-p4 submit --continue")
655 system(applyPatchCmd
)
658 p4_system("add \"%s\"" % f
)
659 for f
in filesToDelete
:
660 p4_system("revert \"%s\"" % f
)
661 p4_system("delete \"%s\"" % f
)
663 # Set/clear executable bits
664 for f
in filesToChangeExecBit
.keys():
665 mode
= filesToChangeExecBit
[f
]
666 setP4ExecBit(f
, mode
)
668 logMessage
= extractLogMessageFromGitCommit(id)
669 logMessage
= logMessage
.strip()
671 template
= self
.prepareSubmitTemplate()
674 submitTemplate
= self
.prepareLogMessage(template
, logMessage
)
675 if os
.environ
.has_key("P4DIFF"):
676 del(os
.environ
["P4DIFF"])
677 diff
= read_pipe("p4 diff -du ...")
680 for newFile
in filesToAdd
:
681 newdiff
+= "==== new file ====\n"
682 newdiff
+= "--- /dev/null\n"
683 newdiff
+= "+++ %s\n" % newFile
684 f
= open(newFile
, "r")
685 for line
in f
.readlines():
686 newdiff
+= "+" + line
689 separatorLine
= "######## everything below this line is just the diff #######\n"
691 [handle
, fileName
] = tempfile
.mkstemp()
692 tmpFile
= os
.fdopen(handle
, "w+")
694 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
695 separatorLine
= separatorLine
.replace("\n", "\r\n")
696 newdiff
= newdiff
.replace("\n", "\r\n")
697 tmpFile
.write(submitTemplate
+ separatorLine
+ diff
+ newdiff
)
700 if platform
.system() == "Windows":
701 defaultEditor
= "notepad"
702 if os
.environ
.has_key("P4EDITOR"):
703 editor
= os
.environ
.get("P4EDITOR")
705 editor
= os
.environ
.get("EDITOR", defaultEditor
);
706 system(editor
+ " " + fileName
)
707 tmpFile
= open(fileName
, "rb")
708 message
= tmpFile
.read()
711 submitTemplate
= message
[:message
.index(separatorLine
)]
713 submitTemplate
= submitTemplate
.replace("\r\n", "\n")
715 write_pipe("p4 submit -i", submitTemplate
)
717 fileName
= "submit.txt"
718 file = open(fileName
, "w+")
719 file.write(self
.prepareLogMessage(template
, logMessage
))
721 print ("Perforce submit template written as %s. "
722 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
723 % (fileName
, fileName
))
727 self
.master
= currentGitBranch()
728 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
729 die("Detecting current git branch failed!")
731 self
.master
= args
[0]
735 allowSubmit
= gitConfig("git-p4.allowSubmit")
736 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
737 die("%s is not in git-p4.allowSubmit" % self
.master
)
739 [upstream
, settings
] = findUpstreamBranchPoint()
740 self
.depotPath
= settings
['depot-paths'][0]
741 if len(self
.origin
) == 0:
742 self
.origin
= upstream
745 print "Origin branch is " + self
.origin
747 if len(self
.depotPath
) == 0:
748 print "Internal error: cannot locate perforce depot path from existing branches"
751 self
.clientPath
= p4Where(self
.depotPath
)
753 if len(self
.clientPath
) == 0:
754 print "Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
757 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
758 self
.oldWorkingDirectory
= os
.getcwd()
760 os
.chdir(self
.clientPath
)
761 print "Syncronizing p4 checkout..."
762 p4_system("sync ...")
767 for line
in read_pipe_lines("git rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)):
768 commits
.append(line
.strip())
771 while len(commits
) > 0:
773 commits
= commits
[1:]
774 self
.applyCommit(commit
)
775 if not self
.interactive
:
778 if len(commits
) == 0:
779 print "All changes applied!"
780 os
.chdir(self
.oldWorkingDirectory
)
790 class P4Sync(Command
):
792 Command
.__init
__(self
)
794 optparse
.make_option("--branch", dest
="branch"),
795 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
796 optparse
.make_option("--changesfile", dest
="changesFile"),
797 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
798 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
799 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
800 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
801 help="Import into refs/heads/ , not refs/remotes"),
802 optparse
.make_option("--max-changes", dest
="maxChanges"),
803 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
804 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
805 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
806 help="Only sync files that are included in the Perforce Client Spec")
808 self
.description
= """Imports from Perforce into a git repository.\n
810 //depot/my/project/ -- to import the current head
811 //depot/my/project/@all -- to import everything
812 //depot/my/project/@1,6 -- to import only from revision 1 to 6
814 (a ... is not needed in the path p4 specification, it's added implicitly)"""
816 self
.usage
+= " //depot/path[@revRange]"
818 self
.createdBranches
= Set()
819 self
.committedChanges
= Set()
821 self
.detectBranches
= False
822 self
.detectLabels
= False
823 self
.changesFile
= ""
824 self
.syncWithOrigin
= True
826 self
.importIntoRemotes
= True
828 self
.isWindows
= (platform
.system() == "Windows")
829 self
.keepRepoPath
= False
830 self
.depotPaths
= None
831 self
.p4BranchesInGit
= []
832 self
.cloneExclude
= []
833 self
.useClientSpec
= False
834 self
.clientSpecDirs
= []
836 if gitConfig("git-p4.syncFromOrigin") == "false":
837 self
.syncWithOrigin
= False
839 def extractFilesFromCommit(self
, commit
):
840 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
841 for path
in self
.cloneExclude
]
844 while commit
.has_key("depotFile%s" % fnum
):
845 path
= commit
["depotFile%s" % fnum
]
847 if [p
for p
in self
.cloneExclude
848 if path
.startswith (p
)]:
851 found
= [p
for p
in self
.depotPaths
852 if path
.startswith (p
)]
859 file["rev"] = commit
["rev%s" % fnum
]
860 file["action"] = commit
["action%s" % fnum
]
861 file["type"] = commit
["type%s" % fnum
]
866 def stripRepoPath(self
, path
, prefixes
):
867 if self
.keepRepoPath
:
868 prefixes
= [re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])]
871 if path
.startswith(p
):
876 def splitFilesIntoBranches(self
, commit
):
879 while commit
.has_key("depotFile%s" % fnum
):
880 path
= commit
["depotFile%s" % fnum
]
881 found
= [p
for p
in self
.depotPaths
882 if path
.startswith (p
)]
889 file["rev"] = commit
["rev%s" % fnum
]
890 file["action"] = commit
["action%s" % fnum
]
891 file["type"] = commit
["type%s" % fnum
]
894 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
896 for branch
in self
.knownBranches
.keys():
898 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
899 if relPath
.startswith(branch
+ "/"):
900 if branch
not in branches
:
901 branches
[branch
] = []
902 branches
[branch
].append(file)
907 ## Should move this out, doesn't use SELF.
908 def readP4Files(self
, files
):
914 for val
in self
.clientSpecDirs
:
915 if f
['path'].startswith(val
[0]):
921 filesForCommit
.append(f
)
922 if f
['action'] != 'delete':
923 filesToRead
.append(f
)
926 if len(filesToRead
) > 0:
927 filedata
= p4CmdList('-x - print',
928 stdin
='\n'.join(['%s#%s' % (f
['path'], f
['rev'])
929 for f
in filesToRead
]),
932 if "p4ExitCode" in filedata
[0]:
933 die("Problems executing p4. Error: [%d]."
934 % (filedata
[0]['p4ExitCode']));
938 while j
< len(filedata
):
942 while j
< len(filedata
) and filedata
[j
]['code'] in ('text', 'unicode', 'binary'):
943 text
.append(filedata
[j
]['data'])
947 if not stat
.has_key('depotFile'):
948 sys
.stderr
.write("p4 print fails with: %s\n" % repr(stat
))
951 if stat
['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
952 text
= re
.sub(r
'(?i)\$(Id|Header):[^$]*\$',r
'$\1$', text
)
953 elif stat
['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
954 text
= re
.sub(r
'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r
'$\1$', text
)
956 contents
[stat
['depotFile']] = text
958 for f
in filesForCommit
:
960 if contents
.has_key(path
):
961 f
['data'] = contents
[path
]
963 return filesForCommit
965 def commit(self
, details
, files
, branch
, branchPrefixes
, parent
= ""):
966 epoch
= details
["time"]
967 author
= details
["user"]
970 print "commit into %s" % branch
972 # start with reading files; if that fails, we should not
976 if [p
for p
in branchPrefixes
if f
['path'].startswith(p
)]:
979 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % path
)
980 files
= self
.readP4Files(new_files
)
982 self
.gitStream
.write("commit %s\n" % branch
)
983 # gitStream.write("mark :%s\n" % details["change"])
984 self
.committedChanges
.add(int(details
["change"]))
986 if author
not in self
.users
:
987 self
.getUserMapFromPerforceServer()
988 if author
in self
.users
:
989 committer
= "%s %s %s" % (self
.users
[author
], epoch
, self
.tz
)
991 committer
= "%s <a@b> %s %s" % (author
, epoch
, self
.tz
)
993 self
.gitStream
.write("committer %s\n" % committer
)
995 self
.gitStream
.write("data <<EOT\n")
996 self
.gitStream
.write(details
["desc"])
997 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s"
998 % (','.join (branchPrefixes
), details
["change"]))
999 if len(details
['options']) > 0:
1000 self
.gitStream
.write(": options = %s" % details
['options'])
1001 self
.gitStream
.write("]\nEOT\n\n")
1005 print "parent %s" % parent
1006 self
.gitStream
.write("from %s\n" % parent
)
1009 if file["type"] == "apple":
1010 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1013 relPath
= self
.stripRepoPath(file['path'], branchPrefixes
)
1014 if file["action"] == "delete":
1015 self
.gitStream
.write("D %s\n" % relPath
)
1020 if isP4Exec(file["type"]):
1022 elif file["type"] == "symlink":
1024 # p4 print on a symlink contains "target\n", so strip it off
1027 if self
.isWindows
and file["type"].endswith("text"):
1028 data
= data
.replace("\r\n", "\n")
1030 self
.gitStream
.write("M %s inline %s\n" % (mode
, relPath
))
1031 self
.gitStream
.write("data %s\n" % len(data
))
1032 self
.gitStream
.write(data
)
1033 self
.gitStream
.write("\n")
1035 self
.gitStream
.write("\n")
1037 change
= int(details
["change"])
1039 if self
.labels
.has_key(change
):
1040 label
= self
.labels
[change
]
1041 labelDetails
= label
[0]
1042 labelRevisions
= label
[1]
1044 print "Change %s is labelled %s" % (change
, labelDetails
)
1046 files
= p4CmdList("files " + ' '.join (["%s...@%s" % (p
, change
)
1047 for p
in branchPrefixes
]))
1049 if len(files
) == len(labelRevisions
):
1053 if info
["action"] == "delete":
1055 cleanedFiles
[info
["depotFile"]] = info
["rev"]
1057 if cleanedFiles
== labelRevisions
:
1058 self
.gitStream
.write("tag tag_%s\n" % labelDetails
["label"])
1059 self
.gitStream
.write("from %s\n" % branch
)
1061 owner
= labelDetails
["Owner"]
1063 if author
in self
.users
:
1064 tagger
= "%s %s %s" % (self
.users
[owner
], epoch
, self
.tz
)
1066 tagger
= "%s <a@b> %s %s" % (owner
, epoch
, self
.tz
)
1067 self
.gitStream
.write("tagger %s\n" % tagger
)
1068 self
.gitStream
.write("data <<EOT\n")
1069 self
.gitStream
.write(labelDetails
["Description"])
1070 self
.gitStream
.write("EOT\n\n")
1074 print ("Tag %s does not match with change %s: files do not match."
1075 % (labelDetails
["label"], change
))
1079 print ("Tag %s does not match with change %s: file count is different."
1080 % (labelDetails
["label"], change
))
1082 def getUserCacheFilename(self
):
1083 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
1084 return home
+ "/.gitp4-usercache.txt"
1086 def getUserMapFromPerforceServer(self
):
1087 if self
.userMapFromPerforceServer
:
1091 for output
in p4CmdList("users"):
1092 if not output
.has_key("User"):
1094 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
1098 for (key
, val
) in self
.users
.items():
1099 s
+= "%s\t%s\n" % (key
, val
)
1101 open(self
.getUserCacheFilename(), "wb").write(s
)
1102 self
.userMapFromPerforceServer
= True
1104 def loadUserMapFromCache(self
):
1106 self
.userMapFromPerforceServer
= False
1108 cache
= open(self
.getUserCacheFilename(), "rb")
1109 lines
= cache
.readlines()
1112 entry
= line
.strip().split("\t")
1113 self
.users
[entry
[0]] = entry
[1]
1115 self
.getUserMapFromPerforceServer()
1117 def getLabels(self
):
1120 l
= p4CmdList("labels %s..." % ' '.join (self
.depotPaths
))
1121 if len(l
) > 0 and not self
.silent
:
1122 print "Finding files belonging to labels in %s" % `self
.depotPaths`
1125 label
= output
["label"]
1129 print "Querying files for label %s" % label
1130 for file in p4CmdList("files "
1131 + ' '.join (["%s...@%s" % (p
, label
)
1132 for p
in self
.depotPaths
])):
1133 revisions
[file["depotFile"]] = file["rev"]
1134 change
= int(file["change"])
1135 if change
> newestChange
:
1136 newestChange
= change
1138 self
.labels
[newestChange
] = [output
, revisions
]
1141 print "Label changes: %s" % self
.labels
.keys()
1143 def guessProjectName(self
):
1144 for p
in self
.depotPaths
:
1147 p
= p
[p
.strip().rfind("/") + 1:]
1148 if not p
.endswith("/"):
1152 def getBranchMapping(self
):
1153 lostAndFoundBranches
= set()
1155 for info
in p4CmdList("branches"):
1156 details
= p4Cmd("branch -o %s" % info
["branch"])
1158 while details
.has_key("View%s" % viewIdx
):
1159 paths
= details
["View%s" % viewIdx
].split(" ")
1160 viewIdx
= viewIdx
+ 1
1161 # require standard //depot/foo/... //depot/bar/... mapping
1162 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
1165 destination
= paths
[1]
1167 if source
.startswith(self
.depotPaths
[0]) and destination
.startswith(self
.depotPaths
[0]):
1168 source
= source
[len(self
.depotPaths
[0]):-4]
1169 destination
= destination
[len(self
.depotPaths
[0]):-4]
1171 if destination
in self
.knownBranches
:
1173 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
1174 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
1177 self
.knownBranches
[destination
] = source
1179 lostAndFoundBranches
.discard(destination
)
1181 if source
not in self
.knownBranches
:
1182 lostAndFoundBranches
.add(source
)
1185 for branch
in lostAndFoundBranches
:
1186 self
.knownBranches
[branch
] = branch
1188 def getBranchMappingFromGitBranches(self
):
1189 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1190 for branch
in branches
.keys():
1191 if branch
== "master":
1194 branch
= branch
[len(self
.projectName
):]
1195 self
.knownBranches
[branch
] = branch
1197 def listExistingP4GitBranches(self
):
1198 # branches holds mapping from name to commit
1199 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1200 self
.p4BranchesInGit
= branches
.keys()
1201 for branch
in branches
.keys():
1202 self
.initialParents
[self
.refPrefix
+ branch
] = branches
[branch
]
1204 def updateOptionDict(self
, d
):
1206 if self
.keepRepoPath
:
1207 option_keys
['keepRepoPath'] = 1
1209 d
["options"] = ' '.join(sorted(option_keys
.keys()))
1211 def readOptions(self
, d
):
1212 self
.keepRepoPath
= (d
.has_key('options')
1213 and ('keepRepoPath' in d
['options']))
1215 def gitRefForBranch(self
, branch
):
1216 if branch
== "main":
1217 return self
.refPrefix
+ "master"
1219 if len(branch
) <= 0:
1222 return self
.refPrefix
+ self
.projectName
+ branch
1224 def gitCommitByP4Change(self
, ref
, change
):
1226 print "looking in ref " + ref
+ " for change %s using bisect..." % change
1229 latestCommit
= parseRevision(ref
)
1233 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
1234 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
1239 log
= extractLogMessageFromGitCommit(next
)
1240 settings
= extractSettingsGitLog(log
)
1241 currentChange
= int(settings
['change'])
1243 print "current change %s" % currentChange
1245 if currentChange
== change
:
1247 print "found %s" % next
1250 if currentChange
< change
:
1251 earliestCommit
= "^%s" % next
1253 latestCommit
= "%s" % next
1257 def importNewBranch(self
, branch
, maxChange
):
1258 # make fast-import flush all changes to disk and update the refs using the checkpoint
1259 # command so that we can try to find the branch parent in the git history
1260 self
.gitStream
.write("checkpoint\n\n");
1261 self
.gitStream
.flush();
1262 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1263 range = "@1,%s" % maxChange
1264 #print "prefix" + branchPrefix
1265 changes
= p4ChangesForPaths([branchPrefix
], range)
1266 if len(changes
) <= 0:
1268 firstChange
= changes
[0]
1269 #print "first change in branch: %s" % firstChange
1270 sourceBranch
= self
.knownBranches
[branch
]
1271 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
1272 sourceRef
= self
.gitRefForBranch(sourceBranch
)
1273 #print "source " + sourceBranch
1275 branchParentChange
= int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath
, firstChange
))["change"])
1276 #print "branch parent: %s" % branchParentChange
1277 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
1278 if len(gitParent
) > 0:
1279 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
1280 #print "parent git commit: %s" % gitParent
1282 self
.importChanges(changes
)
1285 def importChanges(self
, changes
):
1287 for change
in changes
:
1288 description
= p4Cmd("describe %s" % change
)
1289 self
.updateOptionDict(description
)
1292 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
1297 if self
.detectBranches
:
1298 branches
= self
.splitFilesIntoBranches(description
)
1299 for branch
in branches
.keys():
1301 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1305 filesForCommit
= branches
[branch
]
1308 print "branch is %s" % branch
1310 self
.updatedBranches
.add(branch
)
1312 if branch
not in self
.createdBranches
:
1313 self
.createdBranches
.add(branch
)
1314 parent
= self
.knownBranches
[branch
]
1315 if parent
== branch
:
1318 fullBranch
= self
.projectName
+ branch
1319 if fullBranch
not in self
.p4BranchesInGit
:
1321 print("\n Importing new branch %s" % fullBranch
);
1322 if self
.importNewBranch(branch
, change
- 1):
1324 self
.p4BranchesInGit
.append(fullBranch
)
1326 print("\n Resuming with change %s" % change
);
1329 print "parent determined through known branches: %s" % parent
1331 branch
= self
.gitRefForBranch(branch
)
1332 parent
= self
.gitRefForBranch(parent
)
1335 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
1337 if len(parent
) == 0 and branch
in self
.initialParents
:
1338 parent
= self
.initialParents
[branch
]
1339 del self
.initialParents
[branch
]
1341 self
.commit(description
, filesForCommit
, branch
, [branchPrefix
], parent
)
1343 files
= self
.extractFilesFromCommit(description
)
1344 self
.commit(description
, files
, self
.branch
, self
.depotPaths
,
1346 self
.initialParent
= ""
1348 print self
.gitError
.read()
1351 def importHeadRevision(self
, revision
):
1352 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
1354 details
= { "user" : "git perforce import user", "time" : int(time
.time()) }
1355 details
["desc"] = ("Initial import of %s from the state at revision %s"
1356 % (' '.join(self
.depotPaths
), revision
))
1357 details
["change"] = revision
1361 for info
in p4CmdList("files "
1362 + ' '.join(["%s...%s"
1364 for p
in self
.depotPaths
])):
1366 if info
['code'] == 'error':
1367 sys
.stderr
.write("p4 returned an error: %s\n"
1372 change
= int(info
["change"])
1373 if change
> newestRevision
:
1374 newestRevision
= change
1376 if info
["action"] == "delete":
1377 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1378 #fileCnt = fileCnt + 1
1381 for prop
in ["depotFile", "rev", "action", "type" ]:
1382 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
1384 fileCnt
= fileCnt
+ 1
1386 details
["change"] = newestRevision
1387 self
.updateOptionDict(details
)
1389 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
, self
.depotPaths
)
1391 print "IO error with git fast-import. Is your git version recent enough?"
1392 print self
.gitError
.read()
1395 def getClientSpec(self
):
1396 specList
= p4CmdList( "client -o" )
1398 for entry
in specList
:
1399 for k
,v
in entry
.iteritems():
1400 if k
.startswith("View"):
1401 if v
.startswith('"'):
1405 index
= v
.find("...")
1407 if v
.startswith("-"):
1412 self
.clientSpecDirs
= temp
.items()
1413 self
.clientSpecDirs
.sort( lambda x
, y
: abs( y
[1] ) - abs( x
[1] ) )
1415 def run(self
, args
):
1416 self
.depotPaths
= []
1417 self
.changeRange
= ""
1418 self
.initialParent
= ""
1419 self
.previousDepotPaths
= []
1421 # map from branch depot path to parent branch
1422 self
.knownBranches
= {}
1423 self
.initialParents
= {}
1424 self
.hasOrigin
= originP4BranchesExist()
1425 if not self
.syncWithOrigin
:
1426 self
.hasOrigin
= False
1428 if self
.importIntoRemotes
:
1429 self
.refPrefix
= "refs/remotes/p4/"
1431 self
.refPrefix
= "refs/heads/p4/"
1433 if self
.syncWithOrigin
and self
.hasOrigin
:
1435 print "Syncing with origin first by calling git fetch origin"
1436 system("git fetch origin")
1438 if len(self
.branch
) == 0:
1439 self
.branch
= self
.refPrefix
+ "master"
1440 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
1441 system("git update-ref %s refs/heads/p4" % self
.branch
)
1442 system("git branch -D p4");
1443 # create it /after/ importing, when master exists
1444 if not gitBranchExists(self
.refPrefix
+ "HEAD") and self
.importIntoRemotes
and gitBranchExists(self
.branch
):
1445 system("git symbolic-ref %sHEAD %s" % (self
.refPrefix
, self
.branch
))
1447 if self
.useClientSpec
or gitConfig("git-p4.useclientspec") == "true":
1448 self
.getClientSpec()
1450 # TODO: should always look at previous commits,
1451 # merge with previous imports, if possible.
1454 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
1455 self
.listExistingP4GitBranches()
1457 if len(self
.p4BranchesInGit
) > 1:
1459 print "Importing from/into multiple branches"
1460 self
.detectBranches
= True
1463 print "branches: %s" % self
.p4BranchesInGit
1466 for branch
in self
.p4BranchesInGit
:
1467 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
1469 settings
= extractSettingsGitLog(logMsg
)
1471 self
.readOptions(settings
)
1472 if (settings
.has_key('depot-paths')
1473 and settings
.has_key ('change')):
1474 change
= int(settings
['change']) + 1
1475 p4Change
= max(p4Change
, change
)
1477 depotPaths
= sorted(settings
['depot-paths'])
1478 if self
.previousDepotPaths
== []:
1479 self
.previousDepotPaths
= depotPaths
1482 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
1483 for i
in range(0, min(len(cur
), len(prev
))):
1484 if cur
[i
] <> prev
[i
]:
1488 paths
.append (cur
[:i
+ 1])
1490 self
.previousDepotPaths
= paths
1493 self
.depotPaths
= sorted(self
.previousDepotPaths
)
1494 self
.changeRange
= "@%s,#head" % p4Change
1495 if not self
.detectBranches
:
1496 self
.initialParent
= parseRevision(self
.branch
)
1497 if not self
.silent
and not self
.detectBranches
:
1498 print "Performing incremental import into %s git branch" % self
.branch
1500 if not self
.branch
.startswith("refs/"):
1501 self
.branch
= "refs/heads/" + self
.branch
1503 if len(args
) == 0 and self
.depotPaths
:
1505 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
1507 if self
.depotPaths
and self
.depotPaths
!= args
:
1508 print ("previous import used depot path %s and now %s was specified. "
1509 "This doesn't work!" % (' '.join (self
.depotPaths
),
1513 self
.depotPaths
= sorted(args
)
1519 for p
in self
.depotPaths
:
1520 if p
.find("@") != -1:
1521 atIdx
= p
.index("@")
1522 self
.changeRange
= p
[atIdx
:]
1523 if self
.changeRange
== "@all":
1524 self
.changeRange
= ""
1525 elif ',' not in self
.changeRange
:
1526 revision
= self
.changeRange
1527 self
.changeRange
= ""
1529 elif p
.find("#") != -1:
1530 hashIdx
= p
.index("#")
1531 revision
= p
[hashIdx
:]
1533 elif self
.previousDepotPaths
== []:
1536 p
= re
.sub ("\.\.\.$", "", p
)
1537 if not p
.endswith("/"):
1542 self
.depotPaths
= newPaths
1545 self
.loadUserMapFromCache()
1547 if self
.detectLabels
:
1550 if self
.detectBranches
:
1551 ## FIXME - what's a P4 projectName ?
1552 self
.projectName
= self
.guessProjectName()
1555 self
.getBranchMappingFromGitBranches()
1557 self
.getBranchMapping()
1559 print "p4-git branches: %s" % self
.p4BranchesInGit
1560 print "initial parents: %s" % self
.initialParents
1561 for b
in self
.p4BranchesInGit
:
1565 b
= b
[len(self
.projectName
):]
1566 self
.createdBranches
.add(b
)
1568 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
1570 importProcess
= subprocess
.Popen(["git", "fast-import"],
1571 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
1572 stderr
=subprocess
.PIPE
);
1573 self
.gitOutput
= importProcess
.stdout
1574 self
.gitStream
= importProcess
.stdin
1575 self
.gitError
= importProcess
.stderr
1578 self
.importHeadRevision(revision
)
1582 if len(self
.changesFile
) > 0:
1583 output
= open(self
.changesFile
).readlines()
1586 changeSet
.add(int(line
))
1588 for change
in changeSet
:
1589 changes
.append(change
)
1594 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
1596 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
)
1598 if len(self
.maxChanges
) > 0:
1599 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
1601 if len(changes
) == 0:
1603 print "No changes to import!"
1606 if not self
.silent
and not self
.detectBranches
:
1607 print "Import destination: %s" % self
.branch
1609 self
.updatedBranches
= set()
1611 self
.importChanges(changes
)
1615 if len(self
.updatedBranches
) > 0:
1616 sys
.stdout
.write("Updated branches: ")
1617 for b
in self
.updatedBranches
:
1618 sys
.stdout
.write("%s " % b
)
1619 sys
.stdout
.write("\n")
1621 self
.gitStream
.close()
1622 if importProcess
.wait() != 0:
1623 die("fast-import failed: %s" % self
.gitError
.read())
1624 self
.gitOutput
.close()
1625 self
.gitError
.close()
1629 class P4Rebase(Command
):
1631 Command
.__init
__(self
)
1633 self
.description
= ("Fetches the latest revision from perforce and "
1634 + "rebases the current work (branch) against it")
1635 self
.verbose
= False
1637 def run(self
, args
):
1641 return self
.rebase()
1644 if os
.system("git update-index --refresh") != 0:
1645 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.");
1646 if len(read_pipe("git diff-index HEAD --")) > 0:
1647 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1649 [upstream
, settings
] = findUpstreamBranchPoint()
1650 if len(upstream
) == 0:
1651 die("Cannot find upstream branchpoint for rebase")
1653 # the branchpoint may be p4/foo~3, so strip off the parent
1654 upstream
= re
.sub("~[0-9]+$", "", upstream
)
1656 print "Rebasing the current branch onto %s" % upstream
1657 oldHead
= read_pipe("git rev-parse HEAD").strip()
1658 system("git rebase %s" % upstream
)
1659 system("git diff-tree --stat --summary -M %s HEAD" % oldHead
)
1662 class P4Clone(P4Sync
):
1664 P4Sync
.__init
__(self
)
1665 self
.description
= "Creates a new git repository and imports from Perforce into it"
1666 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
1668 optparse
.make_option("--destination", dest
="cloneDestination",
1669 action
='store', default
=None,
1670 help="where to leave result of the clone"),
1671 optparse
.make_option("-/", dest
="cloneExclude",
1672 action
="append", type="string",
1673 help="exclude depot path")
1675 self
.cloneDestination
= None
1676 self
.needsGit
= False
1678 # This is required for the "append" cloneExclude action
1679 def ensure_value(self
, attr
, value
):
1680 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
1681 setattr(self
, attr
, value
)
1682 return getattr(self
, attr
)
1684 def defaultDestination(self
, args
):
1685 ## TODO: use common prefix of args?
1687 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
1688 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
1689 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
1690 depotDir
= re
.sub(r
"/$", "", depotDir
)
1691 return os
.path
.split(depotDir
)[1]
1693 def run(self
, args
):
1697 if self
.keepRepoPath
and not self
.cloneDestination
:
1698 sys
.stderr
.write("Must specify destination for --keep-path\n")
1703 if not self
.cloneDestination
and len(depotPaths
) > 1:
1704 self
.cloneDestination
= depotPaths
[-1]
1705 depotPaths
= depotPaths
[:-1]
1707 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
1708 for p
in depotPaths
:
1709 if not p
.startswith("//"):
1712 if not self
.cloneDestination
:
1713 self
.cloneDestination
= self
.defaultDestination(args
)
1715 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
1716 if not os
.path
.exists(self
.cloneDestination
):
1717 os
.makedirs(self
.cloneDestination
)
1718 os
.chdir(self
.cloneDestination
)
1720 self
.gitdir
= os
.getcwd() + "/.git"
1721 if not P4Sync
.run(self
, depotPaths
):
1723 if self
.branch
!= "master":
1724 if gitBranchExists("refs/remotes/p4/master"):
1725 system("git branch master refs/remotes/p4/master")
1726 system("git checkout -f")
1728 print "Could not detect main branch. No checkout/master branch created."
1732 class P4Branches(Command
):
1734 Command
.__init
__(self
)
1736 self
.description
= ("Shows the git branches that hold imports and their "
1737 + "corresponding perforce depot paths")
1738 self
.verbose
= False
1740 def run(self
, args
):
1741 if originP4BranchesExist():
1742 createOrUpdateBranchesFromOrigin()
1744 cmdline
= "git rev-parse --symbolic "
1745 cmdline
+= " --remotes"
1747 for line
in read_pipe_lines(cmdline
):
1750 if not line
.startswith('p4/') or line
== "p4/HEAD":
1754 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
1755 settings
= extractSettingsGitLog(log
)
1757 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
1760 class HelpFormatter(optparse
.IndentedHelpFormatter
):
1762 optparse
.IndentedHelpFormatter
.__init
__(self
)
1764 def format_description(self
, description
):
1766 return description
+ "\n"
1770 def printUsage(commands
):
1771 print "usage: %s <command> [options]" % sys
.argv
[0]
1773 print "valid commands: %s" % ", ".join(commands
)
1775 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
1780 "submit" : P4Submit
,
1781 "commit" : P4Submit
,
1783 "rebase" : P4Rebase
,
1785 "rollback" : P4RollBack
,
1786 "branches" : P4Branches
1791 if len(sys
.argv
[1:]) == 0:
1792 printUsage(commands
.keys())
1796 cmdName
= sys
.argv
[1]
1798 klass
= commands
[cmdName
]
1801 print "unknown command %s" % cmdName
1803 printUsage(commands
.keys())
1806 options
= cmd
.options
1807 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
1811 if len(options
) > 0:
1812 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
1814 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
1816 description
= cmd
.description
,
1817 formatter
= HelpFormatter())
1819 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
1821 verbose
= cmd
.verbose
1823 if cmd
.gitdir
== None:
1824 cmd
.gitdir
= os
.path
.abspath(".git")
1825 if not isValidGitDir(cmd
.gitdir
):
1826 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
1827 if os
.path
.exists(cmd
.gitdir
):
1828 cdup
= read_pipe("git rev-parse --show-cdup").strip()
1832 if not isValidGitDir(cmd
.gitdir
):
1833 if isValidGitDir(cmd
.gitdir
+ "/.git"):
1834 cmd
.gitdir
+= "/.git"
1836 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
1838 os
.environ
["GIT_DIR"] = cmd
.gitdir
1840 if not cmd
.run(args
):
1844 if __name__
== '__main__':