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 p4_write_pipe(c
, str):
73 real_cmd
= p4_build_cmd(c
)
74 return write_pipe(c
, str)
76 def read_pipe(c
, ignore_error
=False):
78 sys
.stderr
.write('Reading pipe: %s\n' % c
)
80 pipe
= os
.popen(c
, 'rb')
82 if pipe
.close() and not ignore_error
:
83 die('Command failed: %s' % c
)
87 def p4_read_pipe(c
, ignore_error
=False):
88 real_cmd
= p4_build_cmd(c
)
89 return read_pipe(real_cmd
, ignore_error
)
91 def read_pipe_lines(c
):
93 sys
.stderr
.write('Reading pipe: %s\n' % c
)
94 ## todo: check return status
95 pipe
= os
.popen(c
, 'rb')
96 val
= pipe
.readlines()
98 die('Command failed: %s' % c
)
102 def p4_read_pipe_lines(c
):
103 """Specifically invoke p4 on the command supplied. """
104 real_cmd
= p4_build_cmd(c
)
105 return read_pipe_lines(real_cmd
)
109 sys
.stderr
.write("executing %s\n" % cmd
)
110 if os
.system(cmd
) != 0:
111 die("command failed: %s" % cmd
)
114 """Specifically invoke p4 as the system command. """
115 real_cmd
= p4_build_cmd(cmd
)
116 return system(real_cmd
)
119 """Determine if a Perforce 'kind' should have execute permission
121 'p4 help filetypes' gives a list of the types. If it starts with 'x',
122 or x follows one of a few letters. Otherwise, if there is an 'x' after
123 a plus sign, it is also executable"""
124 return (re
.search(r
"(^[cku]?x)|\+.*x", kind
) != None)
126 def setP4ExecBit(file, mode
):
127 # Reopens an already open file and changes the execute bit to match
128 # the execute bit setting in the passed in mode.
132 if not isModeExec(mode
):
133 p4Type
= getP4OpenedType(file)
134 p4Type
= re
.sub('^([cku]?)x(.*)', '\\1\\2', p4Type
)
135 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
136 if p4Type
[-1] == "+":
137 p4Type
= p4Type
[0:-1]
139 p4_system("reopen -t %s %s" % (p4Type
, file))
141 def getP4OpenedType(file):
142 # Returns the perforce file type for the given file.
144 result
= p4_read_pipe("opened %s" % file)
145 match
= re
.match(".*\((.+)\)\r?$", result
)
147 return match
.group(1)
149 die("Could not determine file type for %s (result: '%s')" % (file, result
))
151 def diffTreePattern():
152 # This is a simple generator for the diff tree regex pattern. This could be
153 # a class variable if this and parseDiffTreeEntry were a part of a class.
154 pattern
= re
.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
158 def parseDiffTreeEntry(entry
):
159 """Parses a single diff tree entry into its component elements.
161 See git-diff-tree(1) manpage for details about the format of the diff
162 output. This method returns a dictionary with the following elements:
164 src_mode - The mode of the source file
165 dst_mode - The mode of the destination file
166 src_sha1 - The sha1 for the source file
167 dst_sha1 - The sha1 fr the destination file
168 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
169 status_score - The score for the status (applicable for 'C' and 'R'
170 statuses). This is None if there is no score.
171 src - The path for the source file.
172 dst - The path for the destination file. This is only present for
173 copy or renames. If it is not present, this is None.
175 If the pattern is not matched, None is returned."""
177 match
= diffTreePattern().next().match(entry
)
180 'src_mode': match
.group(1),
181 'dst_mode': match
.group(2),
182 'src_sha1': match
.group(3),
183 'dst_sha1': match
.group(4),
184 'status': match
.group(5),
185 'status_score': match
.group(6),
186 'src': match
.group(7),
187 'dst': match
.group(10)
191 def isModeExec(mode
):
192 # Returns True if the given git mode represents an executable file,
194 return mode
[-3:] == "755"
196 def isModeExecChanged(src_mode
, dst_mode
):
197 return isModeExec(src_mode
) != isModeExec(dst_mode
)
199 def p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b'):
200 cmd
= p4_build_cmd("-G %s" % (cmd
))
202 sys
.stderr
.write("Opening pipe: %s\n" % cmd
)
204 # Use a temporary file to avoid deadlocks without
205 # subprocess.communicate(), which would put another copy
206 # of stdout into memory.
208 if stdin
is not None:
209 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
210 stdin_file
.write(stdin
)
214 p4
= subprocess
.Popen(cmd
, shell
=True,
216 stdout
=subprocess
.PIPE
)
221 entry
= marshal
.load(p4
.stdout
)
228 entry
["p4ExitCode"] = exitCode
234 list = p4CmdList(cmd
)
240 def p4Where(depotPath
):
241 if not depotPath
.endswith("/"):
243 output
= p4Cmd("where %s..." % depotPath
)
244 if output
["code"] == "error":
248 clientPath
= output
.get("path")
249 elif "data" in output
:
250 data
= output
.get("data")
251 lastSpace
= data
.rfind(" ")
252 clientPath
= data
[lastSpace
+ 1:]
254 if clientPath
.endswith("..."):
255 clientPath
= clientPath
[:-3]
258 def currentGitBranch():
259 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
261 def isValidGitDir(path
):
262 if (os
.path
.exists(path
+ "/HEAD")
263 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
267 def parseRevision(ref
):
268 return read_pipe("git rev-parse %s" % ref
).strip()
270 def extractLogMessageFromGitCommit(commit
):
273 ## fixme: title is first line of commit, not 1st paragraph.
275 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
284 def extractSettingsGitLog(log
):
286 for line
in log
.split("\n"):
288 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
292 assignments
= m
.group(1).split (':')
293 for a
in assignments
:
295 key
= vals
[0].strip()
296 val
= ('='.join (vals
[1:])).strip()
297 if val
.endswith ('\"') and val
.startswith('"'):
302 paths
= values
.get("depot-paths")
304 paths
= values
.get("depot-path")
306 values
['depot-paths'] = paths
.split(',')
309 def gitBranchExists(branch
):
310 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
311 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
312 return proc
.wait() == 0;
315 return read_pipe("git config %s" % key
, ignore_error
=True).strip()
317 def p4BranchesInGit(branchesAreInRemotes
= True):
320 cmdline
= "git rev-parse --symbolic "
321 if branchesAreInRemotes
:
322 cmdline
+= " --remotes"
324 cmdline
+= " --branches"
326 for line
in read_pipe_lines(cmdline
):
329 ## only import to p4/
330 if not line
.startswith('p4/') or line
== "p4/HEAD":
335 branch
= re
.sub ("^p4/", "", line
)
337 branches
[branch
] = parseRevision(line
)
340 def findUpstreamBranchPoint(head
= "HEAD"):
341 branches
= p4BranchesInGit()
342 # map from depot-path to branch name
343 branchByDepotPath
= {}
344 for branch
in branches
.keys():
345 tip
= branches
[branch
]
346 log
= extractLogMessageFromGitCommit(tip
)
347 settings
= extractSettingsGitLog(log
)
348 if settings
.has_key("depot-paths"):
349 paths
= ",".join(settings
["depot-paths"])
350 branchByDepotPath
[paths
] = "remotes/p4/" + branch
354 while parent
< 65535:
355 commit
= head
+ "~%s" % parent
356 log
= extractLogMessageFromGitCommit(commit
)
357 settings
= extractSettingsGitLog(log
)
358 if settings
.has_key("depot-paths"):
359 paths
= ",".join(settings
["depot-paths"])
360 if branchByDepotPath
.has_key(paths
):
361 return [branchByDepotPath
[paths
], settings
]
365 return ["", settings
]
367 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
369 print ("Creating/updating branch(es) in %s based on origin branch(es)"
372 originPrefix
= "origin/p4/"
374 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
376 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
379 headName
= line
[len(originPrefix
):]
380 remoteHead
= localRefPrefix
+ headName
383 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
384 if (not original
.has_key('depot-paths')
385 or not original
.has_key('change')):
389 if not gitBranchExists(remoteHead
):
391 print "creating %s" % remoteHead
394 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
395 if settings
.has_key('change') > 0:
396 if settings
['depot-paths'] == original
['depot-paths']:
397 originP4Change
= int(original
['change'])
398 p4Change
= int(settings
['change'])
399 if originP4Change
> p4Change
:
400 print ("%s (%s) is newer than %s (%s). "
401 "Updating p4 branch from origin."
402 % (originHead
, originP4Change
,
403 remoteHead
, p4Change
))
406 print ("Ignoring: %s was imported from %s while "
407 "%s was imported from %s"
408 % (originHead
, ','.join(original
['depot-paths']),
409 remoteHead
, ','.join(settings
['depot-paths'])))
412 system("git update-ref %s %s" % (remoteHead
, originHead
))
414 def originP4BranchesExist():
415 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
417 def p4ChangesForPaths(depotPaths
, changeRange
):
419 output
= p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p
, changeRange
)
420 for p
in depotPaths
]))
424 changeNum
= line
.split(" ")[1]
425 changes
.append(int(changeNum
))
432 self
.usage
= "usage: %prog [options]"
435 class P4Debug(Command
):
437 Command
.__init
__(self
)
439 optparse
.make_option("--verbose", dest
="verbose", action
="store_true",
442 self
.description
= "A tool to debug the output of p4 -G."
443 self
.needsGit
= False
448 for output
in p4CmdList(" ".join(args
)):
449 print 'Element: %d' % j
454 class P4RollBack(Command
):
456 Command
.__init
__(self
)
458 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
459 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
461 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
463 self
.rollbackLocalBranches
= False
468 maxChange
= int(args
[0])
470 if "p4ExitCode" in p4Cmd("changes -m 1"):
471 die("Problems executing p4");
473 if self
.rollbackLocalBranches
:
474 refPrefix
= "refs/heads/"
475 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
477 refPrefix
= "refs/remotes/"
478 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
481 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
483 ref
= refPrefix
+ line
484 log
= extractLogMessageFromGitCommit(ref
)
485 settings
= extractSettingsGitLog(log
)
487 depotPaths
= settings
['depot-paths']
488 change
= settings
['change']
492 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
493 for p
in depotPaths
]))) == 0:
494 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
495 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
498 while change
and int(change
) > maxChange
:
501 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
502 system("git update-ref %s \"%s^\"" % (ref
, ref
))
503 log
= extractLogMessageFromGitCommit(ref
)
504 settings
= extractSettingsGitLog(log
)
507 depotPaths
= settings
['depot-paths']
508 change
= settings
['change']
511 print "%s rewound to %s" % (ref
, change
)
515 class P4Submit(Command
):
517 Command
.__init
__(self
)
519 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
520 optparse
.make_option("--origin", dest
="origin"),
521 optparse
.make_option("-M", dest
="detectRename", action
="store_true"),
523 self
.description
= "Submit changes from git to the perforce depot."
524 self
.usage
+= " [name of git branch to submit into perforce depot]"
525 self
.interactive
= True
527 self
.detectRename
= False
529 self
.isWindows
= (platform
.system() == "Windows")
532 if len(p4CmdList("opened ...")) > 0:
533 die("You have files opened with perforce! Close them before starting the sync.")
535 # replaces everything between 'Description:' and the next P4 submit template field with the
537 def prepareLogMessage(self
, template
, message
):
540 inDescriptionSection
= False
542 for line
in template
.split("\n"):
543 if line
.startswith("#"):
544 result
+= line
+ "\n"
547 if inDescriptionSection
:
548 if line
.startswith("Files:"):
549 inDescriptionSection
= False
553 if line
.startswith("Description:"):
554 inDescriptionSection
= True
556 for messageLine
in message
.split("\n"):
557 line
+= "\t" + messageLine
+ "\n"
559 result
+= line
+ "\n"
563 def prepareSubmitTemplate(self
):
564 # remove lines in the Files section that show changes to files outside the depot path we're committing into
566 inFilesSection
= False
567 for line
in p4_read_pipe_lines("change -o"):
568 if line
.endswith("\r\n"):
569 line
= line
[:-2] + "\n"
571 if line
.startswith("\t"):
572 # path starts and ends with a tab
574 lastTab
= path
.rfind("\t")
576 path
= path
[:lastTab
]
577 if not path
.startswith(self
.depotPath
):
580 inFilesSection
= False
582 if line
.startswith("Files:"):
583 inFilesSection
= True
589 def applyCommit(self
, id):
590 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
591 diffOpts
= ("", "-M")[self
.detectRename
]
592 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts
, id, id))
594 filesToDelete
= set()
596 filesToChangeExecBit
= {}
598 diff
= parseDiffTreeEntry(line
)
599 modifier
= diff
['status']
602 p4_system("edit \"%s\"" % path
)
603 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
604 filesToChangeExecBit
[path
] = diff
['dst_mode']
605 editedFiles
.add(path
)
606 elif modifier
== "A":
608 filesToChangeExecBit
[path
] = diff
['dst_mode']
609 if path
in filesToDelete
:
610 filesToDelete
.remove(path
)
611 elif modifier
== "D":
612 filesToDelete
.add(path
)
613 if path
in filesToAdd
:
614 filesToAdd
.remove(path
)
615 elif modifier
== "R":
616 src
, dest
= diff
['src'], diff
['dst']
617 p4_system("integrate -Dt \"%s\" \"%s\"" % (src
, dest
))
618 p4_system("edit \"%s\"" % (dest
))
619 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
620 filesToChangeExecBit
[dest
] = diff
['dst_mode']
622 editedFiles
.add(dest
)
623 filesToDelete
.add(src
)
625 die("unknown modifier %s for %s" % (modifier
, path
))
627 diffcmd
= "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
628 patchcmd
= diffcmd
+ " | git apply "
629 tryPatchCmd
= patchcmd
+ "--check -"
630 applyPatchCmd
= patchcmd
+ "--check --apply -"
632 if os
.system(tryPatchCmd
) != 0:
633 print "Unfortunately applying the change failed!"
634 print "What do you want to do?"
636 while response
!= "s" and response
!= "a" and response
!= "w":
637 response
= raw_input("[s]kip this patch / [a]pply the patch forcibly "
638 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
640 print "Skipping! Good luck with the next patches..."
641 for f
in editedFiles
:
642 p4_system("revert \"%s\"" % f
);
646 elif response
== "a":
647 os
.system(applyPatchCmd
)
648 if len(filesToAdd
) > 0:
649 print "You may also want to call p4 add on the following files:"
650 print " ".join(filesToAdd
)
651 if len(filesToDelete
):
652 print "The following files should be scheduled for deletion with p4 delete:"
653 print " ".join(filesToDelete
)
654 die("Please resolve and submit the conflict manually and "
655 + "continue afterwards with git-p4 submit --continue")
656 elif response
== "w":
657 system(diffcmd
+ " > patch.txt")
658 print "Patch saved to patch.txt in %s !" % self
.clientPath
659 die("Please resolve and submit the conflict manually and "
660 "continue afterwards with git-p4 submit --continue")
662 system(applyPatchCmd
)
665 p4_system("add \"%s\"" % f
)
666 for f
in filesToDelete
:
667 p4_system("revert \"%s\"" % f
)
668 p4_system("delete \"%s\"" % f
)
670 # Set/clear executable bits
671 for f
in filesToChangeExecBit
.keys():
672 mode
= filesToChangeExecBit
[f
]
673 setP4ExecBit(f
, mode
)
675 logMessage
= extractLogMessageFromGitCommit(id)
676 logMessage
= logMessage
.strip()
678 template
= self
.prepareSubmitTemplate()
681 submitTemplate
= self
.prepareLogMessage(template
, logMessage
)
682 if os
.environ
.has_key("P4DIFF"):
683 del(os
.environ
["P4DIFF"])
684 diff
= p4_read_pipe("diff -du ...")
687 for newFile
in filesToAdd
:
688 newdiff
+= "==== new file ====\n"
689 newdiff
+= "--- /dev/null\n"
690 newdiff
+= "+++ %s\n" % newFile
691 f
= open(newFile
, "r")
692 for line
in f
.readlines():
693 newdiff
+= "+" + line
696 separatorLine
= "######## everything below this line is just the diff #######\n"
698 [handle
, fileName
] = tempfile
.mkstemp()
699 tmpFile
= os
.fdopen(handle
, "w+")
701 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
702 separatorLine
= separatorLine
.replace("\n", "\r\n")
703 newdiff
= newdiff
.replace("\n", "\r\n")
704 tmpFile
.write(submitTemplate
+ separatorLine
+ diff
+ newdiff
)
707 if platform
.system() == "Windows":
708 defaultEditor
= "notepad"
709 if os
.environ
.has_key("P4EDITOR"):
710 editor
= os
.environ
.get("P4EDITOR")
712 editor
= os
.environ
.get("EDITOR", defaultEditor
);
713 system(editor
+ " " + fileName
)
714 tmpFile
= open(fileName
, "rb")
715 message
= tmpFile
.read()
718 submitTemplate
= message
[:message
.index(separatorLine
)]
720 submitTemplate
= submitTemplate
.replace("\r\n", "\n")
722 p4_write_pipe("submit -i", submitTemplate
)
724 fileName
= "submit.txt"
725 file = open(fileName
, "w+")
726 file.write(self
.prepareLogMessage(template
, logMessage
))
728 print ("Perforce submit template written as %s. "
729 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
730 % (fileName
, fileName
))
734 self
.master
= currentGitBranch()
735 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
736 die("Detecting current git branch failed!")
738 self
.master
= args
[0]
742 allowSubmit
= gitConfig("git-p4.allowSubmit")
743 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
744 die("%s is not in git-p4.allowSubmit" % self
.master
)
746 [upstream
, settings
] = findUpstreamBranchPoint()
747 self
.depotPath
= settings
['depot-paths'][0]
748 if len(self
.origin
) == 0:
749 self
.origin
= upstream
752 print "Origin branch is " + self
.origin
754 if len(self
.depotPath
) == 0:
755 print "Internal error: cannot locate perforce depot path from existing branches"
758 self
.clientPath
= p4Where(self
.depotPath
)
760 if len(self
.clientPath
) == 0:
761 print "Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
764 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
765 self
.oldWorkingDirectory
= os
.getcwd()
767 os
.chdir(self
.clientPath
)
768 print "Syncronizing p4 checkout..."
769 p4_system("sync ...")
774 for line
in read_pipe_lines("git rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)):
775 commits
.append(line
.strip())
778 while len(commits
) > 0:
780 commits
= commits
[1:]
781 self
.applyCommit(commit
)
782 if not self
.interactive
:
785 if len(commits
) == 0:
786 print "All changes applied!"
787 os
.chdir(self
.oldWorkingDirectory
)
797 class P4Sync(Command
):
799 Command
.__init
__(self
)
801 optparse
.make_option("--branch", dest
="branch"),
802 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
803 optparse
.make_option("--changesfile", dest
="changesFile"),
804 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
805 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
806 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
807 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
808 help="Import into refs/heads/ , not refs/remotes"),
809 optparse
.make_option("--max-changes", dest
="maxChanges"),
810 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
811 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
812 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
813 help="Only sync files that are included in the Perforce Client Spec")
815 self
.description
= """Imports from Perforce into a git repository.\n
817 //depot/my/project/ -- to import the current head
818 //depot/my/project/@all -- to import everything
819 //depot/my/project/@1,6 -- to import only from revision 1 to 6
821 (a ... is not needed in the path p4 specification, it's added implicitly)"""
823 self
.usage
+= " //depot/path[@revRange]"
825 self
.createdBranches
= Set()
826 self
.committedChanges
= Set()
828 self
.detectBranches
= False
829 self
.detectLabels
= False
830 self
.changesFile
= ""
831 self
.syncWithOrigin
= True
833 self
.importIntoRemotes
= True
835 self
.isWindows
= (platform
.system() == "Windows")
836 self
.keepRepoPath
= False
837 self
.depotPaths
= None
838 self
.p4BranchesInGit
= []
839 self
.cloneExclude
= []
840 self
.useClientSpec
= False
841 self
.clientSpecDirs
= []
843 if gitConfig("git-p4.syncFromOrigin") == "false":
844 self
.syncWithOrigin
= False
846 def extractFilesFromCommit(self
, commit
):
847 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
848 for path
in self
.cloneExclude
]
851 while commit
.has_key("depotFile%s" % fnum
):
852 path
= commit
["depotFile%s" % fnum
]
854 if [p
for p
in self
.cloneExclude
855 if path
.startswith (p
)]:
858 found
= [p
for p
in self
.depotPaths
859 if path
.startswith (p
)]
866 file["rev"] = commit
["rev%s" % fnum
]
867 file["action"] = commit
["action%s" % fnum
]
868 file["type"] = commit
["type%s" % fnum
]
873 def stripRepoPath(self
, path
, prefixes
):
874 if self
.keepRepoPath
:
875 prefixes
= [re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])]
878 if path
.startswith(p
):
883 def splitFilesIntoBranches(self
, commit
):
886 while commit
.has_key("depotFile%s" % fnum
):
887 path
= commit
["depotFile%s" % fnum
]
888 found
= [p
for p
in self
.depotPaths
889 if path
.startswith (p
)]
896 file["rev"] = commit
["rev%s" % fnum
]
897 file["action"] = commit
["action%s" % fnum
]
898 file["type"] = commit
["type%s" % fnum
]
901 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
903 for branch
in self
.knownBranches
.keys():
905 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
906 if relPath
.startswith(branch
+ "/"):
907 if branch
not in branches
:
908 branches
[branch
] = []
909 branches
[branch
].append(file)
914 ## Should move this out, doesn't use SELF.
915 def readP4Files(self
, files
):
921 for val
in self
.clientSpecDirs
:
922 if f
['path'].startswith(val
[0]):
928 filesForCommit
.append(f
)
929 if f
['action'] != 'delete':
930 filesToRead
.append(f
)
933 if len(filesToRead
) > 0:
934 filedata
= p4CmdList('-x - print',
935 stdin
='\n'.join(['%s#%s' % (f
['path'], f
['rev'])
936 for f
in filesToRead
]),
939 if "p4ExitCode" in filedata
[0]:
940 die("Problems executing p4. Error: [%d]."
941 % (filedata
[0]['p4ExitCode']));
945 while j
< len(filedata
):
949 while j
< len(filedata
) and filedata
[j
]['code'] in ('text', 'unicode', 'binary'):
950 text
.append(filedata
[j
]['data'])
954 if not stat
.has_key('depotFile'):
955 sys
.stderr
.write("p4 print fails with: %s\n" % repr(stat
))
958 if stat
['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
959 text
= re
.sub(r
'(?i)\$(Id|Header):[^$]*\$',r
'$\1$', text
)
960 elif stat
['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
961 text
= re
.sub(r
'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r
'$\1$', text
)
963 contents
[stat
['depotFile']] = text
965 for f
in filesForCommit
:
967 if contents
.has_key(path
):
968 f
['data'] = contents
[path
]
970 return filesForCommit
972 def commit(self
, details
, files
, branch
, branchPrefixes
, parent
= ""):
973 epoch
= details
["time"]
974 author
= details
["user"]
977 print "commit into %s" % branch
979 # start with reading files; if that fails, we should not
983 if [p
for p
in branchPrefixes
if f
['path'].startswith(p
)]:
986 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % path
)
987 files
= self
.readP4Files(new_files
)
989 self
.gitStream
.write("commit %s\n" % branch
)
990 # gitStream.write("mark :%s\n" % details["change"])
991 self
.committedChanges
.add(int(details
["change"]))
993 if author
not in self
.users
:
994 self
.getUserMapFromPerforceServer()
995 if author
in self
.users
:
996 committer
= "%s %s %s" % (self
.users
[author
], epoch
, self
.tz
)
998 committer
= "%s <a@b> %s %s" % (author
, epoch
, self
.tz
)
1000 self
.gitStream
.write("committer %s\n" % committer
)
1002 self
.gitStream
.write("data <<EOT\n")
1003 self
.gitStream
.write(details
["desc"])
1004 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1005 % (','.join (branchPrefixes
), details
["change"]))
1006 if len(details
['options']) > 0:
1007 self
.gitStream
.write(": options = %s" % details
['options'])
1008 self
.gitStream
.write("]\nEOT\n\n")
1012 print "parent %s" % parent
1013 self
.gitStream
.write("from %s\n" % parent
)
1016 if file["type"] == "apple":
1017 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1020 relPath
= self
.stripRepoPath(file['path'], branchPrefixes
)
1021 if file["action"] == "delete":
1022 self
.gitStream
.write("D %s\n" % relPath
)
1027 if isP4Exec(file["type"]):
1029 elif file["type"] == "symlink":
1031 # p4 print on a symlink contains "target\n", so strip it off
1034 if self
.isWindows
and file["type"].endswith("text"):
1035 data
= data
.replace("\r\n", "\n")
1037 self
.gitStream
.write("M %s inline %s\n" % (mode
, relPath
))
1038 self
.gitStream
.write("data %s\n" % len(data
))
1039 self
.gitStream
.write(data
)
1040 self
.gitStream
.write("\n")
1042 self
.gitStream
.write("\n")
1044 change
= int(details
["change"])
1046 if self
.labels
.has_key(change
):
1047 label
= self
.labels
[change
]
1048 labelDetails
= label
[0]
1049 labelRevisions
= label
[1]
1051 print "Change %s is labelled %s" % (change
, labelDetails
)
1053 files
= p4CmdList("files " + ' '.join (["%s...@%s" % (p
, change
)
1054 for p
in branchPrefixes
]))
1056 if len(files
) == len(labelRevisions
):
1060 if info
["action"] == "delete":
1062 cleanedFiles
[info
["depotFile"]] = info
["rev"]
1064 if cleanedFiles
== labelRevisions
:
1065 self
.gitStream
.write("tag tag_%s\n" % labelDetails
["label"])
1066 self
.gitStream
.write("from %s\n" % branch
)
1068 owner
= labelDetails
["Owner"]
1070 if author
in self
.users
:
1071 tagger
= "%s %s %s" % (self
.users
[owner
], epoch
, self
.tz
)
1073 tagger
= "%s <a@b> %s %s" % (owner
, epoch
, self
.tz
)
1074 self
.gitStream
.write("tagger %s\n" % tagger
)
1075 self
.gitStream
.write("data <<EOT\n")
1076 self
.gitStream
.write(labelDetails
["Description"])
1077 self
.gitStream
.write("EOT\n\n")
1081 print ("Tag %s does not match with change %s: files do not match."
1082 % (labelDetails
["label"], change
))
1086 print ("Tag %s does not match with change %s: file count is different."
1087 % (labelDetails
["label"], change
))
1089 def getUserCacheFilename(self
):
1090 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
1091 return home
+ "/.gitp4-usercache.txt"
1093 def getUserMapFromPerforceServer(self
):
1094 if self
.userMapFromPerforceServer
:
1098 for output
in p4CmdList("users"):
1099 if not output
.has_key("User"):
1101 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
1105 for (key
, val
) in self
.users
.items():
1106 s
+= "%s\t%s\n" % (key
, val
)
1108 open(self
.getUserCacheFilename(), "wb").write(s
)
1109 self
.userMapFromPerforceServer
= True
1111 def loadUserMapFromCache(self
):
1113 self
.userMapFromPerforceServer
= False
1115 cache
= open(self
.getUserCacheFilename(), "rb")
1116 lines
= cache
.readlines()
1119 entry
= line
.strip().split("\t")
1120 self
.users
[entry
[0]] = entry
[1]
1122 self
.getUserMapFromPerforceServer()
1124 def getLabels(self
):
1127 l
= p4CmdList("labels %s..." % ' '.join (self
.depotPaths
))
1128 if len(l
) > 0 and not self
.silent
:
1129 print "Finding files belonging to labels in %s" % `self
.depotPaths`
1132 label
= output
["label"]
1136 print "Querying files for label %s" % label
1137 for file in p4CmdList("files "
1138 + ' '.join (["%s...@%s" % (p
, label
)
1139 for p
in self
.depotPaths
])):
1140 revisions
[file["depotFile"]] = file["rev"]
1141 change
= int(file["change"])
1142 if change
> newestChange
:
1143 newestChange
= change
1145 self
.labels
[newestChange
] = [output
, revisions
]
1148 print "Label changes: %s" % self
.labels
.keys()
1150 def guessProjectName(self
):
1151 for p
in self
.depotPaths
:
1154 p
= p
[p
.strip().rfind("/") + 1:]
1155 if not p
.endswith("/"):
1159 def getBranchMapping(self
):
1160 lostAndFoundBranches
= set()
1162 for info
in p4CmdList("branches"):
1163 details
= p4Cmd("branch -o %s" % info
["branch"])
1165 while details
.has_key("View%s" % viewIdx
):
1166 paths
= details
["View%s" % viewIdx
].split(" ")
1167 viewIdx
= viewIdx
+ 1
1168 # require standard //depot/foo/... //depot/bar/... mapping
1169 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
1172 destination
= paths
[1]
1174 if source
.startswith(self
.depotPaths
[0]) and destination
.startswith(self
.depotPaths
[0]):
1175 source
= source
[len(self
.depotPaths
[0]):-4]
1176 destination
= destination
[len(self
.depotPaths
[0]):-4]
1178 if destination
in self
.knownBranches
:
1180 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
1181 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
1184 self
.knownBranches
[destination
] = source
1186 lostAndFoundBranches
.discard(destination
)
1188 if source
not in self
.knownBranches
:
1189 lostAndFoundBranches
.add(source
)
1192 for branch
in lostAndFoundBranches
:
1193 self
.knownBranches
[branch
] = branch
1195 def getBranchMappingFromGitBranches(self
):
1196 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1197 for branch
in branches
.keys():
1198 if branch
== "master":
1201 branch
= branch
[len(self
.projectName
):]
1202 self
.knownBranches
[branch
] = branch
1204 def listExistingP4GitBranches(self
):
1205 # branches holds mapping from name to commit
1206 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1207 self
.p4BranchesInGit
= branches
.keys()
1208 for branch
in branches
.keys():
1209 self
.initialParents
[self
.refPrefix
+ branch
] = branches
[branch
]
1211 def updateOptionDict(self
, d
):
1213 if self
.keepRepoPath
:
1214 option_keys
['keepRepoPath'] = 1
1216 d
["options"] = ' '.join(sorted(option_keys
.keys()))
1218 def readOptions(self
, d
):
1219 self
.keepRepoPath
= (d
.has_key('options')
1220 and ('keepRepoPath' in d
['options']))
1222 def gitRefForBranch(self
, branch
):
1223 if branch
== "main":
1224 return self
.refPrefix
+ "master"
1226 if len(branch
) <= 0:
1229 return self
.refPrefix
+ self
.projectName
+ branch
1231 def gitCommitByP4Change(self
, ref
, change
):
1233 print "looking in ref " + ref
+ " for change %s using bisect..." % change
1236 latestCommit
= parseRevision(ref
)
1240 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
1241 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
1246 log
= extractLogMessageFromGitCommit(next
)
1247 settings
= extractSettingsGitLog(log
)
1248 currentChange
= int(settings
['change'])
1250 print "current change %s" % currentChange
1252 if currentChange
== change
:
1254 print "found %s" % next
1257 if currentChange
< change
:
1258 earliestCommit
= "^%s" % next
1260 latestCommit
= "%s" % next
1264 def importNewBranch(self
, branch
, maxChange
):
1265 # make fast-import flush all changes to disk and update the refs using the checkpoint
1266 # command so that we can try to find the branch parent in the git history
1267 self
.gitStream
.write("checkpoint\n\n");
1268 self
.gitStream
.flush();
1269 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1270 range = "@1,%s" % maxChange
1271 #print "prefix" + branchPrefix
1272 changes
= p4ChangesForPaths([branchPrefix
], range)
1273 if len(changes
) <= 0:
1275 firstChange
= changes
[0]
1276 #print "first change in branch: %s" % firstChange
1277 sourceBranch
= self
.knownBranches
[branch
]
1278 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
1279 sourceRef
= self
.gitRefForBranch(sourceBranch
)
1280 #print "source " + sourceBranch
1282 branchParentChange
= int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath
, firstChange
))["change"])
1283 #print "branch parent: %s" % branchParentChange
1284 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
1285 if len(gitParent
) > 0:
1286 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
1287 #print "parent git commit: %s" % gitParent
1289 self
.importChanges(changes
)
1292 def importChanges(self
, changes
):
1294 for change
in changes
:
1295 description
= p4Cmd("describe %s" % change
)
1296 self
.updateOptionDict(description
)
1299 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
1304 if self
.detectBranches
:
1305 branches
= self
.splitFilesIntoBranches(description
)
1306 for branch
in branches
.keys():
1308 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1312 filesForCommit
= branches
[branch
]
1315 print "branch is %s" % branch
1317 self
.updatedBranches
.add(branch
)
1319 if branch
not in self
.createdBranches
:
1320 self
.createdBranches
.add(branch
)
1321 parent
= self
.knownBranches
[branch
]
1322 if parent
== branch
:
1325 fullBranch
= self
.projectName
+ branch
1326 if fullBranch
not in self
.p4BranchesInGit
:
1328 print("\n Importing new branch %s" % fullBranch
);
1329 if self
.importNewBranch(branch
, change
- 1):
1331 self
.p4BranchesInGit
.append(fullBranch
)
1333 print("\n Resuming with change %s" % change
);
1336 print "parent determined through known branches: %s" % parent
1338 branch
= self
.gitRefForBranch(branch
)
1339 parent
= self
.gitRefForBranch(parent
)
1342 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
1344 if len(parent
) == 0 and branch
in self
.initialParents
:
1345 parent
= self
.initialParents
[branch
]
1346 del self
.initialParents
[branch
]
1348 self
.commit(description
, filesForCommit
, branch
, [branchPrefix
], parent
)
1350 files
= self
.extractFilesFromCommit(description
)
1351 self
.commit(description
, files
, self
.branch
, self
.depotPaths
,
1353 self
.initialParent
= ""
1355 print self
.gitError
.read()
1358 def importHeadRevision(self
, revision
):
1359 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
1361 details
= { "user" : "git perforce import user", "time" : int(time
.time()) }
1362 details
["desc"] = ("Initial import of %s from the state at revision %s"
1363 % (' '.join(self
.depotPaths
), revision
))
1364 details
["change"] = revision
1368 for info
in p4CmdList("files "
1369 + ' '.join(["%s...%s"
1371 for p
in self
.depotPaths
])):
1373 if info
['code'] == 'error':
1374 sys
.stderr
.write("p4 returned an error: %s\n"
1379 change
= int(info
["change"])
1380 if change
> newestRevision
:
1381 newestRevision
= change
1383 if info
["action"] == "delete":
1384 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1385 #fileCnt = fileCnt + 1
1388 for prop
in ["depotFile", "rev", "action", "type" ]:
1389 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
1391 fileCnt
= fileCnt
+ 1
1393 details
["change"] = newestRevision
1394 self
.updateOptionDict(details
)
1396 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
, self
.depotPaths
)
1398 print "IO error with git fast-import. Is your git version recent enough?"
1399 print self
.gitError
.read()
1402 def getClientSpec(self
):
1403 specList
= p4CmdList( "client -o" )
1405 for entry
in specList
:
1406 for k
,v
in entry
.iteritems():
1407 if k
.startswith("View"):
1408 if v
.startswith('"'):
1412 index
= v
.find("...")
1414 if v
.startswith("-"):
1419 self
.clientSpecDirs
= temp
.items()
1420 self
.clientSpecDirs
.sort( lambda x
, y
: abs( y
[1] ) - abs( x
[1] ) )
1422 def run(self
, args
):
1423 self
.depotPaths
= []
1424 self
.changeRange
= ""
1425 self
.initialParent
= ""
1426 self
.previousDepotPaths
= []
1428 # map from branch depot path to parent branch
1429 self
.knownBranches
= {}
1430 self
.initialParents
= {}
1431 self
.hasOrigin
= originP4BranchesExist()
1432 if not self
.syncWithOrigin
:
1433 self
.hasOrigin
= False
1435 if self
.importIntoRemotes
:
1436 self
.refPrefix
= "refs/remotes/p4/"
1438 self
.refPrefix
= "refs/heads/p4/"
1440 if self
.syncWithOrigin
and self
.hasOrigin
:
1442 print "Syncing with origin first by calling git fetch origin"
1443 system("git fetch origin")
1445 if len(self
.branch
) == 0:
1446 self
.branch
= self
.refPrefix
+ "master"
1447 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
1448 system("git update-ref %s refs/heads/p4" % self
.branch
)
1449 system("git branch -D p4");
1450 # create it /after/ importing, when master exists
1451 if not gitBranchExists(self
.refPrefix
+ "HEAD") and self
.importIntoRemotes
and gitBranchExists(self
.branch
):
1452 system("git symbolic-ref %sHEAD %s" % (self
.refPrefix
, self
.branch
))
1454 if self
.useClientSpec
or gitConfig("git-p4.useclientspec") == "true":
1455 self
.getClientSpec()
1457 # TODO: should always look at previous commits,
1458 # merge with previous imports, if possible.
1461 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
1462 self
.listExistingP4GitBranches()
1464 if len(self
.p4BranchesInGit
) > 1:
1466 print "Importing from/into multiple branches"
1467 self
.detectBranches
= True
1470 print "branches: %s" % self
.p4BranchesInGit
1473 for branch
in self
.p4BranchesInGit
:
1474 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
1476 settings
= extractSettingsGitLog(logMsg
)
1478 self
.readOptions(settings
)
1479 if (settings
.has_key('depot-paths')
1480 and settings
.has_key ('change')):
1481 change
= int(settings
['change']) + 1
1482 p4Change
= max(p4Change
, change
)
1484 depotPaths
= sorted(settings
['depot-paths'])
1485 if self
.previousDepotPaths
== []:
1486 self
.previousDepotPaths
= depotPaths
1489 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
1490 for i
in range(0, min(len(cur
), len(prev
))):
1491 if cur
[i
] <> prev
[i
]:
1495 paths
.append (cur
[:i
+ 1])
1497 self
.previousDepotPaths
= paths
1500 self
.depotPaths
= sorted(self
.previousDepotPaths
)
1501 self
.changeRange
= "@%s,#head" % p4Change
1502 if not self
.detectBranches
:
1503 self
.initialParent
= parseRevision(self
.branch
)
1504 if not self
.silent
and not self
.detectBranches
:
1505 print "Performing incremental import into %s git branch" % self
.branch
1507 if not self
.branch
.startswith("refs/"):
1508 self
.branch
= "refs/heads/" + self
.branch
1510 if len(args
) == 0 and self
.depotPaths
:
1512 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
1514 if self
.depotPaths
and self
.depotPaths
!= args
:
1515 print ("previous import used depot path %s and now %s was specified. "
1516 "This doesn't work!" % (' '.join (self
.depotPaths
),
1520 self
.depotPaths
= sorted(args
)
1526 for p
in self
.depotPaths
:
1527 if p
.find("@") != -1:
1528 atIdx
= p
.index("@")
1529 self
.changeRange
= p
[atIdx
:]
1530 if self
.changeRange
== "@all":
1531 self
.changeRange
= ""
1532 elif ',' not in self
.changeRange
:
1533 revision
= self
.changeRange
1534 self
.changeRange
= ""
1536 elif p
.find("#") != -1:
1537 hashIdx
= p
.index("#")
1538 revision
= p
[hashIdx
:]
1540 elif self
.previousDepotPaths
== []:
1543 p
= re
.sub ("\.\.\.$", "", p
)
1544 if not p
.endswith("/"):
1549 self
.depotPaths
= newPaths
1552 self
.loadUserMapFromCache()
1554 if self
.detectLabels
:
1557 if self
.detectBranches
:
1558 ## FIXME - what's a P4 projectName ?
1559 self
.projectName
= self
.guessProjectName()
1562 self
.getBranchMappingFromGitBranches()
1564 self
.getBranchMapping()
1566 print "p4-git branches: %s" % self
.p4BranchesInGit
1567 print "initial parents: %s" % self
.initialParents
1568 for b
in self
.p4BranchesInGit
:
1572 b
= b
[len(self
.projectName
):]
1573 self
.createdBranches
.add(b
)
1575 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
1577 importProcess
= subprocess
.Popen(["git", "fast-import"],
1578 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
1579 stderr
=subprocess
.PIPE
);
1580 self
.gitOutput
= importProcess
.stdout
1581 self
.gitStream
= importProcess
.stdin
1582 self
.gitError
= importProcess
.stderr
1585 self
.importHeadRevision(revision
)
1589 if len(self
.changesFile
) > 0:
1590 output
= open(self
.changesFile
).readlines()
1593 changeSet
.add(int(line
))
1595 for change
in changeSet
:
1596 changes
.append(change
)
1601 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
1603 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
)
1605 if len(self
.maxChanges
) > 0:
1606 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
1608 if len(changes
) == 0:
1610 print "No changes to import!"
1613 if not self
.silent
and not self
.detectBranches
:
1614 print "Import destination: %s" % self
.branch
1616 self
.updatedBranches
= set()
1618 self
.importChanges(changes
)
1622 if len(self
.updatedBranches
) > 0:
1623 sys
.stdout
.write("Updated branches: ")
1624 for b
in self
.updatedBranches
:
1625 sys
.stdout
.write("%s " % b
)
1626 sys
.stdout
.write("\n")
1628 self
.gitStream
.close()
1629 if importProcess
.wait() != 0:
1630 die("fast-import failed: %s" % self
.gitError
.read())
1631 self
.gitOutput
.close()
1632 self
.gitError
.close()
1636 class P4Rebase(Command
):
1638 Command
.__init
__(self
)
1640 self
.description
= ("Fetches the latest revision from perforce and "
1641 + "rebases the current work (branch) against it")
1642 self
.verbose
= False
1644 def run(self
, args
):
1648 return self
.rebase()
1651 if os
.system("git update-index --refresh") != 0:
1652 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.");
1653 if len(read_pipe("git diff-index HEAD --")) > 0:
1654 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1656 [upstream
, settings
] = findUpstreamBranchPoint()
1657 if len(upstream
) == 0:
1658 die("Cannot find upstream branchpoint for rebase")
1660 # the branchpoint may be p4/foo~3, so strip off the parent
1661 upstream
= re
.sub("~[0-9]+$", "", upstream
)
1663 print "Rebasing the current branch onto %s" % upstream
1664 oldHead
= read_pipe("git rev-parse HEAD").strip()
1665 system("git rebase %s" % upstream
)
1666 system("git diff-tree --stat --summary -M %s HEAD" % oldHead
)
1669 class P4Clone(P4Sync
):
1671 P4Sync
.__init
__(self
)
1672 self
.description
= "Creates a new git repository and imports from Perforce into it"
1673 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
1675 optparse
.make_option("--destination", dest
="cloneDestination",
1676 action
='store', default
=None,
1677 help="where to leave result of the clone"),
1678 optparse
.make_option("-/", dest
="cloneExclude",
1679 action
="append", type="string",
1680 help="exclude depot path")
1682 self
.cloneDestination
= None
1683 self
.needsGit
= False
1685 # This is required for the "append" cloneExclude action
1686 def ensure_value(self
, attr
, value
):
1687 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
1688 setattr(self
, attr
, value
)
1689 return getattr(self
, attr
)
1691 def defaultDestination(self
, args
):
1692 ## TODO: use common prefix of args?
1694 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
1695 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
1696 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
1697 depotDir
= re
.sub(r
"/$", "", depotDir
)
1698 return os
.path
.split(depotDir
)[1]
1700 def run(self
, args
):
1704 if self
.keepRepoPath
and not self
.cloneDestination
:
1705 sys
.stderr
.write("Must specify destination for --keep-path\n")
1710 if not self
.cloneDestination
and len(depotPaths
) > 1:
1711 self
.cloneDestination
= depotPaths
[-1]
1712 depotPaths
= depotPaths
[:-1]
1714 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
1715 for p
in depotPaths
:
1716 if not p
.startswith("//"):
1719 if not self
.cloneDestination
:
1720 self
.cloneDestination
= self
.defaultDestination(args
)
1722 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
1723 if not os
.path
.exists(self
.cloneDestination
):
1724 os
.makedirs(self
.cloneDestination
)
1725 os
.chdir(self
.cloneDestination
)
1727 self
.gitdir
= os
.getcwd() + "/.git"
1728 if not P4Sync
.run(self
, depotPaths
):
1730 if self
.branch
!= "master":
1731 if gitBranchExists("refs/remotes/p4/master"):
1732 system("git branch master refs/remotes/p4/master")
1733 system("git checkout -f")
1735 print "Could not detect main branch. No checkout/master branch created."
1739 class P4Branches(Command
):
1741 Command
.__init
__(self
)
1743 self
.description
= ("Shows the git branches that hold imports and their "
1744 + "corresponding perforce depot paths")
1745 self
.verbose
= False
1747 def run(self
, args
):
1748 if originP4BranchesExist():
1749 createOrUpdateBranchesFromOrigin()
1751 cmdline
= "git rev-parse --symbolic "
1752 cmdline
+= " --remotes"
1754 for line
in read_pipe_lines(cmdline
):
1757 if not line
.startswith('p4/') or line
== "p4/HEAD":
1761 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
1762 settings
= extractSettingsGitLog(log
)
1764 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
1767 class HelpFormatter(optparse
.IndentedHelpFormatter
):
1769 optparse
.IndentedHelpFormatter
.__init
__(self
)
1771 def format_description(self
, description
):
1773 return description
+ "\n"
1777 def printUsage(commands
):
1778 print "usage: %s <command> [options]" % sys
.argv
[0]
1780 print "valid commands: %s" % ", ".join(commands
)
1782 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
1787 "submit" : P4Submit
,
1788 "commit" : P4Submit
,
1790 "rebase" : P4Rebase
,
1792 "rollback" : P4RollBack
,
1793 "branches" : P4Branches
1798 if len(sys
.argv
[1:]) == 0:
1799 printUsage(commands
.keys())
1803 cmdName
= sys
.argv
[1]
1805 klass
= commands
[cmdName
]
1808 print "unknown command %s" % cmdName
1810 printUsage(commands
.keys())
1813 options
= cmd
.options
1814 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
1818 if len(options
) > 0:
1819 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
1821 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
1823 description
= cmd
.description
,
1824 formatter
= HelpFormatter())
1826 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
1828 verbose
= cmd
.verbose
1830 if cmd
.gitdir
== None:
1831 cmd
.gitdir
= os
.path
.abspath(".git")
1832 if not isValidGitDir(cmd
.gitdir
):
1833 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
1834 if os
.path
.exists(cmd
.gitdir
):
1835 cdup
= read_pipe("git rev-parse --show-cdup").strip()
1839 if not isValidGitDir(cmd
.gitdir
):
1840 if isValidGitDir(cmd
.gitdir
+ "/.git"):
1841 cmd
.gitdir
+= "/.git"
1843 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
1845 os
.environ
["GIT_DIR"] = cmd
.gitdir
1847 if not cmd
.run(args
):
1851 if __name__
== '__main__':