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
, subprocess
, shelve
12 import tempfile
, getopt
, os
.path
, time
, platform
18 def p4_build_cmd(cmd
):
19 """Build a suitable p4 command line.
21 This consolidates building and returning a p4 command line into one
22 location. It means that hooking into the environment, or other configuration
23 can be done more easily.
25 real_cmd
= "%s " % "p4"
27 user
= gitConfig("git-p4.user")
29 real_cmd
+= "-u %s " % user
31 password
= gitConfig("git-p4.password")
33 real_cmd
+= "-P %s " % password
35 port
= gitConfig("git-p4.port")
37 real_cmd
+= "-p %s " % port
39 host
= gitConfig("git-p4.host")
41 real_cmd
+= "-h %s " % host
43 client
= gitConfig("git-p4.client")
45 real_cmd
+= "-c %s " % client
47 real_cmd
+= "%s" % (cmd
)
61 sys
.stderr
.write(msg
+ "\n")
64 def write_pipe(c
, str):
66 sys
.stderr
.write('Writing pipe: %s\n' % c
)
68 pipe
= os
.popen(c
, 'w')
71 die('Command failed: %s' % c
)
75 def p4_write_pipe(c
, str):
76 real_cmd
= p4_build_cmd(c
)
77 return write_pipe(real_cmd
, str)
79 def read_pipe(c
, ignore_error
=False):
81 sys
.stderr
.write('Reading pipe: %s\n' % c
)
83 pipe
= os
.popen(c
, 'rb')
85 if pipe
.close() and not ignore_error
:
86 die('Command failed: %s' % c
)
90 def p4_read_pipe(c
, ignore_error
=False):
91 real_cmd
= p4_build_cmd(c
)
92 return read_pipe(real_cmd
, ignore_error
)
94 def read_pipe_lines(c
):
96 sys
.stderr
.write('Reading pipe: %s\n' % c
)
97 ## todo: check return status
98 pipe
= os
.popen(c
, 'rb')
99 val
= pipe
.readlines()
101 die('Command failed: %s' % c
)
105 def p4_read_pipe_lines(c
):
106 """Specifically invoke p4 on the command supplied. """
107 real_cmd
= p4_build_cmd(c
)
108 return read_pipe_lines(real_cmd
)
112 sys
.stderr
.write("executing %s\n" % cmd
)
113 if os
.system(cmd
) != 0:
114 die("command failed: %s" % cmd
)
117 """Specifically invoke p4 as the system command. """
118 real_cmd
= p4_build_cmd(cmd
)
119 return system(real_cmd
)
122 """Determine if a Perforce 'kind' should have execute permission
124 'p4 help filetypes' gives a list of the types. If it starts with 'x',
125 or x follows one of a few letters. Otherwise, if there is an 'x' after
126 a plus sign, it is also executable"""
127 return (re
.search(r
"(^[cku]?x)|\+.*x", kind
) != None)
129 def setP4ExecBit(file, mode
):
130 # Reopens an already open file and changes the execute bit to match
131 # the execute bit setting in the passed in mode.
135 if not isModeExec(mode
):
136 p4Type
= getP4OpenedType(file)
137 p4Type
= re
.sub('^([cku]?)x(.*)', '\\1\\2', p4Type
)
138 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
139 if p4Type
[-1] == "+":
140 p4Type
= p4Type
[0:-1]
142 p4_system("reopen -t %s %s" % (p4Type
, file))
144 def getP4OpenedType(file):
145 # Returns the perforce file type for the given file.
147 result
= p4_read_pipe("opened %s" % file)
148 match
= re
.match(".*\((.+)\)\r?$", result
)
150 return match
.group(1)
152 die("Could not determine file type for %s (result: '%s')" % (file, result
))
154 def diffTreePattern():
155 # This is a simple generator for the diff tree regex pattern. This could be
156 # a class variable if this and parseDiffTreeEntry were a part of a class.
157 pattern
= re
.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
161 def parseDiffTreeEntry(entry
):
162 """Parses a single diff tree entry into its component elements.
164 See git-diff-tree(1) manpage for details about the format of the diff
165 output. This method returns a dictionary with the following elements:
167 src_mode - The mode of the source file
168 dst_mode - The mode of the destination file
169 src_sha1 - The sha1 for the source file
170 dst_sha1 - The sha1 fr the destination file
171 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
172 status_score - The score for the status (applicable for 'C' and 'R'
173 statuses). This is None if there is no score.
174 src - The path for the source file.
175 dst - The path for the destination file. This is only present for
176 copy or renames. If it is not present, this is None.
178 If the pattern is not matched, None is returned."""
180 match
= diffTreePattern().next().match(entry
)
183 'src_mode': match
.group(1),
184 'dst_mode': match
.group(2),
185 'src_sha1': match
.group(3),
186 'dst_sha1': match
.group(4),
187 'status': match
.group(5),
188 'status_score': match
.group(6),
189 'src': match
.group(7),
190 'dst': match
.group(10)
194 def isModeExec(mode
):
195 # Returns True if the given git mode represents an executable file,
197 return mode
[-3:] == "755"
199 def isModeExecChanged(src_mode
, dst_mode
):
200 return isModeExec(src_mode
) != isModeExec(dst_mode
)
202 def p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b', cb
=None):
203 cmd
= p4_build_cmd("-G %s" % (cmd
))
205 sys
.stderr
.write("Opening pipe: %s\n" % cmd
)
207 # Use a temporary file to avoid deadlocks without
208 # subprocess.communicate(), which would put another copy
209 # of stdout into memory.
211 if stdin
is not None:
212 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
213 stdin_file
.write(stdin
)
217 p4
= subprocess
.Popen(cmd
, shell
=True,
219 stdout
=subprocess
.PIPE
)
224 entry
= marshal
.load(p4
.stdout
)
234 entry
["p4ExitCode"] = exitCode
240 list = p4CmdList(cmd
)
246 def p4Where(depotPath
):
247 if not depotPath
.endswith("/"):
249 depotPath
= depotPath
+ "..."
250 outputList
= p4CmdList("where %s" % depotPath
)
252 for entry
in outputList
:
253 if "depotFile" in entry
:
254 if entry
["depotFile"] == depotPath
:
257 elif "data" in entry
:
258 data
= entry
.get("data")
259 space
= data
.find(" ")
260 if data
[:space
] == depotPath
:
265 if output
["code"] == "error":
269 clientPath
= output
.get("path")
270 elif "data" in output
:
271 data
= output
.get("data")
272 lastSpace
= data
.rfind(" ")
273 clientPath
= data
[lastSpace
+ 1:]
275 if clientPath
.endswith("..."):
276 clientPath
= clientPath
[:-3]
279 def currentGitBranch():
280 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
282 def isValidGitDir(path
):
283 if (os
.path
.exists(path
+ "/HEAD")
284 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
288 def parseRevision(ref
):
289 return read_pipe("git rev-parse %s" % ref
).strip()
291 def extractLogMessageFromGitCommit(commit
):
294 ## fixme: title is first line of commit, not 1st paragraph.
296 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
305 def extractSettingsGitLog(log
):
307 for line
in log
.split("\n"):
309 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
313 assignments
= m
.group(1).split (':')
314 for a
in assignments
:
316 key
= vals
[0].strip()
317 val
= ('='.join (vals
[1:])).strip()
318 if val
.endswith ('\"') and val
.startswith('"'):
323 paths
= values
.get("depot-paths")
325 paths
= values
.get("depot-path")
327 values
['depot-paths'] = paths
.split(',')
330 def gitBranchExists(branch
):
331 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
332 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
333 return proc
.wait() == 0;
336 def gitConfig(key
, args
= None): # set args to "--bool", for instance
337 if not _gitConfig
.has_key(key
):
340 argsFilter
= "%s " % args
341 cmd
= "git config %s%s" % (argsFilter
, key
)
342 _gitConfig
[key
] = read_pipe(cmd
, ignore_error
=True).strip()
343 return _gitConfig
[key
]
345 def p4BranchesInGit(branchesAreInRemotes
= True):
348 cmdline
= "git rev-parse --symbolic "
349 if branchesAreInRemotes
:
350 cmdline
+= " --remotes"
352 cmdline
+= " --branches"
354 for line
in read_pipe_lines(cmdline
):
357 ## only import to p4/
358 if not line
.startswith('p4/') or line
== "p4/HEAD":
363 branch
= re
.sub ("^p4/", "", line
)
365 branches
[branch
] = parseRevision(line
)
368 def findUpstreamBranchPoint(head
= "HEAD"):
369 branches
= p4BranchesInGit()
370 # map from depot-path to branch name
371 branchByDepotPath
= {}
372 for branch
in branches
.keys():
373 tip
= branches
[branch
]
374 log
= extractLogMessageFromGitCommit(tip
)
375 settings
= extractSettingsGitLog(log
)
376 if settings
.has_key("depot-paths"):
377 paths
= ",".join(settings
["depot-paths"])
378 branchByDepotPath
[paths
] = "remotes/p4/" + branch
382 while parent
< 65535:
383 commit
= head
+ "~%s" % parent
384 log
= extractLogMessageFromGitCommit(commit
)
385 settings
= extractSettingsGitLog(log
)
386 if settings
.has_key("depot-paths"):
387 paths
= ",".join(settings
["depot-paths"])
388 if branchByDepotPath
.has_key(paths
):
389 return [branchByDepotPath
[paths
], settings
]
393 return ["", settings
]
395 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
397 print ("Creating/updating branch(es) in %s based on origin branch(es)"
400 originPrefix
= "origin/p4/"
402 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
404 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
407 headName
= line
[len(originPrefix
):]
408 remoteHead
= localRefPrefix
+ headName
411 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
412 if (not original
.has_key('depot-paths')
413 or not original
.has_key('change')):
417 if not gitBranchExists(remoteHead
):
419 print "creating %s" % remoteHead
422 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
423 if settings
.has_key('change') > 0:
424 if settings
['depot-paths'] == original
['depot-paths']:
425 originP4Change
= int(original
['change'])
426 p4Change
= int(settings
['change'])
427 if originP4Change
> p4Change
:
428 print ("%s (%s) is newer than %s (%s). "
429 "Updating p4 branch from origin."
430 % (originHead
, originP4Change
,
431 remoteHead
, p4Change
))
434 print ("Ignoring: %s was imported from %s while "
435 "%s was imported from %s"
436 % (originHead
, ','.join(original
['depot-paths']),
437 remoteHead
, ','.join(settings
['depot-paths'])))
440 system("git update-ref %s %s" % (remoteHead
, originHead
))
442 def originP4BranchesExist():
443 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
445 def p4ChangesForPaths(depotPaths
, changeRange
):
447 output
= p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p
, changeRange
)
448 for p
in depotPaths
]))
452 changeNum
= int(line
.split(" ")[1])
453 changes
[changeNum
] = True
455 changelist
= changes
.keys()
459 def p4PathStartsWith(path
, prefix
):
460 # This method tries to remedy a potential mixed-case issue:
462 # If UserA adds //depot/DirA/file1
463 # and UserB adds //depot/dira/file2
465 # we may or may not have a problem. If you have core.ignorecase=true,
466 # we treat DirA and dira as the same directory
467 ignorecase
= gitConfig("core.ignorecase", "--bool") == "true"
469 return path
.lower().startswith(prefix
.lower())
470 return path
.startswith(prefix
)
474 self
.usage
= "usage: %prog [options]"
477 class P4Debug(Command
):
479 Command
.__init
__(self
)
481 optparse
.make_option("--verbose", dest
="verbose", action
="store_true",
484 self
.description
= "A tool to debug the output of p4 -G."
485 self
.needsGit
= False
490 for output
in p4CmdList(" ".join(args
)):
491 print 'Element: %d' % j
496 class P4RollBack(Command
):
498 Command
.__init
__(self
)
500 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
501 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
503 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
505 self
.rollbackLocalBranches
= False
510 maxChange
= int(args
[0])
512 if "p4ExitCode" in p4Cmd("changes -m 1"):
513 die("Problems executing p4");
515 if self
.rollbackLocalBranches
:
516 refPrefix
= "refs/heads/"
517 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
519 refPrefix
= "refs/remotes/"
520 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
523 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
525 ref
= refPrefix
+ line
526 log
= extractLogMessageFromGitCommit(ref
)
527 settings
= extractSettingsGitLog(log
)
529 depotPaths
= settings
['depot-paths']
530 change
= settings
['change']
534 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
535 for p
in depotPaths
]))) == 0:
536 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
537 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
540 while change
and int(change
) > maxChange
:
543 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
544 system("git update-ref %s \"%s^\"" % (ref
, ref
))
545 log
= extractLogMessageFromGitCommit(ref
)
546 settings
= extractSettingsGitLog(log
)
549 depotPaths
= settings
['depot-paths']
550 change
= settings
['change']
553 print "%s rewound to %s" % (ref
, change
)
557 class P4Submit(Command
):
559 Command
.__init
__(self
)
561 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
562 optparse
.make_option("--origin", dest
="origin"),
563 optparse
.make_option("-M", dest
="detectRenames", action
="store_true"),
565 self
.description
= "Submit changes from git to the perforce depot."
566 self
.usage
+= " [name of git branch to submit into perforce depot]"
567 self
.interactive
= True
569 self
.detectRenames
= False
571 self
.isWindows
= (platform
.system() == "Windows")
574 if len(p4CmdList("opened ...")) > 0:
575 die("You have files opened with perforce! Close them before starting the sync.")
577 # replaces everything between 'Description:' and the next P4 submit template field with the
579 def prepareLogMessage(self
, template
, message
):
582 inDescriptionSection
= False
584 for line
in template
.split("\n"):
585 if line
.startswith("#"):
586 result
+= line
+ "\n"
589 if inDescriptionSection
:
590 if line
.startswith("Files:") or line
.startswith("Jobs:"):
591 inDescriptionSection
= False
595 if line
.startswith("Description:"):
596 inDescriptionSection
= True
598 for messageLine
in message
.split("\n"):
599 line
+= "\t" + messageLine
+ "\n"
601 result
+= line
+ "\n"
605 def prepareSubmitTemplate(self
):
606 # remove lines in the Files section that show changes to files outside the depot path we're committing into
608 inFilesSection
= False
609 for line
in p4_read_pipe_lines("change -o"):
610 if line
.endswith("\r\n"):
611 line
= line
[:-2] + "\n"
613 if line
.startswith("\t"):
614 # path starts and ends with a tab
616 lastTab
= path
.rfind("\t")
618 path
= path
[:lastTab
]
619 if not p4PathStartsWith(path
, self
.depotPath
):
622 inFilesSection
= False
624 if line
.startswith("Files:"):
625 inFilesSection
= True
631 def applyCommit(self
, id):
632 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
634 if not self
.detectRenames
:
635 # If not explicitly set check the config variable
636 self
.detectRenames
= gitConfig("git-p4.detectRenames").lower() == "true"
638 if self
.detectRenames
:
643 if gitConfig("git-p4.detectCopies").lower() == "true":
646 if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
647 diffOpts
+= " --find-copies-harder"
649 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts
, id, id))
651 filesToDelete
= set()
653 filesToChangeExecBit
= {}
655 diff
= parseDiffTreeEntry(line
)
656 modifier
= diff
['status']
659 p4_system("edit \"%s\"" % path
)
660 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
661 filesToChangeExecBit
[path
] = diff
['dst_mode']
662 editedFiles
.add(path
)
663 elif modifier
== "A":
665 filesToChangeExecBit
[path
] = diff
['dst_mode']
666 if path
in filesToDelete
:
667 filesToDelete
.remove(path
)
668 elif modifier
== "D":
669 filesToDelete
.add(path
)
670 if path
in filesToAdd
:
671 filesToAdd
.remove(path
)
672 elif modifier
== "C":
673 src
, dest
= diff
['src'], diff
['dst']
674 p4_system("integrate -Dt \"%s\" \"%s\"" % (src
, dest
))
675 if diff
['src_sha1'] != diff
['dst_sha1']:
676 p4_system("edit \"%s\"" % (dest
))
677 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
678 p4_system("edit \"%s\"" % (dest
))
679 filesToChangeExecBit
[dest
] = diff
['dst_mode']
681 editedFiles
.add(dest
)
682 elif modifier
== "R":
683 src
, dest
= diff
['src'], diff
['dst']
684 p4_system("integrate -Dt \"%s\" \"%s\"" % (src
, dest
))
685 if diff
['src_sha1'] != diff
['dst_sha1']:
686 p4_system("edit \"%s\"" % (dest
))
687 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
688 p4_system("edit \"%s\"" % (dest
))
689 filesToChangeExecBit
[dest
] = diff
['dst_mode']
691 editedFiles
.add(dest
)
692 filesToDelete
.add(src
)
694 die("unknown modifier %s for %s" % (modifier
, path
))
696 diffcmd
= "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
697 patchcmd
= diffcmd
+ " | git apply "
698 tryPatchCmd
= patchcmd
+ "--check -"
699 applyPatchCmd
= patchcmd
+ "--check --apply -"
701 if os
.system(tryPatchCmd
) != 0:
702 print "Unfortunately applying the change failed!"
703 print "What do you want to do?"
705 while response
!= "s" and response
!= "a" and response
!= "w":
706 response
= raw_input("[s]kip this patch / [a]pply the patch forcibly "
707 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
709 print "Skipping! Good luck with the next patches..."
710 for f
in editedFiles
:
711 p4_system("revert \"%s\"" % f
);
715 elif response
== "a":
716 os
.system(applyPatchCmd
)
717 if len(filesToAdd
) > 0:
718 print "You may also want to call p4 add on the following files:"
719 print " ".join(filesToAdd
)
720 if len(filesToDelete
):
721 print "The following files should be scheduled for deletion with p4 delete:"
722 print " ".join(filesToDelete
)
723 die("Please resolve and submit the conflict manually and "
724 + "continue afterwards with git-p4 submit --continue")
725 elif response
== "w":
726 system(diffcmd
+ " > patch.txt")
727 print "Patch saved to patch.txt in %s !" % self
.clientPath
728 die("Please resolve and submit the conflict manually and "
729 "continue afterwards with git-p4 submit --continue")
731 system(applyPatchCmd
)
734 p4_system("add \"%s\"" % f
)
735 for f
in filesToDelete
:
736 p4_system("revert \"%s\"" % f
)
737 p4_system("delete \"%s\"" % f
)
739 # Set/clear executable bits
740 for f
in filesToChangeExecBit
.keys():
741 mode
= filesToChangeExecBit
[f
]
742 setP4ExecBit(f
, mode
)
744 logMessage
= extractLogMessageFromGitCommit(id)
745 logMessage
= logMessage
.strip()
747 template
= self
.prepareSubmitTemplate()
750 submitTemplate
= self
.prepareLogMessage(template
, logMessage
)
751 if os
.environ
.has_key("P4DIFF"):
752 del(os
.environ
["P4DIFF"])
754 for editedFile
in editedFiles
:
755 diff
+= p4_read_pipe("diff -du %r" % editedFile
)
758 for newFile
in filesToAdd
:
759 newdiff
+= "==== new file ====\n"
760 newdiff
+= "--- /dev/null\n"
761 newdiff
+= "+++ %s\n" % newFile
762 f
= open(newFile
, "r")
763 for line
in f
.readlines():
764 newdiff
+= "+" + line
767 separatorLine
= "######## everything below this line is just the diff #######\n"
769 [handle
, fileName
] = tempfile
.mkstemp()
770 tmpFile
= os
.fdopen(handle
, "w+")
772 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
773 separatorLine
= separatorLine
.replace("\n", "\r\n")
774 newdiff
= newdiff
.replace("\n", "\r\n")
775 tmpFile
.write(submitTemplate
+ separatorLine
+ diff
+ newdiff
)
777 mtime
= os
.stat(fileName
).st_mtime
778 if os
.environ
.has_key("P4EDITOR"):
779 editor
= os
.environ
.get("P4EDITOR")
781 editor
= read_pipe("git var GIT_EDITOR").strip()
782 system(editor
+ " " + fileName
)
785 if os
.stat(fileName
).st_mtime
<= mtime
:
787 while response
!= "y" and response
!= "n":
788 response
= raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
791 tmpFile
= open(fileName
, "rb")
792 message
= tmpFile
.read()
794 submitTemplate
= message
[:message
.index(separatorLine
)]
796 submitTemplate
= submitTemplate
.replace("\r\n", "\n")
797 p4_write_pipe("submit -i", submitTemplate
)
799 for f
in editedFiles
:
800 p4_system("revert \"%s\"" % f
);
802 p4_system("revert \"%s\"" % f
);
807 fileName
= "submit.txt"
808 file = open(fileName
, "w+")
809 file.write(self
.prepareLogMessage(template
, logMessage
))
811 print ("Perforce submit template written as %s. "
812 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
813 % (fileName
, fileName
))
817 self
.master
= currentGitBranch()
818 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
819 die("Detecting current git branch failed!")
821 self
.master
= args
[0]
825 allowSubmit
= gitConfig("git-p4.allowSubmit")
826 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
827 die("%s is not in git-p4.allowSubmit" % self
.master
)
829 [upstream
, settings
] = findUpstreamBranchPoint()
830 self
.depotPath
= settings
['depot-paths'][0]
831 if len(self
.origin
) == 0:
832 self
.origin
= upstream
835 print "Origin branch is " + self
.origin
837 if len(self
.depotPath
) == 0:
838 print "Internal error: cannot locate perforce depot path from existing branches"
841 self
.clientPath
= p4Where(self
.depotPath
)
843 if len(self
.clientPath
) == 0:
844 print "Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
847 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
848 self
.oldWorkingDirectory
= os
.getcwd()
850 chdir(self
.clientPath
)
851 print "Synchronizing p4 checkout..."
852 p4_system("sync ...")
857 for line
in read_pipe_lines("git rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)):
858 commits
.append(line
.strip())
861 while len(commits
) > 0:
863 commits
= commits
[1:]
864 self
.applyCommit(commit
)
865 if not self
.interactive
:
868 if len(commits
) == 0:
869 print "All changes applied!"
870 chdir(self
.oldWorkingDirectory
)
880 class P4Sync(Command
):
881 delete_actions
= ( "delete", "move/delete", "purge" )
884 Command
.__init
__(self
)
886 optparse
.make_option("--branch", dest
="branch"),
887 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
888 optparse
.make_option("--changesfile", dest
="changesFile"),
889 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
890 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
891 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
892 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
893 help="Import into refs/heads/ , not refs/remotes"),
894 optparse
.make_option("--max-changes", dest
="maxChanges"),
895 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
896 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
897 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
898 help="Only sync files that are included in the Perforce Client Spec")
900 self
.description
= """Imports from Perforce into a git repository.\n
902 //depot/my/project/ -- to import the current head
903 //depot/my/project/@all -- to import everything
904 //depot/my/project/@1,6 -- to import only from revision 1 to 6
906 (a ... is not needed in the path p4 specification, it's added implicitly)"""
908 self
.usage
+= " //depot/path[@revRange]"
910 self
.createdBranches
= set()
911 self
.committedChanges
= set()
913 self
.detectBranches
= False
914 self
.detectLabels
= False
915 self
.changesFile
= ""
916 self
.syncWithOrigin
= True
918 self
.importIntoRemotes
= True
920 self
.isWindows
= (platform
.system() == "Windows")
921 self
.keepRepoPath
= False
922 self
.depotPaths
= None
923 self
.p4BranchesInGit
= []
924 self
.cloneExclude
= []
925 self
.useClientSpec
= False
926 self
.clientSpecDirs
= []
928 if gitConfig("git-p4.syncFromOrigin") == "false":
929 self
.syncWithOrigin
= False
932 # P4 wildcards are not allowed in filenames. P4 complains
933 # if you simply add them, but you can force it with "-f", in
934 # which case it translates them into %xx encoding internally.
935 # Search for and fix just these four characters. Do % last so
936 # that fixing it does not inadvertently create new %-escapes.
938 def wildcard_decode(self
, path
):
939 # Cannot have * in a filename in windows; untested as to
940 # what p4 would do in such a case.
941 if not self
.isWindows
:
942 path
= path
.replace("%2A", "*")
943 path
= path
.replace("%23", "#") \
944 .replace("%40", "@") \
948 def extractFilesFromCommit(self
, commit
):
949 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
950 for path
in self
.cloneExclude
]
953 while commit
.has_key("depotFile%s" % fnum
):
954 path
= commit
["depotFile%s" % fnum
]
956 if [p
for p
in self
.cloneExclude
957 if p4PathStartsWith(path
, p
)]:
960 found
= [p
for p
in self
.depotPaths
961 if p4PathStartsWith(path
, p
)]
968 file["rev"] = commit
["rev%s" % fnum
]
969 file["action"] = commit
["action%s" % fnum
]
970 file["type"] = commit
["type%s" % fnum
]
975 def stripRepoPath(self
, path
, prefixes
):
976 if self
.useClientSpec
:
978 # if using the client spec, we use the output directory
979 # specified in the client. For example, a view
980 # //depot/foo/branch/... //client/branch/foo/...
981 # will end up putting all foo/branch files into
983 for val
in self
.clientSpecDirs
:
984 if path
.startswith(val
[0]):
985 # replace the depot path with the client path
986 path
= path
.replace(val
[0], val
[1][1])
987 # now strip out the client (//client/...)
988 path
= re
.sub("^(//[^/]+/)", '', path
)
989 # the rest is all path
992 if self
.keepRepoPath
:
993 prefixes
= [re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])]
996 if p4PathStartsWith(path
, p
):
1001 def splitFilesIntoBranches(self
, commit
):
1004 while commit
.has_key("depotFile%s" % fnum
):
1005 path
= commit
["depotFile%s" % fnum
]
1006 found
= [p
for p
in self
.depotPaths
1007 if p4PathStartsWith(path
, p
)]
1014 file["rev"] = commit
["rev%s" % fnum
]
1015 file["action"] = commit
["action%s" % fnum
]
1016 file["type"] = commit
["type%s" % fnum
]
1019 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
1021 for branch
in self
.knownBranches
.keys():
1023 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1024 if relPath
.startswith(branch
+ "/"):
1025 if branch
not in branches
:
1026 branches
[branch
] = []
1027 branches
[branch
].append(file)
1032 # output one file from the P4 stream
1033 # - helper for streamP4Files
1035 def streamOneP4File(self
, file, contents
):
1036 if file["type"] == "apple":
1037 print "\nfile %s is a strange apple file that forks. Ignoring" % \
1041 relPath
= self
.stripRepoPath(file['depotFile'], self
.branchPrefixes
)
1042 relPath
= self
.wildcard_decode(relPath
)
1044 sys
.stderr
.write("%s\n" % relPath
)
1047 if isP4Exec(file["type"]):
1049 elif file["type"] == "symlink":
1051 # p4 print on a symlink contains "target\n", so strip it off
1052 data
= ''.join(contents
)
1053 contents
= [data
[:-1]]
1055 if self
.isWindows
and file["type"].endswith("text"):
1057 for data
in contents
:
1058 data
= data
.replace("\r\n", "\n")
1059 mangled
.append(data
)
1062 if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1063 contents
= map(lambda text
: re
.sub(r
'(?i)\$(Id|Header):[^$]*\$',r
'$\1$', text
), contents
)
1064 elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1065 contents
= map(lambda text
: re
.sub(r
'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r
'$\1$', text
), contents
)
1067 self
.gitStream
.write("M %s inline %s\n" % (mode
, relPath
))
1072 length
= length
+ len(d
)
1074 self
.gitStream
.write("data %d\n" % length
)
1076 self
.gitStream
.write(d
)
1077 self
.gitStream
.write("\n")
1079 def streamOneP4Deletion(self
, file):
1080 relPath
= self
.stripRepoPath(file['path'], self
.branchPrefixes
)
1082 sys
.stderr
.write("delete %s\n" % relPath
)
1083 self
.gitStream
.write("D %s\n" % relPath
)
1085 # handle another chunk of streaming data
1086 def streamP4FilesCb(self
, marshalled
):
1088 if marshalled
.has_key('depotFile') and self
.stream_have_file_info
:
1089 # start of a new file - output the old one first
1090 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
1091 self
.stream_file
= {}
1092 self
.stream_contents
= []
1093 self
.stream_have_file_info
= False
1095 # pick up the new file information... for the
1096 # 'data' field we need to append to our array
1097 for k
in marshalled
.keys():
1099 self
.stream_contents
.append(marshalled
['data'])
1101 self
.stream_file
[k
] = marshalled
[k
]
1103 self
.stream_have_file_info
= True
1105 # Stream directly from "p4 files" into "git fast-import"
1106 def streamP4Files(self
, files
):
1113 for val
in self
.clientSpecDirs
:
1114 if f
['path'].startswith(val
[0]):
1120 filesForCommit
.append(f
)
1121 if f
['action'] in self
.delete_actions
:
1122 filesToDelete
.append(f
)
1124 filesToRead
.append(f
)
1127 for f
in filesToDelete
:
1128 self
.streamOneP4Deletion(f
)
1130 if len(filesToRead
) > 0:
1131 self
.stream_file
= {}
1132 self
.stream_contents
= []
1133 self
.stream_have_file_info
= False
1135 # curry self argument
1136 def streamP4FilesCbSelf(entry
):
1137 self
.streamP4FilesCb(entry
)
1139 p4CmdList("-x - print",
1140 '\n'.join(['%s#%s' % (f
['path'], f
['rev'])
1141 for f
in filesToRead
]),
1142 cb
=streamP4FilesCbSelf
)
1145 if self
.stream_file
.has_key('depotFile'):
1146 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
1148 def commit(self
, details
, files
, branch
, branchPrefixes
, parent
= ""):
1149 epoch
= details
["time"]
1150 author
= details
["user"]
1151 self
.branchPrefixes
= branchPrefixes
1154 print "commit into %s" % branch
1156 # start with reading files; if that fails, we should not
1160 if [p
for p
in branchPrefixes
if p4PathStartsWith(f
['path'], p
)]:
1161 new_files
.append (f
)
1163 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % f
['path'])
1165 self
.gitStream
.write("commit %s\n" % branch
)
1166 # gitStream.write("mark :%s\n" % details["change"])
1167 self
.committedChanges
.add(int(details
["change"]))
1169 if author
not in self
.users
:
1170 self
.getUserMapFromPerforceServer()
1171 if author
in self
.users
:
1172 committer
= "%s %s %s" % (self
.users
[author
], epoch
, self
.tz
)
1174 committer
= "%s <a@b> %s %s" % (author
, epoch
, self
.tz
)
1176 self
.gitStream
.write("committer %s\n" % committer
)
1178 self
.gitStream
.write("data <<EOT\n")
1179 self
.gitStream
.write(details
["desc"])
1180 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1181 % (','.join (branchPrefixes
), details
["change"]))
1182 if len(details
['options']) > 0:
1183 self
.gitStream
.write(": options = %s" % details
['options'])
1184 self
.gitStream
.write("]\nEOT\n\n")
1188 print "parent %s" % parent
1189 self
.gitStream
.write("from %s\n" % parent
)
1191 self
.streamP4Files(new_files
)
1192 self
.gitStream
.write("\n")
1194 change
= int(details
["change"])
1196 if self
.labels
.has_key(change
):
1197 label
= self
.labels
[change
]
1198 labelDetails
= label
[0]
1199 labelRevisions
= label
[1]
1201 print "Change %s is labelled %s" % (change
, labelDetails
)
1203 files
= p4CmdList("files " + ' '.join (["%s...@%s" % (p
, change
)
1204 for p
in branchPrefixes
]))
1206 if len(files
) == len(labelRevisions
):
1210 if info
["action"] in self
.delete_actions
:
1212 cleanedFiles
[info
["depotFile"]] = info
["rev"]
1214 if cleanedFiles
== labelRevisions
:
1215 self
.gitStream
.write("tag tag_%s\n" % labelDetails
["label"])
1216 self
.gitStream
.write("from %s\n" % branch
)
1218 owner
= labelDetails
["Owner"]
1220 if author
in self
.users
:
1221 tagger
= "%s %s %s" % (self
.users
[owner
], epoch
, self
.tz
)
1223 tagger
= "%s <a@b> %s %s" % (owner
, epoch
, self
.tz
)
1224 self
.gitStream
.write("tagger %s\n" % tagger
)
1225 self
.gitStream
.write("data <<EOT\n")
1226 self
.gitStream
.write(labelDetails
["Description"])
1227 self
.gitStream
.write("EOT\n\n")
1231 print ("Tag %s does not match with change %s: files do not match."
1232 % (labelDetails
["label"], change
))
1236 print ("Tag %s does not match with change %s: file count is different."
1237 % (labelDetails
["label"], change
))
1239 def getUserCacheFilename(self
):
1240 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
1241 return home
+ "/.gitp4-usercache.txt"
1243 def getUserMapFromPerforceServer(self
):
1244 if self
.userMapFromPerforceServer
:
1248 for output
in p4CmdList("users"):
1249 if not output
.has_key("User"):
1251 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
1255 for (key
, val
) in self
.users
.items():
1256 s
+= "%s\t%s\n" % (key
.expandtabs(1), val
.expandtabs(1))
1258 open(self
.getUserCacheFilename(), "wb").write(s
)
1259 self
.userMapFromPerforceServer
= True
1261 def loadUserMapFromCache(self
):
1263 self
.userMapFromPerforceServer
= False
1265 cache
= open(self
.getUserCacheFilename(), "rb")
1266 lines
= cache
.readlines()
1269 entry
= line
.strip().split("\t")
1270 self
.users
[entry
[0]] = entry
[1]
1272 self
.getUserMapFromPerforceServer()
1274 def getLabels(self
):
1277 l
= p4CmdList("labels %s..." % ' '.join (self
.depotPaths
))
1278 if len(l
) > 0 and not self
.silent
:
1279 print "Finding files belonging to labels in %s" % `self
.depotPaths`
1282 label
= output
["label"]
1286 print "Querying files for label %s" % label
1287 for file in p4CmdList("files "
1288 + ' '.join (["%s...@%s" % (p
, label
)
1289 for p
in self
.depotPaths
])):
1290 revisions
[file["depotFile"]] = file["rev"]
1291 change
= int(file["change"])
1292 if change
> newestChange
:
1293 newestChange
= change
1295 self
.labels
[newestChange
] = [output
, revisions
]
1298 print "Label changes: %s" % self
.labels
.keys()
1300 def guessProjectName(self
):
1301 for p
in self
.depotPaths
:
1304 p
= p
[p
.strip().rfind("/") + 1:]
1305 if not p
.endswith("/"):
1309 def getBranchMapping(self
):
1310 lostAndFoundBranches
= set()
1312 for info
in p4CmdList("branches"):
1313 details
= p4Cmd("branch -o %s" % info
["branch"])
1315 while details
.has_key("View%s" % viewIdx
):
1316 paths
= details
["View%s" % viewIdx
].split(" ")
1317 viewIdx
= viewIdx
+ 1
1318 # require standard //depot/foo/... //depot/bar/... mapping
1319 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
1322 destination
= paths
[1]
1324 if p4PathStartsWith(source
, self
.depotPaths
[0]) and p4PathStartsWith(destination
, self
.depotPaths
[0]):
1325 source
= source
[len(self
.depotPaths
[0]):-4]
1326 destination
= destination
[len(self
.depotPaths
[0]):-4]
1328 if destination
in self
.knownBranches
:
1330 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
1331 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
1334 self
.knownBranches
[destination
] = source
1336 lostAndFoundBranches
.discard(destination
)
1338 if source
not in self
.knownBranches
:
1339 lostAndFoundBranches
.add(source
)
1342 for branch
in lostAndFoundBranches
:
1343 self
.knownBranches
[branch
] = branch
1345 def getBranchMappingFromGitBranches(self
):
1346 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1347 for branch
in branches
.keys():
1348 if branch
== "master":
1351 branch
= branch
[len(self
.projectName
):]
1352 self
.knownBranches
[branch
] = branch
1354 def listExistingP4GitBranches(self
):
1355 # branches holds mapping from name to commit
1356 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1357 self
.p4BranchesInGit
= branches
.keys()
1358 for branch
in branches
.keys():
1359 self
.initialParents
[self
.refPrefix
+ branch
] = branches
[branch
]
1361 def updateOptionDict(self
, d
):
1363 if self
.keepRepoPath
:
1364 option_keys
['keepRepoPath'] = 1
1366 d
["options"] = ' '.join(sorted(option_keys
.keys()))
1368 def readOptions(self
, d
):
1369 self
.keepRepoPath
= (d
.has_key('options')
1370 and ('keepRepoPath' in d
['options']))
1372 def gitRefForBranch(self
, branch
):
1373 if branch
== "main":
1374 return self
.refPrefix
+ "master"
1376 if len(branch
) <= 0:
1379 return self
.refPrefix
+ self
.projectName
+ branch
1381 def gitCommitByP4Change(self
, ref
, change
):
1383 print "looking in ref " + ref
+ " for change %s using bisect..." % change
1386 latestCommit
= parseRevision(ref
)
1390 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
1391 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
1396 log
= extractLogMessageFromGitCommit(next
)
1397 settings
= extractSettingsGitLog(log
)
1398 currentChange
= int(settings
['change'])
1400 print "current change %s" % currentChange
1402 if currentChange
== change
:
1404 print "found %s" % next
1407 if currentChange
< change
:
1408 earliestCommit
= "^%s" % next
1410 latestCommit
= "%s" % next
1414 def importNewBranch(self
, branch
, maxChange
):
1415 # make fast-import flush all changes to disk and update the refs using the checkpoint
1416 # command so that we can try to find the branch parent in the git history
1417 self
.gitStream
.write("checkpoint\n\n");
1418 self
.gitStream
.flush();
1419 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1420 range = "@1,%s" % maxChange
1421 #print "prefix" + branchPrefix
1422 changes
= p4ChangesForPaths([branchPrefix
], range)
1423 if len(changes
) <= 0:
1425 firstChange
= changes
[0]
1426 #print "first change in branch: %s" % firstChange
1427 sourceBranch
= self
.knownBranches
[branch
]
1428 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
1429 sourceRef
= self
.gitRefForBranch(sourceBranch
)
1430 #print "source " + sourceBranch
1432 branchParentChange
= int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath
, firstChange
))["change"])
1433 #print "branch parent: %s" % branchParentChange
1434 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
1435 if len(gitParent
) > 0:
1436 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
1437 #print "parent git commit: %s" % gitParent
1439 self
.importChanges(changes
)
1442 def importChanges(self
, changes
):
1444 for change
in changes
:
1445 description
= p4Cmd("describe %s" % change
)
1446 self
.updateOptionDict(description
)
1449 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
1454 if self
.detectBranches
:
1455 branches
= self
.splitFilesIntoBranches(description
)
1456 for branch
in branches
.keys():
1458 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1462 filesForCommit
= branches
[branch
]
1465 print "branch is %s" % branch
1467 self
.updatedBranches
.add(branch
)
1469 if branch
not in self
.createdBranches
:
1470 self
.createdBranches
.add(branch
)
1471 parent
= self
.knownBranches
[branch
]
1472 if parent
== branch
:
1475 fullBranch
= self
.projectName
+ branch
1476 if fullBranch
not in self
.p4BranchesInGit
:
1478 print("\n Importing new branch %s" % fullBranch
);
1479 if self
.importNewBranch(branch
, change
- 1):
1481 self
.p4BranchesInGit
.append(fullBranch
)
1483 print("\n Resuming with change %s" % change
);
1486 print "parent determined through known branches: %s" % parent
1488 branch
= self
.gitRefForBranch(branch
)
1489 parent
= self
.gitRefForBranch(parent
)
1492 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
1494 if len(parent
) == 0 and branch
in self
.initialParents
:
1495 parent
= self
.initialParents
[branch
]
1496 del self
.initialParents
[branch
]
1498 self
.commit(description
, filesForCommit
, branch
, [branchPrefix
], parent
)
1500 files
= self
.extractFilesFromCommit(description
)
1501 self
.commit(description
, files
, self
.branch
, self
.depotPaths
,
1503 self
.initialParent
= ""
1505 print self
.gitError
.read()
1508 def importHeadRevision(self
, revision
):
1509 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
1511 details
= { "user" : "git perforce import user", "time" : int(time
.time()) }
1512 details
["desc"] = ("Initial import of %s from the state at revision %s\n"
1513 % (' '.join(self
.depotPaths
), revision
))
1514 details
["change"] = revision
1518 for info
in p4CmdList("files "
1519 + ' '.join(["%s...%s"
1521 for p
in self
.depotPaths
])):
1523 if 'code' in info
and info
['code'] == 'error':
1524 sys
.stderr
.write("p4 returned an error: %s\n"
1526 if info
['data'].find("must refer to client") >= 0:
1527 sys
.stderr
.write("This particular p4 error is misleading.\n")
1528 sys
.stderr
.write("Perhaps the depot path was misspelled.\n");
1529 sys
.stderr
.write("Depot path: %s\n" % " ".join(self
.depotPaths
))
1531 if 'p4ExitCode' in info
:
1532 sys
.stderr
.write("p4 exitcode: %s\n" % info
['p4ExitCode'])
1536 change
= int(info
["change"])
1537 if change
> newestRevision
:
1538 newestRevision
= change
1540 if info
["action"] in self
.delete_actions
:
1541 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1542 #fileCnt = fileCnt + 1
1545 for prop
in ["depotFile", "rev", "action", "type" ]:
1546 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
1548 fileCnt
= fileCnt
+ 1
1550 details
["change"] = newestRevision
1551 self
.updateOptionDict(details
)
1553 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
, self
.depotPaths
)
1555 print "IO error with git fast-import. Is your git version recent enough?"
1556 print self
.gitError
.read()
1559 def getClientSpec(self
):
1560 specList
= p4CmdList( "client -o" )
1562 for entry
in specList
:
1563 for k
,v
in entry
.iteritems():
1564 if k
.startswith("View"):
1566 # p4 has these %%1 to %%9 arguments in specs to
1567 # reorder paths; which we can't handle (yet :)
1568 if re
.match('%%\d', v
) != None:
1569 print "Sorry, can't handle %%n arguments in client specs"
1572 if v
.startswith('"'):
1576 index
= v
.find("...")
1578 # save the "client view"; i.e the RHS of the view
1579 # line that tells the client where to put the
1580 # files for this view.
1581 cv
= v
[index
+3:].strip() # +3 to remove previous '...'
1583 # if the client view doesn't end with a
1584 # ... wildcard, then we're going to mess up the
1585 # output directory, so fail gracefully.
1586 if not cv
.endswith('...'):
1587 print 'Sorry, client view in "%s" needs to end with wildcard' % (k
)
1591 # now save the view; +index means included, -index
1592 # means it should be filtered out.
1594 if v
.startswith("-"):
1600 temp
[v
] = (include
, cv
)
1602 self
.clientSpecDirs
= temp
.items()
1603 self
.clientSpecDirs
.sort( lambda x
, y
: abs( y
[1][0] ) - abs( x
[1][0] ) )
1605 def run(self
, args
):
1606 self
.depotPaths
= []
1607 self
.changeRange
= ""
1608 self
.initialParent
= ""
1609 self
.previousDepotPaths
= []
1611 # map from branch depot path to parent branch
1612 self
.knownBranches
= {}
1613 self
.initialParents
= {}
1614 self
.hasOrigin
= originP4BranchesExist()
1615 if not self
.syncWithOrigin
:
1616 self
.hasOrigin
= False
1618 if self
.importIntoRemotes
:
1619 self
.refPrefix
= "refs/remotes/p4/"
1621 self
.refPrefix
= "refs/heads/p4/"
1623 if self
.syncWithOrigin
and self
.hasOrigin
:
1625 print "Syncing with origin first by calling git fetch origin"
1626 system("git fetch origin")
1628 if len(self
.branch
) == 0:
1629 self
.branch
= self
.refPrefix
+ "master"
1630 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
1631 system("git update-ref %s refs/heads/p4" % self
.branch
)
1632 system("git branch -D p4");
1633 # create it /after/ importing, when master exists
1634 if not gitBranchExists(self
.refPrefix
+ "HEAD") and self
.importIntoRemotes
and gitBranchExists(self
.branch
):
1635 system("git symbolic-ref %sHEAD %s" % (self
.refPrefix
, self
.branch
))
1637 if self
.useClientSpec
or gitConfig("git-p4.useclientspec") == "true":
1638 self
.getClientSpec()
1640 # TODO: should always look at previous commits,
1641 # merge with previous imports, if possible.
1644 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
1645 self
.listExistingP4GitBranches()
1647 if len(self
.p4BranchesInGit
) > 1:
1649 print "Importing from/into multiple branches"
1650 self
.detectBranches
= True
1653 print "branches: %s" % self
.p4BranchesInGit
1656 for branch
in self
.p4BranchesInGit
:
1657 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
1659 settings
= extractSettingsGitLog(logMsg
)
1661 self
.readOptions(settings
)
1662 if (settings
.has_key('depot-paths')
1663 and settings
.has_key ('change')):
1664 change
= int(settings
['change']) + 1
1665 p4Change
= max(p4Change
, change
)
1667 depotPaths
= sorted(settings
['depot-paths'])
1668 if self
.previousDepotPaths
== []:
1669 self
.previousDepotPaths
= depotPaths
1672 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
1673 for i
in range(0, min(len(cur
), len(prev
))):
1674 if cur
[i
] <> prev
[i
]:
1678 paths
.append (cur
[:i
+ 1])
1680 self
.previousDepotPaths
= paths
1683 self
.depotPaths
= sorted(self
.previousDepotPaths
)
1684 self
.changeRange
= "@%s,#head" % p4Change
1685 if not self
.detectBranches
:
1686 self
.initialParent
= parseRevision(self
.branch
)
1687 if not self
.silent
and not self
.detectBranches
:
1688 print "Performing incremental import into %s git branch" % self
.branch
1690 if not self
.branch
.startswith("refs/"):
1691 self
.branch
= "refs/heads/" + self
.branch
1693 if len(args
) == 0 and self
.depotPaths
:
1695 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
1697 if self
.depotPaths
and self
.depotPaths
!= args
:
1698 print ("previous import used depot path %s and now %s was specified. "
1699 "This doesn't work!" % (' '.join (self
.depotPaths
),
1703 self
.depotPaths
= sorted(args
)
1709 for p
in self
.depotPaths
:
1710 if p
.find("@") != -1:
1711 atIdx
= p
.index("@")
1712 self
.changeRange
= p
[atIdx
:]
1713 if self
.changeRange
== "@all":
1714 self
.changeRange
= ""
1715 elif ',' not in self
.changeRange
:
1716 revision
= self
.changeRange
1717 self
.changeRange
= ""
1719 elif p
.find("#") != -1:
1720 hashIdx
= p
.index("#")
1721 revision
= p
[hashIdx
:]
1723 elif self
.previousDepotPaths
== []:
1726 p
= re
.sub ("\.\.\.$", "", p
)
1727 if not p
.endswith("/"):
1732 self
.depotPaths
= newPaths
1735 self
.loadUserMapFromCache()
1737 if self
.detectLabels
:
1740 if self
.detectBranches
:
1741 ## FIXME - what's a P4 projectName ?
1742 self
.projectName
= self
.guessProjectName()
1745 self
.getBranchMappingFromGitBranches()
1747 self
.getBranchMapping()
1749 print "p4-git branches: %s" % self
.p4BranchesInGit
1750 print "initial parents: %s" % self
.initialParents
1751 for b
in self
.p4BranchesInGit
:
1755 b
= b
[len(self
.projectName
):]
1756 self
.createdBranches
.add(b
)
1758 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
1760 importProcess
= subprocess
.Popen(["git", "fast-import"],
1761 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
1762 stderr
=subprocess
.PIPE
);
1763 self
.gitOutput
= importProcess
.stdout
1764 self
.gitStream
= importProcess
.stdin
1765 self
.gitError
= importProcess
.stderr
1768 self
.importHeadRevision(revision
)
1772 if len(self
.changesFile
) > 0:
1773 output
= open(self
.changesFile
).readlines()
1776 changeSet
.add(int(line
))
1778 for change
in changeSet
:
1779 changes
.append(change
)
1783 # catch "git-p4 sync" with no new branches, in a repo that
1784 # does not have any existing git-p4 branches
1785 if len(args
) == 0 and not self
.p4BranchesInGit
:
1786 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
1788 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
1790 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
)
1792 if len(self
.maxChanges
) > 0:
1793 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
1795 if len(changes
) == 0:
1797 print "No changes to import!"
1800 if not self
.silent
and not self
.detectBranches
:
1801 print "Import destination: %s" % self
.branch
1803 self
.updatedBranches
= set()
1805 self
.importChanges(changes
)
1809 if len(self
.updatedBranches
) > 0:
1810 sys
.stdout
.write("Updated branches: ")
1811 for b
in self
.updatedBranches
:
1812 sys
.stdout
.write("%s " % b
)
1813 sys
.stdout
.write("\n")
1815 self
.gitStream
.close()
1816 if importProcess
.wait() != 0:
1817 die("fast-import failed: %s" % self
.gitError
.read())
1818 self
.gitOutput
.close()
1819 self
.gitError
.close()
1823 class P4Rebase(Command
):
1825 Command
.__init
__(self
)
1827 self
.description
= ("Fetches the latest revision from perforce and "
1828 + "rebases the current work (branch) against it")
1829 self
.verbose
= False
1831 def run(self
, args
):
1835 return self
.rebase()
1838 if os
.system("git update-index --refresh") != 0:
1839 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.");
1840 if len(read_pipe("git diff-index HEAD --")) > 0:
1841 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1843 [upstream
, settings
] = findUpstreamBranchPoint()
1844 if len(upstream
) == 0:
1845 die("Cannot find upstream branchpoint for rebase")
1847 # the branchpoint may be p4/foo~3, so strip off the parent
1848 upstream
= re
.sub("~[0-9]+$", "", upstream
)
1850 print "Rebasing the current branch onto %s" % upstream
1851 oldHead
= read_pipe("git rev-parse HEAD").strip()
1852 system("git rebase %s" % upstream
)
1853 system("git diff-tree --stat --summary -M %s HEAD" % oldHead
)
1856 class P4Clone(P4Sync
):
1858 P4Sync
.__init
__(self
)
1859 self
.description
= "Creates a new git repository and imports from Perforce into it"
1860 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
1862 optparse
.make_option("--destination", dest
="cloneDestination",
1863 action
='store', default
=None,
1864 help="where to leave result of the clone"),
1865 optparse
.make_option("-/", dest
="cloneExclude",
1866 action
="append", type="string",
1867 help="exclude depot path"),
1868 optparse
.make_option("--bare", dest
="cloneBare",
1869 action
="store_true", default
=False),
1871 self
.cloneDestination
= None
1872 self
.needsGit
= False
1873 self
.cloneBare
= False
1875 # This is required for the "append" cloneExclude action
1876 def ensure_value(self
, attr
, value
):
1877 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
1878 setattr(self
, attr
, value
)
1879 return getattr(self
, attr
)
1881 def defaultDestination(self
, args
):
1882 ## TODO: use common prefix of args?
1884 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
1885 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
1886 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
1887 depotDir
= re
.sub(r
"/$", "", depotDir
)
1888 return os
.path
.split(depotDir
)[1]
1890 def run(self
, args
):
1894 if self
.keepRepoPath
and not self
.cloneDestination
:
1895 sys
.stderr
.write("Must specify destination for --keep-path\n")
1900 if not self
.cloneDestination
and len(depotPaths
) > 1:
1901 self
.cloneDestination
= depotPaths
[-1]
1902 depotPaths
= depotPaths
[:-1]
1904 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
1905 for p
in depotPaths
:
1906 if not p
.startswith("//"):
1909 if not self
.cloneDestination
:
1910 self
.cloneDestination
= self
.defaultDestination(args
)
1912 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
1914 if not os
.path
.exists(self
.cloneDestination
):
1915 os
.makedirs(self
.cloneDestination
)
1916 chdir(self
.cloneDestination
)
1918 init_cmd
= [ "git", "init" ]
1920 init_cmd
.append("--bare")
1921 subprocess
.check_call(init_cmd
)
1923 if not P4Sync
.run(self
, depotPaths
):
1925 if self
.branch
!= "master":
1926 if self
.importIntoRemotes
:
1927 masterbranch
= "refs/remotes/p4/master"
1929 masterbranch
= "refs/heads/p4/master"
1930 if gitBranchExists(masterbranch
):
1931 system("git branch master %s" % masterbranch
)
1932 if not self
.cloneBare
:
1933 system("git checkout -f")
1935 print "Could not detect main branch. No checkout/master branch created."
1939 class P4Branches(Command
):
1941 Command
.__init
__(self
)
1943 self
.description
= ("Shows the git branches that hold imports and their "
1944 + "corresponding perforce depot paths")
1945 self
.verbose
= False
1947 def run(self
, args
):
1948 if originP4BranchesExist():
1949 createOrUpdateBranchesFromOrigin()
1951 cmdline
= "git rev-parse --symbolic "
1952 cmdline
+= " --remotes"
1954 for line
in read_pipe_lines(cmdline
):
1957 if not line
.startswith('p4/') or line
== "p4/HEAD":
1961 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
1962 settings
= extractSettingsGitLog(log
)
1964 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
1967 class HelpFormatter(optparse
.IndentedHelpFormatter
):
1969 optparse
.IndentedHelpFormatter
.__init
__(self
)
1971 def format_description(self
, description
):
1973 return description
+ "\n"
1977 def printUsage(commands
):
1978 print "usage: %s <command> [options]" % sys
.argv
[0]
1980 print "valid commands: %s" % ", ".join(commands
)
1982 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
1987 "submit" : P4Submit
,
1988 "commit" : P4Submit
,
1990 "rebase" : P4Rebase
,
1992 "rollback" : P4RollBack
,
1993 "branches" : P4Branches
1998 if len(sys
.argv
[1:]) == 0:
1999 printUsage(commands
.keys())
2003 cmdName
= sys
.argv
[1]
2005 klass
= commands
[cmdName
]
2008 print "unknown command %s" % cmdName
2010 printUsage(commands
.keys())
2013 options
= cmd
.options
2014 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
2018 if len(options
) > 0:
2019 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
2021 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
2023 description
= cmd
.description
,
2024 formatter
= HelpFormatter())
2026 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
2028 verbose
= cmd
.verbose
2030 if cmd
.gitdir
== None:
2031 cmd
.gitdir
= os
.path
.abspath(".git")
2032 if not isValidGitDir(cmd
.gitdir
):
2033 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
2034 if os
.path
.exists(cmd
.gitdir
):
2035 cdup
= read_pipe("git rev-parse --show-cdup").strip()
2039 if not isValidGitDir(cmd
.gitdir
):
2040 if isValidGitDir(cmd
.gitdir
+ "/.git"):
2041 cmd
.gitdir
+= "/.git"
2043 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
2045 os
.environ
["GIT_DIR"] = cmd
.gitdir
2047 if not cmd
.run(args
):
2051 if __name__
== '__main__':