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.
27 user
= gitConfig("git-p4.user")
29 real_cmd
+= ["-u",user
]
31 password
= gitConfig("git-p4.password")
33 real_cmd
+= ["-P", password
]
35 port
= gitConfig("git-p4.port")
37 real_cmd
+= ["-p", port
]
39 host
= gitConfig("git-p4.host")
41 real_cmd
+= ["-h", host
]
43 client
= gitConfig("git-p4.client")
45 real_cmd
+= ["-c", client
]
48 if isinstance(cmd
,basestring
):
49 real_cmd
= ' '.join(real_cmd
) + ' ' + cmd
55 # P4 uses the PWD environment variable rather than getcwd(). Since we're
56 # not using the shell, we have to set it ourselves.
64 sys
.stderr
.write(msg
+ "\n")
67 def write_pipe(c
, stdin
):
69 sys
.stderr
.write('Writing pipe: %s\n' % str(c
))
71 expand
= isinstance(c
,basestring
)
72 p
= subprocess
.Popen(c
, stdin
=subprocess
.PIPE
, shell
=expand
)
74 val
= pipe
.write(stdin
)
77 die('Command failed: %s' % str(c
))
81 def p4_write_pipe(c
, stdin
):
82 real_cmd
= p4_build_cmd(c
)
83 return write_pipe(real_cmd
, stdin
)
85 def read_pipe(c
, ignore_error
=False):
87 sys
.stderr
.write('Reading pipe: %s\n' % str(c
))
89 expand
= isinstance(c
,basestring
)
90 p
= subprocess
.Popen(c
, stdout
=subprocess
.PIPE
, shell
=expand
)
93 if p
.wait() and not ignore_error
:
94 die('Command failed: %s' % str(c
))
98 def p4_read_pipe(c
, ignore_error
=False):
99 real_cmd
= p4_build_cmd(c
)
100 return read_pipe(real_cmd
, ignore_error
)
102 def read_pipe_lines(c
):
104 sys
.stderr
.write('Reading pipe: %s\n' % str(c
))
106 expand
= isinstance(c
, basestring
)
107 p
= subprocess
.Popen(c
, stdout
=subprocess
.PIPE
, shell
=expand
)
109 val
= pipe
.readlines()
110 if pipe
.close() or p
.wait():
111 die('Command failed: %s' % str(c
))
115 def p4_read_pipe_lines(c
):
116 """Specifically invoke p4 on the command supplied. """
117 real_cmd
= p4_build_cmd(c
)
118 return read_pipe_lines(real_cmd
)
121 expand
= isinstance(cmd
,basestring
)
123 sys
.stderr
.write("executing %s\n" % str(cmd
))
124 subprocess
.check_call(cmd
, shell
=expand
)
127 """Specifically invoke p4 as the system command. """
128 real_cmd
= p4_build_cmd(cmd
)
129 expand
= isinstance(real_cmd
, basestring
)
130 subprocess
.check_call(real_cmd
, shell
=expand
)
132 def p4_integrate(src
, dest
):
133 p4_system(["integrate", "-Dt", src
, dest
])
136 p4_system(["sync", path
])
139 p4_system(["add", f
])
142 p4_system(["delete", f
])
145 p4_system(["edit", f
])
148 p4_system(["revert", f
])
150 def p4_reopen(type, file):
151 p4_system(["reopen", "-t", type, file])
154 # Canonicalize the p4 type and return a tuple of the
155 # base type, plus any modifiers. See "p4 help filetypes"
156 # for a list and explanation.
158 def split_p4_type(p4type
):
160 p4_filetypes_historical
= {
161 "ctempobj": "binary+Sw",
167 "tempobj": "binary+FSw",
168 "ubinary": "binary+F",
169 "uresource": "resource+F",
170 "uxbinary": "binary+Fx",
171 "xbinary": "binary+x",
173 "xtempobj": "binary+Swx",
175 "xunicode": "unicode+x",
178 if p4type
in p4_filetypes_historical
:
179 p4type
= p4_filetypes_historical
[p4type
]
181 s
= p4type
.split("+")
189 def setP4ExecBit(file, mode
):
190 # Reopens an already open file and changes the execute bit to match
191 # the execute bit setting in the passed in mode.
195 if not isModeExec(mode
):
196 p4Type
= getP4OpenedType(file)
197 p4Type
= re
.sub('^([cku]?)x(.*)', '\\1\\2', p4Type
)
198 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
199 if p4Type
[-1] == "+":
200 p4Type
= p4Type
[0:-1]
202 p4_reopen(p4Type
, file)
204 def getP4OpenedType(file):
205 # Returns the perforce file type for the given file.
207 result
= p4_read_pipe(["opened", file])
208 match
= re
.match(".*\((.+)\)\r?$", result
)
210 return match
.group(1)
212 die("Could not determine file type for %s (result: '%s')" % (file, result
))
214 def diffTreePattern():
215 # This is a simple generator for the diff tree regex pattern. This could be
216 # a class variable if this and parseDiffTreeEntry were a part of a class.
217 pattern
= re
.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
221 def parseDiffTreeEntry(entry
):
222 """Parses a single diff tree entry into its component elements.
224 See git-diff-tree(1) manpage for details about the format of the diff
225 output. This method returns a dictionary with the following elements:
227 src_mode - The mode of the source file
228 dst_mode - The mode of the destination file
229 src_sha1 - The sha1 for the source file
230 dst_sha1 - The sha1 fr the destination file
231 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
232 status_score - The score for the status (applicable for 'C' and 'R'
233 statuses). This is None if there is no score.
234 src - The path for the source file.
235 dst - The path for the destination file. This is only present for
236 copy or renames. If it is not present, this is None.
238 If the pattern is not matched, None is returned."""
240 match
= diffTreePattern().next().match(entry
)
243 'src_mode': match
.group(1),
244 'dst_mode': match
.group(2),
245 'src_sha1': match
.group(3),
246 'dst_sha1': match
.group(4),
247 'status': match
.group(5),
248 'status_score': match
.group(6),
249 'src': match
.group(7),
250 'dst': match
.group(10)
254 def isModeExec(mode
):
255 # Returns True if the given git mode represents an executable file,
257 return mode
[-3:] == "755"
259 def isModeExecChanged(src_mode
, dst_mode
):
260 return isModeExec(src_mode
) != isModeExec(dst_mode
)
262 def p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b', cb
=None):
264 if isinstance(cmd
,basestring
):
271 cmd
= p4_build_cmd(cmd
)
273 sys
.stderr
.write("Opening pipe: %s\n" % str(cmd
))
275 # Use a temporary file to avoid deadlocks without
276 # subprocess.communicate(), which would put another copy
277 # of stdout into memory.
279 if stdin
is not None:
280 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
281 if isinstance(stdin
,basestring
):
282 stdin_file
.write(stdin
)
285 stdin_file
.write(i
+ '\n')
289 p4
= subprocess
.Popen(cmd
,
292 stdout
=subprocess
.PIPE
)
297 entry
= marshal
.load(p4
.stdout
)
307 entry
["p4ExitCode"] = exitCode
313 list = p4CmdList(cmd
)
319 def p4Where(depotPath
):
320 if not depotPath
.endswith("/"):
322 depotPath
= depotPath
+ "..."
323 outputList
= p4CmdList(["where", depotPath
])
325 for entry
in outputList
:
326 if "depotFile" in entry
:
327 if entry
["depotFile"] == depotPath
:
330 elif "data" in entry
:
331 data
= entry
.get("data")
332 space
= data
.find(" ")
333 if data
[:space
] == depotPath
:
338 if output
["code"] == "error":
342 clientPath
= output
.get("path")
343 elif "data" in output
:
344 data
= output
.get("data")
345 lastSpace
= data
.rfind(" ")
346 clientPath
= data
[lastSpace
+ 1:]
348 if clientPath
.endswith("..."):
349 clientPath
= clientPath
[:-3]
352 def currentGitBranch():
353 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
355 def isValidGitDir(path
):
356 if (os
.path
.exists(path
+ "/HEAD")
357 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
361 def parseRevision(ref
):
362 return read_pipe("git rev-parse %s" % ref
).strip()
364 def extractLogMessageFromGitCommit(commit
):
367 ## fixme: title is first line of commit, not 1st paragraph.
369 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
378 def extractSettingsGitLog(log
):
380 for line
in log
.split("\n"):
382 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
386 assignments
= m
.group(1).split (':')
387 for a
in assignments
:
389 key
= vals
[0].strip()
390 val
= ('='.join (vals
[1:])).strip()
391 if val
.endswith ('\"') and val
.startswith('"'):
396 paths
= values
.get("depot-paths")
398 paths
= values
.get("depot-path")
400 values
['depot-paths'] = paths
.split(',')
403 def gitBranchExists(branch
):
404 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
405 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
406 return proc
.wait() == 0;
409 def gitConfig(key
, args
= None): # set args to "--bool", for instance
410 if not _gitConfig
.has_key(key
):
413 argsFilter
= "%s " % args
414 cmd
= "git config %s%s" % (argsFilter
, key
)
415 _gitConfig
[key
] = read_pipe(cmd
, ignore_error
=True).strip()
416 return _gitConfig
[key
]
418 def gitConfigList(key
):
419 if not _gitConfig
.has_key(key
):
420 _gitConfig
[key
] = read_pipe("git config --get-all %s" % key
, ignore_error
=True).strip().split(os
.linesep
)
421 return _gitConfig
[key
]
423 def p4BranchesInGit(branchesAreInRemotes
= True):
426 cmdline
= "git rev-parse --symbolic "
427 if branchesAreInRemotes
:
428 cmdline
+= " --remotes"
430 cmdline
+= " --branches"
432 for line
in read_pipe_lines(cmdline
):
435 ## only import to p4/
436 if not line
.startswith('p4/') or line
== "p4/HEAD":
441 branch
= re
.sub ("^p4/", "", line
)
443 branches
[branch
] = parseRevision(line
)
446 def findUpstreamBranchPoint(head
= "HEAD"):
447 branches
= p4BranchesInGit()
448 # map from depot-path to branch name
449 branchByDepotPath
= {}
450 for branch
in branches
.keys():
451 tip
= branches
[branch
]
452 log
= extractLogMessageFromGitCommit(tip
)
453 settings
= extractSettingsGitLog(log
)
454 if settings
.has_key("depot-paths"):
455 paths
= ",".join(settings
["depot-paths"])
456 branchByDepotPath
[paths
] = "remotes/p4/" + branch
460 while parent
< 65535:
461 commit
= head
+ "~%s" % parent
462 log
= extractLogMessageFromGitCommit(commit
)
463 settings
= extractSettingsGitLog(log
)
464 if settings
.has_key("depot-paths"):
465 paths
= ",".join(settings
["depot-paths"])
466 if branchByDepotPath
.has_key(paths
):
467 return [branchByDepotPath
[paths
], settings
]
471 return ["", settings
]
473 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
475 print ("Creating/updating branch(es) in %s based on origin branch(es)"
478 originPrefix
= "origin/p4/"
480 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
482 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
485 headName
= line
[len(originPrefix
):]
486 remoteHead
= localRefPrefix
+ headName
489 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
490 if (not original
.has_key('depot-paths')
491 or not original
.has_key('change')):
495 if not gitBranchExists(remoteHead
):
497 print "creating %s" % remoteHead
500 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
501 if settings
.has_key('change') > 0:
502 if settings
['depot-paths'] == original
['depot-paths']:
503 originP4Change
= int(original
['change'])
504 p4Change
= int(settings
['change'])
505 if originP4Change
> p4Change
:
506 print ("%s (%s) is newer than %s (%s). "
507 "Updating p4 branch from origin."
508 % (originHead
, originP4Change
,
509 remoteHead
, p4Change
))
512 print ("Ignoring: %s was imported from %s while "
513 "%s was imported from %s"
514 % (originHead
, ','.join(original
['depot-paths']),
515 remoteHead
, ','.join(settings
['depot-paths'])))
518 system("git update-ref %s %s" % (remoteHead
, originHead
))
520 def originP4BranchesExist():
521 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
523 def p4ChangesForPaths(depotPaths
, changeRange
):
527 cmd
+= ["%s...%s" % (p
, changeRange
)]
528 output
= p4_read_pipe_lines(cmd
)
532 changeNum
= int(line
.split(" ")[1])
533 changes
[changeNum
] = True
535 changelist
= changes
.keys()
539 def p4PathStartsWith(path
, prefix
):
540 # This method tries to remedy a potential mixed-case issue:
542 # If UserA adds //depot/DirA/file1
543 # and UserB adds //depot/dira/file2
545 # we may or may not have a problem. If you have core.ignorecase=true,
546 # we treat DirA and dira as the same directory
547 ignorecase
= gitConfig("core.ignorecase", "--bool") == "true"
549 return path
.lower().startswith(prefix
.lower())
550 return path
.startswith(prefix
)
554 self
.usage
= "usage: %prog [options]"
559 self
.userMapFromPerforceServer
= False
561 def getUserCacheFilename(self
):
562 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
563 return home
+ "/.gitp4-usercache.txt"
565 def getUserMapFromPerforceServer(self
):
566 if self
.userMapFromPerforceServer
:
571 for output
in p4CmdList("users"):
572 if not output
.has_key("User"):
574 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
575 self
.emails
[output
["Email"]] = output
["User"]
579 for (key
, val
) in self
.users
.items():
580 s
+= "%s\t%s\n" % (key
.expandtabs(1), val
.expandtabs(1))
582 open(self
.getUserCacheFilename(), "wb").write(s
)
583 self
.userMapFromPerforceServer
= True
585 def loadUserMapFromCache(self
):
587 self
.userMapFromPerforceServer
= False
589 cache
= open(self
.getUserCacheFilename(), "rb")
590 lines
= cache
.readlines()
593 entry
= line
.strip().split("\t")
594 self
.users
[entry
[0]] = entry
[1]
596 self
.getUserMapFromPerforceServer()
598 class P4Debug(Command
):
600 Command
.__init
__(self
)
602 optparse
.make_option("--verbose", dest
="verbose", action
="store_true",
605 self
.description
= "A tool to debug the output of p4 -G."
606 self
.needsGit
= False
611 for output
in p4CmdList(args
):
612 print 'Element: %d' % j
617 class P4RollBack(Command
):
619 Command
.__init
__(self
)
621 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
622 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
624 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
626 self
.rollbackLocalBranches
= False
631 maxChange
= int(args
[0])
633 if "p4ExitCode" in p4Cmd("changes -m 1"):
634 die("Problems executing p4");
636 if self
.rollbackLocalBranches
:
637 refPrefix
= "refs/heads/"
638 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
640 refPrefix
= "refs/remotes/"
641 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
644 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
646 ref
= refPrefix
+ line
647 log
= extractLogMessageFromGitCommit(ref
)
648 settings
= extractSettingsGitLog(log
)
650 depotPaths
= settings
['depot-paths']
651 change
= settings
['change']
655 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
656 for p
in depotPaths
]))) == 0:
657 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
658 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
661 while change
and int(change
) > maxChange
:
664 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
665 system("git update-ref %s \"%s^\"" % (ref
, ref
))
666 log
= extractLogMessageFromGitCommit(ref
)
667 settings
= extractSettingsGitLog(log
)
670 depotPaths
= settings
['depot-paths']
671 change
= settings
['change']
674 print "%s rewound to %s" % (ref
, change
)
678 class P4Submit(Command
, P4UserMap
):
680 Command
.__init
__(self
)
681 P4UserMap
.__init
__(self
)
683 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
684 optparse
.make_option("--origin", dest
="origin"),
685 optparse
.make_option("-M", dest
="detectRenames", action
="store_true"),
686 # preserve the user, requires relevant p4 permissions
687 optparse
.make_option("--preserve-user", dest
="preserveUser", action
="store_true"),
689 self
.description
= "Submit changes from git to the perforce depot."
690 self
.usage
+= " [name of git branch to submit into perforce depot]"
691 self
.interactive
= True
693 self
.detectRenames
= False
695 self
.preserveUser
= gitConfig("git-p4.preserveUser").lower() == "true"
696 self
.isWindows
= (platform
.system() == "Windows")
697 self
.myP4UserId
= None
700 if len(p4CmdList("opened ...")) > 0:
701 die("You have files opened with perforce! Close them before starting the sync.")
703 # replaces everything between 'Description:' and the next P4 submit template field with the
705 def prepareLogMessage(self
, template
, message
):
708 inDescriptionSection
= False
710 for line
in template
.split("\n"):
711 if line
.startswith("#"):
712 result
+= line
+ "\n"
715 if inDescriptionSection
:
716 if line
.startswith("Files:") or line
.startswith("Jobs:"):
717 inDescriptionSection
= False
721 if line
.startswith("Description:"):
722 inDescriptionSection
= True
724 for messageLine
in message
.split("\n"):
725 line
+= "\t" + messageLine
+ "\n"
727 result
+= line
+ "\n"
731 def p4UserForCommit(self
,id):
732 # Return the tuple (perforce user,git email) for a given git commit id
733 self
.getUserMapFromPerforceServer()
734 gitEmail
= read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
735 gitEmail
= gitEmail
.strip()
736 if not self
.emails
.has_key(gitEmail
):
737 return (None,gitEmail
)
739 return (self
.emails
[gitEmail
],gitEmail
)
741 def checkValidP4Users(self
,commits
):
742 # check if any git authors cannot be mapped to p4 users
744 (user
,email
) = self
.p4UserForCommit(id)
746 msg
= "Cannot find p4 user for email %s in commit %s." % (email
, id)
747 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
750 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg
)
752 def lastP4Changelist(self
):
753 # Get back the last changelist number submitted in this client spec. This
754 # then gets used to patch up the username in the change. If the same
755 # client spec is being used by multiple processes then this might go
757 results
= p4CmdList("client -o") # find the current client
760 if r
.has_key('Client'):
764 die("could not get client spec")
765 results
= p4CmdList(["changes", "-c", client
, "-m", "1"])
767 if r
.has_key('change'):
769 die("Could not get changelist number for last submit - cannot patch up user details")
771 def modifyChangelistUser(self
, changelist
, newUser
):
772 # fixup the user field of a changelist after it has been submitted.
773 changes
= p4CmdList("change -o %s" % changelist
)
774 if len(changes
) != 1:
775 die("Bad output from p4 change modifying %s to user %s" %
776 (changelist
, newUser
))
779 if c
['User'] == newUser
: return # nothing to do
781 input = marshal
.dumps(c
)
783 result
= p4CmdList("change -f -i", stdin
=input)
785 if r
.has_key('code'):
786 if r
['code'] == 'error':
787 die("Could not modify user field of changelist %s to %s:%s" % (changelist
, newUser
, r
['data']))
788 if r
.has_key('data'):
789 print("Updated user field for changelist %s to %s" % (changelist
, newUser
))
791 die("Could not modify user field of changelist %s to %s" % (changelist
, newUser
))
793 def canChangeChangelists(self
):
794 # check to see if we have p4 admin or super-user permissions, either of
795 # which are required to modify changelists.
796 results
= p4CmdList("protects %s" % self
.depotPath
)
798 if r
.has_key('perm'):
799 if r
['perm'] == 'admin':
801 if r
['perm'] == 'super':
807 return self
.myP4UserId
809 results
= p4CmdList("user -o")
811 if r
.has_key('User'):
812 self
.myP4UserId
= r
['User']
814 die("Could not find your p4 user id")
816 def p4UserIsMe(self
, p4User
):
817 # return True if the given p4 user is actually me
819 if not p4User
or p4User
!= me
:
824 def prepareSubmitTemplate(self
):
825 # remove lines in the Files section that show changes to files outside the depot path we're committing into
827 inFilesSection
= False
828 for line
in p4_read_pipe_lines(['change', '-o']):
829 if line
.endswith("\r\n"):
830 line
= line
[:-2] + "\n"
832 if line
.startswith("\t"):
833 # path starts and ends with a tab
835 lastTab
= path
.rfind("\t")
837 path
= path
[:lastTab
]
838 if not p4PathStartsWith(path
, self
.depotPath
):
841 inFilesSection
= False
843 if line
.startswith("Files:"):
844 inFilesSection
= True
850 def applyCommit(self
, id):
851 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
853 (p4User
, gitEmail
) = self
.p4UserForCommit(id)
855 if not self
.detectRenames
:
856 # If not explicitly set check the config variable
857 self
.detectRenames
= gitConfig("git-p4.detectRenames")
859 if self
.detectRenames
.lower() == "false" or self
.detectRenames
== "":
861 elif self
.detectRenames
.lower() == "true":
864 diffOpts
= "-M%s" % self
.detectRenames
866 detectCopies
= gitConfig("git-p4.detectCopies")
867 if detectCopies
.lower() == "true":
869 elif detectCopies
!= "" and detectCopies
.lower() != "false":
870 diffOpts
+= " -C%s" % detectCopies
872 if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
873 diffOpts
+= " --find-copies-harder"
875 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts
, id, id))
877 filesToDelete
= set()
879 filesToChangeExecBit
= {}
881 diff
= parseDiffTreeEntry(line
)
882 modifier
= diff
['status']
886 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
887 filesToChangeExecBit
[path
] = diff
['dst_mode']
888 editedFiles
.add(path
)
889 elif modifier
== "A":
891 filesToChangeExecBit
[path
] = diff
['dst_mode']
892 if path
in filesToDelete
:
893 filesToDelete
.remove(path
)
894 elif modifier
== "D":
895 filesToDelete
.add(path
)
896 if path
in filesToAdd
:
897 filesToAdd
.remove(path
)
898 elif modifier
== "C":
899 src
, dest
= diff
['src'], diff
['dst']
900 p4_integrate(src
, dest
)
901 if diff
['src_sha1'] != diff
['dst_sha1']:
903 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
905 filesToChangeExecBit
[dest
] = diff
['dst_mode']
907 editedFiles
.add(dest
)
908 elif modifier
== "R":
909 src
, dest
= diff
['src'], diff
['dst']
910 p4_integrate(src
, dest
)
911 if diff
['src_sha1'] != diff
['dst_sha1']:
913 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
915 filesToChangeExecBit
[dest
] = diff
['dst_mode']
917 editedFiles
.add(dest
)
918 filesToDelete
.add(src
)
920 die("unknown modifier %s for %s" % (modifier
, path
))
922 diffcmd
= "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
923 patchcmd
= diffcmd
+ " | git apply "
924 tryPatchCmd
= patchcmd
+ "--check -"
925 applyPatchCmd
= patchcmd
+ "--check --apply -"
927 if os
.system(tryPatchCmd
) != 0:
928 print "Unfortunately applying the change failed!"
929 print "What do you want to do?"
931 while response
!= "s" and response
!= "a" and response
!= "w":
932 response
= raw_input("[s]kip this patch / [a]pply the patch forcibly "
933 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
935 print "Skipping! Good luck with the next patches..."
936 for f
in editedFiles
:
941 elif response
== "a":
942 os
.system(applyPatchCmd
)
943 if len(filesToAdd
) > 0:
944 print "You may also want to call p4 add on the following files:"
945 print " ".join(filesToAdd
)
946 if len(filesToDelete
):
947 print "The following files should be scheduled for deletion with p4 delete:"
948 print " ".join(filesToDelete
)
949 die("Please resolve and submit the conflict manually and "
950 + "continue afterwards with git-p4 submit --continue")
951 elif response
== "w":
952 system(diffcmd
+ " > patch.txt")
953 print "Patch saved to patch.txt in %s !" % self
.clientPath
954 die("Please resolve and submit the conflict manually and "
955 "continue afterwards with git-p4 submit --continue")
957 system(applyPatchCmd
)
961 for f
in filesToDelete
:
965 # Set/clear executable bits
966 for f
in filesToChangeExecBit
.keys():
967 mode
= filesToChangeExecBit
[f
]
968 setP4ExecBit(f
, mode
)
970 logMessage
= extractLogMessageFromGitCommit(id)
971 logMessage
= logMessage
.strip()
973 template
= self
.prepareSubmitTemplate()
976 submitTemplate
= self
.prepareLogMessage(template
, logMessage
)
978 if self
.preserveUser
:
979 submitTemplate
= submitTemplate
+ ("\n######## Actual user %s, modified after commit\n" % p4User
)
981 if os
.environ
.has_key("P4DIFF"):
982 del(os
.environ
["P4DIFF"])
984 for editedFile
in editedFiles
:
985 diff
+= p4_read_pipe(['diff', '-du', editedFile
])
988 for newFile
in filesToAdd
:
989 newdiff
+= "==== new file ====\n"
990 newdiff
+= "--- /dev/null\n"
991 newdiff
+= "+++ %s\n" % newFile
992 f
= open(newFile
, "r")
993 for line
in f
.readlines():
994 newdiff
+= "+" + line
997 if self
.checkAuthorship
and not self
.p4UserIsMe(p4User
):
998 submitTemplate
+= "######## git author %s does not match your p4 account.\n" % gitEmail
999 submitTemplate
+= "######## Use git-p4 option --preserve-user to modify authorship\n"
1000 submitTemplate
+= "######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n"
1002 separatorLine
= "######## everything below this line is just the diff #######\n"
1004 [handle
, fileName
] = tempfile
.mkstemp()
1005 tmpFile
= os
.fdopen(handle
, "w+")
1007 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
1008 separatorLine
= separatorLine
.replace("\n", "\r\n")
1009 newdiff
= newdiff
.replace("\n", "\r\n")
1010 tmpFile
.write(submitTemplate
+ separatorLine
+ diff
+ newdiff
)
1012 mtime
= os
.stat(fileName
).st_mtime
1013 if os
.environ
.has_key("P4EDITOR"):
1014 editor
= os
.environ
.get("P4EDITOR")
1016 editor
= read_pipe("git var GIT_EDITOR").strip()
1017 system(editor
+ " " + fileName
)
1019 if gitConfig("git-p4.skipSubmitEditCheck") == "true":
1020 checkModTime
= False
1025 if checkModTime
and (os
.stat(fileName
).st_mtime
<= mtime
):
1027 while response
!= "y" and response
!= "n":
1028 response
= raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1031 tmpFile
= open(fileName
, "rb")
1032 message
= tmpFile
.read()
1034 submitTemplate
= message
[:message
.index(separatorLine
)]
1036 submitTemplate
= submitTemplate
.replace("\r\n", "\n")
1037 p4_write_pipe(['submit', '-i'], submitTemplate
)
1039 if self
.preserveUser
:
1041 # Get last changelist number. Cannot easily get it from
1042 # the submit command output as the output is unmarshalled.
1043 changelist
= self
.lastP4Changelist()
1044 self
.modifyChangelistUser(changelist
, p4User
)
1047 for f
in editedFiles
:
1049 for f
in filesToAdd
:
1055 fileName
= "submit.txt"
1056 file = open(fileName
, "w+")
1057 file.write(self
.prepareLogMessage(template
, logMessage
))
1059 print ("Perforce submit template written as %s. "
1060 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
1061 % (fileName
, fileName
))
1063 def run(self
, args
):
1065 self
.master
= currentGitBranch()
1066 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
1067 die("Detecting current git branch failed!")
1068 elif len(args
) == 1:
1069 self
.master
= args
[0]
1073 allowSubmit
= gitConfig("git-p4.allowSubmit")
1074 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
1075 die("%s is not in git-p4.allowSubmit" % self
.master
)
1077 [upstream
, settings
] = findUpstreamBranchPoint()
1078 self
.depotPath
= settings
['depot-paths'][0]
1079 if len(self
.origin
) == 0:
1080 self
.origin
= upstream
1082 if self
.preserveUser
:
1083 if not self
.canChangeChangelists():
1084 die("Cannot preserve user names without p4 super-user or admin permissions")
1087 print "Origin branch is " + self
.origin
1089 if len(self
.depotPath
) == 0:
1090 print "Internal error: cannot locate perforce depot path from existing branches"
1093 self
.clientPath
= p4Where(self
.depotPath
)
1095 if len(self
.clientPath
) == 0:
1096 print "Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
1099 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
1100 self
.oldWorkingDirectory
= os
.getcwd()
1102 chdir(self
.clientPath
)
1103 print "Synchronizing p4 checkout..."
1108 for line
in read_pipe_lines("git rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)):
1109 commits
.append(line
.strip())
1112 if self
.preserveUser
or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1113 self
.checkAuthorship
= False
1115 self
.checkAuthorship
= True
1117 if self
.preserveUser
:
1118 self
.checkValidP4Users(commits
)
1120 while len(commits
) > 0:
1122 commits
= commits
[1:]
1123 self
.applyCommit(commit
)
1124 if not self
.interactive
:
1127 if len(commits
) == 0:
1128 print "All changes applied!"
1129 chdir(self
.oldWorkingDirectory
)
1139 class P4Sync(Command
, P4UserMap
):
1140 delete_actions
= ( "delete", "move/delete", "purge" )
1143 Command
.__init
__(self
)
1144 P4UserMap
.__init
__(self
)
1146 optparse
.make_option("--branch", dest
="branch"),
1147 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
1148 optparse
.make_option("--changesfile", dest
="changesFile"),
1149 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
1150 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
1151 optparse
.make_option("--verbose", dest
="verbose", action
="store_true"),
1152 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
1153 help="Import into refs/heads/ , not refs/remotes"),
1154 optparse
.make_option("--max-changes", dest
="maxChanges"),
1155 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
1156 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1157 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
1158 help="Only sync files that are included in the Perforce Client Spec")
1160 self
.description
= """Imports from Perforce into a git repository.\n
1162 //depot/my/project/ -- to import the current head
1163 //depot/my/project/@all -- to import everything
1164 //depot/my/project/@1,6 -- to import only from revision 1 to 6
1166 (a ... is not needed in the path p4 specification, it's added implicitly)"""
1168 self
.usage
+= " //depot/path[@revRange]"
1170 self
.createdBranches
= set()
1171 self
.committedChanges
= set()
1173 self
.detectBranches
= False
1174 self
.detectLabels
= False
1175 self
.changesFile
= ""
1176 self
.syncWithOrigin
= True
1177 self
.verbose
= False
1178 self
.importIntoRemotes
= True
1179 self
.maxChanges
= ""
1180 self
.isWindows
= (platform
.system() == "Windows")
1181 self
.keepRepoPath
= False
1182 self
.depotPaths
= None
1183 self
.p4BranchesInGit
= []
1184 self
.cloneExclude
= []
1185 self
.useClientSpec
= False
1186 self
.clientSpecDirs
= []
1188 if gitConfig("git-p4.syncFromOrigin") == "false":
1189 self
.syncWithOrigin
= False
1192 # P4 wildcards are not allowed in filenames. P4 complains
1193 # if you simply add them, but you can force it with "-f", in
1194 # which case it translates them into %xx encoding internally.
1195 # Search for and fix just these four characters. Do % last so
1196 # that fixing it does not inadvertently create new %-escapes.
1198 def wildcard_decode(self
, path
):
1199 # Cannot have * in a filename in windows; untested as to
1200 # what p4 would do in such a case.
1201 if not self
.isWindows
:
1202 path
= path
.replace("%2A", "*")
1203 path
= path
.replace("%23", "#") \
1204 .replace("%40", "@") \
1205 .replace("%25", "%")
1208 def extractFilesFromCommit(self
, commit
):
1209 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
1210 for path
in self
.cloneExclude
]
1213 while commit
.has_key("depotFile%s" % fnum
):
1214 path
= commit
["depotFile%s" % fnum
]
1216 if [p
for p
in self
.cloneExclude
1217 if p4PathStartsWith(path
, p
)]:
1220 found
= [p
for p
in self
.depotPaths
1221 if p4PathStartsWith(path
, p
)]
1228 file["rev"] = commit
["rev%s" % fnum
]
1229 file["action"] = commit
["action%s" % fnum
]
1230 file["type"] = commit
["type%s" % fnum
]
1235 def stripRepoPath(self
, path
, prefixes
):
1236 if self
.useClientSpec
:
1238 # if using the client spec, we use the output directory
1239 # specified in the client. For example, a view
1240 # //depot/foo/branch/... //client/branch/foo/...
1241 # will end up putting all foo/branch files into
1243 for val
in self
.clientSpecDirs
:
1244 if path
.startswith(val
[0]):
1245 # replace the depot path with the client path
1246 path
= path
.replace(val
[0], val
[1][1])
1247 # now strip out the client (//client/...)
1248 path
= re
.sub("^(//[^/]+/)", '', path
)
1249 # the rest is all path
1252 if self
.keepRepoPath
:
1253 prefixes
= [re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])]
1256 if p4PathStartsWith(path
, p
):
1257 path
= path
[len(p
):]
1261 def splitFilesIntoBranches(self
, commit
):
1264 while commit
.has_key("depotFile%s" % fnum
):
1265 path
= commit
["depotFile%s" % fnum
]
1266 found
= [p
for p
in self
.depotPaths
1267 if p4PathStartsWith(path
, p
)]
1274 file["rev"] = commit
["rev%s" % fnum
]
1275 file["action"] = commit
["action%s" % fnum
]
1276 file["type"] = commit
["type%s" % fnum
]
1279 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
1281 for branch
in self
.knownBranches
.keys():
1283 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1284 if relPath
.startswith(branch
+ "/"):
1285 if branch
not in branches
:
1286 branches
[branch
] = []
1287 branches
[branch
].append(file)
1292 # output one file from the P4 stream
1293 # - helper for streamP4Files
1295 def streamOneP4File(self
, file, contents
):
1296 relPath
= self
.stripRepoPath(file['depotFile'], self
.branchPrefixes
)
1297 relPath
= self
.wildcard_decode(relPath
)
1299 sys
.stderr
.write("%s\n" % relPath
)
1301 (type_base
, type_mods
) = split_p4_type(file["type"])
1304 if "x" in type_mods
:
1306 if type_base
== "symlink":
1308 # p4 print on a symlink contains "target\n"; remove the newline
1309 data
= ''.join(contents
)
1310 contents
= [data
[:-1]]
1312 if type_base
== "utf16":
1313 # p4 delivers different text in the python output to -G
1314 # than it does when using "print -o", or normal p4 client
1315 # operations. utf16 is converted to ascii or utf8, perhaps.
1316 # But ascii text saved as -t utf16 is completely mangled.
1317 # Invoke print -o to get the real contents.
1318 text
= p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
1321 if type_base
== "apple":
1322 # Apple filetype files will be streamed as a concatenation of
1323 # its appledouble header and the contents. This is useless
1324 # on both macs and non-macs. If using "print -q -o xx", it
1325 # will create "xx" with the data, and "%xx" with the header.
1326 # This is also not very useful.
1328 # Ideally, someday, this script can learn how to generate
1329 # appledouble files directly and import those to git, but
1330 # non-mac machines can never find a use for apple filetype.
1331 print "\nIgnoring apple filetype file %s" % file['depotFile']
1334 # Perhaps windows wants unicode, utf16 newlines translated too;
1335 # but this is not doing it.
1336 if self
.isWindows
and type_base
== "text":
1338 for data
in contents
:
1339 data
= data
.replace("\r\n", "\n")
1340 mangled
.append(data
)
1343 # Note that we do not try to de-mangle keywords on utf16 files,
1344 # even though in theory somebody may want that.
1345 if type_base
in ("text", "unicode", "binary"):
1346 if "ko" in type_mods
:
1347 text
= ''.join(contents
)
1348 text
= re
.sub(r
'\$(Id|Header):[^$]*\$', r
'$\1$', text
)
1350 elif "k" in type_mods
:
1351 text
= ''.join(contents
)
1352 text
= re
.sub(r
'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$', r
'$\1$', text
)
1355 self
.gitStream
.write("M %s inline %s\n" % (git_mode
, relPath
))
1360 length
= length
+ len(d
)
1362 self
.gitStream
.write("data %d\n" % length
)
1364 self
.gitStream
.write(d
)
1365 self
.gitStream
.write("\n")
1367 def streamOneP4Deletion(self
, file):
1368 relPath
= self
.stripRepoPath(file['path'], self
.branchPrefixes
)
1370 sys
.stderr
.write("delete %s\n" % relPath
)
1371 self
.gitStream
.write("D %s\n" % relPath
)
1373 # handle another chunk of streaming data
1374 def streamP4FilesCb(self
, marshalled
):
1376 if marshalled
.has_key('depotFile') and self
.stream_have_file_info
:
1377 # start of a new file - output the old one first
1378 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
1379 self
.stream_file
= {}
1380 self
.stream_contents
= []
1381 self
.stream_have_file_info
= False
1383 # pick up the new file information... for the
1384 # 'data' field we need to append to our array
1385 for k
in marshalled
.keys():
1387 self
.stream_contents
.append(marshalled
['data'])
1389 self
.stream_file
[k
] = marshalled
[k
]
1391 self
.stream_have_file_info
= True
1393 # Stream directly from "p4 files" into "git fast-import"
1394 def streamP4Files(self
, files
):
1401 for val
in self
.clientSpecDirs
:
1402 if f
['path'].startswith(val
[0]):
1408 filesForCommit
.append(f
)
1409 if f
['action'] in self
.delete_actions
:
1410 filesToDelete
.append(f
)
1412 filesToRead
.append(f
)
1415 for f
in filesToDelete
:
1416 self
.streamOneP4Deletion(f
)
1418 if len(filesToRead
) > 0:
1419 self
.stream_file
= {}
1420 self
.stream_contents
= []
1421 self
.stream_have_file_info
= False
1423 # curry self argument
1424 def streamP4FilesCbSelf(entry
):
1425 self
.streamP4FilesCb(entry
)
1427 fileArgs
= ['%s#%s' % (f
['path'], f
['rev']) for f
in filesToRead
]
1429 p4CmdList(["-x", "-", "print"],
1431 cb
=streamP4FilesCbSelf
)
1434 if self
.stream_file
.has_key('depotFile'):
1435 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
1437 def commit(self
, details
, files
, branch
, branchPrefixes
, parent
= ""):
1438 epoch
= details
["time"]
1439 author
= details
["user"]
1440 self
.branchPrefixes
= branchPrefixes
1443 print "commit into %s" % branch
1445 # start with reading files; if that fails, we should not
1449 if [p
for p
in branchPrefixes
if p4PathStartsWith(f
['path'], p
)]:
1450 new_files
.append (f
)
1452 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % f
['path'])
1454 self
.gitStream
.write("commit %s\n" % branch
)
1455 # gitStream.write("mark :%s\n" % details["change"])
1456 self
.committedChanges
.add(int(details
["change"]))
1458 if author
not in self
.users
:
1459 self
.getUserMapFromPerforceServer()
1460 if author
in self
.users
:
1461 committer
= "%s %s %s" % (self
.users
[author
], epoch
, self
.tz
)
1463 committer
= "%s <a@b> %s %s" % (author
, epoch
, self
.tz
)
1465 self
.gitStream
.write("committer %s\n" % committer
)
1467 self
.gitStream
.write("data <<EOT\n")
1468 self
.gitStream
.write(details
["desc"])
1469 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1470 % (','.join (branchPrefixes
), details
["change"]))
1471 if len(details
['options']) > 0:
1472 self
.gitStream
.write(": options = %s" % details
['options'])
1473 self
.gitStream
.write("]\nEOT\n\n")
1477 print "parent %s" % parent
1478 self
.gitStream
.write("from %s\n" % parent
)
1480 self
.streamP4Files(new_files
)
1481 self
.gitStream
.write("\n")
1483 change
= int(details
["change"])
1485 if self
.labels
.has_key(change
):
1486 label
= self
.labels
[change
]
1487 labelDetails
= label
[0]
1488 labelRevisions
= label
[1]
1490 print "Change %s is labelled %s" % (change
, labelDetails
)
1492 files
= p4CmdList(["files"] + ["%s...@%s" % (p
, change
)
1493 for p
in branchPrefixes
])
1495 if len(files
) == len(labelRevisions
):
1499 if info
["action"] in self
.delete_actions
:
1501 cleanedFiles
[info
["depotFile"]] = info
["rev"]
1503 if cleanedFiles
== labelRevisions
:
1504 self
.gitStream
.write("tag tag_%s\n" % labelDetails
["label"])
1505 self
.gitStream
.write("from %s\n" % branch
)
1507 owner
= labelDetails
["Owner"]
1509 if author
in self
.users
:
1510 tagger
= "%s %s %s" % (self
.users
[owner
], epoch
, self
.tz
)
1512 tagger
= "%s <a@b> %s %s" % (owner
, epoch
, self
.tz
)
1513 self
.gitStream
.write("tagger %s\n" % tagger
)
1514 self
.gitStream
.write("data <<EOT\n")
1515 self
.gitStream
.write(labelDetails
["Description"])
1516 self
.gitStream
.write("EOT\n\n")
1520 print ("Tag %s does not match with change %s: files do not match."
1521 % (labelDetails
["label"], change
))
1525 print ("Tag %s does not match with change %s: file count is different."
1526 % (labelDetails
["label"], change
))
1528 def getLabels(self
):
1531 l
= p4CmdList("labels %s..." % ' '.join (self
.depotPaths
))
1532 if len(l
) > 0 and not self
.silent
:
1533 print "Finding files belonging to labels in %s" % `self
.depotPaths`
1536 label
= output
["label"]
1540 print "Querying files for label %s" % label
1541 for file in p4CmdList(["files"] +
1542 ["%s...@%s" % (p
, label
)
1543 for p
in self
.depotPaths
]):
1544 revisions
[file["depotFile"]] = file["rev"]
1545 change
= int(file["change"])
1546 if change
> newestChange
:
1547 newestChange
= change
1549 self
.labels
[newestChange
] = [output
, revisions
]
1552 print "Label changes: %s" % self
.labels
.keys()
1554 def guessProjectName(self
):
1555 for p
in self
.depotPaths
:
1558 p
= p
[p
.strip().rfind("/") + 1:]
1559 if not p
.endswith("/"):
1563 def getBranchMapping(self
):
1564 lostAndFoundBranches
= set()
1566 user
= gitConfig("git-p4.branchUser")
1568 command
= "branches -u %s" % user
1570 command
= "branches"
1572 for info
in p4CmdList(command
):
1573 details
= p4Cmd("branch -o %s" % info
["branch"])
1575 while details
.has_key("View%s" % viewIdx
):
1576 paths
= details
["View%s" % viewIdx
].split(" ")
1577 viewIdx
= viewIdx
+ 1
1578 # require standard //depot/foo/... //depot/bar/... mapping
1579 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
1582 destination
= paths
[1]
1584 if p4PathStartsWith(source
, self
.depotPaths
[0]) and p4PathStartsWith(destination
, self
.depotPaths
[0]):
1585 source
= source
[len(self
.depotPaths
[0]):-4]
1586 destination
= destination
[len(self
.depotPaths
[0]):-4]
1588 if destination
in self
.knownBranches
:
1590 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
1591 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
1594 self
.knownBranches
[destination
] = source
1596 lostAndFoundBranches
.discard(destination
)
1598 if source
not in self
.knownBranches
:
1599 lostAndFoundBranches
.add(source
)
1601 # Perforce does not strictly require branches to be defined, so we also
1602 # check git config for a branch list.
1604 # Example of branch definition in git config file:
1606 # branchList=main:branchA
1607 # branchList=main:branchB
1608 # branchList=branchA:branchC
1609 configBranches
= gitConfigList("git-p4.branchList")
1610 for branch
in configBranches
:
1612 (source
, destination
) = branch
.split(":")
1613 self
.knownBranches
[destination
] = source
1615 lostAndFoundBranches
.discard(destination
)
1617 if source
not in self
.knownBranches
:
1618 lostAndFoundBranches
.add(source
)
1621 for branch
in lostAndFoundBranches
:
1622 self
.knownBranches
[branch
] = branch
1624 def getBranchMappingFromGitBranches(self
):
1625 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1626 for branch
in branches
.keys():
1627 if branch
== "master":
1630 branch
= branch
[len(self
.projectName
):]
1631 self
.knownBranches
[branch
] = branch
1633 def listExistingP4GitBranches(self
):
1634 # branches holds mapping from name to commit
1635 branches
= p4BranchesInGit(self
.importIntoRemotes
)
1636 self
.p4BranchesInGit
= branches
.keys()
1637 for branch
in branches
.keys():
1638 self
.initialParents
[self
.refPrefix
+ branch
] = branches
[branch
]
1640 def updateOptionDict(self
, d
):
1642 if self
.keepRepoPath
:
1643 option_keys
['keepRepoPath'] = 1
1645 d
["options"] = ' '.join(sorted(option_keys
.keys()))
1647 def readOptions(self
, d
):
1648 self
.keepRepoPath
= (d
.has_key('options')
1649 and ('keepRepoPath' in d
['options']))
1651 def gitRefForBranch(self
, branch
):
1652 if branch
== "main":
1653 return self
.refPrefix
+ "master"
1655 if len(branch
) <= 0:
1658 return self
.refPrefix
+ self
.projectName
+ branch
1660 def gitCommitByP4Change(self
, ref
, change
):
1662 print "looking in ref " + ref
+ " for change %s using bisect..." % change
1665 latestCommit
= parseRevision(ref
)
1669 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
1670 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
1675 log
= extractLogMessageFromGitCommit(next
)
1676 settings
= extractSettingsGitLog(log
)
1677 currentChange
= int(settings
['change'])
1679 print "current change %s" % currentChange
1681 if currentChange
== change
:
1683 print "found %s" % next
1686 if currentChange
< change
:
1687 earliestCommit
= "^%s" % next
1689 latestCommit
= "%s" % next
1693 def importNewBranch(self
, branch
, maxChange
):
1694 # make fast-import flush all changes to disk and update the refs using the checkpoint
1695 # command so that we can try to find the branch parent in the git history
1696 self
.gitStream
.write("checkpoint\n\n");
1697 self
.gitStream
.flush();
1698 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1699 range = "@1,%s" % maxChange
1700 #print "prefix" + branchPrefix
1701 changes
= p4ChangesForPaths([branchPrefix
], range)
1702 if len(changes
) <= 0:
1704 firstChange
= changes
[0]
1705 #print "first change in branch: %s" % firstChange
1706 sourceBranch
= self
.knownBranches
[branch
]
1707 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
1708 sourceRef
= self
.gitRefForBranch(sourceBranch
)
1709 #print "source " + sourceBranch
1711 branchParentChange
= int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath
, firstChange
))["change"])
1712 #print "branch parent: %s" % branchParentChange
1713 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
1714 if len(gitParent
) > 0:
1715 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
1716 #print "parent git commit: %s" % gitParent
1718 self
.importChanges(changes
)
1721 def importChanges(self
, changes
):
1723 for change
in changes
:
1724 description
= p4Cmd("describe %s" % change
)
1725 self
.updateOptionDict(description
)
1728 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
1733 if self
.detectBranches
:
1734 branches
= self
.splitFilesIntoBranches(description
)
1735 for branch
in branches
.keys():
1737 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
1741 filesForCommit
= branches
[branch
]
1744 print "branch is %s" % branch
1746 self
.updatedBranches
.add(branch
)
1748 if branch
not in self
.createdBranches
:
1749 self
.createdBranches
.add(branch
)
1750 parent
= self
.knownBranches
[branch
]
1751 if parent
== branch
:
1754 fullBranch
= self
.projectName
+ branch
1755 if fullBranch
not in self
.p4BranchesInGit
:
1757 print("\n Importing new branch %s" % fullBranch
);
1758 if self
.importNewBranch(branch
, change
- 1):
1760 self
.p4BranchesInGit
.append(fullBranch
)
1762 print("\n Resuming with change %s" % change
);
1765 print "parent determined through known branches: %s" % parent
1767 branch
= self
.gitRefForBranch(branch
)
1768 parent
= self
.gitRefForBranch(parent
)
1771 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
1773 if len(parent
) == 0 and branch
in self
.initialParents
:
1774 parent
= self
.initialParents
[branch
]
1775 del self
.initialParents
[branch
]
1777 self
.commit(description
, filesForCommit
, branch
, [branchPrefix
], parent
)
1779 files
= self
.extractFilesFromCommit(description
)
1780 self
.commit(description
, files
, self
.branch
, self
.depotPaths
,
1782 self
.initialParent
= ""
1784 print self
.gitError
.read()
1787 def importHeadRevision(self
, revision
):
1788 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
1791 details
["user"] = "git perforce import user"
1792 details
["desc"] = ("Initial import of %s from the state at revision %s\n"
1793 % (' '.join(self
.depotPaths
), revision
))
1794 details
["change"] = revision
1798 fileArgs
= ["%s...%s" % (p
,revision
) for p
in self
.depotPaths
]
1800 for info
in p4CmdList(["files"] + fileArgs
):
1802 if 'code' in info
and info
['code'] == 'error':
1803 sys
.stderr
.write("p4 returned an error: %s\n"
1805 if info
['data'].find("must refer to client") >= 0:
1806 sys
.stderr
.write("This particular p4 error is misleading.\n")
1807 sys
.stderr
.write("Perhaps the depot path was misspelled.\n");
1808 sys
.stderr
.write("Depot path: %s\n" % " ".join(self
.depotPaths
))
1810 if 'p4ExitCode' in info
:
1811 sys
.stderr
.write("p4 exitcode: %s\n" % info
['p4ExitCode'])
1815 change
= int(info
["change"])
1816 if change
> newestRevision
:
1817 newestRevision
= change
1819 if info
["action"] in self
.delete_actions
:
1820 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1821 #fileCnt = fileCnt + 1
1824 for prop
in ["depotFile", "rev", "action", "type" ]:
1825 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
1827 fileCnt
= fileCnt
+ 1
1829 details
["change"] = newestRevision
1831 # Use time from top-most change so that all git-p4 clones of
1832 # the same p4 repo have the same commit SHA1s.
1833 res
= p4CmdList("describe -s %d" % newestRevision
)
1836 if r
.has_key('time'):
1837 newestTime
= int(r
['time'])
1838 if newestTime
is None:
1839 die("\"describe -s\" on newest change %d did not give a time")
1840 details
["time"] = newestTime
1842 self
.updateOptionDict(details
)
1844 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
, self
.depotPaths
)
1846 print "IO error with git fast-import. Is your git version recent enough?"
1847 print self
.gitError
.read()
1850 def getClientSpec(self
):
1851 specList
= p4CmdList( "client -o" )
1853 for entry
in specList
:
1854 for k
,v
in entry
.iteritems():
1855 if k
.startswith("View"):
1857 # p4 has these %%1 to %%9 arguments in specs to
1858 # reorder paths; which we can't handle (yet :)
1859 if re
.match('%%\d', v
) != None:
1860 print "Sorry, can't handle %%n arguments in client specs"
1863 if v
.startswith('"'):
1867 index
= v
.find("...")
1869 # save the "client view"; i.e the RHS of the view
1870 # line that tells the client where to put the
1871 # files for this view.
1872 cv
= v
[index
+3:].strip() # +3 to remove previous '...'
1874 # if the client view doesn't end with a
1875 # ... wildcard, then we're going to mess up the
1876 # output directory, so fail gracefully.
1877 if not cv
.endswith('...'):
1878 print 'Sorry, client view in "%s" needs to end with wildcard' % (k
)
1882 # now save the view; +index means included, -index
1883 # means it should be filtered out.
1885 if v
.startswith("-"):
1891 temp
[v
] = (include
, cv
)
1893 self
.clientSpecDirs
= temp
.items()
1894 self
.clientSpecDirs
.sort( lambda x
, y
: abs( y
[1][0] ) - abs( x
[1][0] ) )
1896 def run(self
, args
):
1897 self
.depotPaths
= []
1898 self
.changeRange
= ""
1899 self
.initialParent
= ""
1900 self
.previousDepotPaths
= []
1902 # map from branch depot path to parent branch
1903 self
.knownBranches
= {}
1904 self
.initialParents
= {}
1905 self
.hasOrigin
= originP4BranchesExist()
1906 if not self
.syncWithOrigin
:
1907 self
.hasOrigin
= False
1909 if self
.importIntoRemotes
:
1910 self
.refPrefix
= "refs/remotes/p4/"
1912 self
.refPrefix
= "refs/heads/p4/"
1914 if self
.syncWithOrigin
and self
.hasOrigin
:
1916 print "Syncing with origin first by calling git fetch origin"
1917 system("git fetch origin")
1919 if len(self
.branch
) == 0:
1920 self
.branch
= self
.refPrefix
+ "master"
1921 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
1922 system("git update-ref %s refs/heads/p4" % self
.branch
)
1923 system("git branch -D p4");
1924 # create it /after/ importing, when master exists
1925 if not gitBranchExists(self
.refPrefix
+ "HEAD") and self
.importIntoRemotes
and gitBranchExists(self
.branch
):
1926 system("git symbolic-ref %sHEAD %s" % (self
.refPrefix
, self
.branch
))
1928 if self
.useClientSpec
or gitConfig("git-p4.useclientspec") == "true":
1929 self
.getClientSpec()
1931 # TODO: should always look at previous commits,
1932 # merge with previous imports, if possible.
1935 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
1936 self
.listExistingP4GitBranches()
1938 if len(self
.p4BranchesInGit
) > 1:
1940 print "Importing from/into multiple branches"
1941 self
.detectBranches
= True
1944 print "branches: %s" % self
.p4BranchesInGit
1947 for branch
in self
.p4BranchesInGit
:
1948 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
1950 settings
= extractSettingsGitLog(logMsg
)
1952 self
.readOptions(settings
)
1953 if (settings
.has_key('depot-paths')
1954 and settings
.has_key ('change')):
1955 change
= int(settings
['change']) + 1
1956 p4Change
= max(p4Change
, change
)
1958 depotPaths
= sorted(settings
['depot-paths'])
1959 if self
.previousDepotPaths
== []:
1960 self
.previousDepotPaths
= depotPaths
1963 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
1964 prev_list
= prev
.split("/")
1965 cur_list
= cur
.split("/")
1966 for i
in range(0, min(len(cur_list
), len(prev_list
))):
1967 if cur_list
[i
] <> prev_list
[i
]:
1971 paths
.append ("/".join(cur_list
[:i
+ 1]))
1973 self
.previousDepotPaths
= paths
1976 self
.depotPaths
= sorted(self
.previousDepotPaths
)
1977 self
.changeRange
= "@%s,#head" % p4Change
1978 if not self
.detectBranches
:
1979 self
.initialParent
= parseRevision(self
.branch
)
1980 if not self
.silent
and not self
.detectBranches
:
1981 print "Performing incremental import into %s git branch" % self
.branch
1983 if not self
.branch
.startswith("refs/"):
1984 self
.branch
= "refs/heads/" + self
.branch
1986 if len(args
) == 0 and self
.depotPaths
:
1988 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
1990 if self
.depotPaths
and self
.depotPaths
!= args
:
1991 print ("previous import used depot path %s and now %s was specified. "
1992 "This doesn't work!" % (' '.join (self
.depotPaths
),
1996 self
.depotPaths
= sorted(args
)
2002 for p
in self
.depotPaths
:
2003 if p
.find("@") != -1:
2004 atIdx
= p
.index("@")
2005 self
.changeRange
= p
[atIdx
:]
2006 if self
.changeRange
== "@all":
2007 self
.changeRange
= ""
2008 elif ',' not in self
.changeRange
:
2009 revision
= self
.changeRange
2010 self
.changeRange
= ""
2012 elif p
.find("#") != -1:
2013 hashIdx
= p
.index("#")
2014 revision
= p
[hashIdx
:]
2016 elif self
.previousDepotPaths
== []:
2019 p
= re
.sub ("\.\.\.$", "", p
)
2020 if not p
.endswith("/"):
2025 self
.depotPaths
= newPaths
2028 self
.loadUserMapFromCache()
2030 if self
.detectLabels
:
2033 if self
.detectBranches
:
2034 ## FIXME - what's a P4 projectName ?
2035 self
.projectName
= self
.guessProjectName()
2038 self
.getBranchMappingFromGitBranches()
2040 self
.getBranchMapping()
2042 print "p4-git branches: %s" % self
.p4BranchesInGit
2043 print "initial parents: %s" % self
.initialParents
2044 for b
in self
.p4BranchesInGit
:
2048 b
= b
[len(self
.projectName
):]
2049 self
.createdBranches
.add(b
)
2051 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
2053 importProcess
= subprocess
.Popen(["git", "fast-import"],
2054 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
2055 stderr
=subprocess
.PIPE
);
2056 self
.gitOutput
= importProcess
.stdout
2057 self
.gitStream
= importProcess
.stdin
2058 self
.gitError
= importProcess
.stderr
2061 self
.importHeadRevision(revision
)
2065 if len(self
.changesFile
) > 0:
2066 output
= open(self
.changesFile
).readlines()
2069 changeSet
.add(int(line
))
2071 for change
in changeSet
:
2072 changes
.append(change
)
2076 # catch "git-p4 sync" with no new branches, in a repo that
2077 # does not have any existing git-p4 branches
2078 if len(args
) == 0 and not self
.p4BranchesInGit
:
2079 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
2081 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
2083 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
)
2085 if len(self
.maxChanges
) > 0:
2086 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
2088 if len(changes
) == 0:
2090 print "No changes to import!"
2093 if not self
.silent
and not self
.detectBranches
:
2094 print "Import destination: %s" % self
.branch
2096 self
.updatedBranches
= set()
2098 self
.importChanges(changes
)
2102 if len(self
.updatedBranches
) > 0:
2103 sys
.stdout
.write("Updated branches: ")
2104 for b
in self
.updatedBranches
:
2105 sys
.stdout
.write("%s " % b
)
2106 sys
.stdout
.write("\n")
2108 self
.gitStream
.close()
2109 if importProcess
.wait() != 0:
2110 die("fast-import failed: %s" % self
.gitError
.read())
2111 self
.gitOutput
.close()
2112 self
.gitError
.close()
2116 class P4Rebase(Command
):
2118 Command
.__init
__(self
)
2120 self
.description
= ("Fetches the latest revision from perforce and "
2121 + "rebases the current work (branch) against it")
2122 self
.verbose
= False
2124 def run(self
, args
):
2128 return self
.rebase()
2131 if os
.system("git update-index --refresh") != 0:
2132 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.");
2133 if len(read_pipe("git diff-index HEAD --")) > 0:
2134 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2136 [upstream
, settings
] = findUpstreamBranchPoint()
2137 if len(upstream
) == 0:
2138 die("Cannot find upstream branchpoint for rebase")
2140 # the branchpoint may be p4/foo~3, so strip off the parent
2141 upstream
= re
.sub("~[0-9]+$", "", upstream
)
2143 print "Rebasing the current branch onto %s" % upstream
2144 oldHead
= read_pipe("git rev-parse HEAD").strip()
2145 system("git rebase %s" % upstream
)
2146 system("git diff-tree --stat --summary -M %s HEAD" % oldHead
)
2149 class P4Clone(P4Sync
):
2151 P4Sync
.__init
__(self
)
2152 self
.description
= "Creates a new git repository and imports from Perforce into it"
2153 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
2155 optparse
.make_option("--destination", dest
="cloneDestination",
2156 action
='store', default
=None,
2157 help="where to leave result of the clone"),
2158 optparse
.make_option("-/", dest
="cloneExclude",
2159 action
="append", type="string",
2160 help="exclude depot path"),
2161 optparse
.make_option("--bare", dest
="cloneBare",
2162 action
="store_true", default
=False),
2164 self
.cloneDestination
= None
2165 self
.needsGit
= False
2166 self
.cloneBare
= False
2168 # This is required for the "append" cloneExclude action
2169 def ensure_value(self
, attr
, value
):
2170 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
2171 setattr(self
, attr
, value
)
2172 return getattr(self
, attr
)
2174 def defaultDestination(self
, args
):
2175 ## TODO: use common prefix of args?
2177 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
2178 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
2179 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
2180 depotDir
= re
.sub(r
"/$", "", depotDir
)
2181 return os
.path
.split(depotDir
)[1]
2183 def run(self
, args
):
2187 if self
.keepRepoPath
and not self
.cloneDestination
:
2188 sys
.stderr
.write("Must specify destination for --keep-path\n")
2193 if not self
.cloneDestination
and len(depotPaths
) > 1:
2194 self
.cloneDestination
= depotPaths
[-1]
2195 depotPaths
= depotPaths
[:-1]
2197 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
2198 for p
in depotPaths
:
2199 if not p
.startswith("//"):
2202 if not self
.cloneDestination
:
2203 self
.cloneDestination
= self
.defaultDestination(args
)
2205 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
2207 if not os
.path
.exists(self
.cloneDestination
):
2208 os
.makedirs(self
.cloneDestination
)
2209 chdir(self
.cloneDestination
)
2211 init_cmd
= [ "git", "init" ]
2213 init_cmd
.append("--bare")
2214 subprocess
.check_call(init_cmd
)
2216 if not P4Sync
.run(self
, depotPaths
):
2218 if self
.branch
!= "master":
2219 if self
.importIntoRemotes
:
2220 masterbranch
= "refs/remotes/p4/master"
2222 masterbranch
= "refs/heads/p4/master"
2223 if gitBranchExists(masterbranch
):
2224 system("git branch master %s" % masterbranch
)
2225 if not self
.cloneBare
:
2226 system("git checkout -f")
2228 print "Could not detect main branch. No checkout/master branch created."
2232 class P4Branches(Command
):
2234 Command
.__init
__(self
)
2236 self
.description
= ("Shows the git branches that hold imports and their "
2237 + "corresponding perforce depot paths")
2238 self
.verbose
= False
2240 def run(self
, args
):
2241 if originP4BranchesExist():
2242 createOrUpdateBranchesFromOrigin()
2244 cmdline
= "git rev-parse --symbolic "
2245 cmdline
+= " --remotes"
2247 for line
in read_pipe_lines(cmdline
):
2250 if not line
.startswith('p4/') or line
== "p4/HEAD":
2254 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
2255 settings
= extractSettingsGitLog(log
)
2257 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
2260 class HelpFormatter(optparse
.IndentedHelpFormatter
):
2262 optparse
.IndentedHelpFormatter
.__init
__(self
)
2264 def format_description(self
, description
):
2266 return description
+ "\n"
2270 def printUsage(commands
):
2271 print "usage: %s <command> [options]" % sys
.argv
[0]
2273 print "valid commands: %s" % ", ".join(commands
)
2275 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
2280 "submit" : P4Submit
,
2281 "commit" : P4Submit
,
2283 "rebase" : P4Rebase
,
2285 "rollback" : P4RollBack
,
2286 "branches" : P4Branches
2291 if len(sys
.argv
[1:]) == 0:
2292 printUsage(commands
.keys())
2296 cmdName
= sys
.argv
[1]
2298 klass
= commands
[cmdName
]
2301 print "unknown command %s" % cmdName
2303 printUsage(commands
.keys())
2306 options
= cmd
.options
2307 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
2311 if len(options
) > 0:
2312 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
2314 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
2316 description
= cmd
.description
,
2317 formatter
= HelpFormatter())
2319 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
2321 verbose
= cmd
.verbose
2323 if cmd
.gitdir
== None:
2324 cmd
.gitdir
= os
.path
.abspath(".git")
2325 if not isValidGitDir(cmd
.gitdir
):
2326 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
2327 if os
.path
.exists(cmd
.gitdir
):
2328 cdup
= read_pipe("git rev-parse --show-cdup").strip()
2332 if not isValidGitDir(cmd
.gitdir
):
2333 if isValidGitDir(cmd
.gitdir
+ "/.git"):
2334 cmd
.gitdir
+= "/.git"
2336 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
2338 os
.environ
["GIT_DIR"] = cmd
.gitdir
2340 if not cmd
.run(args
):
2344 if __name__
== '__main__':