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 filesForCommit
.append(f
)
2311 if f
['action'] in self
.delete_actions
:
2312 filesToDelete
.append(f
)
2314 filesToRead
.append(f
)
2317 for f
in filesToDelete
:
2318 self
.streamOneP4Deletion(f
)
2320 if len(filesToRead
) > 0:
2321 self
.stream_file
= {}
2322 self
.stream_contents
= []
2323 self
.stream_have_file_info
= False
2325 # curry self argument
2326 def streamP4FilesCbSelf(entry
):
2327 self
.streamP4FilesCb(entry
)
2329 fileArgs
= ['%s#%s' % (f
['path'], f
['rev']) for f
in filesToRead
]
2331 p4CmdList(["-x", "-", "print"],
2333 cb
=streamP4FilesCbSelf
)
2336 if self
.stream_file
.has_key('depotFile'):
2337 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
2339 def make_email(self
, userid
):
2340 if userid
in self
.users
:
2341 return self
.users
[userid
]
2343 return "%s <a@b>" % userid
2345 def streamTag(self
, gitStream
, labelName
, labelDetails
, commit
, epoch
):
2346 """ Stream a p4 tag.
2347 commit is either a git commit, or a fast-import mark, ":<p4commit>"
2351 print "writing tag %s for commit %s" % (labelName
, commit
)
2352 gitStream
.write("tag %s\n" % labelName
)
2353 gitStream
.write("from %s\n" % commit
)
2355 if labelDetails
.has_key('Owner'):
2356 owner
= labelDetails
["Owner"]
2360 # Try to use the owner of the p4 label, or failing that,
2361 # the current p4 user id.
2363 email
= self
.make_email(owner
)
2365 email
= self
.make_email(self
.p4UserId())
2366 tagger
= "%s %s %s" % (email
, epoch
, self
.tz
)
2368 gitStream
.write("tagger %s\n" % tagger
)
2370 print "labelDetails=",labelDetails
2371 if labelDetails
.has_key('Description'):
2372 description
= labelDetails
['Description']
2374 description
= 'Label from git p4'
2376 gitStream
.write("data %d\n" % len(description
))
2377 gitStream
.write(description
)
2378 gitStream
.write("\n")
2380 def inClientSpec(self
, path
):
2381 if not self
.clientSpecDirs
:
2383 inClientSpec
= self
.clientSpecDirs
.map_in_client(path
)
2384 if not inClientSpec
and self
.verbose
:
2385 print('Ignoring file outside of client spec: {0}'.format(path
))
2388 def hasBranchPrefix(self
, path
):
2389 if not self
.branchPrefixes
:
2391 hasPrefix
= [p
for p
in self
.branchPrefixes
2392 if p4PathStartsWith(path
, p
)]
2393 if hasPrefix
and self
.verbose
:
2394 print('Ignoring file outside of prefix: {0}'.format(path
))
2397 def commit(self
, details
, files
, branch
, parent
= ""):
2398 epoch
= details
["time"]
2399 author
= details
["user"]
2402 print('commit into {0}'.format(branch
))
2404 if self
.clientSpecDirs
:
2405 self
.clientSpecDirs
.update_client_spec_path_cache(files
)
2407 files
= [f
for f
in files
2408 if self
.inClientSpec(f
['path']) and self
.hasBranchPrefix(f
['path'])]
2410 if not files
and not gitConfigBool('git-p4.keepEmptyCommits'):
2411 print('Ignoring revision {0} as it would produce an empty commit.'
2412 .format(details
['change']))
2415 self
.gitStream
.write("commit %s\n" % branch
)
2416 self
.gitStream
.write("mark :%s\n" % details
["change"])
2417 self
.committedChanges
.add(int(details
["change"]))
2419 if author
not in self
.users
:
2420 self
.getUserMapFromPerforceServer()
2421 committer
= "%s %s %s" % (self
.make_email(author
), epoch
, self
.tz
)
2423 self
.gitStream
.write("committer %s\n" % committer
)
2425 self
.gitStream
.write("data <<EOT\n")
2426 self
.gitStream
.write(details
["desc"])
2427 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2428 (','.join(self
.branchPrefixes
), details
["change"]))
2429 if len(details
['options']) > 0:
2430 self
.gitStream
.write(": options = %s" % details
['options'])
2431 self
.gitStream
.write("]\nEOT\n\n")
2435 print "parent %s" % parent
2436 self
.gitStream
.write("from %s\n" % parent
)
2438 self
.streamP4Files(files
)
2439 self
.gitStream
.write("\n")
2441 change
= int(details
["change"])
2443 if self
.labels
.has_key(change
):
2444 label
= self
.labels
[change
]
2445 labelDetails
= label
[0]
2446 labelRevisions
= label
[1]
2448 print "Change %s is labelled %s" % (change
, labelDetails
)
2450 files
= p4CmdList(["files"] + ["%s...@%s" % (p
, change
)
2451 for p
in self
.branchPrefixes
])
2453 if len(files
) == len(labelRevisions
):
2457 if info
["action"] in self
.delete_actions
:
2459 cleanedFiles
[info
["depotFile"]] = info
["rev"]
2461 if cleanedFiles
== labelRevisions
:
2462 self
.streamTag(self
.gitStream
, 'tag_%s' % labelDetails
['label'], labelDetails
, branch
, epoch
)
2466 print ("Tag %s does not match with change %s: files do not match."
2467 % (labelDetails
["label"], change
))
2471 print ("Tag %s does not match with change %s: file count is different."
2472 % (labelDetails
["label"], change
))
2474 # Build a dictionary of changelists and labels, for "detect-labels" option.
2475 def getLabels(self
):
2478 l
= p4CmdList(["labels"] + ["%s..." % p
for p
in self
.depotPaths
])
2479 if len(l
) > 0 and not self
.silent
:
2480 print "Finding files belonging to labels in %s" % `self
.depotPaths`
2483 label
= output
["label"]
2487 print "Querying files for label %s" % label
2488 for file in p4CmdList(["files"] +
2489 ["%s...@%s" % (p
, label
)
2490 for p
in self
.depotPaths
]):
2491 revisions
[file["depotFile"]] = file["rev"]
2492 change
= int(file["change"])
2493 if change
> newestChange
:
2494 newestChange
= change
2496 self
.labels
[newestChange
] = [output
, revisions
]
2499 print "Label changes: %s" % self
.labels
.keys()
2501 # Import p4 labels as git tags. A direct mapping does not
2502 # exist, so assume that if all the files are at the same revision
2503 # then we can use that, or it's something more complicated we should
2505 def importP4Labels(self
, stream
, p4Labels
):
2507 print "import p4 labels: " + ' '.join(p4Labels
)
2509 ignoredP4Labels
= gitConfigList("git-p4.ignoredP4Labels")
2510 validLabelRegexp
= gitConfig("git-p4.labelImportRegexp")
2511 if len(validLabelRegexp
) == 0:
2512 validLabelRegexp
= defaultLabelRegexp
2513 m
= re
.compile(validLabelRegexp
)
2515 for name
in p4Labels
:
2518 if not m
.match(name
):
2520 print "label %s does not match regexp %s" % (name
,validLabelRegexp
)
2523 if name
in ignoredP4Labels
:
2526 labelDetails
= p4CmdList(['label', "-o", name
])[0]
2528 # get the most recent changelist for each file in this label
2529 change
= p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p
, name
)
2530 for p
in self
.depotPaths
])
2532 if change
.has_key('change'):
2533 # find the corresponding git commit; take the oldest commit
2534 changelist
= int(change
['change'])
2535 if changelist
in self
.committedChanges
:
2536 gitCommit
= ":%d" % changelist
# use a fast-import mark
2539 gitCommit
= read_pipe(["git", "rev-list", "--max-count=1",
2540 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist
], ignore_error
=True)
2541 if len(gitCommit
) == 0:
2542 print "importing label %s: could not find git commit for changelist %d" % (name
, changelist
)
2545 gitCommit
= gitCommit
.strip()
2548 # Convert from p4 time format
2550 tmwhen
= time
.strptime(labelDetails
['Update'], "%Y/%m/%d %H:%M:%S")
2552 print "Could not convert label time %s" % labelDetails
['Update']
2555 when
= int(time
.mktime(tmwhen
))
2556 self
.streamTag(stream
, name
, labelDetails
, gitCommit
, when
)
2558 print "p4 label %s mapped to git commit %s" % (name
, gitCommit
)
2561 print "Label %s has no changelists - possibly deleted?" % name
2564 # We can't import this label; don't try again as it will get very
2565 # expensive repeatedly fetching all the files for labels that will
2566 # never be imported. If the label is moved in the future, the
2567 # ignore will need to be removed manually.
2568 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name
])
2570 def guessProjectName(self
):
2571 for p
in self
.depotPaths
:
2574 p
= p
[p
.strip().rfind("/") + 1:]
2575 if not p
.endswith("/"):
2579 def getBranchMapping(self
):
2580 lostAndFoundBranches
= set()
2582 user
= gitConfig("git-p4.branchUser")
2584 command
= "branches -u %s" % user
2586 command
= "branches"
2588 for info
in p4CmdList(command
):
2589 details
= p4Cmd(["branch", "-o", info
["branch"]])
2591 while details
.has_key("View%s" % viewIdx
):
2592 paths
= details
["View%s" % viewIdx
].split(" ")
2593 viewIdx
= viewIdx
+ 1
2594 # require standard //depot/foo/... //depot/bar/... mapping
2595 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
2598 destination
= paths
[1]
2600 if p4PathStartsWith(source
, self
.depotPaths
[0]) and p4PathStartsWith(destination
, self
.depotPaths
[0]):
2601 source
= source
[len(self
.depotPaths
[0]):-4]
2602 destination
= destination
[len(self
.depotPaths
[0]):-4]
2604 if destination
in self
.knownBranches
:
2606 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
2607 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
2610 self
.knownBranches
[destination
] = source
2612 lostAndFoundBranches
.discard(destination
)
2614 if source
not in self
.knownBranches
:
2615 lostAndFoundBranches
.add(source
)
2617 # Perforce does not strictly require branches to be defined, so we also
2618 # check git config for a branch list.
2620 # Example of branch definition in git config file:
2622 # branchList=main:branchA
2623 # branchList=main:branchB
2624 # branchList=branchA:branchC
2625 configBranches
= gitConfigList("git-p4.branchList")
2626 for branch
in configBranches
:
2628 (source
, destination
) = branch
.split(":")
2629 self
.knownBranches
[destination
] = source
2631 lostAndFoundBranches
.discard(destination
)
2633 if source
not in self
.knownBranches
:
2634 lostAndFoundBranches
.add(source
)
2637 for branch
in lostAndFoundBranches
:
2638 self
.knownBranches
[branch
] = branch
2640 def getBranchMappingFromGitBranches(self
):
2641 branches
= p4BranchesInGit(self
.importIntoRemotes
)
2642 for branch
in branches
.keys():
2643 if branch
== "master":
2646 branch
= branch
[len(self
.projectName
):]
2647 self
.knownBranches
[branch
] = branch
2649 def updateOptionDict(self
, d
):
2651 if self
.keepRepoPath
:
2652 option_keys
['keepRepoPath'] = 1
2654 d
["options"] = ' '.join(sorted(option_keys
.keys()))
2656 def readOptions(self
, d
):
2657 self
.keepRepoPath
= (d
.has_key('options')
2658 and ('keepRepoPath' in d
['options']))
2660 def gitRefForBranch(self
, branch
):
2661 if branch
== "main":
2662 return self
.refPrefix
+ "master"
2664 if len(branch
) <= 0:
2667 return self
.refPrefix
+ self
.projectName
+ branch
2669 def gitCommitByP4Change(self
, ref
, change
):
2671 print "looking in ref " + ref
+ " for change %s using bisect..." % change
2674 latestCommit
= parseRevision(ref
)
2678 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
2679 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
2684 log
= extractLogMessageFromGitCommit(next
)
2685 settings
= extractSettingsGitLog(log
)
2686 currentChange
= int(settings
['change'])
2688 print "current change %s" % currentChange
2690 if currentChange
== change
:
2692 print "found %s" % next
2695 if currentChange
< change
:
2696 earliestCommit
= "^%s" % next
2698 latestCommit
= "%s" % next
2702 def importNewBranch(self
, branch
, maxChange
):
2703 # make fast-import flush all changes to disk and update the refs using the checkpoint
2704 # command so that we can try to find the branch parent in the git history
2705 self
.gitStream
.write("checkpoint\n\n");
2706 self
.gitStream
.flush();
2707 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
2708 range = "@1,%s" % maxChange
2709 #print "prefix" + branchPrefix
2710 changes
= p4ChangesForPaths([branchPrefix
], range, self
.changes_block_size
)
2711 if len(changes
) <= 0:
2713 firstChange
= changes
[0]
2714 #print "first change in branch: %s" % firstChange
2715 sourceBranch
= self
.knownBranches
[branch
]
2716 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
2717 sourceRef
= self
.gitRefForBranch(sourceBranch
)
2718 #print "source " + sourceBranch
2720 branchParentChange
= int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath
, firstChange
)])["change"])
2721 #print "branch parent: %s" % branchParentChange
2722 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
2723 if len(gitParent
) > 0:
2724 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
2725 #print "parent git commit: %s" % gitParent
2727 self
.importChanges(changes
)
2730 def searchParent(self
, parent
, branch
, target
):
2732 for blob
in read_pipe_lines(["git", "rev-list", "--reverse",
2733 "--no-merges", parent
]):
2735 if len(read_pipe(["git", "diff-tree", blob
, target
])) == 0:
2738 print "Found parent of %s in commit %s" % (branch
, blob
)
2745 def importChanges(self
, changes
):
2747 for change
in changes
:
2748 description
= p4_describe(change
)
2749 self
.updateOptionDict(description
)
2752 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
2757 if self
.detectBranches
:
2758 branches
= self
.splitFilesIntoBranches(description
)
2759 for branch
in branches
.keys():
2761 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
2762 self
.branchPrefixes
= [ branchPrefix
]
2766 filesForCommit
= branches
[branch
]
2769 print "branch is %s" % branch
2771 self
.updatedBranches
.add(branch
)
2773 if branch
not in self
.createdBranches
:
2774 self
.createdBranches
.add(branch
)
2775 parent
= self
.knownBranches
[branch
]
2776 if parent
== branch
:
2779 fullBranch
= self
.projectName
+ branch
2780 if fullBranch
not in self
.p4BranchesInGit
:
2782 print("\n Importing new branch %s" % fullBranch
);
2783 if self
.importNewBranch(branch
, change
- 1):
2785 self
.p4BranchesInGit
.append(fullBranch
)
2787 print("\n Resuming with change %s" % change
);
2790 print "parent determined through known branches: %s" % parent
2792 branch
= self
.gitRefForBranch(branch
)
2793 parent
= self
.gitRefForBranch(parent
)
2796 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
2798 if len(parent
) == 0 and branch
in self
.initialParents
:
2799 parent
= self
.initialParents
[branch
]
2800 del self
.initialParents
[branch
]
2804 tempBranch
= "%s/%d" % (self
.tempBranchLocation
, change
)
2806 print "Creating temporary branch: " + tempBranch
2807 self
.commit(description
, filesForCommit
, tempBranch
)
2808 self
.tempBranches
.append(tempBranch
)
2810 blob
= self
.searchParent(parent
, branch
, tempBranch
)
2812 self
.commit(description
, filesForCommit
, branch
, blob
)
2815 print "Parent of %s not found. Committing into head of %s" % (branch
, parent
)
2816 self
.commit(description
, filesForCommit
, branch
, parent
)
2818 files
= self
.extractFilesFromCommit(description
)
2819 self
.commit(description
, files
, self
.branch
,
2821 # only needed once, to connect to the previous commit
2822 self
.initialParent
= ""
2824 print self
.gitError
.read()
2827 def importHeadRevision(self
, revision
):
2828 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
2831 details
["user"] = "git perforce import user"
2832 details
["desc"] = ("Initial import of %s from the state at revision %s\n"
2833 % (' '.join(self
.depotPaths
), revision
))
2834 details
["change"] = revision
2838 fileArgs
= ["%s...%s" % (p
,revision
) for p
in self
.depotPaths
]
2840 for info
in p4CmdList(["files"] + fileArgs
):
2842 if 'code' in info
and info
['code'] == 'error':
2843 sys
.stderr
.write("p4 returned an error: %s\n"
2845 if info
['data'].find("must refer to client") >= 0:
2846 sys
.stderr
.write("This particular p4 error is misleading.\n")
2847 sys
.stderr
.write("Perhaps the depot path was misspelled.\n");
2848 sys
.stderr
.write("Depot path: %s\n" % " ".join(self
.depotPaths
))
2850 if 'p4ExitCode' in info
:
2851 sys
.stderr
.write("p4 exitcode: %s\n" % info
['p4ExitCode'])
2855 change
= int(info
["change"])
2856 if change
> newestRevision
:
2857 newestRevision
= change
2859 if info
["action"] in self
.delete_actions
:
2860 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2861 #fileCnt = fileCnt + 1
2864 for prop
in ["depotFile", "rev", "action", "type" ]:
2865 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
2867 fileCnt
= fileCnt
+ 1
2869 details
["change"] = newestRevision
2871 # Use time from top-most change so that all git p4 clones of
2872 # the same p4 repo have the same commit SHA1s.
2873 res
= p4_describe(newestRevision
)
2874 details
["time"] = res
["time"]
2876 self
.updateOptionDict(details
)
2878 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
)
2880 print "IO error with git fast-import. Is your git version recent enough?"
2881 print self
.gitError
.read()
2884 def run(self
, args
):
2885 self
.depotPaths
= []
2886 self
.changeRange
= ""
2887 self
.previousDepotPaths
= []
2888 self
.hasOrigin
= False
2890 # map from branch depot path to parent branch
2891 self
.knownBranches
= {}
2892 self
.initialParents
= {}
2894 if self
.importIntoRemotes
:
2895 self
.refPrefix
= "refs/remotes/p4/"
2897 self
.refPrefix
= "refs/heads/p4/"
2899 if self
.syncWithOrigin
:
2900 self
.hasOrigin
= originP4BranchesExist()
2903 print 'Syncing with origin first, using "git fetch origin"'
2904 system("git fetch origin")
2906 branch_arg_given
= bool(self
.branch
)
2907 if len(self
.branch
) == 0:
2908 self
.branch
= self
.refPrefix
+ "master"
2909 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
2910 system("git update-ref %s refs/heads/p4" % self
.branch
)
2911 system("git branch -D p4")
2913 # accept either the command-line option, or the configuration variable
2914 if self
.useClientSpec
:
2915 # will use this after clone to set the variable
2916 self
.useClientSpec_from_options
= True
2918 if gitConfigBool("git-p4.useclientspec"):
2919 self
.useClientSpec
= True
2920 if self
.useClientSpec
:
2921 self
.clientSpecDirs
= getClientSpec()
2923 # TODO: should always look at previous commits,
2924 # merge with previous imports, if possible.
2927 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
2929 # branches holds mapping from branch name to sha1
2930 branches
= p4BranchesInGit(self
.importIntoRemotes
)
2932 # restrict to just this one, disabling detect-branches
2933 if branch_arg_given
:
2934 short
= self
.branch
.split("/")[-1]
2935 if short
in branches
:
2936 self
.p4BranchesInGit
= [ short
]
2938 self
.p4BranchesInGit
= branches
.keys()
2940 if len(self
.p4BranchesInGit
) > 1:
2942 print "Importing from/into multiple branches"
2943 self
.detectBranches
= True
2944 for branch
in branches
.keys():
2945 self
.initialParents
[self
.refPrefix
+ branch
] = \
2949 print "branches: %s" % self
.p4BranchesInGit
2952 for branch
in self
.p4BranchesInGit
:
2953 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
2955 settings
= extractSettingsGitLog(logMsg
)
2957 self
.readOptions(settings
)
2958 if (settings
.has_key('depot-paths')
2959 and settings
.has_key ('change')):
2960 change
= int(settings
['change']) + 1
2961 p4Change
= max(p4Change
, change
)
2963 depotPaths
= sorted(settings
['depot-paths'])
2964 if self
.previousDepotPaths
== []:
2965 self
.previousDepotPaths
= depotPaths
2968 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
2969 prev_list
= prev
.split("/")
2970 cur_list
= cur
.split("/")
2971 for i
in range(0, min(len(cur_list
), len(prev_list
))):
2972 if cur_list
[i
] <> prev_list
[i
]:
2976 paths
.append ("/".join(cur_list
[:i
+ 1]))
2978 self
.previousDepotPaths
= paths
2981 self
.depotPaths
= sorted(self
.previousDepotPaths
)
2982 self
.changeRange
= "@%s,#head" % p4Change
2983 if not self
.silent
and not self
.detectBranches
:
2984 print "Performing incremental import into %s git branch" % self
.branch
2986 # accept multiple ref name abbreviations:
2987 # refs/foo/bar/branch -> use it exactly
2988 # p4/branch -> prepend refs/remotes/ or refs/heads/
2989 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2990 if not self
.branch
.startswith("refs/"):
2991 if self
.importIntoRemotes
:
2992 prepend
= "refs/remotes/"
2994 prepend
= "refs/heads/"
2995 if not self
.branch
.startswith("p4/"):
2997 self
.branch
= prepend
+ self
.branch
2999 if len(args
) == 0 and self
.depotPaths
:
3001 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
3003 if self
.depotPaths
and self
.depotPaths
!= args
:
3004 print ("previous import used depot path %s and now %s was specified. "
3005 "This doesn't work!" % (' '.join (self
.depotPaths
),
3009 self
.depotPaths
= sorted(args
)
3014 # Make sure no revision specifiers are used when --changesfile
3016 bad_changesfile
= False
3017 if len(self
.changesFile
) > 0:
3018 for p
in self
.depotPaths
:
3019 if p
.find("@") >= 0 or p
.find("#") >= 0:
3020 bad_changesfile
= True
3023 die("Option --changesfile is incompatible with revision specifiers")
3026 for p
in self
.depotPaths
:
3027 if p
.find("@") != -1:
3028 atIdx
= p
.index("@")
3029 self
.changeRange
= p
[atIdx
:]
3030 if self
.changeRange
== "@all":
3031 self
.changeRange
= ""
3032 elif ',' not in self
.changeRange
:
3033 revision
= self
.changeRange
3034 self
.changeRange
= ""
3036 elif p
.find("#") != -1:
3037 hashIdx
= p
.index("#")
3038 revision
= p
[hashIdx
:]
3040 elif self
.previousDepotPaths
== []:
3041 # pay attention to changesfile, if given, else import
3042 # the entire p4 tree at the head revision
3043 if len(self
.changesFile
) == 0:
3046 p
= re
.sub ("\.\.\.$", "", p
)
3047 if not p
.endswith("/"):
3052 self
.depotPaths
= newPaths
3054 # --detect-branches may change this for each branch
3055 self
.branchPrefixes
= self
.depotPaths
3057 self
.loadUserMapFromCache()
3059 if self
.detectLabels
:
3062 if self
.detectBranches
:
3063 ## FIXME - what's a P4 projectName ?
3064 self
.projectName
= self
.guessProjectName()
3067 self
.getBranchMappingFromGitBranches()
3069 self
.getBranchMapping()
3071 print "p4-git branches: %s" % self
.p4BranchesInGit
3072 print "initial parents: %s" % self
.initialParents
3073 for b
in self
.p4BranchesInGit
:
3077 b
= b
[len(self
.projectName
):]
3078 self
.createdBranches
.add(b
)
3080 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
3082 self
.importProcess
= subprocess
.Popen(["git", "fast-import"],
3083 stdin
=subprocess
.PIPE
,
3084 stdout
=subprocess
.PIPE
,
3085 stderr
=subprocess
.PIPE
);
3086 self
.gitOutput
= self
.importProcess
.stdout
3087 self
.gitStream
= self
.importProcess
.stdin
3088 self
.gitError
= self
.importProcess
.stderr
3091 self
.importHeadRevision(revision
)
3095 if len(self
.changesFile
) > 0:
3096 output
= open(self
.changesFile
).readlines()
3099 changeSet
.add(int(line
))
3101 for change
in changeSet
:
3102 changes
.append(change
)
3106 # catch "git p4 sync" with no new branches, in a repo that
3107 # does not have any existing p4 branches
3109 if not self
.p4BranchesInGit
:
3110 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3112 # The default branch is master, unless --branch is used to
3113 # specify something else. Make sure it exists, or complain
3114 # nicely about how to use --branch.
3115 if not self
.detectBranches
:
3116 if not branch_exists(self
.branch
):
3117 if branch_arg_given
:
3118 die("Error: branch %s does not exist." % self
.branch
)
3120 die("Error: no branch %s; perhaps specify one with --branch." %
3124 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
3126 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
, self
.changes_block_size
)
3128 if len(self
.maxChanges
) > 0:
3129 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
3131 if len(changes
) == 0:
3133 print "No changes to import!"
3135 if not self
.silent
and not self
.detectBranches
:
3136 print "Import destination: %s" % self
.branch
3138 self
.updatedBranches
= set()
3140 if not self
.detectBranches
:
3142 # start a new branch
3143 self
.initialParent
= ""
3145 # build on a previous revision
3146 self
.initialParent
= parseRevision(self
.branch
)
3148 self
.importChanges(changes
)
3152 if len(self
.updatedBranches
) > 0:
3153 sys
.stdout
.write("Updated branches: ")
3154 for b
in self
.updatedBranches
:
3155 sys
.stdout
.write("%s " % b
)
3156 sys
.stdout
.write("\n")
3158 if gitConfigBool("git-p4.importLabels"):
3159 self
.importLabels
= True
3161 if self
.importLabels
:
3162 p4Labels
= getP4Labels(self
.depotPaths
)
3163 gitTags
= getGitTags()
3165 missingP4Labels
= p4Labels
- gitTags
3166 self
.importP4Labels(self
.gitStream
, missingP4Labels
)
3168 self
.gitStream
.close()
3169 if self
.importProcess
.wait() != 0:
3170 die("fast-import failed: %s" % self
.gitError
.read())
3171 self
.gitOutput
.close()
3172 self
.gitError
.close()
3174 # Cleanup temporary branches created during import
3175 if self
.tempBranches
!= []:
3176 for branch
in self
.tempBranches
:
3177 read_pipe("git update-ref -d %s" % branch
)
3178 os
.rmdir(os
.path
.join(os
.environ
.get("GIT_DIR", ".git"), self
.tempBranchLocation
))
3180 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3181 # a convenient shortcut refname "p4".
3182 if self
.importIntoRemotes
:
3183 head_ref
= self
.refPrefix
+ "HEAD"
3184 if not gitBranchExists(head_ref
) and gitBranchExists(self
.branch
):
3185 system(["git", "symbolic-ref", head_ref
, self
.branch
])
3189 class P4Rebase(Command
):
3191 Command
.__init
__(self
)
3193 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
3195 self
.importLabels
= False
3196 self
.description
= ("Fetches the latest revision from perforce and "
3197 + "rebases the current work (branch) against it")
3199 def run(self
, args
):
3201 sync
.importLabels
= self
.importLabels
3204 return self
.rebase()
3207 if os
.system("git update-index --refresh") != 0:
3208 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.");
3209 if len(read_pipe("git diff-index HEAD --")) > 0:
3210 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3212 [upstream
, settings
] = findUpstreamBranchPoint()
3213 if len(upstream
) == 0:
3214 die("Cannot find upstream branchpoint for rebase")
3216 # the branchpoint may be p4/foo~3, so strip off the parent
3217 upstream
= re
.sub("~[0-9]+$", "", upstream
)
3219 print "Rebasing the current branch onto %s" % upstream
3220 oldHead
= read_pipe("git rev-parse HEAD").strip()
3221 system("git rebase %s" % upstream
)
3222 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead
)
3225 class P4Clone(P4Sync
):
3227 P4Sync
.__init
__(self
)
3228 self
.description
= "Creates a new git repository and imports from Perforce into it"
3229 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
3231 optparse
.make_option("--destination", dest
="cloneDestination",
3232 action
='store', default
=None,
3233 help="where to leave result of the clone"),
3234 optparse
.make_option("--bare", dest
="cloneBare",
3235 action
="store_true", default
=False),
3237 self
.cloneDestination
= None
3238 self
.needsGit
= False
3239 self
.cloneBare
= False
3241 def defaultDestination(self
, args
):
3242 ## TODO: use common prefix of args?
3244 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
3245 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
3246 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
3247 depotDir
= re
.sub(r
"/$", "", depotDir
)
3248 return os
.path
.split(depotDir
)[1]
3250 def run(self
, args
):
3254 if self
.keepRepoPath
and not self
.cloneDestination
:
3255 sys
.stderr
.write("Must specify destination for --keep-path\n")
3260 if not self
.cloneDestination
and len(depotPaths
) > 1:
3261 self
.cloneDestination
= depotPaths
[-1]
3262 depotPaths
= depotPaths
[:-1]
3264 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
3265 for p
in depotPaths
:
3266 if not p
.startswith("//"):
3267 sys
.stderr
.write('Depot paths must start with "//": %s\n' % p
)
3270 if not self
.cloneDestination
:
3271 self
.cloneDestination
= self
.defaultDestination(args
)
3273 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
3275 if not os
.path
.exists(self
.cloneDestination
):
3276 os
.makedirs(self
.cloneDestination
)
3277 chdir(self
.cloneDestination
)
3279 init_cmd
= [ "git", "init" ]
3281 init_cmd
.append("--bare")
3282 retcode
= subprocess
.call(init_cmd
)
3284 raise CalledProcessError(retcode
, init_cmd
)
3286 if not P4Sync
.run(self
, depotPaths
):
3289 # create a master branch and check out a work tree
3290 if gitBranchExists(self
.branch
):
3291 system([ "git", "branch", "master", self
.branch
])
3292 if not self
.cloneBare
:
3293 system([ "git", "checkout", "-f" ])
3295 print 'Not checking out any branch, use ' \
3296 '"git checkout -q -b master <branch>"'
3298 # auto-set this variable if invoked with --use-client-spec
3299 if self
.useClientSpec_from_options
:
3300 system("git config --bool git-p4.useclientspec true")
3304 class P4Branches(Command
):
3306 Command
.__init
__(self
)
3308 self
.description
= ("Shows the git branches that hold imports and their "
3309 + "corresponding perforce depot paths")
3310 self
.verbose
= False
3312 def run(self
, args
):
3313 if originP4BranchesExist():
3314 createOrUpdateBranchesFromOrigin()
3316 cmdline
= "git rev-parse --symbolic "
3317 cmdline
+= " --remotes"
3319 for line
in read_pipe_lines(cmdline
):
3322 if not line
.startswith('p4/') or line
== "p4/HEAD":
3326 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
3327 settings
= extractSettingsGitLog(log
)
3329 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
3332 class HelpFormatter(optparse
.IndentedHelpFormatter
):
3334 optparse
.IndentedHelpFormatter
.__init
__(self
)
3336 def format_description(self
, description
):
3338 return description
+ "\n"
3342 def printUsage(commands
):
3343 print "usage: %s <command> [options]" % sys
.argv
[0]
3345 print "valid commands: %s" % ", ".join(commands
)
3347 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
3352 "submit" : P4Submit
,
3353 "commit" : P4Submit
,
3355 "rebase" : P4Rebase
,
3357 "rollback" : P4RollBack
,
3358 "branches" : P4Branches
3363 if len(sys
.argv
[1:]) == 0:
3364 printUsage(commands
.keys())
3367 cmdName
= sys
.argv
[1]
3369 klass
= commands
[cmdName
]
3372 print "unknown command %s" % cmdName
3374 printUsage(commands
.keys())
3377 options
= cmd
.options
3378 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
3382 options
.append(optparse
.make_option("--verbose", "-v", dest
="verbose", action
="store_true"))
3384 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
3386 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
3388 description
= cmd
.description
,
3389 formatter
= HelpFormatter())
3391 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
3393 verbose
= cmd
.verbose
3395 if cmd
.gitdir
== None:
3396 cmd
.gitdir
= os
.path
.abspath(".git")
3397 if not isValidGitDir(cmd
.gitdir
):
3398 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
3399 if os
.path
.exists(cmd
.gitdir
):
3400 cdup
= read_pipe("git rev-parse --show-cdup").strip()
3404 if not isValidGitDir(cmd
.gitdir
):
3405 if isValidGitDir(cmd
.gitdir
+ "/.git"):
3406 cmd
.gitdir
+= "/.git"
3408 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
3410 os
.environ
["GIT_DIR"] = cmd
.gitdir
3412 if not cmd
.run(args
):
3417 if __name__
== '__main__':