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