git-p4: add p4-pre-submit exit text
[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         try:
2409             if not run_git_hook("p4-pre-submit"):
2410                 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip " \
2411                     "this pre-submission check by adding\nthe command line option '--no-verify', " \
2412                     "however,\nthis will also skip the p4-changelist hook as well.")
2413                 sys.exit(1)
2414         except Exception as e:
2415             print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "\
2416                 "with the error '{0}'".format(e.message) )
2417             sys.exit(1)
2418
2419         #
2420         # Apply the commits, one at a time.  On failure, ask if should
2421         # continue to try the rest of the patches, or quit.
2422         #
2423         if self.dry_run:
2424             print("Would apply")
2425         applied = []
2426         last = len(commits) - 1
2427         for i, commit in enumerate(commits):
2428             if self.dry_run:
2429                 print(" ", read_pipe(["git", "show", "-s",
2430                                       "--format=format:%h %s", commit]))
2431                 ok = True
2432             else:
2433                 ok = self.applyCommit(commit)
2434             if ok:
2435                 applied.append(commit)
2436             else:
2437                 if self.prepare_p4_only and i < last:
2438                     print("Processing only the first commit due to option" \
2439                           " --prepare-p4-only")
2440                     break
2441                 if i < last:
2442                     # prompt for what to do, or use the option/variable
2443                     if self.conflict_behavior == "ask":
2444                         print("What do you want to do?")
2445                         response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2446                     elif self.conflict_behavior == "skip":
2447                         response = "s"
2448                     elif self.conflict_behavior == "quit":
2449                         response = "q"
2450                     else:
2451                         die("Unknown conflict_behavior '%s'" %
2452                             self.conflict_behavior)
2453
2454                     if response == "s":
2455                         print("Skipping this commit, but applying the rest")
2456                     if response == "q":
2457                         print("Quitting")
2458                         break
2459
2460         chdir(self.oldWorkingDirectory)
2461         shelved_applied = "shelved" if self.shelve else "applied"
2462         if self.dry_run:
2463             pass
2464         elif self.prepare_p4_only:
2465             pass
2466         elif len(commits) == len(applied):
2467             print("All commits {0}!".format(shelved_applied))
2468
2469             sync = P4Sync()
2470             if self.branch:
2471                 sync.branch = self.branch
2472             if self.disable_p4sync:
2473                 sync.sync_origin_only()
2474             else:
2475                 sync.run([])
2476
2477                 if not self.disable_rebase:
2478                     rebase = P4Rebase()
2479                     rebase.rebase()
2480
2481         else:
2482             if len(applied) == 0:
2483                 print("No commits {0}.".format(shelved_applied))
2484             else:
2485                 print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
2486                 for c in commits:
2487                     if c in applied:
2488                         star = "*"
2489                     else:
2490                         star = " "
2491                     print(star, read_pipe(["git", "show", "-s",
2492                                            "--format=format:%h %s",  c]))
2493                 print("You will have to do 'git p4 sync' and rebase.")
2494
2495         if gitConfigBool("git-p4.exportLabels"):
2496             self.exportLabels = True
2497
2498         if self.exportLabels:
2499             p4Labels = getP4Labels(self.depotPath)
2500             gitTags = getGitTags()
2501
2502             missingGitTags = gitTags - p4Labels
2503             self.exportGitTags(missingGitTags)
2504
2505         # exit with error unless everything applied perfectly
2506         if len(commits) != len(applied):
2507                 sys.exit(1)
2508
2509         return True
2510
2511 class View(object):
2512     """Represent a p4 view ("p4 help views"), and map files in a
2513        repo according to the view."""
2514
2515     def __init__(self, client_name):
2516         self.mappings = []
2517         self.client_prefix = "//%s/" % client_name
2518         # cache results of "p4 where" to lookup client file locations
2519         self.client_spec_path_cache = {}
2520
2521     def append(self, view_line):
2522         """Parse a view line, splitting it into depot and client
2523            sides.  Append to self.mappings, preserving order.  This
2524            is only needed for tag creation."""
2525
2526         # Split the view line into exactly two words.  P4 enforces
2527         # structure on these lines that simplifies this quite a bit.
2528         #
2529         # Either or both words may be double-quoted.
2530         # Single quotes do not matter.
2531         # Double-quote marks cannot occur inside the words.
2532         # A + or - prefix is also inside the quotes.
2533         # There are no quotes unless they contain a space.
2534         # The line is already white-space stripped.
2535         # The two words are separated by a single space.
2536         #
2537         if view_line[0] == '"':
2538             # First word is double quoted.  Find its end.
2539             close_quote_index = view_line.find('"', 1)
2540             if close_quote_index <= 0:
2541                 die("No first-word closing quote found: %s" % view_line)
2542             depot_side = view_line[1:close_quote_index]
2543             # skip closing quote and space
2544             rhs_index = close_quote_index + 1 + 1
2545         else:
2546             space_index = view_line.find(" ")
2547             if space_index <= 0:
2548                 die("No word-splitting space found: %s" % view_line)
2549             depot_side = view_line[0:space_index]
2550             rhs_index = space_index + 1
2551
2552         # prefix + means overlay on previous mapping
2553         if depot_side.startswith("+"):
2554             depot_side = depot_side[1:]
2555
2556         # prefix - means exclude this path, leave out of mappings
2557         exclude = False
2558         if depot_side.startswith("-"):
2559             exclude = True
2560             depot_side = depot_side[1:]
2561
2562         if not exclude:
2563             self.mappings.append(depot_side)
2564
2565     def convert_client_path(self, clientFile):
2566         # chop off //client/ part to make it relative
2567         if not clientFile.startswith(self.client_prefix):
2568             die("No prefix '%s' on clientFile '%s'" %
2569                 (self.client_prefix, clientFile))
2570         return clientFile[len(self.client_prefix):]
2571
2572     def update_client_spec_path_cache(self, files):
2573         """ Caching file paths by "p4 where" batch query """
2574
2575         # List depot file paths exclude that already cached
2576         fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
2577
2578         if len(fileArgs) == 0:
2579             return  # All files in cache
2580
2581         where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2582         for res in where_result:
2583             if "code" in res and res["code"] == "error":
2584                 # assume error is "... file(s) not in client view"
2585                 continue
2586             if "clientFile" not in res:
2587                 die("No clientFile in 'p4 where' output")
2588             if "unmap" in res:
2589                 # it will list all of them, but only one not unmap-ped
2590                 continue
2591             if gitConfigBool("core.ignorecase"):
2592                 res['depotFile'] = res['depotFile'].lower()
2593             self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
2594
2595         # not found files or unmap files set to ""
2596         for depotFile in fileArgs:
2597             if gitConfigBool("core.ignorecase"):
2598                 depotFile = depotFile.lower()
2599             if depotFile not in self.client_spec_path_cache:
2600                 self.client_spec_path_cache[depotFile] = ""
2601
2602     def map_in_client(self, depot_path):
2603         """Return the relative location in the client where this
2604            depot file should live.  Returns "" if the file should
2605            not be mapped in the client."""
2606
2607         if gitConfigBool("core.ignorecase"):
2608             depot_path = depot_path.lower()
2609
2610         if depot_path in self.client_spec_path_cache:
2611             return self.client_spec_path_cache[depot_path]
2612
2613         die( "Error: %s is not found in client spec path" % depot_path )
2614         return ""
2615
2616 def cloneExcludeCallback(option, opt_str, value, parser):
2617     # prepend "/" because the first "/" was consumed as part of the option itself.
2618     # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2619     parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2620
2621 class P4Sync(Command, P4UserMap):
2622
2623     def __init__(self):
2624         Command.__init__(self)
2625         P4UserMap.__init__(self)
2626         self.options = [
2627                 optparse.make_option("--branch", dest="branch"),
2628                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2629                 optparse.make_option("--changesfile", dest="changesFile"),
2630                 optparse.make_option("--silent", dest="silent", action="store_true"),
2631                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2632                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2633                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2634                                      help="Import into refs/heads/ , not refs/remotes"),
2635                 optparse.make_option("--max-changes", dest="maxChanges",
2636                                      help="Maximum number of changes to import"),
2637                 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2638                                      help="Internal block size to use when iteratively calling p4 changes"),
2639                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2640                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2641                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2642                                      help="Only sync files that are included in the Perforce Client Spec"),
2643                 optparse.make_option("-/", dest="cloneExclude",
2644                                      action="callback", callback=cloneExcludeCallback, type="string",
2645                                      help="exclude depot path"),
2646         ]
2647         self.description = """Imports from Perforce into a git repository.\n
2648     example:
2649     //depot/my/project/ -- to import the current head
2650     //depot/my/project/@all -- to import everything
2651     //depot/my/project/@1,6 -- to import only from revision 1 to 6
2652
2653     (a ... is not needed in the path p4 specification, it's added implicitly)"""
2654
2655         self.usage += " //depot/path[@revRange]"
2656         self.silent = False
2657         self.createdBranches = set()
2658         self.committedChanges = set()
2659         self.branch = ""
2660         self.detectBranches = False
2661         self.detectLabels = False
2662         self.importLabels = False
2663         self.changesFile = ""
2664         self.syncWithOrigin = True
2665         self.importIntoRemotes = True
2666         self.maxChanges = ""
2667         self.changes_block_size = None
2668         self.keepRepoPath = False
2669         self.depotPaths = None
2670         self.p4BranchesInGit = []
2671         self.cloneExclude = []
2672         self.useClientSpec = False
2673         self.useClientSpec_from_options = False
2674         self.clientSpecDirs = None
2675         self.tempBranches = []
2676         self.tempBranchLocation = "refs/git-p4-tmp"
2677         self.largeFileSystem = None
2678         self.suppress_meta_comment = False
2679
2680         if gitConfig('git-p4.largeFileSystem'):
2681             largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2682             self.largeFileSystem = largeFileSystemConstructor(
2683                 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2684             )
2685
2686         if gitConfig("git-p4.syncFromOrigin") == "false":
2687             self.syncWithOrigin = False
2688
2689         self.depotPaths = []
2690         self.changeRange = ""
2691         self.previousDepotPaths = []
2692         self.hasOrigin = False
2693
2694         # map from branch depot path to parent branch
2695         self.knownBranches = {}
2696         self.initialParents = {}
2697
2698         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2699         self.labels = {}
2700
2701     # Force a checkpoint in fast-import and wait for it to finish
2702     def checkpoint(self):
2703         self.gitStream.write("checkpoint\n\n")
2704         self.gitStream.write("progress checkpoint\n\n")
2705         out = self.gitOutput.readline()
2706         if self.verbose:
2707             print("checkpoint finished: " + out)
2708
2709     def isPathWanted(self, path):
2710         for p in self.cloneExclude:
2711             if p.endswith("/"):
2712                 if p4PathStartsWith(path, p):
2713                     return False
2714             # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2715             elif path.lower() == p.lower():
2716                 return False
2717         for p in self.depotPaths:
2718             if p4PathStartsWith(path, p):
2719                 return True
2720         return False
2721
2722     def extractFilesFromCommit(self, commit, shelved=False, shelved_cl = 0):
2723         files = []
2724         fnum = 0
2725         while "depotFile%s" % fnum in commit:
2726             path =  commit["depotFile%s" % fnum]
2727             found = self.isPathWanted(path)
2728             if not found:
2729                 fnum = fnum + 1
2730                 continue
2731
2732             file = {}
2733             file["path"] = path
2734             file["rev"] = commit["rev%s" % fnum]
2735             file["action"] = commit["action%s" % fnum]
2736             file["type"] = commit["type%s" % fnum]
2737             if shelved:
2738                 file["shelved_cl"] = int(shelved_cl)
2739             files.append(file)
2740             fnum = fnum + 1
2741         return files
2742
2743     def extractJobsFromCommit(self, commit):
2744         jobs = []
2745         jnum = 0
2746         while "job%s" % jnum in commit:
2747             job = commit["job%s" % jnum]
2748             jobs.append(job)
2749             jnum = jnum + 1
2750         return jobs
2751
2752     def stripRepoPath(self, path, prefixes):
2753         """When streaming files, this is called to map a p4 depot path
2754            to where it should go in git.  The prefixes are either
2755            self.depotPaths, or self.branchPrefixes in the case of
2756            branch detection."""
2757
2758         if self.useClientSpec:
2759             # branch detection moves files up a level (the branch name)
2760             # from what client spec interpretation gives
2761             path = self.clientSpecDirs.map_in_client(path)
2762             if self.detectBranches:
2763                 for b in self.knownBranches:
2764                     if p4PathStartsWith(path, b + "/"):
2765                         path = path[len(b)+1:]
2766
2767         elif self.keepRepoPath:
2768             # Preserve everything in relative path name except leading
2769             # //depot/; just look at first prefix as they all should
2770             # be in the same depot.
2771             depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2772             if p4PathStartsWith(path, depot):
2773                 path = path[len(depot):]
2774
2775         else:
2776             for p in prefixes:
2777                 if p4PathStartsWith(path, p):
2778                     path = path[len(p):]
2779                     break
2780
2781         path = wildcard_decode(path)
2782         return path
2783
2784     def splitFilesIntoBranches(self, commit):
2785         """Look at each depotFile in the commit to figure out to what
2786            branch it belongs."""
2787
2788         if self.clientSpecDirs:
2789             files = self.extractFilesFromCommit(commit)
2790             self.clientSpecDirs.update_client_spec_path_cache(files)
2791
2792         branches = {}
2793         fnum = 0
2794         while "depotFile%s" % fnum in commit:
2795             path =  commit["depotFile%s" % fnum]
2796             found = self.isPathWanted(path)
2797             if not found:
2798                 fnum = fnum + 1
2799                 continue
2800
2801             file = {}
2802             file["path"] = path
2803             file["rev"] = commit["rev%s" % fnum]
2804             file["action"] = commit["action%s" % fnum]
2805             file["type"] = commit["type%s" % fnum]
2806             fnum = fnum + 1
2807
2808             # start with the full relative path where this file would
2809             # go in a p4 client
2810             if self.useClientSpec:
2811                 relPath = self.clientSpecDirs.map_in_client(path)
2812             else:
2813                 relPath = self.stripRepoPath(path, self.depotPaths)
2814
2815             for branch in self.knownBranches.keys():
2816                 # add a trailing slash so that a commit into qt/4.2foo
2817                 # doesn't end up in qt/4.2, e.g.
2818                 if p4PathStartsWith(relPath, branch + "/"):
2819                     if branch not in branches:
2820                         branches[branch] = []
2821                     branches[branch].append(file)
2822                     break
2823
2824         return branches
2825
2826     def writeToGitStream(self, gitMode, relPath, contents):
2827         self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
2828         self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2829         for d in contents:
2830             self.gitStream.write(d)
2831         self.gitStream.write('\n')
2832
2833     def encodeWithUTF8(self, path):
2834         try:
2835             path.decode('ascii')
2836         except:
2837             encoding = 'utf8'
2838             if gitConfig('git-p4.pathEncoding'):
2839                 encoding = gitConfig('git-p4.pathEncoding')
2840             path = path.decode(encoding, 'replace').encode('utf8', 'replace')
2841             if self.verbose:
2842                 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
2843         return path
2844
2845     # output one file from the P4 stream
2846     # - helper for streamP4Files
2847
2848     def streamOneP4File(self, file, contents):
2849         relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2850         relPath = self.encodeWithUTF8(relPath)
2851         if verbose:
2852             if 'fileSize' in self.stream_file:
2853                 size = int(self.stream_file['fileSize'])
2854             else:
2855                 size = 0 # deleted files don't get a fileSize apparently
2856             sys.stdout.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath, size/1024/1024))
2857             sys.stdout.flush()
2858
2859         (type_base, type_mods) = split_p4_type(file["type"])
2860
2861         git_mode = "100644"
2862         if "x" in type_mods:
2863             git_mode = "100755"
2864         if type_base == "symlink":
2865             git_mode = "120000"
2866             # p4 print on a symlink sometimes contains "target\n";
2867             # if it does, remove the newline
2868             data = ''.join(contents)
2869             if not data:
2870                 # Some version of p4 allowed creating a symlink that pointed
2871                 # to nothing.  This causes p4 errors when checking out such
2872                 # a change, and errors here too.  Work around it by ignoring
2873                 # the bad symlink; hopefully a future change fixes it.
2874                 print("\nIgnoring empty symlink in %s" % file['depotFile'])
2875                 return
2876             elif data[-1] == '\n':
2877                 contents = [data[:-1]]
2878             else:
2879                 contents = [data]
2880
2881         if type_base == "utf16":
2882             # p4 delivers different text in the python output to -G
2883             # than it does when using "print -o", or normal p4 client
2884             # operations.  utf16 is converted to ascii or utf8, perhaps.
2885             # But ascii text saved as -t utf16 is completely mangled.
2886             # Invoke print -o to get the real contents.
2887             #
2888             # On windows, the newlines will always be mangled by print, so put
2889             # them back too.  This is not needed to the cygwin windows version,
2890             # just the native "NT" type.
2891             #
2892             try:
2893                 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2894             except Exception as e:
2895                 if 'Translation of file content failed' in str(e):
2896                     type_base = 'binary'
2897                 else:
2898                     raise e
2899             else:
2900                 if p4_version_string().find('/NT') >= 0:
2901                     text = text.replace('\r\n', '\n')
2902                 contents = [ text ]
2903
2904         if type_base == "apple":
2905             # Apple filetype files will be streamed as a concatenation of
2906             # its appledouble header and the contents.  This is useless
2907             # on both macs and non-macs.  If using "print -q -o xx", it
2908             # will create "xx" with the data, and "%xx" with the header.
2909             # This is also not very useful.
2910             #
2911             # Ideally, someday, this script can learn how to generate
2912             # appledouble files directly and import those to git, but
2913             # non-mac machines can never find a use for apple filetype.
2914             print("\nIgnoring apple filetype file %s" % file['depotFile'])
2915             return
2916
2917         # Note that we do not try to de-mangle keywords on utf16 files,
2918         # even though in theory somebody may want that.
2919         pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2920         if pattern:
2921             regexp = re.compile(pattern, re.VERBOSE)
2922             text = ''.join(contents)
2923             text = regexp.sub(r'$\1$', text)
2924             contents = [ text ]
2925
2926         if self.largeFileSystem:
2927             (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
2928
2929         self.writeToGitStream(git_mode, relPath, contents)
2930
2931     def streamOneP4Deletion(self, file):
2932         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2933         relPath = self.encodeWithUTF8(relPath)
2934         if verbose:
2935             sys.stdout.write("delete %s\n" % relPath)
2936             sys.stdout.flush()
2937         self.gitStream.write("D %s\n" % relPath)
2938
2939         if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2940             self.largeFileSystem.removeLargeFile(relPath)
2941
2942     # handle another chunk of streaming data
2943     def streamP4FilesCb(self, marshalled):
2944
2945         # catch p4 errors and complain
2946         err = None
2947         if "code" in marshalled:
2948             if marshalled["code"] == "error":
2949                 if "data" in marshalled:
2950                     err = marshalled["data"].rstrip()
2951
2952         if not err and 'fileSize' in self.stream_file:
2953             required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
2954             if required_bytes > 0:
2955                 err = 'Not enough space left on %s! Free at least %i MB.' % (
2956                     os.getcwd(), required_bytes/1024/1024
2957                 )
2958
2959         if err:
2960             f = None
2961             if self.stream_have_file_info:
2962                 if "depotFile" in self.stream_file:
2963                     f = self.stream_file["depotFile"]
2964             # force a failure in fast-import, else an empty
2965             # commit will be made
2966             self.gitStream.write("\n")
2967             self.gitStream.write("die-now\n")
2968             self.gitStream.close()
2969             # ignore errors, but make sure it exits first
2970             self.importProcess.wait()
2971             if f:
2972                 die("Error from p4 print for %s: %s" % (f, err))
2973             else:
2974                 die("Error from p4 print: %s" % err)
2975
2976         if 'depotFile' in marshalled and self.stream_have_file_info:
2977             # start of a new file - output the old one first
2978             self.streamOneP4File(self.stream_file, self.stream_contents)
2979             self.stream_file = {}
2980             self.stream_contents = []
2981             self.stream_have_file_info = False
2982
2983         # pick up the new file information... for the
2984         # 'data' field we need to append to our array
2985         for k in marshalled.keys():
2986             if k == 'data':
2987                 if 'streamContentSize' not in self.stream_file:
2988                     self.stream_file['streamContentSize'] = 0
2989                 self.stream_file['streamContentSize'] += len(marshalled['data'])
2990                 self.stream_contents.append(marshalled['data'])
2991             else:
2992                 self.stream_file[k] = marshalled[k]
2993
2994         if (verbose and
2995             'streamContentSize' in self.stream_file and
2996             'fileSize' in self.stream_file and
2997             'depotFile' in self.stream_file):
2998             size = int(self.stream_file["fileSize"])
2999             if size > 0:
3000                 progress = 100*self.stream_file['streamContentSize']/size
3001                 sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
3002                 sys.stdout.flush()
3003
3004         self.stream_have_file_info = True
3005
3006     # Stream directly from "p4 files" into "git fast-import"
3007     def streamP4Files(self, files):
3008         filesForCommit = []
3009         filesToRead = []
3010         filesToDelete = []
3011
3012         for f in files:
3013             filesForCommit.append(f)
3014             if f['action'] in self.delete_actions:
3015                 filesToDelete.append(f)
3016             else:
3017                 filesToRead.append(f)
3018
3019         # deleted files...
3020         for f in filesToDelete:
3021             self.streamOneP4Deletion(f)
3022
3023         if len(filesToRead) > 0:
3024             self.stream_file = {}
3025             self.stream_contents = []
3026             self.stream_have_file_info = False
3027
3028             # curry self argument
3029             def streamP4FilesCbSelf(entry):
3030                 self.streamP4FilesCb(entry)
3031
3032             fileArgs = []
3033             for f in filesToRead:
3034                 if 'shelved_cl' in f:
3035                     # Handle shelved CLs using the "p4 print file@=N" syntax to print
3036                     # the contents
3037                     fileArg = '%s@=%d' % (f['path'], f['shelved_cl'])
3038                 else:
3039                     fileArg = '%s#%s' % (f['path'], f['rev'])
3040
3041                 fileArgs.append(fileArg)
3042
3043             p4CmdList(["-x", "-", "print"],
3044                       stdin=fileArgs,
3045                       cb=streamP4FilesCbSelf)
3046
3047             # do the last chunk
3048             if 'depotFile' in self.stream_file:
3049                 self.streamOneP4File(self.stream_file, self.stream_contents)
3050
3051     def make_email(self, userid):
3052         if userid in self.users:
3053             return self.users[userid]
3054         else:
3055             return "%s <a@b>" % userid
3056
3057     def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
3058         """ Stream a p4 tag.
3059         commit is either a git commit, or a fast-import mark, ":<p4commit>"
3060         """
3061
3062         if verbose:
3063             print("writing tag %s for commit %s" % (labelName, commit))
3064         gitStream.write("tag %s\n" % labelName)
3065         gitStream.write("from %s\n" % commit)
3066
3067         if 'Owner' in labelDetails:
3068             owner = labelDetails["Owner"]
3069         else:
3070             owner = None
3071
3072         # Try to use the owner of the p4 label, or failing that,
3073         # the current p4 user id.
3074         if owner:
3075             email = self.make_email(owner)
3076         else:
3077             email = self.make_email(self.p4UserId())
3078         tagger = "%s %s %s" % (email, epoch, self.tz)
3079
3080         gitStream.write("tagger %s\n" % tagger)
3081
3082         print("labelDetails=",labelDetails)
3083         if 'Description' in labelDetails:
3084             description = labelDetails['Description']
3085         else:
3086             description = 'Label from git p4'
3087
3088         gitStream.write("data %d\n" % len(description))
3089         gitStream.write(description)
3090         gitStream.write("\n")
3091
3092     def inClientSpec(self, path):
3093         if not self.clientSpecDirs:
3094             return True
3095         inClientSpec = self.clientSpecDirs.map_in_client(path)
3096         if not inClientSpec and self.verbose:
3097             print('Ignoring file outside of client spec: {0}'.format(path))
3098         return inClientSpec
3099
3100     def hasBranchPrefix(self, path):
3101         if not self.branchPrefixes:
3102             return True
3103         hasPrefix = [p for p in self.branchPrefixes
3104                         if p4PathStartsWith(path, p)]
3105         if not hasPrefix and self.verbose:
3106             print('Ignoring file outside of prefix: {0}'.format(path))
3107         return hasPrefix
3108
3109     def commit(self, details, files, branch, parent = "", allow_empty=False):
3110         epoch = details["time"]
3111         author = details["user"]
3112         jobs = self.extractJobsFromCommit(details)
3113
3114         if self.verbose:
3115             print('commit into {0}'.format(branch))
3116
3117         if self.clientSpecDirs:
3118             self.clientSpecDirs.update_client_spec_path_cache(files)
3119
3120         files = [f for f in files
3121             if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]
3122
3123         if gitConfigBool('git-p4.keepEmptyCommits'):
3124             allow_empty = True
3125
3126         if not files and not allow_empty:
3127             print('Ignoring revision {0} as it would produce an empty commit.'
3128                 .format(details['change']))
3129             return
3130
3131         self.gitStream.write("commit %s\n" % branch)
3132         self.gitStream.write("mark :%s\n" % details["change"])
3133         self.committedChanges.add(int(details["change"]))
3134         committer = ""
3135         if author not in self.users:
3136             self.getUserMapFromPerforceServer()
3137         committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
3138
3139         self.gitStream.write("committer %s\n" % committer)
3140
3141         self.gitStream.write("data <<EOT\n")
3142         self.gitStream.write(details["desc"])
3143         if len(jobs) > 0:
3144             self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
3145
3146         if not self.suppress_meta_comment:
3147             self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3148                                 (','.join(self.branchPrefixes), details["change"]))
3149             if len(details['options']) > 0:
3150                 self.gitStream.write(": options = %s" % details['options'])
3151             self.gitStream.write("]\n")
3152
3153         self.gitStream.write("EOT\n\n")
3154
3155         if len(parent) > 0:
3156             if self.verbose:
3157                 print("parent %s" % parent)
3158             self.gitStream.write("from %s\n" % parent)
3159
3160         self.streamP4Files(files)
3161         self.gitStream.write("\n")
3162
3163         change = int(details["change"])
3164
3165         if change in self.labels:
3166             label = self.labels[change]
3167             labelDetails = label[0]
3168             labelRevisions = label[1]
3169             if self.verbose:
3170                 print("Change %s is labelled %s" % (change, labelDetails))
3171
3172             files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
3173                                                 for p in self.branchPrefixes])
3174
3175             if len(files) == len(labelRevisions):
3176
3177                 cleanedFiles = {}
3178                 for info in files:
3179                     if info["action"] in self.delete_actions:
3180                         continue
3181                     cleanedFiles[info["depotFile"]] = info["rev"]
3182
3183                 if cleanedFiles == labelRevisions:
3184                     self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
3185
3186                 else:
3187                     if not self.silent:
3188                         print("Tag %s does not match with change %s: files do not match."
3189                                % (labelDetails["label"], change))
3190
3191             else:
3192                 if not self.silent:
3193                     print("Tag %s does not match with change %s: file count is different."
3194                            % (labelDetails["label"], change))
3195
3196     # Build a dictionary of changelists and labels, for "detect-labels" option.
3197     def getLabels(self):
3198         self.labels = {}
3199
3200         l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
3201         if len(l) > 0 and not self.silent:
3202             print("Finding files belonging to labels in %s" % self.depotPaths)
3203
3204         for output in l:
3205             label = output["label"]
3206             revisions = {}
3207             newestChange = 0
3208             if self.verbose:
3209                 print("Querying files for label %s" % label)
3210             for file in p4CmdList(["files"] +
3211                                       ["%s...@%s" % (p, label)
3212                                           for p in self.depotPaths]):
3213                 revisions[file["depotFile"]] = file["rev"]
3214                 change = int(file["change"])
3215                 if change > newestChange:
3216                     newestChange = change
3217
3218             self.labels[newestChange] = [output, revisions]
3219
3220         if self.verbose:
3221             print("Label changes: %s" % self.labels.keys())
3222
3223     # Import p4 labels as git tags. A direct mapping does not
3224     # exist, so assume that if all the files are at the same revision
3225     # then we can use that, or it's something more complicated we should
3226     # just ignore.
3227     def importP4Labels(self, stream, p4Labels):
3228         if verbose:
3229             print("import p4 labels: " + ' '.join(p4Labels))
3230
3231         ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
3232         validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
3233         if len(validLabelRegexp) == 0:
3234             validLabelRegexp = defaultLabelRegexp
3235         m = re.compile(validLabelRegexp)
3236
3237         for name in p4Labels:
3238             commitFound = False
3239
3240             if not m.match(name):
3241                 if verbose:
3242                     print("label %s does not match regexp %s" % (name,validLabelRegexp))
3243                 continue
3244
3245             if name in ignoredP4Labels:
3246                 continue
3247
3248             labelDetails = p4CmdList(['label', "-o", name])[0]
3249
3250             # get the most recent changelist for each file in this label
3251             change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3252                                 for p in self.depotPaths])
3253
3254             if 'change' in change:
3255                 # find the corresponding git commit; take the oldest commit
3256                 changelist = int(change['change'])
3257                 if changelist in self.committedChanges:
3258                     gitCommit = ":%d" % changelist       # use a fast-import mark
3259                     commitFound = True
3260                 else:
3261                     gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3262                         "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3263                     if len(gitCommit) == 0:
3264                         print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
3265                     else:
3266                         commitFound = True
3267                         gitCommit = gitCommit.strip()
3268
3269                 if commitFound:
3270                     # Convert from p4 time format
3271                     try:
3272                         tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3273                     except ValueError:
3274                         print("Could not convert label time %s" % labelDetails['Update'])
3275                         tmwhen = 1
3276
3277                     when = int(time.mktime(tmwhen))
3278                     self.streamTag(stream, name, labelDetails, gitCommit, when)
3279                     if verbose:
3280                         print("p4 label %s mapped to git commit %s" % (name, gitCommit))
3281             else:
3282                 if verbose:
3283                     print("Label %s has no changelists - possibly deleted?" % name)
3284
3285             if not commitFound:
3286                 # We can't import this label; don't try again as it will get very
3287                 # expensive repeatedly fetching all the files for labels that will
3288                 # never be imported. If the label is moved in the future, the
3289                 # ignore will need to be removed manually.
3290                 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3291
3292     def guessProjectName(self):
3293         for p in self.depotPaths:
3294             if p.endswith("/"):
3295                 p = p[:-1]
3296             p = p[p.strip().rfind("/") + 1:]
3297             if not p.endswith("/"):
3298                p += "/"
3299             return p
3300
3301     def getBranchMapping(self):
3302         lostAndFoundBranches = set()
3303
3304         user = gitConfig("git-p4.branchUser")
3305         if len(user) > 0:
3306             command = "branches -u %s" % user
3307         else:
3308             command = "branches"
3309
3310         for info in p4CmdList(command):
3311             details = p4Cmd(["branch", "-o", info["branch"]])
3312             viewIdx = 0
3313             while "View%s" % viewIdx in details:
3314                 paths = details["View%s" % viewIdx].split(" ")
3315                 viewIdx = viewIdx + 1
3316                 # require standard //depot/foo/... //depot/bar/... mapping
3317                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3318                     continue
3319                 source = paths[0]
3320                 destination = paths[1]
3321                 ## HACK
3322                 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
3323                     source = source[len(self.depotPaths[0]):-4]
3324                     destination = destination[len(self.depotPaths[0]):-4]
3325
3326                     if destination in self.knownBranches:
3327                         if not self.silent:
3328                             print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3329                             print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
3330                         continue
3331
3332                     self.knownBranches[destination] = source
3333
3334                     lostAndFoundBranches.discard(destination)
3335
3336                     if source not in self.knownBranches:
3337                         lostAndFoundBranches.add(source)
3338
3339         # Perforce does not strictly require branches to be defined, so we also
3340         # check git config for a branch list.
3341         #
3342         # Example of branch definition in git config file:
3343         # [git-p4]
3344         #   branchList=main:branchA
3345         #   branchList=main:branchB
3346         #   branchList=branchA:branchC
3347         configBranches = gitConfigList("git-p4.branchList")
3348         for branch in configBranches:
3349             if branch:
3350                 (source, destination) = branch.split(":")
3351                 self.knownBranches[destination] = source
3352
3353                 lostAndFoundBranches.discard(destination)
3354
3355                 if source not in self.knownBranches:
3356                     lostAndFoundBranches.add(source)
3357
3358
3359         for branch in lostAndFoundBranches:
3360             self.knownBranches[branch] = branch
3361
3362     def getBranchMappingFromGitBranches(self):
3363         branches = p4BranchesInGit(self.importIntoRemotes)
3364         for branch in branches.keys():
3365             if branch == "master":
3366                 branch = "main"
3367             else:
3368                 branch = branch[len(self.projectName):]
3369             self.knownBranches[branch] = branch
3370
3371     def updateOptionDict(self, d):
3372         option_keys = {}
3373         if self.keepRepoPath:
3374             option_keys['keepRepoPath'] = 1
3375
3376         d["options"] = ' '.join(sorted(option_keys.keys()))
3377
3378     def readOptions(self, d):
3379         self.keepRepoPath = ('options' in d
3380                              and ('keepRepoPath' in d['options']))
3381
3382     def gitRefForBranch(self, branch):
3383         if branch == "main":
3384             return self.refPrefix + "master"
3385
3386         if len(branch) <= 0:
3387             return branch
3388
3389         return self.refPrefix + self.projectName + branch
3390
3391     def gitCommitByP4Change(self, ref, change):
3392         if self.verbose:
3393             print("looking in ref " + ref + " for change %s using bisect..." % change)
3394
3395         earliestCommit = ""
3396         latestCommit = parseRevision(ref)
3397
3398         while True:
3399             if self.verbose:
3400                 print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
3401             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
3402             if len(next) == 0:
3403                 if self.verbose:
3404                     print("argh")
3405                 return ""
3406             log = extractLogMessageFromGitCommit(next)
3407             settings = extractSettingsGitLog(log)
3408             currentChange = int(settings['change'])
3409             if self.verbose:
3410                 print("current change %s" % currentChange)
3411
3412             if currentChange == change:
3413                 if self.verbose:
3414                     print("found %s" % next)
3415                 return next
3416
3417             if currentChange < change:
3418                 earliestCommit = "^%s" % next
3419             else:
3420                 if next == latestCommit:
3421                     die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3422                 latestCommit = "%s^@" % next
3423
3424         return ""
3425
3426     def importNewBranch(self, branch, maxChange):
3427         # make fast-import flush all changes to disk and update the refs using the checkpoint
3428         # command so that we can try to find the branch parent in the git history
3429         self.gitStream.write("checkpoint\n\n");
3430         self.gitStream.flush();
3431         branchPrefix = self.depotPaths[0] + branch + "/"
3432         range = "@1,%s" % maxChange
3433         #print "prefix" + branchPrefix
3434         changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3435         if len(changes) <= 0:
3436             return False
3437         firstChange = changes[0]
3438         #print "first change in branch: %s" % firstChange
3439         sourceBranch = self.knownBranches[branch]
3440         sourceDepotPath = self.depotPaths[0] + sourceBranch
3441         sourceRef = self.gitRefForBranch(sourceBranch)
3442         #print "source " + sourceBranch
3443
3444         branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3445         #print "branch parent: %s" % branchParentChange
3446         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3447         if len(gitParent) > 0:
3448             self.initialParents[self.gitRefForBranch(branch)] = gitParent
3449             #print "parent git commit: %s" % gitParent
3450
3451         self.importChanges(changes)
3452         return True
3453
3454     def searchParent(self, parent, branch, target):
3455         parentFound = False
3456         for blob in read_pipe_lines(["git", "rev-list", "--reverse",
3457                                      "--no-merges", parent]):
3458             blob = blob.strip()
3459             if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
3460                 parentFound = True
3461                 if self.verbose:
3462                     print("Found parent of %s in commit %s" % (branch, blob))
3463                 break
3464         if parentFound:
3465             return blob
3466         else:
3467             return None
3468
3469     def importChanges(self, changes, origin_revision=0):
3470         cnt = 1
3471         for change in changes:
3472             description = p4_describe(change)
3473             self.updateOptionDict(description)
3474
3475             if not self.silent:
3476                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
3477                 sys.stdout.flush()
3478             cnt = cnt + 1
3479
3480             try:
3481                 if self.detectBranches:
3482                     branches = self.splitFilesIntoBranches(description)
3483                     for branch in branches.keys():
3484                         ## HACK  --hwn
3485                         branchPrefix = self.depotPaths[0] + branch + "/"
3486                         self.branchPrefixes = [ branchPrefix ]
3487
3488                         parent = ""
3489
3490                         filesForCommit = branches[branch]
3491
3492                         if self.verbose:
3493                             print("branch is %s" % branch)
3494
3495                         self.updatedBranches.add(branch)
3496
3497                         if branch not in self.createdBranches:
3498                             self.createdBranches.add(branch)
3499                             parent = self.knownBranches[branch]
3500                             if parent == branch:
3501                                 parent = ""
3502                             else:
3503                                 fullBranch = self.projectName + branch
3504                                 if fullBranch not in self.p4BranchesInGit:
3505                                     if not self.silent:
3506                                         print("\n    Importing new branch %s" % fullBranch);
3507                                     if self.importNewBranch(branch, change - 1):
3508                                         parent = ""
3509                                         self.p4BranchesInGit.append(fullBranch)
3510                                     if not self.silent:
3511                                         print("\n    Resuming with change %s" % change);
3512
3513                                 if self.verbose:
3514                                     print("parent determined through known branches: %s" % parent)
3515
3516                         branch = self.gitRefForBranch(branch)
3517                         parent = self.gitRefForBranch(parent)
3518
3519                         if self.verbose:
3520                             print("looking for initial parent for %s; current parent is %s" % (branch, parent))
3521
3522                         if len(parent) == 0 and branch in self.initialParents:
3523                             parent = self.initialParents[branch]
3524                             del self.initialParents[branch]
3525
3526                         blob = None
3527                         if len(parent) > 0:
3528                             tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3529                             if self.verbose:
3530                                 print("Creating temporary branch: " + tempBranch)
3531                             self.commit(description, filesForCommit, tempBranch)
3532                             self.tempBranches.append(tempBranch)
3533                             self.checkpoint()
3534                             blob = self.searchParent(parent, branch, tempBranch)
3535                         if blob:
3536                             self.commit(description, filesForCommit, branch, blob)
3537                         else:
3538                             if self.verbose:
3539                                 print("Parent of %s not found. Committing into head of %s" % (branch, parent))
3540                             self.commit(description, filesForCommit, branch, parent)
3541                 else:
3542                     files = self.extractFilesFromCommit(description)
3543                     self.commit(description, files, self.branch,
3544                                 self.initialParent)
3545                     # only needed once, to connect to the previous commit
3546                     self.initialParent = ""
3547             except IOError:
3548                 print(self.gitError.read())
3549                 sys.exit(1)
3550
3551     def sync_origin_only(self):
3552         if self.syncWithOrigin:
3553             self.hasOrigin = originP4BranchesExist()
3554             if self.hasOrigin:
3555                 if not self.silent:
3556                     print('Syncing with origin first, using "git fetch origin"')
3557                 system("git fetch origin")
3558
3559     def importHeadRevision(self, revision):
3560         print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
3561
3562         details = {}
3563         details["user"] = "git perforce import user"
3564         details["desc"] = ("Initial import of %s from the state at revision %s\n"
3565                            % (' '.join(self.depotPaths), revision))
3566         details["change"] = revision
3567         newestRevision = 0
3568
3569         fileCnt = 0
3570         fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3571
3572         for info in p4CmdList(["files"] + fileArgs):
3573
3574             if 'code' in info and info['code'] == 'error':
3575                 sys.stderr.write("p4 returned an error: %s\n"
3576                                  % info['data'])
3577                 if info['data'].find("must refer to client") >= 0:
3578                     sys.stderr.write("This particular p4 error is misleading.\n")
3579                     sys.stderr.write("Perhaps the depot path was misspelled.\n");
3580                     sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
3581                 sys.exit(1)
3582             if 'p4ExitCode' in info:
3583                 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3584                 sys.exit(1)
3585
3586
3587             change = int(info["change"])
3588             if change > newestRevision:
3589                 newestRevision = change
3590
3591             if info["action"] in self.delete_actions:
3592                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3593                 #fileCnt = fileCnt + 1
3594                 continue
3595
3596             for prop in ["depotFile", "rev", "action", "type" ]:
3597                 details["%s%s" % (prop, fileCnt)] = info[prop]
3598
3599             fileCnt = fileCnt + 1
3600
3601         details["change"] = newestRevision
3602
3603         # Use time from top-most change so that all git p4 clones of
3604         # the same p4 repo have the same commit SHA1s.
3605         res = p4_describe(newestRevision)
3606         details["time"] = res["time"]
3607
3608         self.updateOptionDict(details)
3609         try:
3610             self.commit(details, self.extractFilesFromCommit(details), self.branch)
3611         except IOError as err:
3612             print("IO error with git fast-import. Is your git version recent enough?")
3613             print("IO error details: {}".format(err))
3614             print(self.gitError.read())
3615
3616     def openStreams(self):
3617         self.importProcess = subprocess.Popen(["git", "fast-import"],
3618                                               stdin=subprocess.PIPE,
3619                                               stdout=subprocess.PIPE,
3620                                               stderr=subprocess.PIPE);
3621         self.gitOutput = self.importProcess.stdout
3622         self.gitStream = self.importProcess.stdin
3623         self.gitError = self.importProcess.stderr
3624
3625     def closeStreams(self):
3626         self.gitStream.close()
3627         if self.importProcess.wait() != 0:
3628             die("fast-import failed: %s" % self.gitError.read())
3629         self.gitOutput.close()
3630         self.gitError.close()
3631
3632     def run(self, args):
3633         if self.importIntoRemotes:
3634             self.refPrefix = "refs/remotes/p4/"
3635         else:
3636             self.refPrefix = "refs/heads/p4/"
3637
3638         self.sync_origin_only()
3639
3640         branch_arg_given = bool(self.branch)
3641         if len(self.branch) == 0:
3642             self.branch = self.refPrefix + "master"
3643             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
3644                 system("git update-ref %s refs/heads/p4" % self.branch)
3645                 system("git branch -D p4")
3646
3647         # accept either the command-line option, or the configuration variable
3648         if self.useClientSpec:
3649             # will use this after clone to set the variable
3650             self.useClientSpec_from_options = True
3651         else:
3652             if gitConfigBool("git-p4.useclientspec"):
3653                 self.useClientSpec = True
3654         if self.useClientSpec:
3655             self.clientSpecDirs = getClientSpec()
3656
3657         # TODO: should always look at previous commits,
3658         # merge with previous imports, if possible.
3659         if args == []:
3660             if self.hasOrigin:
3661                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
3662
3663             # branches holds mapping from branch name to sha1
3664             branches = p4BranchesInGit(self.importIntoRemotes)
3665
3666             # restrict to just this one, disabling detect-branches
3667             if branch_arg_given:
3668                 short = self.branch.split("/")[-1]
3669                 if short in branches:
3670                     self.p4BranchesInGit = [ short ]
3671             else:
3672                 self.p4BranchesInGit = branches.keys()
3673
3674             if len(self.p4BranchesInGit) > 1:
3675                 if not self.silent:
3676                     print("Importing from/into multiple branches")
3677                 self.detectBranches = True
3678                 for branch in branches.keys():
3679                     self.initialParents[self.refPrefix + branch] = \
3680                         branches[branch]
3681
3682             if self.verbose:
3683                 print("branches: %s" % self.p4BranchesInGit)
3684
3685             p4Change = 0
3686             for branch in self.p4BranchesInGit:
3687                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
3688
3689                 settings = extractSettingsGitLog(logMsg)
3690
3691                 self.readOptions(settings)
3692                 if ('depot-paths' in settings
3693                     and 'change' in settings):
3694                     change = int(settings['change']) + 1
3695                     p4Change = max(p4Change, change)
3696
3697                     depotPaths = sorted(settings['depot-paths'])
3698                     if self.previousDepotPaths == []:
3699                         self.previousDepotPaths = depotPaths
3700                     else:
3701                         paths = []
3702                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
3703                             prev_list = prev.split("/")
3704                             cur_list = cur.split("/")
3705                             for i in range(0, min(len(cur_list), len(prev_list))):
3706                                 if cur_list[i] != prev_list[i]:
3707                                     i = i - 1
3708                                     break
3709
3710                             paths.append ("/".join(cur_list[:i + 1]))
3711
3712                         self.previousDepotPaths = paths
3713
3714             if p4Change > 0:
3715                 self.depotPaths = sorted(self.previousDepotPaths)
3716                 self.changeRange = "@%s,#head" % p4Change
3717                 if not self.silent and not self.detectBranches:
3718                     print("Performing incremental import into %s git branch" % self.branch)
3719
3720         # accept multiple ref name abbreviations:
3721         #    refs/foo/bar/branch -> use it exactly
3722         #    p4/branch -> prepend refs/remotes/ or refs/heads/
3723         #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
3724         if not self.branch.startswith("refs/"):
3725             if self.importIntoRemotes:
3726                 prepend = "refs/remotes/"
3727             else:
3728                 prepend = "refs/heads/"
3729             if not self.branch.startswith("p4/"):
3730                 prepend += "p4/"
3731             self.branch = prepend + self.branch
3732
3733         if len(args) == 0 and self.depotPaths:
3734             if not self.silent:
3735                 print("Depot paths: %s" % ' '.join(self.depotPaths))
3736         else:
3737             if self.depotPaths and self.depotPaths != args:
3738                 print("previous import used depot path %s and now %s was specified. "
3739                        "This doesn't work!" % (' '.join (self.depotPaths),
3740                                                ' '.join (args)))
3741                 sys.exit(1)
3742
3743             self.depotPaths = sorted(args)
3744
3745         revision = ""
3746         self.users = {}
3747
3748         # Make sure no revision specifiers are used when --changesfile
3749         # is specified.
3750         bad_changesfile = False
3751         if len(self.changesFile) > 0:
3752             for p in self.depotPaths:
3753                 if p.find("@") >= 0 or p.find("#") >= 0:
3754                     bad_changesfile = True
3755                     break
3756         if bad_changesfile:
3757             die("Option --changesfile is incompatible with revision specifiers")
3758
3759         newPaths = []
3760         for p in self.depotPaths:
3761             if p.find("@") != -1:
3762                 atIdx = p.index("@")
3763                 self.changeRange = p[atIdx:]
3764                 if self.changeRange == "@all":
3765                     self.changeRange = ""
3766                 elif ',' not in self.changeRange:
3767                     revision = self.changeRange
3768                     self.changeRange = ""
3769                 p = p[:atIdx]
3770             elif p.find("#") != -1:
3771                 hashIdx = p.index("#")
3772                 revision = p[hashIdx:]
3773                 p = p[:hashIdx]
3774             elif self.previousDepotPaths == []:
3775                 # pay attention to changesfile, if given, else import
3776                 # the entire p4 tree at the head revision
3777                 if len(self.changesFile) == 0:
3778                     revision = "#head"
3779
3780             p = re.sub ("\.\.\.$", "", p)
3781             if not p.endswith("/"):
3782                 p += "/"
3783
3784             newPaths.append(p)
3785
3786         self.depotPaths = newPaths
3787
3788         # --detect-branches may change this for each branch
3789         self.branchPrefixes = self.depotPaths
3790
3791         self.loadUserMapFromCache()
3792         self.labels = {}
3793         if self.detectLabels:
3794             self.getLabels();
3795
3796         if self.detectBranches:
3797             ## FIXME - what's a P4 projectName ?
3798             self.projectName = self.guessProjectName()
3799
3800             if self.hasOrigin:
3801                 self.getBranchMappingFromGitBranches()
3802             else:
3803                 self.getBranchMapping()
3804             if self.verbose:
3805                 print("p4-git branches: %s" % self.p4BranchesInGit)
3806                 print("initial parents: %s" % self.initialParents)
3807             for b in self.p4BranchesInGit:
3808                 if b != "master":
3809
3810                     ## FIXME
3811                     b = b[len(self.projectName):]
3812                 self.createdBranches.add(b)
3813
3814         self.openStreams()
3815
3816         if revision:
3817             self.importHeadRevision(revision)
3818         else:
3819             changes = []
3820
3821             if len(self.changesFile) > 0:
3822                 output = open(self.changesFile).readlines()
3823                 changeSet = set()
3824                 for line in output:
3825                     changeSet.add(int(line))
3826
3827                 for change in changeSet:
3828                     changes.append(change)
3829
3830                 changes.sort()
3831             else:
3832                 # catch "git p4 sync" with no new branches, in a repo that
3833                 # does not have any existing p4 branches
3834                 if len(args) == 0:
3835                     if not self.p4BranchesInGit:
3836                         die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
3837
3838                     # The default branch is master, unless --branch is used to
3839                     # specify something else.  Make sure it exists, or complain
3840                     # nicely about how to use --branch.
3841                     if not self.detectBranches:
3842                         if not branch_exists(self.branch):
3843                             if branch_arg_given:
3844                                 die("Error: branch %s does not exist." % self.branch)
3845                             else:
3846                                 die("Error: no branch %s; perhaps specify one with --branch." %
3847                                     self.branch)
3848
3849                 if self.verbose:
3850                     print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3851                                                               self.changeRange))
3852                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3853
3854                 if len(self.maxChanges) > 0:
3855                     changes = changes[:min(int(self.maxChanges), len(changes))]
3856
3857             if len(changes) == 0:
3858                 if not self.silent:
3859                     print("No changes to import!")
3860             else:
3861                 if not self.silent and not self.detectBranches:
3862                     print("Import destination: %s" % self.branch)
3863
3864                 self.updatedBranches = set()
3865
3866                 if not self.detectBranches:
3867                     if args:
3868                         # start a new branch
3869                         self.initialParent = ""
3870                     else:
3871                         # build on a previous revision
3872                         self.initialParent = parseRevision(self.branch)
3873
3874                 self.importChanges(changes)
3875
3876                 if not self.silent:
3877                     print("")
3878                     if len(self.updatedBranches) > 0:
3879                         sys.stdout.write("Updated branches: ")
3880                         for b in self.updatedBranches:
3881                             sys.stdout.write("%s " % b)
3882                         sys.stdout.write("\n")
3883
3884         if gitConfigBool("git-p4.importLabels"):
3885             self.importLabels = True
3886
3887         if self.importLabels:
3888             p4Labels = getP4Labels(self.depotPaths)
3889             gitTags = getGitTags()
3890
3891             missingP4Labels = p4Labels - gitTags
3892             self.importP4Labels(self.gitStream, missingP4Labels)
3893
3894         self.closeStreams()
3895
3896         # Cleanup temporary branches created during import
3897         if self.tempBranches != []:
3898             for branch in self.tempBranches:
3899                 read_pipe("git update-ref -d %s" % branch)
3900             os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3901
3902         # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3903         # a convenient shortcut refname "p4".
3904         if self.importIntoRemotes:
3905             head_ref = self.refPrefix + "HEAD"
3906             if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3907                 system(["git", "symbolic-ref", head_ref, self.branch])
3908
3909         return True
3910
3911 class P4Rebase(Command):
3912     def __init__(self):
3913         Command.__init__(self)
3914         self.options = [
3915                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3916         ]
3917         self.importLabels = False
3918         self.description = ("Fetches the latest revision from perforce and "
3919                             + "rebases the current work (branch) against it")
3920
3921     def run(self, args):
3922         sync = P4Sync()
3923         sync.importLabels = self.importLabels
3924         sync.run([])
3925
3926         return self.rebase()
3927
3928     def rebase(self):
3929         if os.system("git update-index --refresh") != 0:
3930             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.");
3931         if len(read_pipe("git diff-index HEAD --")) > 0:
3932             die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3933
3934         [upstream, settings] = findUpstreamBranchPoint()
3935         if len(upstream) == 0:
3936             die("Cannot find upstream branchpoint for rebase")
3937
3938         # the branchpoint may be p4/foo~3, so strip off the parent
3939         upstream = re.sub("~[0-9]+$", "", upstream)
3940
3941         print("Rebasing the current branch onto %s" % upstream)
3942         oldHead = read_pipe("git rev-parse HEAD").strip()
3943         system("git rebase %s" % upstream)
3944         system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3945         return True
3946
3947 class P4Clone(P4Sync):
3948     def __init__(self):
3949         P4Sync.__init__(self)
3950         self.description = "Creates a new git repository and imports from Perforce into it"
3951         self.usage = "usage: %prog [options] //depot/path[@revRange]"
3952         self.options += [
3953             optparse.make_option("--destination", dest="cloneDestination",
3954                                  action='store', default=None,
3955                                  help="where to leave result of the clone"),
3956             optparse.make_option("--bare", dest="cloneBare",
3957                                  action="store_true", default=False),
3958         ]
3959         self.cloneDestination = None
3960         self.needsGit = False
3961         self.cloneBare = False
3962
3963     def defaultDestination(self, args):
3964         ## TODO: use common prefix of args?
3965         depotPath = args[0]
3966         depotDir = re.sub("(@[^@]*)$", "", depotPath)
3967         depotDir = re.sub("(#[^#]*)$", "", depotDir)
3968         depotDir = re.sub(r"\.\.\.$", "", depotDir)
3969         depotDir = re.sub(r"/$", "", depotDir)
3970         return os.path.split(depotDir)[1]
3971
3972     def run(self, args):
3973         if len(args) < 1:
3974             return False
3975
3976         if self.keepRepoPath and not self.cloneDestination:
3977             sys.stderr.write("Must specify destination for --keep-path\n")
3978             sys.exit(1)
3979
3980         depotPaths = args
3981
3982         if not self.cloneDestination and len(depotPaths) > 1:
3983             self.cloneDestination = depotPaths[-1]
3984             depotPaths = depotPaths[:-1]
3985
3986         for p in depotPaths:
3987             if not p.startswith("//"):
3988                 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3989                 return False
3990
3991         if not self.cloneDestination:
3992             self.cloneDestination = self.defaultDestination(args)
3993
3994         print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
3995
3996         if not os.path.exists(self.cloneDestination):
3997             os.makedirs(self.cloneDestination)
3998         chdir(self.cloneDestination)
3999
4000         init_cmd = [ "git", "init" ]
4001         if self.cloneBare:
4002             init_cmd.append("--bare")
4003         retcode = subprocess.call(init_cmd)
4004         if retcode:
4005             raise CalledProcessError(retcode, init_cmd)
4006
4007         if not P4Sync.run(self, depotPaths):
4008             return False
4009
4010         # create a master branch and check out a work tree
4011         if gitBranchExists(self.branch):
4012             system([ "git", "branch", "master", self.branch ])
4013             if not self.cloneBare:
4014                 system([ "git", "checkout", "-f" ])
4015         else:
4016             print('Not checking out any branch, use ' \
4017                   '"git checkout -q -b master <branch>"')
4018
4019         # auto-set this variable if invoked with --use-client-spec
4020         if self.useClientSpec_from_options:
4021             system("git config --bool git-p4.useclientspec true")
4022
4023         return True
4024
4025 class P4Unshelve(Command):
4026     def __init__(self):
4027         Command.__init__(self)
4028         self.options = []
4029         self.origin = "HEAD"
4030         self.description = "Unshelve a P4 changelist into a git commit"
4031         self.usage = "usage: %prog [options] changelist"
4032         self.options += [
4033                 optparse.make_option("--origin", dest="origin",
4034                     help="Use this base revision instead of the default (%s)" % self.origin),
4035         ]
4036         self.verbose = False
4037         self.noCommit = False
4038         self.destbranch = "refs/remotes/p4-unshelved"
4039
4040     def renameBranch(self, branch_name):
4041         """ Rename the existing branch to branch_name.N
4042         """
4043
4044         found = True
4045         for i in range(0,1000):
4046             backup_branch_name = "{0}.{1}".format(branch_name, i)
4047             if not gitBranchExists(backup_branch_name):
4048                 gitUpdateRef(backup_branch_name, branch_name) # copy ref to backup
4049                 gitDeleteRef(branch_name)
4050                 found = True
4051                 print("renamed old unshelve branch to {0}".format(backup_branch_name))
4052                 break
4053
4054         if not found:
4055             sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
4056
4057     def findLastP4Revision(self, starting_point):
4058         """ Look back from starting_point for the first commit created by git-p4
4059             to find the P4 commit we are based on, and the depot-paths.
4060         """
4061
4062         for parent in (range(65535)):
4063             log = extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))
4064             settings = extractSettingsGitLog(log)
4065             if 'change' in settings:
4066                 return settings
4067
4068         sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4069
4070     def createShelveParent(self, change, branch_name, sync, origin):
4071         """ Create a commit matching the parent of the shelved changelist 'change'
4072         """
4073         parent_description = p4_describe(change, shelved=True)
4074         parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4075         files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4076
4077         parent_files = []
4078         for f in files:
4079             # if it was added in the shelved changelist, it won't exist in the parent
4080             if f['action'] in self.add_actions:
4081                 continue
4082
4083             # if it was deleted in the shelved changelist it must not be deleted
4084             # in the parent - we might even need to create it if the origin branch
4085             # does not have it
4086             if f['action'] in self.delete_actions:
4087                 f['action'] = 'add'
4088
4089             parent_files.append(f)
4090
4091         sync.commit(parent_description, parent_files, branch_name,
4092                 parent=origin, allow_empty=True)
4093         print("created parent commit for {0} based on {1} in {2}".format(
4094             change, self.origin, branch_name))
4095
4096     def run(self, args):
4097         if len(args) != 1:
4098             return False
4099
4100         if not gitBranchExists(self.origin):
4101             sys.exit("origin branch {0} does not exist".format(self.origin))
4102
4103         sync = P4Sync()
4104         changes = args
4105
4106         # only one change at a time
4107         change = changes[0]
4108
4109         # if the target branch already exists, rename it
4110         branch_name = "{0}/{1}".format(self.destbranch, change)
4111         if gitBranchExists(branch_name):
4112             self.renameBranch(branch_name)
4113         sync.branch = branch_name
4114
4115         sync.verbose = self.verbose
4116         sync.suppress_meta_comment = True
4117
4118         settings = self.findLastP4Revision(self.origin)
4119         sync.depotPaths = settings['depot-paths']
4120         sync.branchPrefixes = sync.depotPaths
4121
4122         sync.openStreams()
4123         sync.loadUserMapFromCache()
4124         sync.silent = True
4125
4126         # create a commit for the parent of the shelved changelist
4127         self.createShelveParent(change, branch_name, sync, self.origin)
4128
4129         # create the commit for the shelved changelist itself
4130         description = p4_describe(change, True)
4131         files = sync.extractFilesFromCommit(description, True, change)
4132
4133         sync.commit(description, files, branch_name, "")
4134         sync.closeStreams()
4135
4136         print("unshelved changelist {0} into {1}".format(change, branch_name))
4137
4138         return True
4139
4140 class P4Branches(Command):
4141     def __init__(self):
4142         Command.__init__(self)
4143         self.options = [ ]
4144         self.description = ("Shows the git branches that hold imports and their "
4145                             + "corresponding perforce depot paths")
4146         self.verbose = False
4147
4148     def run(self, args):
4149         if originP4BranchesExist():
4150             createOrUpdateBranchesFromOrigin()
4151
4152         cmdline = "git rev-parse --symbolic "
4153         cmdline += " --remotes"
4154
4155         for line in read_pipe_lines(cmdline):
4156             line = line.strip()
4157
4158             if not line.startswith('p4/') or line == "p4/HEAD":
4159                 continue
4160             branch = line
4161
4162             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4163             settings = extractSettingsGitLog(log)
4164
4165             print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
4166         return True
4167
4168 class HelpFormatter(optparse.IndentedHelpFormatter):
4169     def __init__(self):
4170         optparse.IndentedHelpFormatter.__init__(self)
4171
4172     def format_description(self, description):
4173         if description:
4174             return description + "\n"
4175         else:
4176             return ""
4177
4178 def printUsage(commands):
4179     print("usage: %s <command> [options]" % sys.argv[0])
4180     print("")
4181     print("valid commands: %s" % ", ".join(commands))
4182     print("")
4183     print("Try %s <command> --help for command specific help." % sys.argv[0])
4184     print("")
4185
4186 commands = {
4187     "debug" : P4Debug,
4188     "submit" : P4Submit,
4189     "commit" : P4Submit,
4190     "sync" : P4Sync,
4191     "rebase" : P4Rebase,
4192     "clone" : P4Clone,
4193     "rollback" : P4RollBack,
4194     "branches" : P4Branches,
4195     "unshelve" : P4Unshelve,
4196 }
4197
4198 def main():
4199     if len(sys.argv[1:]) == 0:
4200         printUsage(commands.keys())
4201         sys.exit(2)
4202
4203     cmdName = sys.argv[1]
4204     try:
4205         klass = commands[cmdName]
4206         cmd = klass()
4207     except KeyError:
4208         print("unknown command %s" % cmdName)
4209         print("")
4210         printUsage(commands.keys())
4211         sys.exit(2)
4212
4213     options = cmd.options
4214     cmd.gitdir = os.environ.get("GIT_DIR", None)
4215
4216     args = sys.argv[2:]
4217
4218     options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
4219     if cmd.needsGit:
4220         options.append(optparse.make_option("--git-dir", dest="gitdir"))
4221
4222     parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4223                                    options,
4224                                    description = cmd.description,
4225                                    formatter = HelpFormatter())
4226
4227     try:
4228         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
4229     except:
4230         parser.print_help()
4231         raise
4232
4233     global verbose
4234     verbose = cmd.verbose
4235     if cmd.needsGit:
4236         if cmd.gitdir == None:
4237             cmd.gitdir = os.path.abspath(".git")
4238             if not isValidGitDir(cmd.gitdir):
4239                 # "rev-parse --git-dir" without arguments will try $PWD/.git
4240                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
4241                 if os.path.exists(cmd.gitdir):
4242                     cdup = read_pipe("git rev-parse --show-cdup").strip()
4243                     if len(cdup) > 0:
4244                         chdir(cdup);
4245
4246         if not isValidGitDir(cmd.gitdir):
4247             if isValidGitDir(cmd.gitdir + "/.git"):
4248                 cmd.gitdir += "/.git"
4249             else:
4250                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
4251
4252         # so git commands invoked from the P4 workspace will succeed
4253         os.environ["GIT_DIR"] = cmd.gitdir
4254
4255     if not cmd.run(args):
4256         parser.print_help()
4257         sys.exit(2)
4258
4259
4260 if __name__ == '__main__':
4261     main()