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