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]"
479 self
.userMapFromPerforceServer
= False
481 def getUserCacheFilename(self
):
482 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
483 return home
+ "/.gitp4-usercache.txt"
485 def getUserMapFromPerforceServer(self
):
486 if self
.userMapFromPerforceServer
:
491 for output
in p4CmdList("users"):
492 if not output
.has_key("User"):
494 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
495 self
.emails
[output
["Email"]] = output
["User"]
499 for (key
, val
) in self
.users
.items():
500 s
+= "%s\t%s\n" % (key
.expandtabs(1), val
.expandtabs(1))
502 open(self
.getUserCacheFilename(), "wb").write(s
)
503 self
.userMapFromPerforceServer
= True
505 def loadUserMapFromCache(self
):
507 self
.userMapFromPerforceServer
= False
509 cache
= open(self
.getUserCacheFilename(), "rb")
510 lines
= cache
.readlines()
513 entry
= line
.strip().split("\t")
514 self
.users
[entry
[0]] = entry
[1]
516 self
.getUserMapFromPerforceServer()
518 class P4Debug(Command
):
520 Command
.__init
__(self
)
522 optparse
.make_option("--verbose", dest
="verbose", action
="store_true",
525 self
.description
= "A tool to debug the output of p4 -G."
526 self
.needsGit
= False
531 for output
in p4CmdList(" ".join(args
)):
532 print 'Element: %d' % j
537 class P4RollBack(Command
):
539 Command
.__init
__(self
)
541 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
542 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
544 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
546 self
.rollbackLocalBranches
= False
551 maxChange
= int(args
[0])
553 if "p4ExitCode" in p4Cmd("changes -m 1"):
554 die("Problems executing p4");
556 if self
.rollbackLocalBranches
:
557 refPrefix
= "refs/heads/"
558 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
560 refPrefix
= "refs/remotes/"
561 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
564 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
566 ref
= refPrefix
+ line
567 log
= extractLogMessageFromGitCommit(ref
)
568 settings
= extractSettingsGitLog(log
)
570 depotPaths
= settings
['depot-paths']
571 change
= settings
['change']
575 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
576 for p
in depotPaths
]))) == 0:
577 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
578 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
581 while change
and int(change
) > maxChange
:
584 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
585 system("git update-ref %s \"%s^\"" % (ref
, ref
))
586 log
= extractLogMessageFromGitCommit(ref
)
587 settings
= extractSettingsGitLog(log
)
590 depotPaths
= settings
['depot-paths']
591 change
= settings
['change']
594 print "%s rewound to %s" % (ref
, change
)
598 class P4Submit(Command
, P4UserMap
):
600 Command
.__init
__(self
)
601 P4UserMap
.__init
__(self
)
603 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
604 optparse
.make_option("--origin", dest
="origin"),
605 optparse
.make_option("-M", dest
="detectRenames", action
="store_true"),
606 # preserve the user, requires relevant p4 permissions
607 optparse
.make_option("--preserve-user", dest
="preserveUser", action
="store_true"),
609 self
.description
= "Submit changes from git to the perforce depot."
610 self
.usage
+= " [name of git branch to submit into perforce depot]"
611 self
.interactive
= True
613 self
.detectRenames
= False
615 self
.preserveUser
= gitConfig("git-p4.preserveUser").lower() == "true"
616 self
.isWindows
= (platform
.system() == "Windows")
619 if len(p4CmdList("opened ...")) > 0:
620 die("You have files opened with perforce! Close them before starting the sync.")
622 # replaces everything between 'Description:' and the next P4 submit template field with the
624 def prepareLogMessage(self
, template
, message
):
627 inDescriptionSection
= False
629 for line
in template
.split("\n"):
630 if line
.startswith("#"):
631 result
+= line
+ "\n"
634 if inDescriptionSection
:
635 if line
.startswith("Files:") or line
.startswith("Jobs:"):
636 inDescriptionSection
= False
640 if line
.startswith("Description:"):
641 inDescriptionSection
= True
643 for messageLine
in message
.split("\n"):
644 line
+= "\t" + messageLine
+ "\n"
646 result
+= line
+ "\n"
650 def p4UserForCommit(self
,id):
651 # Return the tuple (perforce user,git email) for a given git commit id
652 self
.getUserMapFromPerforceServer()
653 gitEmail
= read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
654 gitEmail
= gitEmail
.strip()
655 if not self
.emails
.has_key(gitEmail
):
656 return (None,gitEmail
)
658 return (self
.emails
[gitEmail
],gitEmail
)
660 def checkValidP4Users(self
,commits
):
661 # check if any git authors cannot be mapped to p4 users
663 (user
,email
) = self
.p4UserForCommit(id)
665 msg
= "Cannot find p4 user for email %s in commit %s." % (email
, id)
666 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
669 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg
)
671 def lastP4Changelist(self
):
672 # Get back the last changelist number submitted in this client spec. This
673 # then gets used to patch up the username in the change. If the same
674 # client spec is being used by multiple processes then this might go
676 results
= p4CmdList("client -o") # find the current client
679 if r
.has_key('Client'):
683 die("could not get client spec")
684 results
= p4CmdList("changes -c %s -m 1" % client
)
686 if r
.has_key('change'):
688 die("Could not get changelist number for last submit - cannot patch up user details")
690 def modifyChangelistUser(self
, changelist
, newUser
):
691 # fixup the user field of a changelist after it has been submitted.
692 changes
= p4CmdList("change -o %s" % changelist
)
693 if len(changes
) != 1:
694 die("Bad output from p4 change modifying %s to user %s" %
695 (changelist
, newUser
))
698 if c
['User'] == newUser
: return # nothing to do
700 input = marshal
.dumps(c
)
702 result
= p4CmdList("change -f -i", stdin
=input)
704 if r
.has_key('code'):
705 if r
['code'] == 'error':
706 die("Could not modify user field of changelist %s to %s:%s" % (changelist
, newUser
, r
['data']))
707 if r
.has_key('data'):
708 print("Updated user field for changelist %s to %s" % (changelist
, newUser
))
710 die("Could not modify user field of changelist %s to %s" % (changelist
, newUser
))
712 def canChangeChangelists(self
):
713 # check to see if we have p4 admin or super-user permissions, either of
714 # which are required to modify changelists.
715 results
= p4CmdList("protects %s" % self
.depotPath
)
717 if r
.has_key('perm'):
718 if r
['perm'] == 'admin':
720 if r
['perm'] == 'super':
724 def prepareSubmitTemplate(self
):
725 # remove lines in the Files section that show changes to files outside the depot path we're committing into
727 inFilesSection
= False
728 for line
in p4_read_pipe_lines("change -o"):
729 if line
.endswith("\r\n"):
730 line
= line
[:-2] + "\n"
732 if line
.startswith("\t"):
733 # path starts and ends with a tab
735 lastTab
= path
.rfind("\t")
737 path
= path
[:lastTab
]
738 if not p4PathStartsWith(path
, self
.depotPath
):
741 inFilesSection
= False
743 if line
.startswith("Files:"):
744 inFilesSection
= True
750 def applyCommit(self
, id):
751 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
753 if self
.preserveUser
:
754 (p4User
, gitEmail
) = self
.p4UserForCommit(id)
756 if not self
.detectRenames
:
757 # If not explicitly set check the config variable
758 self
.detectRenames
= gitConfig("git-p4.detectRenames").lower() == "true"
760 if self
.detectRenames
:
765 if gitConfig("git-p4.detectCopies").lower() == "true":
768 if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
769 diffOpts
+= " --find-copies-harder"
771 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts
, id, id))
773 filesToDelete
= set()
775 filesToChangeExecBit
= {}
777 diff
= parseDiffTreeEntry(line
)
778 modifier
= diff
['status']
781 p4_system("edit \"%s\"" % path
)
782 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
783 filesToChangeExecBit
[path
] = diff
['dst_mode']
784 editedFiles
.add(path
)
785 elif modifier
== "A":
787 filesToChangeExecBit
[path
] = diff
['dst_mode']
788 if path
in filesToDelete
:
789 filesToDelete
.remove(path
)
790 elif modifier
== "D":
791 filesToDelete
.add(path
)
792 if path
in filesToAdd
:
793 filesToAdd
.remove(path
)
794 elif modifier
== "C":
795 src
, dest
= diff
['src'], diff
['dst']
796 p4_system("integrate -Dt \"%s\" \"%s\"" % (src
, dest
))
797 if diff
['src_sha1'] != diff
['dst_sha1']:
798 p4_system("edit \"%s\"" % (dest
))
799 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
800 p4_system("edit \"%s\"" % (dest
))
801 filesToChangeExecBit
[dest
] = diff
['dst_mode']
803 editedFiles
.add(dest
)
804 elif modifier
== "R":
805 src
, dest
= diff
['src'], diff
['dst']
806 p4_system("integrate -Dt \"%s\" \"%s\"" % (src
, dest
))
807 if diff
['src_sha1'] != diff
['dst_sha1']:
808 p4_system("edit \"%s\"" % (dest
))
809 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
810 p4_system("edit \"%s\"" % (dest
))
811 filesToChangeExecBit
[dest
] = diff
['dst_mode']
813 editedFiles
.add(dest
)
814 filesToDelete
.add(src
)
816 die("unknown modifier %s for %s" % (modifier
, path
))
818 diffcmd
= "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
819 patchcmd
= diffcmd
+ " | git apply "
820 tryPatchCmd
= patchcmd
+ "--check -"
821 applyPatchCmd
= patchcmd
+ "--check --apply -"
823 if os
.system(tryPatchCmd
) != 0:
824 print "Unfortunately applying the change failed!"
825 print "What do you want to do?"
827 while response
!= "s" and response
!= "a" and response
!= "w":
828 response
= raw_input("[s]kip this patch / [a]pply the patch forcibly "
829 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
831 print "Skipping! Good luck with the next patches..."
832 for f
in editedFiles
:
833 p4_system("revert \"%s\"" % f
);
837 elif response
== "a":
838 os
.system(applyPatchCmd
)
839 if len(filesToAdd
) > 0:
840 print "You may also want to call p4 add on the following files:"
841 print " ".join(filesToAdd
)
842 if len(filesToDelete
):
843 print "The following files should be scheduled for deletion with p4 delete:"
844 print " ".join(filesToDelete
)
845 die("Please resolve and submit the conflict manually and "
846 + "continue afterwards with git-p4 submit --continue")
847 elif response
== "w":
848 system(diffcmd
+ " > patch.txt")
849 print "Patch saved to patch.txt in %s !" % self
.clientPath
850 die("Please resolve and submit the conflict manually and "
851 "continue afterwards with git-p4 submit --continue")
853 system(applyPatchCmd
)
856 p4_system("add \"%s\"" % f
)
857 for f
in filesToDelete
:
858 p4_system("revert \"%s\"" % f
)
859 p4_system("delete \"%s\"" % f
)
861 # Set/clear executable bits
862 for f
in filesToChangeExecBit
.keys():
863 mode
= filesToChangeExecBit
[f
]
864 setP4ExecBit(f
, mode
)
866 logMessage
= extractLogMessageFromGitCommit(id)
867 logMessage
= logMessage
.strip()
869 template
= self
.prepareSubmitTemplate()
872 submitTemplate
= self
.prepareLogMessage(template
, logMessage
)
874 if self
.preserveUser
:
875 submitTemplate
= submitTemplate
+ ("\n######## Actual user %s, modified after commit\n" % p4User
)
877 if os
.environ
.has_key("P4DIFF"):
878 del(os
.environ
["P4DIFF"])
880 for editedFile
in editedFiles
:
881 diff
+= p4_read_pipe("diff -du %r" % editedFile
)
884 for newFile
in filesToAdd
:
885 newdiff
+= "==== new file ====\n"
886 newdiff
+= "--- /dev/null\n"
887 newdiff
+= "+++ %s\n" % newFile
888 f
= open(newFile
, "r")
889 for line
in f
.readlines():
890 newdiff
+= "+" + line
893 separatorLine
= "######## everything below this line is just the diff #######\n"
895 [handle
, fileName
] = tempfile
.mkstemp()
896 tmpFile
= os
.fdopen(handle
, "w+")
898 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
899 separatorLine
= separatorLine
.replace("\n", "\r\n")
900 newdiff
= newdiff
.replace("\n", "\r\n")
901 tmpFile
.write(submitTemplate
+ separatorLine
+ diff
+ newdiff
)
903 mtime
= os
.stat(fileName
).st_mtime
904 if os
.environ
.has_key("P4EDITOR"):
905 editor
= os
.environ
.get("P4EDITOR")
907 editor
= read_pipe("git var GIT_EDITOR").strip()
908 system(editor
+ " " + fileName
)
910 if gitConfig("git-p4.skipSubmitEditCheck") == "true":
916 if checkModTime
and (os
.stat(fileName
).st_mtime
<= mtime
):
918 while response
!= "y" and response
!= "n":
919 response
= raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
922 tmpFile
= open(fileName
, "rb")
923 message
= tmpFile
.read()
925 submitTemplate
= message
[:message
.index(separatorLine
)]
927 submitTemplate
= submitTemplate
.replace("\r\n", "\n")
928 p4_write_pipe("submit -i", submitTemplate
)
930 if self
.preserveUser
:
932 # Get last changelist number. Cannot easily get it from
933 # the submit command output as the output is unmarshalled.
934 changelist
= self
.lastP4Changelist()
935 self
.modifyChangelistUser(changelist
, p4User
)
938 for f
in editedFiles
:
939 p4_system("revert \"%s\"" % f
);
941 p4_system("revert \"%s\"" % f
);
946 fileName
= "submit.txt"
947 file = open(fileName
, "w+")
948 file.write(self
.prepareLogMessage(template
, logMessage
))
950 print ("Perforce submit template written as %s. "
951 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
952 % (fileName
, fileName
))
956 self
.master
= currentGitBranch()
957 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
958 die("Detecting current git branch failed!")
960 self
.master
= args
[0]
964 allowSubmit
= gitConfig("git-p4.allowSubmit")
965 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
966 die("%s is not in git-p4.allowSubmit" % self
.master
)
968 [upstream
, settings
] = findUpstreamBranchPoint()
969 self
.depotPath
= settings
['depot-paths'][0]
970 if len(self
.origin
) == 0:
971 self
.origin
= upstream
973 if self
.preserveUser
:
974 if not self
.canChangeChangelists():
975 die("Cannot preserve user names without p4 super-user or admin permissions")
978 print "Origin branch is " + self
.origin
980 if len(self
.depotPath
) == 0:
981 print "Internal error: cannot locate perforce depot path from existing branches"
984 self
.clientPath
= p4Where(self
.depotPath
)
986 if len(self
.clientPath
) == 0:
987 print "Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
990 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
991 self
.oldWorkingDirectory
= os
.getcwd()
993 chdir(self
.clientPath
)
994 print "Synchronizing p4 checkout..."
995 p4_system("sync ...")
1000 for line
in read_pipe_lines("git rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)):
1001 commits
.append(line
.strip())
1004 if self
.preserveUser
:
1005 self
.checkValidP4Users(commits
)
1007 while len(commits
) > 0:
1009 commits
= commits
[1:]
1010 self
.applyCommit(commit
)
1011 if not self
.interactive
:
1014 if len(commits
) == 0:
1015 print "All changes applied!"
1016 chdir(self
.oldWorkingDirectory
)
1026 class P4Sync(Command
, P4UserMap
):
1027 delete_actions
= ( "delete", "move/delete", "purge" )
1030 Command
.__init
__(self
)
1031 P4UserMap
.__init
__(self
)
1033 optparse
.make_option("--branch", dest
="branch"),
1034 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
1035 optparse
.make_option("--changesfile", dest
="changesFile"),
1036 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
1037 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
1038 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
1039 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
1040 help="Import into refs/heads/ , not refs/remotes"),
1041 optparse
.make_option("--max-changes", dest
="maxChanges"),
1042 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
1043 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1044 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
1045 help="Only sync files that are included in the Perforce Client Spec")
1047 self
.description
= """Imports from Perforce into a git repository.\n
1049 //depot/my/project/ -- to import the current head
1050 //depot/my/project/@all -- to import everything
1051 //depot/my/project/@1,6 -- to import only from revision 1 to 6
1053 (a ... is not needed in the path p4 specification, it's added implicitly)"""
1055 self
.usage
+= " //depot/path[@revRange]"
1057 self
.createdBranches
= set()
1058 self
.committedChanges
= set()
1060 self
.detectBranches
= False
1061 self
.detectLabels
= False
1062 self
.changesFile
= ""
1063 self
.syncWithOrigin
= True
1064 self
.verbose
= False
1065 self
.importIntoRemotes
= True
1066 self
.maxChanges
= ""
1067 self
.isWindows
= (platform
.system() == "Windows")
1068 self
.keepRepoPath
= False
1069 self
.depotPaths
= None
1070 self
.p4BranchesInGit
= []
1071 self
.cloneExclude
= []
1072 self
.useClientSpec
= False
1073 self
.clientSpecDirs
= []
1075 if gitConfig("git-p4.syncFromOrigin") == "false":
1076 self
.syncWithOrigin
= False
1079 # P4 wildcards are not allowed in filenames. P4 complains
1080 # if you simply add them, but you can force it with "-f", in
1081 # which case it translates them into %xx encoding internally.
1082 # Search for and fix just these four characters. Do % last so
1083 # that fixing it does not inadvertently create new %-escapes.
1085 def wildcard_decode(self
, path
):
1086 # Cannot have * in a filename in windows; untested as to
1087 # what p4 would do in such a case.
1088 if not self
.isWindows
:
1089 path
= path
.replace("%2A", "*")
1090 path
= path
.replace("%23", "#") \
1091 .replace("%40", "@") \
1092 .replace("%25", "%")
1095 def extractFilesFromCommit(self
, commit
):
1096 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
1097 for path
in self
.cloneExclude
]
1100 while commit
.has_key("depotFile%s" % fnum
):
1101 path
= commit
["depotFile%s" % fnum
]
1103 if [p
for p
in self
.cloneExclude
1104 if p4PathStartsWith(path
, p
)]:
1107 found
= [p
for p
in self
.depotPaths
1108 if p4PathStartsWith(path
, p
)]
1115 file["rev"] = commit
["rev%s" % fnum
]
1116 file["action"] = commit
["action%s" % fnum
]
1117 file["type"] = commit
["type%s" % fnum
]
1122 def stripRepoPath(self
, path
, prefixes
):
1123 if self
.useClientSpec
:
1125 # if using the client spec, we use the output directory
1126 # specified in the client. For example, a view
1127 # //depot/foo/branch/... //client/branch/foo/...
1128 # will end up putting all foo/branch files into
1130 for val
in self
.clientSpecDirs
:
1131 if path
.startswith(val
[0]):
1132 # replace the depot path with the client path
1133 path
= path
.replace(val
[0], val
[1][1])
1134 # now strip out the client (//client/...)
1135 path
= re
.sub("^(//[^/]+/)", '', path
)
1136 # the rest is all path
1139 if self
.keepRepoPath
:
1140 prefixes
= [re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])]
1143 if p4PathStartsWith(path
, p
):
1144 path
= path
[len(p
):]
1148 def splitFilesIntoBranches(self
, commit
):
1151 while commit
.has_key("depotFile%s" % fnum
):
1152 path
= commit
["depotFile%s" % fnum
]
1153 found
= [p
for p
in self
.depotPaths
1154 if p4PathStartsWith(path
, p
)]
1161 file["rev"] = commit
["rev%s" % fnum
]
1162 file["action"] = commit
["action%s" % fnum
]
1163 file["type"] = commit
["type%s" % fnum
]
1166 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
1168 for branch
in self
.knownBranches
.keys():
1170 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1171 if relPath
.startswith(branch
+ "/"):
1172 if branch
not in branches
:
1173 branches
[branch
] = []
1174 branches
[branch
].append(file)
1179 # output one file from the P4 stream
1180 # - helper for streamP4Files
1182 def streamOneP4File(self
, file, contents
):
1183 if file["type"] == "apple":
1184 print "\nfile %s is a strange apple file that forks. Ignoring" % \
1188 relPath
= self
.stripRepoPath(file['depotFile'], self
.branchPrefixes
)
1189 relPath
= self
.wildcard_decode(relPath
)
1191 sys
.stderr
.write("%s\n" % relPath
)
1194 if isP4Exec(file["type"]):
1196 elif file["type"] == "symlink":
1198 # p4 print on a symlink contains "target\n", so strip it off
1199 data
= ''.join(contents
)
1200 contents
= [data
[:-1]]
1202 if self
.isWindows
and file["type"].endswith("text"):
1204 for data
in contents
:
1205 data
= data
.replace("\r\n", "\n")
1206 mangled
.append(data
)
1209 if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1210 contents
= map(lambda text
: re
.sub(r
'(?i)\$(Id|Header):[^$]*\$',r
'$\1$', text
), contents
)
1211 elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1212 contents
= map(lambda text
: re
.sub(r
'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r
'$\1$', text
), contents
)
1214 self
.gitStream
.write("M %s inline %s\n" % (mode
, relPath
))
1219 length
= length
+ len(d
)
1221 self
.gitStream
.write("data %d\n" % length
)
1223 self
.gitStream
.write(d
)
1224 self
.gitStream
.write("\n")
1226 def streamOneP4Deletion(self
, file):
1227 relPath
= self
.stripRepoPath(file['path'], self
.branchPrefixes
)
1229 sys
.stderr
.write("delete %s\n" % relPath
)
1230 self
.gitStream
.write("D %s\n" % relPath
)
1232 # handle another chunk of streaming data
1233 def streamP4FilesCb(self
, marshalled
):
1235 if marshalled
.has_key('depotFile') and self
.stream_have_file_info
:
1236 # start of a new file - output the old one first
1237 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
1238 self
.stream_file
= {}
1239 self
.stream_contents
= []
1240 self
.stream_have_file_info
= False
1242 # pick up the new file information... for the
1243 # 'data' field we need to append to our array
1244 for k
in marshalled
.keys():
1246 self
.stream_contents
.append(marshalled
['data'])
1248 self
.stream_file
[k
] = marshalled
[k
]
1250 self
.stream_have_file_info
= True
1252 # Stream directly from "p4 files" into "git fast-import"
1253 def streamP4Files(self
, files
):
1260 for val
in self
.clientSpecDirs
:
1261 if f
['path'].startswith(val
[0]):
1267 filesForCommit
.append(f
)
1268 if f
['action'] in self
.delete_actions
:
1269 filesToDelete
.append(f
)
1271 filesToRead
.append(f
)
1274 for f
in filesToDelete
:
1275 self
.streamOneP4Deletion(f
)
1277 if len(filesToRead
) > 0:
1278 self
.stream_file
= {}
1279 self
.stream_contents
= []
1280 self
.stream_have_file_info
= False
1282 # curry self argument
1283 def streamP4FilesCbSelf(entry
):
1284 self
.streamP4FilesCb(entry
)
1286 p4CmdList("-x - print",
1287 '\n'.join(['%s#%s' % (f
['path'], f
['rev'])
1288 for f
in filesToRead
]),
1289 cb
=streamP4FilesCbSelf
)
1292 if self
.stream_file
.has_key('depotFile'):
1293 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
1295 def commit(self
, details
, files
, branch
, branchPrefixes
, parent
= ""):
1296 epoch
= details
["time"]
1297 author
= details
["user"]
1298 self
.branchPrefixes
= branchPrefixes
1301 print "commit into %s" % branch
1303 # start with reading files; if that fails, we should not
1307 if [p
for p
in branchPrefixes
if p4PathStartsWith(f
['path'], p
)]:
1308 new_files
.append (f
)
1310 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % f
['path'])
1312 self
.gitStream
.write("commit %s\n" % branch
)
1313 # gitStream.write("mark :%s\n" % details["change"])
1314 self
.committedChanges
.add(int(details
["change"]))
1316 if author
not in self
.users
:
1317 self
.getUserMapFromPerforceServer()
1318 if author
in self
.users
:
1319 committer
= "%s %s %s" % (self
.users
[author
], epoch
, self
.tz
)
1321 committer
= "%s <a@b> %s %s" % (author
, epoch
, self
.tz
)
1323 self
.gitStream
.write("committer %s\n" % committer
)
1325 self
.gitStream
.write("data <<EOT\n")
1326 self
.gitStream
.write(details
["desc"])
1327 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1328 % (','.join (branchPrefixes
), details
["change"]))
1329 if len(details
['options']) > 0:
1330 self
.gitStream
.write(": options = %s" % details
['options'])
1331 self
.gitStream
.write("]\nEOT\n\n")
1335 print "parent %s" % parent
1336 self
.gitStream
.write("from %s\n" % parent
)
1338 self
.streamP4Files(new_files
)
1339 self
.gitStream
.write("\n")
1341 change
= int(details
["change"])
1343 if self
.labels
.has_key(change
):
1344 label
= self
.labels
[change
]
1345 labelDetails
= label
[0]
1346 labelRevisions
= label
[1]
1348 print "Change %s is labelled %s" % (change
, labelDetails
)
1350 files
= p4CmdList("files " + ' '.join (["%s...@%s" % (p
, change
)
1351 for p
in branchPrefixes
]))
1353 if len(files
) == len(labelRevisions
):
1357 if info
["action"] in self
.delete_actions
:
1359 cleanedFiles
[info
["depotFile"]] = info
["rev"]
1361 if cleanedFiles
== labelRevisions
:
1362 self
.gitStream
.write("tag tag_%s\n" % labelDetails
["label"])
1363 self
.gitStream
.write("from %s\n" % branch
)
1365 owner
= labelDetails
["Owner"]
1367 if author
in self
.users
:
1368 tagger
= "%s %s %s" % (self
.users
[owner
], epoch
, self
.tz
)
1370 tagger
= "%s <a@b> %s %s" % (owner
, epoch
, self
.tz
)
1371 self
.gitStream
.write("tagger %s\n" % tagger
)
1372 self
.gitStream
.write("data <<EOT\n")
1373 self
.gitStream
.write(labelDetails
["Description"])
1374 self
.gitStream
.write("EOT\n\n")
1378 print ("Tag %s does not match with change %s: files do not match."
1379 % (labelDetails
["label"], change
))
1383 print ("Tag %s does not match with change %s: file count is different."
1384 % (labelDetails
["label"], change
))
1386 def getLabels(self
):
1389 l
= p4CmdList("labels %s..." % ' '.join (self
.depotPaths
))
1390 if len(l
) > 0 and not self
.silent
:
1391 print "Finding files belonging to labels in %s" % `self
.depotPaths`
1394 label
= output
["label"]
1398 print "Querying files for label %s" % label
1399 for file in p4CmdList("files "
1400 + ' '.join (["%s...@%s" % (p
, label
)
1401 for p
in self
.depotPaths
])):
1402 revisions
[file["depotFile"]] = file["rev"]
1403 change
= int(file["change"])
1404 if change
> newestChange
:
1405 newestChange
= change
1407 self
.labels
[newestChange
] = [output
, revisions
]
1410 print "Label changes: %s" % self
.labels
.keys()
1412 def guessProjectName(self
):
1413 for p
in self
.depotPaths
:
1416 p
= p
[p
.strip().rfind("/") + 1:]
1417 if not p
.endswith("/"):
1421 def getBranchMapping(self
):
1422 lostAndFoundBranches
= set()
1424 for info
in p4CmdList("branches"):
1425 details
= p4Cmd("branch -o %s" % info
["branch"])
1427 while details
.has_key("View%s" % viewIdx
):
1428 paths
= details
["View%s" % viewIdx
].split(" ")
1429 viewIdx
= viewIdx
+ 1
1430 # require standard //depot/foo/... //depot/bar/... mapping
1431 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
1434 destination
= paths
[1]
1436 if p4PathStartsWith(source
, self
.depotPaths
[0]) and p4PathStartsWith(destination
, self
.depotPaths
[0]):
1437 source
= source
[len(self
.depotPaths
[0]):-4]
1438 destination
= destination
[len(self
.depotPaths
[0]):-4]
1440 if destination
in self
.knownBranches
:
1442 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
1443 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
1446 self
.knownBranches
[destination
] = source
1448 lostAndFoundBranches
.discard(destination
)
1450 if source
not in self
.knownBranches
:
1451 lostAndFoundBranches
.add(source
)
1454 for branch
in lostAndFoundBranches
:
1455 self
.knownBranches
[branch
] = branch
1457 def getBranchMappingFromGitBranches(self
):
1458 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1459 for branch
in branches
.keys():
1460 if branch
== "master":
1463 branch
= branch
[len(self
.projectName
):]
1464 self
.knownBranches
[branch
] = branch
1466 def listExistingP4GitBranches(self
):
1467 # branches holds mapping from name to commit
1468 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1469 self
.p4BranchesInGit
= branches
.keys()
1470 for branch
in branches
.keys():
1471 self
.initialParents
[self
.refPrefix
+ branch
] = branches
[branch
]
1473 def updateOptionDict(self
, d
):
1475 if self
.keepRepoPath
:
1476 option_keys
['keepRepoPath'] = 1
1478 d
["options"] = ' '.join(sorted(option_keys
.keys()))
1480 def readOptions(self
, d
):
1481 self
.keepRepoPath
= (d
.has_key('options')
1482 and ('keepRepoPath' in d
['options']))
1484 def gitRefForBranch(self
, branch
):
1485 if branch
== "main":
1486 return self
.refPrefix
+ "master"
1488 if len(branch
) <= 0:
1491 return self
.refPrefix
+ self
.projectName
+ branch
1493 def gitCommitByP4Change(self
, ref
, change
):
1495 print "looking in ref " + ref
+ " for change %s using bisect..." % change
1498 latestCommit
= parseRevision(ref
)
1502 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
1503 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
1508 log
= extractLogMessageFromGitCommit(next
)
1509 settings
= extractSettingsGitLog(log
)
1510 currentChange
= int(settings
['change'])
1512 print "current change %s" % currentChange
1514 if currentChange
== change
:
1516 print "found %s" % next
1519 if currentChange
< change
:
1520 earliestCommit
= "^%s" % next
1522 latestCommit
= "%s" % next
1526 def importNewBranch(self
, branch
, maxChange
):
1527 # make fast-import flush all changes to disk and update the refs using the checkpoint
1528 # command so that we can try to find the branch parent in the git history
1529 self
.gitStream
.write("checkpoint\n\n");
1530 self
.gitStream
.flush();
1531 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1532 range = "@1,%s" % maxChange
1533 #print "prefix" + branchPrefix
1534 changes
= p4ChangesForPaths([branchPrefix
], range)
1535 if len(changes
) <= 0:
1537 firstChange
= changes
[0]
1538 #print "first change in branch: %s" % firstChange
1539 sourceBranch
= self
.knownBranches
[branch
]
1540 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
1541 sourceRef
= self
.gitRefForBranch(sourceBranch
)
1542 #print "source " + sourceBranch
1544 branchParentChange
= int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath
, firstChange
))["change"])
1545 #print "branch parent: %s" % branchParentChange
1546 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
1547 if len(gitParent
) > 0:
1548 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
1549 #print "parent git commit: %s" % gitParent
1551 self
.importChanges(changes
)
1554 def importChanges(self
, changes
):
1556 for change
in changes
:
1557 description
= p4Cmd("describe %s" % change
)
1558 self
.updateOptionDict(description
)
1561 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
1566 if self
.detectBranches
:
1567 branches
= self
.splitFilesIntoBranches(description
)
1568 for branch
in branches
.keys():
1570 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1574 filesForCommit
= branches
[branch
]
1577 print "branch is %s" % branch
1579 self
.updatedBranches
.add(branch
)
1581 if branch
not in self
.createdBranches
:
1582 self
.createdBranches
.add(branch
)
1583 parent
= self
.knownBranches
[branch
]
1584 if parent
== branch
:
1587 fullBranch
= self
.projectName
+ branch
1588 if fullBranch
not in self
.p4BranchesInGit
:
1590 print("\n Importing new branch %s" % fullBranch
);
1591 if self
.importNewBranch(branch
, change
- 1):
1593 self
.p4BranchesInGit
.append(fullBranch
)
1595 print("\n Resuming with change %s" % change
);
1598 print "parent determined through known branches: %s" % parent
1600 branch
= self
.gitRefForBranch(branch
)
1601 parent
= self
.gitRefForBranch(parent
)
1604 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
1606 if len(parent
) == 0 and branch
in self
.initialParents
:
1607 parent
= self
.initialParents
[branch
]
1608 del self
.initialParents
[branch
]
1610 self
.commit(description
, filesForCommit
, branch
, [branchPrefix
], parent
)
1612 files
= self
.extractFilesFromCommit(description
)
1613 self
.commit(description
, files
, self
.branch
, self
.depotPaths
,
1615 self
.initialParent
= ""
1617 print self
.gitError
.read()
1620 def importHeadRevision(self
, revision
):
1621 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
1623 details
= { "user" : "git perforce import user", "time" : int(time
.time()) }
1624 details
["desc"] = ("Initial import of %s from the state at revision %s\n"
1625 % (' '.join(self
.depotPaths
), revision
))
1626 details
["change"] = revision
1630 for info
in p4CmdList("files "
1631 + ' '.join(["%s...%s"
1633 for p
in self
.depotPaths
])):
1635 if 'code' in info
and info
['code'] == 'error':
1636 sys
.stderr
.write("p4 returned an error: %s\n"
1638 if info
['data'].find("must refer to client") >= 0:
1639 sys
.stderr
.write("This particular p4 error is misleading.\n")
1640 sys
.stderr
.write("Perhaps the depot path was misspelled.\n");
1641 sys
.stderr
.write("Depot path: %s\n" % " ".join(self
.depotPaths
))
1643 if 'p4ExitCode' in info
:
1644 sys
.stderr
.write("p4 exitcode: %s\n" % info
['p4ExitCode'])
1648 change
= int(info
["change"])
1649 if change
> newestRevision
:
1650 newestRevision
= change
1652 if info
["action"] in self
.delete_actions
:
1653 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1654 #fileCnt = fileCnt + 1
1657 for prop
in ["depotFile", "rev", "action", "type" ]:
1658 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
1660 fileCnt
= fileCnt
+ 1
1662 details
["change"] = newestRevision
1663 self
.updateOptionDict(details
)
1665 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
, self
.depotPaths
)
1667 print "IO error with git fast-import. Is your git version recent enough?"
1668 print self
.gitError
.read()
1671 def getClientSpec(self
):
1672 specList
= p4CmdList( "client -o" )
1674 for entry
in specList
:
1675 for k
,v
in entry
.iteritems():
1676 if k
.startswith("View"):
1678 # p4 has these %%1 to %%9 arguments in specs to
1679 # reorder paths; which we can't handle (yet :)
1680 if re
.match('%%\d', v
) != None:
1681 print "Sorry, can't handle %%n arguments in client specs"
1684 if v
.startswith('"'):
1688 index
= v
.find("...")
1690 # save the "client view"; i.e the RHS of the view
1691 # line that tells the client where to put the
1692 # files for this view.
1693 cv
= v
[index
+3:].strip() # +3 to remove previous '...'
1695 # if the client view doesn't end with a
1696 # ... wildcard, then we're going to mess up the
1697 # output directory, so fail gracefully.
1698 if not cv
.endswith('...'):
1699 print 'Sorry, client view in "%s" needs to end with wildcard' % (k
)
1703 # now save the view; +index means included, -index
1704 # means it should be filtered out.
1706 if v
.startswith("-"):
1712 temp
[v
] = (include
, cv
)
1714 self
.clientSpecDirs
= temp
.items()
1715 self
.clientSpecDirs
.sort( lambda x
, y
: abs( y
[1][0] ) - abs( x
[1][0] ) )
1717 def run(self
, args
):
1718 self
.depotPaths
= []
1719 self
.changeRange
= ""
1720 self
.initialParent
= ""
1721 self
.previousDepotPaths
= []
1723 # map from branch depot path to parent branch
1724 self
.knownBranches
= {}
1725 self
.initialParents
= {}
1726 self
.hasOrigin
= originP4BranchesExist()
1727 if not self
.syncWithOrigin
:
1728 self
.hasOrigin
= False
1730 if self
.importIntoRemotes
:
1731 self
.refPrefix
= "refs/remotes/p4/"
1733 self
.refPrefix
= "refs/heads/p4/"
1735 if self
.syncWithOrigin
and self
.hasOrigin
:
1737 print "Syncing with origin first by calling git fetch origin"
1738 system("git fetch origin")
1740 if len(self
.branch
) == 0:
1741 self
.branch
= self
.refPrefix
+ "master"
1742 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
1743 system("git update-ref %s refs/heads/p4" % self
.branch
)
1744 system("git branch -D p4");
1745 # create it /after/ importing, when master exists
1746 if not gitBranchExists(self
.refPrefix
+ "HEAD") and self
.importIntoRemotes
and gitBranchExists(self
.branch
):
1747 system("git symbolic-ref %sHEAD %s" % (self
.refPrefix
, self
.branch
))
1749 if self
.useClientSpec
or gitConfig("git-p4.useclientspec") == "true":
1750 self
.getClientSpec()
1752 # TODO: should always look at previous commits,
1753 # merge with previous imports, if possible.
1756 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
1757 self
.listExistingP4GitBranches()
1759 if len(self
.p4BranchesInGit
) > 1:
1761 print "Importing from/into multiple branches"
1762 self
.detectBranches
= True
1765 print "branches: %s" % self
.p4BranchesInGit
1768 for branch
in self
.p4BranchesInGit
:
1769 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
1771 settings
= extractSettingsGitLog(logMsg
)
1773 self
.readOptions(settings
)
1774 if (settings
.has_key('depot-paths')
1775 and settings
.has_key ('change')):
1776 change
= int(settings
['change']) + 1
1777 p4Change
= max(p4Change
, change
)
1779 depotPaths
= sorted(settings
['depot-paths'])
1780 if self
.previousDepotPaths
== []:
1781 self
.previousDepotPaths
= depotPaths
1784 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
1785 for i
in range(0, min(len(cur
), len(prev
))):
1786 if cur
[i
] <> prev
[i
]:
1790 paths
.append (cur
[:i
+ 1])
1792 self
.previousDepotPaths
= paths
1795 self
.depotPaths
= sorted(self
.previousDepotPaths
)
1796 self
.changeRange
= "@%s,#head" % p4Change
1797 if not self
.detectBranches
:
1798 self
.initialParent
= parseRevision(self
.branch
)
1799 if not self
.silent
and not self
.detectBranches
:
1800 print "Performing incremental import into %s git branch" % self
.branch
1802 if not self
.branch
.startswith("refs/"):
1803 self
.branch
= "refs/heads/" + self
.branch
1805 if len(args
) == 0 and self
.depotPaths
:
1807 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
1809 if self
.depotPaths
and self
.depotPaths
!= args
:
1810 print ("previous import used depot path %s and now %s was specified. "
1811 "This doesn't work!" % (' '.join (self
.depotPaths
),
1815 self
.depotPaths
= sorted(args
)
1821 for p
in self
.depotPaths
:
1822 if p
.find("@") != -1:
1823 atIdx
= p
.index("@")
1824 self
.changeRange
= p
[atIdx
:]
1825 if self
.changeRange
== "@all":
1826 self
.changeRange
= ""
1827 elif ',' not in self
.changeRange
:
1828 revision
= self
.changeRange
1829 self
.changeRange
= ""
1831 elif p
.find("#") != -1:
1832 hashIdx
= p
.index("#")
1833 revision
= p
[hashIdx
:]
1835 elif self
.previousDepotPaths
== []:
1838 p
= re
.sub ("\.\.\.$", "", p
)
1839 if not p
.endswith("/"):
1844 self
.depotPaths
= newPaths
1847 self
.loadUserMapFromCache()
1849 if self
.detectLabels
:
1852 if self
.detectBranches
:
1853 ## FIXME - what's a P4 projectName ?
1854 self
.projectName
= self
.guessProjectName()
1857 self
.getBranchMappingFromGitBranches()
1859 self
.getBranchMapping()
1861 print "p4-git branches: %s" % self
.p4BranchesInGit
1862 print "initial parents: %s" % self
.initialParents
1863 for b
in self
.p4BranchesInGit
:
1867 b
= b
[len(self
.projectName
):]
1868 self
.createdBranches
.add(b
)
1870 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
1872 importProcess
= subprocess
.Popen(["git", "fast-import"],
1873 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
1874 stderr
=subprocess
.PIPE
);
1875 self
.gitOutput
= importProcess
.stdout
1876 self
.gitStream
= importProcess
.stdin
1877 self
.gitError
= importProcess
.stderr
1880 self
.importHeadRevision(revision
)
1884 if len(self
.changesFile
) > 0:
1885 output
= open(self
.changesFile
).readlines()
1888 changeSet
.add(int(line
))
1890 for change
in changeSet
:
1891 changes
.append(change
)
1895 # catch "git-p4 sync" with no new branches, in a repo that
1896 # does not have any existing git-p4 branches
1897 if len(args
) == 0 and not self
.p4BranchesInGit
:
1898 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
1900 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
1902 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
)
1904 if len(self
.maxChanges
) > 0:
1905 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
1907 if len(changes
) == 0:
1909 print "No changes to import!"
1912 if not self
.silent
and not self
.detectBranches
:
1913 print "Import destination: %s" % self
.branch
1915 self
.updatedBranches
= set()
1917 self
.importChanges(changes
)
1921 if len(self
.updatedBranches
) > 0:
1922 sys
.stdout
.write("Updated branches: ")
1923 for b
in self
.updatedBranches
:
1924 sys
.stdout
.write("%s " % b
)
1925 sys
.stdout
.write("\n")
1927 self
.gitStream
.close()
1928 if importProcess
.wait() != 0:
1929 die("fast-import failed: %s" % self
.gitError
.read())
1930 self
.gitOutput
.close()
1931 self
.gitError
.close()
1935 class P4Rebase(Command
):
1937 Command
.__init
__(self
)
1939 self
.description
= ("Fetches the latest revision from perforce and "
1940 + "rebases the current work (branch) against it")
1941 self
.verbose
= False
1943 def run(self
, args
):
1947 return self
.rebase()
1950 if os
.system("git update-index --refresh") != 0:
1951 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.");
1952 if len(read_pipe("git diff-index HEAD --")) > 0:
1953 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1955 [upstream
, settings
] = findUpstreamBranchPoint()
1956 if len(upstream
) == 0:
1957 die("Cannot find upstream branchpoint for rebase")
1959 # the branchpoint may be p4/foo~3, so strip off the parent
1960 upstream
= re
.sub("~[0-9]+$", "", upstream
)
1962 print "Rebasing the current branch onto %s" % upstream
1963 oldHead
= read_pipe("git rev-parse HEAD").strip()
1964 system("git rebase %s" % upstream
)
1965 system("git diff-tree --stat --summary -M %s HEAD" % oldHead
)
1968 class P4Clone(P4Sync
):
1970 P4Sync
.__init
__(self
)
1971 self
.description
= "Creates a new git repository and imports from Perforce into it"
1972 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
1974 optparse
.make_option("--destination", dest
="cloneDestination",
1975 action
='store', default
=None,
1976 help="where to leave result of the clone"),
1977 optparse
.make_option("-/", dest
="cloneExclude",
1978 action
="append", type="string",
1979 help="exclude depot path"),
1980 optparse
.make_option("--bare", dest
="cloneBare",
1981 action
="store_true", default
=False),
1983 self
.cloneDestination
= None
1984 self
.needsGit
= False
1985 self
.cloneBare
= False
1987 # This is required for the "append" cloneExclude action
1988 def ensure_value(self
, attr
, value
):
1989 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
1990 setattr(self
, attr
, value
)
1991 return getattr(self
, attr
)
1993 def defaultDestination(self
, args
):
1994 ## TODO: use common prefix of args?
1996 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
1997 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
1998 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
1999 depotDir
= re
.sub(r
"/$", "", depotDir
)
2000 return os
.path
.split(depotDir
)[1]
2002 def run(self
, args
):
2006 if self
.keepRepoPath
and not self
.cloneDestination
:
2007 sys
.stderr
.write("Must specify destination for --keep-path\n")
2012 if not self
.cloneDestination
and len(depotPaths
) > 1:
2013 self
.cloneDestination
= depotPaths
[-1]
2014 depotPaths
= depotPaths
[:-1]
2016 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
2017 for p
in depotPaths
:
2018 if not p
.startswith("//"):
2021 if not self
.cloneDestination
:
2022 self
.cloneDestination
= self
.defaultDestination(args
)
2024 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
2026 if not os
.path
.exists(self
.cloneDestination
):
2027 os
.makedirs(self
.cloneDestination
)
2028 chdir(self
.cloneDestination
)
2030 init_cmd
= [ "git", "init" ]
2032 init_cmd
.append("--bare")
2033 subprocess
.check_call(init_cmd
)
2035 if not P4Sync
.run(self
, depotPaths
):
2037 if self
.branch
!= "master":
2038 if self
.importIntoRemotes
:
2039 masterbranch
= "refs/remotes/p4/master"
2041 masterbranch
= "refs/heads/p4/master"
2042 if gitBranchExists(masterbranch
):
2043 system("git branch master %s" % masterbranch
)
2044 if not self
.cloneBare
:
2045 system("git checkout -f")
2047 print "Could not detect main branch. No checkout/master branch created."
2051 class P4Branches(Command
):
2053 Command
.__init
__(self
)
2055 self
.description
= ("Shows the git branches that hold imports and their "
2056 + "corresponding perforce depot paths")
2057 self
.verbose
= False
2059 def run(self
, args
):
2060 if originP4BranchesExist():
2061 createOrUpdateBranchesFromOrigin()
2063 cmdline
= "git rev-parse --symbolic "
2064 cmdline
+= " --remotes"
2066 for line
in read_pipe_lines(cmdline
):
2069 if not line
.startswith('p4/') or line
== "p4/HEAD":
2073 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
2074 settings
= extractSettingsGitLog(log
)
2076 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
2079 class HelpFormatter(optparse
.IndentedHelpFormatter
):
2081 optparse
.IndentedHelpFormatter
.__init
__(self
)
2083 def format_description(self
, description
):
2085 return description
+ "\n"
2089 def printUsage(commands
):
2090 print "usage: %s <command> [options]" % sys
.argv
[0]
2092 print "valid commands: %s" % ", ".join(commands
)
2094 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
2099 "submit" : P4Submit
,
2100 "commit" : P4Submit
,
2102 "rebase" : P4Rebase
,
2104 "rollback" : P4RollBack
,
2105 "branches" : P4Branches
2110 if len(sys
.argv
[1:]) == 0:
2111 printUsage(commands
.keys())
2115 cmdName
= sys
.argv
[1]
2117 klass
= commands
[cmdName
]
2120 print "unknown command %s" % cmdName
2122 printUsage(commands
.keys())
2125 options
= cmd
.options
2126 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
2130 if len(options
) > 0:
2131 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
2133 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
2135 description
= cmd
.description
,
2136 formatter
= HelpFormatter())
2138 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
2140 verbose
= cmd
.verbose
2142 if cmd
.gitdir
== None:
2143 cmd
.gitdir
= os
.path
.abspath(".git")
2144 if not isValidGitDir(cmd
.gitdir
):
2145 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
2146 if os
.path
.exists(cmd
.gitdir
):
2147 cdup
= read_pipe("git rev-parse --show-cdup").strip()
2151 if not isValidGitDir(cmd
.gitdir
):
2152 if isValidGitDir(cmd
.gitdir
+ "/.git"):
2153 cmd
.gitdir
+= "/.git"
2155 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
2157 os
.environ
["GIT_DIR"] = cmd
.gitdir
2159 if not cmd
.run(args
):
2163 if __name__
== '__main__':