git-p4: add RCS keyword status message
[git] / git-p4.py
1 #!/usr/bin/env python
2 #
3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
4 #
5 # Author: Simon Hausmann <simon@lst.de>
6 # Copyright: 2007 Simon Hausmann <simon@lst.de>
7 #            2007 Trolltech ASA
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
9 #
10 import sys
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")
14     sys.exit(1)
15 import os
16 import optparse
17 import marshal
18 import subprocess
19 import tempfile
20 import time
21 import platform
22 import re
23 import shutil
24 import stat
25 import zipfile
26 import zlib
27 import ctypes
28 import errno
29 import glob
30
31 # support basestring in python3
32 try:
33     unicode = unicode
34 except NameError:
35     # 'unicode' is undefined, must be Python 3
36     str = str
37     unicode = str
38     bytes = bytes
39     basestring = (str,bytes)
40 else:
41     # 'unicode' exists, must be Python 2
42     str = str
43     unicode = unicode
44     bytes = str
45     basestring = basestring
46
47 try:
48     from subprocess import CalledProcessError
49 except ImportError:
50     # from python2.7:subprocess.py
51     # Exception classes used by this module.
52     class CalledProcessError(Exception):
53         """This exception is raised when a process run by check_call() returns
54         a non-zero exit status.  The exit status will be stored in the
55         returncode attribute."""
56         def __init__(self, returncode, cmd):
57             self.returncode = returncode
58             self.cmd = cmd
59         def __str__(self):
60             return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
61
62 verbose = False
63
64 # Only labels/tags matching this will be imported/exported
65 defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
66
67 # The block size is reduced automatically if required
68 defaultBlockSize = 1<<20
69
70 p4_access_checked = False
71
72 def p4_build_cmd(cmd):
73     """Build a suitable p4 command line.
74
75     This consolidates building and returning a p4 command line into one
76     location. It means that hooking into the environment, or other configuration
77     can be done more easily.
78     """
79     real_cmd = ["p4"]
80
81     user = gitConfig("git-p4.user")
82     if len(user) > 0:
83         real_cmd += ["-u",user]
84
85     password = gitConfig("git-p4.password")
86     if len(password) > 0:
87         real_cmd += ["-P", password]
88
89     port = gitConfig("git-p4.port")
90     if len(port) > 0:
91         real_cmd += ["-p", port]
92
93     host = gitConfig("git-p4.host")
94     if len(host) > 0:
95         real_cmd += ["-H", host]
96
97     client = gitConfig("git-p4.client")
98     if len(client) > 0:
99         real_cmd += ["-c", client]
100
101     retries = gitConfigInt("git-p4.retries")
102     if retries is None:
103         # Perform 3 retries by default
104         retries = 3
105     if retries > 0:
106         # Provide a way to not pass this option by setting git-p4.retries to 0
107         real_cmd += ["-r", str(retries)]
108
109     if isinstance(cmd,basestring):
110         real_cmd = ' '.join(real_cmd) + ' ' + cmd
111     else:
112         real_cmd += cmd
113
114     # now check that we can actually talk to the server
115     global p4_access_checked
116     if not p4_access_checked:
117         p4_access_checked = True    # suppress access checks in p4_check_access itself
118         p4_check_access()
119
120     return real_cmd
121
122 def git_dir(path):
123     """ Return TRUE if the given path is a git directory (/path/to/dir/.git).
124         This won't automatically add ".git" to a directory.
125     """
126     d = read_pipe(["git", "--git-dir", path, "rev-parse", "--git-dir"], True).strip()
127     if not d or len(d) == 0:
128         return None
129     else:
130         return d
131
132 def chdir(path, is_client_path=False):
133     """Do chdir to the given path, and set the PWD environment
134        variable for use by P4.  It does not look at getcwd() output.
135        Since we're not using the shell, it is necessary to set the
136        PWD environment variable explicitly.
137
138        Normally, expand the path to force it to be absolute.  This
139        addresses the use of relative path names inside P4 settings,
140        e.g. P4CONFIG=.p4config.  P4 does not simply open the filename
141        as given; it looks for .p4config using PWD.
142
143        If is_client_path, the path was handed to us directly by p4,
144        and may be a symbolic link.  Do not call os.getcwd() in this
145        case, because it will cause p4 to think that PWD is not inside
146        the client path.
147        """
148
149     os.chdir(path)
150     if not is_client_path:
151         path = os.getcwd()
152     os.environ['PWD'] = path
153
154 def calcDiskFree():
155     """Return free space in bytes on the disk of the given dirname."""
156     if platform.system() == 'Windows':
157         free_bytes = ctypes.c_ulonglong(0)
158         ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
159         return free_bytes.value
160     else:
161         st = os.statvfs(os.getcwd())
162         return st.f_bavail * st.f_frsize
163
164 def die(msg):
165     if verbose:
166         raise Exception(msg)
167     else:
168         sys.stderr.write(msg + "\n")
169         sys.exit(1)
170
171 def prompt(prompt_text):
172     """ Prompt the user to choose one of the choices
173
174     Choices are identified in the prompt_text by square brackets around
175     a single letter option.
176     """
177     choices = set(m.group(1) for m in re.finditer(r"\[(.)\]", prompt_text))
178     while True:
179         sys.stderr.flush()
180         sys.stdout.write(prompt_text)
181         sys.stdout.flush()
182         response=sys.stdin.readline().strip().lower()
183         if not response:
184             continue
185         response = response[0]
186         if response in choices:
187             return response
188
189 def run_git_hook(cmd, param=[]):
190     """Execute a hook if the hook exists."""
191     if verbose:
192         sys.stderr.write("Looking for hook: %s\n" % cmd)
193         sys.stderr.flush()
194
195     hooks_path = gitConfig("core.hooksPath")
196     if len(hooks_path) <= 0:
197         hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
198
199     if not isinstance(param, list):
200         param=[param]
201
202     # resolve hook file name, OS depdenent
203     hook_file = os.path.join(hooks_path, cmd)
204     if platform.system() == 'Windows':
205         if not os.path.isfile(hook_file):
206             # look for the file with an extension
207             files = glob.glob(hook_file + ".*")
208             if not files:
209                 return True
210             files.sort()
211             hook_file = files.pop()
212             while hook_file.upper().endswith(".SAMPLE"):
213                 # The file is a sample hook. We don't want it
214                 if len(files) > 0:
215                     hook_file = files.pop()
216                 else:
217                     return True
218
219     if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
220         return True
221
222     return run_hook_command(hook_file, param) == 0
223
224 def run_hook_command(cmd, param):
225     """Executes a git hook command
226        cmd = the command line file to be executed. This can be
227        a file that is run by OS association.
228
229        param = a list of parameters to pass to the cmd command
230
231        On windows, the extension is checked to see if it should
232        be run with the Git for Windows Bash shell.  If there
233        is no file extension, the file is deemed a bash shell
234        and will be handed off to sh.exe. Otherwise, Windows
235        will be called with the shell to handle the file assocation.
236
237        For non Windows operating systems, the file is called
238        as an executable.
239     """
240     cli = [cmd] + param
241     use_shell = False
242     if platform.system() == 'Windows':
243         (root,ext) = os.path.splitext(cmd)
244         if ext == "":
245             exe_path = os.environ.get("EXEPATH")
246             if exe_path is None:
247                 exe_path = ""
248             else:
249                 exe_path = os.path.join(exe_path, "bin")
250             cli = [os.path.join(exe_path, "SH.EXE")] + cli
251         else:
252             use_shell = True
253     return subprocess.call(cli, shell=use_shell)
254
255
256 def write_pipe(c, stdin):
257     if verbose:
258         sys.stderr.write('Writing pipe: %s\n' % str(c))
259
260     expand = isinstance(c,basestring)
261     p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
262     pipe = p.stdin
263     val = pipe.write(stdin)
264     pipe.close()
265     if p.wait():
266         die('Command failed: %s' % str(c))
267
268     return val
269
270 def p4_write_pipe(c, stdin):
271     real_cmd = p4_build_cmd(c)
272     return write_pipe(real_cmd, stdin)
273
274 def read_pipe_full(c):
275     """ Read output from  command. Returns a tuple
276         of the return status, stdout text and stderr
277         text.
278     """
279     if verbose:
280         sys.stderr.write('Reading pipe: %s\n' % str(c))
281
282     expand = isinstance(c,basestring)
283     p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
284     (out, err) = p.communicate()
285     return (p.returncode, out, err)
286
287 def read_pipe(c, ignore_error=False):
288     """ Read output from  command. Returns the output text on
289         success. On failure, terminates execution, unless
290         ignore_error is True, when it returns an empty string.
291     """
292     (retcode, out, err) = read_pipe_full(c)
293     if retcode != 0:
294         if ignore_error:
295             out = ""
296         else:
297             die('Command failed: %s\nError: %s' % (str(c), err))
298     return out
299
300 def read_pipe_text(c):
301     """ Read output from a command with trailing whitespace stripped.
302         On error, returns None.
303     """
304     (retcode, out, err) = read_pipe_full(c)
305     if retcode != 0:
306         return None
307     else:
308         return out.rstrip()
309
310 def p4_read_pipe(c, ignore_error=False):
311     real_cmd = p4_build_cmd(c)
312     return read_pipe(real_cmd, ignore_error)
313
314 def read_pipe_lines(c):
315     if verbose:
316         sys.stderr.write('Reading pipe: %s\n' % str(c))
317
318     expand = isinstance(c, basestring)
319     p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
320     pipe = p.stdout
321     val = pipe.readlines()
322     if pipe.close() or p.wait():
323         die('Command failed: %s' % str(c))
324
325     return val
326
327 def p4_read_pipe_lines(c):
328     """Specifically invoke p4 on the command supplied. """
329     real_cmd = p4_build_cmd(c)
330     return read_pipe_lines(real_cmd)
331
332 def p4_has_command(cmd):
333     """Ask p4 for help on this command.  If it returns an error, the
334        command does not exist in this version of p4."""
335     real_cmd = p4_build_cmd(["help", cmd])
336     p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
337                                    stderr=subprocess.PIPE)
338     p.communicate()
339     return p.returncode == 0
340
341 def p4_has_move_command():
342     """See if the move command exists, that it supports -k, and that
343        it has not been administratively disabled.  The arguments
344        must be correct, but the filenames do not have to exist.  Use
345        ones with wildcards so even if they exist, it will fail."""
346
347     if not p4_has_command("move"):
348         return False
349     cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
350     p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
351     (out, err) = p.communicate()
352     # return code will be 1 in either case
353     if err.find("Invalid option") >= 0:
354         return False
355     if err.find("disabled") >= 0:
356         return False
357     # assume it failed because @... was invalid changelist
358     return True
359
360 def system(cmd, ignore_error=False):
361     expand = isinstance(cmd,basestring)
362     if verbose:
363         sys.stderr.write("executing %s\n" % str(cmd))
364     retcode = subprocess.call(cmd, shell=expand)
365     if retcode and not ignore_error:
366         raise CalledProcessError(retcode, cmd)
367
368     return retcode
369
370 def p4_system(cmd):
371     """Specifically invoke p4 as the system command. """
372     real_cmd = p4_build_cmd(cmd)
373     expand = isinstance(real_cmd, basestring)
374     retcode = subprocess.call(real_cmd, shell=expand)
375     if retcode:
376         raise CalledProcessError(retcode, real_cmd)
377
378 def die_bad_access(s):
379     die("failure accessing depot: {0}".format(s.rstrip()))
380
381 def p4_check_access(min_expiration=1):
382     """ Check if we can access Perforce - account still logged in
383     """
384     results = p4CmdList(["login", "-s"])
385
386     if len(results) == 0:
387         # should never get here: always get either some results, or a p4ExitCode
388         assert("could not parse response from perforce")
389
390     result = results[0]
391
392     if 'p4ExitCode' in result:
393         # p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path
394         die_bad_access("could not run p4")
395
396     code = result.get("code")
397     if not code:
398         # we get here if we couldn't connect and there was nothing to unmarshal
399         die_bad_access("could not connect")
400
401     elif code == "stat":
402         expiry = result.get("TicketExpiration")
403         if expiry:
404             expiry = int(expiry)
405             if expiry > min_expiration:
406                 # ok to carry on
407                 return
408             else:
409                 die_bad_access("perforce ticket expires in {0} seconds".format(expiry))
410
411         else:
412             # account without a timeout - all ok
413             return
414
415     elif code == "error":
416         data = result.get("data")
417         if data:
418             die_bad_access("p4 error: {0}".format(data))
419         else:
420             die_bad_access("unknown error")
421     elif code == "info":
422         return
423     else:
424         die_bad_access("unknown error code {0}".format(code))
425
426 _p4_version_string = None
427 def p4_version_string():
428     """Read the version string, showing just the last line, which
429        hopefully is the interesting version bit.
430
431        $ p4 -V
432        Perforce - The Fast Software Configuration Management System.
433        Copyright 1995-2011 Perforce Software.  All rights reserved.
434        Rev. P4/NTX86/2011.1/393975 (2011/12/16).
435     """
436     global _p4_version_string
437     if not _p4_version_string:
438         a = p4_read_pipe_lines(["-V"])
439         _p4_version_string = a[-1].rstrip()
440     return _p4_version_string
441
442 def p4_integrate(src, dest):
443     p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
444
445 def p4_sync(f, *options):
446     p4_system(["sync"] + list(options) + [wildcard_encode(f)])
447
448 def p4_add(f):
449     # forcibly add file names with wildcards
450     if wildcard_present(f):
451         p4_system(["add", "-f", f])
452     else:
453         p4_system(["add", f])
454
455 def p4_delete(f):
456     p4_system(["delete", wildcard_encode(f)])
457
458 def p4_edit(f, *options):
459     p4_system(["edit"] + list(options) + [wildcard_encode(f)])
460
461 def p4_revert(f):
462     p4_system(["revert", wildcard_encode(f)])
463
464 def p4_reopen(type, f):
465     p4_system(["reopen", "-t", type, wildcard_encode(f)])
466
467 def p4_reopen_in_change(changelist, files):
468     cmd = ["reopen", "-c", str(changelist)] + files
469     p4_system(cmd)
470
471 def p4_move(src, dest):
472     p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
473
474 def p4_last_change():
475     results = p4CmdList(["changes", "-m", "1"], skip_info=True)
476     return int(results[0]['change'])
477
478 def p4_describe(change, shelved=False):
479     """Make sure it returns a valid result by checking for
480        the presence of field "time".  Return a dict of the
481        results."""
482
483     cmd = ["describe", "-s"]
484     if shelved:
485         cmd += ["-S"]
486     cmd += [str(change)]
487
488     ds = p4CmdList(cmd, skip_info=True)
489     if len(ds) != 1:
490         die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
491
492     d = ds[0]
493
494     if "p4ExitCode" in d:
495         die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
496                                                       str(d)))
497     if "code" in d:
498         if d["code"] == "error":
499             die("p4 describe -s %d returned error code: %s" % (change, str(d)))
500
501     if "time" not in d:
502         die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
503
504     return d
505
506 #
507 # Canonicalize the p4 type and return a tuple of the
508 # base type, plus any modifiers.  See "p4 help filetypes"
509 # for a list and explanation.
510 #
511 def split_p4_type(p4type):
512
513     p4_filetypes_historical = {
514         "ctempobj": "binary+Sw",
515         "ctext": "text+C",
516         "cxtext": "text+Cx",
517         "ktext": "text+k",
518         "kxtext": "text+kx",
519         "ltext": "text+F",
520         "tempobj": "binary+FSw",
521         "ubinary": "binary+F",
522         "uresource": "resource+F",
523         "uxbinary": "binary+Fx",
524         "xbinary": "binary+x",
525         "xltext": "text+Fx",
526         "xtempobj": "binary+Swx",
527         "xtext": "text+x",
528         "xunicode": "unicode+x",
529         "xutf16": "utf16+x",
530     }
531     if p4type in p4_filetypes_historical:
532         p4type = p4_filetypes_historical[p4type]
533     mods = ""
534     s = p4type.split("+")
535     base = s[0]
536     mods = ""
537     if len(s) > 1:
538         mods = s[1]
539     return (base, mods)
540
541 #
542 # return the raw p4 type of a file (text, text+ko, etc)
543 #
544 def p4_type(f):
545     results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
546     return results[0]['headType']
547
548 #
549 # Given a type base and modifier, return a regexp matching
550 # the keywords that can be expanded in the file
551 #
552 def p4_keywords_regexp_for_type(base, type_mods):
553     if base in ("text", "unicode", "binary"):
554         kwords = None
555         if "ko" in type_mods:
556             kwords = 'Id|Header'
557         elif "k" in type_mods:
558             kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
559         else:
560             return None
561         pattern = r"""
562             \$              # Starts with a dollar, followed by...
563             (%s)            # one of the keywords, followed by...
564             (:[^$\n]+)?     # possibly an old expansion, followed by...
565             \$              # another dollar
566             """ % kwords
567         return pattern
568     else:
569         return None
570
571 #
572 # Given a file, return a regexp matching the possible
573 # RCS keywords that will be expanded, or None for files
574 # with kw expansion turned off.
575 #
576 def p4_keywords_regexp_for_file(file):
577     if not os.path.exists(file):
578         return None
579     else:
580         (type_base, type_mods) = split_p4_type(p4_type(file))
581         return p4_keywords_regexp_for_type(type_base, type_mods)
582
583 def setP4ExecBit(file, mode):
584     # Reopens an already open file and changes the execute bit to match
585     # the execute bit setting in the passed in mode.
586
587     p4Type = "+x"
588
589     if not isModeExec(mode):
590         p4Type = getP4OpenedType(file)
591         p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
592         p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
593         if p4Type[-1] == "+":
594             p4Type = p4Type[0:-1]
595
596     p4_reopen(p4Type, file)
597
598 def getP4OpenedType(file):
599     # Returns the perforce file type for the given file.
600
601     result = p4_read_pipe(["opened", wildcard_encode(file)])
602     match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
603     if match:
604         return match.group(1)
605     else:
606         die("Could not determine file type for %s (result: '%s')" % (file, result))
607
608 # Return the set of all p4 labels
609 def getP4Labels(depotPaths):
610     labels = set()
611     if isinstance(depotPaths,basestring):
612         depotPaths = [depotPaths]
613
614     for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
615         label = l['label']
616         labels.add(label)
617
618     return labels
619
620 # Return the set of all git tags
621 def getGitTags():
622     gitTags = set()
623     for line in read_pipe_lines(["git", "tag"]):
624         tag = line.strip()
625         gitTags.add(tag)
626     return gitTags
627
628 def diffTreePattern():
629     # This is a simple generator for the diff tree regex pattern. This could be
630     # a class variable if this and parseDiffTreeEntry were a part of a class.
631     pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
632     while True:
633         yield pattern
634
635 def parseDiffTreeEntry(entry):
636     """Parses a single diff tree entry into its component elements.
637
638     See git-diff-tree(1) manpage for details about the format of the diff
639     output. This method returns a dictionary with the following elements:
640
641     src_mode - The mode of the source file
642     dst_mode - The mode of the destination file
643     src_sha1 - The sha1 for the source file
644     dst_sha1 - The sha1 fr the destination file
645     status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
646     status_score - The score for the status (applicable for 'C' and 'R'
647                    statuses). This is None if there is no score.
648     src - The path for the source file.
649     dst - The path for the destination file. This is only present for
650           copy or renames. If it is not present, this is None.
651
652     If the pattern is not matched, None is returned."""
653
654     match = diffTreePattern().next().match(entry)
655     if match:
656         return {
657             'src_mode': match.group(1),
658             'dst_mode': match.group(2),
659             'src_sha1': match.group(3),
660             'dst_sha1': match.group(4),
661             'status': match.group(5),
662             'status_score': match.group(6),
663             'src': match.group(7),
664             'dst': match.group(10)
665         }
666     return None
667
668 def isModeExec(mode):
669     # Returns True if the given git mode represents an executable file,
670     # otherwise False.
671     return mode[-3:] == "755"
672
673 class P4Exception(Exception):
674     """ Base class for exceptions from the p4 client """
675     def __init__(self, exit_code):
676         self.p4ExitCode = exit_code
677
678 class P4ServerException(P4Exception):
679     """ Base class for exceptions where we get some kind of marshalled up result from the server """
680     def __init__(self, exit_code, p4_result):
681         super(P4ServerException, self).__init__(exit_code)
682         self.p4_result = p4_result
683         self.code = p4_result[0]['code']
684         self.data = p4_result[0]['data']
685
686 class P4RequestSizeException(P4ServerException):
687     """ One of the maxresults or maxscanrows errors """
688     def __init__(self, exit_code, p4_result, limit):
689         super(P4RequestSizeException, self).__init__(exit_code, p4_result)
690         self.limit = limit
691
692 def isModeExecChanged(src_mode, dst_mode):
693     return isModeExec(src_mode) != isModeExec(dst_mode)
694
695 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
696         errors_as_exceptions=False):
697
698     if isinstance(cmd,basestring):
699         cmd = "-G " + cmd
700         expand = True
701     else:
702         cmd = ["-G"] + cmd
703         expand = False
704
705     cmd = p4_build_cmd(cmd)
706     if verbose:
707         sys.stderr.write("Opening pipe: %s\n" % str(cmd))
708
709     # Use a temporary file to avoid deadlocks without
710     # subprocess.communicate(), which would put another copy
711     # of stdout into memory.
712     stdin_file = None
713     if stdin is not None:
714         stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
715         if isinstance(stdin,basestring):
716             stdin_file.write(stdin)
717         else:
718             for i in stdin:
719                 stdin_file.write(i + '\n')
720         stdin_file.flush()
721         stdin_file.seek(0)
722
723     p4 = subprocess.Popen(cmd,
724                           shell=expand,
725                           stdin=stdin_file,
726                           stdout=subprocess.PIPE)
727
728     result = []
729     try:
730         while True:
731             entry = marshal.load(p4.stdout)
732             if skip_info:
733                 if 'code' in entry and entry['code'] == 'info':
734                     continue
735             if cb is not None:
736                 cb(entry)
737             else:
738                 result.append(entry)
739     except EOFError:
740         pass
741     exitCode = p4.wait()
742     if exitCode != 0:
743         if errors_as_exceptions:
744             if len(result) > 0:
745                 data = result[0].get('data')
746                 if data:
747                     m = re.search('Too many rows scanned \(over (\d+)\)', data)
748                     if not m:
749                         m = re.search('Request too large \(over (\d+)\)', data)
750
751                     if m:
752                         limit = int(m.group(1))
753                         raise P4RequestSizeException(exitCode, result, limit)
754
755                 raise P4ServerException(exitCode, result)
756             else:
757                 raise P4Exception(exitCode)
758         else:
759             entry = {}
760             entry["p4ExitCode"] = exitCode
761             result.append(entry)
762
763     return result
764
765 def p4Cmd(cmd):
766     list = p4CmdList(cmd)
767     result = {}
768     for entry in list:
769         result.update(entry)
770     return result;
771
772 def p4Where(depotPath):
773     if not depotPath.endswith("/"):
774         depotPath += "/"
775     depotPathLong = depotPath + "..."
776     outputList = p4CmdList(["where", depotPathLong])
777     output = None
778     for entry in outputList:
779         if "depotFile" in entry:
780             # Search for the base client side depot path, as long as it starts with the branch's P4 path.
781             # The base path always ends with "/...".
782             if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
783                 output = entry
784                 break
785         elif "data" in entry:
786             data = entry.get("data")
787             space = data.find(" ")
788             if data[:space] == depotPath:
789                 output = entry
790                 break
791     if output == None:
792         return ""
793     if output["code"] == "error":
794         return ""
795     clientPath = ""
796     if "path" in output:
797         clientPath = output.get("path")
798     elif "data" in output:
799         data = output.get("data")
800         lastSpace = data.rfind(" ")
801         clientPath = data[lastSpace + 1:]
802
803     if clientPath.endswith("..."):
804         clientPath = clientPath[:-3]
805     return clientPath
806
807 def currentGitBranch():
808     return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
809
810 def isValidGitDir(path):
811     return git_dir(path) != None
812
813 def parseRevision(ref):
814     return read_pipe("git rev-parse %s" % ref).strip()
815
816 def branchExists(ref):
817     rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
818                      ignore_error=True)
819     return len(rev) > 0
820
821 def extractLogMessageFromGitCommit(commit):
822     logMessage = ""
823
824     ## fixme: title is first line of commit, not 1st paragraph.
825     foundTitle = False
826     for log in read_pipe_lines(["git", "cat-file", "commit", commit]):
827        if not foundTitle:
828            if len(log) == 1:
829                foundTitle = True
830            continue
831
832        logMessage += log
833     return logMessage
834
835 def extractSettingsGitLog(log):
836     values = {}
837     for line in log.split("\n"):
838         line = line.strip()
839         m = re.search (r"^ *\[git-p4: (.*)\]$", line)
840         if not m:
841             continue
842
843         assignments = m.group(1).split (':')
844         for a in assignments:
845             vals = a.split ('=')
846             key = vals[0].strip()
847             val = ('='.join (vals[1:])).strip()
848             if val.endswith ('\"') and val.startswith('"'):
849                 val = val[1:-1]
850
851             values[key] = val
852
853     paths = values.get("depot-paths")
854     if not paths:
855         paths = values.get("depot-path")
856     if paths:
857         values['depot-paths'] = paths.split(',')
858     return values
859
860 def gitBranchExists(branch):
861     proc = subprocess.Popen(["git", "rev-parse", branch],
862                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);
863     return proc.wait() == 0;
864
865 def gitUpdateRef(ref, newvalue):
866     subprocess.check_call(["git", "update-ref", ref, newvalue])
867
868 def gitDeleteRef(ref):
869     subprocess.check_call(["git", "update-ref", "-d", ref])
870
871 _gitConfig = {}
872
873 def gitConfig(key, typeSpecifier=None):
874     if key not in _gitConfig:
875         cmd = [ "git", "config" ]
876         if typeSpecifier:
877             cmd += [ typeSpecifier ]
878         cmd += [ key ]
879         s = read_pipe(cmd, ignore_error=True)
880         _gitConfig[key] = s.strip()
881     return _gitConfig[key]
882
883 def gitConfigBool(key):
884     """Return a bool, using git config --bool.  It is True only if the
885        variable is set to true, and False if set to false or not present
886        in the config."""
887
888     if key not in _gitConfig:
889         _gitConfig[key] = gitConfig(key, '--bool') == "true"
890     return _gitConfig[key]
891
892 def gitConfigInt(key):
893     if key not in _gitConfig:
894         cmd = [ "git", "config", "--int", key ]
895         s = read_pipe(cmd, ignore_error=True)
896         v = s.strip()
897         try:
898             _gitConfig[key] = int(gitConfig(key, '--int'))
899         except ValueError:
900             _gitConfig[key] = None
901     return _gitConfig[key]
902
903 def gitConfigList(key):
904     if key not in _gitConfig:
905         s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
906         _gitConfig[key] = s.strip().splitlines()
907         if _gitConfig[key] == ['']:
908             _gitConfig[key] = []
909     return _gitConfig[key]
910
911 def p4BranchesInGit(branchesAreInRemotes=True):
912     """Find all the branches whose names start with "p4/", looking
913        in remotes or heads as specified by the argument.  Return
914        a dictionary of { branch: revision } for each one found.
915        The branch names are the short names, without any
916        "p4/" prefix."""
917
918     branches = {}
919
920     cmdline = "git rev-parse --symbolic "
921     if branchesAreInRemotes:
922         cmdline += "--remotes"
923     else:
924         cmdline += "--branches"
925
926     for line in read_pipe_lines(cmdline):
927         line = line.strip()
928
929         # only import to p4/
930         if not line.startswith('p4/'):
931             continue
932         # special symbolic ref to p4/master
933         if line == "p4/HEAD":
934             continue
935
936         # strip off p4/ prefix
937         branch = line[len("p4/"):]
938
939         branches[branch] = parseRevision(line)
940
941     return branches
942
943 def branch_exists(branch):
944     """Make sure that the given ref name really exists."""
945
946     cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
947     p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
948     out, _ = p.communicate()
949     if p.returncode:
950         return False
951     # expect exactly one line of output: the branch name
952     return out.rstrip() == branch
953
954 def findUpstreamBranchPoint(head = "HEAD"):
955     branches = p4BranchesInGit()
956     # map from depot-path to branch name
957     branchByDepotPath = {}
958     for branch in branches.keys():
959         tip = branches[branch]
960         log = extractLogMessageFromGitCommit(tip)
961         settings = extractSettingsGitLog(log)
962         if "depot-paths" in settings:
963             paths = ",".join(settings["depot-paths"])
964             branchByDepotPath[paths] = "remotes/p4/" + branch
965
966     settings = None
967     parent = 0
968     while parent < 65535:
969         commit = head + "~%s" % parent
970         log = extractLogMessageFromGitCommit(commit)
971         settings = extractSettingsGitLog(log)
972         if "depot-paths" in settings:
973             paths = ",".join(settings["depot-paths"])
974             if paths in branchByDepotPath:
975                 return [branchByDepotPath[paths], settings]
976
977         parent = parent + 1
978
979     return ["", settings]
980
981 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
982     if not silent:
983         print("Creating/updating branch(es) in %s based on origin branch(es)"
984                % localRefPrefix)
985
986     originPrefix = "origin/p4/"
987
988     for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
989         line = line.strip()
990         if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
991             continue
992
993         headName = line[len(originPrefix):]
994         remoteHead = localRefPrefix + headName
995         originHead = line
996
997         original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
998         if ('depot-paths' not in original
999             or 'change' not in original):
1000             continue
1001
1002         update = False
1003         if not gitBranchExists(remoteHead):
1004             if verbose:
1005                 print("creating %s" % remoteHead)
1006             update = True
1007         else:
1008             settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
1009             if 'change' in settings:
1010                 if settings['depot-paths'] == original['depot-paths']:
1011                     originP4Change = int(original['change'])
1012                     p4Change = int(settings['change'])
1013                     if originP4Change > p4Change:
1014                         print("%s (%s) is newer than %s (%s). "
1015                                "Updating p4 branch from origin."
1016                                % (originHead, originP4Change,
1017                                   remoteHead, p4Change))
1018                         update = True
1019                 else:
1020                     print("Ignoring: %s was imported from %s while "
1021                            "%s was imported from %s"
1022                            % (originHead, ','.join(original['depot-paths']),
1023                               remoteHead, ','.join(settings['depot-paths'])))
1024
1025         if update:
1026             system("git update-ref %s %s" % (remoteHead, originHead))
1027
1028 def originP4BranchesExist():
1029         return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
1030
1031
1032 def p4ParseNumericChangeRange(parts):
1033     changeStart = int(parts[0][1:])
1034     if parts[1] == '#head':
1035         changeEnd = p4_last_change()
1036     else:
1037         changeEnd = int(parts[1])
1038
1039     return (changeStart, changeEnd)
1040
1041 def chooseBlockSize(blockSize):
1042     if blockSize:
1043         return blockSize
1044     else:
1045         return defaultBlockSize
1046
1047 def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
1048     assert depotPaths
1049
1050     # Parse the change range into start and end. Try to find integer
1051     # revision ranges as these can be broken up into blocks to avoid
1052     # hitting server-side limits (maxrows, maxscanresults). But if
1053     # that doesn't work, fall back to using the raw revision specifier
1054     # strings, without using block mode.
1055
1056     if changeRange is None or changeRange == '':
1057         changeStart = 1
1058         changeEnd = p4_last_change()
1059         block_size = chooseBlockSize(requestedBlockSize)
1060     else:
1061         parts = changeRange.split(',')
1062         assert len(parts) == 2
1063         try:
1064             (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
1065             block_size = chooseBlockSize(requestedBlockSize)
1066         except ValueError:
1067             changeStart = parts[0][1:]
1068             changeEnd = parts[1]
1069             if requestedBlockSize:
1070                 die("cannot use --changes-block-size with non-numeric revisions")
1071             block_size = None
1072
1073     changes = set()
1074
1075     # Retrieve changes a block at a time, to prevent running
1076     # into a MaxResults/MaxScanRows error from the server. If
1077     # we _do_ hit one of those errors, turn down the block size
1078
1079     while True:
1080         cmd = ['changes']
1081
1082         if block_size:
1083             end = min(changeEnd, changeStart + block_size)
1084             revisionRange = "%d,%d" % (changeStart, end)
1085         else:
1086             revisionRange = "%s,%s" % (changeStart, changeEnd)
1087
1088         for p in depotPaths:
1089             cmd += ["%s...@%s" % (p, revisionRange)]
1090
1091         # fetch the changes
1092         try:
1093             result = p4CmdList(cmd, errors_as_exceptions=True)
1094         except P4RequestSizeException as e:
1095             if not block_size:
1096                 block_size = e.limit
1097             elif block_size > e.limit:
1098                 block_size = e.limit
1099             else:
1100                 block_size = max(2, block_size // 2)
1101
1102             if verbose: print("block size error, retrying with block size {0}".format(block_size))
1103             continue
1104         except P4Exception as e:
1105             die('Error retrieving changes description ({0})'.format(e.p4ExitCode))
1106
1107         # Insert changes in chronological order
1108         for entry in reversed(result):
1109             if 'change' not in entry:
1110                 continue
1111             changes.add(int(entry['change']))
1112
1113         if not block_size:
1114             break
1115
1116         if end >= changeEnd:
1117             break
1118
1119         changeStart = end + 1
1120
1121     changes = sorted(changes)
1122     return changes
1123
1124 def p4PathStartsWith(path, prefix):
1125     # This method tries to remedy a potential mixed-case issue:
1126     #
1127     # If UserA adds  //depot/DirA/file1
1128     # and UserB adds //depot/dira/file2
1129     #
1130     # we may or may not have a problem. If you have core.ignorecase=true,
1131     # we treat DirA and dira as the same directory
1132     if gitConfigBool("core.ignorecase"):
1133         return path.lower().startswith(prefix.lower())
1134     return path.startswith(prefix)
1135
1136 def getClientSpec():
1137     """Look at the p4 client spec, create a View() object that contains
1138        all the mappings, and return it."""
1139
1140     specList = p4CmdList("client -o")
1141     if len(specList) != 1:
1142         die('Output from "client -o" is %d lines, expecting 1' %
1143             len(specList))
1144
1145     # dictionary of all client parameters
1146     entry = specList[0]
1147
1148     # the //client/ name
1149     client_name = entry["Client"]
1150
1151     # just the keys that start with "View"
1152     view_keys = [ k for k in entry.keys() if k.startswith("View") ]
1153
1154     # hold this new View
1155     view = View(client_name)
1156
1157     # append the lines, in order, to the view
1158     for view_num in range(len(view_keys)):
1159         k = "View%d" % view_num
1160         if k not in view_keys:
1161             die("Expected view key %s missing" % k)
1162         view.append(entry[k])
1163
1164     return view
1165
1166 def getClientRoot():
1167     """Grab the client directory."""
1168
1169     output = p4CmdList("client -o")
1170     if len(output) != 1:
1171         die('Output from "client -o" is %d lines, expecting 1' % len(output))
1172
1173     entry = output[0]
1174     if "Root" not in entry:
1175         die('Client has no "Root"')
1176
1177     return entry["Root"]
1178
1179 #
1180 # P4 wildcards are not allowed in filenames.  P4 complains
1181 # if you simply add them, but you can force it with "-f", in
1182 # which case it translates them into %xx encoding internally.
1183 #
1184 def wildcard_decode(path):
1185     # Search for and fix just these four characters.  Do % last so
1186     # that fixing it does not inadvertently create new %-escapes.
1187     # Cannot have * in a filename in windows; untested as to
1188     # what p4 would do in such a case.
1189     if not platform.system() == "Windows":
1190         path = path.replace("%2A", "*")
1191     path = path.replace("%23", "#") \
1192                .replace("%40", "@") \
1193                .replace("%25", "%")
1194     return path
1195
1196 def wildcard_encode(path):
1197     # do % first to avoid double-encoding the %s introduced here
1198     path = path.replace("%", "%25") \
1199                .replace("*", "%2A") \
1200                .replace("#", "%23") \
1201                .replace("@", "%40")
1202     return path
1203
1204 def wildcard_present(path):
1205     m = re.search("[*#@%]", path)
1206     return m is not None
1207
1208 class LargeFileSystem(object):
1209     """Base class for large file system support."""
1210
1211     def __init__(self, writeToGitStream):
1212         self.largeFiles = set()
1213         self.writeToGitStream = writeToGitStream
1214
1215     def generatePointer(self, cloneDestination, contentFile):
1216         """Return the content of a pointer file that is stored in Git instead of
1217            the actual content."""
1218         assert False, "Method 'generatePointer' required in " + self.__class__.__name__
1219
1220     def pushFile(self, localLargeFile):
1221         """Push the actual content which is not stored in the Git repository to
1222            a server."""
1223         assert False, "Method 'pushFile' required in " + self.__class__.__name__
1224
1225     def hasLargeFileExtension(self, relPath):
1226         return reduce(
1227             lambda a, b: a or b,
1228             [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1229             False
1230         )
1231
1232     def generateTempFile(self, contents):
1233         contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1234         for d in contents:
1235             contentFile.write(d)
1236         contentFile.close()
1237         return contentFile.name
1238
1239     def exceedsLargeFileThreshold(self, relPath, contents):
1240         if gitConfigInt('git-p4.largeFileThreshold'):
1241             contentsSize = sum(len(d) for d in contents)
1242             if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1243                 return True
1244         if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1245             contentsSize = sum(len(d) for d in contents)
1246             if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1247                 return False
1248             contentTempFile = self.generateTempFile(contents)
1249             compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=True)
1250             with zipfile.ZipFile(compressedContentFile, mode='w') as zf:
1251                 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1252                 compressedContentsSize = zf.infolist()[0].compress_size
1253             os.remove(contentTempFile)
1254             if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1255                 return True
1256         return False
1257
1258     def addLargeFile(self, relPath):
1259         self.largeFiles.add(relPath)
1260
1261     def removeLargeFile(self, relPath):
1262         self.largeFiles.remove(relPath)
1263
1264     def isLargeFile(self, relPath):
1265         return relPath in self.largeFiles
1266
1267     def processContent(self, git_mode, relPath, contents):
1268         """Processes the content of git fast import. This method decides if a
1269            file is stored in the large file system and handles all necessary
1270            steps."""
1271         if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1272             contentTempFile = self.generateTempFile(contents)
1273             (pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
1274             if pointer_git_mode:
1275                 git_mode = pointer_git_mode
1276             if localLargeFile:
1277                 # Move temp file to final location in large file system
1278                 largeFileDir = os.path.dirname(localLargeFile)
1279                 if not os.path.isdir(largeFileDir):
1280                     os.makedirs(largeFileDir)
1281                 shutil.move(contentTempFile, localLargeFile)
1282                 self.addLargeFile(relPath)
1283                 if gitConfigBool('git-p4.largeFilePush'):
1284                     self.pushFile(localLargeFile)
1285                 if verbose:
1286                     sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
1287         return (git_mode, contents)
1288
1289 class MockLFS(LargeFileSystem):
1290     """Mock large file system for testing."""
1291
1292     def generatePointer(self, contentFile):
1293         """The pointer content is the original content prefixed with "pointer-".
1294            The local filename of the large file storage is derived from the file content.
1295            """
1296         with open(contentFile, 'r') as f:
1297             content = next(f)
1298             gitMode = '100644'
1299             pointerContents = 'pointer-' + content
1300             localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1301             return (gitMode, pointerContents, localLargeFile)
1302
1303     def pushFile(self, localLargeFile):
1304         """The remote filename of the large file storage is the same as the local
1305            one but in a different directory.
1306            """
1307         remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1308         if not os.path.exists(remotePath):
1309             os.makedirs(remotePath)
1310         shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1311
1312 class GitLFS(LargeFileSystem):
1313     """Git LFS as backend for the git-p4 large file system.
1314        See https://git-lfs.github.com/ for details."""
1315
1316     def __init__(self, *args):
1317         LargeFileSystem.__init__(self, *args)
1318         self.baseGitAttributes = []
1319
1320     def generatePointer(self, contentFile):
1321         """Generate a Git LFS pointer for the content. Return LFS Pointer file
1322            mode and content which is stored in the Git repository instead of
1323            the actual content. Return also the new location of the actual
1324            content.
1325            """
1326         if os.path.getsize(contentFile) == 0:
1327             return (None, '', None)
1328
1329         pointerProcess = subprocess.Popen(
1330             ['git', 'lfs', 'pointer', '--file=' + contentFile],
1331             stdout=subprocess.PIPE
1332         )
1333         pointerFile = pointerProcess.stdout.read()
1334         if pointerProcess.wait():
1335             os.remove(contentFile)
1336             die('git-lfs pointer command failed. Did you install the extension?')
1337
1338         # Git LFS removed the preamble in the output of the 'pointer' command
1339         # starting from version 1.2.0. Check for the preamble here to support
1340         # earlier versions.
1341         # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1342         if pointerFile.startswith('Git LFS pointer for'):
1343             pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1344
1345         oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
1346         # if someone use external lfs.storage ( not in local repo git )
1347         lfs_path = gitConfig('lfs.storage')
1348         if not lfs_path:
1349             lfs_path = 'lfs'
1350         if not os.path.isabs(lfs_path):
1351             lfs_path = os.path.join(os.getcwd(), '.git', lfs_path)
1352         localLargeFile = os.path.join(
1353             lfs_path,
1354             'objects', oid[:2], oid[2:4],
1355             oid,
1356         )
1357         # LFS Spec states that pointer files should not have the executable bit set.
1358         gitMode = '100644'
1359         return (gitMode, pointerFile, localLargeFile)
1360
1361     def pushFile(self, localLargeFile):
1362         uploadProcess = subprocess.Popen(
1363             ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1364         )
1365         if uploadProcess.wait():
1366             die('git-lfs push command failed. Did you define a remote?')
1367
1368     def generateGitAttributes(self):
1369         return (
1370             self.baseGitAttributes +
1371             [
1372                 '\n',
1373                 '#\n',
1374                 '# Git LFS (see https://git-lfs.github.com/)\n',
1375                 '#\n',
1376             ] +
1377             ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1378                 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1379             ] +
1380             ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1381                 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1382             ]
1383         )
1384
1385     def addLargeFile(self, relPath):
1386         LargeFileSystem.addLargeFile(self, relPath)
1387         self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1388
1389     def removeLargeFile(self, relPath):
1390         LargeFileSystem.removeLargeFile(self, relPath)
1391         self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1392
1393     def processContent(self, git_mode, relPath, contents):
1394         if relPath == '.gitattributes':
1395             self.baseGitAttributes = contents
1396             return (git_mode, self.generateGitAttributes())
1397         else:
1398             return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1399
1400 class Command:
1401     delete_actions = ( "delete", "move/delete", "purge" )
1402     add_actions = ( "add", "branch", "move/add" )
1403
1404     def __init__(self):
1405         self.usage = "usage: %prog [options]"
1406         self.needsGit = True
1407         self.verbose = False
1408
1409     # This is required for the "append" update_shelve action
1410     def ensure_value(self, attr, value):
1411         if not hasattr(self, attr) or getattr(self, attr) is None:
1412             setattr(self, attr, value)
1413         return getattr(self, attr)
1414
1415 class P4UserMap:
1416     def __init__(self):
1417         self.userMapFromPerforceServer = False
1418         self.myP4UserId = None
1419
1420     def p4UserId(self):
1421         if self.myP4UserId:
1422             return self.myP4UserId
1423
1424         results = p4CmdList("user -o")
1425         for r in results:
1426             if 'User' in r:
1427                 self.myP4UserId = r['User']
1428                 return r['User']
1429         die("Could not find your p4 user id")
1430
1431     def p4UserIsMe(self, p4User):
1432         # return True if the given p4 user is actually me
1433         me = self.p4UserId()
1434         if not p4User or p4User != me:
1435             return False
1436         else:
1437             return True
1438
1439     def getUserCacheFilename(self):
1440         home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1441         return home + "/.gitp4-usercache.txt"
1442
1443     def getUserMapFromPerforceServer(self):
1444         if self.userMapFromPerforceServer:
1445             return
1446         self.users = {}
1447         self.emails = {}
1448
1449         for output in p4CmdList("users"):
1450             if "User" not in output:
1451                 continue
1452             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1453             self.emails[output["Email"]] = output["User"]
1454
1455         mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1456         for mapUserConfig in gitConfigList("git-p4.mapUser"):
1457             mapUser = mapUserConfigRegex.findall(mapUserConfig)
1458             if mapUser and len(mapUser[0]) == 3:
1459                 user = mapUser[0][0]
1460                 fullname = mapUser[0][1]
1461                 email = mapUser[0][2]
1462                 self.users[user] = fullname + " <" + email + ">"
1463                 self.emails[email] = user
1464
1465         s = ''
1466         for (key, val) in self.users.items():
1467             s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1468
1469         open(self.getUserCacheFilename(), "wb").write(s)
1470         self.userMapFromPerforceServer = True
1471
1472     def loadUserMapFromCache(self):
1473         self.users = {}
1474         self.userMapFromPerforceServer = False
1475         try:
1476             cache = open(self.getUserCacheFilename(), "rb")
1477             lines = cache.readlines()
1478             cache.close()
1479             for line in lines:
1480                 entry = line.strip().split("\t")
1481                 self.users[entry[0]] = entry[1]
1482         except IOError:
1483             self.getUserMapFromPerforceServer()
1484
1485 class P4Debug(Command):
1486     def __init__(self):
1487         Command.__init__(self)
1488         self.options = []
1489         self.description = "A tool to debug the output of p4 -G."
1490         self.needsGit = False
1491
1492     def run(self, args):
1493         j = 0
1494         for output in p4CmdList(args):
1495             print('Element: %d' % j)
1496             j += 1
1497             print(output)
1498         return True
1499
1500 class P4RollBack(Command):
1501     def __init__(self):
1502         Command.__init__(self)
1503         self.options = [
1504             optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
1505         ]
1506         self.description = "A tool to debug the multi-branch import. Don't use :)"
1507         self.rollbackLocalBranches = False
1508
1509     def run(self, args):
1510         if len(args) != 1:
1511             return False
1512         maxChange = int(args[0])
1513
1514         if "p4ExitCode" in p4Cmd("changes -m 1"):
1515             die("Problems executing p4");
1516
1517         if self.rollbackLocalBranches:
1518             refPrefix = "refs/heads/"
1519             lines = read_pipe_lines("git rev-parse --symbolic --branches")
1520         else:
1521             refPrefix = "refs/remotes/"
1522             lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1523
1524         for line in lines:
1525             if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1526                 line = line.strip()
1527                 ref = refPrefix + line
1528                 log = extractLogMessageFromGitCommit(ref)
1529                 settings = extractSettingsGitLog(log)
1530
1531                 depotPaths = settings['depot-paths']
1532                 change = settings['change']
1533
1534                 changed = False
1535
1536                 if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
1537                                                            for p in depotPaths]))) == 0:
1538                     print("Branch %s did not exist at change %s, deleting." % (ref, maxChange))
1539                     system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1540                     continue
1541
1542                 while change and int(change) > maxChange:
1543                     changed = True
1544                     if self.verbose:
1545                         print("%s is at %s ; rewinding towards %s" % (ref, change, maxChange))
1546                     system("git update-ref %s \"%s^\"" % (ref, ref))
1547                     log = extractLogMessageFromGitCommit(ref)
1548                     settings =  extractSettingsGitLog(log)
1549
1550
1551                     depotPaths = settings['depot-paths']
1552                     change = settings['change']
1553
1554                 if changed:
1555                     print("%s rewound to %s" % (ref, change))
1556
1557         return True
1558
1559 class P4Submit(Command, P4UserMap):
1560
1561     conflict_behavior_choices = ("ask", "skip", "quit")
1562
1563     def __init__(self):
1564         Command.__init__(self)
1565         P4UserMap.__init__(self)
1566         self.options = [
1567                 optparse.make_option("--origin", dest="origin"),
1568                 optparse.make_option("-M", dest="detectRenames", action="store_true"),
1569                 # preserve the user, requires relevant p4 permissions
1570                 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1571                 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1572                 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1573                 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1574                 optparse.make_option("--conflict", dest="conflict_behavior",
1575                                      choices=self.conflict_behavior_choices),
1576                 optparse.make_option("--branch", dest="branch"),
1577                 optparse.make_option("--shelve", dest="shelve", action="store_true",
1578                                      help="Shelve instead of submit. Shelved files are reverted, "
1579                                      "restoring the workspace to the state before the shelve"),
1580                 optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
1581                                      metavar="CHANGELIST",
1582                                      help="update an existing shelved changelist, implies --shelve, "
1583                                            "repeat in-order for multiple shelved changelists"),
1584                 optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1585                                      help="submit only the specified commit(s), one commit or xxx..xxx"),
1586                 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1587                                      help="Disable rebase after submit is completed. Can be useful if you "
1588                                      "work from a local git branch that is not master"),
1589                 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1590                                      help="Skip Perforce sync of p4/master after submit or shelve"),
1591                 optparse.make_option("--no-verify", dest="no_verify", action="store_true",
1592                                      help="Bypass p4-pre-submit and p4-changelist hooks"),
1593         ]
1594         self.description = """Submit changes from git to the perforce depot.\n
1595     The `p4-pre-submit` hook is executed if it exists and is executable. It
1596     can be bypassed with the `--no-verify` command line option. The hook takes
1597     no parameters and nothing from standard input. Exiting with a non-zero status
1598     from this script prevents `git-p4 submit` from launching.
1599
1600     One usage scenario is to run unit tests in the hook.
1601
1602     The `p4-prepare-changelist` hook is executed right after preparing the default
1603     changelist message and before the editor is started. It takes one parameter,
1604     the name of the file that contains the changelist text. Exiting with a non-zero
1605     status from the script will abort the process.
1606
1607     The purpose of the hook is to edit the message file in place, and it is not
1608     supressed by the `--no-verify` option. This hook is called even if
1609     `--prepare-p4-only` is set.
1610
1611     The `p4-changelist` hook is executed after the changelist message has been
1612     edited by the user. It can be bypassed with the `--no-verify` option. It
1613     takes a single parameter, the name of the file that holds the proposed
1614     changelist text. Exiting with a non-zero status causes the command to abort.
1615
1616     The hook is allowed to edit the changelist file and can be used to normalize
1617     the text into some project standard format. It can also be used to refuse the
1618     Submit after inspect the message file.
1619
1620     The `p4-post-changelist` hook is invoked after the submit has successfully
1621     occured in P4. It takes no parameters and is meant primarily for notification
1622     and cannot affect the outcome of the git p4 submit action.
1623     """
1624
1625         self.usage += " [name of git branch to submit into perforce depot]"
1626         self.origin = ""
1627         self.detectRenames = False
1628         self.preserveUser = gitConfigBool("git-p4.preserveUser")
1629         self.dry_run = False
1630         self.shelve = False
1631         self.update_shelve = list()
1632         self.commit = ""
1633         self.disable_rebase = gitConfigBool("git-p4.disableRebase")
1634         self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
1635         self.prepare_p4_only = False
1636         self.conflict_behavior = None
1637         self.isWindows = (platform.system() == "Windows")
1638         self.exportLabels = False
1639         self.p4HasMoveCommand = p4_has_move_command()
1640         self.branch = None
1641         self.no_verify = False
1642
1643         if gitConfig('git-p4.largeFileSystem'):
1644             die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1645
1646     def check(self):
1647         if len(p4CmdList("opened ...")) > 0:
1648             die("You have files opened with perforce! Close them before starting the sync.")
1649
1650     def separate_jobs_from_description(self, message):
1651         """Extract and return a possible Jobs field in the commit
1652            message.  It goes into a separate section in the p4 change
1653            specification.
1654
1655            A jobs line starts with "Jobs:" and looks like a new field
1656            in a form.  Values are white-space separated on the same
1657            line or on following lines that start with a tab.
1658
1659            This does not parse and extract the full git commit message
1660            like a p4 form.  It just sees the Jobs: line as a marker
1661            to pass everything from then on directly into the p4 form,
1662            but outside the description section.
1663
1664            Return a tuple (stripped log message, jobs string)."""
1665
1666         m = re.search(r'^Jobs:', message, re.MULTILINE)
1667         if m is None:
1668             return (message, None)
1669
1670         jobtext = message[m.start():]
1671         stripped_message = message[:m.start()].rstrip()
1672         return (stripped_message, jobtext)
1673
1674     def prepareLogMessage(self, template, message, jobs):
1675         """Edits the template returned from "p4 change -o" to insert
1676            the message in the Description field, and the jobs text in
1677            the Jobs field."""
1678         result = ""
1679
1680         inDescriptionSection = False
1681
1682         for line in template.split("\n"):
1683             if line.startswith("#"):
1684                 result += line + "\n"
1685                 continue
1686
1687             if inDescriptionSection:
1688                 if line.startswith("Files:") or line.startswith("Jobs:"):
1689                     inDescriptionSection = False
1690                     # insert Jobs section
1691                     if jobs:
1692                         result += jobs + "\n"
1693                 else:
1694                     continue
1695             else:
1696                 if line.startswith("Description:"):
1697                     inDescriptionSection = True
1698                     line += "\n"
1699                     for messageLine in message.split("\n"):
1700                         line += "\t" + messageLine + "\n"
1701
1702             result += line + "\n"
1703
1704         return result
1705
1706     def patchRCSKeywords(self, file, pattern):
1707         # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1708         (handle, outFileName) = tempfile.mkstemp(dir='.')
1709         try:
1710             outFile = os.fdopen(handle, "w+")
1711             inFile = open(file, "r")
1712             regexp = re.compile(pattern, re.VERBOSE)
1713             for line in inFile.readlines():
1714                 line = regexp.sub(r'$\1$', line)
1715                 outFile.write(line)
1716             inFile.close()
1717             outFile.close()
1718             # Forcibly overwrite the original file
1719             os.unlink(file)
1720             shutil.move(outFileName, file)
1721         except:
1722             # cleanup our temporary file
1723             os.unlink(outFileName)
1724             print("Failed to strip RCS keywords in %s" % file)
1725             raise
1726
1727         print("Patched up RCS keywords in %s" % file)
1728
1729     def p4UserForCommit(self,id):
1730         # Return the tuple (perforce user,git email) for a given git commit id
1731         self.getUserMapFromPerforceServer()
1732         gitEmail = read_pipe(["git", "log", "--max-count=1",
1733                               "--format=%ae", id])
1734         gitEmail = gitEmail.strip()
1735         if gitEmail not in self.emails:
1736             return (None,gitEmail)
1737         else:
1738             return (self.emails[gitEmail],gitEmail)
1739
1740     def checkValidP4Users(self,commits):
1741         # check if any git authors cannot be mapped to p4 users
1742         for id in commits:
1743             (user,email) = self.p4UserForCommit(id)
1744             if not user:
1745                 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1746                 if gitConfigBool("git-p4.allowMissingP4Users"):
1747                     print("%s" % msg)
1748                 else:
1749                     die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1750
1751     def lastP4Changelist(self):
1752         # Get back the last changelist number submitted in this client spec. This
1753         # then gets used to patch up the username in the change. If the same
1754         # client spec is being used by multiple processes then this might go
1755         # wrong.
1756         results = p4CmdList("client -o")        # find the current client
1757         client = None
1758         for r in results:
1759             if 'Client' in r:
1760                 client = r['Client']
1761                 break
1762         if not client:
1763             die("could not get client spec")
1764         results = p4CmdList(["changes", "-c", client, "-m", "1"])
1765         for r in results:
1766             if 'change' in r:
1767                 return r['change']
1768         die("Could not get changelist number for last submit - cannot patch up user details")
1769
1770     def modifyChangelistUser(self, changelist, newUser):
1771         # fixup the user field of a changelist after it has been submitted.
1772         changes = p4CmdList("change -o %s" % changelist)
1773         if len(changes) != 1:
1774             die("Bad output from p4 change modifying %s to user %s" %
1775                 (changelist, newUser))
1776
1777         c = changes[0]
1778         if c['User'] == newUser: return   # nothing to do
1779         c['User'] = newUser
1780         input = marshal.dumps(c)
1781
1782         result = p4CmdList("change -f -i", stdin=input)
1783         for r in result:
1784             if 'code' in r:
1785                 if r['code'] == 'error':
1786                     die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1787             if 'data' in r:
1788                 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1789                 return
1790         die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1791
1792     def canChangeChangelists(self):
1793         # check to see if we have p4 admin or super-user permissions, either of
1794         # which are required to modify changelists.
1795         results = p4CmdList(["protects", self.depotPath])
1796         for r in results:
1797             if 'perm' in r:
1798                 if r['perm'] == 'admin':
1799                     return 1
1800                 if r['perm'] == 'super':
1801                     return 1
1802         return 0
1803
1804     def prepareSubmitTemplate(self, changelist=None):
1805         """Run "p4 change -o" to grab a change specification template.
1806            This does not use "p4 -G", as it is nice to keep the submission
1807            template in original order, since a human might edit it.
1808
1809            Remove lines in the Files section that show changes to files
1810            outside the depot path we're committing into."""
1811
1812         [upstream, settings] = findUpstreamBranchPoint()
1813
1814         template = """\
1815 # A Perforce Change Specification.
1816 #
1817 #  Change:      The change number. 'new' on a new changelist.
1818 #  Date:        The date this specification was last modified.
1819 #  Client:      The client on which the changelist was created.  Read-only.
1820 #  User:        The user who created the changelist.
1821 #  Status:      Either 'pending' or 'submitted'. Read-only.
1822 #  Type:        Either 'public' or 'restricted'. Default is 'public'.
1823 #  Description: Comments about the changelist.  Required.
1824 #  Jobs:        What opened jobs are to be closed by this changelist.
1825 #               You may delete jobs from this list.  (New changelists only.)
1826 #  Files:       What opened files from the default changelist are to be added
1827 #               to this changelist.  You may delete files from this list.
1828 #               (New changelists only.)
1829 """
1830         files_list = []
1831         inFilesSection = False
1832         change_entry = None
1833         args = ['change', '-o']
1834         if changelist:
1835             args.append(str(changelist))
1836         for entry in p4CmdList(args):
1837             if 'code' not in entry:
1838                 continue
1839             if entry['code'] == 'stat':
1840                 change_entry = entry
1841                 break
1842         if not change_entry:
1843             die('Failed to decode output of p4 change -o')
1844         for key, value in change_entry.iteritems():
1845             if key.startswith('File'):
1846                 if 'depot-paths' in settings:
1847                     if not [p for p in settings['depot-paths']
1848                             if p4PathStartsWith(value, p)]:
1849                         continue
1850                 else:
1851                     if not p4PathStartsWith(value, self.depotPath):
1852                         continue
1853                 files_list.append(value)
1854                 continue
1855         # Output in the order expected by prepareLogMessage
1856         for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
1857             if key not in change_entry:
1858                 continue
1859             template += '\n'
1860             template += key + ':'
1861             if key == 'Description':
1862                 template += '\n'
1863             for field_line in change_entry[key].splitlines():
1864                 template += '\t'+field_line+'\n'
1865         if len(files_list) > 0:
1866             template += '\n'
1867             template += 'Files:\n'
1868         for path in files_list:
1869             template += '\t'+path+'\n'
1870         return template
1871
1872     def edit_template(self, template_file):
1873         """Invoke the editor to let the user change the submission
1874            message.  Return true if okay to continue with the submit."""
1875
1876         # if configured to skip the editing part, just submit
1877         if gitConfigBool("git-p4.skipSubmitEdit"):
1878             return True
1879
1880         # look at the modification time, to check later if the user saved
1881         # the file
1882         mtime = os.stat(template_file).st_mtime
1883
1884         # invoke the editor
1885         if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
1886             editor = os.environ.get("P4EDITOR")
1887         else:
1888             editor = read_pipe("git var GIT_EDITOR").strip()
1889         system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1890
1891         # If the file was not saved, prompt to see if this patch should
1892         # be skipped.  But skip this verification step if configured so.
1893         if gitConfigBool("git-p4.skipSubmitEditCheck"):
1894             return True
1895
1896         # modification time updated means user saved the file
1897         if os.stat(template_file).st_mtime > mtime:
1898             return True
1899
1900         response = prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1901         if response == 'y':
1902             return True
1903         if response == 'n':
1904             return False
1905
1906     def get_diff_description(self, editedFiles, filesToAdd, symlinks):
1907         # diff
1908         if "P4DIFF" in os.environ:
1909             del(os.environ["P4DIFF"])
1910         diff = ""
1911         for editedFile in editedFiles:
1912             diff += p4_read_pipe(['diff', '-du',
1913                                   wildcard_encode(editedFile)])
1914
1915         # new file diff
1916         newdiff = ""
1917         for newFile in filesToAdd:
1918             newdiff += "==== new file ====\n"
1919             newdiff += "--- /dev/null\n"
1920             newdiff += "+++ %s\n" % newFile
1921
1922             is_link = os.path.islink(newFile)
1923             expect_link = newFile in symlinks
1924
1925             if is_link and expect_link:
1926                 newdiff += "+%s\n" % os.readlink(newFile)
1927             else:
1928                 f = open(newFile, "r")
1929                 for line in f.readlines():
1930                     newdiff += "+" + line
1931                 f.close()
1932
1933         return (diff + newdiff).replace('\r\n', '\n')
1934
1935     def applyCommit(self, id):
1936         """Apply one commit, return True if it succeeded."""
1937
1938         print("Applying", read_pipe(["git", "show", "-s",
1939                                      "--format=format:%h %s", id]))
1940
1941         (p4User, gitEmail) = self.p4UserForCommit(id)
1942
1943         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1944         filesToAdd = set()
1945         filesToChangeType = set()
1946         filesToDelete = set()
1947         editedFiles = set()
1948         pureRenameCopy = set()
1949         symlinks = set()
1950         filesToChangeExecBit = {}
1951         all_files = list()
1952
1953         for line in diff:
1954             diff = parseDiffTreeEntry(line)
1955             modifier = diff['status']
1956             path = diff['src']
1957             all_files.append(path)
1958
1959             if modifier == "M":
1960                 p4_edit(path)
1961                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1962                     filesToChangeExecBit[path] = diff['dst_mode']
1963                 editedFiles.add(path)
1964             elif modifier == "A":
1965                 filesToAdd.add(path)
1966                 filesToChangeExecBit[path] = diff['dst_mode']
1967                 if path in filesToDelete:
1968                     filesToDelete.remove(path)
1969
1970                 dst_mode = int(diff['dst_mode'], 8)
1971                 if dst_mode == 0o120000:
1972                     symlinks.add(path)
1973
1974             elif modifier == "D":
1975                 filesToDelete.add(path)
1976                 if path in filesToAdd:
1977                     filesToAdd.remove(path)
1978             elif modifier == "C":
1979                 src, dest = diff['src'], diff['dst']
1980                 all_files.append(dest)
1981                 p4_integrate(src, dest)
1982                 pureRenameCopy.add(dest)
1983                 if diff['src_sha1'] != diff['dst_sha1']:
1984                     p4_edit(dest)
1985                     pureRenameCopy.discard(dest)
1986                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1987                     p4_edit(dest)
1988                     pureRenameCopy.discard(dest)
1989                     filesToChangeExecBit[dest] = diff['dst_mode']
1990                 if self.isWindows:
1991                     # turn off read-only attribute
1992                     os.chmod(dest, stat.S_IWRITE)
1993                 os.unlink(dest)
1994                 editedFiles.add(dest)
1995             elif modifier == "R":
1996                 src, dest = diff['src'], diff['dst']
1997                 all_files.append(dest)
1998                 if self.p4HasMoveCommand:
1999                     p4_edit(src)        # src must be open before move
2000                     p4_move(src, dest)  # opens for (move/delete, move/add)
2001                 else:
2002                     p4_integrate(src, dest)
2003                     if diff['src_sha1'] != diff['dst_sha1']:
2004                         p4_edit(dest)
2005                     else:
2006                         pureRenameCopy.add(dest)
2007                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2008                     if not self.p4HasMoveCommand:
2009                         p4_edit(dest)   # with move: already open, writable
2010                     filesToChangeExecBit[dest] = diff['dst_mode']
2011                 if not self.p4HasMoveCommand:
2012                     if self.isWindows:
2013                         os.chmod(dest, stat.S_IWRITE)
2014                     os.unlink(dest)
2015                     filesToDelete.add(src)
2016                 editedFiles.add(dest)
2017             elif modifier == "T":
2018                 filesToChangeType.add(path)
2019             else:
2020                 die("unknown modifier %s for %s" % (modifier, path))
2021
2022         diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
2023         patchcmd = diffcmd + " | git apply "
2024         tryPatchCmd = patchcmd + "--check -"
2025         applyPatchCmd = patchcmd + "--check --apply -"
2026         patch_succeeded = True
2027
2028         if verbose:
2029             print("TryPatch: %s" % tryPatchCmd)
2030
2031         if os.system(tryPatchCmd) != 0:
2032             fixed_rcs_keywords = False
2033             patch_succeeded = False
2034             print("Unfortunately applying the change failed!")
2035
2036             # Patch failed, maybe it's just RCS keyword woes. Look through
2037             # the patch to see if that's possible.
2038             if gitConfigBool("git-p4.attemptRCSCleanup"):
2039                 file = None
2040                 pattern = None
2041                 kwfiles = {}
2042                 for file in editedFiles | filesToDelete:
2043                     # did this file's delta contain RCS keywords?
2044                     pattern = p4_keywords_regexp_for_file(file)
2045
2046                     if pattern:
2047                         # this file is a possibility...look for RCS keywords.
2048                         regexp = re.compile(pattern, re.VERBOSE)
2049                         for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
2050                             if regexp.search(line):
2051                                 if verbose:
2052                                     print("got keyword match on %s in %s in %s" % (pattern, line, file))
2053                                 kwfiles[file] = pattern
2054                                 break
2055
2056                 for file in kwfiles:
2057                     if verbose:
2058                         print("zapping %s with %s" % (line,pattern))
2059                     # File is being deleted, so not open in p4.  Must
2060                     # disable the read-only bit on windows.
2061                     if self.isWindows and file not in editedFiles:
2062                         os.chmod(file, stat.S_IWRITE)
2063                     self.patchRCSKeywords(file, kwfiles[file])
2064                     fixed_rcs_keywords = True
2065
2066             if fixed_rcs_keywords:
2067                 print("Retrying the patch with RCS keywords cleaned up")
2068                 if os.system(tryPatchCmd) == 0:
2069                     patch_succeeded = True
2070                     print("Patch succeesed this time with RCS keywords cleaned")
2071
2072         if not patch_succeeded:
2073             for f in editedFiles:
2074                 p4_revert(f)
2075             return False
2076
2077         #
2078         # Apply the patch for real, and do add/delete/+x handling.
2079         #
2080         system(applyPatchCmd)
2081
2082         for f in filesToChangeType:
2083             p4_edit(f, "-t", "auto")
2084         for f in filesToAdd:
2085             p4_add(f)
2086         for f in filesToDelete:
2087             p4_revert(f)
2088             p4_delete(f)
2089
2090         # Set/clear executable bits
2091         for f in filesToChangeExecBit.keys():
2092             mode = filesToChangeExecBit[f]
2093             setP4ExecBit(f, mode)
2094
2095         update_shelve = 0
2096         if len(self.update_shelve) > 0:
2097             update_shelve = self.update_shelve.pop(0)
2098             p4_reopen_in_change(update_shelve, all_files)
2099
2100         #
2101         # Build p4 change description, starting with the contents
2102         # of the git commit message.
2103         #
2104         logMessage = extractLogMessageFromGitCommit(id)
2105         logMessage = logMessage.strip()
2106         (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
2107
2108         template = self.prepareSubmitTemplate(update_shelve)
2109         submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
2110
2111         if self.preserveUser:
2112            submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
2113
2114         if self.checkAuthorship and not self.p4UserIsMe(p4User):
2115             submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
2116             submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
2117             submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2118
2119         separatorLine = "######## everything below this line is just the diff #######\n"
2120         if not self.prepare_p4_only:
2121             submitTemplate += separatorLine
2122             submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
2123
2124         (handle, fileName) = tempfile.mkstemp()
2125         tmpFile = os.fdopen(handle, "w+b")
2126         if self.isWindows:
2127             submitTemplate = submitTemplate.replace("\n", "\r\n")
2128         tmpFile.write(submitTemplate)
2129         tmpFile.close()
2130
2131         submitted = False
2132
2133         try:
2134             # Allow the hook to edit the changelist text before presenting it
2135             # to the user.
2136             if not run_git_hook("p4-prepare-changelist", [fileName]):
2137                 return False
2138
2139             if self.prepare_p4_only:
2140                 #
2141                 # Leave the p4 tree prepared, and the submit template around
2142                 # and let the user decide what to do next
2143                 #
2144                 submitted = True
2145                 print("")
2146                 print("P4 workspace prepared for submission.")
2147                 print("To submit or revert, go to client workspace")
2148                 print("  " + self.clientPath)
2149                 print("")
2150                 print("To submit, use \"p4 submit\" to write a new description,")
2151                 print("or \"p4 submit -i <%s\" to use the one prepared by" \
2152                       " \"git p4\"." % fileName)
2153                 print("You can delete the file \"%s\" when finished." % fileName)
2154
2155                 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
2156                     print("To preserve change ownership by user %s, you must\n" \
2157                           "do \"p4 change -f <change>\" after submitting and\n" \
2158                           "edit the User field.")
2159                 if pureRenameCopy:
2160                     print("After submitting, renamed files must be re-synced.")
2161                     print("Invoke \"p4 sync -f\" on each of these files:")
2162                     for f in pureRenameCopy:
2163                         print("  " + f)
2164
2165                 print("")
2166                 print("To revert the changes, use \"p4 revert ...\", and delete")
2167                 print("the submit template file \"%s\"" % fileName)
2168                 if filesToAdd:
2169                     print("Since the commit adds new files, they must be deleted:")
2170                     for f in filesToAdd:
2171                         print("  " + f)
2172                 print("")
2173                 sys.stdout.flush()
2174                 return True
2175
2176             if self.edit_template(fileName):
2177                 if not self.no_verify:
2178                     if not run_git_hook("p4-changelist", [fileName]):
2179                         print("The p4-changelist hook failed.")
2180                         sys.stdout.flush()
2181                         return False
2182
2183                 # read the edited message and submit
2184                 tmpFile = open(fileName, "rb")
2185                 message = tmpFile.read()
2186                 tmpFile.close()
2187                 if self.isWindows:
2188                     message = message.replace("\r\n", "\n")
2189                 if message.find(separatorLine) != -1:
2190                     submitTemplate = message[:message.index(separatorLine)]
2191                 else:
2192                     submitTemplate = message
2193
2194                 if len(submitTemplate.strip()) == 0:
2195                     print("Changelist is empty, aborting this changelist.")
2196                     sys.stdout.flush()
2197                     return False
2198
2199                 if update_shelve:
2200                     p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2201                 elif self.shelve:
2202                     p4_write_pipe(['shelve', '-i'], submitTemplate)
2203                 else:
2204                     p4_write_pipe(['submit', '-i'], submitTemplate)
2205                     # The rename/copy happened by applying a patch that created a
2206                     # new file.  This leaves it writable, which confuses p4.
2207                     for f in pureRenameCopy:
2208                         p4_sync(f, "-f")
2209
2210                 if self.preserveUser:
2211                     if p4User:
2212                         # Get last changelist number. Cannot easily get it from
2213                         # the submit command output as the output is
2214                         # unmarshalled.
2215                         changelist = self.lastP4Changelist()
2216                         self.modifyChangelistUser(changelist, p4User)
2217
2218                 submitted = True
2219
2220                 run_git_hook("p4-post-changelist")
2221         finally:
2222             # Revert changes if we skip this patch
2223             if not submitted or self.shelve:
2224                 if self.shelve:
2225                     print ("Reverting shelved files.")
2226                 else:
2227                     print ("Submission cancelled, undoing p4 changes.")
2228                 sys.stdout.flush()
2229                 for f in editedFiles | filesToDelete:
2230                     p4_revert(f)
2231                 for f in filesToAdd:
2232                     p4_revert(f)
2233                     os.remove(f)
2234
2235             if not self.prepare_p4_only:
2236                 os.remove(fileName)
2237         return submitted
2238
2239     # Export git tags as p4 labels. Create a p4 label and then tag
2240     # with that.
2241     def exportGitTags(self, gitTags):
2242         validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2243         if len(validLabelRegexp) == 0:
2244             validLabelRegexp = defaultLabelRegexp
2245         m = re.compile(validLabelRegexp)
2246
2247         for name in gitTags:
2248
2249             if not m.match(name):
2250                 if verbose:
2251                     print("tag %s does not match regexp %s" % (name, validLabelRegexp))
2252                 continue
2253
2254             # Get the p4 commit this corresponds to
2255             logMessage = extractLogMessageFromGitCommit(name)
2256             values = extractSettingsGitLog(logMessage)
2257
2258             if 'change' not in values:
2259                 # a tag pointing to something not sent to p4; ignore
2260                 if verbose:
2261                     print("git tag %s does not give a p4 commit" % name)
2262                 continue
2263             else:
2264                 changelist = values['change']
2265
2266             # Get the tag details.
2267             inHeader = True
2268             isAnnotated = False
2269             body = []
2270             for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2271                 l = l.strip()
2272                 if inHeader:
2273                     if re.match(r'tag\s+', l):
2274                         isAnnotated = True
2275                     elif re.match(r'\s*$', l):
2276                         inHeader = False
2277                         continue
2278                 else:
2279                     body.append(l)
2280
2281             if not isAnnotated:
2282                 body = ["lightweight tag imported by git p4\n"]
2283
2284             # Create the label - use the same view as the client spec we are using
2285             clientSpec = getClientSpec()
2286
2287             labelTemplate  = "Label: %s\n" % name
2288             labelTemplate += "Description:\n"
2289             for b in body:
2290                 labelTemplate += "\t" + b + "\n"
2291             labelTemplate += "View:\n"
2292             for depot_side in clientSpec.mappings:
2293                 labelTemplate += "\t%s\n" % depot_side
2294
2295             if self.dry_run:
2296                 print("Would create p4 label %s for tag" % name)
2297             elif self.prepare_p4_only:
2298                 print("Not creating p4 label %s for tag due to option" \
2299                       " --prepare-p4-only" % name)
2300             else:
2301                 p4_write_pipe(["label", "-i"], labelTemplate)
2302
2303                 # Use the label
2304                 p4_system(["tag", "-l", name] +
2305                           ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
2306
2307                 if verbose:
2308                     print("created p4 label for tag %s" % name)
2309
2310     def run(self, args):
2311         if len(args) == 0:
2312             self.master = currentGitBranch()
2313         elif len(args) == 1:
2314             self.master = args[0]
2315             if not branchExists(self.master):
2316                 die("Branch %s does not exist" % self.master)
2317         else:
2318             return False
2319
2320         for i in self.update_shelve:
2321             if i <= 0:
2322                 sys.exit("invalid changelist %d" % i)
2323
2324         if self.master:
2325             allowSubmit = gitConfig("git-p4.allowSubmit")
2326             if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2327                 die("%s is not in git-p4.allowSubmit" % self.master)
2328
2329         [upstream, settings] = findUpstreamBranchPoint()
2330         self.depotPath = settings['depot-paths'][0]
2331         if len(self.origin) == 0:
2332             self.origin = upstream
2333
2334         if len(self.update_shelve) > 0:
2335             self.shelve = True
2336
2337         if self.preserveUser:
2338             if not self.canChangeChangelists():
2339                 die("Cannot preserve user names without p4 super-user or admin permissions")
2340
2341         # if not set from the command line, try the config file
2342         if self.conflict_behavior is None:
2343             val = gitConfig("git-p4.conflict")
2344             if val:
2345                 if val not in self.conflict_behavior_choices:
2346                     die("Invalid value '%s' for config git-p4.conflict" % val)
2347             else:
2348                 val = "ask"
2349             self.conflict_behavior = val
2350
2351         if self.verbose:
2352             print("Origin branch is " + self.origin)
2353
2354         if len(self.depotPath) == 0:
2355             print("Internal error: cannot locate perforce depot path from existing branches")
2356             sys.exit(128)
2357
2358         self.useClientSpec = False
2359         if gitConfigBool("git-p4.useclientspec"):
2360             self.useClientSpec = True
2361         if self.useClientSpec:
2362             self.clientSpecDirs = getClientSpec()
2363
2364         # Check for the existence of P4 branches
2365         branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2366
2367         if self.useClientSpec and not branchesDetected:
2368             # all files are relative to the client spec
2369             self.clientPath = getClientRoot()
2370         else:
2371             self.clientPath = p4Where(self.depotPath)
2372
2373         if self.clientPath == "":
2374             die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
2375
2376         print("Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath))
2377         self.oldWorkingDirectory = os.getcwd()
2378
2379         # ensure the clientPath exists
2380         new_client_dir = False
2381         if not os.path.exists(self.clientPath):
2382             new_client_dir = True
2383             os.makedirs(self.clientPath)
2384
2385         chdir(self.clientPath, is_client_path=True)
2386         if self.dry_run:
2387             print("Would synchronize p4 checkout in %s" % self.clientPath)
2388         else:
2389             print("Synchronizing p4 checkout...")
2390             if new_client_dir:
2391                 # old one was destroyed, and maybe nobody told p4
2392                 p4_sync("...", "-f")
2393             else:
2394                 p4_sync("...")
2395         self.check()
2396
2397         commits = []
2398         if self.master:
2399             committish = self.master
2400         else:
2401             committish = 'HEAD'
2402
2403         if self.commit != "":
2404             if self.commit.find("..") != -1:
2405                 limits_ish = self.commit.split("..")
2406                 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2407                     commits.append(line.strip())
2408                 commits.reverse()
2409             else:
2410                 commits.append(self.commit)
2411         else:
2412             for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
2413                 commits.append(line.strip())
2414             commits.reverse()
2415
2416         if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
2417             self.checkAuthorship = False
2418         else:
2419             self.checkAuthorship = True
2420
2421         if self.preserveUser:
2422             self.checkValidP4Users(commits)
2423
2424         #
2425         # Build up a set of options to be passed to diff when
2426         # submitting each commit to p4.
2427         #
2428         if self.detectRenames:
2429             # command-line -M arg
2430             self.diffOpts = "-M"
2431         else:
2432             # If not explicitly set check the config variable
2433             detectRenames = gitConfig("git-p4.detectRenames")
2434
2435             if detectRenames.lower() == "false" or detectRenames == "":
2436                 self.diffOpts = ""
2437             elif detectRenames.lower() == "true":
2438                 self.diffOpts = "-M"
2439             else:
2440                 self.diffOpts = "-M%s" % detectRenames
2441
2442         # no command-line arg for -C or --find-copies-harder, just
2443         # config variables
2444         detectCopies = gitConfig("git-p4.detectCopies")
2445         if detectCopies.lower() == "false" or detectCopies == "":
2446             pass
2447         elif detectCopies.lower() == "true":
2448             self.diffOpts += " -C"
2449         else:
2450             self.diffOpts += " -C%s" % detectCopies
2451
2452         if gitConfigBool("git-p4.detectCopiesHarder"):
2453             self.diffOpts += " --find-copies-harder"
2454
2455         num_shelves = len(self.update_shelve)
2456         if num_shelves > 0 and num_shelves != len(commits):
2457             sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2458                      (len(commits), num_shelves))
2459
2460         if not self.no_verify:
2461             try:
2462                 if not run_git_hook("p4-pre-submit"):
2463                     print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip " \
2464                         "this pre-submission check by adding\nthe command line option '--no-verify', " \
2465                         "however,\nthis will also skip the p4-changelist hook as well.")
2466                     sys.exit(1)
2467             except Exception as e:
2468                 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "\
2469                     "with the error '{0}'".format(e.message) )
2470                 sys.exit(1)
2471
2472         #
2473         # Apply the commits, one at a time.  On failure, ask if should
2474         # continue to try the rest of the patches, or quit.
2475         #
2476         if self.dry_run:
2477             print("Would apply")
2478         applied = []
2479         last = len(commits) - 1
2480         for i, commit in enumerate(commits):
2481             if self.dry_run:
2482                 print(" ", read_pipe(["git", "show", "-s",
2483                                       "--format=format:%h %s", commit]))
2484                 ok = True
2485             else:
2486                 ok = self.applyCommit(commit)
2487             if ok:
2488                 applied.append(commit)
2489             else:
2490                 if self.prepare_p4_only and i < last:
2491                     print("Processing only the first commit due to option" \
2492                           " --prepare-p4-only")
2493                     break
2494                 if i < last:
2495                     # prompt for what to do, or use the option/variable
2496                     if self.conflict_behavior == "ask":
2497                         print("What do you want to do?")
2498                         response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2499                     elif self.conflict_behavior == "skip":
2500                         response = "s"
2501                     elif self.conflict_behavior == "quit":
2502                         response = "q"
2503                     else:
2504                         die("Unknown conflict_behavior '%s'" %
2505                             self.conflict_behavior)
2506
2507                     if response == "s":
2508                         print("Skipping this commit, but applying the rest")
2509                     if response == "q":
2510                         print("Quitting")
2511                         break
2512
2513         chdir(self.oldWorkingDirectory)
2514         shelved_applied = "shelved" if self.shelve else "applied"
2515         if self.dry_run:
2516             pass
2517         elif self.prepare_p4_only:
2518             pass
2519         elif len(commits) == len(applied):
2520             print("All commits {0}!".format(shelved_applied))
2521
2522             sync = P4Sync()
2523             if self.branch:
2524                 sync.branch = self.branch
2525             if self.disable_p4sync:
2526                 sync.sync_origin_only()
2527             else:
2528                 sync.run([])
2529
2530                 if not self.disable_rebase:
2531                     rebase = P4Rebase()
2532                     rebase.rebase()
2533
2534         else:
2535             if len(applied) == 0:
2536                 print("No commits {0}.".format(shelved_applied))
2537             else:
2538                 print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
2539                 for c in commits:
2540                     if c in applied:
2541                         star = "*"
2542                     else:
2543                         star = " "
2544                     print(star, read_pipe(["git", "show", "-s",
2545                                            "--format=format:%h %s",  c]))
2546                 print("You will have to do 'git p4 sync' and rebase.")
2547
2548         if gitConfigBool("git-p4.exportLabels"):
2549             self.exportLabels = True
2550
2551         if self.exportLabels:
2552             p4Labels = getP4Labels(self.depotPath)
2553             gitTags = getGitTags()
2554
2555             missingGitTags = gitTags - p4Labels
2556             self.exportGitTags(missingGitTags)
2557
2558         # exit with error unless everything applied perfectly
2559         if len(commits) != len(applied):
2560                 sys.exit(1)
2561
2562         return True
2563
2564 class View(object):
2565     """Represent a p4 view ("p4 help views"), and map files in a
2566        repo according to the view."""
2567
2568     def __init__(self, client_name):
2569         self.mappings = []
2570         self.client_prefix = "//%s/" % client_name
2571         # cache results of "p4 where" to lookup client file locations
2572         self.client_spec_path_cache = {}
2573
2574     def append(self, view_line):
2575         """Parse a view line, splitting it into depot and client
2576            sides.  Append to self.mappings, preserving order.  This
2577            is only needed for tag creation."""
2578
2579         # Split the view line into exactly two words.  P4 enforces
2580         # structure on these lines that simplifies this quite a bit.
2581         #
2582         # Either or both words may be double-quoted.
2583         # Single quotes do not matter.
2584         # Double-quote marks cannot occur inside the words.
2585         # A + or - prefix is also inside the quotes.
2586         # There are no quotes unless they contain a space.
2587         # The line is already white-space stripped.
2588         # The two words are separated by a single space.
2589         #
2590         if view_line[0] == '"':
2591             # First word is double quoted.  Find its end.
2592             close_quote_index = view_line.find('"', 1)
2593             if close_quote_index <= 0:
2594                 die("No first-word closing quote found: %s" % view_line)
2595             depot_side = view_line[1:close_quote_index]
2596             # skip closing quote and space
2597             rhs_index = close_quote_index + 1 + 1
2598         else:
2599             space_index = view_line.find(" ")
2600             if space_index <= 0:
2601                 die("No word-splitting space found: %s" % view_line)
2602             depot_side = view_line[0:space_index]
2603             rhs_index = space_index + 1
2604
2605         # prefix + means overlay on previous mapping
2606         if depot_side.startswith("+"):
2607             depot_side = depot_side[1:]
2608
2609         # prefix - means exclude this path, leave out of mappings
2610         exclude = False
2611         if depot_side.startswith("-"):
2612             exclude = True
2613             depot_side = depot_side[1:]
2614
2615         if not exclude:
2616             self.mappings.append(depot_side)
2617
2618     def convert_client_path(self, clientFile):
2619         # chop off //client/ part to make it relative
2620         if not clientFile.startswith(self.client_prefix):
2621             die("No prefix '%s' on clientFile '%s'" %
2622                 (self.client_prefix, clientFile))
2623         return clientFile[len(self.client_prefix):]
2624
2625     def update_client_spec_path_cache(self, files):
2626         """ Caching file paths by "p4 where" batch query """
2627
2628         # List depot file paths exclude that already cached
2629         fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
2630
2631         if len(fileArgs) == 0:
2632             return  # All files in cache
2633
2634         where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2635         for res in where_result:
2636             if "code" in res and res["code"] == "error":
2637                 # assume error is "... file(s) not in client view"
2638                 continue
2639             if "clientFile" not in res:
2640                 die("No clientFile in 'p4 where' output")
2641             if "unmap" in res:
2642                 # it will list all of them, but only one not unmap-ped
2643                 continue
2644             if gitConfigBool("core.ignorecase"):
2645                 res['depotFile'] = res['depotFile'].lower()
2646             self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
2647
2648         # not found files or unmap files set to ""
2649         for depotFile in fileArgs:
2650             if gitConfigBool("core.ignorecase"):
2651                 depotFile = depotFile.lower()
2652             if depotFile not in self.client_spec_path_cache:
2653                 self.client_spec_path_cache[depotFile] = ""
2654
2655     def map_in_client(self, depot_path):
2656         """Return the relative location in the client where this
2657            depot file should live.  Returns "" if the file should
2658            not be mapped in the client."""
2659
2660         if gitConfigBool("core.ignorecase"):
2661             depot_path = depot_path.lower()
2662
2663         if depot_path in self.client_spec_path_cache:
2664             return self.client_spec_path_cache[depot_path]
2665
2666         die( "Error: %s is not found in client spec path" % depot_path )
2667         return ""
2668
2669 def cloneExcludeCallback(option, opt_str, value, parser):
2670     # prepend "/" because the first "/" was consumed as part of the option itself.
2671     # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2672     parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2673
2674 class P4Sync(Command, P4UserMap):
2675
2676     def __init__(self):
2677         Command.__init__(self)
2678         P4UserMap.__init__(self)
2679         self.options = [
2680                 optparse.make_option("--branch", dest="branch"),
2681                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2682                 optparse.make_option("--changesfile", dest="changesFile"),
2683                 optparse.make_option("--silent", dest="silent", action="store_true"),
2684                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2685                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2686                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2687                                      help="Import into refs/heads/ , not refs/remotes"),
2688                 optparse.make_option("--max-changes", dest="maxChanges",
2689                                      help="Maximum number of changes to import"),
2690                 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2691                                      help="Internal block size to use when iteratively calling p4 changes"),
2692                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2693                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2694                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2695                                      help="Only sync files that are included in the Perforce Client Spec"),
2696                 optparse.make_option("-/", dest="cloneExclude",
2697                                      action="callback", callback=cloneExcludeCallback, type="string",
2698                                      help="exclude depot path"),
2699         ]
2700         self.description = """Imports from Perforce into a git repository.\n
2701     example:
2702     //depot/my/project/ -- to import the current head
2703     //depot/my/project/@all -- to import everything
2704     //depot/my/project/@1,6 -- to import only from revision 1 to 6
2705
2706     (a ... is not needed in the path p4 specification, it's added implicitly)"""
2707
2708         self.usage += " //depot/path[@revRange]"
2709         self.silent = False
2710         self.createdBranches = set()
2711         self.committedChanges = set()
2712         self.branch = ""
2713         self.detectBranches = False
2714         self.detectLabels = False
2715         self.importLabels = False
2716         self.changesFile = ""
2717         self.syncWithOrigin = True
2718         self.importIntoRemotes = True
2719         self.maxChanges = ""
2720         self.changes_block_size = None
2721         self.keepRepoPath = False
2722         self.depotPaths = None
2723         self.p4BranchesInGit = []
2724         self.cloneExclude = []
2725         self.useClientSpec = False
2726         self.useClientSpec_from_options = False
2727         self.clientSpecDirs = None
2728         self.tempBranches = []
2729         self.tempBranchLocation = "refs/git-p4-tmp"
2730         self.largeFileSystem = None
2731         self.suppress_meta_comment = False
2732
2733         if gitConfig('git-p4.largeFileSystem'):
2734             largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2735             self.largeFileSystem = largeFileSystemConstructor(
2736                 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2737             )
2738
2739         if gitConfig("git-p4.syncFromOrigin") == "false":
2740             self.syncWithOrigin = False
2741
2742         self.depotPaths = []
2743         self.changeRange = ""
2744         self.previousDepotPaths = []
2745         self.hasOrigin = False
2746
2747         # map from branch depot path to parent branch
2748         self.knownBranches = {}
2749         self.initialParents = {}
2750
2751         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2752         self.labels = {}
2753
2754     # Force a checkpoint in fast-import and wait for it to finish
2755     def checkpoint(self):
2756         self.gitStream.write("checkpoint\n\n")
2757         self.gitStream.write("progress checkpoint\n\n")
2758         out = self.gitOutput.readline()
2759         if self.verbose:
2760             print("checkpoint finished: " + out)
2761
2762     def isPathWanted(self, path):
2763         for p in self.cloneExclude:
2764             if p.endswith("/"):
2765                 if p4PathStartsWith(path, p):
2766                     return False
2767             # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2768             elif path.lower() == p.lower():
2769                 return False
2770         for p in self.depotPaths:
2771             if p4PathStartsWith(path, p):
2772                 return True
2773         return False
2774
2775     def extractFilesFromCommit(self, commit, shelved=False, shelved_cl = 0):
2776         files = []
2777         fnum = 0
2778         while "depotFile%s" % fnum in commit:
2779             path =  commit["depotFile%s" % fnum]
2780             found = self.isPathWanted(path)
2781             if not found:
2782                 fnum = fnum + 1
2783                 continue
2784
2785             file = {}
2786             file["path"] = path
2787             file["rev"] = commit["rev%s" % fnum]
2788             file["action"] = commit["action%s" % fnum]
2789             file["type"] = commit["type%s" % fnum]
2790             if shelved:
2791                 file["shelved_cl"] = int(shelved_cl)
2792             files.append(file)
2793             fnum = fnum + 1
2794         return files
2795
2796     def extractJobsFromCommit(self, commit):
2797         jobs = []
2798         jnum = 0
2799         while "job%s" % jnum in commit:
2800             job = commit["job%s" % jnum]
2801             jobs.append(job)
2802             jnum = jnum + 1
2803         return jobs
2804
2805     def stripRepoPath(self, path, prefixes):
2806         """When streaming files, this is called to map a p4 depot path
2807            to where it should go in git.  The prefixes are either
2808            self.depotPaths, or self.branchPrefixes in the case of
2809            branch detection."""
2810
2811         if self.useClientSpec:
2812             # branch detection moves files up a level (the branch name)
2813             # from what client spec interpretation gives
2814             path = self.clientSpecDirs.map_in_client(path)
2815             if self.detectBranches:
2816                 for b in self.knownBranches:
2817                     if p4PathStartsWith(path, b + "/"):
2818                         path = path[len(b)+1:]
2819
2820         elif self.keepRepoPath:
2821             # Preserve everything in relative path name except leading
2822             # //depot/; just look at first prefix as they all should
2823             # be in the same depot.
2824             depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2825             if p4PathStartsWith(path, depot):
2826                 path = path[len(depot):]
2827
2828         else:
2829             for p in prefixes:
2830                 if p4PathStartsWith(path, p):
2831                     path = path[len(p):]
2832                     break
2833
2834         path = wildcard_decode(path)
2835         return path
2836
2837     def splitFilesIntoBranches(self, commit):
2838         """Look at each depotFile in the commit to figure out to what
2839            branch it belongs."""
2840
2841         if self.clientSpecDirs:
2842             files = self.extractFilesFromCommit(commit)
2843             self.clientSpecDirs.update_client_spec_path_cache(files)
2844
2845         branches = {}
2846         fnum = 0
2847         while "depotFile%s" % fnum in commit:
2848             path =  commit["depotFile%s" % fnum]
2849             found = self.isPathWanted(path)
2850             if not found:
2851                 fnum = fnum + 1
2852                 continue
2853
2854             file = {}
2855             file["path"] = path
2856             file["rev"] = commit["rev%s" % fnum]
2857             file["action"] = commit["action%s" % fnum]
2858             file["type"] = commit["type%s" % fnum]
2859             fnum = fnum + 1
2860
2861             # start with the full relative path where this file would
2862             # go in a p4 client
2863             if self.useClientSpec:
2864                 relPath = self.clientSpecDirs.map_in_client(path)
2865             else:
2866                 relPath = self.stripRepoPath(path, self.depotPaths)
2867
2868             for branch in self.knownBranches.keys():
2869                 # add a trailing slash so that a commit into qt/4.2foo
2870                 # doesn't end up in qt/4.2, e.g.
2871                 if p4PathStartsWith(relPath, branch + "/"):
2872                     if branch not in branches:
2873                         branches[branch] = []
2874                     branches[branch].append(file)
2875                     break
2876
2877         return branches
2878
2879     def writeToGitStream(self, gitMode, relPath, contents):
2880         self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
2881         self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2882         for d in contents:
2883             self.gitStream.write(d)
2884         self.gitStream.write('\n')
2885
2886     def encodeWithUTF8(self, path):
2887         try:
2888             path.decode('ascii')
2889         except:
2890             encoding = 'utf8'
2891             if gitConfig('git-p4.pathEncoding'):
2892                 encoding = gitConfig('git-p4.pathEncoding')
2893             path = path.decode(encoding, 'replace').encode('utf8', 'replace')
2894             if self.verbose:
2895                 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
2896         return path
2897
2898     # output one file from the P4 stream
2899     # - helper for streamP4Files
2900
2901     def streamOneP4File(self, file, contents):
2902         relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2903         relPath = self.encodeWithUTF8(relPath)
2904         if verbose:
2905             if 'fileSize' in self.stream_file:
2906                 size = int(self.stream_file['fileSize'])
2907             else:
2908                 size = 0 # deleted files don't get a fileSize apparently
2909             sys.stdout.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath, size/1024/1024))
2910             sys.stdout.flush()
2911
2912         (type_base, type_mods) = split_p4_type(file["type"])
2913
2914         git_mode = "100644"
2915         if "x" in type_mods:
2916             git_mode = "100755"
2917         if type_base == "symlink":
2918             git_mode = "120000"
2919             # p4 print on a symlink sometimes contains "target\n";
2920             # if it does, remove the newline
2921             data = ''.join(contents)
2922             if not data:
2923                 # Some version of p4 allowed creating a symlink that pointed
2924                 # to nothing.  This causes p4 errors when checking out such
2925                 # a change, and errors here too.  Work around it by ignoring
2926                 # the bad symlink; hopefully a future change fixes it.
2927                 print("\nIgnoring empty symlink in %s" % file['depotFile'])
2928                 return
2929             elif data[-1] == '\n':
2930                 contents = [data[:-1]]
2931             else:
2932                 contents = [data]
2933
2934         if type_base == "utf16":
2935             # p4 delivers different text in the python output to -G
2936             # than it does when using "print -o", or normal p4 client
2937             # operations.  utf16 is converted to ascii or utf8, perhaps.
2938             # But ascii text saved as -t utf16 is completely mangled.
2939             # Invoke print -o to get the real contents.
2940             #
2941             # On windows, the newlines will always be mangled by print, so put
2942             # them back too.  This is not needed to the cygwin windows version,
2943             # just the native "NT" type.
2944             #
2945             try:
2946                 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2947             except Exception as e:
2948                 if 'Translation of file content failed' in str(e):
2949                     type_base = 'binary'
2950                 else:
2951                     raise e
2952             else:
2953                 if p4_version_string().find('/NT') >= 0:
2954                     text = text.replace('\r\n', '\n')
2955                 contents = [ text ]
2956
2957         if type_base == "apple":
2958             # Apple filetype files will be streamed as a concatenation of
2959             # its appledouble header and the contents.  This is useless
2960             # on both macs and non-macs.  If using "print -q -o xx", it
2961             # will create "xx" with the data, and "%xx" with the header.
2962             # This is also not very useful.
2963             #
2964             # Ideally, someday, this script can learn how to generate
2965             # appledouble files directly and import those to git, but
2966             # non-mac machines can never find a use for apple filetype.
2967             print("\nIgnoring apple filetype file %s" % file['depotFile'])
2968             return
2969
2970         # Note that we do not try to de-mangle keywords on utf16 files,
2971         # even though in theory somebody may want that.
2972         pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2973         if pattern:
2974             regexp = re.compile(pattern, re.VERBOSE)
2975             text = ''.join(contents)
2976             text = regexp.sub(r'$\1$', text)
2977             contents = [ text ]
2978
2979         if self.largeFileSystem:
2980             (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
2981
2982         self.writeToGitStream(git_mode, relPath, contents)
2983
2984     def streamOneP4Deletion(self, file):
2985         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2986         relPath = self.encodeWithUTF8(relPath)
2987         if verbose:
2988             sys.stdout.write("delete %s\n" % relPath)
2989             sys.stdout.flush()
2990         self.gitStream.write("D %s\n" % relPath)
2991
2992         if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2993             self.largeFileSystem.removeLargeFile(relPath)
2994
2995     # handle another chunk of streaming data
2996     def streamP4FilesCb(self, marshalled):
2997
2998         # catch p4 errors and complain
2999         err = None
3000         if "code" in marshalled:
3001             if marshalled["code"] == "error":
3002                 if "data" in marshalled:
3003                     err = marshalled["data"].rstrip()
3004
3005         if not err and 'fileSize' in self.stream_file:
3006             required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
3007             if required_bytes > 0:
3008                 err = 'Not enough space left on %s! Free at least %i MB.' % (
3009                     os.getcwd(), required_bytes/1024/1024
3010                 )
3011
3012         if err:
3013             f = None
3014             if self.stream_have_file_info:
3015                 if "depotFile" in self.stream_file:
3016                     f = self.stream_file["depotFile"]
3017             # force a failure in fast-import, else an empty
3018             # commit will be made
3019             self.gitStream.write("\n")
3020             self.gitStream.write("die-now\n")
3021             self.gitStream.close()
3022             # ignore errors, but make sure it exits first
3023             self.importProcess.wait()
3024             if f:
3025                 die("Error from p4 print for %s: %s" % (f, err))
3026             else:
3027                 die("Error from p4 print: %s" % err)
3028
3029         if 'depotFile' in marshalled and self.stream_have_file_info:
3030             # start of a new file - output the old one first
3031             self.streamOneP4File(self.stream_file, self.stream_contents)
3032             self.stream_file = {}
3033             self.stream_contents = []
3034             self.stream_have_file_info = False
3035
3036         # pick up the new file information... for the
3037         # 'data' field we need to append to our array
3038         for k in marshalled.keys():
3039             if k == 'data':
3040                 if 'streamContentSize' not in self.stream_file:
3041                     self.stream_file['streamContentSize'] = 0
3042                 self.stream_file['streamContentSize'] += len(marshalled['data'])
3043                 self.stream_contents.append(marshalled['data'])
3044             else:
3045                 self.stream_file[k] = marshalled[k]
3046
3047         if (verbose and
3048             'streamContentSize' in self.stream_file and
3049             'fileSize' in self.stream_file and
3050             'depotFile' in self.stream_file):
3051             size = int(self.stream_file["fileSize"])
3052             if size > 0:
3053                 progress = 100*self.stream_file['streamContentSize']/size
3054                 sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
3055                 sys.stdout.flush()
3056
3057         self.stream_have_file_info = True
3058
3059     # Stream directly from "p4 files" into "git fast-import"
3060     def streamP4Files(self, files):
3061         filesForCommit = []
3062         filesToRead = []
3063         filesToDelete = []
3064
3065         for f in files:
3066             filesForCommit.append(f)
3067             if f['action'] in self.delete_actions:
3068                 filesToDelete.append(f)
3069             else:
3070                 filesToRead.append(f)
3071
3072         # deleted files...
3073         for f in filesToDelete:
3074             self.streamOneP4Deletion(f)
3075
3076         if len(filesToRead) > 0:
3077             self.stream_file = {}
3078             self.stream_contents = []
3079             self.stream_have_file_info = False
3080
3081             # curry self argument
3082             def streamP4FilesCbSelf(entry):
3083                 self.streamP4FilesCb(entry)
3084
3085             fileArgs = []
3086             for f in filesToRead:
3087                 if 'shelved_cl' in f:
3088                     # Handle shelved CLs using the "p4 print file@=N" syntax to print
3089                     # the contents
3090                     fileArg = '%s@=%d' % (f['path'], f['shelved_cl'])
3091                 else:
3092                     fileArg = '%s#%s' % (f['path'], f['rev'])
3093
3094                 fileArgs.append(fileArg)
3095
3096             p4CmdList(["-x", "-", "print"],
3097                       stdin=fileArgs,
3098                       cb=streamP4FilesCbSelf)
3099
3100             # do the last chunk
3101             if 'depotFile' in self.stream_file:
3102                 self.streamOneP4File(self.stream_file, self.stream_contents)
3103
3104     def make_email(self, userid):
3105         if userid in self.users:
3106             return self.users[userid]
3107         else:
3108             return "%s <a@b>" % userid
3109
3110     def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
3111         """ Stream a p4 tag.
3112         commit is either a git commit, or a fast-import mark, ":<p4commit>"
3113         """
3114
3115         if verbose:
3116             print("writing tag %s for commit %s" % (labelName, commit))
3117         gitStream.write("tag %s\n" % labelName)
3118         gitStream.write("from %s\n" % commit)
3119
3120         if 'Owner' in labelDetails:
3121             owner = labelDetails["Owner"]
3122         else:
3123             owner = None
3124
3125         # Try to use the owner of the p4 label, or failing that,
3126         # the current p4 user id.
3127         if owner:
3128             email = self.make_email(owner)
3129         else:
3130             email = self.make_email(self.p4UserId())
3131         tagger = "%s %s %s" % (email, epoch, self.tz)
3132
3133         gitStream.write("tagger %s\n" % tagger)
3134
3135         print("labelDetails=",labelDetails)
3136         if 'Description' in labelDetails:
3137             description = labelDetails['Description']
3138         else:
3139             description = 'Label from git p4'
3140
3141         gitStream.write("data %d\n" % len(description))
3142         gitStream.write(description)
3143         gitStream.write("\n")
3144
3145     def inClientSpec(self, path):
3146         if not self.clientSpecDirs:
3147             return True
3148         inClientSpec = self.clientSpecDirs.map_in_client(path)
3149         if not inClientSpec and self.verbose:
3150             print('Ignoring file outside of client spec: {0}'.format(path))
3151         return inClientSpec
3152
3153     def hasBranchPrefix(self, path):
3154         if not self.branchPrefixes:
3155             return True
3156         hasPrefix = [p for p in self.branchPrefixes
3157                         if p4PathStartsWith(path, p)]
3158         if not hasPrefix and self.verbose:
3159             print('Ignoring file outside of prefix: {0}'.format(path))
3160         return hasPrefix
3161
3162     def commit(self, details, files, branch, parent = "", allow_empty=False):
3163         epoch = details["time"]
3164         author = details["user"]
3165         jobs = self.extractJobsFromCommit(details)
3166
3167         if self.verbose:
3168             print('commit into {0}'.format(branch))
3169
3170         if self.clientSpecDirs:
3171             self.clientSpecDirs.update_client_spec_path_cache(files)
3172
3173         files = [f for f in files
3174             if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]
3175
3176         if gitConfigBool('git-p4.keepEmptyCommits'):
3177             allow_empty = True
3178
3179         if not files and not allow_empty:
3180             print('Ignoring revision {0} as it would produce an empty commit.'
3181                 .format(details['change']))
3182             return
3183
3184         self.gitStream.write("commit %s\n" % branch)
3185         self.gitStream.write("mark :%s\n" % details["change"])
3186         self.committedChanges.add(int(details["change"]))
3187         committer = ""
3188         if author not in self.users:
3189             self.getUserMapFromPerforceServer()
3190         committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
3191
3192         self.gitStream.write("committer %s\n" % committer)
3193
3194         self.gitStream.write("data <<EOT\n")
3195         self.gitStream.write(details["desc"])
3196         if len(jobs) > 0:
3197             self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
3198
3199         if not self.suppress_meta_comment:
3200             self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3201                                 (','.join(self.branchPrefixes), details["change"]))
3202             if len(details['options']) > 0:
3203                 self.gitStream.write(": options = %s" % details['options'])
3204             self.gitStream.write("]\n")
3205
3206         self.gitStream.write("EOT\n\n")
3207
3208         if len(parent) > 0:
3209             if self.verbose:
3210                 print("parent %s" % parent)
3211             self.gitStream.write("from %s\n" % parent)
3212
3213         self.streamP4Files(files)
3214         self.gitStream.write("\n")
3215
3216         change = int(details["change"])
3217
3218         if change in self.labels:
3219             label = self.labels[change]
3220             labelDetails = label[0]
3221             labelRevisions = label[1]
3222             if self.verbose:
3223                 print("Change %s is labelled %s" % (change, labelDetails))
3224
3225             files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
3226                                                 for p in self.branchPrefixes])
3227
3228             if len(files) == len(labelRevisions):
3229
3230                 cleanedFiles = {}
3231                 for info in files:
3232                     if info["action"] in self.delete_actions:
3233                         continue
3234                     cleanedFiles[info["depotFile"]] = info["rev"]
3235
3236                 if cleanedFiles == labelRevisions:
3237                     self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
3238
3239                 else:
3240                     if not self.silent:
3241                         print("Tag %s does not match with change %s: files do not match."
3242                                % (labelDetails["label"], change))
3243
3244             else:
3245                 if not self.silent:
3246                     print("Tag %s does not match with change %s: file count is different."
3247                            % (labelDetails["label"], change))
3248
3249     # Build a dictionary of changelists and labels, for "detect-labels" option.
3250     def getLabels(self):
3251         self.labels = {}
3252
3253         l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
3254         if len(l) > 0 and not self.silent:
3255             print("Finding files belonging to labels in %s" % self.depotPaths)
3256
3257         for output in l:
3258             label = output["label"]
3259             revisions = {}
3260             newestChange = 0
3261             if self.verbose:
3262                 print("Querying files for label %s" % label)
3263             for file in p4CmdList(["files"] +
3264                                       ["%s...@%s" % (p, label)
3265                                           for p in self.depotPaths]):
3266                 revisions[file["depotFile"]] = file["rev"]
3267                 change = int(file["change"])
3268                 if change > newestChange:
3269                     newestChange = change
3270
3271             self.labels[newestChange] = [output, revisions]
3272
3273         if self.verbose:
3274             print("Label changes: %s" % self.labels.keys())
3275
3276     # Import p4 labels as git tags. A direct mapping does not
3277     # exist, so assume that if all the files are at the same revision
3278     # then we can use that, or it's something more complicated we should
3279     # just ignore.
3280     def importP4Labels(self, stream, p4Labels):
3281         if verbose:
3282             print("import p4 labels: " + ' '.join(p4Labels))
3283
3284         ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
3285         validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
3286         if len(validLabelRegexp) == 0:
3287             validLabelRegexp = defaultLabelRegexp
3288         m = re.compile(validLabelRegexp)
3289
3290         for name in p4Labels:
3291             commitFound = False
3292
3293             if not m.match(name):
3294                 if verbose:
3295                     print("label %s does not match regexp %s" % (name,validLabelRegexp))
3296                 continue
3297
3298             if name in ignoredP4Labels:
3299                 continue
3300
3301             labelDetails = p4CmdList(['label', "-o", name])[0]
3302
3303             # get the most recent changelist for each file in this label
3304             change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3305                                 for p in self.depotPaths])
3306
3307             if 'change' in change:
3308                 # find the corresponding git commit; take the oldest commit
3309                 changelist = int(change['change'])
3310                 if changelist in self.committedChanges:
3311                     gitCommit = ":%d" % changelist       # use a fast-import mark
3312                     commitFound = True
3313                 else:
3314                     gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3315                         "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3316                     if len(gitCommit) == 0:
3317                         print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
3318                     else:
3319                         commitFound = True
3320                         gitCommit = gitCommit.strip()
3321
3322                 if commitFound:
3323                     # Convert from p4 time format
3324                     try:
3325                         tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3326                     except ValueError:
3327                         print("Could not convert label time %s" % labelDetails['Update'])
3328                         tmwhen = 1
3329
3330                     when = int(time.mktime(tmwhen))
3331                     self.streamTag(stream, name, labelDetails, gitCommit, when)
3332                     if verbose:
3333                         print("p4 label %s mapped to git commit %s" % (name, gitCommit))
3334             else:
3335                 if verbose:
3336                     print("Label %s has no changelists - possibly deleted?" % name)
3337
3338             if not commitFound:
3339                 # We can't import this label; don't try again as it will get very
3340                 # expensive repeatedly fetching all the files for labels that will
3341                 # never be imported. If the label is moved in the future, the
3342                 # ignore will need to be removed manually.
3343                 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3344
3345     def guessProjectName(self):
3346         for p in self.depotPaths:
3347             if p.endswith("/"):
3348                 p = p[:-1]
3349             p = p[p.strip().rfind("/") + 1:]
3350             if not p.endswith("/"):
3351                p += "/"
3352             return p
3353
3354     def getBranchMapping(self):
3355         lostAndFoundBranches = set()
3356
3357         user = gitConfig("git-p4.branchUser")
3358         if len(user) > 0:
3359             command = "branches -u %s" % user
3360         else:
3361             command = "branches"
3362
3363         for info in p4CmdList(command):
3364             details = p4Cmd(["branch", "-o", info["branch"]])
3365             viewIdx = 0
3366             while "View%s" % viewIdx in details:
3367                 paths = details["View%s" % viewIdx].split(" ")
3368                 viewIdx = viewIdx + 1
3369                 # require standard //depot/foo/... //depot/bar/... mapping
3370                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3371                     continue
3372                 source = paths[0]
3373                 destination = paths[1]
3374                 ## HACK
3375                 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
3376                     source = source[len(self.depotPaths[0]):-4]
3377                     destination = destination[len(self.depotPaths[0]):-4]
3378
3379                     if destination in self.knownBranches:
3380                         if not self.silent:
3381                             print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3382                             print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
3383                         continue
3384
3385                     self.knownBranches[destination] = source
3386
3387                     lostAndFoundBranches.discard(destination)
3388
3389                     if source not in self.knownBranches:
3390                         lostAndFoundBranches.add(source)
3391
3392         # Perforce does not strictly require branches to be defined, so we also
3393         # check git config for a branch list.
3394         #
3395         # Example of branch definition in git config file:
3396         # [git-p4]
3397         #   branchList=main:branchA
3398         #   branchList=main:branchB
3399         #   branchList=branchA:branchC
3400         configBranches = gitConfigList("git-p4.branchList")
3401         for branch in configBranches:
3402             if branch:
3403                 (source, destination) = branch.split(":")
3404                 self.knownBranches[destination] = source
3405
3406                 lostAndFoundBranches.discard(destination)
3407
3408                 if source not in self.knownBranches:
3409                     lostAndFoundBranches.add(source)
3410
3411
3412         for branch in lostAndFoundBranches:
3413             self.knownBranches[branch] = branch
3414
3415     def getBranchMappingFromGitBranches(self):
3416         branches = p4BranchesInGit(self.importIntoRemotes)
3417         for branch in branches.keys():
3418             if branch == "master":
3419                 branch = "main"
3420             else:
3421                 branch = branch[len(self.projectName):]
3422             self.knownBranches[branch] = branch
3423
3424     def updateOptionDict(self, d):
3425         option_keys = {}
3426         if self.keepRepoPath:
3427             option_keys['keepRepoPath'] = 1
3428
3429         d["options"] = ' '.join(sorted(option_keys.keys()))
3430
3431     def readOptions(self, d):
3432         self.keepRepoPath = ('options' in d
3433                              and ('keepRepoPath' in d['options']))
3434
3435     def gitRefForBranch(self, branch):
3436         if branch == "main":
3437             return self.refPrefix + "master"
3438
3439         if len(branch) <= 0:
3440             return branch
3441
3442         return self.refPrefix + self.projectName + branch
3443
3444     def gitCommitByP4Change(self, ref, change):
3445         if self.verbose:
3446             print("looking in ref " + ref + " for change %s using bisect..." % change)
3447
3448         earliestCommit = ""
3449         latestCommit = parseRevision(ref)
3450
3451         while True:
3452             if self.verbose:
3453                 print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
3454             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
3455             if len(next) == 0:
3456                 if self.verbose:
3457                     print("argh")
3458                 return ""
3459             log = extractLogMessageFromGitCommit(next)
3460             settings = extractSettingsGitLog(log)
3461             currentChange = int(settings['change'])
3462             if self.verbose:
3463                 print("current change %s" % currentChange)
3464
3465             if currentChange == change:
3466                 if self.verbose:
3467                     print("found %s" % next)
3468                 return next
3469
3470             if currentChange < change:
3471                 earliestCommit = "^%s" % next
3472             else:
3473                 if next == latestCommit:
3474                     die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3475                 latestCommit = "%s^@" % next
3476
3477         return ""
3478
3479     def importNewBranch(self, branch, maxChange):
3480         # make fast-import flush all changes to disk and update the refs using the checkpoint
3481         # command so that we can try to find the branch parent in the git history
3482         self.gitStream.write("checkpoint\n\n");
3483         self.gitStream.flush();
3484         branchPrefix = self.depotPaths[0] + branch + "/"
3485         range = "@1,%s" % maxChange
3486         #print "prefix" + branchPrefix
3487         changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3488         if len(changes) <= 0:
3489             return False
3490         firstChange = changes[0]
3491         #print "first change in branch: %s" % firstChange
3492         sourceBranch = self.knownBranches[branch]
3493         sourceDepotPath = self.depotPaths[0] + sourceBranch
3494         sourceRef = self.gitRefForBranch(sourceBranch)
3495         #print "source " + sourceBranch
3496
3497         branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3498         #print "branch parent: %s" % branchParentChange
3499         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3500         if len(gitParent) > 0:
3501             self.initialParents[self.gitRefForBranch(branch)] = gitParent
3502             #print "parent git commit: %s" % gitParent
3503
3504         self.importChanges(changes)
3505         return True
3506
3507     def searchParent(self, parent, branch, target):
3508         parentFound = False
3509         for blob in read_pipe_lines(["git", "rev-list", "--reverse",
3510                                      "--no-merges", parent]):
3511             blob = blob.strip()
3512             if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
3513                 parentFound = True
3514                 if self.verbose:
3515                     print("Found parent of %s in commit %s" % (branch, blob))
3516                 break
3517         if parentFound:
3518             return blob
3519         else:
3520             return None
3521
3522     def importChanges(self, changes, origin_revision=0):
3523         cnt = 1
3524         for change in changes:
3525             description = p4_describe(change)
3526             self.updateOptionDict(description)
3527
3528             if not self.silent:
3529                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
3530                 sys.stdout.flush()
3531             cnt = cnt + 1
3532
3533             try:
3534                 if self.detectBranches:
3535                     branches = self.splitFilesIntoBranches(description)
3536                     for branch in branches.keys():
3537                         ## HACK  --hwn
3538                         branchPrefix = self.depotPaths[0] + branch + "/"
3539                         self.branchPrefixes = [ branchPrefix ]
3540
3541                         parent = ""
3542
3543                         filesForCommit = branches[branch]
3544
3545                         if self.verbose:
3546                             print("branch is %s" % branch)
3547
3548                         self.updatedBranches.add(branch)
3549
3550                         if branch not in self.createdBranches:
3551                             self.createdBranches.add(branch)
3552                             parent = self.knownBranches[branch]
3553                             if parent == branch:
3554                                 parent = ""
3555                             else:
3556                                 fullBranch = self.projectName + branch
3557                                 if fullBranch not in self.p4BranchesInGit:
3558                                     if not self.silent:
3559                                         print("\n    Importing new branch %s" % fullBranch);
3560                                     if self.importNewBranch(branch, change - 1):
3561                                         parent = ""
3562                                         self.p4BranchesInGit.append(fullBranch)
3563                                     if not self.silent:
3564                                         print("\n    Resuming with change %s" % change);
3565
3566                                 if self.verbose:
3567                                     print("parent determined through known branches: %s" % parent)
3568
3569                         branch = self.gitRefForBranch(branch)
3570                         parent = self.gitRefForBranch(parent)
3571
3572                         if self.verbose:
3573                             print("looking for initial parent for %s; current parent is %s" % (branch, parent))
3574
3575                         if len(parent) == 0 and branch in self.initialParents:
3576                             parent = self.initialParents[branch]
3577                             del self.initialParents[branch]
3578
3579                         blob = None
3580                         if len(parent) > 0:
3581                             tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3582                             if self.verbose:
3583                                 print("Creating temporary branch: " + tempBranch)
3584                             self.commit(description, filesForCommit, tempBranch)
3585                             self.tempBranches.append(tempBranch)
3586                             self.checkpoint()
3587                             blob = self.searchParent(parent, branch, tempBranch)
3588                         if blob:
3589                             self.commit(description, filesForCommit, branch, blob)
3590                         else:
3591                             if self.verbose:
3592                                 print("Parent of %s not found. Committing into head of %s" % (branch, parent))
3593                             self.commit(description, filesForCommit, branch, parent)
3594                 else:
3595                     files = self.extractFilesFromCommit(description)
3596                     self.commit(description, files, self.branch,
3597                                 self.initialParent)
3598                     # only needed once, to connect to the previous commit
3599                     self.initialParent = ""
3600             except IOError:
3601                 print(self.gitError.read())
3602                 sys.exit(1)
3603
3604     def sync_origin_only(self):
3605         if self.syncWithOrigin:
3606             self.hasOrigin = originP4BranchesExist()
3607             if self.hasOrigin:
3608                 if not self.silent:
3609                     print('Syncing with origin first, using "git fetch origin"')
3610                 system("git fetch origin")
3611
3612     def importHeadRevision(self, revision):
3613         print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
3614
3615         details = {}
3616         details["user"] = "git perforce import user"
3617         details["desc"] = ("Initial import of %s from the state at revision %s\n"
3618                            % (' '.join(self.depotPaths), revision))
3619         details["change"] = revision
3620         newestRevision = 0
3621
3622         fileCnt = 0
3623         fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3624
3625         for info in p4CmdList(["files"] + fileArgs):
3626
3627             if 'code' in info and info['code'] == 'error':
3628                 sys.stderr.write("p4 returned an error: %s\n"
3629                                  % info['data'])
3630                 if info['data'].find("must refer to client") >= 0:
3631                     sys.stderr.write("This particular p4 error is misleading.\n")
3632                     sys.stderr.write("Perhaps the depot path was misspelled.\n");
3633                     sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
3634                 sys.exit(1)
3635             if 'p4ExitCode' in info:
3636                 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3637                 sys.exit(1)
3638
3639
3640             change = int(info["change"])
3641             if change > newestRevision:
3642                 newestRevision = change
3643
3644             if info["action"] in self.delete_actions:
3645                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3646                 #fileCnt = fileCnt + 1
3647                 continue
3648
3649             for prop in ["depotFile", "rev", "action", "type" ]:
3650                 details["%s%s" % (prop, fileCnt)] = info[prop]
3651
3652             fileCnt = fileCnt + 1
3653
3654         details["change"] = newestRevision
3655
3656         # Use time from top-most change so that all git p4 clones of
3657         # the same p4 repo have the same commit SHA1s.
3658         res = p4_describe(newestRevision)
3659         details["time"] = res["time"]
3660
3661         self.updateOptionDict(details)
3662         try:
3663             self.commit(details, self.extractFilesFromCommit(details), self.branch)
3664         except IOError as err:
3665             print("IO error with git fast-import. Is your git version recent enough?")
3666             print("IO error details: {}".format(err))
3667             print(self.gitError.read())
3668
3669     def openStreams(self):
3670         self.importProcess = subprocess.Popen(["git", "fast-import"],
3671                                               stdin=subprocess.PIPE,
3672                                               stdout=subprocess.PIPE,
3673                                               stderr=subprocess.PIPE);
3674         self.gitOutput = self.importProcess.stdout
3675         self.gitStream = self.importProcess.stdin
3676         self.gitError = self.importProcess.stderr
3677
3678     def closeStreams(self):
3679         self.gitStream.close()
3680         if self.importProcess.wait() != 0:
3681             die("fast-import failed: %s" % self.gitError.read())
3682         self.gitOutput.close()
3683         self.gitError.close()
3684
3685     def run(self, args):
3686         if self.importIntoRemotes:
3687             self.refPrefix = "refs/remotes/p4/"
3688         else:
3689             self.refPrefix = "refs/heads/p4/"
3690
3691         self.sync_origin_only()
3692
3693         branch_arg_given = bool(self.branch)
3694         if len(self.branch) == 0:
3695             self.branch = self.refPrefix + "master"
3696             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
3697                 system("git update-ref %s refs/heads/p4" % self.branch)
3698                 system("git branch -D p4")
3699
3700         # accept either the command-line option, or the configuration variable
3701         if self.useClientSpec:
3702             # will use this after clone to set the variable
3703             self.useClientSpec_from_options = True
3704         else:
3705             if gitConfigBool("git-p4.useclientspec"):
3706                 self.useClientSpec = True
3707         if self.useClientSpec:
3708             self.clientSpecDirs = getClientSpec()
3709
3710         # TODO: should always look at previous commits,
3711         # merge with previous imports, if possible.
3712         if args == []:
3713             if self.hasOrigin:
3714                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
3715
3716             # branches holds mapping from branch name to sha1
3717             branches = p4BranchesInGit(self.importIntoRemotes)
3718
3719             # restrict to just this one, disabling detect-branches
3720             if branch_arg_given:
3721                 short = self.branch.split("/")[-1]
3722                 if short in branches:
3723                     self.p4BranchesInGit = [ short ]
3724             else:
3725                 self.p4BranchesInGit = branches.keys()
3726
3727             if len(self.p4BranchesInGit) > 1:
3728                 if not self.silent:
3729                     print("Importing from/into multiple branches")
3730                 self.detectBranches = True
3731                 for branch in branches.keys():
3732                     self.initialParents[self.refPrefix + branch] = \
3733                         branches[branch]
3734
3735             if self.verbose:
3736                 print("branches: %s" % self.p4BranchesInGit)
3737
3738             p4Change = 0
3739             for branch in self.p4BranchesInGit:
3740                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
3741
3742                 settings = extractSettingsGitLog(logMsg)
3743
3744                 self.readOptions(settings)
3745                 if ('depot-paths' in settings
3746                     and 'change' in settings):
3747                     change = int(settings['change']) + 1
3748                     p4Change = max(p4Change, change)
3749
3750                     depotPaths = sorted(settings['depot-paths'])
3751                     if self.previousDepotPaths == []:
3752                         self.previousDepotPaths = depotPaths
3753                     else:
3754                         paths = []
3755                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
3756                             prev_list = prev.split("/")
3757                             cur_list = cur.split("/")
3758                             for i in range(0, min(len(cur_list), len(prev_list))):
3759                                 if cur_list[i] != prev_list[i]:
3760                                     i = i - 1
3761                                     break
3762
3763                             paths.append ("/".join(cur_list[:i + 1]))
3764
3765                         self.previousDepotPaths = paths
3766
3767             if p4Change > 0:
3768                 self.depotPaths = sorted(self.previousDepotPaths)
3769                 self.changeRange = "@%s,#head" % p4Change
3770                 if not self.silent and not self.detectBranches:
3771                     print("Performing incremental import into %s git branch" % self.branch)
3772
3773         # accept multiple ref name abbreviations:
3774         #    refs/foo/bar/branch -> use it exactly
3775         #    p4/branch -> prepend refs/remotes/ or refs/heads/
3776         #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
3777         if not self.branch.startswith("refs/"):
3778             if self.importIntoRemotes:
3779                 prepend = "refs/remotes/"
3780             else:
3781                 prepend = "refs/heads/"
3782             if not self.branch.startswith("p4/"):
3783                 prepend += "p4/"
3784             self.branch = prepend + self.branch
3785
3786         if len(args) == 0 and self.depotPaths:
3787             if not self.silent:
3788                 print("Depot paths: %s" % ' '.join(self.depotPaths))
3789         else:
3790             if self.depotPaths and self.depotPaths != args:
3791                 print("previous import used depot path %s and now %s was specified. "
3792                        "This doesn't work!" % (' '.join (self.depotPaths),
3793                                                ' '.join (args)))
3794                 sys.exit(1)
3795
3796             self.depotPaths = sorted(args)
3797
3798         revision = ""
3799         self.users = {}
3800
3801         # Make sure no revision specifiers are used when --changesfile
3802         # is specified.
3803         bad_changesfile = False
3804         if len(self.changesFile) > 0:
3805             for p in self.depotPaths:
3806                 if p.find("@") >= 0 or p.find("#") >= 0:
3807                     bad_changesfile = True
3808                     break
3809         if bad_changesfile:
3810             die("Option --changesfile is incompatible with revision specifiers")
3811
3812         newPaths = []
3813         for p in self.depotPaths:
3814             if p.find("@") != -1:
3815                 atIdx = p.index("@")
3816                 self.changeRange = p[atIdx:]
3817                 if self.changeRange == "@all":
3818                     self.changeRange = ""
3819                 elif ',' not in self.changeRange:
3820                     revision = self.changeRange
3821                     self.changeRange = ""
3822                 p = p[:atIdx]
3823             elif p.find("#") != -1:
3824                 hashIdx = p.index("#")
3825                 revision = p[hashIdx:]
3826                 p = p[:hashIdx]
3827             elif self.previousDepotPaths == []:
3828                 # pay attention to changesfile, if given, else import
3829                 # the entire p4 tree at the head revision
3830                 if len(self.changesFile) == 0:
3831                     revision = "#head"
3832
3833             p = re.sub ("\.\.\.$", "", p)
3834             if not p.endswith("/"):
3835                 p += "/"
3836
3837             newPaths.append(p)
3838
3839         self.depotPaths = newPaths
3840
3841         # --detect-branches may change this for each branch
3842         self.branchPrefixes = self.depotPaths
3843
3844         self.loadUserMapFromCache()
3845         self.labels = {}
3846         if self.detectLabels:
3847             self.getLabels();
3848
3849         if self.detectBranches:
3850             ## FIXME - what's a P4 projectName ?
3851             self.projectName = self.guessProjectName()
3852
3853             if self.hasOrigin:
3854                 self.getBranchMappingFromGitBranches()
3855             else:
3856                 self.getBranchMapping()
3857             if self.verbose:
3858                 print("p4-git branches: %s" % self.p4BranchesInGit)
3859                 print("initial parents: %s" % self.initialParents)
3860             for b in self.p4BranchesInGit:
3861                 if b != "master":
3862
3863                     ## FIXME
3864                     b = b[len(self.projectName):]
3865                 self.createdBranches.add(b)
3866
3867         self.openStreams()
3868
3869         if revision:
3870             self.importHeadRevision(revision)
3871         else:
3872             changes = []
3873
3874             if len(self.changesFile) > 0:
3875                 output = open(self.changesFile).readlines()
3876                 changeSet = set()
3877                 for line in output:
3878                     changeSet.add(int(line))
3879
3880                 for change in changeSet:
3881                     changes.append(change)
3882
3883                 changes.sort()
3884             else:
3885                 # catch "git p4 sync" with no new branches, in a repo that
3886                 # does not have any existing p4 branches
3887                 if len(args) == 0:
3888                     if not self.p4BranchesInGit:
3889                         die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
3890
3891                     # The default branch is master, unless --branch is used to
3892                     # specify something else.  Make sure it exists, or complain
3893                     # nicely about how to use --branch.
3894                     if not self.detectBranches:
3895                         if not branch_exists(self.branch):
3896                             if branch_arg_given:
3897                                 die("Error: branch %s does not exist." % self.branch)
3898                             else:
3899                                 die("Error: no branch %s; perhaps specify one with --branch." %
3900                                     self.branch)
3901
3902                 if self.verbose:
3903                     print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3904                                                               self.changeRange))
3905                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3906
3907                 if len(self.maxChanges) > 0:
3908                     changes = changes[:min(int(self.maxChanges), len(changes))]
3909
3910             if len(changes) == 0:
3911                 if not self.silent:
3912                     print("No changes to import!")
3913             else:
3914                 if not self.silent and not self.detectBranches:
3915                     print("Import destination: %s" % self.branch)
3916
3917                 self.updatedBranches = set()
3918
3919                 if not self.detectBranches:
3920                     if args:
3921                         # start a new branch
3922                         self.initialParent = ""
3923                     else:
3924                         # build on a previous revision
3925                         self.initialParent = parseRevision(self.branch)
3926
3927                 self.importChanges(changes)
3928
3929                 if not self.silent:
3930                     print("")
3931                     if len(self.updatedBranches) > 0:
3932                         sys.stdout.write("Updated branches: ")
3933                         for b in self.updatedBranches:
3934                             sys.stdout.write("%s " % b)
3935                         sys.stdout.write("\n")
3936
3937         if gitConfigBool("git-p4.importLabels"):
3938             self.importLabels = True
3939
3940         if self.importLabels:
3941             p4Labels = getP4Labels(self.depotPaths)
3942             gitTags = getGitTags()
3943
3944             missingP4Labels = p4Labels - gitTags
3945             self.importP4Labels(self.gitStream, missingP4Labels)
3946
3947         self.closeStreams()
3948
3949         # Cleanup temporary branches created during import
3950         if self.tempBranches != []:
3951             for branch in self.tempBranches:
3952                 read_pipe("git update-ref -d %s" % branch)
3953             os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3954
3955         # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3956         # a convenient shortcut refname "p4".
3957         if self.importIntoRemotes:
3958             head_ref = self.refPrefix + "HEAD"
3959             if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3960                 system(["git", "symbolic-ref", head_ref, self.branch])
3961
3962         return True
3963
3964 class P4Rebase(Command):
3965     def __init__(self):
3966         Command.__init__(self)
3967         self.options = [
3968                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3969         ]
3970         self.importLabels = False
3971         self.description = ("Fetches the latest revision from perforce and "
3972                             + "rebases the current work (branch) against it")
3973
3974     def run(self, args):
3975         sync = P4Sync()
3976         sync.importLabels = self.importLabels
3977         sync.run([])
3978
3979         return self.rebase()
3980
3981     def rebase(self):
3982         if os.system("git update-index --refresh") != 0:
3983             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.");
3984         if len(read_pipe("git diff-index HEAD --")) > 0:
3985             die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3986
3987         [upstream, settings] = findUpstreamBranchPoint()
3988         if len(upstream) == 0:
3989             die("Cannot find upstream branchpoint for rebase")
3990
3991         # the branchpoint may be p4/foo~3, so strip off the parent
3992         upstream = re.sub("~[0-9]+$", "", upstream)
3993
3994         print("Rebasing the current branch onto %s" % upstream)
3995         oldHead = read_pipe("git rev-parse HEAD").strip()
3996         system("git rebase %s" % upstream)
3997         system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3998         return True
3999
4000 class P4Clone(P4Sync):
4001     def __init__(self):
4002         P4Sync.__init__(self)
4003         self.description = "Creates a new git repository and imports from Perforce into it"
4004         self.usage = "usage: %prog [options] //depot/path[@revRange]"
4005         self.options += [
4006             optparse.make_option("--destination", dest="cloneDestination",
4007                                  action='store', default=None,
4008                                  help="where to leave result of the clone"),
4009             optparse.make_option("--bare", dest="cloneBare",
4010                                  action="store_true", default=False),
4011         ]
4012         self.cloneDestination = None
4013         self.needsGit = False
4014         self.cloneBare = False
4015
4016     def defaultDestination(self, args):
4017         ## TODO: use common prefix of args?
4018         depotPath = args[0]
4019         depotDir = re.sub("(@[^@]*)$", "", depotPath)
4020         depotDir = re.sub("(#[^#]*)$", "", depotDir)
4021         depotDir = re.sub(r"\.\.\.$", "", depotDir)
4022         depotDir = re.sub(r"/$", "", depotDir)
4023         return os.path.split(depotDir)[1]
4024
4025     def run(self, args):
4026         if len(args) < 1:
4027             return False
4028
4029         if self.keepRepoPath and not self.cloneDestination:
4030             sys.stderr.write("Must specify destination for --keep-path\n")
4031             sys.exit(1)
4032
4033         depotPaths = args
4034
4035         if not self.cloneDestination and len(depotPaths) > 1:
4036             self.cloneDestination = depotPaths[-1]
4037             depotPaths = depotPaths[:-1]
4038
4039         for p in depotPaths:
4040             if not p.startswith("//"):
4041                 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
4042                 return False
4043
4044         if not self.cloneDestination:
4045             self.cloneDestination = self.defaultDestination(args)
4046
4047         print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
4048
4049         if not os.path.exists(self.cloneDestination):
4050             os.makedirs(self.cloneDestination)
4051         chdir(self.cloneDestination)
4052
4053         init_cmd = [ "git", "init" ]
4054         if self.cloneBare:
4055             init_cmd.append("--bare")
4056         retcode = subprocess.call(init_cmd)
4057         if retcode:
4058             raise CalledProcessError(retcode, init_cmd)
4059
4060         if not P4Sync.run(self, depotPaths):
4061             return False
4062
4063         # create a master branch and check out a work tree
4064         if gitBranchExists(self.branch):
4065             system([ "git", "branch", "master", self.branch ])
4066             if not self.cloneBare:
4067                 system([ "git", "checkout", "-f" ])
4068         else:
4069             print('Not checking out any branch, use ' \
4070                   '"git checkout -q -b master <branch>"')
4071
4072         # auto-set this variable if invoked with --use-client-spec
4073         if self.useClientSpec_from_options:
4074             system("git config --bool git-p4.useclientspec true")
4075
4076         return True
4077
4078 class P4Unshelve(Command):
4079     def __init__(self):
4080         Command.__init__(self)
4081         self.options = []
4082         self.origin = "HEAD"
4083         self.description = "Unshelve a P4 changelist into a git commit"
4084         self.usage = "usage: %prog [options] changelist"
4085         self.options += [
4086                 optparse.make_option("--origin", dest="origin",
4087                     help="Use this base revision instead of the default (%s)" % self.origin),
4088         ]
4089         self.verbose = False
4090         self.noCommit = False
4091         self.destbranch = "refs/remotes/p4-unshelved"
4092
4093     def renameBranch(self, branch_name):
4094         """ Rename the existing branch to branch_name.N
4095         """
4096
4097         found = True
4098         for i in range(0,1000):
4099             backup_branch_name = "{0}.{1}".format(branch_name, i)
4100             if not gitBranchExists(backup_branch_name):
4101                 gitUpdateRef(backup_branch_name, branch_name) # copy ref to backup
4102                 gitDeleteRef(branch_name)
4103                 found = True
4104                 print("renamed old unshelve branch to {0}".format(backup_branch_name))
4105                 break
4106
4107         if not found:
4108             sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
4109
4110     def findLastP4Revision(self, starting_point):
4111         """ Look back from starting_point for the first commit created by git-p4
4112             to find the P4 commit we are based on, and the depot-paths.
4113         """
4114
4115         for parent in (range(65535)):
4116             log = extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))
4117             settings = extractSettingsGitLog(log)
4118             if 'change' in settings:
4119                 return settings
4120
4121         sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4122
4123     def createShelveParent(self, change, branch_name, sync, origin):
4124         """ Create a commit matching the parent of the shelved changelist 'change'
4125         """
4126         parent_description = p4_describe(change, shelved=True)
4127         parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4128         files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4129
4130         parent_files = []
4131         for f in files:
4132             # if it was added in the shelved changelist, it won't exist in the parent
4133             if f['action'] in self.add_actions:
4134                 continue
4135
4136             # if it was deleted in the shelved changelist it must not be deleted
4137             # in the parent - we might even need to create it if the origin branch
4138             # does not have it
4139             if f['action'] in self.delete_actions:
4140                 f['action'] = 'add'
4141
4142             parent_files.append(f)
4143
4144         sync.commit(parent_description, parent_files, branch_name,
4145                 parent=origin, allow_empty=True)
4146         print("created parent commit for {0} based on {1} in {2}".format(
4147             change, self.origin, branch_name))
4148
4149     def run(self, args):
4150         if len(args) != 1:
4151             return False
4152
4153         if not gitBranchExists(self.origin):
4154             sys.exit("origin branch {0} does not exist".format(self.origin))
4155
4156         sync = P4Sync()
4157         changes = args
4158
4159         # only one change at a time
4160         change = changes[0]
4161
4162         # if the target branch already exists, rename it
4163         branch_name = "{0}/{1}".format(self.destbranch, change)
4164         if gitBranchExists(branch_name):
4165             self.renameBranch(branch_name)
4166         sync.branch = branch_name
4167
4168         sync.verbose = self.verbose
4169         sync.suppress_meta_comment = True
4170
4171         settings = self.findLastP4Revision(self.origin)
4172         sync.depotPaths = settings['depot-paths']
4173         sync.branchPrefixes = sync.depotPaths
4174
4175         sync.openStreams()
4176         sync.loadUserMapFromCache()
4177         sync.silent = True
4178
4179         # create a commit for the parent of the shelved changelist
4180         self.createShelveParent(change, branch_name, sync, self.origin)
4181
4182         # create the commit for the shelved changelist itself
4183         description = p4_describe(change, True)
4184         files = sync.extractFilesFromCommit(description, True, change)
4185
4186         sync.commit(description, files, branch_name, "")
4187         sync.closeStreams()
4188
4189         print("unshelved changelist {0} into {1}".format(change, branch_name))
4190
4191         return True
4192
4193 class P4Branches(Command):
4194     def __init__(self):
4195         Command.__init__(self)
4196         self.options = [ ]
4197         self.description = ("Shows the git branches that hold imports and their "
4198                             + "corresponding perforce depot paths")
4199         self.verbose = False
4200
4201     def run(self, args):
4202         if originP4BranchesExist():
4203             createOrUpdateBranchesFromOrigin()
4204
4205         cmdline = "git rev-parse --symbolic "
4206         cmdline += " --remotes"
4207
4208         for line in read_pipe_lines(cmdline):
4209             line = line.strip()
4210
4211             if not line.startswith('p4/') or line == "p4/HEAD":
4212                 continue
4213             branch = line
4214
4215             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4216             settings = extractSettingsGitLog(log)
4217
4218             print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
4219         return True
4220
4221 class HelpFormatter(optparse.IndentedHelpFormatter):
4222     def __init__(self):
4223         optparse.IndentedHelpFormatter.__init__(self)
4224
4225     def format_description(self, description):
4226         if description:
4227             return description + "\n"
4228         else:
4229             return ""
4230
4231 def printUsage(commands):
4232     print("usage: %s <command> [options]" % sys.argv[0])
4233     print("")
4234     print("valid commands: %s" % ", ".join(commands))
4235     print("")
4236     print("Try %s <command> --help for command specific help." % sys.argv[0])
4237     print("")
4238
4239 commands = {
4240     "debug" : P4Debug,
4241     "submit" : P4Submit,
4242     "commit" : P4Submit,
4243     "sync" : P4Sync,
4244     "rebase" : P4Rebase,
4245     "clone" : P4Clone,
4246     "rollback" : P4RollBack,
4247     "branches" : P4Branches,
4248     "unshelve" : P4Unshelve,
4249 }
4250
4251 def main():
4252     if len(sys.argv[1:]) == 0:
4253         printUsage(commands.keys())
4254         sys.exit(2)
4255
4256     cmdName = sys.argv[1]
4257     try:
4258         klass = commands[cmdName]
4259         cmd = klass()
4260     except KeyError:
4261         print("unknown command %s" % cmdName)
4262         print("")
4263         printUsage(commands.keys())
4264         sys.exit(2)
4265
4266     options = cmd.options
4267     cmd.gitdir = os.environ.get("GIT_DIR", None)
4268
4269     args = sys.argv[2:]
4270
4271     options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
4272     if cmd.needsGit:
4273         options.append(optparse.make_option("--git-dir", dest="gitdir"))
4274
4275     parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4276                                    options,
4277                                    description = cmd.description,
4278                                    formatter = HelpFormatter())
4279
4280     try:
4281         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
4282     except:
4283         parser.print_help()
4284         raise
4285
4286     global verbose
4287     verbose = cmd.verbose
4288     if cmd.needsGit:
4289         if cmd.gitdir == None:
4290             cmd.gitdir = os.path.abspath(".git")
4291             if not isValidGitDir(cmd.gitdir):
4292                 # "rev-parse --git-dir" without arguments will try $PWD/.git
4293                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
4294                 if os.path.exists(cmd.gitdir):
4295                     cdup = read_pipe("git rev-parse --show-cdup").strip()
4296                     if len(cdup) > 0:
4297                         chdir(cdup);
4298
4299         if not isValidGitDir(cmd.gitdir):
4300             if isValidGitDir(cmd.gitdir + "/.git"):
4301                 cmd.gitdir += "/.git"
4302             else:
4303                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
4304
4305         # so git commands invoked from the P4 workspace will succeed
4306         os.environ["GIT_DIR"] = cmd.gitdir
4307
4308     if not cmd.run(args):
4309         parser.print_help()
4310         sys.exit(2)
4311
4312
4313 if __name__ == '__main__':
4314     main()