git-p4: create new function run_git_hook
[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         ]
1592         self.description = """Submit changes from git to the perforce depot.\n
1593     The `p4-pre-submit` hook is executed if it exists and is executable.
1594     The hook takes no parameters and nothing from standard input. Exiting with
1595     non-zero status from this script prevents `git-p4 submit` from launching.
1596
1597     One usage scenario is to run unit tests in the hook."""
1598
1599         self.usage += " [name of git branch to submit into perforce depot]"
1600         self.origin = ""
1601         self.detectRenames = False
1602         self.preserveUser = gitConfigBool("git-p4.preserveUser")
1603         self.dry_run = False
1604         self.shelve = False
1605         self.update_shelve = list()
1606         self.commit = ""
1607         self.disable_rebase = gitConfigBool("git-p4.disableRebase")
1608         self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
1609         self.prepare_p4_only = False
1610         self.conflict_behavior = None
1611         self.isWindows = (platform.system() == "Windows")
1612         self.exportLabels = False
1613         self.p4HasMoveCommand = p4_has_move_command()
1614         self.branch = None
1615
1616         if gitConfig('git-p4.largeFileSystem'):
1617             die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1618
1619     def check(self):
1620         if len(p4CmdList("opened ...")) > 0:
1621             die("You have files opened with perforce! Close them before starting the sync.")
1622
1623     def separate_jobs_from_description(self, message):
1624         """Extract and return a possible Jobs field in the commit
1625            message.  It goes into a separate section in the p4 change
1626            specification.
1627
1628            A jobs line starts with "Jobs:" and looks like a new field
1629            in a form.  Values are white-space separated on the same
1630            line or on following lines that start with a tab.
1631
1632            This does not parse and extract the full git commit message
1633            like a p4 form.  It just sees the Jobs: line as a marker
1634            to pass everything from then on directly into the p4 form,
1635            but outside the description section.
1636
1637            Return a tuple (stripped log message, jobs string)."""
1638
1639         m = re.search(r'^Jobs:', message, re.MULTILINE)
1640         if m is None:
1641             return (message, None)
1642
1643         jobtext = message[m.start():]
1644         stripped_message = message[:m.start()].rstrip()
1645         return (stripped_message, jobtext)
1646
1647     def prepareLogMessage(self, template, message, jobs):
1648         """Edits the template returned from "p4 change -o" to insert
1649            the message in the Description field, and the jobs text in
1650            the Jobs field."""
1651         result = ""
1652
1653         inDescriptionSection = False
1654
1655         for line in template.split("\n"):
1656             if line.startswith("#"):
1657                 result += line + "\n"
1658                 continue
1659
1660             if inDescriptionSection:
1661                 if line.startswith("Files:") or line.startswith("Jobs:"):
1662                     inDescriptionSection = False
1663                     # insert Jobs section
1664                     if jobs:
1665                         result += jobs + "\n"
1666                 else:
1667                     continue
1668             else:
1669                 if line.startswith("Description:"):
1670                     inDescriptionSection = True
1671                     line += "\n"
1672                     for messageLine in message.split("\n"):
1673                         line += "\t" + messageLine + "\n"
1674
1675             result += line + "\n"
1676
1677         return result
1678
1679     def patchRCSKeywords(self, file, pattern):
1680         # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1681         (handle, outFileName) = tempfile.mkstemp(dir='.')
1682         try:
1683             outFile = os.fdopen(handle, "w+")
1684             inFile = open(file, "r")
1685             regexp = re.compile(pattern, re.VERBOSE)
1686             for line in inFile.readlines():
1687                 line = regexp.sub(r'$\1$', line)
1688                 outFile.write(line)
1689             inFile.close()
1690             outFile.close()
1691             # Forcibly overwrite the original file
1692             os.unlink(file)
1693             shutil.move(outFileName, file)
1694         except:
1695             # cleanup our temporary file
1696             os.unlink(outFileName)
1697             print("Failed to strip RCS keywords in %s" % file)
1698             raise
1699
1700         print("Patched up RCS keywords in %s" % file)
1701
1702     def p4UserForCommit(self,id):
1703         # Return the tuple (perforce user,git email) for a given git commit id
1704         self.getUserMapFromPerforceServer()
1705         gitEmail = read_pipe(["git", "log", "--max-count=1",
1706                               "--format=%ae", id])
1707         gitEmail = gitEmail.strip()
1708         if gitEmail not in self.emails:
1709             return (None,gitEmail)
1710         else:
1711             return (self.emails[gitEmail],gitEmail)
1712
1713     def checkValidP4Users(self,commits):
1714         # check if any git authors cannot be mapped to p4 users
1715         for id in commits:
1716             (user,email) = self.p4UserForCommit(id)
1717             if not user:
1718                 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1719                 if gitConfigBool("git-p4.allowMissingP4Users"):
1720                     print("%s" % msg)
1721                 else:
1722                     die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1723
1724     def lastP4Changelist(self):
1725         # Get back the last changelist number submitted in this client spec. This
1726         # then gets used to patch up the username in the change. If the same
1727         # client spec is being used by multiple processes then this might go
1728         # wrong.
1729         results = p4CmdList("client -o")        # find the current client
1730         client = None
1731         for r in results:
1732             if 'Client' in r:
1733                 client = r['Client']
1734                 break
1735         if not client:
1736             die("could not get client spec")
1737         results = p4CmdList(["changes", "-c", client, "-m", "1"])
1738         for r in results:
1739             if 'change' in r:
1740                 return r['change']
1741         die("Could not get changelist number for last submit - cannot patch up user details")
1742
1743     def modifyChangelistUser(self, changelist, newUser):
1744         # fixup the user field of a changelist after it has been submitted.
1745         changes = p4CmdList("change -o %s" % changelist)
1746         if len(changes) != 1:
1747             die("Bad output from p4 change modifying %s to user %s" %
1748                 (changelist, newUser))
1749
1750         c = changes[0]
1751         if c['User'] == newUser: return   # nothing to do
1752         c['User'] = newUser
1753         input = marshal.dumps(c)
1754
1755         result = p4CmdList("change -f -i", stdin=input)
1756         for r in result:
1757             if 'code' in r:
1758                 if r['code'] == 'error':
1759                     die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1760             if 'data' in r:
1761                 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1762                 return
1763         die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1764
1765     def canChangeChangelists(self):
1766         # check to see if we have p4 admin or super-user permissions, either of
1767         # which are required to modify changelists.
1768         results = p4CmdList(["protects", self.depotPath])
1769         for r in results:
1770             if 'perm' in r:
1771                 if r['perm'] == 'admin':
1772                     return 1
1773                 if r['perm'] == 'super':
1774                     return 1
1775         return 0
1776
1777     def prepareSubmitTemplate(self, changelist=None):
1778         """Run "p4 change -o" to grab a change specification template.
1779            This does not use "p4 -G", as it is nice to keep the submission
1780            template in original order, since a human might edit it.
1781
1782            Remove lines in the Files section that show changes to files
1783            outside the depot path we're committing into."""
1784
1785         [upstream, settings] = findUpstreamBranchPoint()
1786
1787         template = """\
1788 # A Perforce Change Specification.
1789 #
1790 #  Change:      The change number. 'new' on a new changelist.
1791 #  Date:        The date this specification was last modified.
1792 #  Client:      The client on which the changelist was created.  Read-only.
1793 #  User:        The user who created the changelist.
1794 #  Status:      Either 'pending' or 'submitted'. Read-only.
1795 #  Type:        Either 'public' or 'restricted'. Default is 'public'.
1796 #  Description: Comments about the changelist.  Required.
1797 #  Jobs:        What opened jobs are to be closed by this changelist.
1798 #               You may delete jobs from this list.  (New changelists only.)
1799 #  Files:       What opened files from the default changelist are to be added
1800 #               to this changelist.  You may delete files from this list.
1801 #               (New changelists only.)
1802 """
1803         files_list = []
1804         inFilesSection = False
1805         change_entry = None
1806         args = ['change', '-o']
1807         if changelist:
1808             args.append(str(changelist))
1809         for entry in p4CmdList(args):
1810             if 'code' not in entry:
1811                 continue
1812             if entry['code'] == 'stat':
1813                 change_entry = entry
1814                 break
1815         if not change_entry:
1816             die('Failed to decode output of p4 change -o')
1817         for key, value in change_entry.iteritems():
1818             if key.startswith('File'):
1819                 if 'depot-paths' in settings:
1820                     if not [p for p in settings['depot-paths']
1821                             if p4PathStartsWith(value, p)]:
1822                         continue
1823                 else:
1824                     if not p4PathStartsWith(value, self.depotPath):
1825                         continue
1826                 files_list.append(value)
1827                 continue
1828         # Output in the order expected by prepareLogMessage
1829         for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
1830             if key not in change_entry:
1831                 continue
1832             template += '\n'
1833             template += key + ':'
1834             if key == 'Description':
1835                 template += '\n'
1836             for field_line in change_entry[key].splitlines():
1837                 template += '\t'+field_line+'\n'
1838         if len(files_list) > 0:
1839             template += '\n'
1840             template += 'Files:\n'
1841         for path in files_list:
1842             template += '\t'+path+'\n'
1843         return template
1844
1845     def edit_template(self, template_file):
1846         """Invoke the editor to let the user change the submission
1847            message.  Return true if okay to continue with the submit."""
1848
1849         # if configured to skip the editing part, just submit
1850         if gitConfigBool("git-p4.skipSubmitEdit"):
1851             return True
1852
1853         # look at the modification time, to check later if the user saved
1854         # the file
1855         mtime = os.stat(template_file).st_mtime
1856
1857         # invoke the editor
1858         if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
1859             editor = os.environ.get("P4EDITOR")
1860         else:
1861             editor = read_pipe("git var GIT_EDITOR").strip()
1862         system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1863
1864         # If the file was not saved, prompt to see if this patch should
1865         # be skipped.  But skip this verification step if configured so.
1866         if gitConfigBool("git-p4.skipSubmitEditCheck"):
1867             return True
1868
1869         # modification time updated means user saved the file
1870         if os.stat(template_file).st_mtime > mtime:
1871             return True
1872
1873         response = prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1874         if response == 'y':
1875             return True
1876         if response == 'n':
1877             return False
1878
1879     def get_diff_description(self, editedFiles, filesToAdd, symlinks):
1880         # diff
1881         if "P4DIFF" in os.environ:
1882             del(os.environ["P4DIFF"])
1883         diff = ""
1884         for editedFile in editedFiles:
1885             diff += p4_read_pipe(['diff', '-du',
1886                                   wildcard_encode(editedFile)])
1887
1888         # new file diff
1889         newdiff = ""
1890         for newFile in filesToAdd:
1891             newdiff += "==== new file ====\n"
1892             newdiff += "--- /dev/null\n"
1893             newdiff += "+++ %s\n" % newFile
1894
1895             is_link = os.path.islink(newFile)
1896             expect_link = newFile in symlinks
1897
1898             if is_link and expect_link:
1899                 newdiff += "+%s\n" % os.readlink(newFile)
1900             else:
1901                 f = open(newFile, "r")
1902                 for line in f.readlines():
1903                     newdiff += "+" + line
1904                 f.close()
1905
1906         return (diff + newdiff).replace('\r\n', '\n')
1907
1908     def applyCommit(self, id):
1909         """Apply one commit, return True if it succeeded."""
1910
1911         print("Applying", read_pipe(["git", "show", "-s",
1912                                      "--format=format:%h %s", id]))
1913
1914         (p4User, gitEmail) = self.p4UserForCommit(id)
1915
1916         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1917         filesToAdd = set()
1918         filesToChangeType = set()
1919         filesToDelete = set()
1920         editedFiles = set()
1921         pureRenameCopy = set()
1922         symlinks = set()
1923         filesToChangeExecBit = {}
1924         all_files = list()
1925
1926         for line in diff:
1927             diff = parseDiffTreeEntry(line)
1928             modifier = diff['status']
1929             path = diff['src']
1930             all_files.append(path)
1931
1932             if modifier == "M":
1933                 p4_edit(path)
1934                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1935                     filesToChangeExecBit[path] = diff['dst_mode']
1936                 editedFiles.add(path)
1937             elif modifier == "A":
1938                 filesToAdd.add(path)
1939                 filesToChangeExecBit[path] = diff['dst_mode']
1940                 if path in filesToDelete:
1941                     filesToDelete.remove(path)
1942
1943                 dst_mode = int(diff['dst_mode'], 8)
1944                 if dst_mode == 0o120000:
1945                     symlinks.add(path)
1946
1947             elif modifier == "D":
1948                 filesToDelete.add(path)
1949                 if path in filesToAdd:
1950                     filesToAdd.remove(path)
1951             elif modifier == "C":
1952                 src, dest = diff['src'], diff['dst']
1953                 all_files.append(dest)
1954                 p4_integrate(src, dest)
1955                 pureRenameCopy.add(dest)
1956                 if diff['src_sha1'] != diff['dst_sha1']:
1957                     p4_edit(dest)
1958                     pureRenameCopy.discard(dest)
1959                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1960                     p4_edit(dest)
1961                     pureRenameCopy.discard(dest)
1962                     filesToChangeExecBit[dest] = diff['dst_mode']
1963                 if self.isWindows:
1964                     # turn off read-only attribute
1965                     os.chmod(dest, stat.S_IWRITE)
1966                 os.unlink(dest)
1967                 editedFiles.add(dest)
1968             elif modifier == "R":
1969                 src, dest = diff['src'], diff['dst']
1970                 all_files.append(dest)
1971                 if self.p4HasMoveCommand:
1972                     p4_edit(src)        # src must be open before move
1973                     p4_move(src, dest)  # opens for (move/delete, move/add)
1974                 else:
1975                     p4_integrate(src, dest)
1976                     if diff['src_sha1'] != diff['dst_sha1']:
1977                         p4_edit(dest)
1978                     else:
1979                         pureRenameCopy.add(dest)
1980                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1981                     if not self.p4HasMoveCommand:
1982                         p4_edit(dest)   # with move: already open, writable
1983                     filesToChangeExecBit[dest] = diff['dst_mode']
1984                 if not self.p4HasMoveCommand:
1985                     if self.isWindows:
1986                         os.chmod(dest, stat.S_IWRITE)
1987                     os.unlink(dest)
1988                     filesToDelete.add(src)
1989                 editedFiles.add(dest)
1990             elif modifier == "T":
1991                 filesToChangeType.add(path)
1992             else:
1993                 die("unknown modifier %s for %s" % (modifier, path))
1994
1995         diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1996         patchcmd = diffcmd + " | git apply "
1997         tryPatchCmd = patchcmd + "--check -"
1998         applyPatchCmd = patchcmd + "--check --apply -"
1999         patch_succeeded = True
2000
2001         if os.system(tryPatchCmd) != 0:
2002             fixed_rcs_keywords = False
2003             patch_succeeded = False
2004             print("Unfortunately applying the change failed!")
2005
2006             # Patch failed, maybe it's just RCS keyword woes. Look through
2007             # the patch to see if that's possible.
2008             if gitConfigBool("git-p4.attemptRCSCleanup"):
2009                 file = None
2010                 pattern = None
2011                 kwfiles = {}
2012                 for file in editedFiles | filesToDelete:
2013                     # did this file's delta contain RCS keywords?
2014                     pattern = p4_keywords_regexp_for_file(file)
2015
2016                     if pattern:
2017                         # this file is a possibility...look for RCS keywords.
2018                         regexp = re.compile(pattern, re.VERBOSE)
2019                         for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
2020                             if regexp.search(line):
2021                                 if verbose:
2022                                     print("got keyword match on %s in %s in %s" % (pattern, line, file))
2023                                 kwfiles[file] = pattern
2024                                 break
2025
2026                 for file in kwfiles:
2027                     if verbose:
2028                         print("zapping %s with %s" % (line,pattern))
2029                     # File is being deleted, so not open in p4.  Must
2030                     # disable the read-only bit on windows.
2031                     if self.isWindows and file not in editedFiles:
2032                         os.chmod(file, stat.S_IWRITE)
2033                     self.patchRCSKeywords(file, kwfiles[file])
2034                     fixed_rcs_keywords = True
2035
2036             if fixed_rcs_keywords:
2037                 print("Retrying the patch with RCS keywords cleaned up")
2038                 if os.system(tryPatchCmd) == 0:
2039                     patch_succeeded = True
2040
2041         if not patch_succeeded:
2042             for f in editedFiles:
2043                 p4_revert(f)
2044             return False
2045
2046         #
2047         # Apply the patch for real, and do add/delete/+x handling.
2048         #
2049         system(applyPatchCmd)
2050
2051         for f in filesToChangeType:
2052             p4_edit(f, "-t", "auto")
2053         for f in filesToAdd:
2054             p4_add(f)
2055         for f in filesToDelete:
2056             p4_revert(f)
2057             p4_delete(f)
2058
2059         # Set/clear executable bits
2060         for f in filesToChangeExecBit.keys():
2061             mode = filesToChangeExecBit[f]
2062             setP4ExecBit(f, mode)
2063
2064         update_shelve = 0
2065         if len(self.update_shelve) > 0:
2066             update_shelve = self.update_shelve.pop(0)
2067             p4_reopen_in_change(update_shelve, all_files)
2068
2069         #
2070         # Build p4 change description, starting with the contents
2071         # of the git commit message.
2072         #
2073         logMessage = extractLogMessageFromGitCommit(id)
2074         logMessage = logMessage.strip()
2075         (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
2076
2077         template = self.prepareSubmitTemplate(update_shelve)
2078         submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
2079
2080         if self.preserveUser:
2081            submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
2082
2083         if self.checkAuthorship and not self.p4UserIsMe(p4User):
2084             submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
2085             submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
2086             submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2087
2088         separatorLine = "######## everything below this line is just the diff #######\n"
2089         if not self.prepare_p4_only:
2090             submitTemplate += separatorLine
2091             submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
2092
2093         (handle, fileName) = tempfile.mkstemp()
2094         tmpFile = os.fdopen(handle, "w+b")
2095         if self.isWindows:
2096             submitTemplate = submitTemplate.replace("\n", "\r\n")
2097         tmpFile.write(submitTemplate)
2098         tmpFile.close()
2099
2100         if self.prepare_p4_only:
2101             #
2102             # Leave the p4 tree prepared, and the submit template around
2103             # and let the user decide what to do next
2104             #
2105             print()
2106             print("P4 workspace prepared for submission.")
2107             print("To submit or revert, go to client workspace")
2108             print("  " + self.clientPath)
2109             print()
2110             print("To submit, use \"p4 submit\" to write a new description,")
2111             print("or \"p4 submit -i <%s\" to use the one prepared by" \
2112                   " \"git p4\"." % fileName)
2113             print("You can delete the file \"%s\" when finished." % fileName)
2114
2115             if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
2116                 print("To preserve change ownership by user %s, you must\n" \
2117                       "do \"p4 change -f <change>\" after submitting and\n" \
2118                       "edit the User field.")
2119             if pureRenameCopy:
2120                 print("After submitting, renamed files must be re-synced.")
2121                 print("Invoke \"p4 sync -f\" on each of these files:")
2122                 for f in pureRenameCopy:
2123                     print("  " + f)
2124
2125             print()
2126             print("To revert the changes, use \"p4 revert ...\", and delete")
2127             print("the submit template file \"%s\"" % fileName)
2128             if filesToAdd:
2129                 print("Since the commit adds new files, they must be deleted:")
2130                 for f in filesToAdd:
2131                     print("  " + f)
2132             print()
2133             return True
2134
2135         #
2136         # Let the user edit the change description, then submit it.
2137         #
2138         submitted = False
2139
2140         try:
2141             if self.edit_template(fileName):
2142                 # read the edited message and submit
2143                 tmpFile = open(fileName, "rb")
2144                 message = tmpFile.read()
2145                 tmpFile.close()
2146                 if self.isWindows:
2147                     message = message.replace("\r\n", "\n")
2148                 submitTemplate = message[:message.index(separatorLine)]
2149
2150                 if update_shelve:
2151                     p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2152                 elif self.shelve:
2153                     p4_write_pipe(['shelve', '-i'], submitTemplate)
2154                 else:
2155                     p4_write_pipe(['submit', '-i'], submitTemplate)
2156                     # The rename/copy happened by applying a patch that created a
2157                     # new file.  This leaves it writable, which confuses p4.
2158                     for f in pureRenameCopy:
2159                         p4_sync(f, "-f")
2160
2161                 if self.preserveUser:
2162                     if p4User:
2163                         # Get last changelist number. Cannot easily get it from
2164                         # the submit command output as the output is
2165                         # unmarshalled.
2166                         changelist = self.lastP4Changelist()
2167                         self.modifyChangelistUser(changelist, p4User)
2168
2169                 submitted = True
2170
2171         finally:
2172             # skip this patch
2173             if not submitted or self.shelve:
2174                 if self.shelve:
2175                     print ("Reverting shelved files.")
2176                 else:
2177                     print ("Submission cancelled, undoing p4 changes.")
2178                 for f in editedFiles | filesToDelete:
2179                     p4_revert(f)
2180                 for f in filesToAdd:
2181                     p4_revert(f)
2182                     os.remove(f)
2183
2184         os.remove(fileName)
2185         return submitted
2186
2187     # Export git tags as p4 labels. Create a p4 label and then tag
2188     # with that.
2189     def exportGitTags(self, gitTags):
2190         validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2191         if len(validLabelRegexp) == 0:
2192             validLabelRegexp = defaultLabelRegexp
2193         m = re.compile(validLabelRegexp)
2194
2195         for name in gitTags:
2196
2197             if not m.match(name):
2198                 if verbose:
2199                     print("tag %s does not match regexp %s" % (name, validLabelRegexp))
2200                 continue
2201
2202             # Get the p4 commit this corresponds to
2203             logMessage = extractLogMessageFromGitCommit(name)
2204             values = extractSettingsGitLog(logMessage)
2205
2206             if 'change' not in values:
2207                 # a tag pointing to something not sent to p4; ignore
2208                 if verbose:
2209                     print("git tag %s does not give a p4 commit" % name)
2210                 continue
2211             else:
2212                 changelist = values['change']
2213
2214             # Get the tag details.
2215             inHeader = True
2216             isAnnotated = False
2217             body = []
2218             for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2219                 l = l.strip()
2220                 if inHeader:
2221                     if re.match(r'tag\s+', l):
2222                         isAnnotated = True
2223                     elif re.match(r'\s*$', l):
2224                         inHeader = False
2225                         continue
2226                 else:
2227                     body.append(l)
2228
2229             if not isAnnotated:
2230                 body = ["lightweight tag imported by git p4\n"]
2231
2232             # Create the label - use the same view as the client spec we are using
2233             clientSpec = getClientSpec()
2234
2235             labelTemplate  = "Label: %s\n" % name
2236             labelTemplate += "Description:\n"
2237             for b in body:
2238                 labelTemplate += "\t" + b + "\n"
2239             labelTemplate += "View:\n"
2240             for depot_side in clientSpec.mappings:
2241                 labelTemplate += "\t%s\n" % depot_side
2242
2243             if self.dry_run:
2244                 print("Would create p4 label %s for tag" % name)
2245             elif self.prepare_p4_only:
2246                 print("Not creating p4 label %s for tag due to option" \
2247                       " --prepare-p4-only" % name)
2248             else:
2249                 p4_write_pipe(["label", "-i"], labelTemplate)
2250
2251                 # Use the label
2252                 p4_system(["tag", "-l", name] +
2253                           ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
2254
2255                 if verbose:
2256                     print("created p4 label for tag %s" % name)
2257
2258     def run(self, args):
2259         if len(args) == 0:
2260             self.master = currentGitBranch()
2261         elif len(args) == 1:
2262             self.master = args[0]
2263             if not branchExists(self.master):
2264                 die("Branch %s does not exist" % self.master)
2265         else:
2266             return False
2267
2268         for i in self.update_shelve:
2269             if i <= 0:
2270                 sys.exit("invalid changelist %d" % i)
2271
2272         if self.master:
2273             allowSubmit = gitConfig("git-p4.allowSubmit")
2274             if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2275                 die("%s is not in git-p4.allowSubmit" % self.master)
2276
2277         [upstream, settings] = findUpstreamBranchPoint()
2278         self.depotPath = settings['depot-paths'][0]
2279         if len(self.origin) == 0:
2280             self.origin = upstream
2281
2282         if len(self.update_shelve) > 0:
2283             self.shelve = True
2284
2285         if self.preserveUser:
2286             if not self.canChangeChangelists():
2287                 die("Cannot preserve user names without p4 super-user or admin permissions")
2288
2289         # if not set from the command line, try the config file
2290         if self.conflict_behavior is None:
2291             val = gitConfig("git-p4.conflict")
2292             if val:
2293                 if val not in self.conflict_behavior_choices:
2294                     die("Invalid value '%s' for config git-p4.conflict" % val)
2295             else:
2296                 val = "ask"
2297             self.conflict_behavior = val
2298
2299         if self.verbose:
2300             print("Origin branch is " + self.origin)
2301
2302         if len(self.depotPath) == 0:
2303             print("Internal error: cannot locate perforce depot path from existing branches")
2304             sys.exit(128)
2305
2306         self.useClientSpec = False
2307         if gitConfigBool("git-p4.useclientspec"):
2308             self.useClientSpec = True
2309         if self.useClientSpec:
2310             self.clientSpecDirs = getClientSpec()
2311
2312         # Check for the existence of P4 branches
2313         branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2314
2315         if self.useClientSpec and not branchesDetected:
2316             # all files are relative to the client spec
2317             self.clientPath = getClientRoot()
2318         else:
2319             self.clientPath = p4Where(self.depotPath)
2320
2321         if self.clientPath == "":
2322             die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
2323
2324         print("Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath))
2325         self.oldWorkingDirectory = os.getcwd()
2326
2327         # ensure the clientPath exists
2328         new_client_dir = False
2329         if not os.path.exists(self.clientPath):
2330             new_client_dir = True
2331             os.makedirs(self.clientPath)
2332
2333         chdir(self.clientPath, is_client_path=True)
2334         if self.dry_run:
2335             print("Would synchronize p4 checkout in %s" % self.clientPath)
2336         else:
2337             print("Synchronizing p4 checkout...")
2338             if new_client_dir:
2339                 # old one was destroyed, and maybe nobody told p4
2340                 p4_sync("...", "-f")
2341             else:
2342                 p4_sync("...")
2343         self.check()
2344
2345         commits = []
2346         if self.master:
2347             committish = self.master
2348         else:
2349             committish = 'HEAD'
2350
2351         if self.commit != "":
2352             if self.commit.find("..") != -1:
2353                 limits_ish = self.commit.split("..")
2354                 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2355                     commits.append(line.strip())
2356                 commits.reverse()
2357             else:
2358                 commits.append(self.commit)
2359         else:
2360             for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
2361                 commits.append(line.strip())
2362             commits.reverse()
2363
2364         if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
2365             self.checkAuthorship = False
2366         else:
2367             self.checkAuthorship = True
2368
2369         if self.preserveUser:
2370             self.checkValidP4Users(commits)
2371
2372         #
2373         # Build up a set of options to be passed to diff when
2374         # submitting each commit to p4.
2375         #
2376         if self.detectRenames:
2377             # command-line -M arg
2378             self.diffOpts = "-M"
2379         else:
2380             # If not explicitly set check the config variable
2381             detectRenames = gitConfig("git-p4.detectRenames")
2382
2383             if detectRenames.lower() == "false" or detectRenames == "":
2384                 self.diffOpts = ""
2385             elif detectRenames.lower() == "true":
2386                 self.diffOpts = "-M"
2387             else:
2388                 self.diffOpts = "-M%s" % detectRenames
2389
2390         # no command-line arg for -C or --find-copies-harder, just
2391         # config variables
2392         detectCopies = gitConfig("git-p4.detectCopies")
2393         if detectCopies.lower() == "false" or detectCopies == "":
2394             pass
2395         elif detectCopies.lower() == "true":
2396             self.diffOpts += " -C"
2397         else:
2398             self.diffOpts += " -C%s" % detectCopies
2399
2400         if gitConfigBool("git-p4.detectCopiesHarder"):
2401             self.diffOpts += " --find-copies-harder"
2402
2403         num_shelves = len(self.update_shelve)
2404         if num_shelves > 0 and num_shelves != len(commits):
2405             sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2406                      (len(commits), num_shelves))
2407
2408         if not run_git_hook("p4-pre-submit"):
2409             sys.exit(1)
2410
2411         #
2412         # Apply the commits, one at a time.  On failure, ask if should
2413         # continue to try the rest of the patches, or quit.
2414         #
2415         if self.dry_run:
2416             print("Would apply")
2417         applied = []
2418         last = len(commits) - 1
2419         for i, commit in enumerate(commits):
2420             if self.dry_run:
2421                 print(" ", read_pipe(["git", "show", "-s",
2422                                       "--format=format:%h %s", commit]))
2423                 ok = True
2424             else:
2425                 ok = self.applyCommit(commit)
2426             if ok:
2427                 applied.append(commit)
2428             else:
2429                 if self.prepare_p4_only and i < last:
2430                     print("Processing only the first commit due to option" \
2431                           " --prepare-p4-only")
2432                     break
2433                 if i < last:
2434                     # prompt for what to do, or use the option/variable
2435                     if self.conflict_behavior == "ask":
2436                         print("What do you want to do?")
2437                         response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2438                     elif self.conflict_behavior == "skip":
2439                         response = "s"
2440                     elif self.conflict_behavior == "quit":
2441                         response = "q"
2442                     else:
2443                         die("Unknown conflict_behavior '%s'" %
2444                             self.conflict_behavior)
2445
2446                     if response == "s":
2447                         print("Skipping this commit, but applying the rest")
2448                     if response == "q":
2449                         print("Quitting")
2450                         break
2451
2452         chdir(self.oldWorkingDirectory)
2453         shelved_applied = "shelved" if self.shelve else "applied"
2454         if self.dry_run:
2455             pass
2456         elif self.prepare_p4_only:
2457             pass
2458         elif len(commits) == len(applied):
2459             print("All commits {0}!".format(shelved_applied))
2460
2461             sync = P4Sync()
2462             if self.branch:
2463                 sync.branch = self.branch
2464             if self.disable_p4sync:
2465                 sync.sync_origin_only()
2466             else:
2467                 sync.run([])
2468
2469                 if not self.disable_rebase:
2470                     rebase = P4Rebase()
2471                     rebase.rebase()
2472
2473         else:
2474             if len(applied) == 0:
2475                 print("No commits {0}.".format(shelved_applied))
2476             else:
2477                 print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
2478                 for c in commits:
2479                     if c in applied:
2480                         star = "*"
2481                     else:
2482                         star = " "
2483                     print(star, read_pipe(["git", "show", "-s",
2484                                            "--format=format:%h %s",  c]))
2485                 print("You will have to do 'git p4 sync' and rebase.")
2486
2487         if gitConfigBool("git-p4.exportLabels"):
2488             self.exportLabels = True
2489
2490         if self.exportLabels:
2491             p4Labels = getP4Labels(self.depotPath)
2492             gitTags = getGitTags()
2493
2494             missingGitTags = gitTags - p4Labels
2495             self.exportGitTags(missingGitTags)
2496
2497         # exit with error unless everything applied perfectly
2498         if len(commits) != len(applied):
2499                 sys.exit(1)
2500
2501         return True
2502
2503 class View(object):
2504     """Represent a p4 view ("p4 help views"), and map files in a
2505        repo according to the view."""
2506
2507     def __init__(self, client_name):
2508         self.mappings = []
2509         self.client_prefix = "//%s/" % client_name
2510         # cache results of "p4 where" to lookup client file locations
2511         self.client_spec_path_cache = {}
2512
2513     def append(self, view_line):
2514         """Parse a view line, splitting it into depot and client
2515            sides.  Append to self.mappings, preserving order.  This
2516            is only needed for tag creation."""
2517
2518         # Split the view line into exactly two words.  P4 enforces
2519         # structure on these lines that simplifies this quite a bit.
2520         #
2521         # Either or both words may be double-quoted.
2522         # Single quotes do not matter.
2523         # Double-quote marks cannot occur inside the words.
2524         # A + or - prefix is also inside the quotes.
2525         # There are no quotes unless they contain a space.
2526         # The line is already white-space stripped.
2527         # The two words are separated by a single space.
2528         #
2529         if view_line[0] == '"':
2530             # First word is double quoted.  Find its end.
2531             close_quote_index = view_line.find('"', 1)
2532             if close_quote_index <= 0:
2533                 die("No first-word closing quote found: %s" % view_line)
2534             depot_side = view_line[1:close_quote_index]
2535             # skip closing quote and space
2536             rhs_index = close_quote_index + 1 + 1
2537         else:
2538             space_index = view_line.find(" ")
2539             if space_index <= 0:
2540                 die("No word-splitting space found: %s" % view_line)
2541             depot_side = view_line[0:space_index]
2542             rhs_index = space_index + 1
2543
2544         # prefix + means overlay on previous mapping
2545         if depot_side.startswith("+"):
2546             depot_side = depot_side[1:]
2547
2548         # prefix - means exclude this path, leave out of mappings
2549         exclude = False
2550         if depot_side.startswith("-"):
2551             exclude = True
2552             depot_side = depot_side[1:]
2553
2554         if not exclude:
2555             self.mappings.append(depot_side)
2556
2557     def convert_client_path(self, clientFile):
2558         # chop off //client/ part to make it relative
2559         if not clientFile.startswith(self.client_prefix):
2560             die("No prefix '%s' on clientFile '%s'" %
2561                 (self.client_prefix, clientFile))
2562         return clientFile[len(self.client_prefix):]
2563
2564     def update_client_spec_path_cache(self, files):
2565         """ Caching file paths by "p4 where" batch query """
2566
2567         # List depot file paths exclude that already cached
2568         fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
2569
2570         if len(fileArgs) == 0:
2571             return  # All files in cache
2572
2573         where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2574         for res in where_result:
2575             if "code" in res and res["code"] == "error":
2576                 # assume error is "... file(s) not in client view"
2577                 continue
2578             if "clientFile" not in res:
2579                 die("No clientFile in 'p4 where' output")
2580             if "unmap" in res:
2581                 # it will list all of them, but only one not unmap-ped
2582                 continue
2583             if gitConfigBool("core.ignorecase"):
2584                 res['depotFile'] = res['depotFile'].lower()
2585             self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
2586
2587         # not found files or unmap files set to ""
2588         for depotFile in fileArgs:
2589             if gitConfigBool("core.ignorecase"):
2590                 depotFile = depotFile.lower()
2591             if depotFile not in self.client_spec_path_cache:
2592                 self.client_spec_path_cache[depotFile] = ""
2593
2594     def map_in_client(self, depot_path):
2595         """Return the relative location in the client where this
2596            depot file should live.  Returns "" if the file should
2597            not be mapped in the client."""
2598
2599         if gitConfigBool("core.ignorecase"):
2600             depot_path = depot_path.lower()
2601
2602         if depot_path in self.client_spec_path_cache:
2603             return self.client_spec_path_cache[depot_path]
2604
2605         die( "Error: %s is not found in client spec path" % depot_path )
2606         return ""
2607
2608 def cloneExcludeCallback(option, opt_str, value, parser):
2609     # prepend "/" because the first "/" was consumed as part of the option itself.
2610     # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2611     parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2612
2613 class P4Sync(Command, P4UserMap):
2614
2615     def __init__(self):
2616         Command.__init__(self)
2617         P4UserMap.__init__(self)
2618         self.options = [
2619                 optparse.make_option("--branch", dest="branch"),
2620                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2621                 optparse.make_option("--changesfile", dest="changesFile"),
2622                 optparse.make_option("--silent", dest="silent", action="store_true"),
2623                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2624                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2625                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2626                                      help="Import into refs/heads/ , not refs/remotes"),
2627                 optparse.make_option("--max-changes", dest="maxChanges",
2628                                      help="Maximum number of changes to import"),
2629                 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2630                                      help="Internal block size to use when iteratively calling p4 changes"),
2631                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2632                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2633                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2634                                      help="Only sync files that are included in the Perforce Client Spec"),
2635                 optparse.make_option("-/", dest="cloneExclude",
2636                                      action="callback", callback=cloneExcludeCallback, type="string",
2637                                      help="exclude depot path"),
2638         ]
2639         self.description = """Imports from Perforce into a git repository.\n
2640     example:
2641     //depot/my/project/ -- to import the current head
2642     //depot/my/project/@all -- to import everything
2643     //depot/my/project/@1,6 -- to import only from revision 1 to 6
2644
2645     (a ... is not needed in the path p4 specification, it's added implicitly)"""
2646
2647         self.usage += " //depot/path[@revRange]"
2648         self.silent = False
2649         self.createdBranches = set()
2650         self.committedChanges = set()
2651         self.branch = ""
2652         self.detectBranches = False
2653         self.detectLabels = False
2654         self.importLabels = False
2655         self.changesFile = ""
2656         self.syncWithOrigin = True
2657         self.importIntoRemotes = True
2658         self.maxChanges = ""
2659         self.changes_block_size = None
2660         self.keepRepoPath = False
2661         self.depotPaths = None
2662         self.p4BranchesInGit = []
2663         self.cloneExclude = []
2664         self.useClientSpec = False
2665         self.useClientSpec_from_options = False
2666         self.clientSpecDirs = None
2667         self.tempBranches = []
2668         self.tempBranchLocation = "refs/git-p4-tmp"
2669         self.largeFileSystem = None
2670         self.suppress_meta_comment = False
2671
2672         if gitConfig('git-p4.largeFileSystem'):
2673             largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2674             self.largeFileSystem = largeFileSystemConstructor(
2675                 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2676             )
2677
2678         if gitConfig("git-p4.syncFromOrigin") == "false":
2679             self.syncWithOrigin = False
2680
2681         self.depotPaths = []
2682         self.changeRange = ""
2683         self.previousDepotPaths = []
2684         self.hasOrigin = False
2685
2686         # map from branch depot path to parent branch
2687         self.knownBranches = {}
2688         self.initialParents = {}
2689
2690         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2691         self.labels = {}
2692
2693     # Force a checkpoint in fast-import and wait for it to finish
2694     def checkpoint(self):
2695         self.gitStream.write("checkpoint\n\n")
2696         self.gitStream.write("progress checkpoint\n\n")
2697         out = self.gitOutput.readline()
2698         if self.verbose:
2699             print("checkpoint finished: " + out)
2700
2701     def isPathWanted(self, path):
2702         for p in self.cloneExclude:
2703             if p.endswith("/"):
2704                 if p4PathStartsWith(path, p):
2705                     return False
2706             # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2707             elif path.lower() == p.lower():
2708                 return False
2709         for p in self.depotPaths:
2710             if p4PathStartsWith(path, p):
2711                 return True
2712         return False
2713
2714     def extractFilesFromCommit(self, commit, shelved=False, shelved_cl = 0):
2715         files = []
2716         fnum = 0
2717         while "depotFile%s" % fnum in commit:
2718             path =  commit["depotFile%s" % fnum]
2719             found = self.isPathWanted(path)
2720             if not found:
2721                 fnum = fnum + 1
2722                 continue
2723
2724             file = {}
2725             file["path"] = path
2726             file["rev"] = commit["rev%s" % fnum]
2727             file["action"] = commit["action%s" % fnum]
2728             file["type"] = commit["type%s" % fnum]
2729             if shelved:
2730                 file["shelved_cl"] = int(shelved_cl)
2731             files.append(file)
2732             fnum = fnum + 1
2733         return files
2734
2735     def extractJobsFromCommit(self, commit):
2736         jobs = []
2737         jnum = 0
2738         while "job%s" % jnum in commit:
2739             job = commit["job%s" % jnum]
2740             jobs.append(job)
2741             jnum = jnum + 1
2742         return jobs
2743
2744     def stripRepoPath(self, path, prefixes):
2745         """When streaming files, this is called to map a p4 depot path
2746            to where it should go in git.  The prefixes are either
2747            self.depotPaths, or self.branchPrefixes in the case of
2748            branch detection."""
2749
2750         if self.useClientSpec:
2751             # branch detection moves files up a level (the branch name)
2752             # from what client spec interpretation gives
2753             path = self.clientSpecDirs.map_in_client(path)
2754             if self.detectBranches:
2755                 for b in self.knownBranches:
2756                     if p4PathStartsWith(path, b + "/"):
2757                         path = path[len(b)+1:]
2758
2759         elif self.keepRepoPath:
2760             # Preserve everything in relative path name except leading
2761             # //depot/; just look at first prefix as they all should
2762             # be in the same depot.
2763             depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2764             if p4PathStartsWith(path, depot):
2765                 path = path[len(depot):]
2766
2767         else:
2768             for p in prefixes:
2769                 if p4PathStartsWith(path, p):
2770                     path = path[len(p):]
2771                     break
2772
2773         path = wildcard_decode(path)
2774         return path
2775
2776     def splitFilesIntoBranches(self, commit):
2777         """Look at each depotFile in the commit to figure out to what
2778            branch it belongs."""
2779
2780         if self.clientSpecDirs:
2781             files = self.extractFilesFromCommit(commit)
2782             self.clientSpecDirs.update_client_spec_path_cache(files)
2783
2784         branches = {}
2785         fnum = 0
2786         while "depotFile%s" % fnum in commit:
2787             path =  commit["depotFile%s" % fnum]
2788             found = self.isPathWanted(path)
2789             if not found:
2790                 fnum = fnum + 1
2791                 continue
2792
2793             file = {}
2794             file["path"] = path
2795             file["rev"] = commit["rev%s" % fnum]
2796             file["action"] = commit["action%s" % fnum]
2797             file["type"] = commit["type%s" % fnum]
2798             fnum = fnum + 1
2799
2800             # start with the full relative path where this file would
2801             # go in a p4 client
2802             if self.useClientSpec:
2803                 relPath = self.clientSpecDirs.map_in_client(path)
2804             else:
2805                 relPath = self.stripRepoPath(path, self.depotPaths)
2806
2807             for branch in self.knownBranches.keys():
2808                 # add a trailing slash so that a commit into qt/4.2foo
2809                 # doesn't end up in qt/4.2, e.g.
2810                 if p4PathStartsWith(relPath, branch + "/"):
2811                     if branch not in branches:
2812                         branches[branch] = []
2813                     branches[branch].append(file)
2814                     break
2815
2816         return branches
2817
2818     def writeToGitStream(self, gitMode, relPath, contents):
2819         self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
2820         self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2821         for d in contents:
2822             self.gitStream.write(d)
2823         self.gitStream.write('\n')
2824
2825     def encodeWithUTF8(self, path):
2826         try:
2827             path.decode('ascii')
2828         except:
2829             encoding = 'utf8'
2830             if gitConfig('git-p4.pathEncoding'):
2831                 encoding = gitConfig('git-p4.pathEncoding')
2832             path = path.decode(encoding, 'replace').encode('utf8', 'replace')
2833             if self.verbose:
2834                 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
2835         return path
2836
2837     # output one file from the P4 stream
2838     # - helper for streamP4Files
2839
2840     def streamOneP4File(self, file, contents):
2841         relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2842         relPath = self.encodeWithUTF8(relPath)
2843         if verbose:
2844             if 'fileSize' in self.stream_file:
2845                 size = int(self.stream_file['fileSize'])
2846             else:
2847                 size = 0 # deleted files don't get a fileSize apparently
2848             sys.stdout.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath, size/1024/1024))
2849             sys.stdout.flush()
2850
2851         (type_base, type_mods) = split_p4_type(file["type"])
2852
2853         git_mode = "100644"
2854         if "x" in type_mods:
2855             git_mode = "100755"
2856         if type_base == "symlink":
2857             git_mode = "120000"
2858             # p4 print on a symlink sometimes contains "target\n";
2859             # if it does, remove the newline
2860             data = ''.join(contents)
2861             if not data:
2862                 # Some version of p4 allowed creating a symlink that pointed
2863                 # to nothing.  This causes p4 errors when checking out such
2864                 # a change, and errors here too.  Work around it by ignoring
2865                 # the bad symlink; hopefully a future change fixes it.
2866                 print("\nIgnoring empty symlink in %s" % file['depotFile'])
2867                 return
2868             elif data[-1] == '\n':
2869                 contents = [data[:-1]]
2870             else:
2871                 contents = [data]
2872
2873         if type_base == "utf16":
2874             # p4 delivers different text in the python output to -G
2875             # than it does when using "print -o", or normal p4 client
2876             # operations.  utf16 is converted to ascii or utf8, perhaps.
2877             # But ascii text saved as -t utf16 is completely mangled.
2878             # Invoke print -o to get the real contents.
2879             #
2880             # On windows, the newlines will always be mangled by print, so put
2881             # them back too.  This is not needed to the cygwin windows version,
2882             # just the native "NT" type.
2883             #
2884             try:
2885                 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2886             except Exception as e:
2887                 if 'Translation of file content failed' in str(e):
2888                     type_base = 'binary'
2889                 else:
2890                     raise e
2891             else:
2892                 if p4_version_string().find('/NT') >= 0:
2893                     text = text.replace('\r\n', '\n')
2894                 contents = [ text ]
2895
2896         if type_base == "apple":
2897             # Apple filetype files will be streamed as a concatenation of
2898             # its appledouble header and the contents.  This is useless
2899             # on both macs and non-macs.  If using "print -q -o xx", it
2900             # will create "xx" with the data, and "%xx" with the header.
2901             # This is also not very useful.
2902             #
2903             # Ideally, someday, this script can learn how to generate
2904             # appledouble files directly and import those to git, but
2905             # non-mac machines can never find a use for apple filetype.
2906             print("\nIgnoring apple filetype file %s" % file['depotFile'])
2907             return
2908
2909         # Note that we do not try to de-mangle keywords on utf16 files,
2910         # even though in theory somebody may want that.
2911         pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2912         if pattern:
2913             regexp = re.compile(pattern, re.VERBOSE)
2914             text = ''.join(contents)
2915             text = regexp.sub(r'$\1$', text)
2916             contents = [ text ]
2917
2918         if self.largeFileSystem:
2919             (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
2920
2921         self.writeToGitStream(git_mode, relPath, contents)
2922
2923     def streamOneP4Deletion(self, file):
2924         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2925         relPath = self.encodeWithUTF8(relPath)
2926         if verbose:
2927             sys.stdout.write("delete %s\n" % relPath)
2928             sys.stdout.flush()
2929         self.gitStream.write("D %s\n" % relPath)
2930
2931         if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2932             self.largeFileSystem.removeLargeFile(relPath)
2933
2934     # handle another chunk of streaming data
2935     def streamP4FilesCb(self, marshalled):
2936
2937         # catch p4 errors and complain
2938         err = None
2939         if "code" in marshalled:
2940             if marshalled["code"] == "error":
2941                 if "data" in marshalled:
2942                     err = marshalled["data"].rstrip()
2943
2944         if not err and 'fileSize' in self.stream_file:
2945             required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
2946             if required_bytes > 0:
2947                 err = 'Not enough space left on %s! Free at least %i MB.' % (
2948                     os.getcwd(), required_bytes/1024/1024
2949                 )
2950
2951         if err:
2952             f = None
2953             if self.stream_have_file_info:
2954                 if "depotFile" in self.stream_file:
2955                     f = self.stream_file["depotFile"]
2956             # force a failure in fast-import, else an empty
2957             # commit will be made
2958             self.gitStream.write("\n")
2959             self.gitStream.write("die-now\n")
2960             self.gitStream.close()
2961             # ignore errors, but make sure it exits first
2962             self.importProcess.wait()
2963             if f:
2964                 die("Error from p4 print for %s: %s" % (f, err))
2965             else:
2966                 die("Error from p4 print: %s" % err)
2967
2968         if 'depotFile' in marshalled and self.stream_have_file_info:
2969             # start of a new file - output the old one first
2970             self.streamOneP4File(self.stream_file, self.stream_contents)
2971             self.stream_file = {}
2972             self.stream_contents = []
2973             self.stream_have_file_info = False
2974
2975         # pick up the new file information... for the
2976         # 'data' field we need to append to our array
2977         for k in marshalled.keys():
2978             if k == 'data':
2979                 if 'streamContentSize' not in self.stream_file:
2980                     self.stream_file['streamContentSize'] = 0
2981                 self.stream_file['streamContentSize'] += len(marshalled['data'])
2982                 self.stream_contents.append(marshalled['data'])
2983             else:
2984                 self.stream_file[k] = marshalled[k]
2985
2986         if (verbose and
2987             'streamContentSize' in self.stream_file and
2988             'fileSize' in self.stream_file and
2989             'depotFile' in self.stream_file):
2990             size = int(self.stream_file["fileSize"])
2991             if size > 0:
2992                 progress = 100*self.stream_file['streamContentSize']/size
2993                 sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
2994                 sys.stdout.flush()
2995
2996         self.stream_have_file_info = True
2997
2998     # Stream directly from "p4 files" into "git fast-import"
2999     def streamP4Files(self, files):
3000         filesForCommit = []
3001         filesToRead = []
3002         filesToDelete = []
3003
3004         for f in files:
3005             filesForCommit.append(f)
3006             if f['action'] in self.delete_actions:
3007                 filesToDelete.append(f)
3008             else:
3009                 filesToRead.append(f)
3010
3011         # deleted files...
3012         for f in filesToDelete:
3013             self.streamOneP4Deletion(f)
3014
3015         if len(filesToRead) > 0:
3016             self.stream_file = {}
3017             self.stream_contents = []
3018             self.stream_have_file_info = False
3019
3020             # curry self argument
3021             def streamP4FilesCbSelf(entry):
3022                 self.streamP4FilesCb(entry)
3023
3024             fileArgs = []
3025             for f in filesToRead:
3026                 if 'shelved_cl' in f:
3027                     # Handle shelved CLs using the "p4 print file@=N" syntax to print
3028                     # the contents
3029                     fileArg = '%s@=%d' % (f['path'], f['shelved_cl'])
3030                 else:
3031                     fileArg = '%s#%s' % (f['path'], f['rev'])
3032
3033                 fileArgs.append(fileArg)
3034
3035             p4CmdList(["-x", "-", "print"],
3036                       stdin=fileArgs,
3037                       cb=streamP4FilesCbSelf)
3038
3039             # do the last chunk
3040             if 'depotFile' in self.stream_file:
3041                 self.streamOneP4File(self.stream_file, self.stream_contents)
3042
3043     def make_email(self, userid):
3044         if userid in self.users:
3045             return self.users[userid]
3046         else:
3047             return "%s <a@b>" % userid
3048
3049     def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
3050         """ Stream a p4 tag.
3051         commit is either a git commit, or a fast-import mark, ":<p4commit>"
3052         """
3053
3054         if verbose:
3055             print("writing tag %s for commit %s" % (labelName, commit))
3056         gitStream.write("tag %s\n" % labelName)
3057         gitStream.write("from %s\n" % commit)
3058
3059         if 'Owner' in labelDetails:
3060             owner = labelDetails["Owner"]
3061         else:
3062             owner = None
3063
3064         # Try to use the owner of the p4 label, or failing that,
3065         # the current p4 user id.
3066         if owner:
3067             email = self.make_email(owner)
3068         else:
3069             email = self.make_email(self.p4UserId())
3070         tagger = "%s %s %s" % (email, epoch, self.tz)
3071
3072         gitStream.write("tagger %s\n" % tagger)
3073
3074         print("labelDetails=",labelDetails)
3075         if 'Description' in labelDetails:
3076             description = labelDetails['Description']
3077         else:
3078             description = 'Label from git p4'
3079
3080         gitStream.write("data %d\n" % len(description))
3081         gitStream.write(description)
3082         gitStream.write("\n")
3083
3084     def inClientSpec(self, path):
3085         if not self.clientSpecDirs:
3086             return True
3087         inClientSpec = self.clientSpecDirs.map_in_client(path)
3088         if not inClientSpec and self.verbose:
3089             print('Ignoring file outside of client spec: {0}'.format(path))
3090         return inClientSpec
3091
3092     def hasBranchPrefix(self, path):
3093         if not self.branchPrefixes:
3094             return True
3095         hasPrefix = [p for p in self.branchPrefixes
3096                         if p4PathStartsWith(path, p)]
3097         if not hasPrefix and self.verbose:
3098             print('Ignoring file outside of prefix: {0}'.format(path))
3099         return hasPrefix
3100
3101     def commit(self, details, files, branch, parent = "", allow_empty=False):
3102         epoch = details["time"]
3103         author = details["user"]
3104         jobs = self.extractJobsFromCommit(details)
3105
3106         if self.verbose:
3107             print('commit into {0}'.format(branch))
3108
3109         if self.clientSpecDirs:
3110             self.clientSpecDirs.update_client_spec_path_cache(files)
3111
3112         files = [f for f in files
3113             if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]
3114
3115         if gitConfigBool('git-p4.keepEmptyCommits'):
3116             allow_empty = True
3117
3118         if not files and not allow_empty:
3119             print('Ignoring revision {0} as it would produce an empty commit.'
3120                 .format(details['change']))
3121             return
3122
3123         self.gitStream.write("commit %s\n" % branch)
3124         self.gitStream.write("mark :%s\n" % details["change"])
3125         self.committedChanges.add(int(details["change"]))
3126         committer = ""
3127         if author not in self.users:
3128             self.getUserMapFromPerforceServer()
3129         committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
3130
3131         self.gitStream.write("committer %s\n" % committer)
3132
3133         self.gitStream.write("data <<EOT\n")
3134         self.gitStream.write(details["desc"])
3135         if len(jobs) > 0:
3136             self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
3137
3138         if not self.suppress_meta_comment:
3139             self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3140                                 (','.join(self.branchPrefixes), details["change"]))
3141             if len(details['options']) > 0:
3142                 self.gitStream.write(": options = %s" % details['options'])
3143             self.gitStream.write("]\n")
3144
3145         self.gitStream.write("EOT\n\n")
3146
3147         if len(parent) > 0:
3148             if self.verbose:
3149                 print("parent %s" % parent)
3150             self.gitStream.write("from %s\n" % parent)
3151
3152         self.streamP4Files(files)
3153         self.gitStream.write("\n")
3154
3155         change = int(details["change"])
3156
3157         if change in self.labels:
3158             label = self.labels[change]
3159             labelDetails = label[0]
3160             labelRevisions = label[1]
3161             if self.verbose:
3162                 print("Change %s is labelled %s" % (change, labelDetails))
3163
3164             files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
3165                                                 for p in self.branchPrefixes])
3166
3167             if len(files) == len(labelRevisions):
3168
3169                 cleanedFiles = {}
3170                 for info in files:
3171                     if info["action"] in self.delete_actions:
3172                         continue
3173                     cleanedFiles[info["depotFile"]] = info["rev"]
3174
3175                 if cleanedFiles == labelRevisions:
3176                     self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
3177
3178                 else:
3179                     if not self.silent:
3180                         print("Tag %s does not match with change %s: files do not match."
3181                                % (labelDetails["label"], change))
3182
3183             else:
3184                 if not self.silent:
3185                     print("Tag %s does not match with change %s: file count is different."
3186                            % (labelDetails["label"], change))
3187
3188     # Build a dictionary of changelists and labels, for "detect-labels" option.
3189     def getLabels(self):
3190         self.labels = {}
3191
3192         l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
3193         if len(l) > 0 and not self.silent:
3194             print("Finding files belonging to labels in %s" % self.depotPaths)
3195
3196         for output in l:
3197             label = output["label"]
3198             revisions = {}
3199             newestChange = 0
3200             if self.verbose:
3201                 print("Querying files for label %s" % label)
3202             for file in p4CmdList(["files"] +
3203                                       ["%s...@%s" % (p, label)
3204                                           for p in self.depotPaths]):
3205                 revisions[file["depotFile"]] = file["rev"]
3206                 change = int(file["change"])
3207                 if change > newestChange:
3208                     newestChange = change
3209
3210             self.labels[newestChange] = [output, revisions]
3211
3212         if self.verbose:
3213             print("Label changes: %s" % self.labels.keys())
3214
3215     # Import p4 labels as git tags. A direct mapping does not
3216     # exist, so assume that if all the files are at the same revision
3217     # then we can use that, or it's something more complicated we should
3218     # just ignore.
3219     def importP4Labels(self, stream, p4Labels):
3220         if verbose:
3221             print("import p4 labels: " + ' '.join(p4Labels))
3222
3223         ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
3224         validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
3225         if len(validLabelRegexp) == 0:
3226             validLabelRegexp = defaultLabelRegexp
3227         m = re.compile(validLabelRegexp)
3228
3229         for name in p4Labels:
3230             commitFound = False
3231
3232             if not m.match(name):
3233                 if verbose:
3234                     print("label %s does not match regexp %s" % (name,validLabelRegexp))
3235                 continue
3236
3237             if name in ignoredP4Labels:
3238                 continue
3239
3240             labelDetails = p4CmdList(['label', "-o", name])[0]
3241
3242             # get the most recent changelist for each file in this label
3243             change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3244                                 for p in self.depotPaths])
3245
3246             if 'change' in change:
3247                 # find the corresponding git commit; take the oldest commit
3248                 changelist = int(change['change'])
3249                 if changelist in self.committedChanges:
3250                     gitCommit = ":%d" % changelist       # use a fast-import mark
3251                     commitFound = True
3252                 else:
3253                     gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3254                         "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3255                     if len(gitCommit) == 0:
3256                         print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
3257                     else:
3258                         commitFound = True
3259                         gitCommit = gitCommit.strip()
3260
3261                 if commitFound:
3262                     # Convert from p4 time format
3263                     try:
3264                         tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3265                     except ValueError:
3266                         print("Could not convert label time %s" % labelDetails['Update'])
3267                         tmwhen = 1
3268
3269                     when = int(time.mktime(tmwhen))
3270                     self.streamTag(stream, name, labelDetails, gitCommit, when)
3271                     if verbose:
3272                         print("p4 label %s mapped to git commit %s" % (name, gitCommit))
3273             else:
3274                 if verbose:
3275                     print("Label %s has no changelists - possibly deleted?" % name)
3276
3277             if not commitFound:
3278                 # We can't import this label; don't try again as it will get very
3279                 # expensive repeatedly fetching all the files for labels that will
3280                 # never be imported. If the label is moved in the future, the
3281                 # ignore will need to be removed manually.
3282                 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3283
3284     def guessProjectName(self):
3285         for p in self.depotPaths:
3286             if p.endswith("/"):
3287                 p = p[:-1]
3288             p = p[p.strip().rfind("/") + 1:]
3289             if not p.endswith("/"):
3290                p += "/"
3291             return p
3292
3293     def getBranchMapping(self):
3294         lostAndFoundBranches = set()
3295
3296         user = gitConfig("git-p4.branchUser")
3297         if len(user) > 0:
3298             command = "branches -u %s" % user
3299         else:
3300             command = "branches"
3301
3302         for info in p4CmdList(command):
3303             details = p4Cmd(["branch", "-o", info["branch"]])
3304             viewIdx = 0
3305             while "View%s" % viewIdx in details:
3306                 paths = details["View%s" % viewIdx].split(" ")
3307                 viewIdx = viewIdx + 1
3308                 # require standard //depot/foo/... //depot/bar/... mapping
3309                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3310                     continue
3311                 source = paths[0]
3312                 destination = paths[1]
3313                 ## HACK
3314                 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
3315                     source = source[len(self.depotPaths[0]):-4]
3316                     destination = destination[len(self.depotPaths[0]):-4]
3317
3318                     if destination in self.knownBranches:
3319                         if not self.silent:
3320                             print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3321                             print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
3322                         continue
3323
3324                     self.knownBranches[destination] = source
3325
3326                     lostAndFoundBranches.discard(destination)
3327
3328                     if source not in self.knownBranches:
3329                         lostAndFoundBranches.add(source)
3330
3331         # Perforce does not strictly require branches to be defined, so we also
3332         # check git config for a branch list.
3333         #
3334         # Example of branch definition in git config file:
3335         # [git-p4]
3336         #   branchList=main:branchA
3337         #   branchList=main:branchB
3338         #   branchList=branchA:branchC
3339         configBranches = gitConfigList("git-p4.branchList")
3340         for branch in configBranches:
3341             if branch:
3342                 (source, destination) = branch.split(":")
3343                 self.knownBranches[destination] = source
3344
3345                 lostAndFoundBranches.discard(destination)
3346
3347                 if source not in self.knownBranches:
3348                     lostAndFoundBranches.add(source)
3349
3350
3351         for branch in lostAndFoundBranches:
3352             self.knownBranches[branch] = branch
3353
3354     def getBranchMappingFromGitBranches(self):
3355         branches = p4BranchesInGit(self.importIntoRemotes)
3356         for branch in branches.keys():
3357             if branch == "master":
3358                 branch = "main"
3359             else:
3360                 branch = branch[len(self.projectName):]
3361             self.knownBranches[branch] = branch
3362
3363     def updateOptionDict(self, d):
3364         option_keys = {}
3365         if self.keepRepoPath:
3366             option_keys['keepRepoPath'] = 1
3367
3368         d["options"] = ' '.join(sorted(option_keys.keys()))
3369
3370     def readOptions(self, d):
3371         self.keepRepoPath = ('options' in d
3372                              and ('keepRepoPath' in d['options']))
3373
3374     def gitRefForBranch(self, branch):
3375         if branch == "main":
3376             return self.refPrefix + "master"
3377
3378         if len(branch) <= 0:
3379             return branch
3380
3381         return self.refPrefix + self.projectName + branch
3382
3383     def gitCommitByP4Change(self, ref, change):
3384         if self.verbose:
3385             print("looking in ref " + ref + " for change %s using bisect..." % change)
3386
3387         earliestCommit = ""
3388         latestCommit = parseRevision(ref)
3389
3390         while True:
3391             if self.verbose:
3392                 print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
3393             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
3394             if len(next) == 0:
3395                 if self.verbose:
3396                     print("argh")
3397                 return ""
3398             log = extractLogMessageFromGitCommit(next)
3399             settings = extractSettingsGitLog(log)
3400             currentChange = int(settings['change'])
3401             if self.verbose:
3402                 print("current change %s" % currentChange)
3403
3404             if currentChange == change:
3405                 if self.verbose:
3406                     print("found %s" % next)
3407                 return next
3408
3409             if currentChange < change:
3410                 earliestCommit = "^%s" % next
3411             else:
3412                 if next == latestCommit:
3413                     die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3414                 latestCommit = "%s^@" % next
3415
3416         return ""
3417
3418     def importNewBranch(self, branch, maxChange):
3419         # make fast-import flush all changes to disk and update the refs using the checkpoint
3420         # command so that we can try to find the branch parent in the git history
3421         self.gitStream.write("checkpoint\n\n");
3422         self.gitStream.flush();
3423         branchPrefix = self.depotPaths[0] + branch + "/"
3424         range = "@1,%s" % maxChange
3425         #print "prefix" + branchPrefix
3426         changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3427         if len(changes) <= 0:
3428             return False
3429         firstChange = changes[0]
3430         #print "first change in branch: %s" % firstChange
3431         sourceBranch = self.knownBranches[branch]
3432         sourceDepotPath = self.depotPaths[0] + sourceBranch
3433         sourceRef = self.gitRefForBranch(sourceBranch)
3434         #print "source " + sourceBranch
3435
3436         branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3437         #print "branch parent: %s" % branchParentChange
3438         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3439         if len(gitParent) > 0:
3440             self.initialParents[self.gitRefForBranch(branch)] = gitParent
3441             #print "parent git commit: %s" % gitParent
3442
3443         self.importChanges(changes)
3444         return True
3445
3446     def searchParent(self, parent, branch, target):
3447         parentFound = False
3448         for blob in read_pipe_lines(["git", "rev-list", "--reverse",
3449                                      "--no-merges", parent]):
3450             blob = blob.strip()
3451             if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
3452                 parentFound = True
3453                 if self.verbose:
3454                     print("Found parent of %s in commit %s" % (branch, blob))
3455                 break
3456         if parentFound:
3457             return blob
3458         else:
3459             return None
3460
3461     def importChanges(self, changes, origin_revision=0):
3462         cnt = 1
3463         for change in changes:
3464             description = p4_describe(change)
3465             self.updateOptionDict(description)
3466
3467             if not self.silent:
3468                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
3469                 sys.stdout.flush()
3470             cnt = cnt + 1
3471
3472             try:
3473                 if self.detectBranches:
3474                     branches = self.splitFilesIntoBranches(description)
3475                     for branch in branches.keys():
3476                         ## HACK  --hwn
3477                         branchPrefix = self.depotPaths[0] + branch + "/"
3478                         self.branchPrefixes = [ branchPrefix ]
3479
3480                         parent = ""
3481
3482                         filesForCommit = branches[branch]
3483
3484                         if self.verbose:
3485                             print("branch is %s" % branch)
3486
3487                         self.updatedBranches.add(branch)
3488
3489                         if branch not in self.createdBranches:
3490                             self.createdBranches.add(branch)
3491                             parent = self.knownBranches[branch]
3492                             if parent == branch:
3493                                 parent = ""
3494                             else:
3495                                 fullBranch = self.projectName + branch
3496                                 if fullBranch not in self.p4BranchesInGit:
3497                                     if not self.silent:
3498                                         print("\n    Importing new branch %s" % fullBranch);
3499                                     if self.importNewBranch(branch, change - 1):
3500                                         parent = ""
3501                                         self.p4BranchesInGit.append(fullBranch)
3502                                     if not self.silent:
3503                                         print("\n    Resuming with change %s" % change);
3504
3505                                 if self.verbose:
3506                                     print("parent determined through known branches: %s" % parent)
3507
3508                         branch = self.gitRefForBranch(branch)
3509                         parent = self.gitRefForBranch(parent)
3510
3511                         if self.verbose:
3512                             print("looking for initial parent for %s; current parent is %s" % (branch, parent))
3513
3514                         if len(parent) == 0 and branch in self.initialParents:
3515                             parent = self.initialParents[branch]
3516                             del self.initialParents[branch]
3517
3518                         blob = None
3519                         if len(parent) > 0:
3520                             tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3521                             if self.verbose:
3522                                 print("Creating temporary branch: " + tempBranch)
3523                             self.commit(description, filesForCommit, tempBranch)
3524                             self.tempBranches.append(tempBranch)
3525                             self.checkpoint()
3526                             blob = self.searchParent(parent, branch, tempBranch)
3527                         if blob:
3528                             self.commit(description, filesForCommit, branch, blob)
3529                         else:
3530                             if self.verbose:
3531                                 print("Parent of %s not found. Committing into head of %s" % (branch, parent))
3532                             self.commit(description, filesForCommit, branch, parent)
3533                 else:
3534                     files = self.extractFilesFromCommit(description)
3535                     self.commit(description, files, self.branch,
3536                                 self.initialParent)
3537                     # only needed once, to connect to the previous commit
3538                     self.initialParent = ""
3539             except IOError:
3540                 print(self.gitError.read())
3541                 sys.exit(1)
3542
3543     def sync_origin_only(self):
3544         if self.syncWithOrigin:
3545             self.hasOrigin = originP4BranchesExist()
3546             if self.hasOrigin:
3547                 if not self.silent:
3548                     print('Syncing with origin first, using "git fetch origin"')
3549                 system("git fetch origin")
3550
3551     def importHeadRevision(self, revision):
3552         print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
3553
3554         details = {}
3555         details["user"] = "git perforce import user"
3556         details["desc"] = ("Initial import of %s from the state at revision %s\n"
3557                            % (' '.join(self.depotPaths), revision))
3558         details["change"] = revision
3559         newestRevision = 0
3560
3561         fileCnt = 0
3562         fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3563
3564         for info in p4CmdList(["files"] + fileArgs):
3565
3566             if 'code' in info and info['code'] == 'error':
3567                 sys.stderr.write("p4 returned an error: %s\n"
3568                                  % info['data'])
3569                 if info['data'].find("must refer to client") >= 0:
3570                     sys.stderr.write("This particular p4 error is misleading.\n")
3571                     sys.stderr.write("Perhaps the depot path was misspelled.\n");
3572                     sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
3573                 sys.exit(1)
3574             if 'p4ExitCode' in info:
3575                 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3576                 sys.exit(1)
3577
3578
3579             change = int(info["change"])
3580             if change > newestRevision:
3581                 newestRevision = change
3582
3583             if info["action"] in self.delete_actions:
3584                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3585                 #fileCnt = fileCnt + 1
3586                 continue
3587
3588             for prop in ["depotFile", "rev", "action", "type" ]:
3589                 details["%s%s" % (prop, fileCnt)] = info[prop]
3590
3591             fileCnt = fileCnt + 1
3592
3593         details["change"] = newestRevision
3594
3595         # Use time from top-most change so that all git p4 clones of
3596         # the same p4 repo have the same commit SHA1s.
3597         res = p4_describe(newestRevision)
3598         details["time"] = res["time"]
3599
3600         self.updateOptionDict(details)
3601         try:
3602             self.commit(details, self.extractFilesFromCommit(details), self.branch)
3603         except IOError as err:
3604             print("IO error with git fast-import. Is your git version recent enough?")
3605             print("IO error details: {}".format(err))
3606             print(self.gitError.read())
3607
3608     def openStreams(self):
3609         self.importProcess = subprocess.Popen(["git", "fast-import"],
3610                                               stdin=subprocess.PIPE,
3611                                               stdout=subprocess.PIPE,
3612                                               stderr=subprocess.PIPE);
3613         self.gitOutput = self.importProcess.stdout
3614         self.gitStream = self.importProcess.stdin
3615         self.gitError = self.importProcess.stderr
3616
3617     def closeStreams(self):
3618         self.gitStream.close()
3619         if self.importProcess.wait() != 0:
3620             die("fast-import failed: %s" % self.gitError.read())
3621         self.gitOutput.close()
3622         self.gitError.close()
3623
3624     def run(self, args):
3625         if self.importIntoRemotes:
3626             self.refPrefix = "refs/remotes/p4/"
3627         else:
3628             self.refPrefix = "refs/heads/p4/"
3629
3630         self.sync_origin_only()
3631
3632         branch_arg_given = bool(self.branch)
3633         if len(self.branch) == 0:
3634             self.branch = self.refPrefix + "master"
3635             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
3636                 system("git update-ref %s refs/heads/p4" % self.branch)
3637                 system("git branch -D p4")
3638
3639         # accept either the command-line option, or the configuration variable
3640         if self.useClientSpec:
3641             # will use this after clone to set the variable
3642             self.useClientSpec_from_options = True
3643         else:
3644             if gitConfigBool("git-p4.useclientspec"):
3645                 self.useClientSpec = True
3646         if self.useClientSpec:
3647             self.clientSpecDirs = getClientSpec()
3648
3649         # TODO: should always look at previous commits,
3650         # merge with previous imports, if possible.
3651         if args == []:
3652             if self.hasOrigin:
3653                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
3654
3655             # branches holds mapping from branch name to sha1
3656             branches = p4BranchesInGit(self.importIntoRemotes)
3657
3658             # restrict to just this one, disabling detect-branches
3659             if branch_arg_given:
3660                 short = self.branch.split("/")[-1]
3661                 if short in branches:
3662                     self.p4BranchesInGit = [ short ]
3663             else:
3664                 self.p4BranchesInGit = branches.keys()
3665
3666             if len(self.p4BranchesInGit) > 1:
3667                 if not self.silent:
3668                     print("Importing from/into multiple branches")
3669                 self.detectBranches = True
3670                 for branch in branches.keys():
3671                     self.initialParents[self.refPrefix + branch] = \
3672                         branches[branch]
3673
3674             if self.verbose:
3675                 print("branches: %s" % self.p4BranchesInGit)
3676
3677             p4Change = 0
3678             for branch in self.p4BranchesInGit:
3679                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
3680
3681                 settings = extractSettingsGitLog(logMsg)
3682
3683                 self.readOptions(settings)
3684                 if ('depot-paths' in settings
3685                     and 'change' in settings):
3686                     change = int(settings['change']) + 1
3687                     p4Change = max(p4Change, change)
3688
3689                     depotPaths = sorted(settings['depot-paths'])
3690                     if self.previousDepotPaths == []:
3691                         self.previousDepotPaths = depotPaths
3692                     else:
3693                         paths = []
3694                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
3695                             prev_list = prev.split("/")
3696                             cur_list = cur.split("/")
3697                             for i in range(0, min(len(cur_list), len(prev_list))):
3698                                 if cur_list[i] != prev_list[i]:
3699                                     i = i - 1
3700                                     break
3701
3702                             paths.append ("/".join(cur_list[:i + 1]))
3703
3704                         self.previousDepotPaths = paths
3705
3706             if p4Change > 0:
3707                 self.depotPaths = sorted(self.previousDepotPaths)
3708                 self.changeRange = "@%s,#head" % p4Change
3709                 if not self.silent and not self.detectBranches:
3710                     print("Performing incremental import into %s git branch" % self.branch)
3711
3712         # accept multiple ref name abbreviations:
3713         #    refs/foo/bar/branch -> use it exactly
3714         #    p4/branch -> prepend refs/remotes/ or refs/heads/
3715         #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
3716         if not self.branch.startswith("refs/"):
3717             if self.importIntoRemotes:
3718                 prepend = "refs/remotes/"
3719             else:
3720                 prepend = "refs/heads/"
3721             if not self.branch.startswith("p4/"):
3722                 prepend += "p4/"
3723             self.branch = prepend + self.branch
3724
3725         if len(args) == 0 and self.depotPaths:
3726             if not self.silent:
3727                 print("Depot paths: %s" % ' '.join(self.depotPaths))
3728         else:
3729             if self.depotPaths and self.depotPaths != args:
3730                 print("previous import used depot path %s and now %s was specified. "
3731                        "This doesn't work!" % (' '.join (self.depotPaths),
3732                                                ' '.join (args)))
3733                 sys.exit(1)
3734
3735             self.depotPaths = sorted(args)
3736
3737         revision = ""
3738         self.users = {}
3739
3740         # Make sure no revision specifiers are used when --changesfile
3741         # is specified.
3742         bad_changesfile = False
3743         if len(self.changesFile) > 0:
3744             for p in self.depotPaths:
3745                 if p.find("@") >= 0 or p.find("#") >= 0:
3746                     bad_changesfile = True
3747                     break
3748         if bad_changesfile:
3749             die("Option --changesfile is incompatible with revision specifiers")
3750
3751         newPaths = []
3752         for p in self.depotPaths:
3753             if p.find("@") != -1:
3754                 atIdx = p.index("@")
3755                 self.changeRange = p[atIdx:]
3756                 if self.changeRange == "@all":
3757                     self.changeRange = ""
3758                 elif ',' not in self.changeRange:
3759                     revision = self.changeRange
3760                     self.changeRange = ""
3761                 p = p[:atIdx]
3762             elif p.find("#") != -1:
3763                 hashIdx = p.index("#")
3764                 revision = p[hashIdx:]
3765                 p = p[:hashIdx]
3766             elif self.previousDepotPaths == []:
3767                 # pay attention to changesfile, if given, else import
3768                 # the entire p4 tree at the head revision
3769                 if len(self.changesFile) == 0:
3770                     revision = "#head"
3771
3772             p = re.sub ("\.\.\.$", "", p)
3773             if not p.endswith("/"):
3774                 p += "/"
3775
3776             newPaths.append(p)
3777
3778         self.depotPaths = newPaths
3779
3780         # --detect-branches may change this for each branch
3781         self.branchPrefixes = self.depotPaths
3782
3783         self.loadUserMapFromCache()
3784         self.labels = {}
3785         if self.detectLabels:
3786             self.getLabels();
3787
3788         if self.detectBranches:
3789             ## FIXME - what's a P4 projectName ?
3790             self.projectName = self.guessProjectName()
3791
3792             if self.hasOrigin:
3793                 self.getBranchMappingFromGitBranches()
3794             else:
3795                 self.getBranchMapping()
3796             if self.verbose:
3797                 print("p4-git branches: %s" % self.p4BranchesInGit)
3798                 print("initial parents: %s" % self.initialParents)
3799             for b in self.p4BranchesInGit:
3800                 if b != "master":
3801
3802                     ## FIXME
3803                     b = b[len(self.projectName):]
3804                 self.createdBranches.add(b)
3805
3806         self.openStreams()
3807
3808         if revision:
3809             self.importHeadRevision(revision)
3810         else:
3811             changes = []
3812
3813             if len(self.changesFile) > 0:
3814                 output = open(self.changesFile).readlines()
3815                 changeSet = set()
3816                 for line in output:
3817                     changeSet.add(int(line))
3818
3819                 for change in changeSet:
3820                     changes.append(change)
3821
3822                 changes.sort()
3823             else:
3824                 # catch "git p4 sync" with no new branches, in a repo that
3825                 # does not have any existing p4 branches
3826                 if len(args) == 0:
3827                     if not self.p4BranchesInGit:
3828                         die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
3829
3830                     # The default branch is master, unless --branch is used to
3831                     # specify something else.  Make sure it exists, or complain
3832                     # nicely about how to use --branch.
3833                     if not self.detectBranches:
3834                         if not branch_exists(self.branch):
3835                             if branch_arg_given:
3836                                 die("Error: branch %s does not exist." % self.branch)
3837                             else:
3838                                 die("Error: no branch %s; perhaps specify one with --branch." %
3839                                     self.branch)
3840
3841                 if self.verbose:
3842                     print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3843                                                               self.changeRange))
3844                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3845
3846                 if len(self.maxChanges) > 0:
3847                     changes = changes[:min(int(self.maxChanges), len(changes))]
3848
3849             if len(changes) == 0:
3850                 if not self.silent:
3851                     print("No changes to import!")
3852             else:
3853                 if not self.silent and not self.detectBranches:
3854                     print("Import destination: %s" % self.branch)
3855
3856                 self.updatedBranches = set()
3857
3858                 if not self.detectBranches:
3859                     if args:
3860                         # start a new branch
3861                         self.initialParent = ""
3862                     else:
3863                         # build on a previous revision
3864                         self.initialParent = parseRevision(self.branch)
3865
3866                 self.importChanges(changes)
3867
3868                 if not self.silent:
3869                     print("")
3870                     if len(self.updatedBranches) > 0:
3871                         sys.stdout.write("Updated branches: ")
3872                         for b in self.updatedBranches:
3873                             sys.stdout.write("%s " % b)
3874                         sys.stdout.write("\n")
3875
3876         if gitConfigBool("git-p4.importLabels"):
3877             self.importLabels = True
3878
3879         if self.importLabels:
3880             p4Labels = getP4Labels(self.depotPaths)
3881             gitTags = getGitTags()
3882
3883             missingP4Labels = p4Labels - gitTags
3884             self.importP4Labels(self.gitStream, missingP4Labels)
3885
3886         self.closeStreams()
3887
3888         # Cleanup temporary branches created during import
3889         if self.tempBranches != []:
3890             for branch in self.tempBranches:
3891                 read_pipe("git update-ref -d %s" % branch)
3892             os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3893
3894         # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3895         # a convenient shortcut refname "p4".
3896         if self.importIntoRemotes:
3897             head_ref = self.refPrefix + "HEAD"
3898             if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3899                 system(["git", "symbolic-ref", head_ref, self.branch])
3900
3901         return True
3902
3903 class P4Rebase(Command):
3904     def __init__(self):
3905         Command.__init__(self)
3906         self.options = [
3907                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3908         ]
3909         self.importLabels = False
3910         self.description = ("Fetches the latest revision from perforce and "
3911                             + "rebases the current work (branch) against it")
3912
3913     def run(self, args):
3914         sync = P4Sync()
3915         sync.importLabels = self.importLabels
3916         sync.run([])
3917
3918         return self.rebase()
3919
3920     def rebase(self):
3921         if os.system("git update-index --refresh") != 0:
3922             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.");
3923         if len(read_pipe("git diff-index HEAD --")) > 0:
3924             die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3925
3926         [upstream, settings] = findUpstreamBranchPoint()
3927         if len(upstream) == 0:
3928             die("Cannot find upstream branchpoint for rebase")
3929
3930         # the branchpoint may be p4/foo~3, so strip off the parent
3931         upstream = re.sub("~[0-9]+$", "", upstream)
3932
3933         print("Rebasing the current branch onto %s" % upstream)
3934         oldHead = read_pipe("git rev-parse HEAD").strip()
3935         system("git rebase %s" % upstream)
3936         system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3937         return True
3938
3939 class P4Clone(P4Sync):
3940     def __init__(self):
3941         P4Sync.__init__(self)
3942         self.description = "Creates a new git repository and imports from Perforce into it"
3943         self.usage = "usage: %prog [options] //depot/path[@revRange]"
3944         self.options += [
3945             optparse.make_option("--destination", dest="cloneDestination",
3946                                  action='store', default=None,
3947                                  help="where to leave result of the clone"),
3948             optparse.make_option("--bare", dest="cloneBare",
3949                                  action="store_true", default=False),
3950         ]
3951         self.cloneDestination = None
3952         self.needsGit = False
3953         self.cloneBare = False
3954
3955     def defaultDestination(self, args):
3956         ## TODO: use common prefix of args?
3957         depotPath = args[0]
3958         depotDir = re.sub("(@[^@]*)$", "", depotPath)
3959         depotDir = re.sub("(#[^#]*)$", "", depotDir)
3960         depotDir = re.sub(r"\.\.\.$", "", depotDir)
3961         depotDir = re.sub(r"/$", "", depotDir)
3962         return os.path.split(depotDir)[1]
3963
3964     def run(self, args):
3965         if len(args) < 1:
3966             return False
3967
3968         if self.keepRepoPath and not self.cloneDestination:
3969             sys.stderr.write("Must specify destination for --keep-path\n")
3970             sys.exit(1)
3971
3972         depotPaths = args
3973
3974         if not self.cloneDestination and len(depotPaths) > 1:
3975             self.cloneDestination = depotPaths[-1]
3976             depotPaths = depotPaths[:-1]
3977
3978         for p in depotPaths:
3979             if not p.startswith("//"):
3980                 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3981                 return False
3982
3983         if not self.cloneDestination:
3984             self.cloneDestination = self.defaultDestination(args)
3985
3986         print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
3987
3988         if not os.path.exists(self.cloneDestination):
3989             os.makedirs(self.cloneDestination)
3990         chdir(self.cloneDestination)
3991
3992         init_cmd = [ "git", "init" ]
3993         if self.cloneBare:
3994             init_cmd.append("--bare")
3995         retcode = subprocess.call(init_cmd)
3996         if retcode:
3997             raise CalledProcessError(retcode, init_cmd)
3998
3999         if not P4Sync.run(self, depotPaths):
4000             return False
4001
4002         # create a master branch and check out a work tree
4003         if gitBranchExists(self.branch):
4004             system([ "git", "branch", "master", self.branch ])
4005             if not self.cloneBare:
4006                 system([ "git", "checkout", "-f" ])
4007         else:
4008             print('Not checking out any branch, use ' \
4009                   '"git checkout -q -b master <branch>"')
4010
4011         # auto-set this variable if invoked with --use-client-spec
4012         if self.useClientSpec_from_options:
4013             system("git config --bool git-p4.useclientspec true")
4014
4015         return True
4016
4017 class P4Unshelve(Command):
4018     def __init__(self):
4019         Command.__init__(self)
4020         self.options = []
4021         self.origin = "HEAD"
4022         self.description = "Unshelve a P4 changelist into a git commit"
4023         self.usage = "usage: %prog [options] changelist"
4024         self.options += [
4025                 optparse.make_option("--origin", dest="origin",
4026                     help="Use this base revision instead of the default (%s)" % self.origin),
4027         ]
4028         self.verbose = False
4029         self.noCommit = False
4030         self.destbranch = "refs/remotes/p4-unshelved"
4031
4032     def renameBranch(self, branch_name):
4033         """ Rename the existing branch to branch_name.N
4034         """
4035
4036         found = True
4037         for i in range(0,1000):
4038             backup_branch_name = "{0}.{1}".format(branch_name, i)
4039             if not gitBranchExists(backup_branch_name):
4040                 gitUpdateRef(backup_branch_name, branch_name) # copy ref to backup
4041                 gitDeleteRef(branch_name)
4042                 found = True
4043                 print("renamed old unshelve branch to {0}".format(backup_branch_name))
4044                 break
4045
4046         if not found:
4047             sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
4048
4049     def findLastP4Revision(self, starting_point):
4050         """ Look back from starting_point for the first commit created by git-p4
4051             to find the P4 commit we are based on, and the depot-paths.
4052         """
4053
4054         for parent in (range(65535)):
4055             log = extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))
4056             settings = extractSettingsGitLog(log)
4057             if 'change' in settings:
4058                 return settings
4059
4060         sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4061
4062     def createShelveParent(self, change, branch_name, sync, origin):
4063         """ Create a commit matching the parent of the shelved changelist 'change'
4064         """
4065         parent_description = p4_describe(change, shelved=True)
4066         parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4067         files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4068
4069         parent_files = []
4070         for f in files:
4071             # if it was added in the shelved changelist, it won't exist in the parent
4072             if f['action'] in self.add_actions:
4073                 continue
4074
4075             # if it was deleted in the shelved changelist it must not be deleted
4076             # in the parent - we might even need to create it if the origin branch
4077             # does not have it
4078             if f['action'] in self.delete_actions:
4079                 f['action'] = 'add'
4080
4081             parent_files.append(f)
4082
4083         sync.commit(parent_description, parent_files, branch_name,
4084                 parent=origin, allow_empty=True)
4085         print("created parent commit for {0} based on {1} in {2}".format(
4086             change, self.origin, branch_name))
4087
4088     def run(self, args):
4089         if len(args) != 1:
4090             return False
4091
4092         if not gitBranchExists(self.origin):
4093             sys.exit("origin branch {0} does not exist".format(self.origin))
4094
4095         sync = P4Sync()
4096         changes = args
4097
4098         # only one change at a time
4099         change = changes[0]
4100
4101         # if the target branch already exists, rename it
4102         branch_name = "{0}/{1}".format(self.destbranch, change)
4103         if gitBranchExists(branch_name):
4104             self.renameBranch(branch_name)
4105         sync.branch = branch_name
4106
4107         sync.verbose = self.verbose
4108         sync.suppress_meta_comment = True
4109
4110         settings = self.findLastP4Revision(self.origin)
4111         sync.depotPaths = settings['depot-paths']
4112         sync.branchPrefixes = sync.depotPaths
4113
4114         sync.openStreams()
4115         sync.loadUserMapFromCache()
4116         sync.silent = True
4117
4118         # create a commit for the parent of the shelved changelist
4119         self.createShelveParent(change, branch_name, sync, self.origin)
4120
4121         # create the commit for the shelved changelist itself
4122         description = p4_describe(change, True)
4123         files = sync.extractFilesFromCommit(description, True, change)
4124
4125         sync.commit(description, files, branch_name, "")
4126         sync.closeStreams()
4127
4128         print("unshelved changelist {0} into {1}".format(change, branch_name))
4129
4130         return True
4131
4132 class P4Branches(Command):
4133     def __init__(self):
4134         Command.__init__(self)
4135         self.options = [ ]
4136         self.description = ("Shows the git branches that hold imports and their "
4137                             + "corresponding perforce depot paths")
4138         self.verbose = False
4139
4140     def run(self, args):
4141         if originP4BranchesExist():
4142             createOrUpdateBranchesFromOrigin()
4143
4144         cmdline = "git rev-parse --symbolic "
4145         cmdline += " --remotes"
4146
4147         for line in read_pipe_lines(cmdline):
4148             line = line.strip()
4149
4150             if not line.startswith('p4/') or line == "p4/HEAD":
4151                 continue
4152             branch = line
4153
4154             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4155             settings = extractSettingsGitLog(log)
4156
4157             print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
4158         return True
4159
4160 class HelpFormatter(optparse.IndentedHelpFormatter):
4161     def __init__(self):
4162         optparse.IndentedHelpFormatter.__init__(self)
4163
4164     def format_description(self, description):
4165         if description:
4166             return description + "\n"
4167         else:
4168             return ""
4169
4170 def printUsage(commands):
4171     print("usage: %s <command> [options]" % sys.argv[0])
4172     print("")
4173     print("valid commands: %s" % ", ".join(commands))
4174     print("")
4175     print("Try %s <command> --help for command specific help." % sys.argv[0])
4176     print("")
4177
4178 commands = {
4179     "debug" : P4Debug,
4180     "submit" : P4Submit,
4181     "commit" : P4Submit,
4182     "sync" : P4Sync,
4183     "rebase" : P4Rebase,
4184     "clone" : P4Clone,
4185     "rollback" : P4RollBack,
4186     "branches" : P4Branches,
4187     "unshelve" : P4Unshelve,
4188 }
4189
4190 def main():
4191     if len(sys.argv[1:]) == 0:
4192         printUsage(commands.keys())
4193         sys.exit(2)
4194
4195     cmdName = sys.argv[1]
4196     try:
4197         klass = commands[cmdName]
4198         cmd = klass()
4199     except KeyError:
4200         print("unknown command %s" % cmdName)
4201         print("")
4202         printUsage(commands.keys())
4203         sys.exit(2)
4204
4205     options = cmd.options
4206     cmd.gitdir = os.environ.get("GIT_DIR", None)
4207
4208     args = sys.argv[2:]
4209
4210     options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
4211     if cmd.needsGit:
4212         options.append(optparse.make_option("--git-dir", dest="gitdir"))
4213
4214     parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4215                                    options,
4216                                    description = cmd.description,
4217                                    formatter = HelpFormatter())
4218
4219     try:
4220         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
4221     except:
4222         parser.print_help()
4223         raise
4224
4225     global verbose
4226     verbose = cmd.verbose
4227     if cmd.needsGit:
4228         if cmd.gitdir == None:
4229             cmd.gitdir = os.path.abspath(".git")
4230             if not isValidGitDir(cmd.gitdir):
4231                 # "rev-parse --git-dir" without arguments will try $PWD/.git
4232                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
4233                 if os.path.exists(cmd.gitdir):
4234                     cdup = read_pipe("git rev-parse --show-cdup").strip()
4235                     if len(cdup) > 0:
4236                         chdir(cdup);
4237
4238         if not isValidGitDir(cmd.gitdir):
4239             if isValidGitDir(cmd.gitdir + "/.git"):
4240                 cmd.gitdir += "/.git"
4241             else:
4242                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
4243
4244         # so git commands invoked from the P4 workspace will succeed
4245         os.environ["GIT_DIR"] = cmd.gitdir
4246
4247     if not cmd.run(args):
4248         parser.print_help()
4249         sys.exit(2)
4250
4251
4252 if __name__ == '__main__':
4253     main()