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")
29 from subprocess
import CalledProcessError
31 # from python2.7:subprocess.py
32 # Exception classes used by this module.
33 class CalledProcessError(Exception):
34 """This exception is raised when a process run by check_call() returns
35 a non-zero exit status. The exit status will be stored in the
36 returncode attribute."""
37 def __init__(self
, returncode
, cmd
):
38 self
.returncode
= returncode
41 return "Command '%s' returned non-zero exit status %d" % (self
.cmd
, self
.returncode
)
45 # Only labels/tags matching this will be imported/exported
46 defaultLabelRegexp
= r
'[a-zA-Z0-9_\-.]+$'
48 # Grab changes in blocks of this many revisions, unless otherwise requested
49 defaultBlockSize
= 512
51 def p4_build_cmd(cmd
):
52 """Build a suitable p4 command line.
54 This consolidates building and returning a p4 command line into one
55 location. It means that hooking into the environment, or other configuration
56 can be done more easily.
60 user
= gitConfig("git-p4.user")
62 real_cmd
+= ["-u",user
]
64 password
= gitConfig("git-p4.password")
66 real_cmd
+= ["-P", password
]
68 port
= gitConfig("git-p4.port")
70 real_cmd
+= ["-p", port
]
72 host
= gitConfig("git-p4.host")
74 real_cmd
+= ["-H", host
]
76 client
= gitConfig("git-p4.client")
78 real_cmd
+= ["-c", client
]
81 if isinstance(cmd
,basestring
):
82 real_cmd
= ' '.join(real_cmd
) + ' ' + cmd
87 def chdir(path
, is_client_path
=False):
88 """Do chdir to the given path, and set the PWD environment
89 variable for use by P4. It does not look at getcwd() output.
90 Since we're not using the shell, it is necessary to set the
91 PWD environment variable explicitly.
93 Normally, expand the path to force it to be absolute. This
94 addresses the use of relative path names inside P4 settings,
95 e.g. P4CONFIG=.p4config. P4 does not simply open the filename
96 as given; it looks for .p4config using PWD.
98 If is_client_path, the path was handed to us directly by p4,
99 and may be a symbolic link. Do not call os.getcwd() in this
100 case, because it will cause p4 to think that PWD is not inside
105 if not is_client_path
:
107 os
.environ
['PWD'] = path
110 """Return free space in bytes on the disk of the given dirname."""
111 if platform
.system() == 'Windows':
112 free_bytes
= ctypes
.c_ulonglong(0)
113 ctypes
.windll
.kernel32
.GetDiskFreeSpaceExW(ctypes
.c_wchar_p(os
.getcwd()), None, None, ctypes
.pointer(free_bytes
))
114 return free_bytes
.value
116 st
= os
.statvfs(os
.getcwd())
117 return st
.f_bavail
* st
.f_frsize
123 sys
.stderr
.write(msg
+ "\n")
126 def write_pipe(c
, stdin
):
128 sys
.stderr
.write('Writing pipe: %s\n' % str(c
))
130 expand
= isinstance(c
,basestring
)
131 p
= subprocess
.Popen(c
, stdin
=subprocess
.PIPE
, shell
=expand
)
133 val
= pipe
.write(stdin
)
136 die('Command failed: %s' % str(c
))
140 def p4_write_pipe(c
, stdin
):
141 real_cmd
= p4_build_cmd(c
)
142 return write_pipe(real_cmd
, stdin
)
144 def read_pipe(c
, ignore_error
=False):
146 sys
.stderr
.write('Reading pipe: %s\n' % str(c
))
148 expand
= isinstance(c
,basestring
)
149 p
= subprocess
.Popen(c
, stdout
=subprocess
.PIPE
, shell
=expand
)
152 if p
.wait() and not ignore_error
:
153 die('Command failed: %s' % str(c
))
157 def p4_read_pipe(c
, ignore_error
=False):
158 real_cmd
= p4_build_cmd(c
)
159 return read_pipe(real_cmd
, ignore_error
)
161 def read_pipe_lines(c
):
163 sys
.stderr
.write('Reading pipe: %s\n' % str(c
))
165 expand
= isinstance(c
, basestring
)
166 p
= subprocess
.Popen(c
, stdout
=subprocess
.PIPE
, shell
=expand
)
168 val
= pipe
.readlines()
169 if pipe
.close() or p
.wait():
170 die('Command failed: %s' % str(c
))
174 def p4_read_pipe_lines(c
):
175 """Specifically invoke p4 on the command supplied. """
176 real_cmd
= p4_build_cmd(c
)
177 return read_pipe_lines(real_cmd
)
179 def p4_has_command(cmd
):
180 """Ask p4 for help on this command. If it returns an error, the
181 command does not exist in this version of p4."""
182 real_cmd
= p4_build_cmd(["help", cmd
])
183 p
= subprocess
.Popen(real_cmd
, stdout
=subprocess
.PIPE
,
184 stderr
=subprocess
.PIPE
)
186 return p
.returncode
== 0
188 def p4_has_move_command():
189 """See if the move command exists, that it supports -k, and that
190 it has not been administratively disabled. The arguments
191 must be correct, but the filenames do not have to exist. Use
192 ones with wildcards so even if they exist, it will fail."""
194 if not p4_has_command("move"):
196 cmd
= p4_build_cmd(["move", "-k", "@from", "@to"])
197 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
198 (out
, err
) = p
.communicate()
199 # return code will be 1 in either case
200 if err
.find("Invalid option") >= 0:
202 if err
.find("disabled") >= 0:
204 # assume it failed because @... was invalid changelist
208 expand
= isinstance(cmd
,basestring
)
210 sys
.stderr
.write("executing %s\n" % str(cmd
))
211 retcode
= subprocess
.call(cmd
, shell
=expand
)
213 raise CalledProcessError(retcode
, cmd
)
216 """Specifically invoke p4 as the system command. """
217 real_cmd
= p4_build_cmd(cmd
)
218 expand
= isinstance(real_cmd
, basestring
)
219 retcode
= subprocess
.call(real_cmd
, shell
=expand
)
221 raise CalledProcessError(retcode
, real_cmd
)
223 _p4_version_string
= None
224 def p4_version_string():
225 """Read the version string, showing just the last line, which
226 hopefully is the interesting version bit.
229 Perforce - The Fast Software Configuration Management System.
230 Copyright 1995-2011 Perforce Software. All rights reserved.
231 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
233 global _p4_version_string
234 if not _p4_version_string
:
235 a
= p4_read_pipe_lines(["-V"])
236 _p4_version_string
= a
[-1].rstrip()
237 return _p4_version_string
239 def p4_integrate(src
, dest
):
240 p4_system(["integrate", "-Dt", wildcard_encode(src
), wildcard_encode(dest
)])
242 def p4_sync(f
, *options
):
243 p4_system(["sync"] + list(options
) + [wildcard_encode(f
)])
246 # forcibly add file names with wildcards
247 if wildcard_present(f
):
248 p4_system(["add", "-f", f
])
250 p4_system(["add", f
])
253 p4_system(["delete", wildcard_encode(f
)])
256 p4_system(["edit", wildcard_encode(f
)])
259 p4_system(["revert", wildcard_encode(f
)])
261 def p4_reopen(type, f
):
262 p4_system(["reopen", "-t", type, wildcard_encode(f
)])
264 def p4_move(src
, dest
):
265 p4_system(["move", "-k", wildcard_encode(src
), wildcard_encode(dest
)])
267 def p4_last_change():
268 results
= p4CmdList(["changes", "-m", "1"])
269 return int(results
[0]['change'])
271 def p4_describe(change
):
272 """Make sure it returns a valid result by checking for
273 the presence of field "time". Return a dict of the
276 ds
= p4CmdList(["describe", "-s", str(change
)])
278 die("p4 describe -s %d did not return 1 result: %s" % (change
, str(ds
)))
282 if "p4ExitCode" in d
:
283 die("p4 describe -s %d exited with %d: %s" % (change
, d
["p4ExitCode"],
286 if d
["code"] == "error":
287 die("p4 describe -s %d returned error code: %s" % (change
, str(d
)))
290 die("p4 describe -s %d returned no \"time\": %s" % (change
, str(d
)))
295 # Canonicalize the p4 type and return a tuple of the
296 # base type, plus any modifiers. See "p4 help filetypes"
297 # for a list and explanation.
299 def split_p4_type(p4type
):
301 p4_filetypes_historical
= {
302 "ctempobj": "binary+Sw",
308 "tempobj": "binary+FSw",
309 "ubinary": "binary+F",
310 "uresource": "resource+F",
311 "uxbinary": "binary+Fx",
312 "xbinary": "binary+x",
314 "xtempobj": "binary+Swx",
316 "xunicode": "unicode+x",
319 if p4type
in p4_filetypes_historical
:
320 p4type
= p4_filetypes_historical
[p4type
]
322 s
= p4type
.split("+")
330 # return the raw p4 type of a file (text, text+ko, etc)
333 results
= p4CmdList(["fstat", "-T", "headType", wildcard_encode(f
)])
334 return results
[0]['headType']
337 # Given a type base and modifier, return a regexp matching
338 # the keywords that can be expanded in the file
340 def p4_keywords_regexp_for_type(base
, type_mods
):
341 if base
in ("text", "unicode", "binary"):
343 if "ko" in type_mods
:
345 elif "k" in type_mods
:
346 kwords
= 'Id|Header|Author|Date|DateTime|Change|File|Revision'
350 \$ # Starts with a dollar, followed by...
351 (%s) # one of the keywords, followed by...
352 (:[^$\n]+)? # possibly an old expansion, followed by...
360 # Given a file, return a regexp matching the possible
361 # RCS keywords that will be expanded, or None for files
362 # with kw expansion turned off.
364 def p4_keywords_regexp_for_file(file):
365 if not os
.path
.exists(file):
368 (type_base
, type_mods
) = split_p4_type(p4_type(file))
369 return p4_keywords_regexp_for_type(type_base
, type_mods
)
371 def setP4ExecBit(file, mode
):
372 # Reopens an already open file and changes the execute bit to match
373 # the execute bit setting in the passed in mode.
377 if not isModeExec(mode
):
378 p4Type
= getP4OpenedType(file)
379 p4Type
= re
.sub('^([cku]?)x(.*)', '\\1\\2', p4Type
)
380 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
381 if p4Type
[-1] == "+":
382 p4Type
= p4Type
[0:-1]
384 p4_reopen(p4Type
, file)
386 def getP4OpenedType(file):
387 # Returns the perforce file type for the given file.
389 result
= p4_read_pipe(["opened", wildcard_encode(file)])
390 match
= re
.match(".*\((.+)\)( \*exclusive\*)?\r?$", result
)
392 return match
.group(1)
394 die("Could not determine file type for %s (result: '%s')" % (file, result
))
396 # Return the set of all p4 labels
397 def getP4Labels(depotPaths
):
399 if isinstance(depotPaths
,basestring
):
400 depotPaths
= [depotPaths
]
402 for l
in p4CmdList(["labels"] + ["%s..." % p
for p
in depotPaths
]):
408 # Return the set of all git tags
411 for line
in read_pipe_lines(["git", "tag"]):
416 def diffTreePattern():
417 # This is a simple generator for the diff tree regex pattern. This could be
418 # a class variable if this and parseDiffTreeEntry were a part of a class.
419 pattern
= re
.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
423 def parseDiffTreeEntry(entry
):
424 """Parses a single diff tree entry into its component elements.
426 See git-diff-tree(1) manpage for details about the format of the diff
427 output. This method returns a dictionary with the following elements:
429 src_mode - The mode of the source file
430 dst_mode - The mode of the destination file
431 src_sha1 - The sha1 for the source file
432 dst_sha1 - The sha1 fr the destination file
433 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
434 status_score - The score for the status (applicable for 'C' and 'R'
435 statuses). This is None if there is no score.
436 src - The path for the source file.
437 dst - The path for the destination file. This is only present for
438 copy or renames. If it is not present, this is None.
440 If the pattern is not matched, None is returned."""
442 match
= diffTreePattern().next().match(entry
)
445 'src_mode': match
.group(1),
446 'dst_mode': match
.group(2),
447 'src_sha1': match
.group(3),
448 'dst_sha1': match
.group(4),
449 'status': match
.group(5),
450 'status_score': match
.group(6),
451 'src': match
.group(7),
452 'dst': match
.group(10)
456 def isModeExec(mode
):
457 # Returns True if the given git mode represents an executable file,
459 return mode
[-3:] == "755"
461 def isModeExecChanged(src_mode
, dst_mode
):
462 return isModeExec(src_mode
) != isModeExec(dst_mode
)
464 def p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b', cb
=None):
466 if isinstance(cmd
,basestring
):
473 cmd
= p4_build_cmd(cmd
)
475 sys
.stderr
.write("Opening pipe: %s\n" % str(cmd
))
477 # Use a temporary file to avoid deadlocks without
478 # subprocess.communicate(), which would put another copy
479 # of stdout into memory.
481 if stdin
is not None:
482 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
483 if isinstance(stdin
,basestring
):
484 stdin_file
.write(stdin
)
487 stdin_file
.write(i
+ '\n')
491 p4
= subprocess
.Popen(cmd
,
494 stdout
=subprocess
.PIPE
)
499 entry
= marshal
.load(p4
.stdout
)
509 entry
["p4ExitCode"] = exitCode
515 list = p4CmdList(cmd
)
521 def p4Where(depotPath
):
522 if not depotPath
.endswith("/"):
524 depotPathLong
= depotPath
+ "..."
525 outputList
= p4CmdList(["where", depotPathLong
])
527 for entry
in outputList
:
528 if "depotFile" in entry
:
529 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
530 # The base path always ends with "/...".
531 if entry
["depotFile"].find(depotPath
) == 0 and entry
["depotFile"][-4:] == "/...":
534 elif "data" in entry
:
535 data
= entry
.get("data")
536 space
= data
.find(" ")
537 if data
[:space
] == depotPath
:
542 if output
["code"] == "error":
546 clientPath
= output
.get("path")
547 elif "data" in output
:
548 data
= output
.get("data")
549 lastSpace
= data
.rfind(" ")
550 clientPath
= data
[lastSpace
+ 1:]
552 if clientPath
.endswith("..."):
553 clientPath
= clientPath
[:-3]
556 def currentGitBranch():
557 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
559 def isValidGitDir(path
):
560 if (os
.path
.exists(path
+ "/HEAD")
561 and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects")):
565 def parseRevision(ref
):
566 return read_pipe("git rev-parse %s" % ref
).strip()
568 def branchExists(ref
):
569 rev
= read_pipe(["git", "rev-parse", "-q", "--verify", ref
],
573 def extractLogMessageFromGitCommit(commit
):
576 ## fixme: title is first line of commit, not 1st paragraph.
578 for log
in read_pipe_lines("git cat-file commit %s" % commit
):
587 def extractSettingsGitLog(log
):
589 for line
in log
.split("\n"):
591 m
= re
.search (r
"^ *\[git-p4: (.*)\]$", line
)
595 assignments
= m
.group(1).split (':')
596 for a
in assignments
:
598 key
= vals
[0].strip()
599 val
= ('='.join (vals
[1:])).strip()
600 if val
.endswith ('\"') and val
.startswith('"'):
605 paths
= values
.get("depot-paths")
607 paths
= values
.get("depot-path")
609 values
['depot-paths'] = paths
.split(',')
612 def gitBranchExists(branch
):
613 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
614 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
);
615 return proc
.wait() == 0;
619 def gitConfig(key
, typeSpecifier
=None):
620 if not _gitConfig
.has_key(key
):
621 cmd
= [ "git", "config" ]
623 cmd
+= [ typeSpecifier
]
625 s
= read_pipe(cmd
, ignore_error
=True)
626 _gitConfig
[key
] = s
.strip()
627 return _gitConfig
[key
]
629 def gitConfigBool(key
):
630 """Return a bool, using git config --bool. It is True only if the
631 variable is set to true, and False if set to false or not present
634 if not _gitConfig
.has_key(key
):
635 _gitConfig
[key
] = gitConfig(key
, '--bool') == "true"
636 return _gitConfig
[key
]
638 def gitConfigInt(key
):
639 if not _gitConfig
.has_key(key
):
640 cmd
= [ "git", "config", "--int", key
]
641 s
= read_pipe(cmd
, ignore_error
=True)
644 _gitConfig
[key
] = int(gitConfig(key
, '--int'))
646 _gitConfig
[key
] = None
647 return _gitConfig
[key
]
649 def gitConfigList(key
):
650 if not _gitConfig
.has_key(key
):
651 s
= read_pipe(["git", "config", "--get-all", key
], ignore_error
=True)
652 _gitConfig
[key
] = s
.strip().split(os
.linesep
)
653 if _gitConfig
[key
] == ['']:
655 return _gitConfig
[key
]
657 def p4BranchesInGit(branchesAreInRemotes
=True):
658 """Find all the branches whose names start with "p4/", looking
659 in remotes or heads as specified by the argument. Return
660 a dictionary of { branch: revision } for each one found.
661 The branch names are the short names, without any
666 cmdline
= "git rev-parse --symbolic "
667 if branchesAreInRemotes
:
668 cmdline
+= "--remotes"
670 cmdline
+= "--branches"
672 for line
in read_pipe_lines(cmdline
):
676 if not line
.startswith('p4/'):
678 # special symbolic ref to p4/master
679 if line
== "p4/HEAD":
682 # strip off p4/ prefix
683 branch
= line
[len("p4/"):]
685 branches
[branch
] = parseRevision(line
)
689 def branch_exists(branch
):
690 """Make sure that the given ref name really exists."""
692 cmd
= [ "git", "rev-parse", "--symbolic", "--verify", branch
]
693 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
694 out
, _
= p
.communicate()
697 # expect exactly one line of output: the branch name
698 return out
.rstrip() == branch
700 def findUpstreamBranchPoint(head
= "HEAD"):
701 branches
= p4BranchesInGit()
702 # map from depot-path to branch name
703 branchByDepotPath
= {}
704 for branch
in branches
.keys():
705 tip
= branches
[branch
]
706 log
= extractLogMessageFromGitCommit(tip
)
707 settings
= extractSettingsGitLog(log
)
708 if settings
.has_key("depot-paths"):
709 paths
= ",".join(settings
["depot-paths"])
710 branchByDepotPath
[paths
] = "remotes/p4/" + branch
714 while parent
< 65535:
715 commit
= head
+ "~%s" % parent
716 log
= extractLogMessageFromGitCommit(commit
)
717 settings
= extractSettingsGitLog(log
)
718 if settings
.has_key("depot-paths"):
719 paths
= ",".join(settings
["depot-paths"])
720 if branchByDepotPath
.has_key(paths
):
721 return [branchByDepotPath
[paths
], settings
]
725 return ["", settings
]
727 def createOrUpdateBranchesFromOrigin(localRefPrefix
= "refs/remotes/p4/", silent
=True):
729 print ("Creating/updating branch(es) in %s based on origin branch(es)"
732 originPrefix
= "origin/p4/"
734 for line
in read_pipe_lines("git rev-parse --symbolic --remotes"):
736 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
739 headName
= line
[len(originPrefix
):]
740 remoteHead
= localRefPrefix
+ headName
743 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
744 if (not original
.has_key('depot-paths')
745 or not original
.has_key('change')):
749 if not gitBranchExists(remoteHead
):
751 print "creating %s" % remoteHead
754 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
755 if settings
.has_key('change') > 0:
756 if settings
['depot-paths'] == original
['depot-paths']:
757 originP4Change
= int(original
['change'])
758 p4Change
= int(settings
['change'])
759 if originP4Change
> p4Change
:
760 print ("%s (%s) is newer than %s (%s). "
761 "Updating p4 branch from origin."
762 % (originHead
, originP4Change
,
763 remoteHead
, p4Change
))
766 print ("Ignoring: %s was imported from %s while "
767 "%s was imported from %s"
768 % (originHead
, ','.join(original
['depot-paths']),
769 remoteHead
, ','.join(settings
['depot-paths'])))
772 system("git update-ref %s %s" % (remoteHead
, originHead
))
774 def originP4BranchesExist():
775 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
778 def p4ParseNumericChangeRange(parts
):
779 changeStart
= int(parts
[0][1:])
780 if parts
[1] == '#head':
781 changeEnd
= p4_last_change()
783 changeEnd
= int(parts
[1])
785 return (changeStart
, changeEnd
)
787 def chooseBlockSize(blockSize
):
791 return defaultBlockSize
793 def p4ChangesForPaths(depotPaths
, changeRange
, requestedBlockSize
):
796 # Parse the change range into start and end. Try to find integer
797 # revision ranges as these can be broken up into blocks to avoid
798 # hitting server-side limits (maxrows, maxscanresults). But if
799 # that doesn't work, fall back to using the raw revision specifier
800 # strings, without using block mode.
802 if changeRange
is None or changeRange
== '':
804 changeEnd
= p4_last_change()
805 block_size
= chooseBlockSize(requestedBlockSize
)
807 parts
= changeRange
.split(',')
808 assert len(parts
) == 2
810 (changeStart
, changeEnd
) = p4ParseNumericChangeRange(parts
)
811 block_size
= chooseBlockSize(requestedBlockSize
)
813 changeStart
= parts
[0][1:]
815 if requestedBlockSize
:
816 die("cannot use --changes-block-size with non-numeric revisions")
819 # Accumulate change numbers in a dictionary to avoid duplicates
823 # Retrieve changes a block at a time, to prevent running
824 # into a MaxResults/MaxScanRows error from the server.
830 end
= min(changeEnd
, changeStart
+ block_size
)
831 revisionRange
= "%d,%d" % (changeStart
, end
)
833 revisionRange
= "%s,%s" % (changeStart
, changeEnd
)
835 cmd
+= ["%s...@%s" % (p
, revisionRange
)]
837 for line
in p4_read_pipe_lines(cmd
):
838 changeNum
= int(line
.split(" ")[1])
839 changes
[changeNum
] = True
847 changeStart
= end
+ 1
849 changelist
= changes
.keys()
853 def p4PathStartsWith(path
, prefix
):
854 # This method tries to remedy a potential mixed-case issue:
856 # If UserA adds //depot/DirA/file1
857 # and UserB adds //depot/dira/file2
859 # we may or may not have a problem. If you have core.ignorecase=true,
860 # we treat DirA and dira as the same directory
861 if gitConfigBool("core.ignorecase"):
862 return path
.lower().startswith(prefix
.lower())
863 return path
.startswith(prefix
)
866 """Look at the p4 client spec, create a View() object that contains
867 all the mappings, and return it."""
869 specList
= p4CmdList("client -o")
870 if len(specList
) != 1:
871 die('Output from "client -o" is %d lines, expecting 1' %
874 # dictionary of all client parameters
878 client_name
= entry
["Client"]
880 # just the keys that start with "View"
881 view_keys
= [ k
for k
in entry
.keys() if k
.startswith("View") ]
884 view
= View(client_name
)
886 # append the lines, in order, to the view
887 for view_num
in range(len(view_keys
)):
888 k
= "View%d" % view_num
889 if k
not in view_keys
:
890 die("Expected view key %s missing" % k
)
891 view
.append(entry
[k
])
896 """Grab the client directory."""
898 output
= p4CmdList("client -o")
900 die('Output from "client -o" is %d lines, expecting 1' % len(output
))
903 if "Root" not in entry
:
904 die('Client has no "Root"')
909 # P4 wildcards are not allowed in filenames. P4 complains
910 # if you simply add them, but you can force it with "-f", in
911 # which case it translates them into %xx encoding internally.
913 def wildcard_decode(path
):
914 # Search for and fix just these four characters. Do % last so
915 # that fixing it does not inadvertently create new %-escapes.
916 # Cannot have * in a filename in windows; untested as to
917 # what p4 would do in such a case.
918 if not platform
.system() == "Windows":
919 path
= path
.replace("%2A", "*")
920 path
= path
.replace("%23", "#") \
921 .replace("%40", "@") \
925 def wildcard_encode(path
):
926 # do % first to avoid double-encoding the %s introduced here
927 path
= path
.replace("%", "%25") \
928 .replace("*", "%2A") \
929 .replace("#", "%23") \
933 def wildcard_present(path
):
934 m
= re
.search("[*#@%]", path
)
937 class LargeFileSystem(object):
938 """Base class for large file system support."""
940 def __init__(self
, writeToGitStream
):
941 self
.largeFiles
= set()
942 self
.writeToGitStream
= writeToGitStream
944 def generatePointer(self
, cloneDestination
, contentFile
):
945 """Return the content of a pointer file that is stored in Git instead of
946 the actual content."""
947 assert False, "Method 'generatePointer' required in " + self
.__class
__.__name
__
949 def pushFile(self
, localLargeFile
):
950 """Push the actual content which is not stored in the Git repository to
952 assert False, "Method 'pushFile' required in " + self
.__class
__.__name
__
954 def hasLargeFileExtension(self
, relPath
):
957 [relPath
.endswith('.' + e
) for e
in gitConfigList('git-p4.largeFileExtensions')],
961 def generateTempFile(self
, contents
):
962 contentFile
= tempfile
.NamedTemporaryFile(prefix
='git-p4-large-file', delete
=False)
966 return contentFile
.name
968 def exceedsLargeFileThreshold(self
, relPath
, contents
):
969 if gitConfigInt('git-p4.largeFileThreshold'):
970 contentsSize
= sum(len(d
) for d
in contents
)
971 if contentsSize
> gitConfigInt('git-p4.largeFileThreshold'):
973 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
974 contentsSize
= sum(len(d
) for d
in contents
)
975 if contentsSize
<= gitConfigInt('git-p4.largeFileCompressedThreshold'):
977 contentTempFile
= self
.generateTempFile(contents
)
978 compressedContentFile
= tempfile
.NamedTemporaryFile(prefix
='git-p4-large-file', delete
=False)
979 zf
= zipfile
.ZipFile(compressedContentFile
.name
, mode
='w')
980 zf
.write(contentTempFile
, compress_type
=zipfile
.ZIP_DEFLATED
)
982 compressedContentsSize
= zf
.infolist()[0].compress_size
983 os
.remove(contentTempFile
)
984 os
.remove(compressedContentFile
.name
)
985 if compressedContentsSize
> gitConfigInt('git-p4.largeFileCompressedThreshold'):
989 def addLargeFile(self
, relPath
):
990 self
.largeFiles
.add(relPath
)
992 def removeLargeFile(self
, relPath
):
993 self
.largeFiles
.remove(relPath
)
995 def isLargeFile(self
, relPath
):
996 return relPath
in self
.largeFiles
998 def processContent(self
, git_mode
, relPath
, contents
):
999 """Processes the content of git fast import. This method decides if a
1000 file is stored in the large file system and handles all necessary
1002 if self
.exceedsLargeFileThreshold(relPath
, contents
) or self
.hasLargeFileExtension(relPath
):
1003 contentTempFile
= self
.generateTempFile(contents
)
1004 (git_mode
, contents
, localLargeFile
) = self
.generatePointer(contentTempFile
)
1006 # Move temp file to final location in large file system
1007 largeFileDir
= os
.path
.dirname(localLargeFile
)
1008 if not os
.path
.isdir(largeFileDir
):
1009 os
.makedirs(largeFileDir
)
1010 shutil
.move(contentTempFile
, localLargeFile
)
1011 self
.addLargeFile(relPath
)
1012 if gitConfigBool('git-p4.largeFilePush'):
1013 self
.pushFile(localLargeFile
)
1015 sys
.stderr
.write("%s moved to large file system (%s)\n" % (relPath
, localLargeFile
))
1016 return (git_mode
, contents
)
1018 class MockLFS(LargeFileSystem
):
1019 """Mock large file system for testing."""
1021 def generatePointer(self
, contentFile
):
1022 """The pointer content is the original content prefixed with "pointer-".
1023 The local filename of the large file storage is derived from the file content.
1025 with
open(contentFile
, 'r') as f
:
1028 pointerContents
= 'pointer-' + content
1029 localLargeFile
= os
.path
.join(os
.getcwd(), '.git', 'mock-storage', 'local', content
[:-1])
1030 return (gitMode
, pointerContents
, localLargeFile
)
1032 def pushFile(self
, localLargeFile
):
1033 """The remote filename of the large file storage is the same as the local
1034 one but in a different directory.
1036 remotePath
= os
.path
.join(os
.path
.dirname(localLargeFile
), '..', 'remote')
1037 if not os
.path
.exists(remotePath
):
1038 os
.makedirs(remotePath
)
1039 shutil
.copyfile(localLargeFile
, os
.path
.join(remotePath
, os
.path
.basename(localLargeFile
)))
1043 self
.usage
= "usage: %prog [options]"
1044 self
.needsGit
= True
1045 self
.verbose
= False
1049 self
.userMapFromPerforceServer
= False
1050 self
.myP4UserId
= None
1054 return self
.myP4UserId
1056 results
= p4CmdList("user -o")
1058 if r
.has_key('User'):
1059 self
.myP4UserId
= r
['User']
1061 die("Could not find your p4 user id")
1063 def p4UserIsMe(self
, p4User
):
1064 # return True if the given p4 user is actually me
1065 me
= self
.p4UserId()
1066 if not p4User
or p4User
!= me
:
1071 def getUserCacheFilename(self
):
1072 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
1073 return home
+ "/.gitp4-usercache.txt"
1075 def getUserMapFromPerforceServer(self
):
1076 if self
.userMapFromPerforceServer
:
1081 for output
in p4CmdList("users"):
1082 if not output
.has_key("User"):
1084 self
.users
[output
["User"]] = output
["FullName"] + " <" + output
["Email"] + ">"
1085 self
.emails
[output
["Email"]] = output
["User"]
1089 for (key
, val
) in self
.users
.items():
1090 s
+= "%s\t%s\n" % (key
.expandtabs(1), val
.expandtabs(1))
1092 open(self
.getUserCacheFilename(), "wb").write(s
)
1093 self
.userMapFromPerforceServer
= True
1095 def loadUserMapFromCache(self
):
1097 self
.userMapFromPerforceServer
= False
1099 cache
= open(self
.getUserCacheFilename(), "rb")
1100 lines
= cache
.readlines()
1103 entry
= line
.strip().split("\t")
1104 self
.users
[entry
[0]] = entry
[1]
1106 self
.getUserMapFromPerforceServer()
1108 class P4Debug(Command
):
1110 Command
.__init
__(self
)
1112 self
.description
= "A tool to debug the output of p4 -G."
1113 self
.needsGit
= False
1115 def run(self
, args
):
1117 for output
in p4CmdList(args
):
1118 print 'Element: %d' % j
1123 class P4RollBack(Command
):
1125 Command
.__init
__(self
)
1127 optparse
.make_option("--local", dest
="rollbackLocalBranches", action
="store_true")
1129 self
.description
= "A tool to debug the multi-branch import. Don't use :)"
1130 self
.rollbackLocalBranches
= False
1132 def run(self
, args
):
1135 maxChange
= int(args
[0])
1137 if "p4ExitCode" in p4Cmd("changes -m 1"):
1138 die("Problems executing p4");
1140 if self
.rollbackLocalBranches
:
1141 refPrefix
= "refs/heads/"
1142 lines
= read_pipe_lines("git rev-parse --symbolic --branches")
1144 refPrefix
= "refs/remotes/"
1145 lines
= read_pipe_lines("git rev-parse --symbolic --remotes")
1148 if self
.rollbackLocalBranches
or (line
.startswith("p4/") and line
!= "p4/HEAD\n"):
1150 ref
= refPrefix
+ line
1151 log
= extractLogMessageFromGitCommit(ref
)
1152 settings
= extractSettingsGitLog(log
)
1154 depotPaths
= settings
['depot-paths']
1155 change
= settings
['change']
1159 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p
, maxChange
)
1160 for p
in depotPaths
]))) == 0:
1161 print "Branch %s did not exist at change %s, deleting." % (ref
, maxChange
)
1162 system("git update-ref -d %s `git rev-parse %s`" % (ref
, ref
))
1165 while change
and int(change
) > maxChange
:
1168 print "%s is at %s ; rewinding towards %s" % (ref
, change
, maxChange
)
1169 system("git update-ref %s \"%s^\"" % (ref
, ref
))
1170 log
= extractLogMessageFromGitCommit(ref
)
1171 settings
= extractSettingsGitLog(log
)
1174 depotPaths
= settings
['depot-paths']
1175 change
= settings
['change']
1178 print "%s rewound to %s" % (ref
, change
)
1182 class P4Submit(Command
, P4UserMap
):
1184 conflict_behavior_choices
= ("ask", "skip", "quit")
1187 Command
.__init
__(self
)
1188 P4UserMap
.__init
__(self
)
1190 optparse
.make_option("--origin", dest
="origin"),
1191 optparse
.make_option("-M", dest
="detectRenames", action
="store_true"),
1192 # preserve the user, requires relevant p4 permissions
1193 optparse
.make_option("--preserve-user", dest
="preserveUser", action
="store_true"),
1194 optparse
.make_option("--export-labels", dest
="exportLabels", action
="store_true"),
1195 optparse
.make_option("--dry-run", "-n", dest
="dry_run", action
="store_true"),
1196 optparse
.make_option("--prepare-p4-only", dest
="prepare_p4_only", action
="store_true"),
1197 optparse
.make_option("--conflict", dest
="conflict_behavior",
1198 choices
=self
.conflict_behavior_choices
),
1199 optparse
.make_option("--branch", dest
="branch"),
1201 self
.description
= "Submit changes from git to the perforce depot."
1202 self
.usage
+= " [name of git branch to submit into perforce depot]"
1204 self
.detectRenames
= False
1205 self
.preserveUser
= gitConfigBool("git-p4.preserveUser")
1206 self
.dry_run
= False
1207 self
.prepare_p4_only
= False
1208 self
.conflict_behavior
= None
1209 self
.isWindows
= (platform
.system() == "Windows")
1210 self
.exportLabels
= False
1211 self
.p4HasMoveCommand
= p4_has_move_command()
1214 if gitConfig('git-p4.largeFileSystem'):
1215 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1218 if len(p4CmdList("opened ...")) > 0:
1219 die("You have files opened with perforce! Close them before starting the sync.")
1221 def separate_jobs_from_description(self
, message
):
1222 """Extract and return a possible Jobs field in the commit
1223 message. It goes into a separate section in the p4 change
1226 A jobs line starts with "Jobs:" and looks like a new field
1227 in a form. Values are white-space separated on the same
1228 line or on following lines that start with a tab.
1230 This does not parse and extract the full git commit message
1231 like a p4 form. It just sees the Jobs: line as a marker
1232 to pass everything from then on directly into the p4 form,
1233 but outside the description section.
1235 Return a tuple (stripped log message, jobs string)."""
1237 m
= re
.search(r
'^Jobs:', message
, re
.MULTILINE
)
1239 return (message
, None)
1241 jobtext
= message
[m
.start():]
1242 stripped_message
= message
[:m
.start()].rstrip()
1243 return (stripped_message
, jobtext
)
1245 def prepareLogMessage(self
, template
, message
, jobs
):
1246 """Edits the template returned from "p4 change -o" to insert
1247 the message in the Description field, and the jobs text in
1251 inDescriptionSection
= False
1253 for line
in template
.split("\n"):
1254 if line
.startswith("#"):
1255 result
+= line
+ "\n"
1258 if inDescriptionSection
:
1259 if line
.startswith("Files:") or line
.startswith("Jobs:"):
1260 inDescriptionSection
= False
1261 # insert Jobs section
1263 result
+= jobs
+ "\n"
1267 if line
.startswith("Description:"):
1268 inDescriptionSection
= True
1270 for messageLine
in message
.split("\n"):
1271 line
+= "\t" + messageLine
+ "\n"
1273 result
+= line
+ "\n"
1277 def patchRCSKeywords(self
, file, pattern
):
1278 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1279 (handle
, outFileName
) = tempfile
.mkstemp(dir='.')
1281 outFile
= os
.fdopen(handle
, "w+")
1282 inFile
= open(file, "r")
1283 regexp
= re
.compile(pattern
, re
.VERBOSE
)
1284 for line
in inFile
.readlines():
1285 line
= regexp
.sub(r
'$\1$', line
)
1289 # Forcibly overwrite the original file
1291 shutil
.move(outFileName
, file)
1293 # cleanup our temporary file
1294 os
.unlink(outFileName
)
1295 print "Failed to strip RCS keywords in %s" % file
1298 print "Patched up RCS keywords in %s" % file
1300 def p4UserForCommit(self
,id):
1301 # Return the tuple (perforce user,git email) for a given git commit id
1302 self
.getUserMapFromPerforceServer()
1303 gitEmail
= read_pipe(["git", "log", "--max-count=1",
1304 "--format=%ae", id])
1305 gitEmail
= gitEmail
.strip()
1306 if not self
.emails
.has_key(gitEmail
):
1307 return (None,gitEmail
)
1309 return (self
.emails
[gitEmail
],gitEmail
)
1311 def checkValidP4Users(self
,commits
):
1312 # check if any git authors cannot be mapped to p4 users
1314 (user
,email
) = self
.p4UserForCommit(id)
1316 msg
= "Cannot find p4 user for email %s in commit %s." % (email
, id)
1317 if gitConfigBool("git-p4.allowMissingP4Users"):
1320 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg
)
1322 def lastP4Changelist(self
):
1323 # Get back the last changelist number submitted in this client spec. This
1324 # then gets used to patch up the username in the change. If the same
1325 # client spec is being used by multiple processes then this might go
1327 results
= p4CmdList("client -o") # find the current client
1330 if r
.has_key('Client'):
1331 client
= r
['Client']
1334 die("could not get client spec")
1335 results
= p4CmdList(["changes", "-c", client
, "-m", "1"])
1337 if r
.has_key('change'):
1339 die("Could not get changelist number for last submit - cannot patch up user details")
1341 def modifyChangelistUser(self
, changelist
, newUser
):
1342 # fixup the user field of a changelist after it has been submitted.
1343 changes
= p4CmdList("change -o %s" % changelist
)
1344 if len(changes
) != 1:
1345 die("Bad output from p4 change modifying %s to user %s" %
1346 (changelist
, newUser
))
1349 if c
['User'] == newUser
: return # nothing to do
1351 input = marshal
.dumps(c
)
1353 result
= p4CmdList("change -f -i", stdin
=input)
1355 if r
.has_key('code'):
1356 if r
['code'] == 'error':
1357 die("Could not modify user field of changelist %s to %s:%s" % (changelist
, newUser
, r
['data']))
1358 if r
.has_key('data'):
1359 print("Updated user field for changelist %s to %s" % (changelist
, newUser
))
1361 die("Could not modify user field of changelist %s to %s" % (changelist
, newUser
))
1363 def canChangeChangelists(self
):
1364 # check to see if we have p4 admin or super-user permissions, either of
1365 # which are required to modify changelists.
1366 results
= p4CmdList(["protects", self
.depotPath
])
1368 if r
.has_key('perm'):
1369 if r
['perm'] == 'admin':
1371 if r
['perm'] == 'super':
1375 def prepareSubmitTemplate(self
):
1376 """Run "p4 change -o" to grab a change specification template.
1377 This does not use "p4 -G", as it is nice to keep the submission
1378 template in original order, since a human might edit it.
1380 Remove lines in the Files section that show changes to files
1381 outside the depot path we're committing into."""
1384 inFilesSection
= False
1385 for line
in p4_read_pipe_lines(['change', '-o']):
1386 if line
.endswith("\r\n"):
1387 line
= line
[:-2] + "\n"
1389 if line
.startswith("\t"):
1390 # path starts and ends with a tab
1392 lastTab
= path
.rfind("\t")
1394 path
= path
[:lastTab
]
1395 if not p4PathStartsWith(path
, self
.depotPath
):
1398 inFilesSection
= False
1400 if line
.startswith("Files:"):
1401 inFilesSection
= True
1407 def edit_template(self
, template_file
):
1408 """Invoke the editor to let the user change the submission
1409 message. Return true if okay to continue with the submit."""
1411 # if configured to skip the editing part, just submit
1412 if gitConfigBool("git-p4.skipSubmitEdit"):
1415 # look at the modification time, to check later if the user saved
1417 mtime
= os
.stat(template_file
).st_mtime
1420 if os
.environ
.has_key("P4EDITOR") and (os
.environ
.get("P4EDITOR") != ""):
1421 editor
= os
.environ
.get("P4EDITOR")
1423 editor
= read_pipe("git var GIT_EDITOR").strip()
1424 system(["sh", "-c", ('%s "$@"' % editor
), editor
, template_file
])
1426 # If the file was not saved, prompt to see if this patch should
1427 # be skipped. But skip this verification step if configured so.
1428 if gitConfigBool("git-p4.skipSubmitEditCheck"):
1431 # modification time updated means user saved the file
1432 if os
.stat(template_file
).st_mtime
> mtime
:
1436 response
= raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1442 def get_diff_description(self
, editedFiles
, filesToAdd
):
1444 if os
.environ
.has_key("P4DIFF"):
1445 del(os
.environ
["P4DIFF"])
1447 for editedFile
in editedFiles
:
1448 diff
+= p4_read_pipe(['diff', '-du',
1449 wildcard_encode(editedFile
)])
1453 for newFile
in filesToAdd
:
1454 newdiff
+= "==== new file ====\n"
1455 newdiff
+= "--- /dev/null\n"
1456 newdiff
+= "+++ %s\n" % newFile
1457 f
= open(newFile
, "r")
1458 for line
in f
.readlines():
1459 newdiff
+= "+" + line
1462 return (diff
+ newdiff
).replace('\r\n', '\n')
1464 def applyCommit(self
, id):
1465 """Apply one commit, return True if it succeeded."""
1467 print "Applying", read_pipe(["git", "show", "-s",
1468 "--format=format:%h %s", id])
1470 (p4User
, gitEmail
) = self
.p4UserForCommit(id)
1472 diff
= read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self
.diffOpts
, id, id))
1474 filesToDelete
= set()
1476 pureRenameCopy
= set()
1477 filesToChangeExecBit
= {}
1480 diff
= parseDiffTreeEntry(line
)
1481 modifier
= diff
['status']
1485 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
1486 filesToChangeExecBit
[path
] = diff
['dst_mode']
1487 editedFiles
.add(path
)
1488 elif modifier
== "A":
1489 filesToAdd
.add(path
)
1490 filesToChangeExecBit
[path
] = diff
['dst_mode']
1491 if path
in filesToDelete
:
1492 filesToDelete
.remove(path
)
1493 elif modifier
== "D":
1494 filesToDelete
.add(path
)
1495 if path
in filesToAdd
:
1496 filesToAdd
.remove(path
)
1497 elif modifier
== "C":
1498 src
, dest
= diff
['src'], diff
['dst']
1499 p4_integrate(src
, dest
)
1500 pureRenameCopy
.add(dest
)
1501 if diff
['src_sha1'] != diff
['dst_sha1']:
1503 pureRenameCopy
.discard(dest
)
1504 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
1506 pureRenameCopy
.discard(dest
)
1507 filesToChangeExecBit
[dest
] = diff
['dst_mode']
1509 # turn off read-only attribute
1510 os
.chmod(dest
, stat
.S_IWRITE
)
1512 editedFiles
.add(dest
)
1513 elif modifier
== "R":
1514 src
, dest
= diff
['src'], diff
['dst']
1515 if self
.p4HasMoveCommand
:
1516 p4_edit(src
) # src must be open before move
1517 p4_move(src
, dest
) # opens for (move/delete, move/add)
1519 p4_integrate(src
, dest
)
1520 if diff
['src_sha1'] != diff
['dst_sha1']:
1523 pureRenameCopy
.add(dest
)
1524 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
1525 if not self
.p4HasMoveCommand
:
1526 p4_edit(dest
) # with move: already open, writable
1527 filesToChangeExecBit
[dest
] = diff
['dst_mode']
1528 if not self
.p4HasMoveCommand
:
1530 os
.chmod(dest
, stat
.S_IWRITE
)
1532 filesToDelete
.add(src
)
1533 editedFiles
.add(dest
)
1535 die("unknown modifier %s for %s" % (modifier
, path
))
1537 diffcmd
= "git diff-tree --full-index -p \"%s\"" % (id)
1538 patchcmd
= diffcmd
+ " | git apply "
1539 tryPatchCmd
= patchcmd
+ "--check -"
1540 applyPatchCmd
= patchcmd
+ "--check --apply -"
1541 patch_succeeded
= True
1543 if os
.system(tryPatchCmd
) != 0:
1544 fixed_rcs_keywords
= False
1545 patch_succeeded
= False
1546 print "Unfortunately applying the change failed!"
1548 # Patch failed, maybe it's just RCS keyword woes. Look through
1549 # the patch to see if that's possible.
1550 if gitConfigBool("git-p4.attemptRCSCleanup"):
1554 for file in editedFiles | filesToDelete
:
1555 # did this file's delta contain RCS keywords?
1556 pattern
= p4_keywords_regexp_for_file(file)
1559 # this file is a possibility...look for RCS keywords.
1560 regexp
= re
.compile(pattern
, re
.VERBOSE
)
1561 for line
in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1562 if regexp
.search(line
):
1564 print "got keyword match on %s in %s in %s" % (pattern
, line
, file)
1565 kwfiles
[file] = pattern
1568 for file in kwfiles
:
1570 print "zapping %s with %s" % (line
,pattern
)
1571 # File is being deleted, so not open in p4. Must
1572 # disable the read-only bit on windows.
1573 if self
.isWindows
and file not in editedFiles
:
1574 os
.chmod(file, stat
.S_IWRITE
)
1575 self
.patchRCSKeywords(file, kwfiles
[file])
1576 fixed_rcs_keywords
= True
1578 if fixed_rcs_keywords
:
1579 print "Retrying the patch with RCS keywords cleaned up"
1580 if os
.system(tryPatchCmd
) == 0:
1581 patch_succeeded
= True
1583 if not patch_succeeded
:
1584 for f
in editedFiles
:
1589 # Apply the patch for real, and do add/delete/+x handling.
1591 system(applyPatchCmd
)
1593 for f
in filesToAdd
:
1595 for f
in filesToDelete
:
1599 # Set/clear executable bits
1600 for f
in filesToChangeExecBit
.keys():
1601 mode
= filesToChangeExecBit
[f
]
1602 setP4ExecBit(f
, mode
)
1605 # Build p4 change description, starting with the contents
1606 # of the git commit message.
1608 logMessage
= extractLogMessageFromGitCommit(id)
1609 logMessage
= logMessage
.strip()
1610 (logMessage
, jobs
) = self
.separate_jobs_from_description(logMessage
)
1612 template
= self
.prepareSubmitTemplate()
1613 submitTemplate
= self
.prepareLogMessage(template
, logMessage
, jobs
)
1615 if self
.preserveUser
:
1616 submitTemplate
+= "\n######## Actual user %s, modified after commit\n" % p4User
1618 if self
.checkAuthorship
and not self
.p4UserIsMe(p4User
):
1619 submitTemplate
+= "######## git author %s does not match your p4 account.\n" % gitEmail
1620 submitTemplate
+= "######## Use option --preserve-user to modify authorship.\n"
1621 submitTemplate
+= "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1623 separatorLine
= "######## everything below this line is just the diff #######\n"
1624 if not self
.prepare_p4_only
:
1625 submitTemplate
+= separatorLine
1626 submitTemplate
+= self
.get_diff_description(editedFiles
, filesToAdd
)
1628 (handle
, fileName
) = tempfile
.mkstemp()
1629 tmpFile
= os
.fdopen(handle
, "w+b")
1631 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
1632 tmpFile
.write(submitTemplate
)
1635 if self
.prepare_p4_only
:
1637 # Leave the p4 tree prepared, and the submit template around
1638 # and let the user decide what to do next
1641 print "P4 workspace prepared for submission."
1642 print "To submit or revert, go to client workspace"
1643 print " " + self
.clientPath
1645 print "To submit, use \"p4 submit\" to write a new description,"
1646 print "or \"p4 submit -i <%s\" to use the one prepared by" \
1647 " \"git p4\"." % fileName
1648 print "You can delete the file \"%s\" when finished." % fileName
1650 if self
.preserveUser
and p4User
and not self
.p4UserIsMe(p4User
):
1651 print "To preserve change ownership by user %s, you must\n" \
1652 "do \"p4 change -f <change>\" after submitting and\n" \
1653 "edit the User field."
1655 print "After submitting, renamed files must be re-synced."
1656 print "Invoke \"p4 sync -f\" on each of these files:"
1657 for f
in pureRenameCopy
:
1661 print "To revert the changes, use \"p4 revert ...\", and delete"
1662 print "the submit template file \"%s\"" % fileName
1664 print "Since the commit adds new files, they must be deleted:"
1665 for f
in filesToAdd
:
1671 # Let the user edit the change description, then submit it.
1673 if self
.edit_template(fileName
):
1674 # read the edited message and submit
1676 tmpFile
= open(fileName
, "rb")
1677 message
= tmpFile
.read()
1680 message
= message
.replace("\r\n", "\n")
1681 submitTemplate
= message
[:message
.index(separatorLine
)]
1682 p4_write_pipe(['submit', '-i'], submitTemplate
)
1684 if self
.preserveUser
:
1686 # Get last changelist number. Cannot easily get it from
1687 # the submit command output as the output is
1689 changelist
= self
.lastP4Changelist()
1690 self
.modifyChangelistUser(changelist
, p4User
)
1692 # The rename/copy happened by applying a patch that created a
1693 # new file. This leaves it writable, which confuses p4.
1694 for f
in pureRenameCopy
:
1700 print "Submission cancelled, undoing p4 changes."
1701 for f
in editedFiles
:
1703 for f
in filesToAdd
:
1706 for f
in filesToDelete
:
1712 # Export git tags as p4 labels. Create a p4 label and then tag
1714 def exportGitTags(self
, gitTags
):
1715 validLabelRegexp
= gitConfig("git-p4.labelExportRegexp")
1716 if len(validLabelRegexp
) == 0:
1717 validLabelRegexp
= defaultLabelRegexp
1718 m
= re
.compile(validLabelRegexp
)
1720 for name
in gitTags
:
1722 if not m
.match(name
):
1724 print "tag %s does not match regexp %s" % (name
, validLabelRegexp
)
1727 # Get the p4 commit this corresponds to
1728 logMessage
= extractLogMessageFromGitCommit(name
)
1729 values
= extractSettingsGitLog(logMessage
)
1731 if not values
.has_key('change'):
1732 # a tag pointing to something not sent to p4; ignore
1734 print "git tag %s does not give a p4 commit" % name
1737 changelist
= values
['change']
1739 # Get the tag details.
1743 for l
in read_pipe_lines(["git", "cat-file", "-p", name
]):
1746 if re
.match(r
'tag\s+', l
):
1748 elif re
.match(r
'\s*$', l
):
1755 body
= ["lightweight tag imported by git p4\n"]
1757 # Create the label - use the same view as the client spec we are using
1758 clientSpec
= getClientSpec()
1760 labelTemplate
= "Label: %s\n" % name
1761 labelTemplate
+= "Description:\n"
1763 labelTemplate
+= "\t" + b
+ "\n"
1764 labelTemplate
+= "View:\n"
1765 for depot_side
in clientSpec
.mappings
:
1766 labelTemplate
+= "\t%s\n" % depot_side
1769 print "Would create p4 label %s for tag" % name
1770 elif self
.prepare_p4_only
:
1771 print "Not creating p4 label %s for tag due to option" \
1772 " --prepare-p4-only" % name
1774 p4_write_pipe(["label", "-i"], labelTemplate
)
1777 p4_system(["tag", "-l", name
] +
1778 ["%s@%s" % (depot_side
, changelist
) for depot_side
in clientSpec
.mappings
])
1781 print "created p4 label for tag %s" % name
1783 def run(self
, args
):
1785 self
.master
= currentGitBranch()
1786 if len(self
.master
) == 0 or not gitBranchExists("refs/heads/%s" % self
.master
):
1787 die("Detecting current git branch failed!")
1788 elif len(args
) == 1:
1789 self
.master
= args
[0]
1790 if not branchExists(self
.master
):
1791 die("Branch %s does not exist" % self
.master
)
1795 allowSubmit
= gitConfig("git-p4.allowSubmit")
1796 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
1797 die("%s is not in git-p4.allowSubmit" % self
.master
)
1799 [upstream
, settings
] = findUpstreamBranchPoint()
1800 self
.depotPath
= settings
['depot-paths'][0]
1801 if len(self
.origin
) == 0:
1802 self
.origin
= upstream
1804 if self
.preserveUser
:
1805 if not self
.canChangeChangelists():
1806 die("Cannot preserve user names without p4 super-user or admin permissions")
1808 # if not set from the command line, try the config file
1809 if self
.conflict_behavior
is None:
1810 val
= gitConfig("git-p4.conflict")
1812 if val
not in self
.conflict_behavior_choices
:
1813 die("Invalid value '%s' for config git-p4.conflict" % val
)
1816 self
.conflict_behavior
= val
1819 print "Origin branch is " + self
.origin
1821 if len(self
.depotPath
) == 0:
1822 print "Internal error: cannot locate perforce depot path from existing branches"
1825 self
.useClientSpec
= False
1826 if gitConfigBool("git-p4.useclientspec"):
1827 self
.useClientSpec
= True
1828 if self
.useClientSpec
:
1829 self
.clientSpecDirs
= getClientSpec()
1831 # Check for the existance of P4 branches
1832 branchesDetected
= (len(p4BranchesInGit().keys()) > 1)
1834 if self
.useClientSpec
and not branchesDetected
:
1835 # all files are relative to the client spec
1836 self
.clientPath
= getClientRoot()
1838 self
.clientPath
= p4Where(self
.depotPath
)
1840 if self
.clientPath
== "":
1841 die("Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
)
1843 print "Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
)
1844 self
.oldWorkingDirectory
= os
.getcwd()
1846 # ensure the clientPath exists
1847 new_client_dir
= False
1848 if not os
.path
.exists(self
.clientPath
):
1849 new_client_dir
= True
1850 os
.makedirs(self
.clientPath
)
1852 chdir(self
.clientPath
, is_client_path
=True)
1854 print "Would synchronize p4 checkout in %s" % self
.clientPath
1856 print "Synchronizing p4 checkout..."
1858 # old one was destroyed, and maybe nobody told p4
1859 p4_sync("...", "-f")
1865 for line
in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self
.origin
, self
.master
)]):
1866 commits
.append(line
.strip())
1869 if self
.preserveUser
or gitConfigBool("git-p4.skipUserNameCheck"):
1870 self
.checkAuthorship
= False
1872 self
.checkAuthorship
= True
1874 if self
.preserveUser
:
1875 self
.checkValidP4Users(commits
)
1878 # Build up a set of options to be passed to diff when
1879 # submitting each commit to p4.
1881 if self
.detectRenames
:
1882 # command-line -M arg
1883 self
.diffOpts
= "-M"
1885 # If not explicitly set check the config variable
1886 detectRenames
= gitConfig("git-p4.detectRenames")
1888 if detectRenames
.lower() == "false" or detectRenames
== "":
1890 elif detectRenames
.lower() == "true":
1891 self
.diffOpts
= "-M"
1893 self
.diffOpts
= "-M%s" % detectRenames
1895 # no command-line arg for -C or --find-copies-harder, just
1897 detectCopies
= gitConfig("git-p4.detectCopies")
1898 if detectCopies
.lower() == "false" or detectCopies
== "":
1900 elif detectCopies
.lower() == "true":
1901 self
.diffOpts
+= " -C"
1903 self
.diffOpts
+= " -C%s" % detectCopies
1905 if gitConfigBool("git-p4.detectCopiesHarder"):
1906 self
.diffOpts
+= " --find-copies-harder"
1909 # Apply the commits, one at a time. On failure, ask if should
1910 # continue to try the rest of the patches, or quit.
1915 last
= len(commits
) - 1
1916 for i
, commit
in enumerate(commits
):
1918 print " ", read_pipe(["git", "show", "-s",
1919 "--format=format:%h %s", commit
])
1922 ok
= self
.applyCommit(commit
)
1924 applied
.append(commit
)
1926 if self
.prepare_p4_only
and i
< last
:
1927 print "Processing only the first commit due to option" \
1928 " --prepare-p4-only"
1933 # prompt for what to do, or use the option/variable
1934 if self
.conflict_behavior
== "ask":
1935 print "What do you want to do?"
1936 response
= raw_input("[s]kip this commit but apply"
1937 " the rest, or [q]uit? ")
1940 elif self
.conflict_behavior
== "skip":
1942 elif self
.conflict_behavior
== "quit":
1945 die("Unknown conflict_behavior '%s'" %
1946 self
.conflict_behavior
)
1948 if response
[0] == "s":
1949 print "Skipping this commit, but applying the rest"
1951 if response
[0] == "q":
1958 chdir(self
.oldWorkingDirectory
)
1962 elif self
.prepare_p4_only
:
1964 elif len(commits
) == len(applied
):
1965 print "All commits applied!"
1969 sync
.branch
= self
.branch
1976 if len(applied
) == 0:
1977 print "No commits applied."
1979 print "Applied only the commits marked with '*':"
1985 print star
, read_pipe(["git", "show", "-s",
1986 "--format=format:%h %s", c
])
1987 print "You will have to do 'git p4 sync' and rebase."
1989 if gitConfigBool("git-p4.exportLabels"):
1990 self
.exportLabels
= True
1992 if self
.exportLabels
:
1993 p4Labels
= getP4Labels(self
.depotPath
)
1994 gitTags
= getGitTags()
1996 missingGitTags
= gitTags
- p4Labels
1997 self
.exportGitTags(missingGitTags
)
1999 # exit with error unless everything applied perfectly
2000 if len(commits
) != len(applied
):
2006 """Represent a p4 view ("p4 help views"), and map files in a
2007 repo according to the view."""
2009 def __init__(self
, client_name
):
2011 self
.client_prefix
= "//%s/" % client_name
2012 # cache results of "p4 where" to lookup client file locations
2013 self
.client_spec_path_cache
= {}
2015 def append(self
, view_line
):
2016 """Parse a view line, splitting it into depot and client
2017 sides. Append to self.mappings, preserving order. This
2018 is only needed for tag creation."""
2020 # Split the view line into exactly two words. P4 enforces
2021 # structure on these lines that simplifies this quite a bit.
2023 # Either or both words may be double-quoted.
2024 # Single quotes do not matter.
2025 # Double-quote marks cannot occur inside the words.
2026 # A + or - prefix is also inside the quotes.
2027 # There are no quotes unless they contain a space.
2028 # The line is already white-space stripped.
2029 # The two words are separated by a single space.
2031 if view_line
[0] == '"':
2032 # First word is double quoted. Find its end.
2033 close_quote_index
= view_line
.find('"', 1)
2034 if close_quote_index
<= 0:
2035 die("No first-word closing quote found: %s" % view_line
)
2036 depot_side
= view_line
[1:close_quote_index
]
2037 # skip closing quote and space
2038 rhs_index
= close_quote_index
+ 1 + 1
2040 space_index
= view_line
.find(" ")
2041 if space_index
<= 0:
2042 die("No word-splitting space found: %s" % view_line
)
2043 depot_side
= view_line
[0:space_index
]
2044 rhs_index
= space_index
+ 1
2046 # prefix + means overlay on previous mapping
2047 if depot_side
.startswith("+"):
2048 depot_side
= depot_side
[1:]
2050 # prefix - means exclude this path, leave out of mappings
2052 if depot_side
.startswith("-"):
2054 depot_side
= depot_side
[1:]
2057 self
.mappings
.append(depot_side
)
2059 def convert_client_path(self
, clientFile
):
2060 # chop off //client/ part to make it relative
2061 if not clientFile
.startswith(self
.client_prefix
):
2062 die("No prefix '%s' on clientFile '%s'" %
2063 (self
.client_prefix
, clientFile
))
2064 return clientFile
[len(self
.client_prefix
):]
2066 def update_client_spec_path_cache(self
, files
):
2067 """ Caching file paths by "p4 where" batch query """
2069 # List depot file paths exclude that already cached
2070 fileArgs
= [f
['path'] for f
in files
if f
['path'] not in self
.client_spec_path_cache
]
2072 if len(fileArgs
) == 0:
2073 return # All files in cache
2075 where_result
= p4CmdList(["-x", "-", "where"], stdin
=fileArgs
)
2076 for res
in where_result
:
2077 if "code" in res
and res
["code"] == "error":
2078 # assume error is "... file(s) not in client view"
2080 if "clientFile" not in res
:
2081 die("No clientFile in 'p4 where' output")
2083 # it will list all of them, but only one not unmap-ped
2085 if gitConfigBool("core.ignorecase"):
2086 res
['depotFile'] = res
['depotFile'].lower()
2087 self
.client_spec_path_cache
[res
['depotFile']] = self
.convert_client_path(res
["clientFile"])
2089 # not found files or unmap files set to ""
2090 for depotFile
in fileArgs
:
2091 if gitConfigBool("core.ignorecase"):
2092 depotFile
= depotFile
.lower()
2093 if depotFile
not in self
.client_spec_path_cache
:
2094 self
.client_spec_path_cache
[depotFile
] = ""
2096 def map_in_client(self
, depot_path
):
2097 """Return the relative location in the client where this
2098 depot file should live. Returns "" if the file should
2099 not be mapped in the client."""
2101 if gitConfigBool("core.ignorecase"):
2102 depot_path
= depot_path
.lower()
2104 if depot_path
in self
.client_spec_path_cache
:
2105 return self
.client_spec_path_cache
[depot_path
]
2107 die( "Error: %s is not found in client spec path" % depot_path
)
2110 class P4Sync(Command
, P4UserMap
):
2111 delete_actions
= ( "delete", "move/delete", "purge" )
2114 Command
.__init
__(self
)
2115 P4UserMap
.__init
__(self
)
2117 optparse
.make_option("--branch", dest
="branch"),
2118 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
2119 optparse
.make_option("--changesfile", dest
="changesFile"),
2120 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
2121 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
2122 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
2123 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
2124 help="Import into refs/heads/ , not refs/remotes"),
2125 optparse
.make_option("--max-changes", dest
="maxChanges",
2126 help="Maximum number of changes to import"),
2127 optparse
.make_option("--changes-block-size", dest
="changes_block_size", type="int",
2128 help="Internal block size to use when iteratively calling p4 changes"),
2129 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
2130 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2131 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
2132 help="Only sync files that are included in the Perforce Client Spec"),
2133 optparse
.make_option("-/", dest
="cloneExclude",
2134 action
="append", type="string",
2135 help="exclude depot path"),
2137 self
.description
= """Imports from Perforce into a git repository.\n
2139 //depot/my/project/ -- to import the current head
2140 //depot/my/project/@all -- to import everything
2141 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2143 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2145 self
.usage
+= " //depot/path[@revRange]"
2147 self
.createdBranches
= set()
2148 self
.committedChanges
= set()
2150 self
.detectBranches
= False
2151 self
.detectLabels
= False
2152 self
.importLabels
= False
2153 self
.changesFile
= ""
2154 self
.syncWithOrigin
= True
2155 self
.importIntoRemotes
= True
2156 self
.maxChanges
= ""
2157 self
.changes_block_size
= None
2158 self
.keepRepoPath
= False
2159 self
.depotPaths
= None
2160 self
.p4BranchesInGit
= []
2161 self
.cloneExclude
= []
2162 self
.useClientSpec
= False
2163 self
.useClientSpec_from_options
= False
2164 self
.clientSpecDirs
= None
2165 self
.tempBranches
= []
2166 self
.tempBranchLocation
= "git-p4-tmp"
2167 self
.largeFileSystem
= None
2169 if gitConfig('git-p4.largeFileSystem'):
2170 largeFileSystemConstructor
= globals()[gitConfig('git-p4.largeFileSystem')]
2171 self
.largeFileSystem
= largeFileSystemConstructor(
2172 lambda git_mode
, relPath
, contents
: self
.writeToGitStream(git_mode
, relPath
, contents
)
2175 if gitConfig("git-p4.syncFromOrigin") == "false":
2176 self
.syncWithOrigin
= False
2178 # This is required for the "append" cloneExclude action
2179 def ensure_value(self
, attr
, value
):
2180 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
2181 setattr(self
, attr
, value
)
2182 return getattr(self
, attr
)
2184 # Force a checkpoint in fast-import and wait for it to finish
2185 def checkpoint(self
):
2186 self
.gitStream
.write("checkpoint\n\n")
2187 self
.gitStream
.write("progress checkpoint\n\n")
2188 out
= self
.gitOutput
.readline()
2190 print "checkpoint finished: " + out
2192 def extractFilesFromCommit(self
, commit
):
2193 self
.cloneExclude
= [re
.sub(r
"\.\.\.$", "", path
)
2194 for path
in self
.cloneExclude
]
2197 while commit
.has_key("depotFile%s" % fnum
):
2198 path
= commit
["depotFile%s" % fnum
]
2200 if [p
for p
in self
.cloneExclude
2201 if p4PathStartsWith(path
, p
)]:
2204 found
= [p
for p
in self
.depotPaths
2205 if p4PathStartsWith(path
, p
)]
2212 file["rev"] = commit
["rev%s" % fnum
]
2213 file["action"] = commit
["action%s" % fnum
]
2214 file["type"] = commit
["type%s" % fnum
]
2219 def stripRepoPath(self
, path
, prefixes
):
2220 """When streaming files, this is called to map a p4 depot path
2221 to where it should go in git. The prefixes are either
2222 self.depotPaths, or self.branchPrefixes in the case of
2223 branch detection."""
2225 if self
.useClientSpec
:
2226 # branch detection moves files up a level (the branch name)
2227 # from what client spec interpretation gives
2228 path
= self
.clientSpecDirs
.map_in_client(path
)
2229 if self
.detectBranches
:
2230 for b
in self
.knownBranches
:
2231 if path
.startswith(b
+ "/"):
2232 path
= path
[len(b
)+1:]
2234 elif self
.keepRepoPath
:
2235 # Preserve everything in relative path name except leading
2236 # //depot/; just look at first prefix as they all should
2237 # be in the same depot.
2238 depot
= re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])
2239 if p4PathStartsWith(path
, depot
):
2240 path
= path
[len(depot
):]
2244 if p4PathStartsWith(path
, p
):
2245 path
= path
[len(p
):]
2248 path
= wildcard_decode(path
)
2251 def splitFilesIntoBranches(self
, commit
):
2252 """Look at each depotFile in the commit to figure out to what
2253 branch it belongs."""
2255 if self
.clientSpecDirs
:
2256 files
= self
.extractFilesFromCommit(commit
)
2257 self
.clientSpecDirs
.update_client_spec_path_cache(files
)
2261 while commit
.has_key("depotFile%s" % fnum
):
2262 path
= commit
["depotFile%s" % fnum
]
2263 found
= [p
for p
in self
.depotPaths
2264 if p4PathStartsWith(path
, p
)]
2271 file["rev"] = commit
["rev%s" % fnum
]
2272 file["action"] = commit
["action%s" % fnum
]
2273 file["type"] = commit
["type%s" % fnum
]
2276 # start with the full relative path where this file would
2278 if self
.useClientSpec
:
2279 relPath
= self
.clientSpecDirs
.map_in_client(path
)
2281 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
2283 for branch
in self
.knownBranches
.keys():
2284 # add a trailing slash so that a commit into qt/4.2foo
2285 # doesn't end up in qt/4.2, e.g.
2286 if relPath
.startswith(branch
+ "/"):
2287 if branch
not in branches
:
2288 branches
[branch
] = []
2289 branches
[branch
].append(file)
2294 def writeToGitStream(self
, gitMode
, relPath
, contents
):
2295 self
.gitStream
.write('M %s inline %s\n' % (gitMode
, relPath
))
2296 self
.gitStream
.write('data %d\n' % sum(len(d
) for d
in contents
))
2298 self
.gitStream
.write(d
)
2299 self
.gitStream
.write('\n')
2301 # output one file from the P4 stream
2302 # - helper for streamP4Files
2304 def streamOneP4File(self
, file, contents
):
2305 relPath
= self
.stripRepoPath(file['depotFile'], self
.branchPrefixes
)
2307 size
= int(self
.stream_file
['fileSize'])
2308 sys
.stdout
.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath
, size
/1024/1024))
2311 (type_base
, type_mods
) = split_p4_type(file["type"])
2314 if "x" in type_mods
:
2316 if type_base
== "symlink":
2318 # p4 print on a symlink sometimes contains "target\n";
2319 # if it does, remove the newline
2320 data
= ''.join(contents
)
2322 # Some version of p4 allowed creating a symlink that pointed
2323 # to nothing. This causes p4 errors when checking out such
2324 # a change, and errors here too. Work around it by ignoring
2325 # the bad symlink; hopefully a future change fixes it.
2326 print "\nIgnoring empty symlink in %s" % file['depotFile']
2328 elif data
[-1] == '\n':
2329 contents
= [data
[:-1]]
2333 if type_base
== "utf16":
2334 # p4 delivers different text in the python output to -G
2335 # than it does when using "print -o", or normal p4 client
2336 # operations. utf16 is converted to ascii or utf8, perhaps.
2337 # But ascii text saved as -t utf16 is completely mangled.
2338 # Invoke print -o to get the real contents.
2340 # On windows, the newlines will always be mangled by print, so put
2341 # them back too. This is not needed to the cygwin windows version,
2342 # just the native "NT" type.
2344 text
= p4_read_pipe(['print', '-q', '-o', '-', "%s@%s" % (file['depotFile'], file['change']) ])
2345 if p4_version_string().find("/NT") >= 0:
2346 text
= text
.replace("\r\n", "\n")
2349 if type_base
== "apple":
2350 # Apple filetype files will be streamed as a concatenation of
2351 # its appledouble header and the contents. This is useless
2352 # on both macs and non-macs. If using "print -q -o xx", it
2353 # will create "xx" with the data, and "%xx" with the header.
2354 # This is also not very useful.
2356 # Ideally, someday, this script can learn how to generate
2357 # appledouble files directly and import those to git, but
2358 # non-mac machines can never find a use for apple filetype.
2359 print "\nIgnoring apple filetype file %s" % file['depotFile']
2362 # Note that we do not try to de-mangle keywords on utf16 files,
2363 # even though in theory somebody may want that.
2364 pattern
= p4_keywords_regexp_for_type(type_base
, type_mods
)
2366 regexp
= re
.compile(pattern
, re
.VERBOSE
)
2367 text
= ''.join(contents
)
2368 text
= regexp
.sub(r
'$\1$', text
)
2371 if self
.largeFileSystem
:
2372 (git_mode
, contents
) = self
.largeFileSystem
.processContent(git_mode
, relPath
, contents
)
2374 self
.writeToGitStream(git_mode
, relPath
, contents
)
2376 def streamOneP4Deletion(self
, file):
2377 relPath
= self
.stripRepoPath(file['path'], self
.branchPrefixes
)
2379 sys
.stdout
.write("delete %s\n" % relPath
)
2381 self
.gitStream
.write("D %s\n" % relPath
)
2383 if self
.largeFileSystem
and self
.largeFileSystem
.isLargeFile(relPath
):
2384 self
.largeFileSystem
.removeLargeFile(relPath
)
2386 # handle another chunk of streaming data
2387 def streamP4FilesCb(self
, marshalled
):
2389 # catch p4 errors and complain
2391 if "code" in marshalled
:
2392 if marshalled
["code"] == "error":
2393 if "data" in marshalled
:
2394 err
= marshalled
["data"].rstrip()
2396 if not err
and 'fileSize' in self
.stream_file
:
2397 required_bytes
= int((4 * int(self
.stream_file
["fileSize"])) - calcDiskFree())
2398 if required_bytes
> 0:
2399 err
= 'Not enough space left on %s! Free at least %i MB.' % (
2400 os
.getcwd(), required_bytes
/1024/1024
2405 if self
.stream_have_file_info
:
2406 if "depotFile" in self
.stream_file
:
2407 f
= self
.stream_file
["depotFile"]
2408 # force a failure in fast-import, else an empty
2409 # commit will be made
2410 self
.gitStream
.write("\n")
2411 self
.gitStream
.write("die-now\n")
2412 self
.gitStream
.close()
2413 # ignore errors, but make sure it exits first
2414 self
.importProcess
.wait()
2416 die("Error from p4 print for %s: %s" % (f
, err
))
2418 die("Error from p4 print: %s" % err
)
2420 if marshalled
.has_key('depotFile') and self
.stream_have_file_info
:
2421 # start of a new file - output the old one first
2422 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
2423 self
.stream_file
= {}
2424 self
.stream_contents
= []
2425 self
.stream_have_file_info
= False
2427 # pick up the new file information... for the
2428 # 'data' field we need to append to our array
2429 for k
in marshalled
.keys():
2431 if 'streamContentSize' not in self
.stream_file
:
2432 self
.stream_file
['streamContentSize'] = 0
2433 self
.stream_file
['streamContentSize'] += len(marshalled
['data'])
2434 self
.stream_contents
.append(marshalled
['data'])
2436 self
.stream_file
[k
] = marshalled
[k
]
2439 'streamContentSize' in self
.stream_file
and
2440 'fileSize' in self
.stream_file
and
2441 'depotFile' in self
.stream_file
):
2442 size
= int(self
.stream_file
["fileSize"])
2444 progress
= 100*self
.stream_file
['streamContentSize']/size
2445 sys
.stdout
.write('\r%s %d%% (%i MB)' % (self
.stream_file
['depotFile'], progress
, int(size
/1024/1024)))
2448 self
.stream_have_file_info
= True
2450 # Stream directly from "p4 files" into "git fast-import"
2451 def streamP4Files(self
, files
):
2457 # if using a client spec, only add the files that have
2458 # a path in the client
2459 if self
.clientSpecDirs
:
2460 if self
.clientSpecDirs
.map_in_client(f
['path']) == "":
2463 filesForCommit
.append(f
)
2464 if f
['action'] in self
.delete_actions
:
2465 filesToDelete
.append(f
)
2467 filesToRead
.append(f
)
2470 for f
in filesToDelete
:
2471 self
.streamOneP4Deletion(f
)
2473 if len(filesToRead
) > 0:
2474 self
.stream_file
= {}
2475 self
.stream_contents
= []
2476 self
.stream_have_file_info
= False
2478 # curry self argument
2479 def streamP4FilesCbSelf(entry
):
2480 self
.streamP4FilesCb(entry
)
2482 fileArgs
= ['%s#%s' % (f
['path'], f
['rev']) for f
in filesToRead
]
2484 p4CmdList(["-x", "-", "print"],
2486 cb
=streamP4FilesCbSelf
)
2489 if self
.stream_file
.has_key('depotFile'):
2490 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
2492 def make_email(self
, userid
):
2493 if userid
in self
.users
:
2494 return self
.users
[userid
]
2496 return "%s <a@b>" % userid
2499 def streamTag(self
, gitStream
, labelName
, labelDetails
, commit
, epoch
):
2501 print "writing tag %s for commit %s" % (labelName
, commit
)
2502 gitStream
.write("tag %s\n" % labelName
)
2503 gitStream
.write("from %s\n" % commit
)
2505 if labelDetails
.has_key('Owner'):
2506 owner
= labelDetails
["Owner"]
2510 # Try to use the owner of the p4 label, or failing that,
2511 # the current p4 user id.
2513 email
= self
.make_email(owner
)
2515 email
= self
.make_email(self
.p4UserId())
2516 tagger
= "%s %s %s" % (email
, epoch
, self
.tz
)
2518 gitStream
.write("tagger %s\n" % tagger
)
2520 print "labelDetails=",labelDetails
2521 if labelDetails
.has_key('Description'):
2522 description
= labelDetails
['Description']
2524 description
= 'Label from git p4'
2526 gitStream
.write("data %d\n" % len(description
))
2527 gitStream
.write(description
)
2528 gitStream
.write("\n")
2530 def commit(self
, details
, files
, branch
, parent
= ""):
2531 epoch
= details
["time"]
2532 author
= details
["user"]
2535 print "commit into %s" % branch
2537 # start with reading files; if that fails, we should not
2541 if [p
for p
in self
.branchPrefixes
if p4PathStartsWith(f
['path'], p
)]:
2542 new_files
.append (f
)
2544 sys
.stderr
.write("Ignoring file outside of prefix: %s\n" % f
['path'])
2546 if self
.clientSpecDirs
:
2547 self
.clientSpecDirs
.update_client_spec_path_cache(files
)
2549 self
.gitStream
.write("commit %s\n" % branch
)
2550 # gitStream.write("mark :%s\n" % details["change"])
2551 self
.committedChanges
.add(int(details
["change"]))
2553 if author
not in self
.users
:
2554 self
.getUserMapFromPerforceServer()
2555 committer
= "%s %s %s" % (self
.make_email(author
), epoch
, self
.tz
)
2557 self
.gitStream
.write("committer %s\n" % committer
)
2559 self
.gitStream
.write("data <<EOT\n")
2560 self
.gitStream
.write(details
["desc"])
2561 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2562 (','.join(self
.branchPrefixes
), details
["change"]))
2563 if len(details
['options']) > 0:
2564 self
.gitStream
.write(": options = %s" % details
['options'])
2565 self
.gitStream
.write("]\nEOT\n\n")
2569 print "parent %s" % parent
2570 self
.gitStream
.write("from %s\n" % parent
)
2572 self
.streamP4Files(new_files
)
2573 self
.gitStream
.write("\n")
2575 change
= int(details
["change"])
2577 if self
.labels
.has_key(change
):
2578 label
= self
.labels
[change
]
2579 labelDetails
= label
[0]
2580 labelRevisions
= label
[1]
2582 print "Change %s is labelled %s" % (change
, labelDetails
)
2584 files
= p4CmdList(["files"] + ["%s...@%s" % (p
, change
)
2585 for p
in self
.branchPrefixes
])
2587 if len(files
) == len(labelRevisions
):
2591 if info
["action"] in self
.delete_actions
:
2593 cleanedFiles
[info
["depotFile"]] = info
["rev"]
2595 if cleanedFiles
== labelRevisions
:
2596 self
.streamTag(self
.gitStream
, 'tag_%s' % labelDetails
['label'], labelDetails
, branch
, epoch
)
2600 print ("Tag %s does not match with change %s: files do not match."
2601 % (labelDetails
["label"], change
))
2605 print ("Tag %s does not match with change %s: file count is different."
2606 % (labelDetails
["label"], change
))
2608 # Build a dictionary of changelists and labels, for "detect-labels" option.
2609 def getLabels(self
):
2612 l
= p4CmdList(["labels"] + ["%s..." % p
for p
in self
.depotPaths
])
2613 if len(l
) > 0 and not self
.silent
:
2614 print "Finding files belonging to labels in %s" % `self
.depotPaths`
2617 label
= output
["label"]
2621 print "Querying files for label %s" % label
2622 for file in p4CmdList(["files"] +
2623 ["%s...@%s" % (p
, label
)
2624 for p
in self
.depotPaths
]):
2625 revisions
[file["depotFile"]] = file["rev"]
2626 change
= int(file["change"])
2627 if change
> newestChange
:
2628 newestChange
= change
2630 self
.labels
[newestChange
] = [output
, revisions
]
2633 print "Label changes: %s" % self
.labels
.keys()
2635 # Import p4 labels as git tags. A direct mapping does not
2636 # exist, so assume that if all the files are at the same revision
2637 # then we can use that, or it's something more complicated we should
2639 def importP4Labels(self
, stream
, p4Labels
):
2641 print "import p4 labels: " + ' '.join(p4Labels
)
2643 ignoredP4Labels
= gitConfigList("git-p4.ignoredP4Labels")
2644 validLabelRegexp
= gitConfig("git-p4.labelImportRegexp")
2645 if len(validLabelRegexp
) == 0:
2646 validLabelRegexp
= defaultLabelRegexp
2647 m
= re
.compile(validLabelRegexp
)
2649 for name
in p4Labels
:
2652 if not m
.match(name
):
2654 print "label %s does not match regexp %s" % (name
,validLabelRegexp
)
2657 if name
in ignoredP4Labels
:
2660 labelDetails
= p4CmdList(['label', "-o", name
])[0]
2662 # get the most recent changelist for each file in this label
2663 change
= p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p
, name
)
2664 for p
in self
.depotPaths
])
2666 if change
.has_key('change'):
2667 # find the corresponding git commit; take the oldest commit
2668 changelist
= int(change
['change'])
2669 gitCommit
= read_pipe(["git", "rev-list", "--max-count=1",
2670 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist
])
2671 if len(gitCommit
) == 0:
2672 print "could not find git commit for changelist %d" % changelist
2674 gitCommit
= gitCommit
.strip()
2676 # Convert from p4 time format
2678 tmwhen
= time
.strptime(labelDetails
['Update'], "%Y/%m/%d %H:%M:%S")
2680 print "Could not convert label time %s" % labelDetails
['Update']
2683 when
= int(time
.mktime(tmwhen
))
2684 self
.streamTag(stream
, name
, labelDetails
, gitCommit
, when
)
2686 print "p4 label %s mapped to git commit %s" % (name
, gitCommit
)
2689 print "Label %s has no changelists - possibly deleted?" % name
2692 # We can't import this label; don't try again as it will get very
2693 # expensive repeatedly fetching all the files for labels that will
2694 # never be imported. If the label is moved in the future, the
2695 # ignore will need to be removed manually.
2696 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name
])
2698 def guessProjectName(self
):
2699 for p
in self
.depotPaths
:
2702 p
= p
[p
.strip().rfind("/") + 1:]
2703 if not p
.endswith("/"):
2707 def getBranchMapping(self
):
2708 lostAndFoundBranches
= set()
2710 user
= gitConfig("git-p4.branchUser")
2712 command
= "branches -u %s" % user
2714 command
= "branches"
2716 for info
in p4CmdList(command
):
2717 details
= p4Cmd(["branch", "-o", info
["branch"]])
2719 while details
.has_key("View%s" % viewIdx
):
2720 paths
= details
["View%s" % viewIdx
].split(" ")
2721 viewIdx
= viewIdx
+ 1
2722 # require standard //depot/foo/... //depot/bar/... mapping
2723 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
2726 destination
= paths
[1]
2728 if p4PathStartsWith(source
, self
.depotPaths
[0]) and p4PathStartsWith(destination
, self
.depotPaths
[0]):
2729 source
= source
[len(self
.depotPaths
[0]):-4]
2730 destination
= destination
[len(self
.depotPaths
[0]):-4]
2732 if destination
in self
.knownBranches
:
2734 print "p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
)
2735 print "but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
)
2738 self
.knownBranches
[destination
] = source
2740 lostAndFoundBranches
.discard(destination
)
2742 if source
not in self
.knownBranches
:
2743 lostAndFoundBranches
.add(source
)
2745 # Perforce does not strictly require branches to be defined, so we also
2746 # check git config for a branch list.
2748 # Example of branch definition in git config file:
2750 # branchList=main:branchA
2751 # branchList=main:branchB
2752 # branchList=branchA:branchC
2753 configBranches
= gitConfigList("git-p4.branchList")
2754 for branch
in configBranches
:
2756 (source
, destination
) = branch
.split(":")
2757 self
.knownBranches
[destination
] = source
2759 lostAndFoundBranches
.discard(destination
)
2761 if source
not in self
.knownBranches
:
2762 lostAndFoundBranches
.add(source
)
2765 for branch
in lostAndFoundBranches
:
2766 self
.knownBranches
[branch
] = branch
2768 def getBranchMappingFromGitBranches(self
):
2769 branches
= p4BranchesInGit(self
.importIntoRemotes
)
2770 for branch
in branches
.keys():
2771 if branch
== "master":
2774 branch
= branch
[len(self
.projectName
):]
2775 self
.knownBranches
[branch
] = branch
2777 def updateOptionDict(self
, d
):
2779 if self
.keepRepoPath
:
2780 option_keys
['keepRepoPath'] = 1
2782 d
["options"] = ' '.join(sorted(option_keys
.keys()))
2784 def readOptions(self
, d
):
2785 self
.keepRepoPath
= (d
.has_key('options')
2786 and ('keepRepoPath' in d
['options']))
2788 def gitRefForBranch(self
, branch
):
2789 if branch
== "main":
2790 return self
.refPrefix
+ "master"
2792 if len(branch
) <= 0:
2795 return self
.refPrefix
+ self
.projectName
+ branch
2797 def gitCommitByP4Change(self
, ref
, change
):
2799 print "looking in ref " + ref
+ " for change %s using bisect..." % change
2802 latestCommit
= parseRevision(ref
)
2806 print "trying: earliest %s latest %s" % (earliestCommit
, latestCommit
)
2807 next
= read_pipe("git rev-list --bisect %s %s" % (latestCommit
, earliestCommit
)).strip()
2812 log
= extractLogMessageFromGitCommit(next
)
2813 settings
= extractSettingsGitLog(log
)
2814 currentChange
= int(settings
['change'])
2816 print "current change %s" % currentChange
2818 if currentChange
== change
:
2820 print "found %s" % next
2823 if currentChange
< change
:
2824 earliestCommit
= "^%s" % next
2826 latestCommit
= "%s" % next
2830 def importNewBranch(self
, branch
, maxChange
):
2831 # make fast-import flush all changes to disk and update the refs using the checkpoint
2832 # command so that we can try to find the branch parent in the git history
2833 self
.gitStream
.write("checkpoint\n\n");
2834 self
.gitStream
.flush();
2835 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
2836 range = "@1,%s" % maxChange
2837 #print "prefix" + branchPrefix
2838 changes
= p4ChangesForPaths([branchPrefix
], range, self
.changes_block_size
)
2839 if len(changes
) <= 0:
2841 firstChange
= changes
[0]
2842 #print "first change in branch: %s" % firstChange
2843 sourceBranch
= self
.knownBranches
[branch
]
2844 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
2845 sourceRef
= self
.gitRefForBranch(sourceBranch
)
2846 #print "source " + sourceBranch
2848 branchParentChange
= int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath
, firstChange
)])["change"])
2849 #print "branch parent: %s" % branchParentChange
2850 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
2851 if len(gitParent
) > 0:
2852 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
2853 #print "parent git commit: %s" % gitParent
2855 self
.importChanges(changes
)
2858 def searchParent(self
, parent
, branch
, target
):
2860 for blob
in read_pipe_lines(["git", "rev-list", "--reverse",
2861 "--no-merges", parent
]):
2863 if len(read_pipe(["git", "diff-tree", blob
, target
])) == 0:
2866 print "Found parent of %s in commit %s" % (branch
, blob
)
2873 def importChanges(self
, changes
):
2875 for change
in changes
:
2876 description
= p4_describe(change
)
2877 self
.updateOptionDict(description
)
2880 sys
.stdout
.write("\rImporting revision %s (%s%%)" % (change
, cnt
* 100 / len(changes
)))
2885 if self
.detectBranches
:
2886 branches
= self
.splitFilesIntoBranches(description
)
2887 for branch
in branches
.keys():
2889 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
2890 self
.branchPrefixes
= [ branchPrefix
]
2894 filesForCommit
= branches
[branch
]
2897 print "branch is %s" % branch
2899 self
.updatedBranches
.add(branch
)
2901 if branch
not in self
.createdBranches
:
2902 self
.createdBranches
.add(branch
)
2903 parent
= self
.knownBranches
[branch
]
2904 if parent
== branch
:
2907 fullBranch
= self
.projectName
+ branch
2908 if fullBranch
not in self
.p4BranchesInGit
:
2910 print("\n Importing new branch %s" % fullBranch
);
2911 if self
.importNewBranch(branch
, change
- 1):
2913 self
.p4BranchesInGit
.append(fullBranch
)
2915 print("\n Resuming with change %s" % change
);
2918 print "parent determined through known branches: %s" % parent
2920 branch
= self
.gitRefForBranch(branch
)
2921 parent
= self
.gitRefForBranch(parent
)
2924 print "looking for initial parent for %s; current parent is %s" % (branch
, parent
)
2926 if len(parent
) == 0 and branch
in self
.initialParents
:
2927 parent
= self
.initialParents
[branch
]
2928 del self
.initialParents
[branch
]
2932 tempBranch
= "%s/%d" % (self
.tempBranchLocation
, change
)
2934 print "Creating temporary branch: " + tempBranch
2935 self
.commit(description
, filesForCommit
, tempBranch
)
2936 self
.tempBranches
.append(tempBranch
)
2938 blob
= self
.searchParent(parent
, branch
, tempBranch
)
2940 self
.commit(description
, filesForCommit
, branch
, blob
)
2943 print "Parent of %s not found. Committing into head of %s" % (branch
, parent
)
2944 self
.commit(description
, filesForCommit
, branch
, parent
)
2946 files
= self
.extractFilesFromCommit(description
)
2947 self
.commit(description
, files
, self
.branch
,
2949 # only needed once, to connect to the previous commit
2950 self
.initialParent
= ""
2952 print self
.gitError
.read()
2955 def importHeadRevision(self
, revision
):
2956 print "Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
)
2959 details
["user"] = "git perforce import user"
2960 details
["desc"] = ("Initial import of %s from the state at revision %s\n"
2961 % (' '.join(self
.depotPaths
), revision
))
2962 details
["change"] = revision
2966 fileArgs
= ["%s...%s" % (p
,revision
) for p
in self
.depotPaths
]
2968 for info
in p4CmdList(["files"] + fileArgs
):
2970 if 'code' in info
and info
['code'] == 'error':
2971 sys
.stderr
.write("p4 returned an error: %s\n"
2973 if info
['data'].find("must refer to client") >= 0:
2974 sys
.stderr
.write("This particular p4 error is misleading.\n")
2975 sys
.stderr
.write("Perhaps the depot path was misspelled.\n");
2976 sys
.stderr
.write("Depot path: %s\n" % " ".join(self
.depotPaths
))
2978 if 'p4ExitCode' in info
:
2979 sys
.stderr
.write("p4 exitcode: %s\n" % info
['p4ExitCode'])
2983 change
= int(info
["change"])
2984 if change
> newestRevision
:
2985 newestRevision
= change
2987 if info
["action"] in self
.delete_actions
:
2988 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2989 #fileCnt = fileCnt + 1
2992 for prop
in ["depotFile", "rev", "action", "type" ]:
2993 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
2995 fileCnt
= fileCnt
+ 1
2997 details
["change"] = newestRevision
2999 # Use time from top-most change so that all git p4 clones of
3000 # the same p4 repo have the same commit SHA1s.
3001 res
= p4_describe(newestRevision
)
3002 details
["time"] = res
["time"]
3004 self
.updateOptionDict(details
)
3006 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
)
3008 print "IO error with git fast-import. Is your git version recent enough?"
3009 print self
.gitError
.read()
3012 def run(self
, args
):
3013 self
.depotPaths
= []
3014 self
.changeRange
= ""
3015 self
.previousDepotPaths
= []
3016 self
.hasOrigin
= False
3018 # map from branch depot path to parent branch
3019 self
.knownBranches
= {}
3020 self
.initialParents
= {}
3022 if self
.importIntoRemotes
:
3023 self
.refPrefix
= "refs/remotes/p4/"
3025 self
.refPrefix
= "refs/heads/p4/"
3027 if self
.syncWithOrigin
:
3028 self
.hasOrigin
= originP4BranchesExist()
3031 print 'Syncing with origin first, using "git fetch origin"'
3032 system("git fetch origin")
3034 branch_arg_given
= bool(self
.branch
)
3035 if len(self
.branch
) == 0:
3036 self
.branch
= self
.refPrefix
+ "master"
3037 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
3038 system("git update-ref %s refs/heads/p4" % self
.branch
)
3039 system("git branch -D p4")
3041 # accept either the command-line option, or the configuration variable
3042 if self
.useClientSpec
:
3043 # will use this after clone to set the variable
3044 self
.useClientSpec_from_options
= True
3046 if gitConfigBool("git-p4.useclientspec"):
3047 self
.useClientSpec
= True
3048 if self
.useClientSpec
:
3049 self
.clientSpecDirs
= getClientSpec()
3051 # TODO: should always look at previous commits,
3052 # merge with previous imports, if possible.
3055 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
3057 # branches holds mapping from branch name to sha1
3058 branches
= p4BranchesInGit(self
.importIntoRemotes
)
3060 # restrict to just this one, disabling detect-branches
3061 if branch_arg_given
:
3062 short
= self
.branch
.split("/")[-1]
3063 if short
in branches
:
3064 self
.p4BranchesInGit
= [ short
]
3066 self
.p4BranchesInGit
= branches
.keys()
3068 if len(self
.p4BranchesInGit
) > 1:
3070 print "Importing from/into multiple branches"
3071 self
.detectBranches
= True
3072 for branch
in branches
.keys():
3073 self
.initialParents
[self
.refPrefix
+ branch
] = \
3077 print "branches: %s" % self
.p4BranchesInGit
3080 for branch
in self
.p4BranchesInGit
:
3081 logMsg
= extractLogMessageFromGitCommit(self
.refPrefix
+ branch
)
3083 settings
= extractSettingsGitLog(logMsg
)
3085 self
.readOptions(settings
)
3086 if (settings
.has_key('depot-paths')
3087 and settings
.has_key ('change')):
3088 change
= int(settings
['change']) + 1
3089 p4Change
= max(p4Change
, change
)
3091 depotPaths
= sorted(settings
['depot-paths'])
3092 if self
.previousDepotPaths
== []:
3093 self
.previousDepotPaths
= depotPaths
3096 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
3097 prev_list
= prev
.split("/")
3098 cur_list
= cur
.split("/")
3099 for i
in range(0, min(len(cur_list
), len(prev_list
))):
3100 if cur_list
[i
] <> prev_list
[i
]:
3104 paths
.append ("/".join(cur_list
[:i
+ 1]))
3106 self
.previousDepotPaths
= paths
3109 self
.depotPaths
= sorted(self
.previousDepotPaths
)
3110 self
.changeRange
= "@%s,#head" % p4Change
3111 if not self
.silent
and not self
.detectBranches
:
3112 print "Performing incremental import into %s git branch" % self
.branch
3114 # accept multiple ref name abbreviations:
3115 # refs/foo/bar/branch -> use it exactly
3116 # p4/branch -> prepend refs/remotes/ or refs/heads/
3117 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
3118 if not self
.branch
.startswith("refs/"):
3119 if self
.importIntoRemotes
:
3120 prepend
= "refs/remotes/"
3122 prepend
= "refs/heads/"
3123 if not self
.branch
.startswith("p4/"):
3125 self
.branch
= prepend
+ self
.branch
3127 if len(args
) == 0 and self
.depotPaths
:
3129 print "Depot paths: %s" % ' '.join(self
.depotPaths
)
3131 if self
.depotPaths
and self
.depotPaths
!= args
:
3132 print ("previous import used depot path %s and now %s was specified. "
3133 "This doesn't work!" % (' '.join (self
.depotPaths
),
3137 self
.depotPaths
= sorted(args
)
3142 # Make sure no revision specifiers are used when --changesfile
3144 bad_changesfile
= False
3145 if len(self
.changesFile
) > 0:
3146 for p
in self
.depotPaths
:
3147 if p
.find("@") >= 0 or p
.find("#") >= 0:
3148 bad_changesfile
= True
3151 die("Option --changesfile is incompatible with revision specifiers")
3154 for p
in self
.depotPaths
:
3155 if p
.find("@") != -1:
3156 atIdx
= p
.index("@")
3157 self
.changeRange
= p
[atIdx
:]
3158 if self
.changeRange
== "@all":
3159 self
.changeRange
= ""
3160 elif ',' not in self
.changeRange
:
3161 revision
= self
.changeRange
3162 self
.changeRange
= ""
3164 elif p
.find("#") != -1:
3165 hashIdx
= p
.index("#")
3166 revision
= p
[hashIdx
:]
3168 elif self
.previousDepotPaths
== []:
3169 # pay attention to changesfile, if given, else import
3170 # the entire p4 tree at the head revision
3171 if len(self
.changesFile
) == 0:
3174 p
= re
.sub ("\.\.\.$", "", p
)
3175 if not p
.endswith("/"):
3180 self
.depotPaths
= newPaths
3182 # --detect-branches may change this for each branch
3183 self
.branchPrefixes
= self
.depotPaths
3185 self
.loadUserMapFromCache()
3187 if self
.detectLabels
:
3190 if self
.detectBranches
:
3191 ## FIXME - what's a P4 projectName ?
3192 self
.projectName
= self
.guessProjectName()
3195 self
.getBranchMappingFromGitBranches()
3197 self
.getBranchMapping()
3199 print "p4-git branches: %s" % self
.p4BranchesInGit
3200 print "initial parents: %s" % self
.initialParents
3201 for b
in self
.p4BranchesInGit
:
3205 b
= b
[len(self
.projectName
):]
3206 self
.createdBranches
.add(b
)
3208 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
3210 self
.importProcess
= subprocess
.Popen(["git", "fast-import"],
3211 stdin
=subprocess
.PIPE
,
3212 stdout
=subprocess
.PIPE
,
3213 stderr
=subprocess
.PIPE
);
3214 self
.gitOutput
= self
.importProcess
.stdout
3215 self
.gitStream
= self
.importProcess
.stdin
3216 self
.gitError
= self
.importProcess
.stderr
3219 self
.importHeadRevision(revision
)
3223 if len(self
.changesFile
) > 0:
3224 output
= open(self
.changesFile
).readlines()
3227 changeSet
.add(int(line
))
3229 for change
in changeSet
:
3230 changes
.append(change
)
3234 # catch "git p4 sync" with no new branches, in a repo that
3235 # does not have any existing p4 branches
3237 if not self
.p4BranchesInGit
:
3238 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3240 # The default branch is master, unless --branch is used to
3241 # specify something else. Make sure it exists, or complain
3242 # nicely about how to use --branch.
3243 if not self
.detectBranches
:
3244 if not branch_exists(self
.branch
):
3245 if branch_arg_given
:
3246 die("Error: branch %s does not exist." % self
.branch
)
3248 die("Error: no branch %s; perhaps specify one with --branch." %
3252 print "Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
3254 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
, self
.changes_block_size
)
3256 if len(self
.maxChanges
) > 0:
3257 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
3259 if len(changes
) == 0:
3261 print "No changes to import!"
3263 if not self
.silent
and not self
.detectBranches
:
3264 print "Import destination: %s" % self
.branch
3266 self
.updatedBranches
= set()
3268 if not self
.detectBranches
:
3270 # start a new branch
3271 self
.initialParent
= ""
3273 # build on a previous revision
3274 self
.initialParent
= parseRevision(self
.branch
)
3276 self
.importChanges(changes
)
3280 if len(self
.updatedBranches
) > 0:
3281 sys
.stdout
.write("Updated branches: ")
3282 for b
in self
.updatedBranches
:
3283 sys
.stdout
.write("%s " % b
)
3284 sys
.stdout
.write("\n")
3286 if gitConfigBool("git-p4.importLabels"):
3287 self
.importLabels
= True
3289 if self
.importLabels
:
3290 p4Labels
= getP4Labels(self
.depotPaths
)
3291 gitTags
= getGitTags()
3293 missingP4Labels
= p4Labels
- gitTags
3294 self
.importP4Labels(self
.gitStream
, missingP4Labels
)
3296 self
.gitStream
.close()
3297 if self
.importProcess
.wait() != 0:
3298 die("fast-import failed: %s" % self
.gitError
.read())
3299 self
.gitOutput
.close()
3300 self
.gitError
.close()
3302 # Cleanup temporary branches created during import
3303 if self
.tempBranches
!= []:
3304 for branch
in self
.tempBranches
:
3305 read_pipe("git update-ref -d %s" % branch
)
3306 os
.rmdir(os
.path
.join(os
.environ
.get("GIT_DIR", ".git"), self
.tempBranchLocation
))
3308 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3309 # a convenient shortcut refname "p4".
3310 if self
.importIntoRemotes
:
3311 head_ref
= self
.refPrefix
+ "HEAD"
3312 if not gitBranchExists(head_ref
) and gitBranchExists(self
.branch
):
3313 system(["git", "symbolic-ref", head_ref
, self
.branch
])
3317 class P4Rebase(Command
):
3319 Command
.__init
__(self
)
3321 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
3323 self
.importLabels
= False
3324 self
.description
= ("Fetches the latest revision from perforce and "
3325 + "rebases the current work (branch) against it")
3327 def run(self
, args
):
3329 sync
.importLabels
= self
.importLabels
3332 return self
.rebase()
3335 if os
.system("git update-index --refresh") != 0:
3336 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.");
3337 if len(read_pipe("git diff-index HEAD --")) > 0:
3338 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3340 [upstream
, settings
] = findUpstreamBranchPoint()
3341 if len(upstream
) == 0:
3342 die("Cannot find upstream branchpoint for rebase")
3344 # the branchpoint may be p4/foo~3, so strip off the parent
3345 upstream
= re
.sub("~[0-9]+$", "", upstream
)
3347 print "Rebasing the current branch onto %s" % upstream
3348 oldHead
= read_pipe("git rev-parse HEAD").strip()
3349 system("git rebase %s" % upstream
)
3350 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead
)
3353 class P4Clone(P4Sync
):
3355 P4Sync
.__init
__(self
)
3356 self
.description
= "Creates a new git repository and imports from Perforce into it"
3357 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
3359 optparse
.make_option("--destination", dest
="cloneDestination",
3360 action
='store', default
=None,
3361 help="where to leave result of the clone"),
3362 optparse
.make_option("--bare", dest
="cloneBare",
3363 action
="store_true", default
=False),
3365 self
.cloneDestination
= None
3366 self
.needsGit
= False
3367 self
.cloneBare
= False
3369 def defaultDestination(self
, args
):
3370 ## TODO: use common prefix of args?
3372 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
3373 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
3374 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
3375 depotDir
= re
.sub(r
"/$", "", depotDir
)
3376 return os
.path
.split(depotDir
)[1]
3378 def run(self
, args
):
3382 if self
.keepRepoPath
and not self
.cloneDestination
:
3383 sys
.stderr
.write("Must specify destination for --keep-path\n")
3388 if not self
.cloneDestination
and len(depotPaths
) > 1:
3389 self
.cloneDestination
= depotPaths
[-1]
3390 depotPaths
= depotPaths
[:-1]
3392 self
.cloneExclude
= ["/"+p
for p
in self
.cloneExclude
]
3393 for p
in depotPaths
:
3394 if not p
.startswith("//"):
3395 sys
.stderr
.write('Depot paths must start with "//": %s\n' % p
)
3398 if not self
.cloneDestination
:
3399 self
.cloneDestination
= self
.defaultDestination(args
)
3401 print "Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
)
3403 if not os
.path
.exists(self
.cloneDestination
):
3404 os
.makedirs(self
.cloneDestination
)
3405 chdir(self
.cloneDestination
)
3407 init_cmd
= [ "git", "init" ]
3409 init_cmd
.append("--bare")
3410 retcode
= subprocess
.call(init_cmd
)
3412 raise CalledProcessError(retcode
, init_cmd
)
3414 if not P4Sync
.run(self
, depotPaths
):
3417 # create a master branch and check out a work tree
3418 if gitBranchExists(self
.branch
):
3419 system([ "git", "branch", "master", self
.branch
])
3420 if not self
.cloneBare
:
3421 system([ "git", "checkout", "-f" ])
3423 print 'Not checking out any branch, use ' \
3424 '"git checkout -q -b master <branch>"'
3426 # auto-set this variable if invoked with --use-client-spec
3427 if self
.useClientSpec_from_options
:
3428 system("git config --bool git-p4.useclientspec true")
3432 class P4Branches(Command
):
3434 Command
.__init
__(self
)
3436 self
.description
= ("Shows the git branches that hold imports and their "
3437 + "corresponding perforce depot paths")
3438 self
.verbose
= False
3440 def run(self
, args
):
3441 if originP4BranchesExist():
3442 createOrUpdateBranchesFromOrigin()
3444 cmdline
= "git rev-parse --symbolic "
3445 cmdline
+= " --remotes"
3447 for line
in read_pipe_lines(cmdline
):
3450 if not line
.startswith('p4/') or line
== "p4/HEAD":
3454 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
3455 settings
= extractSettingsGitLog(log
)
3457 print "%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"])
3460 class HelpFormatter(optparse
.IndentedHelpFormatter
):
3462 optparse
.IndentedHelpFormatter
.__init
__(self
)
3464 def format_description(self
, description
):
3466 return description
+ "\n"
3470 def printUsage(commands
):
3471 print "usage: %s <command> [options]" % sys
.argv
[0]
3473 print "valid commands: %s" % ", ".join(commands
)
3475 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
3480 "submit" : P4Submit
,
3481 "commit" : P4Submit
,
3483 "rebase" : P4Rebase
,
3485 "rollback" : P4RollBack
,
3486 "branches" : P4Branches
3491 if len(sys
.argv
[1:]) == 0:
3492 printUsage(commands
.keys())
3495 cmdName
= sys
.argv
[1]
3497 klass
= commands
[cmdName
]
3500 print "unknown command %s" % cmdName
3502 printUsage(commands
.keys())
3505 options
= cmd
.options
3506 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
3510 options
.append(optparse
.make_option("--verbose", "-v", dest
="verbose", action
="store_true"))
3512 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
3514 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
3516 description
= cmd
.description
,
3517 formatter
= HelpFormatter())
3519 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
3521 verbose
= cmd
.verbose
3523 if cmd
.gitdir
== None:
3524 cmd
.gitdir
= os
.path
.abspath(".git")
3525 if not isValidGitDir(cmd
.gitdir
):
3526 cmd
.gitdir
= read_pipe("git rev-parse --git-dir").strip()
3527 if os
.path
.exists(cmd
.gitdir
):
3528 cdup
= read_pipe("git rev-parse --show-cdup").strip()
3532 if not isValidGitDir(cmd
.gitdir
):
3533 if isValidGitDir(cmd
.gitdir
+ "/.git"):
3534 cmd
.gitdir
+= "/.git"
3536 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
3538 os
.environ
["GIT_DIR"] = cmd
.gitdir
3540 if not cmd
.run(args
):
3545 if __name__
== '__main__':