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
17 # Only labels/tags matching this will be imported/exported
18 defaultLabelRegexp
= r
'[a-zA-Z0-9_\-.]+$'
20 def p4_build_cmd(cmd
):
21 """Build a suitable p4 command line.
23 This consolidates building and returning a p4 command line into one
24 location. It means that hooking into the environment, or other configuration
25 can be done more easily.
29 user
= gitConfig("git-p4.user")
31 real_cmd
+= ["-u",user
]
33 password
= gitConfig("git-p4.password")
35 real_cmd
+= ["-P", password
]
37 port
= gitConfig("git-p4.port")
39 real_cmd
+= ["-p", port
]
41 host
= gitConfig("git-p4.host")
43 real_cmd
+= ["-H", host
]
45 client
= gitConfig("git-p4.client")
47 real_cmd
+= ["-c", client
]
50 if isinstance(cmd
,basestring
):
51 real_cmd
= ' '.join(real_cmd
) + ' ' + cmd
57 # P4 uses the PWD environment variable rather than getcwd(). Since we're
58 # not using the shell, we have to set it ourselves. This path could
59 # be relative, so go there first, then figure out where we ended up.
61 os
.environ
['PWD'] = os
.getcwd()
67 sys
.stderr
.write(msg
+ "\n")
70 def write_pipe(c
, stdin
):
72 sys
.stderr
.write('Writing pipe: %s\n' % str(c
))
74 expand
= isinstance(c
,basestring
)
75 p
= subprocess
.Popen(c
, stdin
=subprocess
.PIPE
, shell
=expand
)
77 val
= pipe
.write(stdin
)
80 die('Command failed: %s' % str(c
))
84 def p4_write_pipe(c
, stdin
):
85 real_cmd
= p4_build_cmd(c
)
86 return write_pipe(real_cmd
, stdin
)
88 def read_pipe(c
, ignore_error
=False):
90 sys
.stderr
.write('Reading pipe: %s\n' % str(c
))
92 expand
= isinstance(c
,basestring
)
93 p
= subprocess
.Popen(c
, stdout
=subprocess
.PIPE
, shell
=expand
)
96 if p
.wait() and not ignore_error
:
97 die('Command failed: %s' % str(c
))
101 def p4_read_pipe(c
, ignore_error
=False):
102 real_cmd
= p4_build_cmd(c
)
103 return read_pipe(real_cmd
, ignore_error
)
105 def read_pipe_lines(c
):
107 sys
.stderr
.write('Reading pipe: %s\n' % str(c
))
109 expand
= isinstance(c
, basestring
)
110 p
= subprocess
.Popen(c
, stdout
=subprocess
.PIPE
, shell
=expand
)
112 val
= pipe
.readlines()
113 if pipe
.close() or p
.wait():
114 die('Command failed: %s' % str(c
))
118 def p4_read_pipe_lines(c
):
119 """Specifically invoke p4 on the command supplied. """
120 real_cmd
= p4_build_cmd(c
)
121 return read_pipe_lines(real_cmd
)
123 def p4_has_command(cmd
):
124 """Ask p4 for help on this command. If it returns an error, the
125 command does not exist in this version of p4."""
126 real_cmd
= p4_build_cmd(["help", cmd
])
127 p
= subprocess
.Popen(real_cmd
, stdout
=subprocess
.PIPE
,
128 stderr
=subprocess
.PIPE
)
130 return p
.returncode
== 0
133 expand
= isinstance(cmd
,basestring
)
135 sys
.stderr
.write("executing %s\n" % str(cmd
))
136 subprocess
.check_call(cmd
, shell
=expand
)
139 """Specifically invoke p4 as the system command. """
140 real_cmd
= p4_build_cmd(cmd
)
141 expand
= isinstance(real_cmd
, basestring
)
142 subprocess
.check_call(real_cmd
, shell
=expand
)
144 def p4_integrate(src
, dest
):
145 p4_system(["integrate", "-Dt", wildcard_encode(src
), wildcard_encode(dest
)])
147 def p4_sync(f
, *options
):
148 p4_system(["sync"] + list(options
) + [wildcard_encode(f
)])
151 # forcibly add file names with wildcards
152 if wildcard_present(f
):
153 p4_system(["add", "-f", f
])
155 p4_system(["add", f
])
158 p4_system(["delete", wildcard_encode(f
)])
161 p4_system(["edit", wildcard_encode(f
)])
164 p4_system(["revert", wildcard_encode(f
)])
166 def p4_reopen(type, f
):
167 p4_system(["reopen", "-t", type, wildcard_encode(f
)])
169 def p4_move(src
, dest
):
170 p4_system(["move", "-k", wildcard_encode(src
), wildcard_encode(dest
)])
172 def p4_describe(change
):
173 """Make sure it returns a valid result by checking for
174 the presence of field "time". Return a dict of the
177 ds
= p4CmdList(["describe", "-s", str(change
)])
179 die("p4 describe -s %d did not return 1 result: %s" % (change
, str(ds
)))
183 if "p4ExitCode" in d
:
184 die("p4 describe -s %d exited with %d: %s" % (change
, d
["p4ExitCode"],
187 if d
["code"] == "error":
188 die("p4 describe -s %d returned error code: %s" % (change
, str(d
)))
191 die("p4 describe -s %d returned no \"time\": %s" % (change
, str(d
)))
196 # Canonicalize the p4 type and return a tuple of the
197 # base type, plus any modifiers. See "p4 help filetypes"
198 # for a list and explanation.
200 def split_p4_type(p4type
):
202 p4_filetypes_historical
= {
203 "ctempobj": "binary+Sw",
209 "tempobj": "binary+FSw",
210 "ubinary": "binary+F",
211 "uresource": "resource+F",
212 "uxbinary": "binary+Fx",
213 "xbinary": "binary+x",
215 "xtempobj": "binary+Swx",
217 "xunicode": "unicode+x",
220 if p4type
in p4_filetypes_historical
:
221 p4type
= p4_filetypes_historical
[p4type
]
223 s
= p4type
.split("+")
231 # return the raw p4 type of a file (text, text+ko, etc)
234 results
= p4CmdList(["fstat", "-T", "headType", file])
235 return results
[0]['headType']
238 # Given a type base and modifier, return a regexp matching
239 # the keywords that can be expanded in the file
241 def p4_keywords_regexp_for_type(base
, type_mods
):
242 if base
in ("text", "unicode", "binary"):
244 if "ko" in type_mods
:
246 elif "k" in type_mods
:
247 kwords
= 'Id|Header|Author|Date|DateTime|Change|File|Revision'
251 \$ # Starts with a dollar, followed by...
252 (%s) # one of the keywords, followed by...
253 (:[^$\n]+)? # possibly an old expansion, followed by...
261 # Given a file, return a regexp matching the possible
262 # RCS keywords that will be expanded, or None for files
263 # with kw expansion turned off.
265 def p4_keywords_regexp_for_file(file):
266 if not os
.path
.exists(file):
269 (type_base
, type_mods
) = split_p4_type(p4_type(file))
270 return p4_keywords_regexp_for_type(type_base
, type_mods
)
272 def setP4ExecBit(file, mode
):
273 # Reopens an already open file and changes the execute bit to match
274 # the execute bit setting in the passed in mode.
278 if not isModeExec(mode
):
279 p4Type
= getP4OpenedType(file)
280 p4Type
= re
.sub('^([cku]?)x(.*)', '\\1\\2', p4Type
)
281 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
282 if p4Type
[-1] == "+":
283 p4Type
= p4Type
[0:-1]
285 p4_reopen(p4Type
, file)
287 def getP4OpenedType(file):
288 # Returns the perforce file type for the given file.
290 result
= p4_read_pipe(["opened", wildcard_encode(file)])
291 match
= re
.match(".*\((.+)\)\r?$", result
)
293 return match
.group(1)
295 die("Could not determine file type for %s (result: '%s')" % (file, result
))
297 # Return the set of all p4 labels
298 def getP4Labels(depotPaths
):
300 if isinstance(depotPaths
,basestring
):
301 depotPaths
= [depotPaths
]
303 for l
in p4CmdList(["labels"] + ["%s..." % p
for p
in depotPaths
]):
309 # Return the set of all git tags
312 for line
in read_pipe_lines(["git", "tag"]):
317 def diffTreePattern():
318 # This is a simple generator for the diff tree regex pattern. This could be
319 # a class variable if this and parseDiffTreeEntry were a part of a class.
320 pattern
= re
.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
324 def parseDiffTreeEntry(entry
):
325 """Parses a single diff tree entry into its component elements.
327 See git-diff-tree(1) manpage for details about the format of the diff
328 output. This method returns a dictionary with the following elements:
330 src_mode - The mode of the source file
331 dst_mode - The mode of the destination file
332 src_sha1 - The sha1 for the source file
333 dst_sha1 - The sha1 fr the destination file
334 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
335 status_score - The score for the status (applicable for 'C' and 'R'
336 statuses). This is None if there is no score.
337 src - The path for the source file.
338 dst - The path for the destination file. This is only present for
339 copy or renames. If it is not present, this is None.
341 If the pattern is not matched, None is returned."""
343 match
= diffTreePattern().next().match(entry
)
346 'src_mode': match
.group(1),
347 'dst_mode': match
.group(2),
348 'src_sha1': match
.group(3),
349 'dst_sha1': match
.group(4),
350 'status': match
.group(5),
351 'status_score': match
.group(6),
352 'src': match
.group(7),
353 'dst': match
.group(10)
357 def isModeExec(mode
):
358 # Returns True if the given git mode represents an executable file,
360 return mode
[-3:] == "755"
362 def isModeExecChanged(src_mode
, dst_mode
):
363 return isModeExec(src_mode
) != isModeExec(dst_mode
)
365 def p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b', cb
=None):
367 if isinstance(cmd
,basestring
):
374 cmd
= p4_build_cmd(cmd
)
376 sys
.stderr
.write("Opening pipe: %s\n" % str(cmd
))
378 # Use a temporary file to avoid deadlocks without
379 # subprocess.communicate(), which would put another copy
380 # of stdout into memory.
382 if stdin
is not None:
383 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
384 if isinstance(stdin
,basestring
):
385 stdin_file
.write(stdin
)
388 stdin_file
.write(i
+ '\n')
392 p4
= subprocess
.Popen(cmd
,
395 stdout
=subprocess
.PIPE
)
400 entry
= marshal
.load(p4
.stdout
)
410 entry
["p4ExitCode"] = exitCode
416 list = p4CmdList(cmd
)
422 def p4Where(depotPath
):
423 if not depotPath
.endswith("/"):
425 depotPath
= depotPath
+ "..."
426 outputList
= p4CmdList(["where", depotPath
])
428 for entry
in outputList
:
429 if "depotFile" in entry
:
430 if entry
["depotFile"] == depotPath
:
433 elif "data" in entry
:
434 data
= entry
.get("data")
435 space
= data
.find(" ")
436 if data
[:space
] == depotPath
:
441 if output
["code"] == "error":
445 clientPath
= output
.get("path")
446 elif "data" in output
:
447 data
= output
.get("data")
448 lastSpace
= data
.rfind(" ")
449 clientPath
= data
[lastSpace
+ 1:]
451 if clientPath
.endswith("..."):
452 clientPath
= clientPath
[:-3]
455 def currentGitBranch():
456 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
458 def isValidGitDir(path
):
459 if (os
.path
.exists(path
+ "/HEAD")
460 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
464 def parseRevision(ref
):
465 return read_pipe("git rev-parse %s" % ref
).strip()
467 def branchExists(ref
):
468 rev
= read_pipe(["git", "rev-parse", "-q", "--verify", ref
],
472 def extractLogMessageFromGitCommit(commit
):
475 ## fixme: title is first line of commit, not 1st paragraph.
477 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
486 def extractSettingsGitLog(log
):
488 for line
in log
.split("\n"):
490 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
494 assignments
= m
.group(1).split (':')
495 for a
in assignments
:
497 key
= vals
[0].strip()
498 val
= ('='.join (vals
[1:])).strip()
499 if val
.endswith ('\"') and val
.startswith('"'):
504 paths
= values
.get("depot-paths")
506 paths
= values
.get("depot-path")
508 values
['depot-paths'] = paths
.split(',')
511 def gitBranchExists(branch
):
512 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
513 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
514 return proc
.wait() == 0;
517 def gitConfig(key
, args
= None): # set args to "--bool", for instance
518 if not _gitConfig
.has_key(key
):
521 argsFilter
= "%s " % args
522 cmd
= "git config %s%s" % (argsFilter
, key
)
523 _gitConfig
[key
] = read_pipe(cmd
, ignore_error
=True).strip()
524 return _gitConfig
[key
]
526 def gitConfigList(key
):
527 if not _gitConfig
.has_key(key
):
528 _gitConfig
[key
] = read_pipe("git config --get-all %s" % key
, ignore_error
=True).strip().split(os
.linesep
)
529 return _gitConfig
[key
]
531 def p4BranchesInGit(branchesAreInRemotes
= True):
534 cmdline
= "git rev-parse --symbolic "
535 if branchesAreInRemotes
:
536 cmdline
+= " --remotes"
538 cmdline
+= " --branches"
540 for line
in read_pipe_lines(cmdline
):
543 ## only import to p4/
544 if not line
.startswith('p4/') or line
== "p4/HEAD":
549 branch
= re
.sub ("^p4/", "", line
)
551 branches
[branch
] = parseRevision(line
)
554 def findUpstreamBranchPoint(head
= "HEAD"):
555 branches
= p4BranchesInGit()
556 # map from depot-path to branch name
557 branchByDepotPath
= {}
558 for branch
in branches
.keys():
559 tip
= branches
[branch
]
560 log
= extractLogMessageFromGitCommit(tip
)
561 settings
= extractSettingsGitLog(log
)
562 if settings
.has_key("depot-paths"):
563 paths
= ",".join(settings
["depot-paths"])
564 branchByDepotPath
[paths
] = "remotes/p4/" + branch
568 while parent
< 65535:
569 commit
= head
+ "~%s" % parent
570 log
= extractLogMessageFromGitCommit(commit
)
571 settings
= extractSettingsGitLog(log
)
572 if settings
.has_key("depot-paths"):
573 paths
= ",".join(settings
["depot-paths"])
574 if branchByDepotPath
.has_key(paths
):
575 return [branchByDepotPath
[paths
], settings
]
579 return ["", settings
]
581 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
583 print ("Creating/updating branch(es) in %s based on origin branch(es)"
586 originPrefix
= "origin/p4/"
588 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
590 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
593 headName
= line
[len(originPrefix
):]
594 remoteHead
= localRefPrefix
+ headName
597 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
598 if (not original
.has_key('depot-paths')
599 or not original
.has_key('change')):
603 if not gitBranchExists(remoteHead
):
605 print "creating %s" % remoteHead
608 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
609 if settings
.has_key('change') > 0:
610 if settings
['depot-paths'] == original
['depot-paths']:
611 originP4Change
= int(original
['change'])
612 p4Change
= int(settings
['change'])
613 if originP4Change
> p4Change
:
614 print ("%s (%s) is newer than %s (%s). "
615 "Updating p4 branch from origin."
616 % (originHead
, originP4Change
,
617 remoteHead
, p4Change
))
620 print ("Ignoring: %s was imported from %s while "
621 "%s was imported from %s"
622 % (originHead
, ','.join(original
['depot-paths']),
623 remoteHead
, ','.join(settings
['depot-paths'])))
626 system("git update-ref %s %s" % (remoteHead
, originHead
))
628 def originP4BranchesExist():
629 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
631 def p4ChangesForPaths(depotPaths
, changeRange
):
635 cmd
+= ["%s...%s" % (p
, changeRange
)]
636 output
= p4_read_pipe_lines(cmd
)
640 changeNum
= int(line
.split(" ")[1])
641 changes
[changeNum
] = True
643 changelist
= changes
.keys()
647 def p4PathStartsWith(path
, prefix
):
648 # This method tries to remedy a potential mixed-case issue:
650 # If UserA adds //depot/DirA/file1
651 # and UserB adds //depot/dira/file2
653 # we may or may not have a problem. If you have core.ignorecase=true,
654 # we treat DirA and dira as the same directory
655 ignorecase
= gitConfig("core.ignorecase", "--bool") == "true"
657 return path
.lower().startswith(prefix
.lower())
658 return path
.startswith(prefix
)
661 """Look at the p4 client spec, create a View() object that contains
662 all the mappings, and return it."""
664 specList
= p4CmdList("client -o")
665 if len(specList
) != 1:
666 die('Output from "client -o" is %d lines, expecting 1' %
669 # dictionary of all client parameters
672 # just the keys that start with "View"
673 view_keys
= [ k
for k
in entry
.keys() if k
.startswith("View") ]
678 # append the lines, in order, to the view
679 for view_num
in range(len(view_keys
)):
680 k
= "View%d" % view_num
681 if k
not in view_keys
:
682 die("Expected view key %s missing" % k
)
683 view
.append(entry
[k
])
688 """Grab the client directory."""
690 output
= p4CmdList("client -o")
692 die('Output from "client -o" is %d lines, expecting 1' % len(output
))
695 if "Root" not in entry
:
696 die('Client has no "Root"')
701 # P4 wildcards are not allowed in filenames. P4 complains
702 # if you simply add them, but you can force it with "-f", in
703 # which case it translates them into %xx encoding internally.
705 def wildcard_decode(path
):
706 # Search for and fix just these four characters. Do % last so
707 # that fixing it does not inadvertently create new %-escapes.
708 # Cannot have * in a filename in windows; untested as to
709 # what p4 would do in such a case.
710 if not platform
.system() == "Windows":
711 path
= path
.replace("%2A", "*")
712 path
= path
.replace("%23", "#") \
713 .replace("%40", "@") \
717 def wildcard_encode(path
):
718 # do % first to avoid double-encoding the %s introduced here
719 path
= path
.replace("%", "%25") \
720 .replace("*", "%2A") \
721 .replace("#", "%23") \
725 def wildcard_present(path
):
726 return path
.translate(None, "*#@%") != path
730 self
.usage
= "usage: %prog [options]"
736 self
.userMapFromPerforceServer
= False
737 self
.myP4UserId
= None
741 return self
.myP4UserId
743 results
= p4CmdList("user -o")
745 if r
.has_key('User'):
746 self
.myP4UserId
= r
['User']
748 die("Could not find your p4 user id")
750 def p4UserIsMe(self
, p4User
):
751 # return True if the given p4 user is actually me
753 if not p4User
or p4User
!= me
:
758 def getUserCacheFilename(self
):
759 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
760 return home
+ "/.gitp4-usercache.txt"
762 def getUserMapFromPerforceServer(self
):
763 if self
.userMapFromPerforceServer
:
768 for output
in p4CmdList("users"):
769 if not output
.has_key("User"):
771 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
772 self
.emails
[output
["Email"]] = output
["User"]
776 for (key
, val
) in self
.users
.items():
777 s
+= "%s\t%s\n" % (key
.expandtabs(1), val
.expandtabs(1))
779 open(self
.getUserCacheFilename(), "wb").write(s
)
780 self
.userMapFromPerforceServer
= True
782 def loadUserMapFromCache(self
):
784 self
.userMapFromPerforceServer
= False
786 cache
= open(self
.getUserCacheFilename(), "rb")
787 lines
= cache
.readlines()
790 entry
= line
.strip().split("\t")
791 self
.users
[entry
[0]] = entry
[1]
793 self
.getUserMapFromPerforceServer()
795 class P4Debug(Command
):
797 Command
.__init
__(self
)
799 self
.description
= "A tool to debug the output of p4 -G."
800 self
.needsGit
= False
804 for output
in p4CmdList(args
):
805 print 'Element: %d' % j
810 class P4RollBack(Command
):
812 Command
.__init
__(self
)
814 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
816 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
817 self
.rollbackLocalBranches
= False
822 maxChange
= int(args
[0])
824 if "p4ExitCode" in p4Cmd("changes -m 1"):
825 die("Problems executing p4");
827 if self
.rollbackLocalBranches
:
828 refPrefix
= "refs/heads/"
829 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
831 refPrefix
= "refs/remotes/"
832 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
835 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
837 ref
= refPrefix
+ line
838 log
= extractLogMessageFromGitCommit(ref
)
839 settings
= extractSettingsGitLog(log
)
841 depotPaths
= settings
['depot-paths']
842 change
= settings
['change']
846 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
847 for p
in depotPaths
]))) == 0:
848 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
849 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
852 while change
and int(change
) > maxChange
:
855 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
856 system("git update-ref %s \"%s^\"" % (ref
, ref
))
857 log
= extractLogMessageFromGitCommit(ref
)
858 settings
= extractSettingsGitLog(log
)
861 depotPaths
= settings
['depot-paths']
862 change
= settings
['change']
865 print "%s rewound to %s" % (ref
, change
)
869 class P4Submit(Command
, P4UserMap
):
871 conflict_behavior_choices
= ("ask", "skip", "quit")
874 Command
.__init
__(self
)
875 P4UserMap
.__init
__(self
)
877 optparse
.make_option("--origin", dest
="origin"),
878 optparse
.make_option("-M", dest
="detectRenames", action
="store_true"),
879 # preserve the user, requires relevant p4 permissions
880 optparse
.make_option("--preserve-user", dest
="preserveUser", action
="store_true"),
881 optparse
.make_option("--export-labels", dest
="exportLabels", action
="store_true"),
882 optparse
.make_option("--dry-run", "-n", dest
="dry_run", action
="store_true"),
883 optparse
.make_option("--prepare-p4-only", dest
="prepare_p4_only", action
="store_true"),
884 optparse
.make_option("--conflict", dest
="conflict_behavior",
885 choices
=self
.conflict_behavior_choices
)
887 self
.description
= "Submit changes from git to the perforce depot."
888 self
.usage
+= " [name of git branch to submit into perforce depot]"
890 self
.detectRenames
= False
891 self
.preserveUser
= gitConfig("git-p4.preserveUser").lower() == "true"
893 self
.prepare_p4_only
= False
894 self
.conflict_behavior
= None
895 self
.isWindows
= (platform
.system() == "Windows")
896 self
.exportLabels
= False
897 self
.p4HasMoveCommand
= p4_has_command("move")
900 if len(p4CmdList("opened ...")) > 0:
901 die("You have files opened with perforce! Close them before starting the sync.")
903 def separate_jobs_from_description(self
, message
):
904 """Extract and return a possible Jobs field in the commit
905 message. It goes into a separate section in the p4 change
908 A jobs line starts with "Jobs:" and looks like a new field
909 in a form. Values are white-space separated on the same
910 line or on following lines that start with a tab.
912 This does not parse and extract the full git commit message
913 like a p4 form. It just sees the Jobs: line as a marker
914 to pass everything from then on directly into the p4 form,
915 but outside the description section.
917 Return a tuple (stripped log message, jobs string)."""
919 m
= re
.search(r
'^Jobs:', message
, re
.MULTILINE
)
921 return (message
, None)
923 jobtext
= message
[m
.start():]
924 stripped_message
= message
[:m
.start()].rstrip()
925 return (stripped_message
, jobtext
)
927 def prepareLogMessage(self
, template
, message
, jobs
):
928 """Edits the template returned from "p4 change -o" to insert
929 the message in the Description field, and the jobs text in
933 inDescriptionSection
= False
935 for line
in template
.split("\n"):
936 if line
.startswith("#"):
937 result
+= line
+ "\n"
940 if inDescriptionSection
:
941 if line
.startswith("Files:") or line
.startswith("Jobs:"):
942 inDescriptionSection
= False
943 # insert Jobs section
945 result
+= jobs
+ "\n"
949 if line
.startswith("Description:"):
950 inDescriptionSection
= True
952 for messageLine
in message
.split("\n"):
953 line
+= "\t" + messageLine
+ "\n"
955 result
+= line
+ "\n"
959 def patchRCSKeywords(self
, file, pattern
):
960 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
961 (handle
, outFileName
) = tempfile
.mkstemp(dir='.')
963 outFile
= os
.fdopen(handle
, "w+")
964 inFile
= open(file, "r")
965 regexp
= re
.compile(pattern
, re
.VERBOSE
)
966 for line
in inFile
.readlines():
967 line
= regexp
.sub(r
'$\1$', line
)
971 # Forcibly overwrite the original file
973 shutil
.move(outFileName
, file)
975 # cleanup our temporary file
976 os
.unlink(outFileName
)
977 print "Failed to strip RCS keywords in %s" % file
980 print "Patched up RCS keywords in %s" % file
982 def p4UserForCommit(self
,id):
983 # Return the tuple (perforce user,git email) for a given git commit id
984 self
.getUserMapFromPerforceServer()
985 gitEmail
= read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
986 gitEmail
= gitEmail
.strip()
987 if not self
.emails
.has_key(gitEmail
):
988 return (None,gitEmail
)
990 return (self
.emails
[gitEmail
],gitEmail
)
992 def checkValidP4Users(self
,commits
):
993 # check if any git authors cannot be mapped to p4 users
995 (user
,email
) = self
.p4UserForCommit(id)
997 msg
= "Cannot find p4 user for email %s in commit %s." % (email
, id)
998 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
1001 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg
)
1003 def lastP4Changelist(self
):
1004 # Get back the last changelist number submitted in this client spec. This
1005 # then gets used to patch up the username in the change. If the same
1006 # client spec is being used by multiple processes then this might go
1008 results
= p4CmdList("client -o") # find the current client
1011 if r
.has_key('Client'):
1012 client
= r
['Client']
1015 die("could not get client spec")
1016 results
= p4CmdList(["changes", "-c", client
, "-m", "1"])
1018 if r
.has_key('change'):
1020 die("Could not get changelist number for last submit - cannot patch up user details")
1022 def modifyChangelistUser(self
, changelist
, newUser
):
1023 # fixup the user field of a changelist after it has been submitted.
1024 changes
= p4CmdList("change -o %s" % changelist
)
1025 if len(changes
) != 1:
1026 die("Bad output from p4 change modifying %s to user %s" %
1027 (changelist
, newUser
))
1030 if c
['User'] == newUser
: return # nothing to do
1032 input = marshal
.dumps(c
)
1034 result
= p4CmdList("change -f -i", stdin
=input)
1036 if r
.has_key('code'):
1037 if r
['code'] == 'error':
1038 die("Could not modify user field of changelist %s to %s:%s" % (changelist
, newUser
, r
['data']))
1039 if r
.has_key('data'):
1040 print("Updated user field for changelist %s to %s" % (changelist
, newUser
))
1042 die("Could not modify user field of changelist %s to %s" % (changelist
, newUser
))
1044 def canChangeChangelists(self
):
1045 # check to see if we have p4 admin or super-user permissions, either of
1046 # which are required to modify changelists.
1047 results
= p4CmdList(["protects", self
.depotPath
])
1049 if r
.has_key('perm'):
1050 if r
['perm'] == 'admin':
1052 if r
['perm'] == 'super':
1056 def prepareSubmitTemplate(self
):
1057 """Run "p4 change -o" to grab a change specification template.
1058 This does not use "p4 -G", as it is nice to keep the submission
1059 template in original order, since a human might edit it.
1061 Remove lines in the Files section that show changes to files
1062 outside the depot path we're committing into."""
1065 inFilesSection
= False
1066 for line
in p4_read_pipe_lines(['change', '-o']):
1067 if line
.endswith("\r\n"):
1068 line
= line
[:-2] + "\n"
1070 if line
.startswith("\t"):
1071 # path starts and ends with a tab
1073 lastTab
= path
.rfind("\t")
1075 path
= path
[:lastTab
]
1076 if not p4PathStartsWith(path
, self
.depotPath
):
1079 inFilesSection
= False
1081 if line
.startswith("Files:"):
1082 inFilesSection
= True
1088 def edit_template(self
, template_file
):
1089 """Invoke the editor to let the user change the submission
1090 message. Return true if okay to continue with the submit."""
1092 # if configured to skip the editing part, just submit
1093 if gitConfig("git-p4.skipSubmitEdit") == "true":
1096 # look at the modification time, to check later if the user saved
1098 mtime
= os
.stat(template_file
).st_mtime
1101 if os
.environ
.has_key("P4EDITOR") and (os
.environ
.get("P4EDITOR") != ""):
1102 editor
= os
.environ
.get("P4EDITOR")
1104 editor
= read_pipe("git var GIT_EDITOR").strip()
1105 system(editor
+ " " + template_file
)
1107 # If the file was not saved, prompt to see if this patch should
1108 # be skipped. But skip this verification step if configured so.
1109 if gitConfig("git-p4.skipSubmitEditCheck") == "true":
1112 # modification time updated means user saved the file
1113 if os
.stat(template_file
).st_mtime
> mtime
:
1117 response
= raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1123 def applyCommit(self
, id):
1124 """Apply one commit, return True if it succeeded."""
1126 print "Applying", read_pipe(["git", "show", "-s",
1127 "--format=format:%h %s", id])
1129 (p4User
, gitEmail
) = self
.p4UserForCommit(id)
1131 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self
.diffOpts
, id, id))
1133 filesToDelete
= set()
1135 pureRenameCopy
= set()
1136 filesToChangeExecBit
= {}
1139 diff
= parseDiffTreeEntry(line
)
1140 modifier
= diff
['status']
1144 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
1145 filesToChangeExecBit
[path
] = diff
['dst_mode']
1146 editedFiles
.add(path
)
1147 elif modifier
== "A":
1148 filesToAdd
.add(path
)
1149 filesToChangeExecBit
[path
] = diff
['dst_mode']
1150 if path
in filesToDelete
:
1151 filesToDelete
.remove(path
)
1152 elif modifier
== "D":
1153 filesToDelete
.add(path
)
1154 if path
in filesToAdd
:
1155 filesToAdd
.remove(path
)
1156 elif modifier
== "C":
1157 src
, dest
= diff
['src'], diff
['dst']
1158 p4_integrate(src
, dest
)
1159 pureRenameCopy
.add(dest
)
1160 if diff
['src_sha1'] != diff
['dst_sha1']:
1162 pureRenameCopy
.discard(dest
)
1163 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
1165 pureRenameCopy
.discard(dest
)
1166 filesToChangeExecBit
[dest
] = diff
['dst_mode']
1168 editedFiles
.add(dest
)
1169 elif modifier
== "R":
1170 src
, dest
= diff
['src'], diff
['dst']
1171 if self
.p4HasMoveCommand
:
1172 p4_edit(src
) # src must be open before move
1173 p4_move(src
, dest
) # opens for (move/delete, move/add)
1175 p4_integrate(src
, dest
)
1176 if diff
['src_sha1'] != diff
['dst_sha1']:
1179 pureRenameCopy
.add(dest
)
1180 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
1181 if not self
.p4HasMoveCommand
:
1182 p4_edit(dest
) # with move: already open, writable
1183 filesToChangeExecBit
[dest
] = diff
['dst_mode']
1184 if not self
.p4HasMoveCommand
:
1186 filesToDelete
.add(src
)
1187 editedFiles
.add(dest
)
1189 die("unknown modifier %s for %s" % (modifier
, path
))
1191 diffcmd
= "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
1192 patchcmd
= diffcmd
+ " | git apply "
1193 tryPatchCmd
= patchcmd
+ "--check -"
1194 applyPatchCmd
= patchcmd
+ "--check --apply -"
1195 patch_succeeded
= True
1197 if os
.system(tryPatchCmd
) != 0:
1198 fixed_rcs_keywords
= False
1199 patch_succeeded
= False
1200 print "Unfortunately applying the change failed!"
1202 # Patch failed, maybe it's just RCS keyword woes. Look through
1203 # the patch to see if that's possible.
1204 if gitConfig("git-p4.attemptRCSCleanup","--bool") == "true":
1208 for file in editedFiles | filesToDelete
:
1209 # did this file's delta contain RCS keywords?
1210 pattern
= p4_keywords_regexp_for_file(file)
1213 # this file is a possibility...look for RCS keywords.
1214 regexp
= re
.compile(pattern
, re
.VERBOSE
)
1215 for line
in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1216 if regexp
.search(line
):
1218 print "got keyword match on %s in %s in %s" % (pattern
, line
, file)
1219 kwfiles
[file] = pattern
1222 for file in kwfiles
:
1224 print "zapping %s with %s" % (line
,pattern
)
1225 self
.patchRCSKeywords(file, kwfiles
[file])
1226 fixed_rcs_keywords
= True
1228 if fixed_rcs_keywords
:
1229 print "Retrying the patch with RCS keywords cleaned up"
1230 if os
.system(tryPatchCmd
) == 0:
1231 patch_succeeded
= True
1233 if not patch_succeeded
:
1234 for f
in editedFiles
:
1239 # Apply the patch for real, and do add/delete/+x handling.
1241 system(applyPatchCmd
)
1243 for f
in filesToAdd
:
1245 for f
in filesToDelete
:
1249 # Set/clear executable bits
1250 for f
in filesToChangeExecBit
.keys():
1251 mode
= filesToChangeExecBit
[f
]
1252 setP4ExecBit(f
, mode
)
1255 # Build p4 change description, starting with the contents
1256 # of the git commit message.
1258 logMessage
= extractLogMessageFromGitCommit(id)
1259 logMessage
= logMessage
.strip()
1260 (logMessage
, jobs
) = self
.separate_jobs_from_description(logMessage
)
1262 template
= self
.prepareSubmitTemplate()
1263 submitTemplate
= self
.prepareLogMessage(template
, logMessage
, jobs
)
1265 if self
.preserveUser
:
1266 submitTemplate
+= "\n######## Actual user %s, modified after commit\n" % p4User
1268 if self
.checkAuthorship
and not self
.p4UserIsMe(p4User
):
1269 submitTemplate
+= "######## git author %s does not match your p4 account.\n" % gitEmail
1270 submitTemplate
+= "######## Use option --preserve-user to modify authorship.\n"
1271 submitTemplate
+= "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1273 separatorLine
= "######## everything below this line is just the diff #######\n"
1276 if os
.environ
.has_key("P4DIFF"):
1277 del(os
.environ
["P4DIFF"])
1279 for editedFile
in editedFiles
:
1280 diff
+= p4_read_pipe(['diff', '-du',
1281 wildcard_encode(editedFile
)])
1285 for newFile
in filesToAdd
:
1286 newdiff
+= "==== new file ====\n"
1287 newdiff
+= "--- /dev/null\n"
1288 newdiff
+= "+++ %s\n" % newFile
1289 f
= open(newFile
, "r")
1290 for line
in f
.readlines():
1291 newdiff
+= "+" + line
1294 # change description file: submitTemplate, separatorLine, diff, newdiff
1295 (handle
, fileName
) = tempfile
.mkstemp()
1296 tmpFile
= os
.fdopen(handle
, "w+")
1298 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
1299 separatorLine
= separatorLine
.replace("\n", "\r\n")
1300 newdiff
= newdiff
.replace("\n", "\r\n")
1301 tmpFile
.write(submitTemplate
+ separatorLine
+ diff
+ newdiff
)
1304 if self
.prepare_p4_only
:
1306 # Leave the p4 tree prepared, and the submit template around
1307 # and let the user decide what to do next
1310 print "P4 workspace prepared for submission."
1311 print "To submit or revert, go to client workspace"
1312 print " " + self
.clientPath
1314 print "To submit, use \"p4 submit\" to write a new description,"
1315 print "or \"p4 submit -i %s\" to use the one prepared by" \
1316 " \"git p4\"." % fileName
1317 print "You can delete the file \"%s\" when finished." % fileName
1319 if self
.preserveUser
and p4User
and not self
.p4UserIsMe(p4User
):
1320 print "To preserve change ownership by user %s, you must\n" \
1321 "do \"p4 change -f <change>\" after submitting and\n" \
1322 "edit the User field."
1324 print "After submitting, renamed files must be re-synced."
1325 print "Invoke \"p4 sync -f\" on each of these files:"
1326 for f
in pureRenameCopy
:
1330 print "To revert the changes, use \"p4 revert ...\", and delete"
1331 print "the submit template file \"%s\"" % fileName
1333 print "Since the commit adds new files, they must be deleted:"
1334 for f
in filesToAdd
:
1340 # Let the user edit the change description, then submit it.
1342 if self
.edit_template(fileName
):
1343 # read the edited message and submit
1345 tmpFile
= open(fileName
, "rb")
1346 message
= tmpFile
.read()
1348 submitTemplate
= message
[:message
.index(separatorLine
)]
1350 submitTemplate
= submitTemplate
.replace("\r\n", "\n")
1351 p4_write_pipe(['submit', '-i'], submitTemplate
)
1353 if self
.preserveUser
:
1355 # Get last changelist number. Cannot easily get it from
1356 # the submit command output as the output is
1358 changelist
= self
.lastP4Changelist()
1359 self
.modifyChangelistUser(changelist
, p4User
)
1361 # The rename/copy happened by applying a patch that created a
1362 # new file. This leaves it writable, which confuses p4.
1363 for f
in pureRenameCopy
:
1369 print "Submission cancelled, undoing p4 changes."
1370 for f
in editedFiles
:
1372 for f
in filesToAdd
:
1375 for f
in filesToDelete
:
1381 # Export git tags as p4 labels. Create a p4 label and then tag
1383 def exportGitTags(self
, gitTags
):
1384 validLabelRegexp
= gitConfig("git-p4.labelExportRegexp")
1385 if len(validLabelRegexp
) == 0:
1386 validLabelRegexp
= defaultLabelRegexp
1387 m
= re
.compile(validLabelRegexp
)
1389 for name
in gitTags
:
1391 if not m
.match(name
):
1393 print "tag %s does not match regexp %s" % (name
, validLabelRegexp
)
1396 # Get the p4 commit this corresponds to
1397 logMessage
= extractLogMessageFromGitCommit(name
)
1398 values
= extractSettingsGitLog(logMessage
)
1400 if not values
.has_key('change'):
1401 # a tag pointing to something not sent to p4; ignore
1403 print "git tag %s does not give a p4 commit" % name
1406 changelist
= values
['change']
1408 # Get the tag details.
1412 for l
in read_pipe_lines(["git", "cat-file", "-p", name
]):
1415 if re
.match(r
'tag\s+', l
):
1417 elif re
.match(r
'\s*$', l
):
1424 body
= ["lightweight tag imported by git p4\n"]
1426 # Create the label - use the same view as the client spec we are using
1427 clientSpec
= getClientSpec()
1429 labelTemplate
= "Label: %s\n" % name
1430 labelTemplate
+= "Description:\n"
1432 labelTemplate
+= "\t" + b
+ "\n"
1433 labelTemplate
+= "View:\n"
1434 for mapping
in clientSpec
.mappings
:
1435 labelTemplate
+= "\t%s\n" % mapping
.depot_side
.path
1438 print "Would create p4 label %s for tag" % name
1439 elif self
.prepare_p4_only
:
1440 print "Not creating p4 label %s for tag due to option" \
1441 " --prepare-p4-only" % name
1443 p4_write_pipe(["label", "-i"], labelTemplate
)
1446 p4_system(["tag", "-l", name
] +
1447 ["%s@%s" % (mapping
.depot_side
.path
, changelist
) for mapping
in clientSpec
.mappings
])
1450 print "created p4 label for tag %s" % name
1452 def run(self
, args
):
1454 self
.master
= currentGitBranch()
1455 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
1456 die("Detecting current git branch failed!")
1457 elif len(args
) == 1:
1458 self
.master
= args
[0]
1459 if not branchExists(self
.master
):
1460 die("Branch %s does not exist" % self
.master
)
1464 allowSubmit
= gitConfig("git-p4.allowSubmit")
1465 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
1466 die("%s is not in git-p4.allowSubmit" % self
.master
)
1468 [upstream
, settings
] = findUpstreamBranchPoint()
1469 self
.depotPath
= settings
['depot-paths'][0]
1470 if len(self
.origin
) == 0:
1471 self
.origin
= upstream
1473 if self
.preserveUser
:
1474 if not self
.canChangeChangelists():
1475 die("Cannot preserve user names without p4 super-user or admin permissions")
1477 # if not set from the command line, try the config file
1478 if self
.conflict_behavior
is None:
1479 val
= gitConfig("git-p4.conflict")
1481 if val
not in self
.conflict_behavior_choices
:
1482 die("Invalid value '%s' for config git-p4.conflict" % val
)
1485 self
.conflict_behavior
= val
1488 print "Origin branch is " + self
.origin
1490 if len(self
.depotPath
) == 0:
1491 print "Internal error: cannot locate perforce depot path from existing branches"
1494 self
.useClientSpec
= False
1495 if gitConfig("git-p4.useclientspec", "--bool") == "true":
1496 self
.useClientSpec
= True
1497 if self
.useClientSpec
:
1498 self
.clientSpecDirs
= getClientSpec()
1500 if self
.useClientSpec
:
1501 # all files are relative to the client spec
1502 self
.clientPath
= getClientRoot()
1504 self
.clientPath
= p4Where(self
.depotPath
)
1506 if self
.clientPath
== "":
1507 die("Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
)
1509 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
1510 self
.oldWorkingDirectory
= os
.getcwd()
1512 # ensure the clientPath exists
1513 new_client_dir
= False
1514 if not os
.path
.exists(self
.clientPath
):
1515 new_client_dir
= True
1516 os
.makedirs(self
.clientPath
)
1518 chdir(self
.clientPath
)
1520 print "Would synchronize p4 checkout in %s" % self
.clientPath
1522 print "Synchronizing p4 checkout..."
1524 # old one was destroyed, and maybe nobody told p4
1525 p4_sync("...", "-f")
1531 for line
in read_pipe_lines("git rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)):
1532 commits
.append(line
.strip())
1535 if self
.preserveUser
or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1536 self
.checkAuthorship
= False
1538 self
.checkAuthorship
= True
1540 if self
.preserveUser
:
1541 self
.checkValidP4Users(commits
)
1544 # Build up a set of options to be passed to diff when
1545 # submitting each commit to p4.
1547 if self
.detectRenames
:
1548 # command-line -M arg
1549 self
.diffOpts
= "-M"
1551 # If not explicitly set check the config variable
1552 detectRenames
= gitConfig("git-p4.detectRenames")
1554 if detectRenames
.lower() == "false" or detectRenames
== "":
1556 elif detectRenames
.lower() == "true":
1557 self
.diffOpts
= "-M"
1559 self
.diffOpts
= "-M%s" % detectRenames
1561 # no command-line arg for -C or --find-copies-harder, just
1563 detectCopies
= gitConfig("git-p4.detectCopies")
1564 if detectCopies
.lower() == "false" or detectCopies
== "":
1566 elif detectCopies
.lower() == "true":
1567 self
.diffOpts
+= " -C"
1569 self
.diffOpts
+= " -C%s" % detectCopies
1571 if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
1572 self
.diffOpts
+= " --find-copies-harder"
1575 # Apply the commits, one at a time. On failure, ask if should
1576 # continue to try the rest of the patches, or quit.
1581 last
= len(commits
) - 1
1582 for i
, commit
in enumerate(commits
):
1584 print " ", read_pipe(["git", "show", "-s",
1585 "--format=format:%h %s", commit
])
1588 ok
= self
.applyCommit(commit
)
1590 applied
.append(commit
)
1592 if self
.prepare_p4_only
and i
< last
:
1593 print "Processing only the first commit due to option" \
1594 " --prepare-p4-only"
1599 # prompt for what to do, or use the option/variable
1600 if self
.conflict_behavior
== "ask":
1601 print "What do you want to do?"
1602 response
= raw_input("[s]kip this commit but apply"
1603 " the rest, or [q]uit? ")
1606 elif self
.conflict_behavior
== "skip":
1608 elif self
.conflict_behavior
== "quit":
1611 die("Unknown conflict_behavior '%s'" %
1612 self
.conflict_behavior
)
1614 if response
[0] == "s":
1615 print "Skipping this commit, but applying the rest"
1617 if response
[0] == "q":
1624 chdir(self
.oldWorkingDirectory
)
1628 elif self
.prepare_p4_only
:
1630 elif len(commits
) == len(applied
):
1631 print "All commits applied!"
1640 if len(applied
) == 0:
1641 print "No commits applied."
1643 print "Applied only the commits marked with '*':"
1649 print star
, read_pipe(["git", "show", "-s",
1650 "--format=format:%h %s", c
])
1651 print "You will have to do 'git p4 sync' and rebase."
1653 if gitConfig("git-p4.exportLabels", "--bool") == "true":
1654 self
.exportLabels
= True
1656 if self
.exportLabels
:
1657 p4Labels
= getP4Labels(self
.depotPath
)
1658 gitTags
= getGitTags()
1660 missingGitTags
= gitTags
- p4Labels
1661 self
.exportGitTags(missingGitTags
)
1663 # exit with error unless everything applied perfecly
1664 if len(commits
) != len(applied
):
1670 """Represent a p4 view ("p4 help views"), and map files in a
1671 repo according to the view."""
1674 """A depot or client path, possibly containing wildcards.
1675 The only one supported is ... at the end, currently.
1676 Initialize with the full path, with //depot or //client."""
1678 def __init__(self
, path
, is_depot
):
1680 self
.is_depot
= is_depot
1681 self
.find_wildcards()
1682 # remember the prefix bit, useful for relative mappings
1683 m
= re
.match("(//[^/]+/)", self
.path
)
1685 die("Path %s does not start with //prefix/" % self
.path
)
1687 if not self
.is_depot
:
1688 # strip //client/ on client paths
1689 self
.path
= self
.path
[len(prefix
):]
1691 def find_wildcards(self
):
1692 """Make sure wildcards are valid, and set up internal
1695 self
.ends_triple_dot
= False
1696 # There are three wildcards allowed in p4 views
1697 # (see "p4 help views"). This code knows how to
1698 # handle "..." (only at the end), but cannot deal with
1699 # "%%n" or "*". Only check the depot_side, as p4 should
1700 # validate that the client_side matches too.
1701 if re
.search(r
'%%[1-9]', self
.path
):
1702 die("Can't handle %%n wildcards in view: %s" % self
.path
)
1703 if self
.path
.find("*") >= 0:
1704 die("Can't handle * wildcards in view: %s" % self
.path
)
1705 triple_dot_index
= self
.path
.find("...")
1706 if triple_dot_index
>= 0:
1707 if triple_dot_index
!= len(self
.path
) - 3:
1708 die("Can handle only single ... wildcard, at end: %s" %
1710 self
.ends_triple_dot
= True
1712 def ensure_compatible(self
, other_path
):
1713 """Make sure the wildcards agree."""
1714 if self
.ends_triple_dot
!= other_path
.ends_triple_dot
:
1715 die("Both paths must end with ... if either does;\n" +
1716 "paths: %s %s" % (self
.path
, other_path
.path
))
1718 def match_wildcards(self
, test_path
):
1719 """See if this test_path matches us, and fill in the value
1720 of the wildcards if so. Returns a tuple of
1721 (True|False, wildcards[]). For now, only the ... at end
1722 is supported, so at most one wildcard."""
1723 if self
.ends_triple_dot
:
1724 dotless
= self
.path
[:-3]
1725 if test_path
.startswith(dotless
):
1726 wildcard
= test_path
[len(dotless
):]
1727 return (True, [ wildcard
])
1729 if test_path
== self
.path
:
1733 def match(self
, test_path
):
1734 """Just return if it matches; don't bother with the wildcards."""
1735 b
, _
= self
.match_wildcards(test_path
)
1738 def fill_in_wildcards(self
, wildcards
):
1739 """Return the relative path, with the wildcards filled in
1740 if there are any."""
1741 if self
.ends_triple_dot
:
1742 return self
.path
[:-3] + wildcards
[0]
1746 class Mapping(object):
1747 def __init__(self
, depot_side
, client_side
, overlay
, exclude
):
1748 # depot_side is without the trailing /... if it had one
1749 self
.depot_side
= View
.Path(depot_side
, is_depot
=True)
1750 self
.client_side
= View
.Path(client_side
, is_depot
=False)
1751 self
.overlay
= overlay
# started with "+"
1752 self
.exclude
= exclude
# started with "-"
1753 assert not (self
.overlay
and self
.exclude
)
1754 self
.depot_side
.ensure_compatible(self
.client_side
)
1762 return "View.Mapping: %s%s -> %s" % \
1763 (c
, self
.depot_side
.path
, self
.client_side
.path
)
1765 def map_depot_to_client(self
, depot_path
):
1766 """Calculate the client path if using this mapping on the
1767 given depot path; does not consider the effect of other
1768 mappings in a view. Even excluded mappings are returned."""
1769 matches
, wildcards
= self
.depot_side
.match_wildcards(depot_path
)
1772 client_path
= self
.client_side
.fill_in_wildcards(wildcards
)
1781 def append(self
, view_line
):
1782 """Parse a view line, splitting it into depot and client
1783 sides. Append to self.mappings, preserving order."""
1785 # Split the view line into exactly two words. P4 enforces
1786 # structure on these lines that simplifies this quite a bit.
1788 # Either or both words may be double-quoted.
1789 # Single quotes do not matter.
1790 # Double-quote marks cannot occur inside the words.
1791 # A + or - prefix is also inside the quotes.
1792 # There are no quotes unless they contain a space.
1793 # The line is already white-space stripped.
1794 # The two words are separated by a single space.
1796 if view_line
[0] == '"':
1797 # First word is double quoted. Find its end.
1798 close_quote_index
= view_line
.find('"', 1)
1799 if close_quote_index
<= 0:
1800 die("No first-word closing quote found: %s" % view_line
)
1801 depot_side
= view_line
[1:close_quote_index
]
1802 # skip closing quote and space
1803 rhs_index
= close_quote_index
+ 1 + 1
1805 space_index
= view_line
.find(" ")
1806 if space_index
<= 0:
1807 die("No word-splitting space found: %s" % view_line
)
1808 depot_side
= view_line
[0:space_index
]
1809 rhs_index
= space_index
+ 1
1811 if view_line
[rhs_index
] == '"':
1812 # Second word is double quoted. Make sure there is a
1813 # double quote at the end too.
1814 if not view_line
.endswith('"'):
1815 die("View line with rhs quote should end with one: %s" %
1818 client_side
= view_line
[rhs_index
+1:-1]
1820 client_side
= view_line
[rhs_index
:]
1822 # prefix + means overlay on previous mapping
1824 if depot_side
.startswith("+"):
1826 depot_side
= depot_side
[1:]
1828 # prefix - means exclude this path
1830 if depot_side
.startswith("-"):
1832 depot_side
= depot_side
[1:]
1834 m
= View
.Mapping(depot_side
, client_side
, overlay
, exclude
)
1835 self
.mappings
.append(m
)
1837 def map_in_client(self
, depot_path
):
1838 """Return the relative location in the client where this
1839 depot file should live. Returns "" if the file should
1840 not be mapped in the client."""
1845 # look at later entries first
1846 for m
in self
.mappings
[::-1]:
1848 # see where will this path end up in the client
1849 p
= m
.map_depot_to_client(depot_path
)
1852 # Depot path does not belong in client. Must remember
1853 # this, as previous items should not cause files to
1854 # exist in this path either. Remember that the list is
1855 # being walked from the end, which has higher precedence.
1856 # Overlap mappings do not exclude previous mappings.
1858 paths_filled
.append(m
.client_side
)
1861 # This mapping matched; no need to search any further.
1862 # But, the mapping could be rejected if the client path
1863 # has already been claimed by an earlier mapping (i.e.
1864 # one later in the list, which we are walking backwards).
1865 already_mapped_in_client
= False
1866 for f
in paths_filled
:
1867 # this is View.Path.match
1869 already_mapped_in_client
= True
1871 if not already_mapped_in_client
:
1872 # Include this file, unless it is from a line that
1873 # explicitly said to exclude it.
1877 # a match, even if rejected, always stops the search
1882 class P4Sync(Command
, P4UserMap
):
1883 delete_actions
= ( "delete", "move/delete", "purge" )
1886 Command
.__init
__(self
)
1887 P4UserMap
.__init
__(self
)
1889 optparse
.make_option("--branch", dest
="branch"),
1890 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
1891 optparse
.make_option("--changesfile", dest
="changesFile"),
1892 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
1893 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
1894 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
1895 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
1896 help="Import into refs/heads/ , not refs/remotes"),
1897 optparse
.make_option("--max-changes", dest
="maxChanges"),
1898 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
1899 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1900 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
1901 help="Only sync files that are included in the Perforce Client Spec")
1903 self
.description
= """Imports from Perforce into a git repository.\n
1905 //depot/my/project/ -- to import the current head
1906 //depot/my/project/@all -- to import everything
1907 //depot/my/project/@1,6 -- to import only from revision 1 to 6
1909 (a ... is not needed in the path p4 specification, it's added implicitly)"""
1911 self
.usage
+= " //depot/path[@revRange]"
1913 self
.createdBranches
= set()
1914 self
.committedChanges
= set()
1916 self
.detectBranches
= False
1917 self
.detectLabels
= False
1918 self
.importLabels
= False
1919 self
.changesFile
= ""
1920 self
.syncWithOrigin
= True
1921 self
.importIntoRemotes
= True
1922 self
.maxChanges
= ""
1923 self
.isWindows
= (platform
.system() == "Windows")
1924 self
.keepRepoPath
= False
1925 self
.depotPaths
= None
1926 self
.p4BranchesInGit
= []
1927 self
.cloneExclude
= []
1928 self
.useClientSpec
= False
1929 self
.useClientSpec_from_options
= False
1930 self
.clientSpecDirs
= None
1931 self
.tempBranches
= []
1932 self
.tempBranchLocation
= "git-p4-tmp"
1934 if gitConfig("git-p4.syncFromOrigin") == "false":
1935 self
.syncWithOrigin
= False
1937 # Force a checkpoint in fast-import and wait for it to finish
1938 def checkpoint(self
):
1939 self
.gitStream
.write("checkpoint\n\n")
1940 self
.gitStream
.write("progress checkpoint\n\n")
1941 out
= self
.gitOutput
.readline()
1943 print "checkpoint finished: " + out
1945 def extractFilesFromCommit(self
, commit
):
1946 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
1947 for path
in self
.cloneExclude
]
1950 while commit
.has_key("depotFile%s" % fnum
):
1951 path
= commit
["depotFile%s" % fnum
]
1953 if [p
for p
in self
.cloneExclude
1954 if p4PathStartsWith(path
, p
)]:
1957 found
= [p
for p
in self
.depotPaths
1958 if p4PathStartsWith(path
, p
)]
1965 file["rev"] = commit
["rev%s" % fnum
]
1966 file["action"] = commit
["action%s" % fnum
]
1967 file["type"] = commit
["type%s" % fnum
]
1972 def stripRepoPath(self
, path
, prefixes
):
1973 """When streaming files, this is called to map a p4 depot path
1974 to where it should go in git. The prefixes are either
1975 self.depotPaths, or self.branchPrefixes in the case of
1976 branch detection."""
1978 if self
.useClientSpec
:
1979 # branch detection moves files up a level (the branch name)
1980 # from what client spec interpretation gives
1981 path
= self
.clientSpecDirs
.map_in_client(path
)
1982 if self
.detectBranches
:
1983 for b
in self
.knownBranches
:
1984 if path
.startswith(b
+ "/"):
1985 path
= path
[len(b
)+1:]
1987 elif self
.keepRepoPath
:
1988 # Preserve everything in relative path name except leading
1989 # //depot/; just look at first prefix as they all should
1990 # be in the same depot.
1991 depot
= re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])
1992 if p4PathStartsWith(path
, depot
):
1993 path
= path
[len(depot
):]
1997 if p4PathStartsWith(path
, p
):
1998 path
= path
[len(p
):]
2001 path
= wildcard_decode(path
)
2004 def splitFilesIntoBranches(self
, commit
):
2005 """Look at each depotFile in the commit to figure out to what
2006 branch it belongs."""
2010 while commit
.has_key("depotFile%s" % fnum
):
2011 path
= commit
["depotFile%s" % fnum
]
2012 found
= [p
for p
in self
.depotPaths
2013 if p4PathStartsWith(path
, p
)]
2020 file["rev"] = commit
["rev%s" % fnum
]
2021 file["action"] = commit
["action%s" % fnum
]
2022 file["type"] = commit
["type%s" % fnum
]
2025 # start with the full relative path where this file would
2027 if self
.useClientSpec
:
2028 relPath
= self
.clientSpecDirs
.map_in_client(path
)
2030 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
2032 for branch
in self
.knownBranches
.keys():
2033 # add a trailing slash so that a commit into qt/4.2foo
2034 # doesn't end up in qt/4.2, e.g.
2035 if relPath
.startswith(branch
+ "/"):
2036 if branch
not in branches
:
2037 branches
[branch
] = []
2038 branches
[branch
].append(file)
2043 # output one file from the P4 stream
2044 # - helper for streamP4Files
2046 def streamOneP4File(self
, file, contents
):
2047 relPath
= self
.stripRepoPath(file['depotFile'], self
.branchPrefixes
)
2049 sys
.stderr
.write("%s\n" % relPath
)
2051 (type_base
, type_mods
) = split_p4_type(file["type"])
2054 if "x" in type_mods
:
2056 if type_base
== "symlink":
2058 # p4 print on a symlink contains "target\n"; remove the newline
2059 data
= ''.join(contents
)
2060 contents
= [data
[:-1]]
2062 if type_base
== "utf16":
2063 # p4 delivers different text in the python output to -G
2064 # than it does when using "print -o", or normal p4 client
2065 # operations. utf16 is converted to ascii or utf8, perhaps.
2066 # But ascii text saved as -t utf16 is completely mangled.
2067 # Invoke print -o to get the real contents.
2068 text
= p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
2071 if type_base
== "apple":
2072 # Apple filetype files will be streamed as a concatenation of
2073 # its appledouble header and the contents. This is useless
2074 # on both macs and non-macs. If using "print -q -o xx", it
2075 # will create "xx" with the data, and "%xx" with the header.
2076 # This is also not very useful.
2078 # Ideally, someday, this script can learn how to generate
2079 # appledouble files directly and import those to git, but
2080 # non-mac machines can never find a use for apple filetype.
2081 print "\nIgnoring apple filetype file %s" % file['depotFile']
2084 # Perhaps windows wants unicode, utf16 newlines translated too;
2085 # but this is not doing it.
2086 if self
.isWindows
and type_base
== "text":
2088 for data
in contents
:
2089 data
= data
.replace("\r\n", "\n")
2090 mangled
.append(data
)
2093 # Note that we do not try to de-mangle keywords on utf16 files,
2094 # even though in theory somebody may want that.
2095 pattern
= p4_keywords_regexp_for_type(type_base
, type_mods
)
2097 regexp
= re
.compile(pattern
, re
.VERBOSE
)
2098 text
= ''.join(contents
)
2099 text
= regexp
.sub(r
'$\1$', text
)
2102 self
.gitStream
.write("M %s inline %s\n" % (git_mode
, relPath
))
2107 length
= length
+ len(d
)
2109 self
.gitStream
.write("data %d\n" % length
)
2111 self
.gitStream
.write(d
)
2112 self
.gitStream
.write("\n")
2114 def streamOneP4Deletion(self
, file):
2115 relPath
= self
.stripRepoPath(file['path'], self
.branchPrefixes
)
2117 sys
.stderr
.write("delete %s\n" % relPath
)
2118 self
.gitStream
.write("D %s\n" % relPath
)
2120 # handle another chunk of streaming data
2121 def streamP4FilesCb(self
, marshalled
):
2123 if marshalled
.has_key('depotFile') and self
.stream_have_file_info
:
2124 # start of a new file - output the old one first
2125 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
2126 self
.stream_file
= {}
2127 self
.stream_contents
= []
2128 self
.stream_have_file_info
= False
2130 # pick up the new file information... for the
2131 # 'data' field we need to append to our array
2132 for k
in marshalled
.keys():
2134 self
.stream_contents
.append(marshalled
['data'])
2136 self
.stream_file
[k
] = marshalled
[k
]
2138 self
.stream_have_file_info
= True
2140 # Stream directly from "p4 files" into "git fast-import"
2141 def streamP4Files(self
, files
):
2147 # if using a client spec, only add the files that have
2148 # a path in the client
2149 if self
.clientSpecDirs
:
2150 if self
.clientSpecDirs
.map_in_client(f
['path']) == "":
2153 filesForCommit
.append(f
)
2154 if f
['action'] in self
.delete_actions
:
2155 filesToDelete
.append(f
)
2157 filesToRead
.append(f
)
2160 for f
in filesToDelete
:
2161 self
.streamOneP4Deletion(f
)
2163 if len(filesToRead
) > 0:
2164 self
.stream_file
= {}
2165 self
.stream_contents
= []
2166 self
.stream_have_file_info
= False
2168 # curry self argument
2169 def streamP4FilesCbSelf(entry
):
2170 self
.streamP4FilesCb(entry
)
2172 fileArgs
= ['%s#%s' % (f
['path'], f
['rev']) for f
in filesToRead
]
2174 p4CmdList(["-x", "-", "print"],
2176 cb
=streamP4FilesCbSelf
)
2179 if self
.stream_file
.has_key('depotFile'):
2180 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
2182 def make_email(self
, userid
):
2183 if userid
in self
.users
:
2184 return self
.users
[userid
]
2186 return "%s <a@b>" % userid
2189 def streamTag(self
, gitStream
, labelName
, labelDetails
, commit
, epoch
):
2191 print "writing tag %s for commit %s" % (labelName
, commit
)
2192 gitStream
.write("tag %s\n" % labelName
)
2193 gitStream
.write("from %s\n" % commit
)
2195 if labelDetails
.has_key('Owner'):
2196 owner
= labelDetails
["Owner"]
2200 # Try to use the owner of the p4 label, or failing that,
2201 # the current p4 user id.
2203 email
= self
.make_email(owner
)
2205 email
= self
.make_email(self
.p4UserId())
2206 tagger
= "%s %s %s" % (email
, epoch
, self
.tz
)
2208 gitStream
.write("tagger %s\n" % tagger
)
2210 print "labelDetails=",labelDetails
2211 if labelDetails
.has_key('Description'):
2212 description
= labelDetails
['Description']
2214 description
= 'Label from git p4'
2216 gitStream
.write("data %d\n" % len(description
))
2217 gitStream
.write(description
)
2218 gitStream
.write("\n")
2220 def commit(self
, details
, files
, branch
, parent
= ""):
2221 epoch
= details
["time"]
2222 author
= details
["user"]
2225 print "commit into %s" % branch
2227 # start with reading files; if that fails, we should not
2231 if [p
for p
in self
.branchPrefixes
if p4PathStartsWith(f
['path'], p
)]:
2232 new_files
.append (f
)
2234 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % f
['path'])
2236 self
.gitStream
.write("commit %s\n" % branch
)
2237 # gitStream.write("mark :%s\n" % details["change"])
2238 self
.committedChanges
.add(int(details
["change"]))
2240 if author
not in self
.users
:
2241 self
.getUserMapFromPerforceServer()
2242 committer
= "%s %s %s" % (self
.make_email(author
), epoch
, self
.tz
)
2244 self
.gitStream
.write("committer %s\n" % committer
)
2246 self
.gitStream
.write("data <<EOT\n")
2247 self
.gitStream
.write(details
["desc"])
2248 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2249 (','.join(self
.branchPrefixes
), details
["change"]))
2250 if len(details
['options']) > 0:
2251 self
.gitStream
.write(": options = %s" % details
['options'])
2252 self
.gitStream
.write("]\nEOT\n\n")
2256 print "parent %s" % parent
2257 self
.gitStream
.write("from %s\n" % parent
)
2259 self
.streamP4Files(new_files
)
2260 self
.gitStream
.write("\n")
2262 change
= int(details
["change"])
2264 if self
.labels
.has_key(change
):
2265 label
= self
.labels
[change
]
2266 labelDetails
= label
[0]
2267 labelRevisions
= label
[1]
2269 print "Change %s is labelled %s" % (change
, labelDetails
)
2271 files
= p4CmdList(["files"] + ["%s...@%s" % (p
, change
)
2272 for p
in self
.branchPrefixes
])
2274 if len(files
) == len(labelRevisions
):
2278 if info
["action"] in self
.delete_actions
:
2280 cleanedFiles
[info
["depotFile"]] = info
["rev"]
2282 if cleanedFiles
== labelRevisions
:
2283 self
.streamTag(self
.gitStream
, 'tag_%s' % labelDetails
['label'], labelDetails
, branch
, epoch
)
2287 print ("Tag %s does not match with change %s: files do not match."
2288 % (labelDetails
["label"], change
))
2292 print ("Tag %s does not match with change %s: file count is different."
2293 % (labelDetails
["label"], change
))
2295 # Build a dictionary of changelists and labels, for "detect-labels" option.
2296 def getLabels(self
):
2299 l
= p4CmdList(["labels"] + ["%s..." % p
for p
in self
.depotPaths
])
2300 if len(l
) > 0 and not self
.silent
:
2301 print "Finding files belonging to labels in %s" % `self
.depotPaths`
2304 label
= output
["label"]
2308 print "Querying files for label %s" % label
2309 for file in p4CmdList(["files"] +
2310 ["%s...@%s" % (p
, label
)
2311 for p
in self
.depotPaths
]):
2312 revisions
[file["depotFile"]] = file["rev"]
2313 change
= int(file["change"])
2314 if change
> newestChange
:
2315 newestChange
= change
2317 self
.labels
[newestChange
] = [output
, revisions
]
2320 print "Label changes: %s" % self
.labels
.keys()
2322 # Import p4 labels as git tags. A direct mapping does not
2323 # exist, so assume that if all the files are at the same revision
2324 # then we can use that, or it's something more complicated we should
2326 def importP4Labels(self
, stream
, p4Labels
):
2328 print "import p4 labels: " + ' '.join(p4Labels
)
2330 ignoredP4Labels
= gitConfigList("git-p4.ignoredP4Labels")
2331 validLabelRegexp
= gitConfig("git-p4.labelImportRegexp")
2332 if len(validLabelRegexp
) == 0:
2333 validLabelRegexp
= defaultLabelRegexp
2334 m
= re
.compile(validLabelRegexp
)
2336 for name
in p4Labels
:
2339 if not m
.match(name
):
2341 print "label %s does not match regexp %s" % (name
,validLabelRegexp
)
2344 if name
in ignoredP4Labels
:
2347 labelDetails
= p4CmdList(['label', "-o", name
])[0]
2349 # get the most recent changelist for each file in this label
2350 change
= p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p
, name
)
2351 for p
in self
.depotPaths
])
2353 if change
.has_key('change'):
2354 # find the corresponding git commit; take the oldest commit
2355 changelist
= int(change
['change'])
2356 gitCommit
= read_pipe(["git", "rev-list", "--max-count=1",
2357 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist
])
2358 if len(gitCommit
) == 0:
2359 print "could not find git commit for changelist %d" % changelist
2361 gitCommit
= gitCommit
.strip()
2363 # Convert from p4 time format
2365 tmwhen
= time
.strptime(labelDetails
['Update'], "%Y/%m/%d %H:%M:%S")
2367 print "Could not convert label time %s" % labelDetail
['Update']
2370 when
= int(time
.mktime(tmwhen
))
2371 self
.streamTag(stream
, name
, labelDetails
, gitCommit
, when
)
2373 print "p4 label %s mapped to git commit %s" % (name
, gitCommit
)
2376 print "Label %s has no changelists - possibly deleted?" % name
2379 # We can't import this label; don't try again as it will get very
2380 # expensive repeatedly fetching all the files for labels that will
2381 # never be imported. If the label is moved in the future, the
2382 # ignore will need to be removed manually.
2383 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name
])
2385 def guessProjectName(self
):
2386 for p
in self
.depotPaths
:
2389 p
= p
[p
.strip().rfind("/") + 1:]
2390 if not p
.endswith("/"):
2394 def getBranchMapping(self
):
2395 lostAndFoundBranches
= set()
2397 user
= gitConfig("git-p4.branchUser")
2399 command
= "branches -u %s" % user
2401 command
= "branches"
2403 for info
in p4CmdList(command
):
2404 details
= p4Cmd(["branch", "-o", info
["branch"]])
2406 while details
.has_key("View%s" % viewIdx
):
2407 paths
= details
["View%s" % viewIdx
].split(" ")
2408 viewIdx
= viewIdx
+ 1
2409 # require standard //depot/foo/... //depot/bar/... mapping
2410 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
2413 destination
= paths
[1]
2415 if p4PathStartsWith(source
, self
.depotPaths
[0]) and p4PathStartsWith(destination
, self
.depotPaths
[0]):
2416 source
= source
[len(self
.depotPaths
[0]):-4]
2417 destination
= destination
[len(self
.depotPaths
[0]):-4]
2419 if destination
in self
.knownBranches
:
2421 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
2422 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
2425 self
.knownBranches
[destination
] = source
2427 lostAndFoundBranches
.discard(destination
)
2429 if source
not in self
.knownBranches
:
2430 lostAndFoundBranches
.add(source
)
2432 # Perforce does not strictly require branches to be defined, so we also
2433 # check git config for a branch list.
2435 # Example of branch definition in git config file:
2437 # branchList=main:branchA
2438 # branchList=main:branchB
2439 # branchList=branchA:branchC
2440 configBranches
= gitConfigList("git-p4.branchList")
2441 for branch
in configBranches
:
2443 (source
, destination
) = branch
.split(":")
2444 self
.knownBranches
[destination
] = source
2446 lostAndFoundBranches
.discard(destination
)
2448 if source
not in self
.knownBranches
:
2449 lostAndFoundBranches
.add(source
)
2452 for branch
in lostAndFoundBranches
:
2453 self
.knownBranches
[branch
] = branch
2455 def getBranchMappingFromGitBranches(self
):
2456 branches
= p4BranchesInGit(self
.importIntoRemotes
)
2457 for branch
in branches
.keys():
2458 if branch
== "master":
2461 branch
= branch
[len(self
.projectName
):]
2462 self
.knownBranches
[branch
] = branch
2464 def listExistingP4GitBranches(self
):
2465 # branches holds mapping from name to commit
2466 branches
= p4BranchesInGit(self
.importIntoRemotes
)
2467 self
.p4BranchesInGit
= branches
.keys()
2468 for branch
in branches
.keys():
2469 self
.initialParents
[self
.refPrefix
+ branch
] = branches
[branch
]
2471 def updateOptionDict(self
, d
):
2473 if self
.keepRepoPath
:
2474 option_keys
['keepRepoPath'] = 1
2476 d
["options"] = ' '.join(sorted(option_keys
.keys()))
2478 def readOptions(self
, d
):
2479 self
.keepRepoPath
= (d
.has_key('options')
2480 and ('keepRepoPath' in d
['options']))
2482 def gitRefForBranch(self
, branch
):
2483 if branch
== "main":
2484 return self
.refPrefix
+ "master"
2486 if len(branch
) <= 0:
2489 return self
.refPrefix
+ self
.projectName
+ branch
2491 def gitCommitByP4Change(self
, ref
, change
):
2493 print "looking in ref " + ref
+ " for change %s using bisect..." % change
2496 latestCommit
= parseRevision(ref
)
2500 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
2501 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
2506 log
= extractLogMessageFromGitCommit(next
)
2507 settings
= extractSettingsGitLog(log
)
2508 currentChange
= int(settings
['change'])
2510 print "current change %s" % currentChange
2512 if currentChange
== change
:
2514 print "found %s" % next
2517 if currentChange
< change
:
2518 earliestCommit
= "^%s" % next
2520 latestCommit
= "%s" % next
2524 def importNewBranch(self
, branch
, maxChange
):
2525 # make fast-import flush all changes to disk and update the refs using the checkpoint
2526 # command so that we can try to find the branch parent in the git history
2527 self
.gitStream
.write("checkpoint\n\n");
2528 self
.gitStream
.flush();
2529 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
2530 range = "@1,%s" % maxChange
2531 #print "prefix" + branchPrefix
2532 changes
= p4ChangesForPaths([branchPrefix
], range)
2533 if len(changes
) <= 0:
2535 firstChange
= changes
[0]
2536 #print "first change in branch: %s" % firstChange
2537 sourceBranch
= self
.knownBranches
[branch
]
2538 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
2539 sourceRef
= self
.gitRefForBranch(sourceBranch
)
2540 #print "source " + sourceBranch
2542 branchParentChange
= int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath
, firstChange
)])["change"])
2543 #print "branch parent: %s" % branchParentChange
2544 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
2545 if len(gitParent
) > 0:
2546 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
2547 #print "parent git commit: %s" % gitParent
2549 self
.importChanges(changes
)
2552 def searchParent(self
, parent
, branch
, target
):
2554 for blob
in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent
]):
2556 if len(read_pipe(["git", "diff-tree", blob
, target
])) == 0:
2559 print "Found parent of %s in commit %s" % (branch
, blob
)
2566 def importChanges(self
, changes
):
2568 for change
in changes
:
2569 description
= p4_describe(change
)
2570 self
.updateOptionDict(description
)
2573 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
2578 if self
.detectBranches
:
2579 branches
= self
.splitFilesIntoBranches(description
)
2580 for branch
in branches
.keys():
2582 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
2583 self
.branchPrefixes
= [ branchPrefix
]
2587 filesForCommit
= branches
[branch
]
2590 print "branch is %s" % branch
2592 self
.updatedBranches
.add(branch
)
2594 if branch
not in self
.createdBranches
:
2595 self
.createdBranches
.add(branch
)
2596 parent
= self
.knownBranches
[branch
]
2597 if parent
== branch
:
2600 fullBranch
= self
.projectName
+ branch
2601 if fullBranch
not in self
.p4BranchesInGit
:
2603 print("\n Importing new branch %s" % fullBranch
);
2604 if self
.importNewBranch(branch
, change
- 1):
2606 self
.p4BranchesInGit
.append(fullBranch
)
2608 print("\n Resuming with change %s" % change
);
2611 print "parent determined through known branches: %s" % parent
2613 branch
= self
.gitRefForBranch(branch
)
2614 parent
= self
.gitRefForBranch(parent
)
2617 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
2619 if len(parent
) == 0 and branch
in self
.initialParents
:
2620 parent
= self
.initialParents
[branch
]
2621 del self
.initialParents
[branch
]
2625 tempBranch
= os
.path
.join(self
.tempBranchLocation
, "%d" % (change
))
2627 print "Creating temporary branch: " + tempBranch
2628 self
.commit(description
, filesForCommit
, tempBranch
)
2629 self
.tempBranches
.append(tempBranch
)
2631 blob
= self
.searchParent(parent
, branch
, tempBranch
)
2633 self
.commit(description
, filesForCommit
, branch
, blob
)
2636 print "Parent of %s not found. Committing into head of %s" % (branch
, parent
)
2637 self
.commit(description
, filesForCommit
, branch
, parent
)
2639 files
= self
.extractFilesFromCommit(description
)
2640 self
.commit(description
, files
, self
.branch
,
2642 self
.initialParent
= ""
2644 print self
.gitError
.read()
2647 def importHeadRevision(self
, revision
):
2648 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
2651 details
["user"] = "git perforce import user"
2652 details
["desc"] = ("Initial import of %s from the state at revision %s\n"
2653 % (' '.join(self
.depotPaths
), revision
))
2654 details
["change"] = revision
2658 fileArgs
= ["%s...%s" % (p
,revision
) for p
in self
.depotPaths
]
2660 for info
in p4CmdList(["files"] + fileArgs
):
2662 if 'code' in info
and info
['code'] == 'error':
2663 sys
.stderr
.write("p4 returned an error: %s\n"
2665 if info
['data'].find("must refer to client") >= 0:
2666 sys
.stderr
.write("This particular p4 error is misleading.\n")
2667 sys
.stderr
.write("Perhaps the depot path was misspelled.\n");
2668 sys
.stderr
.write("Depot path: %s\n" % " ".join(self
.depotPaths
))
2670 if 'p4ExitCode' in info
:
2671 sys
.stderr
.write("p4 exitcode: %s\n" % info
['p4ExitCode'])
2675 change
= int(info
["change"])
2676 if change
> newestRevision
:
2677 newestRevision
= change
2679 if info
["action"] in self
.delete_actions
:
2680 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2681 #fileCnt = fileCnt + 1
2684 for prop
in ["depotFile", "rev", "action", "type" ]:
2685 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
2687 fileCnt
= fileCnt
+ 1
2689 details
["change"] = newestRevision
2691 # Use time from top-most change so that all git p4 clones of
2692 # the same p4 repo have the same commit SHA1s.
2693 res
= p4_describe(newestRevision
)
2694 details
["time"] = res
["time"]
2696 self
.updateOptionDict(details
)
2698 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
)
2700 print "IO error with git fast-import. Is your git version recent enough?"
2701 print self
.gitError
.read()
2704 def run(self
, args
):
2705 self
.depotPaths
= []
2706 self
.changeRange
= ""
2707 self
.initialParent
= ""
2708 self
.previousDepotPaths
= []
2710 # map from branch depot path to parent branch
2711 self
.knownBranches
= {}
2712 self
.initialParents
= {}
2713 self
.hasOrigin
= originP4BranchesExist()
2714 if not self
.syncWithOrigin
:
2715 self
.hasOrigin
= False
2717 if self
.importIntoRemotes
:
2718 self
.refPrefix
= "refs/remotes/p4/"
2720 self
.refPrefix
= "refs/heads/p4/"
2722 if self
.syncWithOrigin
and self
.hasOrigin
:
2724 print "Syncing with origin first by calling git fetch origin"
2725 system("git fetch origin")
2727 if len(self
.branch
) == 0:
2728 self
.branch
= self
.refPrefix
+ "master"
2729 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
2730 system("git update-ref %s refs/heads/p4" % self
.branch
)
2731 system("git branch -D p4");
2732 # create it /after/ importing, when master exists
2733 if not gitBranchExists(self
.refPrefix
+ "HEAD") and self
.importIntoRemotes
and gitBranchExists(self
.branch
):
2734 system("git symbolic-ref %sHEAD %s" % (self
.refPrefix
, self
.branch
))
2736 # accept either the command-line option, or the configuration variable
2737 if self
.useClientSpec
:
2738 # will use this after clone to set the variable
2739 self
.useClientSpec_from_options
= True
2741 if gitConfig("git-p4.useclientspec", "--bool") == "true":
2742 self
.useClientSpec
= True
2743 if self
.useClientSpec
:
2744 self
.clientSpecDirs
= getClientSpec()
2746 # TODO: should always look at previous commits,
2747 # merge with previous imports, if possible.
2750 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
2751 self
.listExistingP4GitBranches()
2753 if len(self
.p4BranchesInGit
) > 1:
2755 print "Importing from/into multiple branches"
2756 self
.detectBranches
= True
2759 print "branches: %s" % self
.p4BranchesInGit
2762 for branch
in self
.p4BranchesInGit
:
2763 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
2765 settings
= extractSettingsGitLog(logMsg
)
2767 self
.readOptions(settings
)
2768 if (settings
.has_key('depot-paths')
2769 and settings
.has_key ('change')):
2770 change
= int(settings
['change']) + 1
2771 p4Change
= max(p4Change
, change
)
2773 depotPaths
= sorted(settings
['depot-paths'])
2774 if self
.previousDepotPaths
== []:
2775 self
.previousDepotPaths
= depotPaths
2778 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
2779 prev_list
= prev
.split("/")
2780 cur_list
= cur
.split("/")
2781 for i
in range(0, min(len(cur_list
), len(prev_list
))):
2782 if cur_list
[i
] <> prev_list
[i
]:
2786 paths
.append ("/".join(cur_list
[:i
+ 1]))
2788 self
.previousDepotPaths
= paths
2791 self
.depotPaths
= sorted(self
.previousDepotPaths
)
2792 self
.changeRange
= "@%s,#head" % p4Change
2793 if not self
.detectBranches
:
2794 self
.initialParent
= parseRevision(self
.branch
)
2795 if not self
.silent
and not self
.detectBranches
:
2796 print "Performing incremental import into %s git branch" % self
.branch
2798 if not self
.branch
.startswith("refs/"):
2799 self
.branch
= "refs/heads/" + self
.branch
2801 if len(args
) == 0 and self
.depotPaths
:
2803 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
2805 if self
.depotPaths
and self
.depotPaths
!= args
:
2806 print ("previous import used depot path %s and now %s was specified. "
2807 "This doesn't work!" % (' '.join (self
.depotPaths
),
2811 self
.depotPaths
= sorted(args
)
2816 # Make sure no revision specifiers are used when --changesfile
2818 bad_changesfile
= False
2819 if len(self
.changesFile
) > 0:
2820 for p
in self
.depotPaths
:
2821 if p
.find("@") >= 0 or p
.find("#") >= 0:
2822 bad_changesfile
= True
2825 die("Option --changesfile is incompatible with revision specifiers")
2828 for p
in self
.depotPaths
:
2829 if p
.find("@") != -1:
2830 atIdx
= p
.index("@")
2831 self
.changeRange
= p
[atIdx
:]
2832 if self
.changeRange
== "@all":
2833 self
.changeRange
= ""
2834 elif ',' not in self
.changeRange
:
2835 revision
= self
.changeRange
2836 self
.changeRange
= ""
2838 elif p
.find("#") != -1:
2839 hashIdx
= p
.index("#")
2840 revision
= p
[hashIdx
:]
2842 elif self
.previousDepotPaths
== []:
2843 # pay attention to changesfile, if given, else import
2844 # the entire p4 tree at the head revision
2845 if len(self
.changesFile
) == 0:
2848 p
= re
.sub ("\.\.\.$", "", p
)
2849 if not p
.endswith("/"):
2854 self
.depotPaths
= newPaths
2856 # --detect-branches may change this for each branch
2857 self
.branchPrefixes
= self
.depotPaths
2859 self
.loadUserMapFromCache()
2861 if self
.detectLabels
:
2864 if self
.detectBranches
:
2865 ## FIXME - what's a P4 projectName ?
2866 self
.projectName
= self
.guessProjectName()
2869 self
.getBranchMappingFromGitBranches()
2871 self
.getBranchMapping()
2873 print "p4-git branches: %s" % self
.p4BranchesInGit
2874 print "initial parents: %s" % self
.initialParents
2875 for b
in self
.p4BranchesInGit
:
2879 b
= b
[len(self
.projectName
):]
2880 self
.createdBranches
.add(b
)
2882 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
2884 importProcess
= subprocess
.Popen(["git", "fast-import"],
2885 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
2886 stderr
=subprocess
.PIPE
);
2887 self
.gitOutput
= importProcess
.stdout
2888 self
.gitStream
= importProcess
.stdin
2889 self
.gitError
= importProcess
.stderr
2892 self
.importHeadRevision(revision
)
2896 if len(self
.changesFile
) > 0:
2897 output
= open(self
.changesFile
).readlines()
2900 changeSet
.add(int(line
))
2902 for change
in changeSet
:
2903 changes
.append(change
)
2907 # catch "git p4 sync" with no new branches, in a repo that
2908 # does not have any existing p4 branches
2909 if len(args
) == 0 and not self
.p4BranchesInGit
:
2910 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
2912 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
2914 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
)
2916 if len(self
.maxChanges
) > 0:
2917 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
2919 if len(changes
) == 0:
2921 print "No changes to import!"
2923 if not self
.silent
and not self
.detectBranches
:
2924 print "Import destination: %s" % self
.branch
2926 self
.updatedBranches
= set()
2928 self
.importChanges(changes
)
2932 if len(self
.updatedBranches
) > 0:
2933 sys
.stdout
.write("Updated branches: ")
2934 for b
in self
.updatedBranches
:
2935 sys
.stdout
.write("%s " % b
)
2936 sys
.stdout
.write("\n")
2938 if gitConfig("git-p4.importLabels", "--bool") == "true":
2939 self
.importLabels
= True
2941 if self
.importLabels
:
2942 p4Labels
= getP4Labels(self
.depotPaths
)
2943 gitTags
= getGitTags()
2945 missingP4Labels
= p4Labels
- gitTags
2946 self
.importP4Labels(self
.gitStream
, missingP4Labels
)
2948 self
.gitStream
.close()
2949 if importProcess
.wait() != 0:
2950 die("fast-import failed: %s" % self
.gitError
.read())
2951 self
.gitOutput
.close()
2952 self
.gitError
.close()
2954 # Cleanup temporary branches created during import
2955 if self
.tempBranches
!= []:
2956 for branch
in self
.tempBranches
:
2957 read_pipe("git update-ref -d %s" % branch
)
2958 os
.rmdir(os
.path
.join(os
.environ
.get("GIT_DIR", ".git"), self
.tempBranchLocation
))
2962 class P4Rebase(Command
):
2964 Command
.__init
__(self
)
2966 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
2968 self
.importLabels
= False
2969 self
.description
= ("Fetches the latest revision from perforce and "
2970 + "rebases the current work (branch) against it")
2972 def run(self
, args
):
2974 sync
.importLabels
= self
.importLabels
2977 return self
.rebase()
2980 if os
.system("git update-index --refresh") != 0:
2981 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.");
2982 if len(read_pipe("git diff-index HEAD --")) > 0:
2983 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2985 [upstream
, settings
] = findUpstreamBranchPoint()
2986 if len(upstream
) == 0:
2987 die("Cannot find upstream branchpoint for rebase")
2989 # the branchpoint may be p4/foo~3, so strip off the parent
2990 upstream
= re
.sub("~[0-9]+$", "", upstream
)
2992 print "Rebasing the current branch onto %s" % upstream
2993 oldHead
= read_pipe("git rev-parse HEAD").strip()
2994 system("git rebase %s" % upstream
)
2995 system("git diff-tree --stat --summary -M %s HEAD" % oldHead
)
2998 class P4Clone(P4Sync
):
3000 P4Sync
.__init
__(self
)
3001 self
.description
= "Creates a new git repository and imports from Perforce into it"
3002 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
3004 optparse
.make_option("--destination", dest
="cloneDestination",
3005 action
='store', default
=None,
3006 help="where to leave result of the clone"),
3007 optparse
.make_option("-/", dest
="cloneExclude",
3008 action
="append", type="string",
3009 help="exclude depot path"),
3010 optparse
.make_option("--bare", dest
="cloneBare",
3011 action
="store_true", default
=False),
3013 self
.cloneDestination
= None
3014 self
.needsGit
= False
3015 self
.cloneBare
= False
3017 # This is required for the "append" cloneExclude action
3018 def ensure_value(self
, attr
, value
):
3019 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
3020 setattr(self
, attr
, value
)
3021 return getattr(self
, attr
)
3023 def defaultDestination(self
, args
):
3024 ## TODO: use common prefix of args?
3026 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
3027 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
3028 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
3029 depotDir
= re
.sub(r
"/$", "", depotDir
)
3030 return os
.path
.split(depotDir
)[1]
3032 def run(self
, args
):
3036 if self
.keepRepoPath
and not self
.cloneDestination
:
3037 sys
.stderr
.write("Must specify destination for --keep-path\n")
3042 if not self
.cloneDestination
and len(depotPaths
) > 1:
3043 self
.cloneDestination
= depotPaths
[-1]
3044 depotPaths
= depotPaths
[:-1]
3046 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
3047 for p
in depotPaths
:
3048 if not p
.startswith("//"):
3051 if not self
.cloneDestination
:
3052 self
.cloneDestination
= self
.defaultDestination(args
)
3054 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
3056 if not os
.path
.exists(self
.cloneDestination
):
3057 os
.makedirs(self
.cloneDestination
)
3058 chdir(self
.cloneDestination
)
3060 init_cmd
= [ "git", "init" ]
3062 init_cmd
.append("--bare")
3063 subprocess
.check_call(init_cmd
)
3065 if not P4Sync
.run(self
, depotPaths
):
3067 if self
.branch
!= "master":
3068 if self
.importIntoRemotes
:
3069 masterbranch
= "refs/remotes/p4/master"
3071 masterbranch
= "refs/heads/p4/master"
3072 if gitBranchExists(masterbranch
):
3073 system("git branch master %s" % masterbranch
)
3074 if not self
.cloneBare
:
3075 system("git checkout -f")
3077 print "Could not detect main branch. No checkout/master branch created."
3079 # auto-set this variable if invoked with --use-client-spec
3080 if self
.useClientSpec_from_options
:
3081 system("git config --bool git-p4.useclientspec true")
3085 class P4Branches(Command
):
3087 Command
.__init
__(self
)
3089 self
.description
= ("Shows the git branches that hold imports and their "
3090 + "corresponding perforce depot paths")
3091 self
.verbose
= False
3093 def run(self
, args
):
3094 if originP4BranchesExist():
3095 createOrUpdateBranchesFromOrigin()
3097 cmdline
= "git rev-parse --symbolic "
3098 cmdline
+= " --remotes"
3100 for line
in read_pipe_lines(cmdline
):
3103 if not line
.startswith('p4/') or line
== "p4/HEAD":
3107 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
3108 settings
= extractSettingsGitLog(log
)
3110 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
3113 class HelpFormatter(optparse
.IndentedHelpFormatter
):
3115 optparse
.IndentedHelpFormatter
.__init
__(self
)
3117 def format_description(self
, description
):
3119 return description
+ "\n"
3123 def printUsage(commands
):
3124 print "usage: %s <command> [options]" % sys
.argv
[0]
3126 print "valid commands: %s" % ", ".join(commands
)
3128 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
3133 "submit" : P4Submit
,
3134 "commit" : P4Submit
,
3136 "rebase" : P4Rebase
,
3138 "rollback" : P4RollBack
,
3139 "branches" : P4Branches
3144 if len(sys
.argv
[1:]) == 0:
3145 printUsage(commands
.keys())
3149 cmdName
= sys
.argv
[1]
3151 klass
= commands
[cmdName
]
3154 print "unknown command %s" % cmdName
3156 printUsage(commands
.keys())
3159 options
= cmd
.options
3160 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
3164 options
.append(optparse
.make_option("--verbose", "-v", dest
="verbose", action
="store_true"))
3166 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
3168 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
3170 description
= cmd
.description
,
3171 formatter
= HelpFormatter())
3173 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
3175 verbose
= cmd
.verbose
3177 if cmd
.gitdir
== None:
3178 cmd
.gitdir
= os
.path
.abspath(".git")
3179 if not isValidGitDir(cmd
.gitdir
):
3180 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
3181 if os
.path
.exists(cmd
.gitdir
):
3182 cdup
= read_pipe("git rev-parse --show-cdup").strip()
3186 if not isValidGitDir(cmd
.gitdir
):
3187 if isValidGitDir(cmd
.gitdir
+ "/.git"):
3188 cmd
.gitdir
+= "/.git"
3190 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
3192 os
.environ
["GIT_DIR"] = cmd
.gitdir
3194 if not cmd
.run(args
):
3199 if __name__
== '__main__':