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