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 if sys
.hexversion
< 0x02040000:
12 # The limiter is the subprocess module
13 sys
.stderr
.write("git-p4: requires Python 2.4 or later.\n")
27 from subprocess
import CalledProcessError
29 # from python2.7:subprocess.py
30 # Exception classes used by this module.
31 class CalledProcessError(Exception):
32 """This exception is raised when a process run by check_call() returns
33 a non-zero exit status. The exit status will be stored in the
34 returncode attribute."""
35 def __init__(self
, returncode
, cmd
):
36 self
.returncode
= returncode
39 return "Command '%s' returned non-zero exit status %d" % (self
.cmd
, self
.returncode
)
43 # Only labels/tags matching this will be imported/exported
44 defaultLabelRegexp
= r
'[a-zA-Z0-9_\-.]+$'
46 # Grab changes in blocks of this many revisions, unless otherwise requested
47 defaultBlockSize
= 512
49 def p4_build_cmd(cmd
):
50 """Build a suitable p4 command line.
52 This consolidates building and returning a p4 command line into one
53 location. It means that hooking into the environment, or other configuration
54 can be done more easily.
58 user
= gitConfig("git-p4.user")
60 real_cmd
+= ["-u",user
]
62 password
= gitConfig("git-p4.password")
64 real_cmd
+= ["-P", password
]
66 port
= gitConfig("git-p4.port")
68 real_cmd
+= ["-p", port
]
70 host
= gitConfig("git-p4.host")
72 real_cmd
+= ["-H", host
]
74 client
= gitConfig("git-p4.client")
76 real_cmd
+= ["-c", client
]
79 if isinstance(cmd
,basestring
):
80 real_cmd
= ' '.join(real_cmd
) + ' ' + cmd
85 def chdir(path
, is_client_path
=False):
86 """Do chdir to the given path, and set the PWD environment
87 variable for use by P4. It does not look at getcwd() output.
88 Since we're not using the shell, it is necessary to set the
89 PWD environment variable explicitly.
91 Normally, expand the path to force it to be absolute. This
92 addresses the use of relative path names inside P4 settings,
93 e.g. P4CONFIG=.p4config. P4 does not simply open the filename
94 as given; it looks for .p4config using PWD.
96 If is_client_path, the path was handed to us directly by p4,
97 and may be a symbolic link. Do not call os.getcwd() in this
98 case, because it will cause p4 to think that PWD is not inside
103 if not is_client_path
:
105 os
.environ
['PWD'] = path
111 sys
.stderr
.write(msg
+ "\n")
114 def write_pipe(c
, stdin
):
116 sys
.stderr
.write('Writing pipe: %s\n' % str(c
))
118 expand
= isinstance(c
,basestring
)
119 p
= subprocess
.Popen(c
, stdin
=subprocess
.PIPE
, shell
=expand
)
121 val
= pipe
.write(stdin
)
124 die('Command failed: %s' % str(c
))
128 def p4_write_pipe(c
, stdin
):
129 real_cmd
= p4_build_cmd(c
)
130 return write_pipe(real_cmd
, stdin
)
132 def read_pipe(c
, ignore_error
=False):
134 sys
.stderr
.write('Reading pipe: %s\n' % str(c
))
136 expand
= isinstance(c
,basestring
)
137 p
= subprocess
.Popen(c
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, shell
=expand
)
138 (out
, err
) = p
.communicate()
139 if p
.returncode
!= 0 and not ignore_error
:
140 die('Command failed: %s\nError: %s' % (str(c
), err
))
143 def p4_read_pipe(c
, ignore_error
=False):
144 real_cmd
= p4_build_cmd(c
)
145 return read_pipe(real_cmd
, ignore_error
)
147 def read_pipe_lines(c
):
149 sys
.stderr
.write('Reading pipe: %s\n' % str(c
))
151 expand
= isinstance(c
, basestring
)
152 p
= subprocess
.Popen(c
, stdout
=subprocess
.PIPE
, shell
=expand
)
154 val
= pipe
.readlines()
155 if pipe
.close() or p
.wait():
156 die('Command failed: %s' % str(c
))
160 def p4_read_pipe_lines(c
):
161 """Specifically invoke p4 on the command supplied. """
162 real_cmd
= p4_build_cmd(c
)
163 return read_pipe_lines(real_cmd
)
165 def p4_has_command(cmd
):
166 """Ask p4 for help on this command. If it returns an error, the
167 command does not exist in this version of p4."""
168 real_cmd
= p4_build_cmd(["help", cmd
])
169 p
= subprocess
.Popen(real_cmd
, stdout
=subprocess
.PIPE
,
170 stderr
=subprocess
.PIPE
)
172 return p
.returncode
== 0
174 def p4_has_move_command():
175 """See if the move command exists, that it supports -k, and that
176 it has not been administratively disabled. The arguments
177 must be correct, but the filenames do not have to exist. Use
178 ones with wildcards so even if they exist, it will fail."""
180 if not p4_has_command("move"):
182 cmd
= p4_build_cmd(["move", "-k", "@from", "@to"])
183 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
184 (out
, err
) = p
.communicate()
185 # return code will be 1 in either case
186 if err
.find("Invalid option") >= 0:
188 if err
.find("disabled") >= 0:
190 # assume it failed because @... was invalid changelist
193 def system(cmd
, ignore_error
=False):
194 expand
= isinstance(cmd
,basestring
)
196 sys
.stderr
.write("executing %s\n" % str(cmd
))
197 retcode
= subprocess
.call(cmd
, shell
=expand
)
198 if retcode
and not ignore_error
:
199 raise CalledProcessError(retcode
, cmd
)
204 """Specifically invoke p4 as the system command. """
205 real_cmd
= p4_build_cmd(cmd
)
206 expand
= isinstance(real_cmd
, basestring
)
207 retcode
= subprocess
.call(real_cmd
, shell
=expand
)
209 raise CalledProcessError(retcode
, real_cmd
)
211 _p4_version_string
= None
212 def p4_version_string():
213 """Read the version string, showing just the last line, which
214 hopefully is the interesting version bit.
217 Perforce - The Fast Software Configuration Management System.
218 Copyright 1995-2011 Perforce Software. All rights reserved.
219 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
221 global _p4_version_string
222 if not _p4_version_string
:
223 a
= p4_read_pipe_lines(["-V"])
224 _p4_version_string
= a
[-1].rstrip()
225 return _p4_version_string
227 def p4_integrate(src
, dest
):
228 p4_system(["integrate", "-Dt", wildcard_encode(src
), wildcard_encode(dest
)])
230 def p4_sync(f
, *options
):
231 p4_system(["sync"] + list(options
) + [wildcard_encode(f
)])
234 # forcibly add file names with wildcards
235 if wildcard_present(f
):
236 p4_system(["add", "-f", f
])
238 p4_system(["add", f
])
241 p4_system(["delete", wildcard_encode(f
)])
244 p4_system(["edit", wildcard_encode(f
)])
247 p4_system(["revert", wildcard_encode(f
)])
249 def p4_reopen(type, f
):
250 p4_system(["reopen", "-t", type, wildcard_encode(f
)])
252 def p4_move(src
, dest
):
253 p4_system(["move", "-k", wildcard_encode(src
), wildcard_encode(dest
)])
255 def p4_last_change():
256 results
= p4CmdList(["changes", "-m", "1"])
257 return int(results
[0]['change'])
259 def p4_describe(change
):
260 """Make sure it returns a valid result by checking for
261 the presence of field "time". Return a dict of the
264 ds
= p4CmdList(["describe", "-s", str(change
)])
266 die("p4 describe -s %d did not return 1 result: %s" % (change
, str(ds
)))
270 if "p4ExitCode" in d
:
271 die("p4 describe -s %d exited with %d: %s" % (change
, d
["p4ExitCode"],
274 if d
["code"] == "error":
275 die("p4 describe -s %d returned error code: %s" % (change
, str(d
)))
278 die("p4 describe -s %d returned no \"time\": %s" % (change
, str(d
)))
283 # Canonicalize the p4 type and return a tuple of the
284 # base type, plus any modifiers. See "p4 help filetypes"
285 # for a list and explanation.
287 def split_p4_type(p4type
):
289 p4_filetypes_historical
= {
290 "ctempobj": "binary+Sw",
296 "tempobj": "binary+FSw",
297 "ubinary": "binary+F",
298 "uresource": "resource+F",
299 "uxbinary": "binary+Fx",
300 "xbinary": "binary+x",
302 "xtempobj": "binary+Swx",
304 "xunicode": "unicode+x",
307 if p4type
in p4_filetypes_historical
:
308 p4type
= p4_filetypes_historical
[p4type
]
310 s
= p4type
.split("+")
318 # return the raw p4 type of a file (text, text+ko, etc)
321 results
= p4CmdList(["fstat", "-T", "headType", wildcard_encode(f
)])
322 return results
[0]['headType']
325 # Given a type base and modifier, return a regexp matching
326 # the keywords that can be expanded in the file
328 def p4_keywords_regexp_for_type(base
, type_mods
):
329 if base
in ("text", "unicode", "binary"):
331 if "ko" in type_mods
:
333 elif "k" in type_mods
:
334 kwords
= 'Id|Header|Author|Date|DateTime|Change|File|Revision'
338 \$ # Starts with a dollar, followed by...
339 (%s) # one of the keywords, followed by...
340 (:[^$\n]+)? # possibly an old expansion, followed by...
348 # Given a file, return a regexp matching the possible
349 # RCS keywords that will be expanded, or None for files
350 # with kw expansion turned off.
352 def p4_keywords_regexp_for_file(file):
353 if not os
.path
.exists(file):
356 (type_base
, type_mods
) = split_p4_type(p4_type(file))
357 return p4_keywords_regexp_for_type(type_base
, type_mods
)
359 def setP4ExecBit(file, mode
):
360 # Reopens an already open file and changes the execute bit to match
361 # the execute bit setting in the passed in mode.
365 if not isModeExec(mode
):
366 p4Type
= getP4OpenedType(file)
367 p4Type
= re
.sub('^([cku]?)x(.*)', '\\1\\2', p4Type
)
368 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
369 if p4Type
[-1] == "+":
370 p4Type
= p4Type
[0:-1]
372 p4_reopen(p4Type
, file)
374 def getP4OpenedType(file):
375 # Returns the perforce file type for the given file.
377 result
= p4_read_pipe(["opened", wildcard_encode(file)])
378 match
= re
.match(".*\((.+)\)( \*exclusive\*)?\r?$", result
)
380 return match
.group(1)
382 die("Could not determine file type for %s (result: '%s')" % (file, result
))
384 # Return the set of all p4 labels
385 def getP4Labels(depotPaths
):
387 if isinstance(depotPaths
,basestring
):
388 depotPaths
= [depotPaths
]
390 for l
in p4CmdList(["labels"] + ["%s..." % p
for p
in depotPaths
]):
396 # Return the set of all git tags
399 for line
in read_pipe_lines(["git", "tag"]):
404 def diffTreePattern():
405 # This is a simple generator for the diff tree regex pattern. This could be
406 # a class variable if this and parseDiffTreeEntry were a part of a class.
407 pattern
= re
.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
411 def parseDiffTreeEntry(entry
):
412 """Parses a single diff tree entry into its component elements.
414 See git-diff-tree(1) manpage for details about the format of the diff
415 output. This method returns a dictionary with the following elements:
417 src_mode - The mode of the source file
418 dst_mode - The mode of the destination file
419 src_sha1 - The sha1 for the source file
420 dst_sha1 - The sha1 fr the destination file
421 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
422 status_score - The score for the status (applicable for 'C' and 'R'
423 statuses). This is None if there is no score.
424 src - The path for the source file.
425 dst - The path for the destination file. This is only present for
426 copy or renames. If it is not present, this is None.
428 If the pattern is not matched, None is returned."""
430 match
= diffTreePattern().next().match(entry
)
433 'src_mode': match
.group(1),
434 'dst_mode': match
.group(2),
435 'src_sha1': match
.group(3),
436 'dst_sha1': match
.group(4),
437 'status': match
.group(5),
438 'status_score': match
.group(6),
439 'src': match
.group(7),
440 'dst': match
.group(10)
444 def isModeExec(mode
):
445 # Returns True if the given git mode represents an executable file,
447 return mode
[-3:] == "755"
449 def isModeExecChanged(src_mode
, dst_mode
):
450 return isModeExec(src_mode
) != isModeExec(dst_mode
)
452 def p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b', cb
=None):
454 if isinstance(cmd
,basestring
):
461 cmd
= p4_build_cmd(cmd
)
463 sys
.stderr
.write("Opening pipe: %s\n" % str(cmd
))
465 # Use a temporary file to avoid deadlocks without
466 # subprocess.communicate(), which would put another copy
467 # of stdout into memory.
469 if stdin
is not None:
470 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
471 if isinstance(stdin
,basestring
):
472 stdin_file
.write(stdin
)
475 stdin_file
.write(i
+ '\n')
479 p4
= subprocess
.Popen(cmd
,
482 stdout
=subprocess
.PIPE
)
487 entry
= marshal
.load(p4
.stdout
)
497 entry
["p4ExitCode"] = exitCode
503 list = p4CmdList(cmd
)
509 def p4Where(depotPath
):
510 if not depotPath
.endswith("/"):
512 depotPathLong
= depotPath
+ "..."
513 outputList
= p4CmdList(["where", depotPathLong
])
515 for entry
in outputList
:
516 if "depotFile" in entry
:
517 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
518 # The base path always ends with "/...".
519 if entry
["depotFile"].find(depotPath
) == 0 and entry
["depotFile"][-4:] == "/...":
522 elif "data" in entry
:
523 data
= entry
.get("data")
524 space
= data
.find(" ")
525 if data
[:space
] == depotPath
:
530 if output
["code"] == "error":
534 clientPath
= output
.get("path")
535 elif "data" in output
:
536 data
= output
.get("data")
537 lastSpace
= data
.rfind(" ")
538 clientPath
= data
[lastSpace
+ 1:]
540 if clientPath
.endswith("..."):
541 clientPath
= clientPath
[:-3]
544 def currentGitBranch():
545 retcode
= system(["git", "symbolic-ref", "-q", "HEAD"], ignore_error
=True)
550 return read_pipe(["git", "name-rev", "HEAD"]).split(" ")[1].strip()
552 def isValidGitDir(path
):
553 if (os
.path
.exists(path
+ "/HEAD")
554 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
558 def parseRevision(ref
):
559 return read_pipe("git rev-parse %s" % ref
).strip()
561 def branchExists(ref
):
562 rev
= read_pipe(["git", "rev-parse", "-q", "--verify", ref
],
566 def extractLogMessageFromGitCommit(commit
):
569 ## fixme: title is first line of commit, not 1st paragraph.
571 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
580 def extractSettingsGitLog(log
):
582 for line
in log
.split("\n"):
584 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
588 assignments
= m
.group(1).split (':')
589 for a
in assignments
:
591 key
= vals
[0].strip()
592 val
= ('='.join (vals
[1:])).strip()
593 if val
.endswith ('\"') and val
.startswith('"'):
598 paths
= values
.get("depot-paths")
600 paths
= values
.get("depot-path")
602 values
['depot-paths'] = paths
.split(',')
605 def gitBranchExists(branch
):
606 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
607 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
608 return proc
.wait() == 0;
613 if not _gitConfig
.has_key(key
):
614 cmd
= [ "git", "config", key
]
615 s
= read_pipe(cmd
, ignore_error
=True)
616 _gitConfig
[key
] = s
.strip()
617 return _gitConfig
[key
]
619 def gitConfigBool(key
):
620 """Return a bool, using git config --bool. It is True only if the
621 variable is set to true, and False if set to false or not present
624 if not _gitConfig
.has_key(key
):
625 cmd
= [ "git", "config", "--bool", key
]
626 s
= read_pipe(cmd
, ignore_error
=True)
628 _gitConfig
[key
] = v
== "true"
629 return _gitConfig
[key
]
631 def gitConfigList(key
):
632 if not _gitConfig
.has_key(key
):
633 s
= read_pipe(["git", "config", "--get-all", key
], ignore_error
=True)
634 _gitConfig
[key
] = s
.strip().split(os
.linesep
)
635 return _gitConfig
[key
]
637 def p4BranchesInGit(branchesAreInRemotes
=True):
638 """Find all the branches whose names start with "p4/", looking
639 in remotes or heads as specified by the argument. Return
640 a dictionary of { branch: revision } for each one found.
641 The branch names are the short names, without any
646 cmdline
= "git rev-parse --symbolic "
647 if branchesAreInRemotes
:
648 cmdline
+= "--remotes"
650 cmdline
+= "--branches"
652 for line
in read_pipe_lines(cmdline
):
656 if not line
.startswith('p4/'):
658 # special symbolic ref to p4/master
659 if line
== "p4/HEAD":
662 # strip off p4/ prefix
663 branch
= line
[len("p4/"):]
665 branches
[branch
] = parseRevision(line
)
669 def branch_exists(branch
):
670 """Make sure that the given ref name really exists."""
672 cmd
= [ "git", "rev-parse", "--symbolic", "--verify", branch
]
673 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
674 out
, _
= p
.communicate()
677 # expect exactly one line of output: the branch name
678 return out
.rstrip() == branch
680 def findUpstreamBranchPoint(head
= "HEAD"):
681 branches
= p4BranchesInGit()
682 # map from depot-path to branch name
683 branchByDepotPath
= {}
684 for branch
in branches
.keys():
685 tip
= branches
[branch
]
686 log
= extractLogMessageFromGitCommit(tip
)
687 settings
= extractSettingsGitLog(log
)
688 if settings
.has_key("depot-paths"):
689 paths
= ",".join(settings
["depot-paths"])
690 branchByDepotPath
[paths
] = "remotes/p4/" + branch
694 while parent
< 65535:
695 commit
= head
+ "~%s" % parent
696 log
= extractLogMessageFromGitCommit(commit
)
697 settings
= extractSettingsGitLog(log
)
698 if settings
.has_key("depot-paths"):
699 paths
= ",".join(settings
["depot-paths"])
700 if branchByDepotPath
.has_key(paths
):
701 return [branchByDepotPath
[paths
], settings
]
705 return ["", settings
]
707 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
709 print ("Creating/updating branch(es) in %s based on origin branch(es)"
712 originPrefix
= "origin/p4/"
714 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
716 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
719 headName
= line
[len(originPrefix
):]
720 remoteHead
= localRefPrefix
+ headName
723 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
724 if (not original
.has_key('depot-paths')
725 or not original
.has_key('change')):
729 if not gitBranchExists(remoteHead
):
731 print "creating %s" % remoteHead
734 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
735 if settings
.has_key('change') > 0:
736 if settings
['depot-paths'] == original
['depot-paths']:
737 originP4Change
= int(original
['change'])
738 p4Change
= int(settings
['change'])
739 if originP4Change
> p4Change
:
740 print ("%s (%s) is newer than %s (%s). "
741 "Updating p4 branch from origin."
742 % (originHead
, originP4Change
,
743 remoteHead
, p4Change
))
746 print ("Ignoring: %s was imported from %s while "
747 "%s was imported from %s"
748 % (originHead
, ','.join(original
['depot-paths']),
749 remoteHead
, ','.join(settings
['depot-paths'])))
752 system("git update-ref %s %s" % (remoteHead
, originHead
))
754 def originP4BranchesExist():
755 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
758 def p4ParseNumericChangeRange(parts
):
759 changeStart
= int(parts
[0][1:])
760 if parts
[1] == '#head':
761 changeEnd
= p4_last_change()
763 changeEnd
= int(parts
[1])
765 return (changeStart
, changeEnd
)
767 def chooseBlockSize(blockSize
):
771 return defaultBlockSize
773 def p4ChangesForPaths(depotPaths
, changeRange
, requestedBlockSize
):
776 # Parse the change range into start and end. Try to find integer
777 # revision ranges as these can be broken up into blocks to avoid
778 # hitting server-side limits (maxrows, maxscanresults). But if
779 # that doesn't work, fall back to using the raw revision specifier
780 # strings, without using block mode.
782 if changeRange
is None or changeRange
== '':
784 changeEnd
= p4_last_change()
785 block_size
= chooseBlockSize(requestedBlockSize
)
787 parts
= changeRange
.split(',')
788 assert len(parts
) == 2
790 (changeStart
, changeEnd
) = p4ParseNumericChangeRange(parts
)
791 block_size
= chooseBlockSize(requestedBlockSize
)
793 changeStart
= parts
[0][1:]
795 if requestedBlockSize
:
796 die("cannot use --changes-block-size with non-numeric revisions")
799 # Accumulate change numbers in a dictionary to avoid duplicates
803 # Retrieve changes a block at a time, to prevent running
804 # into a MaxResults/MaxScanRows error from the server.
810 end
= min(changeEnd
, changeStart
+ block_size
)
811 revisionRange
= "%d,%d" % (changeStart
, end
)
813 revisionRange
= "%s,%s" % (changeStart
, changeEnd
)
815 cmd
+= ["%s...@%s" % (p
, revisionRange
)]
817 for line
in p4_read_pipe_lines(cmd
):
818 changeNum
= int(line
.split(" ")[1])
819 changes
[changeNum
] = True
827 changeStart
= end
+ 1
829 changelist
= changes
.keys()
833 def p4PathStartsWith(path
, prefix
):
834 # This method tries to remedy a potential mixed-case issue:
836 # If UserA adds //depot/DirA/file1
837 # and UserB adds //depot/dira/file2
839 # we may or may not have a problem. If you have core.ignorecase=true,
840 # we treat DirA and dira as the same directory
841 if gitConfigBool("core.ignorecase"):
842 return path
.lower().startswith(prefix
.lower())
843 return path
.startswith(prefix
)
846 """Look at the p4 client spec, create a View() object that contains
847 all the mappings, and return it."""
849 specList
= p4CmdList("client -o")
850 if len(specList
) != 1:
851 die('Output from "client -o" is %d lines, expecting 1' %
854 # dictionary of all client parameters
858 client_name
= entry
["Client"]
860 # just the keys that start with "View"
861 view_keys
= [ k
for k
in entry
.keys() if k
.startswith("View") ]
864 view
= View(client_name
)
866 # append the lines, in order, to the view
867 for view_num
in range(len(view_keys
)):
868 k
= "View%d" % view_num
869 if k
not in view_keys
:
870 die("Expected view key %s missing" % k
)
871 view
.append(entry
[k
])
876 """Grab the client directory."""
878 output
= p4CmdList("client -o")
880 die('Output from "client -o" is %d lines, expecting 1' % len(output
))
883 if "Root" not in entry
:
884 die('Client has no "Root"')
889 # P4 wildcards are not allowed in filenames. P4 complains
890 # if you simply add them, but you can force it with "-f", in
891 # which case it translates them into %xx encoding internally.
893 def wildcard_decode(path
):
894 # Search for and fix just these four characters. Do % last so
895 # that fixing it does not inadvertently create new %-escapes.
896 # Cannot have * in a filename in windows; untested as to
897 # what p4 would do in such a case.
898 if not platform
.system() == "Windows":
899 path
= path
.replace("%2A", "*")
900 path
= path
.replace("%23", "#") \
901 .replace("%40", "@") \
905 def wildcard_encode(path
):
906 # do % first to avoid double-encoding the %s introduced here
907 path
= path
.replace("%", "%25") \
908 .replace("*", "%2A") \
909 .replace("#", "%23") \
913 def wildcard_present(path
):
914 m
= re
.search("[*#@%]", path
)
919 self
.usage
= "usage: %prog [options]"
925 self
.userMapFromPerforceServer
= False
926 self
.myP4UserId
= None
930 return self
.myP4UserId
932 results
= p4CmdList("user -o")
934 if r
.has_key('User'):
935 self
.myP4UserId
= r
['User']
937 die("Could not find your p4 user id")
939 def p4UserIsMe(self
, p4User
):
940 # return True if the given p4 user is actually me
942 if not p4User
or p4User
!= me
:
947 def getUserCacheFilename(self
):
948 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
949 return home
+ "/.gitp4-usercache.txt"
951 def getUserMapFromPerforceServer(self
):
952 if self
.userMapFromPerforceServer
:
957 for output
in p4CmdList("users"):
958 if not output
.has_key("User"):
960 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
961 self
.emails
[output
["Email"]] = output
["User"]
965 for (key
, val
) in self
.users
.items():
966 s
+= "%s\t%s\n" % (key
.expandtabs(1), val
.expandtabs(1))
968 open(self
.getUserCacheFilename(), "wb").write(s
)
969 self
.userMapFromPerforceServer
= True
971 def loadUserMapFromCache(self
):
973 self
.userMapFromPerforceServer
= False
975 cache
= open(self
.getUserCacheFilename(), "rb")
976 lines
= cache
.readlines()
979 entry
= line
.strip().split("\t")
980 self
.users
[entry
[0]] = entry
[1]
982 self
.getUserMapFromPerforceServer()
984 class P4Debug(Command
):
986 Command
.__init
__(self
)
988 self
.description
= "A tool to debug the output of p4 -G."
989 self
.needsGit
= False
993 for output
in p4CmdList(args
):
994 print 'Element: %d' % j
999 class P4RollBack(Command
):
1001 Command
.__init
__(self
)
1003 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
1005 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
1006 self
.rollbackLocalBranches
= False
1008 def run(self
, args
):
1011 maxChange
= int(args
[0])
1013 if "p4ExitCode" in p4Cmd("changes -m 1"):
1014 die("Problems executing p4");
1016 if self
.rollbackLocalBranches
:
1017 refPrefix
= "refs/heads/"
1018 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
1020 refPrefix
= "refs/remotes/"
1021 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
1024 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
1026 ref
= refPrefix
+ line
1027 log
= extractLogMessageFromGitCommit(ref
)
1028 settings
= extractSettingsGitLog(log
)
1030 depotPaths
= settings
['depot-paths']
1031 change
= settings
['change']
1035 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
1036 for p
in depotPaths
]))) == 0:
1037 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
1038 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
1041 while change
and int(change
) > maxChange
:
1044 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
1045 system("git update-ref %s \"%s^\"" % (ref
, ref
))
1046 log
= extractLogMessageFromGitCommit(ref
)
1047 settings
= extractSettingsGitLog(log
)
1050 depotPaths
= settings
['depot-paths']
1051 change
= settings
['change']
1054 print "%s rewound to %s" % (ref
, change
)
1058 class P4Submit(Command
, P4UserMap
):
1060 conflict_behavior_choices
= ("ask", "skip", "quit")
1063 Command
.__init
__(self
)
1064 P4UserMap
.__init
__(self
)
1066 optparse
.make_option("--origin", dest
="origin"),
1067 optparse
.make_option("-M", dest
="detectRenames", action
="store_true"),
1068 # preserve the user, requires relevant p4 permissions
1069 optparse
.make_option("--preserve-user", dest
="preserveUser", action
="store_true"),
1070 optparse
.make_option("--export-labels", dest
="exportLabels", action
="store_true"),
1071 optparse
.make_option("--dry-run", "-n", dest
="dry_run", action
="store_true"),
1072 optparse
.make_option("--prepare-p4-only", dest
="prepare_p4_only", action
="store_true"),
1073 optparse
.make_option("--conflict", dest
="conflict_behavior",
1074 choices
=self
.conflict_behavior_choices
),
1075 optparse
.make_option("--branch", dest
="branch"),
1077 self
.description
= "Submit changes from git to the perforce depot."
1078 self
.usage
+= " [name of git branch to submit into perforce depot]"
1080 self
.detectRenames
= False
1081 self
.preserveUser
= gitConfigBool("git-p4.preserveUser")
1082 self
.dry_run
= False
1083 self
.prepare_p4_only
= False
1084 self
.conflict_behavior
= None
1085 self
.isWindows
= (platform
.system() == "Windows")
1086 self
.exportLabels
= False
1087 self
.p4HasMoveCommand
= p4_has_move_command()
1091 if len(p4CmdList("opened ...")) > 0:
1092 die("You have files opened with perforce! Close them before starting the sync.")
1094 def separate_jobs_from_description(self
, message
):
1095 """Extract and return a possible Jobs field in the commit
1096 message. It goes into a separate section in the p4 change
1099 A jobs line starts with "Jobs:" and looks like a new field
1100 in a form. Values are white-space separated on the same
1101 line or on following lines that start with a tab.
1103 This does not parse and extract the full git commit message
1104 like a p4 form. It just sees the Jobs: line as a marker
1105 to pass everything from then on directly into the p4 form,
1106 but outside the description section.
1108 Return a tuple (stripped log message, jobs string)."""
1110 m
= re
.search(r
'^Jobs:', message
, re
.MULTILINE
)
1112 return (message
, None)
1114 jobtext
= message
[m
.start():]
1115 stripped_message
= message
[:m
.start()].rstrip()
1116 return (stripped_message
, jobtext
)
1118 def prepareLogMessage(self
, template
, message
, jobs
):
1119 """Edits the template returned from "p4 change -o" to insert
1120 the message in the Description field, and the jobs text in
1124 inDescriptionSection
= False
1126 for line
in template
.split("\n"):
1127 if line
.startswith("#"):
1128 result
+= line
+ "\n"
1131 if inDescriptionSection
:
1132 if line
.startswith("Files:") or line
.startswith("Jobs:"):
1133 inDescriptionSection
= False
1134 # insert Jobs section
1136 result
+= jobs
+ "\n"
1140 if line
.startswith("Description:"):
1141 inDescriptionSection
= True
1143 for messageLine
in message
.split("\n"):
1144 line
+= "\t" + messageLine
+ "\n"
1146 result
+= line
+ "\n"
1150 def patchRCSKeywords(self
, file, pattern
):
1151 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1152 (handle
, outFileName
) = tempfile
.mkstemp(dir='.')
1154 outFile
= os
.fdopen(handle
, "w+")
1155 inFile
= open(file, "r")
1156 regexp
= re
.compile(pattern
, re
.VERBOSE
)
1157 for line
in inFile
.readlines():
1158 line
= regexp
.sub(r
'$\1$', line
)
1162 # Forcibly overwrite the original file
1164 shutil
.move(outFileName
, file)
1166 # cleanup our temporary file
1167 os
.unlink(outFileName
)
1168 print "Failed to strip RCS keywords in %s" % file
1171 print "Patched up RCS keywords in %s" % file
1173 def p4UserForCommit(self
,id):
1174 # Return the tuple (perforce user,git email) for a given git commit id
1175 self
.getUserMapFromPerforceServer()
1176 gitEmail
= read_pipe(["git", "log", "--max-count=1",
1177 "--format=%ae", id])
1178 gitEmail
= gitEmail
.strip()
1179 if not self
.emails
.has_key(gitEmail
):
1180 return (None,gitEmail
)
1182 return (self
.emails
[gitEmail
],gitEmail
)
1184 def checkValidP4Users(self
,commits
):
1185 # check if any git authors cannot be mapped to p4 users
1187 (user
,email
) = self
.p4UserForCommit(id)
1189 msg
= "Cannot find p4 user for email %s in commit %s." % (email
, id)
1190 if gitConfigBool("git-p4.allowMissingP4Users"):
1193 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg
)
1195 def lastP4Changelist(self
):
1196 # Get back the last changelist number submitted in this client spec. This
1197 # then gets used to patch up the username in the change. If the same
1198 # client spec is being used by multiple processes then this might go
1200 results
= p4CmdList("client -o") # find the current client
1203 if r
.has_key('Client'):
1204 client
= r
['Client']
1207 die("could not get client spec")
1208 results
= p4CmdList(["changes", "-c", client
, "-m", "1"])
1210 if r
.has_key('change'):
1212 die("Could not get changelist number for last submit - cannot patch up user details")
1214 def modifyChangelistUser(self
, changelist
, newUser
):
1215 # fixup the user field of a changelist after it has been submitted.
1216 changes
= p4CmdList("change -o %s" % changelist
)
1217 if len(changes
) != 1:
1218 die("Bad output from p4 change modifying %s to user %s" %
1219 (changelist
, newUser
))
1222 if c
['User'] == newUser
: return # nothing to do
1224 input = marshal
.dumps(c
)
1226 result
= p4CmdList("change -f -i", stdin
=input)
1228 if r
.has_key('code'):
1229 if r
['code'] == 'error':
1230 die("Could not modify user field of changelist %s to %s:%s" % (changelist
, newUser
, r
['data']))
1231 if r
.has_key('data'):
1232 print("Updated user field for changelist %s to %s" % (changelist
, newUser
))
1234 die("Could not modify user field of changelist %s to %s" % (changelist
, newUser
))
1236 def canChangeChangelists(self
):
1237 # check to see if we have p4 admin or super-user permissions, either of
1238 # which are required to modify changelists.
1239 results
= p4CmdList(["protects", self
.depotPath
])
1241 if r
.has_key('perm'):
1242 if r
['perm'] == 'admin':
1244 if r
['perm'] == 'super':
1248 def prepareSubmitTemplate(self
):
1249 """Run "p4 change -o" to grab a change specification template.
1250 This does not use "p4 -G", as it is nice to keep the submission
1251 template in original order, since a human might edit it.
1253 Remove lines in the Files section that show changes to files
1254 outside the depot path we're committing into."""
1257 inFilesSection
= False
1258 for line
in p4_read_pipe_lines(['change', '-o']):
1259 if line
.endswith("\r\n"):
1260 line
= line
[:-2] + "\n"
1262 if line
.startswith("\t"):
1263 # path starts and ends with a tab
1265 lastTab
= path
.rfind("\t")
1267 path
= path
[:lastTab
]
1268 if not p4PathStartsWith(path
, self
.depotPath
):
1271 inFilesSection
= False
1273 if line
.startswith("Files:"):
1274 inFilesSection
= True
1280 def edit_template(self
, template_file
):
1281 """Invoke the editor to let the user change the submission
1282 message. Return true if okay to continue with the submit."""
1284 # if configured to skip the editing part, just submit
1285 if gitConfigBool("git-p4.skipSubmitEdit"):
1288 # look at the modification time, to check later if the user saved
1290 mtime
= os
.stat(template_file
).st_mtime
1293 if os
.environ
.has_key("P4EDITOR") and (os
.environ
.get("P4EDITOR") != ""):
1294 editor
= os
.environ
.get("P4EDITOR")
1296 editor
= read_pipe("git var GIT_EDITOR").strip()
1297 system(["sh", "-c", ('%s "$@"' % editor
), editor
, template_file
])
1299 # If the file was not saved, prompt to see if this patch should
1300 # be skipped. But skip this verification step if configured so.
1301 if gitConfigBool("git-p4.skipSubmitEditCheck"):
1304 # modification time updated means user saved the file
1305 if os
.stat(template_file
).st_mtime
> mtime
:
1309 response
= raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1315 def get_diff_description(self
, editedFiles
, filesToAdd
):
1317 if os
.environ
.has_key("P4DIFF"):
1318 del(os
.environ
["P4DIFF"])
1320 for editedFile
in editedFiles
:
1321 diff
+= p4_read_pipe(['diff', '-du',
1322 wildcard_encode(editedFile
)])
1326 for newFile
in filesToAdd
:
1327 newdiff
+= "==== new file ====\n"
1328 newdiff
+= "--- /dev/null\n"
1329 newdiff
+= "+++ %s\n" % newFile
1330 f
= open(newFile
, "r")
1331 for line
in f
.readlines():
1332 newdiff
+= "+" + line
1335 return (diff
+ newdiff
).replace('\r\n', '\n')
1337 def applyCommit(self
, id):
1338 """Apply one commit, return True if it succeeded."""
1340 print "Applying", read_pipe(["git", "show", "-s",
1341 "--format=format:%h %s", id])
1343 (p4User
, gitEmail
) = self
.p4UserForCommit(id)
1345 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self
.diffOpts
, id, id))
1347 filesToDelete
= set()
1349 pureRenameCopy
= set()
1350 filesToChangeExecBit
= {}
1353 diff
= parseDiffTreeEntry(line
)
1354 modifier
= diff
['status']
1358 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
1359 filesToChangeExecBit
[path
] = diff
['dst_mode']
1360 editedFiles
.add(path
)
1361 elif modifier
== "A":
1362 filesToAdd
.add(path
)
1363 filesToChangeExecBit
[path
] = diff
['dst_mode']
1364 if path
in filesToDelete
:
1365 filesToDelete
.remove(path
)
1366 elif modifier
== "D":
1367 filesToDelete
.add(path
)
1368 if path
in filesToAdd
:
1369 filesToAdd
.remove(path
)
1370 elif modifier
== "C":
1371 src
, dest
= diff
['src'], diff
['dst']
1372 p4_integrate(src
, dest
)
1373 pureRenameCopy
.add(dest
)
1374 if diff
['src_sha1'] != diff
['dst_sha1']:
1376 pureRenameCopy
.discard(dest
)
1377 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
1379 pureRenameCopy
.discard(dest
)
1380 filesToChangeExecBit
[dest
] = diff
['dst_mode']
1382 # turn off read-only attribute
1383 os
.chmod(dest
, stat
.S_IWRITE
)
1385 editedFiles
.add(dest
)
1386 elif modifier
== "R":
1387 src
, dest
= diff
['src'], diff
['dst']
1388 if self
.p4HasMoveCommand
:
1389 p4_edit(src
) # src must be open before move
1390 p4_move(src
, dest
) # opens for (move/delete, move/add)
1392 p4_integrate(src
, dest
)
1393 if diff
['src_sha1'] != diff
['dst_sha1']:
1396 pureRenameCopy
.add(dest
)
1397 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
1398 if not self
.p4HasMoveCommand
:
1399 p4_edit(dest
) # with move: already open, writable
1400 filesToChangeExecBit
[dest
] = diff
['dst_mode']
1401 if not self
.p4HasMoveCommand
:
1403 os
.chmod(dest
, stat
.S_IWRITE
)
1405 filesToDelete
.add(src
)
1406 editedFiles
.add(dest
)
1408 die("unknown modifier %s for %s" % (modifier
, path
))
1410 diffcmd
= "git diff-tree --full-index -p \"%s\"" % (id)
1411 patchcmd
= diffcmd
+ " | git apply "
1412 tryPatchCmd
= patchcmd
+ "--check -"
1413 applyPatchCmd
= patchcmd
+ "--check --apply -"
1414 patch_succeeded
= True
1416 if os
.system(tryPatchCmd
) != 0:
1417 fixed_rcs_keywords
= False
1418 patch_succeeded
= False
1419 print "Unfortunately applying the change failed!"
1421 # Patch failed, maybe it's just RCS keyword woes. Look through
1422 # the patch to see if that's possible.
1423 if gitConfigBool("git-p4.attemptRCSCleanup"):
1427 for file in editedFiles | filesToDelete
:
1428 # did this file's delta contain RCS keywords?
1429 pattern
= p4_keywords_regexp_for_file(file)
1432 # this file is a possibility...look for RCS keywords.
1433 regexp
= re
.compile(pattern
, re
.VERBOSE
)
1434 for line
in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1435 if regexp
.search(line
):
1437 print "got keyword match on %s in %s in %s" % (pattern
, line
, file)
1438 kwfiles
[file] = pattern
1441 for file in kwfiles
:
1443 print "zapping %s with %s" % (line
,pattern
)
1444 # File is being deleted, so not open in p4. Must
1445 # disable the read-only bit on windows.
1446 if self
.isWindows
and file not in editedFiles
:
1447 os
.chmod(file, stat
.S_IWRITE
)
1448 self
.patchRCSKeywords(file, kwfiles
[file])
1449 fixed_rcs_keywords
= True
1451 if fixed_rcs_keywords
:
1452 print "Retrying the patch with RCS keywords cleaned up"
1453 if os
.system(tryPatchCmd
) == 0:
1454 patch_succeeded
= True
1456 if not patch_succeeded
:
1457 for f
in editedFiles
:
1462 # Apply the patch for real, and do add/delete/+x handling.
1464 system(applyPatchCmd
)
1466 for f
in filesToAdd
:
1468 for f
in filesToDelete
:
1472 # Set/clear executable bits
1473 for f
in filesToChangeExecBit
.keys():
1474 mode
= filesToChangeExecBit
[f
]
1475 setP4ExecBit(f
, mode
)
1478 # Build p4 change description, starting with the contents
1479 # of the git commit message.
1481 logMessage
= extractLogMessageFromGitCommit(id)
1482 logMessage
= logMessage
.strip()
1483 (logMessage
, jobs
) = self
.separate_jobs_from_description(logMessage
)
1485 template
= self
.prepareSubmitTemplate()
1486 submitTemplate
= self
.prepareLogMessage(template
, logMessage
, jobs
)
1488 if self
.preserveUser
:
1489 submitTemplate
+= "\n######## Actual user %s, modified after commit\n" % p4User
1491 if self
.checkAuthorship
and not self
.p4UserIsMe(p4User
):
1492 submitTemplate
+= "######## git author %s does not match your p4 account.\n" % gitEmail
1493 submitTemplate
+= "######## Use option --preserve-user to modify authorship.\n"
1494 submitTemplate
+= "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1496 separatorLine
= "######## everything below this line is just the diff #######\n"
1497 if not self
.prepare_p4_only
:
1498 submitTemplate
+= separatorLine
1499 submitTemplate
+= self
.get_diff_description(editedFiles
, filesToAdd
)
1501 (handle
, fileName
) = tempfile
.mkstemp()
1502 tmpFile
= os
.fdopen(handle
, "w+b")
1504 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
1505 tmpFile
.write(submitTemplate
)
1508 if self
.prepare_p4_only
:
1510 # Leave the p4 tree prepared, and the submit template around
1511 # and let the user decide what to do next
1514 print "P4 workspace prepared for submission."
1515 print "To submit or revert, go to client workspace"
1516 print " " + self
.clientPath
1518 print "To submit, use \"p4 submit\" to write a new description,"
1519 print "or \"p4 submit -i <%s\" to use the one prepared by" \
1520 " \"git p4\"." % fileName
1521 print "You can delete the file \"%s\" when finished." % fileName
1523 if self
.preserveUser
and p4User
and not self
.p4UserIsMe(p4User
):
1524 print "To preserve change ownership by user %s, you must\n" \
1525 "do \"p4 change -f <change>\" after submitting and\n" \
1526 "edit the User field."
1528 print "After submitting, renamed files must be re-synced."
1529 print "Invoke \"p4 sync -f\" on each of these files:"
1530 for f
in pureRenameCopy
:
1534 print "To revert the changes, use \"p4 revert ...\", and delete"
1535 print "the submit template file \"%s\"" % fileName
1537 print "Since the commit adds new files, they must be deleted:"
1538 for f
in filesToAdd
:
1544 # Let the user edit the change description, then submit it.
1549 if self
.edit_template(fileName
):
1550 # read the edited message and submit
1551 tmpFile
= open(fileName
, "rb")
1552 message
= tmpFile
.read()
1555 message
= message
.replace("\r\n", "\n")
1556 submitTemplate
= message
[:message
.index(separatorLine
)]
1557 p4_write_pipe(['submit', '-i'], submitTemplate
)
1559 if self
.preserveUser
:
1561 # Get last changelist number. Cannot easily get it from
1562 # the submit command output as the output is
1564 changelist
= self
.lastP4Changelist()
1565 self
.modifyChangelistUser(changelist
, p4User
)
1567 # The rename/copy happened by applying a patch that created a
1568 # new file. This leaves it writable, which confuses p4.
1569 for f
in pureRenameCopy
:
1576 print "Submission cancelled, undoing p4 changes."
1577 for f
in editedFiles
:
1579 for f
in filesToAdd
:
1582 for f
in filesToDelete
:
1588 # Export git tags as p4 labels. Create a p4 label and then tag
1590 def exportGitTags(self
, gitTags
):
1591 validLabelRegexp
= gitConfig("git-p4.labelExportRegexp")
1592 if len(validLabelRegexp
) == 0:
1593 validLabelRegexp
= defaultLabelRegexp
1594 m
= re
.compile(validLabelRegexp
)
1596 for name
in gitTags
:
1598 if not m
.match(name
):
1600 print "tag %s does not match regexp %s" % (name
, validLabelRegexp
)
1603 # Get the p4 commit this corresponds to
1604 logMessage
= extractLogMessageFromGitCommit(name
)
1605 values
= extractSettingsGitLog(logMessage
)
1607 if not values
.has_key('change'):
1608 # a tag pointing to something not sent to p4; ignore
1610 print "git tag %s does not give a p4 commit" % name
1613 changelist
= values
['change']
1615 # Get the tag details.
1619 for l
in read_pipe_lines(["git", "cat-file", "-p", name
]):
1622 if re
.match(r
'tag\s+', l
):
1624 elif re
.match(r
'\s*$', l
):
1631 body
= ["lightweight tag imported by git p4\n"]
1633 # Create the label - use the same view as the client spec we are using
1634 clientSpec
= getClientSpec()
1636 labelTemplate
= "Label: %s\n" % name
1637 labelTemplate
+= "Description:\n"
1639 labelTemplate
+= "\t" + b
+ "\n"
1640 labelTemplate
+= "View:\n"
1641 for depot_side
in clientSpec
.mappings
:
1642 labelTemplate
+= "\t%s\n" % depot_side
1645 print "Would create p4 label %s for tag" % name
1646 elif self
.prepare_p4_only
:
1647 print "Not creating p4 label %s for tag due to option" \
1648 " --prepare-p4-only" % name
1650 p4_write_pipe(["label", "-i"], labelTemplate
)
1653 p4_system(["tag", "-l", name
] +
1654 ["%s@%s" % (depot_side
, changelist
) for depot_side
in clientSpec
.mappings
])
1657 print "created p4 label for tag %s" % name
1659 def run(self
, args
):
1661 self
.master
= currentGitBranch()
1662 elif len(args
) == 1:
1663 self
.master
= args
[0]
1664 if not branchExists(self
.master
):
1665 die("Branch %s does not exist" % self
.master
)
1670 allowSubmit
= gitConfig("git-p4.allowSubmit")
1671 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
1672 die("%s is not in git-p4.allowSubmit" % self
.master
)
1674 [upstream
, settings
] = findUpstreamBranchPoint()
1675 self
.depotPath
= settings
['depot-paths'][0]
1676 if len(self
.origin
) == 0:
1677 self
.origin
= upstream
1679 if self
.preserveUser
:
1680 if not self
.canChangeChangelists():
1681 die("Cannot preserve user names without p4 super-user or admin permissions")
1683 # if not set from the command line, try the config file
1684 if self
.conflict_behavior
is None:
1685 val
= gitConfig("git-p4.conflict")
1687 if val
not in self
.conflict_behavior_choices
:
1688 die("Invalid value '%s' for config git-p4.conflict" % val
)
1691 self
.conflict_behavior
= val
1694 print "Origin branch is " + self
.origin
1696 if len(self
.depotPath
) == 0:
1697 print "Internal error: cannot locate perforce depot path from existing branches"
1700 self
.useClientSpec
= False
1701 if gitConfigBool("git-p4.useclientspec"):
1702 self
.useClientSpec
= True
1703 if self
.useClientSpec
:
1704 self
.clientSpecDirs
= getClientSpec()
1706 # Check for the existance of P4 branches
1707 branchesDetected
= (len(p4BranchesInGit().keys()) > 1)
1709 if self
.useClientSpec
and not branchesDetected
:
1710 # all files are relative to the client spec
1711 self
.clientPath
= getClientRoot()
1713 self
.clientPath
= p4Where(self
.depotPath
)
1715 if self
.clientPath
== "":
1716 die("Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
)
1718 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
1719 self
.oldWorkingDirectory
= os
.getcwd()
1721 # ensure the clientPath exists
1722 new_client_dir
= False
1723 if not os
.path
.exists(self
.clientPath
):
1724 new_client_dir
= True
1725 os
.makedirs(self
.clientPath
)
1727 chdir(self
.clientPath
, is_client_path
=True)
1729 print "Would synchronize p4 checkout in %s" % self
.clientPath
1731 print "Synchronizing p4 checkout..."
1733 # old one was destroyed, and maybe nobody told p4
1734 p4_sync("...", "-f")
1741 commitish
= self
.master
1745 for line
in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self
.origin
, commitish
)]):
1746 commits
.append(line
.strip())
1749 if self
.preserveUser
or gitConfigBool("git-p4.skipUserNameCheck"):
1750 self
.checkAuthorship
= False
1752 self
.checkAuthorship
= True
1754 if self
.preserveUser
:
1755 self
.checkValidP4Users(commits
)
1758 # Build up a set of options to be passed to diff when
1759 # submitting each commit to p4.
1761 if self
.detectRenames
:
1762 # command-line -M arg
1763 self
.diffOpts
= "-M"
1765 # If not explicitly set check the config variable
1766 detectRenames
= gitConfig("git-p4.detectRenames")
1768 if detectRenames
.lower() == "false" or detectRenames
== "":
1770 elif detectRenames
.lower() == "true":
1771 self
.diffOpts
= "-M"
1773 self
.diffOpts
= "-M%s" % detectRenames
1775 # no command-line arg for -C or --find-copies-harder, just
1777 detectCopies
= gitConfig("git-p4.detectCopies")
1778 if detectCopies
.lower() == "false" or detectCopies
== "":
1780 elif detectCopies
.lower() == "true":
1781 self
.diffOpts
+= " -C"
1783 self
.diffOpts
+= " -C%s" % detectCopies
1785 if gitConfigBool("git-p4.detectCopiesHarder"):
1786 self
.diffOpts
+= " --find-copies-harder"
1789 # Apply the commits, one at a time. On failure, ask if should
1790 # continue to try the rest of the patches, or quit.
1795 last
= len(commits
) - 1
1796 for i
, commit
in enumerate(commits
):
1798 print " ", read_pipe(["git", "show", "-s",
1799 "--format=format:%h %s", commit
])
1802 ok
= self
.applyCommit(commit
)
1804 applied
.append(commit
)
1806 if self
.prepare_p4_only
and i
< last
:
1807 print "Processing only the first commit due to option" \
1808 " --prepare-p4-only"
1813 # prompt for what to do, or use the option/variable
1814 if self
.conflict_behavior
== "ask":
1815 print "What do you want to do?"
1816 response
= raw_input("[s]kip this commit but apply"
1817 " the rest, or [q]uit? ")
1820 elif self
.conflict_behavior
== "skip":
1822 elif self
.conflict_behavior
== "quit":
1825 die("Unknown conflict_behavior '%s'" %
1826 self
.conflict_behavior
)
1828 if response
[0] == "s":
1829 print "Skipping this commit, but applying the rest"
1831 if response
[0] == "q":
1838 chdir(self
.oldWorkingDirectory
)
1842 elif self
.prepare_p4_only
:
1844 elif len(commits
) == len(applied
):
1845 print "All commits applied!"
1849 sync
.branch
= self
.branch
1856 if len(applied
) == 0:
1857 print "No commits applied."
1859 print "Applied only the commits marked with '*':"
1865 print star
, read_pipe(["git", "show", "-s",
1866 "--format=format:%h %s", c
])
1867 print "You will have to do 'git p4 sync' and rebase."
1869 if gitConfigBool("git-p4.exportLabels"):
1870 self
.exportLabels
= True
1872 if self
.exportLabels
:
1873 p4Labels
= getP4Labels(self
.depotPath
)
1874 gitTags
= getGitTags()
1876 missingGitTags
= gitTags
- p4Labels
1877 self
.exportGitTags(missingGitTags
)
1879 # exit with error unless everything applied perfectly
1880 if len(commits
) != len(applied
):
1886 """Represent a p4 view ("p4 help views"), and map files in a
1887 repo according to the view."""
1889 def __init__(self
, client_name
):
1891 self
.client_prefix
= "//%s/" % client_name
1892 # cache results of "p4 where" to lookup client file locations
1893 self
.client_spec_path_cache
= {}
1895 def append(self
, view_line
):
1896 """Parse a view line, splitting it into depot and client
1897 sides. Append to self.mappings, preserving order. This
1898 is only needed for tag creation."""
1900 # Split the view line into exactly two words. P4 enforces
1901 # structure on these lines that simplifies this quite a bit.
1903 # Either or both words may be double-quoted.
1904 # Single quotes do not matter.
1905 # Double-quote marks cannot occur inside the words.
1906 # A + or - prefix is also inside the quotes.
1907 # There are no quotes unless they contain a space.
1908 # The line is already white-space stripped.
1909 # The two words are separated by a single space.
1911 if view_line
[0] == '"':
1912 # First word is double quoted. Find its end.
1913 close_quote_index
= view_line
.find('"', 1)
1914 if close_quote_index
<= 0:
1915 die("No first-word closing quote found: %s" % view_line
)
1916 depot_side
= view_line
[1:close_quote_index
]
1917 # skip closing quote and space
1918 rhs_index
= close_quote_index
+ 1 + 1
1920 space_index
= view_line
.find(" ")
1921 if space_index
<= 0:
1922 die("No word-splitting space found: %s" % view_line
)
1923 depot_side
= view_line
[0:space_index
]
1924 rhs_index
= space_index
+ 1
1926 # prefix + means overlay on previous mapping
1927 if depot_side
.startswith("+"):
1928 depot_side
= depot_side
[1:]
1930 # prefix - means exclude this path, leave out of mappings
1932 if depot_side
.startswith("-"):
1934 depot_side
= depot_side
[1:]
1937 self
.mappings
.append(depot_side
)
1939 def convert_client_path(self
, clientFile
):
1940 # chop off //client/ part to make it relative
1941 if not clientFile
.startswith(self
.client_prefix
):
1942 die("No prefix '%s' on clientFile '%s'" %
1943 (self
.client_prefix
, clientFile
))
1944 return clientFile
[len(self
.client_prefix
):]
1946 def update_client_spec_path_cache(self
, files
):
1947 """ Caching file paths by "p4 where" batch query """
1949 # List depot file paths exclude that already cached
1950 fileArgs
= [f
['path'] for f
in files
if f
['path'] not in self
.client_spec_path_cache
]
1952 if len(fileArgs
) == 0:
1953 return # All files in cache
1955 where_result
= p4CmdList(["-x", "-", "where"], stdin
=fileArgs
)
1956 for res
in where_result
:
1957 if "code" in res
and res
["code"] == "error":
1958 # assume error is "... file(s) not in client view"
1960 if "clientFile" not in res
:
1961 die("No clientFile in 'p4 where' output")
1963 # it will list all of them, but only one not unmap-ped
1965 if gitConfigBool("core.ignorecase"):
1966 res
['depotFile'] = res
['depotFile'].lower()
1967 self
.client_spec_path_cache
[res
['depotFile']] = self
.convert_client_path(res
["clientFile"])
1969 # not found files or unmap files set to ""
1970 for depotFile
in fileArgs
:
1971 if gitConfigBool("core.ignorecase"):
1972 depotFile
= depotFile
.lower()
1973 if depotFile
not in self
.client_spec_path_cache
:
1974 self
.client_spec_path_cache
[depotFile
] = ""
1976 def map_in_client(self
, depot_path
):
1977 """Return the relative location in the client where this
1978 depot file should live. Returns "" if the file should
1979 not be mapped in the client."""
1981 if gitConfigBool("core.ignorecase"):
1982 depot_path
= depot_path
.lower()
1984 if depot_path
in self
.client_spec_path_cache
:
1985 return self
.client_spec_path_cache
[depot_path
]
1987 die( "Error: %s is not found in client spec path" % depot_path
)
1990 class P4Sync(Command
, P4UserMap
):
1991 delete_actions
= ( "delete", "move/delete", "purge" )
1994 Command
.__init
__(self
)
1995 P4UserMap
.__init
__(self
)
1997 optparse
.make_option("--branch", dest
="branch"),
1998 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
1999 optparse
.make_option("--changesfile", dest
="changesFile"),
2000 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
2001 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
2002 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
2003 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
2004 help="Import into refs/heads/ , not refs/remotes"),
2005 optparse
.make_option("--max-changes", dest
="maxChanges",
2006 help="Maximum number of changes to import"),
2007 optparse
.make_option("--changes-block-size", dest
="changes_block_size", type="int",
2008 help="Internal block size to use when iteratively calling p4 changes"),
2009 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
2010 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2011 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
2012 help="Only sync files that are included in the Perforce Client Spec"),
2013 optparse
.make_option("-/", dest
="cloneExclude",
2014 action
="append", type="string",
2015 help="exclude depot path"),
2017 self
.description
= """Imports from Perforce into a git repository.\n
2019 //depot/my/project/ -- to import the current head
2020 //depot/my/project/@all -- to import everything
2021 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2023 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2025 self
.usage
+= " //depot/path[@revRange]"
2027 self
.createdBranches
= set()
2028 self
.committedChanges
= set()
2030 self
.detectBranches
= False
2031 self
.detectLabels
= False
2032 self
.importLabels
= False
2033 self
.changesFile
= ""
2034 self
.syncWithOrigin
= True
2035 self
.importIntoRemotes
= True
2036 self
.maxChanges
= ""
2037 self
.changes_block_size
= None
2038 self
.keepRepoPath
= False
2039 self
.depotPaths
= None
2040 self
.p4BranchesInGit
= []
2041 self
.cloneExclude
= []
2042 self
.useClientSpec
= False
2043 self
.useClientSpec_from_options
= False
2044 self
.clientSpecDirs
= None
2045 self
.tempBranches
= []
2046 self
.tempBranchLocation
= "git-p4-tmp"
2048 if gitConfig("git-p4.syncFromOrigin") == "false":
2049 self
.syncWithOrigin
= False
2051 # This is required for the "append" cloneExclude action
2052 def ensure_value(self
, attr
, value
):
2053 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
2054 setattr(self
, attr
, value
)
2055 return getattr(self
, attr
)
2057 # Force a checkpoint in fast-import and wait for it to finish
2058 def checkpoint(self
):
2059 self
.gitStream
.write("checkpoint\n\n")
2060 self
.gitStream
.write("progress checkpoint\n\n")
2061 out
= self
.gitOutput
.readline()
2063 print "checkpoint finished: " + out
2065 def extractFilesFromCommit(self
, commit
):
2066 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
2067 for path
in self
.cloneExclude
]
2070 while commit
.has_key("depotFile%s" % fnum
):
2071 path
= commit
["depotFile%s" % fnum
]
2073 if [p
for p
in self
.cloneExclude
2074 if p4PathStartsWith(path
, p
)]:
2077 found
= [p
for p
in self
.depotPaths
2078 if p4PathStartsWith(path
, p
)]
2085 file["rev"] = commit
["rev%s" % fnum
]
2086 file["action"] = commit
["action%s" % fnum
]
2087 file["type"] = commit
["type%s" % fnum
]
2092 def stripRepoPath(self
, path
, prefixes
):
2093 """When streaming files, this is called to map a p4 depot path
2094 to where it should go in git. The prefixes are either
2095 self.depotPaths, or self.branchPrefixes in the case of
2096 branch detection."""
2098 if self
.useClientSpec
:
2099 # branch detection moves files up a level (the branch name)
2100 # from what client spec interpretation gives
2101 path
= self
.clientSpecDirs
.map_in_client(path
)
2102 if self
.detectBranches
:
2103 for b
in self
.knownBranches
:
2104 if path
.startswith(b
+ "/"):
2105 path
= path
[len(b
)+1:]
2107 elif self
.keepRepoPath
:
2108 # Preserve everything in relative path name except leading
2109 # //depot/; just look at first prefix as they all should
2110 # be in the same depot.
2111 depot
= re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])
2112 if p4PathStartsWith(path
, depot
):
2113 path
= path
[len(depot
):]
2117 if p4PathStartsWith(path
, p
):
2118 path
= path
[len(p
):]
2121 path
= wildcard_decode(path
)
2124 def splitFilesIntoBranches(self
, commit
):
2125 """Look at each depotFile in the commit to figure out to what
2126 branch it belongs."""
2128 if self
.clientSpecDirs
:
2129 files
= self
.extractFilesFromCommit(commit
)
2130 self
.clientSpecDirs
.update_client_spec_path_cache(files
)
2134 while commit
.has_key("depotFile%s" % fnum
):
2135 path
= commit
["depotFile%s" % fnum
]
2136 found
= [p
for p
in self
.depotPaths
2137 if p4PathStartsWith(path
, p
)]
2144 file["rev"] = commit
["rev%s" % fnum
]
2145 file["action"] = commit
["action%s" % fnum
]
2146 file["type"] = commit
["type%s" % fnum
]
2149 # start with the full relative path where this file would
2151 if self
.useClientSpec
:
2152 relPath
= self
.clientSpecDirs
.map_in_client(path
)
2154 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
2156 for branch
in self
.knownBranches
.keys():
2157 # add a trailing slash so that a commit into qt/4.2foo
2158 # doesn't end up in qt/4.2, e.g.
2159 if relPath
.startswith(branch
+ "/"):
2160 if branch
not in branches
:
2161 branches
[branch
] = []
2162 branches
[branch
].append(file)
2167 # output one file from the P4 stream
2168 # - helper for streamP4Files
2170 def streamOneP4File(self
, file, contents
):
2171 relPath
= self
.stripRepoPath(file['depotFile'], self
.branchPrefixes
)
2173 sys
.stderr
.write("%s\n" % relPath
)
2175 (type_base
, type_mods
) = split_p4_type(file["type"])
2178 if "x" in type_mods
:
2180 if type_base
== "symlink":
2182 # p4 print on a symlink sometimes contains "target\n";
2183 # if it does, remove the newline
2184 data
= ''.join(contents
)
2186 # Some version of p4 allowed creating a symlink that pointed
2187 # to nothing. This causes p4 errors when checking out such
2188 # a change, and errors here too. Work around it by ignoring
2189 # the bad symlink; hopefully a future change fixes it.
2190 print "\nIgnoring empty symlink in %s" % file['depotFile']
2192 elif data
[-1] == '\n':
2193 contents
= [data
[:-1]]
2197 if type_base
== "utf16":
2198 # p4 delivers different text in the python output to -G
2199 # than it does when using "print -o", or normal p4 client
2200 # operations. utf16 is converted to ascii or utf8, perhaps.
2201 # But ascii text saved as -t utf16 is completely mangled.
2202 # Invoke print -o to get the real contents.
2204 # On windows, the newlines will always be mangled by print, so put
2205 # them back too. This is not needed to the cygwin windows version,
2206 # just the native "NT" type.
2209 text
= p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2210 except Exception as e
:
2211 if 'Translation of file content failed' in str(e
):
2212 type_base
= 'binary'
2216 if p4_version_string().find('/NT') >= 0:
2217 text
= text
.replace('\r\n', '\n')
2220 if type_base
== "apple":
2221 # Apple filetype files will be streamed as a concatenation of
2222 # its appledouble header and the contents. This is useless
2223 # on both macs and non-macs. If using "print -q -o xx", it
2224 # will create "xx" with the data, and "%xx" with the header.
2225 # This is also not very useful.
2227 # Ideally, someday, this script can learn how to generate
2228 # appledouble files directly and import those to git, but
2229 # non-mac machines can never find a use for apple filetype.
2230 print "\nIgnoring apple filetype file %s" % file['depotFile']
2233 # Note that we do not try to de-mangle keywords on utf16 files,
2234 # even though in theory somebody may want that.
2235 pattern
= p4_keywords_regexp_for_type(type_base
, type_mods
)
2237 regexp
= re
.compile(pattern
, re
.VERBOSE
)
2238 text
= ''.join(contents
)
2239 text
= regexp
.sub(r
'$\1$', text
)
2242 self
.gitStream
.write("M %s inline %s\n" % (git_mode
, relPath
))
2247 length
= length
+ len(d
)
2249 self
.gitStream
.write("data %d\n" % length
)
2251 self
.gitStream
.write(d
)
2252 self
.gitStream
.write("\n")
2254 def streamOneP4Deletion(self
, file):
2255 relPath
= self
.stripRepoPath(file['path'], self
.branchPrefixes
)
2257 sys
.stderr
.write("delete %s\n" % relPath
)
2258 self
.gitStream
.write("D %s\n" % relPath
)
2260 # handle another chunk of streaming data
2261 def streamP4FilesCb(self
, marshalled
):
2263 # catch p4 errors and complain
2265 if "code" in marshalled
:
2266 if marshalled
["code"] == "error":
2267 if "data" in marshalled
:
2268 err
= marshalled
["data"].rstrip()
2271 if self
.stream_have_file_info
:
2272 if "depotFile" in self
.stream_file
:
2273 f
= self
.stream_file
["depotFile"]
2274 # force a failure in fast-import, else an empty
2275 # commit will be made
2276 self
.gitStream
.write("\n")
2277 self
.gitStream
.write("die-now\n")
2278 self
.gitStream
.close()
2279 # ignore errors, but make sure it exits first
2280 self
.importProcess
.wait()
2282 die("Error from p4 print for %s: %s" % (f
, err
))
2284 die("Error from p4 print: %s" % err
)
2286 if marshalled
.has_key('depotFile') and self
.stream_have_file_info
:
2287 # start of a new file - output the old one first
2288 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
2289 self
.stream_file
= {}
2290 self
.stream_contents
= []
2291 self
.stream_have_file_info
= False
2293 # pick up the new file information... for the
2294 # 'data' field we need to append to our array
2295 for k
in marshalled
.keys():
2297 self
.stream_contents
.append(marshalled
['data'])
2299 self
.stream_file
[k
] = marshalled
[k
]
2301 self
.stream_have_file_info
= True
2303 # Stream directly from "p4 files" into "git fast-import"
2304 def streamP4Files(self
, files
):
2310 # if using a client spec, only add the files that have
2311 # a path in the client
2312 if self
.clientSpecDirs
:
2313 if self
.clientSpecDirs
.map_in_client(f
['path']) == "":
2316 filesForCommit
.append(f
)
2317 if f
['action'] in self
.delete_actions
:
2318 filesToDelete
.append(f
)
2320 filesToRead
.append(f
)
2323 for f
in filesToDelete
:
2324 self
.streamOneP4Deletion(f
)
2326 if len(filesToRead
) > 0:
2327 self
.stream_file
= {}
2328 self
.stream_contents
= []
2329 self
.stream_have_file_info
= False
2331 # curry self argument
2332 def streamP4FilesCbSelf(entry
):
2333 self
.streamP4FilesCb(entry
)
2335 fileArgs
= ['%s#%s' % (f
['path'], f
['rev']) for f
in filesToRead
]
2337 p4CmdList(["-x", "-", "print"],
2339 cb
=streamP4FilesCbSelf
)
2342 if self
.stream_file
.has_key('depotFile'):
2343 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
2345 def make_email(self
, userid
):
2346 if userid
in self
.users
:
2347 return self
.users
[userid
]
2349 return "%s <a@b>" % userid
2351 def streamTag(self
, gitStream
, labelName
, labelDetails
, commit
, epoch
):
2352 """ Stream a p4 tag.
2353 commit is either a git commit, or a fast-import mark, ":<p4commit>"
2357 print "writing tag %s for commit %s" % (labelName
, commit
)
2358 gitStream
.write("tag %s\n" % labelName
)
2359 gitStream
.write("from %s\n" % commit
)
2361 if labelDetails
.has_key('Owner'):
2362 owner
= labelDetails
["Owner"]
2366 # Try to use the owner of the p4 label, or failing that,
2367 # the current p4 user id.
2369 email
= self
.make_email(owner
)
2371 email
= self
.make_email(self
.p4UserId())
2372 tagger
= "%s %s %s" % (email
, epoch
, self
.tz
)
2374 gitStream
.write("tagger %s\n" % tagger
)
2376 print "labelDetails=",labelDetails
2377 if labelDetails
.has_key('Description'):
2378 description
= labelDetails
['Description']
2380 description
= 'Label from git p4'
2382 gitStream
.write("data %d\n" % len(description
))
2383 gitStream
.write(description
)
2384 gitStream
.write("\n")
2386 def commit(self
, details
, files
, branch
, parent
= ""):
2387 epoch
= details
["time"]
2388 author
= details
["user"]
2391 print "commit into %s" % branch
2393 # start with reading files; if that fails, we should not
2397 if [p
for p
in self
.branchPrefixes
if p4PathStartsWith(f
['path'], p
)]:
2398 new_files
.append (f
)
2400 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % f
['path'])
2402 if self
.clientSpecDirs
:
2403 self
.clientSpecDirs
.update_client_spec_path_cache(files
)
2405 self
.gitStream
.write("commit %s\n" % branch
)
2406 self
.gitStream
.write("mark :%s\n" % details
["change"])
2407 self
.committedChanges
.add(int(details
["change"]))
2409 if author
not in self
.users
:
2410 self
.getUserMapFromPerforceServer()
2411 committer
= "%s %s %s" % (self
.make_email(author
), epoch
, self
.tz
)
2413 self
.gitStream
.write("committer %s\n" % committer
)
2415 self
.gitStream
.write("data <<EOT\n")
2416 self
.gitStream
.write(details
["desc"])
2417 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2418 (','.join(self
.branchPrefixes
), details
["change"]))
2419 if len(details
['options']) > 0:
2420 self
.gitStream
.write(": options = %s" % details
['options'])
2421 self
.gitStream
.write("]\nEOT\n\n")
2425 print "parent %s" % parent
2426 self
.gitStream
.write("from %s\n" % parent
)
2428 self
.streamP4Files(new_files
)
2429 self
.gitStream
.write("\n")
2431 change
= int(details
["change"])
2433 if self
.labels
.has_key(change
):
2434 label
= self
.labels
[change
]
2435 labelDetails
= label
[0]
2436 labelRevisions
= label
[1]
2438 print "Change %s is labelled %s" % (change
, labelDetails
)
2440 files
= p4CmdList(["files"] + ["%s...@%s" % (p
, change
)
2441 for p
in self
.branchPrefixes
])
2443 if len(files
) == len(labelRevisions
):
2447 if info
["action"] in self
.delete_actions
:
2449 cleanedFiles
[info
["depotFile"]] = info
["rev"]
2451 if cleanedFiles
== labelRevisions
:
2452 self
.streamTag(self
.gitStream
, 'tag_%s' % labelDetails
['label'], labelDetails
, branch
, epoch
)
2456 print ("Tag %s does not match with change %s: files do not match."
2457 % (labelDetails
["label"], change
))
2461 print ("Tag %s does not match with change %s: file count is different."
2462 % (labelDetails
["label"], change
))
2464 # Build a dictionary of changelists and labels, for "detect-labels" option.
2465 def getLabels(self
):
2468 l
= p4CmdList(["labels"] + ["%s..." % p
for p
in self
.depotPaths
])
2469 if len(l
) > 0 and not self
.silent
:
2470 print "Finding files belonging to labels in %s" % `self
.depotPaths`
2473 label
= output
["label"]
2477 print "Querying files for label %s" % label
2478 for file in p4CmdList(["files"] +
2479 ["%s...@%s" % (p
, label
)
2480 for p
in self
.depotPaths
]):
2481 revisions
[file["depotFile"]] = file["rev"]
2482 change
= int(file["change"])
2483 if change
> newestChange
:
2484 newestChange
= change
2486 self
.labels
[newestChange
] = [output
, revisions
]
2489 print "Label changes: %s" % self
.labels
.keys()
2491 # Import p4 labels as git tags. A direct mapping does not
2492 # exist, so assume that if all the files are at the same revision
2493 # then we can use that, or it's something more complicated we should
2495 def importP4Labels(self
, stream
, p4Labels
):
2497 print "import p4 labels: " + ' '.join(p4Labels
)
2499 ignoredP4Labels
= gitConfigList("git-p4.ignoredP4Labels")
2500 validLabelRegexp
= gitConfig("git-p4.labelImportRegexp")
2501 if len(validLabelRegexp
) == 0:
2502 validLabelRegexp
= defaultLabelRegexp
2503 m
= re
.compile(validLabelRegexp
)
2505 for name
in p4Labels
:
2508 if not m
.match(name
):
2510 print "label %s does not match regexp %s" % (name
,validLabelRegexp
)
2513 if name
in ignoredP4Labels
:
2516 labelDetails
= p4CmdList(['label', "-o", name
])[0]
2518 # get the most recent changelist for each file in this label
2519 change
= p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p
, name
)
2520 for p
in self
.depotPaths
])
2522 if change
.has_key('change'):
2523 # find the corresponding git commit; take the oldest commit
2524 changelist
= int(change
['change'])
2525 if changelist
in self
.committedChanges
:
2526 gitCommit
= ":%d" % changelist
# use a fast-import mark
2529 gitCommit
= read_pipe(["git", "rev-list", "--max-count=1",
2530 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist
], ignore_error
=True)
2531 if len(gitCommit
) == 0:
2532 print "importing label %s: could not find git commit for changelist %d" % (name
, changelist
)
2535 gitCommit
= gitCommit
.strip()
2538 # Convert from p4 time format
2540 tmwhen
= time
.strptime(labelDetails
['Update'], "%Y/%m/%d %H:%M:%S")
2542 print "Could not convert label time %s" % labelDetails
['Update']
2545 when
= int(time
.mktime(tmwhen
))
2546 self
.streamTag(stream
, name
, labelDetails
, gitCommit
, when
)
2548 print "p4 label %s mapped to git commit %s" % (name
, gitCommit
)
2551 print "Label %s has no changelists - possibly deleted?" % name
2554 # We can't import this label; don't try again as it will get very
2555 # expensive repeatedly fetching all the files for labels that will
2556 # never be imported. If the label is moved in the future, the
2557 # ignore will need to be removed manually.
2558 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name
])
2560 def guessProjectName(self
):
2561 for p
in self
.depotPaths
:
2564 p
= p
[p
.strip().rfind("/") + 1:]
2565 if not p
.endswith("/"):
2569 def getBranchMapping(self
):
2570 lostAndFoundBranches
= set()
2572 user
= gitConfig("git-p4.branchUser")
2574 command
= "branches -u %s" % user
2576 command
= "branches"
2578 for info
in p4CmdList(command
):
2579 details
= p4Cmd(["branch", "-o", info
["branch"]])
2581 while details
.has_key("View%s" % viewIdx
):
2582 paths
= details
["View%s" % viewIdx
].split(" ")
2583 viewIdx
= viewIdx
+ 1
2584 # require standard //depot/foo/... //depot/bar/... mapping
2585 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
2588 destination
= paths
[1]
2590 if p4PathStartsWith(source
, self
.depotPaths
[0]) and p4PathStartsWith(destination
, self
.depotPaths
[0]):
2591 source
= source
[len(self
.depotPaths
[0]):-4]
2592 destination
= destination
[len(self
.depotPaths
[0]):-4]
2594 if destination
in self
.knownBranches
:
2596 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
2597 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
2600 self
.knownBranches
[destination
] = source
2602 lostAndFoundBranches
.discard(destination
)
2604 if source
not in self
.knownBranches
:
2605 lostAndFoundBranches
.add(source
)
2607 # Perforce does not strictly require branches to be defined, so we also
2608 # check git config for a branch list.
2610 # Example of branch definition in git config file:
2612 # branchList=main:branchA
2613 # branchList=main:branchB
2614 # branchList=branchA:branchC
2615 configBranches
= gitConfigList("git-p4.branchList")
2616 for branch
in configBranches
:
2618 (source
, destination
) = branch
.split(":")
2619 self
.knownBranches
[destination
] = source
2621 lostAndFoundBranches
.discard(destination
)
2623 if source
not in self
.knownBranches
:
2624 lostAndFoundBranches
.add(source
)
2627 for branch
in lostAndFoundBranches
:
2628 self
.knownBranches
[branch
] = branch
2630 def getBranchMappingFromGitBranches(self
):
2631 branches
= p4BranchesInGit(self
.importIntoRemotes
)
2632 for branch
in branches
.keys():
2633 if branch
== "master":
2636 branch
= branch
[len(self
.projectName
):]
2637 self
.knownBranches
[branch
] = branch
2639 def updateOptionDict(self
, d
):
2641 if self
.keepRepoPath
:
2642 option_keys
['keepRepoPath'] = 1
2644 d
["options"] = ' '.join(sorted(option_keys
.keys()))
2646 def readOptions(self
, d
):
2647 self
.keepRepoPath
= (d
.has_key('options')
2648 and ('keepRepoPath' in d
['options']))
2650 def gitRefForBranch(self
, branch
):
2651 if branch
== "main":
2652 return self
.refPrefix
+ "master"
2654 if len(branch
) <= 0:
2657 return self
.refPrefix
+ self
.projectName
+ branch
2659 def gitCommitByP4Change(self
, ref
, change
):
2661 print "looking in ref " + ref
+ " for change %s using bisect..." % change
2664 latestCommit
= parseRevision(ref
)
2668 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
2669 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
2674 log
= extractLogMessageFromGitCommit(next
)
2675 settings
= extractSettingsGitLog(log
)
2676 currentChange
= int(settings
['change'])
2678 print "current change %s" % currentChange
2680 if currentChange
== change
:
2682 print "found %s" % next
2685 if currentChange
< change
:
2686 earliestCommit
= "^%s" % next
2688 latestCommit
= "%s" % next
2692 def importNewBranch(self
, branch
, maxChange
):
2693 # make fast-import flush all changes to disk and update the refs using the checkpoint
2694 # command so that we can try to find the branch parent in the git history
2695 self
.gitStream
.write("checkpoint\n\n");
2696 self
.gitStream
.flush();
2697 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
2698 range = "@1,%s" % maxChange
2699 #print "prefix" + branchPrefix
2700 changes
= p4ChangesForPaths([branchPrefix
], range, self
.changes_block_size
)
2701 if len(changes
) <= 0:
2703 firstChange
= changes
[0]
2704 #print "first change in branch: %s" % firstChange
2705 sourceBranch
= self
.knownBranches
[branch
]
2706 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
2707 sourceRef
= self
.gitRefForBranch(sourceBranch
)
2708 #print "source " + sourceBranch
2710 branchParentChange
= int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath
, firstChange
)])["change"])
2711 #print "branch parent: %s" % branchParentChange
2712 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
2713 if len(gitParent
) > 0:
2714 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
2715 #print "parent git commit: %s" % gitParent
2717 self
.importChanges(changes
)
2720 def searchParent(self
, parent
, branch
, target
):
2722 for blob
in read_pipe_lines(["git", "rev-list", "--reverse",
2723 "--no-merges", parent
]):
2725 if len(read_pipe(["git", "diff-tree", blob
, target
])) == 0:
2728 print "Found parent of %s in commit %s" % (branch
, blob
)
2735 def importChanges(self
, changes
):
2737 for change
in changes
:
2738 description
= p4_describe(change
)
2739 self
.updateOptionDict(description
)
2742 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
2747 if self
.detectBranches
:
2748 branches
= self
.splitFilesIntoBranches(description
)
2749 for branch
in branches
.keys():
2751 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
2752 self
.branchPrefixes
= [ branchPrefix
]
2756 filesForCommit
= branches
[branch
]
2759 print "branch is %s" % branch
2761 self
.updatedBranches
.add(branch
)
2763 if branch
not in self
.createdBranches
:
2764 self
.createdBranches
.add(branch
)
2765 parent
= self
.knownBranches
[branch
]
2766 if parent
== branch
:
2769 fullBranch
= self
.projectName
+ branch
2770 if fullBranch
not in self
.p4BranchesInGit
:
2772 print("\n Importing new branch %s" % fullBranch
);
2773 if self
.importNewBranch(branch
, change
- 1):
2775 self
.p4BranchesInGit
.append(fullBranch
)
2777 print("\n Resuming with change %s" % change
);
2780 print "parent determined through known branches: %s" % parent
2782 branch
= self
.gitRefForBranch(branch
)
2783 parent
= self
.gitRefForBranch(parent
)
2786 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
2788 if len(parent
) == 0 and branch
in self
.initialParents
:
2789 parent
= self
.initialParents
[branch
]
2790 del self
.initialParents
[branch
]
2794 tempBranch
= "%s/%d" % (self
.tempBranchLocation
, change
)
2796 print "Creating temporary branch: " + tempBranch
2797 self
.commit(description
, filesForCommit
, tempBranch
)
2798 self
.tempBranches
.append(tempBranch
)
2800 blob
= self
.searchParent(parent
, branch
, tempBranch
)
2802 self
.commit(description
, filesForCommit
, branch
, blob
)
2805 print "Parent of %s not found. Committing into head of %s" % (branch
, parent
)
2806 self
.commit(description
, filesForCommit
, branch
, parent
)
2808 files
= self
.extractFilesFromCommit(description
)
2809 self
.commit(description
, files
, self
.branch
,
2811 # only needed once, to connect to the previous commit
2812 self
.initialParent
= ""
2814 print self
.gitError
.read()
2817 def importHeadRevision(self
, revision
):
2818 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
2821 details
["user"] = "git perforce import user"
2822 details
["desc"] = ("Initial import of %s from the state at revision %s\n"
2823 % (' '.join(self
.depotPaths
), revision
))
2824 details
["change"] = revision
2828 fileArgs
= ["%s...%s" % (p
,revision
) for p
in self
.depotPaths
]
2830 for info
in p4CmdList(["files"] + fileArgs
):
2832 if 'code' in info
and info
['code'] == 'error':
2833 sys
.stderr
.write("p4 returned an error: %s\n"
2835 if info
['data'].find("must refer to client") >= 0:
2836 sys
.stderr
.write("This particular p4 error is misleading.\n")
2837 sys
.stderr
.write("Perhaps the depot path was misspelled.\n");
2838 sys
.stderr
.write("Depot path: %s\n" % " ".join(self
.depotPaths
))
2840 if 'p4ExitCode' in info
:
2841 sys
.stderr
.write("p4 exitcode: %s\n" % info
['p4ExitCode'])
2845 change
= int(info
["change"])
2846 if change
> newestRevision
:
2847 newestRevision
= change
2849 if info
["action"] in self
.delete_actions
:
2850 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2851 #fileCnt = fileCnt + 1
2854 for prop
in ["depotFile", "rev", "action", "type" ]:
2855 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
2857 fileCnt
= fileCnt
+ 1
2859 details
["change"] = newestRevision
2861 # Use time from top-most change so that all git p4 clones of
2862 # the same p4 repo have the same commit SHA1s.
2863 res
= p4_describe(newestRevision
)
2864 details
["time"] = res
["time"]
2866 self
.updateOptionDict(details
)
2868 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
)
2870 print "IO error with git fast-import. Is your git version recent enough?"
2871 print self
.gitError
.read()
2874 def run(self
, args
):
2875 self
.depotPaths
= []
2876 self
.changeRange
= ""
2877 self
.previousDepotPaths
= []
2878 self
.hasOrigin
= False
2880 # map from branch depot path to parent branch
2881 self
.knownBranches
= {}
2882 self
.initialParents
= {}
2884 if self
.importIntoRemotes
:
2885 self
.refPrefix
= "refs/remotes/p4/"
2887 self
.refPrefix
= "refs/heads/p4/"
2889 if self
.syncWithOrigin
:
2890 self
.hasOrigin
= originP4BranchesExist()
2893 print 'Syncing with origin first, using "git fetch origin"'
2894 system("git fetch origin")
2896 branch_arg_given
= bool(self
.branch
)
2897 if len(self
.branch
) == 0:
2898 self
.branch
= self
.refPrefix
+ "master"
2899 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
2900 system("git update-ref %s refs/heads/p4" % self
.branch
)
2901 system("git branch -D p4")
2903 # accept either the command-line option, or the configuration variable
2904 if self
.useClientSpec
:
2905 # will use this after clone to set the variable
2906 self
.useClientSpec_from_options
= True
2908 if gitConfigBool("git-p4.useclientspec"):
2909 self
.useClientSpec
= True
2910 if self
.useClientSpec
:
2911 self
.clientSpecDirs
= getClientSpec()
2913 # TODO: should always look at previous commits,
2914 # merge with previous imports, if possible.
2917 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
2919 # branches holds mapping from branch name to sha1
2920 branches
= p4BranchesInGit(self
.importIntoRemotes
)
2922 # restrict to just this one, disabling detect-branches
2923 if branch_arg_given
:
2924 short
= self
.branch
.split("/")[-1]
2925 if short
in branches
:
2926 self
.p4BranchesInGit
= [ short
]
2928 self
.p4BranchesInGit
= branches
.keys()
2930 if len(self
.p4BranchesInGit
) > 1:
2932 print "Importing from/into multiple branches"
2933 self
.detectBranches
= True
2934 for branch
in branches
.keys():
2935 self
.initialParents
[self
.refPrefix
+ branch
] = \
2939 print "branches: %s" % self
.p4BranchesInGit
2942 for branch
in self
.p4BranchesInGit
:
2943 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
2945 settings
= extractSettingsGitLog(logMsg
)
2947 self
.readOptions(settings
)
2948 if (settings
.has_key('depot-paths')
2949 and settings
.has_key ('change')):
2950 change
= int(settings
['change']) + 1
2951 p4Change
= max(p4Change
, change
)
2953 depotPaths
= sorted(settings
['depot-paths'])
2954 if self
.previousDepotPaths
== []:
2955 self
.previousDepotPaths
= depotPaths
2958 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
2959 prev_list
= prev
.split("/")
2960 cur_list
= cur
.split("/")
2961 for i
in range(0, min(len(cur_list
), len(prev_list
))):
2962 if cur_list
[i
] <> prev_list
[i
]:
2966 paths
.append ("/".join(cur_list
[:i
+ 1]))
2968 self
.previousDepotPaths
= paths
2971 self
.depotPaths
= sorted(self
.previousDepotPaths
)
2972 self
.changeRange
= "@%s,#head" % p4Change
2973 if not self
.silent
and not self
.detectBranches
:
2974 print "Performing incremental import into %s git branch" % self
.branch
2976 # accept multiple ref name abbreviations:
2977 # refs/foo/bar/branch -> use it exactly
2978 # p4/branch -> prepend refs/remotes/ or refs/heads/
2979 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2980 if not self
.branch
.startswith("refs/"):
2981 if self
.importIntoRemotes
:
2982 prepend
= "refs/remotes/"
2984 prepend
= "refs/heads/"
2985 if not self
.branch
.startswith("p4/"):
2987 self
.branch
= prepend
+ self
.branch
2989 if len(args
) == 0 and self
.depotPaths
:
2991 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
2993 if self
.depotPaths
and self
.depotPaths
!= args
:
2994 print ("previous import used depot path %s and now %s was specified. "
2995 "This doesn't work!" % (' '.join (self
.depotPaths
),
2999 self
.depotPaths
= sorted(args
)
3004 # Make sure no revision specifiers are used when --changesfile
3006 bad_changesfile
= False
3007 if len(self
.changesFile
) > 0:
3008 for p
in self
.depotPaths
:
3009 if p
.find("@") >= 0 or p
.find("#") >= 0:
3010 bad_changesfile
= True
3013 die("Option --changesfile is incompatible with revision specifiers")
3016 for p
in self
.depotPaths
:
3017 if p
.find("@") != -1:
3018 atIdx
= p
.index("@")
3019 self
.changeRange
= p
[atIdx
:]
3020 if self
.changeRange
== "@all":
3021 self
.changeRange
= ""
3022 elif ',' not in self
.changeRange
:
3023 revision
= self
.changeRange
3024 self
.changeRange
= ""
3026 elif p
.find("#") != -1:
3027 hashIdx
= p
.index("#")
3028 revision
= p
[hashIdx
:]
3030 elif self
.previousDepotPaths
== []:
3031 # pay attention to changesfile, if given, else import
3032 # the entire p4 tree at the head revision
3033 if len(self
.changesFile
) == 0:
3036 p
= re
.sub ("\.\.\.$", "", p
)
3037 if not p
.endswith("/"):
3042 self
.depotPaths
= newPaths
3044 # --detect-branches may change this for each branch
3045 self
.branchPrefixes
= self
.depotPaths
3047 self
.loadUserMapFromCache()
3049 if self
.detectLabels
:
3052 if self
.detectBranches
:
3053 ## FIXME - what's a P4 projectName ?
3054 self
.projectName
= self
.guessProjectName()
3057 self
.getBranchMappingFromGitBranches()
3059 self
.getBranchMapping()
3061 print "p4-git branches: %s" % self
.p4BranchesInGit
3062 print "initial parents: %s" % self
.initialParents
3063 for b
in self
.p4BranchesInGit
:
3067 b
= b
[len(self
.projectName
):]
3068 self
.createdBranches
.add(b
)
3070 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
3072 self
.importProcess
= subprocess
.Popen(["git", "fast-import"],
3073 stdin
=subprocess
.PIPE
,
3074 stdout
=subprocess
.PIPE
,
3075 stderr
=subprocess
.PIPE
);
3076 self
.gitOutput
= self
.importProcess
.stdout
3077 self
.gitStream
= self
.importProcess
.stdin
3078 self
.gitError
= self
.importProcess
.stderr
3081 self
.importHeadRevision(revision
)
3085 if len(self
.changesFile
) > 0:
3086 output
= open(self
.changesFile
).readlines()
3089 changeSet
.add(int(line
))
3091 for change
in changeSet
:
3092 changes
.append(change
)
3096 # catch "git p4 sync" with no new branches, in a repo that
3097 # does not have any existing p4 branches
3099 if not self
.p4BranchesInGit
:
3100 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3102 # The default branch is master, unless --branch is used to
3103 # specify something else. Make sure it exists, or complain
3104 # nicely about how to use --branch.
3105 if not self
.detectBranches
:
3106 if not branch_exists(self
.branch
):
3107 if branch_arg_given
:
3108 die("Error: branch %s does not exist." % self
.branch
)
3110 die("Error: no branch %s; perhaps specify one with --branch." %
3114 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
3116 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
, self
.changes_block_size
)
3118 if len(self
.maxChanges
) > 0:
3119 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
3121 if len(changes
) == 0:
3123 print "No changes to import!"
3125 if not self
.silent
and not self
.detectBranches
:
3126 print "Import destination: %s" % self
.branch
3128 self
.updatedBranches
= set()
3130 if not self
.detectBranches
:
3132 # start a new branch
3133 self
.initialParent
= ""
3135 # build on a previous revision
3136 self
.initialParent
= parseRevision(self
.branch
)
3138 self
.importChanges(changes
)
3142 if len(self
.updatedBranches
) > 0:
3143 sys
.stdout
.write("Updated branches: ")
3144 for b
in self
.updatedBranches
:
3145 sys
.stdout
.write("%s " % b
)
3146 sys
.stdout
.write("\n")
3148 if gitConfigBool("git-p4.importLabels"):
3149 self
.importLabels
= True
3151 if self
.importLabels
:
3152 p4Labels
= getP4Labels(self
.depotPaths
)
3153 gitTags
= getGitTags()
3155 missingP4Labels
= p4Labels
- gitTags
3156 self
.importP4Labels(self
.gitStream
, missingP4Labels
)
3158 self
.gitStream
.close()
3159 if self
.importProcess
.wait() != 0:
3160 die("fast-import failed: %s" % self
.gitError
.read())
3161 self
.gitOutput
.close()
3162 self
.gitError
.close()
3164 # Cleanup temporary branches created during import
3165 if self
.tempBranches
!= []:
3166 for branch
in self
.tempBranches
:
3167 read_pipe("git update-ref -d %s" % branch
)
3168 os
.rmdir(os
.path
.join(os
.environ
.get("GIT_DIR", ".git"), self
.tempBranchLocation
))
3170 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3171 # a convenient shortcut refname "p4".
3172 if self
.importIntoRemotes
:
3173 head_ref
= self
.refPrefix
+ "HEAD"
3174 if not gitBranchExists(head_ref
) and gitBranchExists(self
.branch
):
3175 system(["git", "symbolic-ref", head_ref
, self
.branch
])
3179 class P4Rebase(Command
):
3181 Command
.__init
__(self
)
3183 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
3185 self
.importLabels
= False
3186 self
.description
= ("Fetches the latest revision from perforce and "
3187 + "rebases the current work (branch) against it")
3189 def run(self
, args
):
3191 sync
.importLabels
= self
.importLabels
3194 return self
.rebase()
3197 if os
.system("git update-index --refresh") != 0:
3198 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.");
3199 if len(read_pipe("git diff-index HEAD --")) > 0:
3200 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3202 [upstream
, settings
] = findUpstreamBranchPoint()
3203 if len(upstream
) == 0:
3204 die("Cannot find upstream branchpoint for rebase")
3206 # the branchpoint may be p4/foo~3, so strip off the parent
3207 upstream
= re
.sub("~[0-9]+$", "", upstream
)
3209 print "Rebasing the current branch onto %s" % upstream
3210 oldHead
= read_pipe("git rev-parse HEAD").strip()
3211 system("git rebase %s" % upstream
)
3212 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead
)
3215 class P4Clone(P4Sync
):
3217 P4Sync
.__init
__(self
)
3218 self
.description
= "Creates a new git repository and imports from Perforce into it"
3219 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
3221 optparse
.make_option("--destination", dest
="cloneDestination",
3222 action
='store', default
=None,
3223 help="where to leave result of the clone"),
3224 optparse
.make_option("--bare", dest
="cloneBare",
3225 action
="store_true", default
=False),
3227 self
.cloneDestination
= None
3228 self
.needsGit
= False
3229 self
.cloneBare
= False
3231 def defaultDestination(self
, args
):
3232 ## TODO: use common prefix of args?
3234 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
3235 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
3236 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
3237 depotDir
= re
.sub(r
"/$", "", depotDir
)
3238 return os
.path
.split(depotDir
)[1]
3240 def run(self
, args
):
3244 if self
.keepRepoPath
and not self
.cloneDestination
:
3245 sys
.stderr
.write("Must specify destination for --keep-path\n")
3250 if not self
.cloneDestination
and len(depotPaths
) > 1:
3251 self
.cloneDestination
= depotPaths
[-1]
3252 depotPaths
= depotPaths
[:-1]
3254 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
3255 for p
in depotPaths
:
3256 if not p
.startswith("//"):
3257 sys
.stderr
.write('Depot paths must start with "//": %s\n' % p
)
3260 if not self
.cloneDestination
:
3261 self
.cloneDestination
= self
.defaultDestination(args
)
3263 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
3265 if not os
.path
.exists(self
.cloneDestination
):
3266 os
.makedirs(self
.cloneDestination
)
3267 chdir(self
.cloneDestination
)
3269 init_cmd
= [ "git", "init" ]
3271 init_cmd
.append("--bare")
3272 retcode
= subprocess
.call(init_cmd
)
3274 raise CalledProcessError(retcode
, init_cmd
)
3276 if not P4Sync
.run(self
, depotPaths
):
3279 # create a master branch and check out a work tree
3280 if gitBranchExists(self
.branch
):
3281 system([ "git", "branch", "master", self
.branch
])
3282 if not self
.cloneBare
:
3283 system([ "git", "checkout", "-f" ])
3285 print 'Not checking out any branch, use ' \
3286 '"git checkout -q -b master <branch>"'
3288 # auto-set this variable if invoked with --use-client-spec
3289 if self
.useClientSpec_from_options
:
3290 system("git config --bool git-p4.useclientspec true")
3294 class P4Branches(Command
):
3296 Command
.__init
__(self
)
3298 self
.description
= ("Shows the git branches that hold imports and their "
3299 + "corresponding perforce depot paths")
3300 self
.verbose
= False
3302 def run(self
, args
):
3303 if originP4BranchesExist():
3304 createOrUpdateBranchesFromOrigin()
3306 cmdline
= "git rev-parse --symbolic "
3307 cmdline
+= " --remotes"
3309 for line
in read_pipe_lines(cmdline
):
3312 if not line
.startswith('p4/') or line
== "p4/HEAD":
3316 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
3317 settings
= extractSettingsGitLog(log
)
3319 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
3322 class HelpFormatter(optparse
.IndentedHelpFormatter
):
3324 optparse
.IndentedHelpFormatter
.__init
__(self
)
3326 def format_description(self
, description
):
3328 return description
+ "\n"
3332 def printUsage(commands
):
3333 print "usage: %s <command> [options]" % sys
.argv
[0]
3335 print "valid commands: %s" % ", ".join(commands
)
3337 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
3342 "submit" : P4Submit
,
3343 "commit" : P4Submit
,
3345 "rebase" : P4Rebase
,
3347 "rollback" : P4RollBack
,
3348 "branches" : P4Branches
3353 if len(sys
.argv
[1:]) == 0:
3354 printUsage(commands
.keys())
3357 cmdName
= sys
.argv
[1]
3359 klass
= commands
[cmdName
]
3362 print "unknown command %s" % cmdName
3364 printUsage(commands
.keys())
3367 options
= cmd
.options
3368 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
3372 options
.append(optparse
.make_option("--verbose", "-v", dest
="verbose", action
="store_true"))
3374 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
3376 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
3378 description
= cmd
.description
,
3379 formatter
= HelpFormatter())
3381 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
3383 verbose
= cmd
.verbose
3385 if cmd
.gitdir
== None:
3386 cmd
.gitdir
= os
.path
.abspath(".git")
3387 if not isValidGitDir(cmd
.gitdir
):
3388 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
3389 if os
.path
.exists(cmd
.gitdir
):
3390 cdup
= read_pipe("git rev-parse --show-cdup").strip()
3394 if not isValidGitDir(cmd
.gitdir
):
3395 if isValidGitDir(cmd
.gitdir
+ "/.git"):
3396 cmd
.gitdir
+= "/.git"
3398 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
3400 os
.environ
["GIT_DIR"] = cmd
.gitdir
3402 if not cmd
.run(args
):
3407 if __name__
== '__main__':