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