git-p4: convert path to unicode before processing them
[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(), "wb").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(), "rb")
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         input = marshal.dumps(c)
1683
1684         result = p4CmdList("change -f -i", stdin=input)
1685         for r in result:
1686             if 'code' in r:
1687                 if r['code'] == 'error':
1688                     die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1689             if 'data' in r:
1690                 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1691                 return
1692         die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1693
1694     def canChangeChangelists(self):
1695         # check to see if we have p4 admin or super-user permissions, either of
1696         # which are required to modify changelists.
1697         results = p4CmdList(["protects", self.depotPath])
1698         for r in results:
1699             if 'perm' in r:
1700                 if r['perm'] == 'admin':
1701                     return 1
1702                 if r['perm'] == 'super':
1703                     return 1
1704         return 0
1705
1706     def prepareSubmitTemplate(self, changelist=None):
1707         """Run "p4 change -o" to grab a change specification template.
1708            This does not use "p4 -G", as it is nice to keep the submission
1709            template in original order, since a human might edit it.
1710
1711            Remove lines in the Files section that show changes to files
1712            outside the depot path we're committing into."""
1713
1714         [upstream, settings] = findUpstreamBranchPoint()
1715
1716         template = """\
1717 # A Perforce Change Specification.
1718 #
1719 #  Change:      The change number. 'new' on a new changelist.
1720 #  Date:        The date this specification was last modified.
1721 #  Client:      The client on which the changelist was created.  Read-only.
1722 #  User:        The user who created the changelist.
1723 #  Status:      Either 'pending' or 'submitted'. Read-only.
1724 #  Type:        Either 'public' or 'restricted'. Default is 'public'.
1725 #  Description: Comments about the changelist.  Required.
1726 #  Jobs:        What opened jobs are to be closed by this changelist.
1727 #               You may delete jobs from this list.  (New changelists only.)
1728 #  Files:       What opened files from the default changelist are to be added
1729 #               to this changelist.  You may delete files from this list.
1730 #               (New changelists only.)
1731 """
1732         files_list = []
1733         inFilesSection = False
1734         change_entry = None
1735         args = ['change', '-o']
1736         if changelist:
1737             args.append(str(changelist))
1738         for entry in p4CmdList(args):
1739             if 'code' not in entry:
1740                 continue
1741             if entry['code'] == 'stat':
1742                 change_entry = entry
1743                 break
1744         if not change_entry:
1745             die('Failed to decode output of p4 change -o')
1746         for key, value in change_entry.iteritems():
1747             if key.startswith('File'):
1748                 if 'depot-paths' in settings:
1749                     if not [p for p in settings['depot-paths']
1750                             if p4PathStartsWith(value, p)]:
1751                         continue
1752                 else:
1753                     if not p4PathStartsWith(value, self.depotPath):
1754                         continue
1755                 files_list.append(value)
1756                 continue
1757         # Output in the order expected by prepareLogMessage
1758         for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
1759             if key not in change_entry:
1760                 continue
1761             template += '\n'
1762             template += key + ':'
1763             if key == 'Description':
1764                 template += '\n'
1765             for field_line in change_entry[key].splitlines():
1766                 template += '\t'+field_line+'\n'
1767         if len(files_list) > 0:
1768             template += '\n'
1769             template += 'Files:\n'
1770         for path in files_list:
1771             template += '\t'+path+'\n'
1772         return template
1773
1774     def edit_template(self, template_file):
1775         """Invoke the editor to let the user change the submission
1776            message.  Return true if okay to continue with the submit."""
1777
1778         # if configured to skip the editing part, just submit
1779         if gitConfigBool("git-p4.skipSubmitEdit"):
1780             return True
1781
1782         # look at the modification time, to check later if the user saved
1783         # the file
1784         mtime = os.stat(template_file).st_mtime
1785
1786         # invoke the editor
1787         if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
1788             editor = os.environ.get("P4EDITOR")
1789         else:
1790             editor = read_pipe("git var GIT_EDITOR").strip()
1791         system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1792
1793         # If the file was not saved, prompt to see if this patch should
1794         # be skipped.  But skip this verification step if configured so.
1795         if gitConfigBool("git-p4.skipSubmitEditCheck"):
1796             return True
1797
1798         # modification time updated means user saved the file
1799         if os.stat(template_file).st_mtime > mtime:
1800             return True
1801
1802         while True:
1803             response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1804             if response == 'y':
1805                 return True
1806             if response == 'n':
1807                 return False
1808
1809     def get_diff_description(self, editedFiles, filesToAdd, symlinks):
1810         # diff
1811         if "P4DIFF" in os.environ:
1812             del(os.environ["P4DIFF"])
1813         diff = ""
1814         for editedFile in editedFiles:
1815             diff += p4_read_pipe(['diff', '-du',
1816                                   wildcard_encode(editedFile)])
1817
1818         # new file diff
1819         newdiff = ""
1820         for newFile in filesToAdd:
1821             newdiff += "==== new file ====\n"
1822             newdiff += "--- /dev/null\n"
1823             newdiff += "+++ %s\n" % newFile
1824
1825             is_link = os.path.islink(newFile)
1826             expect_link = newFile in symlinks
1827
1828             if is_link and expect_link:
1829                 newdiff += "+%s\n" % os.readlink(newFile)
1830             else:
1831                 f = open(newFile, "r")
1832                 for line in f.readlines():
1833                     newdiff += "+" + line
1834                 f.close()
1835
1836         return (diff + newdiff).replace('\r\n', '\n')
1837
1838     def applyCommit(self, id):
1839         """Apply one commit, return True if it succeeded."""
1840
1841         print("Applying", read_pipe(["git", "show", "-s",
1842                                      "--format=format:%h %s", id]))
1843
1844         (p4User, gitEmail) = self.p4UserForCommit(id)
1845
1846         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1847         filesToAdd = set()
1848         filesToChangeType = set()
1849         filesToDelete = set()
1850         editedFiles = set()
1851         pureRenameCopy = set()
1852         symlinks = set()
1853         filesToChangeExecBit = {}
1854         all_files = list()
1855
1856         for line in diff:
1857             diff = parseDiffTreeEntry(line)
1858             modifier = diff['status']
1859             path = diff['src']
1860             all_files.append(path)
1861
1862             if modifier == "M":
1863                 p4_edit(path)
1864                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1865                     filesToChangeExecBit[path] = diff['dst_mode']
1866                 editedFiles.add(path)
1867             elif modifier == "A":
1868                 filesToAdd.add(path)
1869                 filesToChangeExecBit[path] = diff['dst_mode']
1870                 if path in filesToDelete:
1871                     filesToDelete.remove(path)
1872
1873                 dst_mode = int(diff['dst_mode'], 8)
1874                 if dst_mode == 0o120000:
1875                     symlinks.add(path)
1876
1877             elif modifier == "D":
1878                 filesToDelete.add(path)
1879                 if path in filesToAdd:
1880                     filesToAdd.remove(path)
1881             elif modifier == "C":
1882                 src, dest = diff['src'], diff['dst']
1883                 all_files.append(dest)
1884                 p4_integrate(src, dest)
1885                 pureRenameCopy.add(dest)
1886                 if diff['src_sha1'] != diff['dst_sha1']:
1887                     p4_edit(dest)
1888                     pureRenameCopy.discard(dest)
1889                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1890                     p4_edit(dest)
1891                     pureRenameCopy.discard(dest)
1892                     filesToChangeExecBit[dest] = diff['dst_mode']
1893                 if self.isWindows:
1894                     # turn off read-only attribute
1895                     os.chmod(dest, stat.S_IWRITE)
1896                 os.unlink(dest)
1897                 editedFiles.add(dest)
1898             elif modifier == "R":
1899                 src, dest = diff['src'], diff['dst']
1900                 all_files.append(dest)
1901                 if self.p4HasMoveCommand:
1902                     p4_edit(src)        # src must be open before move
1903                     p4_move(src, dest)  # opens for (move/delete, move/add)
1904                 else:
1905                     p4_integrate(src, dest)
1906                     if diff['src_sha1'] != diff['dst_sha1']:
1907                         p4_edit(dest)
1908                     else:
1909                         pureRenameCopy.add(dest)
1910                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1911                     if not self.p4HasMoveCommand:
1912                         p4_edit(dest)   # with move: already open, writable
1913                     filesToChangeExecBit[dest] = diff['dst_mode']
1914                 if not self.p4HasMoveCommand:
1915                     if self.isWindows:
1916                         os.chmod(dest, stat.S_IWRITE)
1917                     os.unlink(dest)
1918                     filesToDelete.add(src)
1919                 editedFiles.add(dest)
1920             elif modifier == "T":
1921                 filesToChangeType.add(path)
1922             else:
1923                 die("unknown modifier %s for %s" % (modifier, path))
1924
1925         diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1926         patchcmd = diffcmd + " | git apply "
1927         tryPatchCmd = patchcmd + "--check -"
1928         applyPatchCmd = patchcmd + "--check --apply -"
1929         patch_succeeded = True
1930
1931         if os.system(tryPatchCmd) != 0:
1932             fixed_rcs_keywords = False
1933             patch_succeeded = False
1934             print("Unfortunately applying the change failed!")
1935
1936             # Patch failed, maybe it's just RCS keyword woes. Look through
1937             # the patch to see if that's possible.
1938             if gitConfigBool("git-p4.attemptRCSCleanup"):
1939                 file = None
1940                 pattern = None
1941                 kwfiles = {}
1942                 for file in editedFiles | filesToDelete:
1943                     # did this file's delta contain RCS keywords?
1944                     pattern = p4_keywords_regexp_for_file(file)
1945
1946                     if pattern:
1947                         # this file is a possibility...look for RCS keywords.
1948                         regexp = re.compile(pattern, re.VERBOSE)
1949                         for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1950                             if regexp.search(line):
1951                                 if verbose:
1952                                     print("got keyword match on %s in %s in %s" % (pattern, line, file))
1953                                 kwfiles[file] = pattern
1954                                 break
1955
1956                 for file in kwfiles:
1957                     if verbose:
1958                         print("zapping %s with %s" % (line,pattern))
1959                     # File is being deleted, so not open in p4.  Must
1960                     # disable the read-only bit on windows.
1961                     if self.isWindows and file not in editedFiles:
1962                         os.chmod(file, stat.S_IWRITE)
1963                     self.patchRCSKeywords(file, kwfiles[file])
1964                     fixed_rcs_keywords = True
1965
1966             if fixed_rcs_keywords:
1967                 print("Retrying the patch with RCS keywords cleaned up")
1968                 if os.system(tryPatchCmd) == 0:
1969                     patch_succeeded = True
1970
1971         if not patch_succeeded:
1972             for f in editedFiles:
1973                 p4_revert(f)
1974             return False
1975
1976         #
1977         # Apply the patch for real, and do add/delete/+x handling.
1978         #
1979         system(applyPatchCmd)
1980
1981         for f in filesToChangeType:
1982             p4_edit(f, "-t", "auto")
1983         for f in filesToAdd:
1984             p4_add(f)
1985         for f in filesToDelete:
1986             p4_revert(f)
1987             p4_delete(f)
1988
1989         # Set/clear executable bits
1990         for f in filesToChangeExecBit.keys():
1991             mode = filesToChangeExecBit[f]
1992             setP4ExecBit(f, mode)
1993
1994         update_shelve = 0
1995         if len(self.update_shelve) > 0:
1996             update_shelve = self.update_shelve.pop(0)
1997             p4_reopen_in_change(update_shelve, all_files)
1998
1999         #
2000         # Build p4 change description, starting with the contents
2001         # of the git commit message.
2002         #
2003         logMessage = extractLogMessageFromGitCommit(id)
2004         logMessage = logMessage.strip()
2005         (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
2006
2007         template = self.prepareSubmitTemplate(update_shelve)
2008         submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
2009
2010         if self.preserveUser:
2011            submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
2012
2013         if self.checkAuthorship and not self.p4UserIsMe(p4User):
2014             submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
2015             submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
2016             submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2017
2018         separatorLine = "######## everything below this line is just the diff #######\n"
2019         if not self.prepare_p4_only:
2020             submitTemplate += separatorLine
2021             submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
2022
2023         (handle, fileName) = tempfile.mkstemp()
2024         tmpFile = os.fdopen(handle, "w+b")
2025         if self.isWindows:
2026             submitTemplate = submitTemplate.replace("\n", "\r\n")
2027         tmpFile.write(encode_text_stream(submitTemplate))
2028         tmpFile.close()
2029
2030         if self.prepare_p4_only:
2031             #
2032             # Leave the p4 tree prepared, and the submit template around
2033             # and let the user decide what to do next
2034             #
2035             print()
2036             print("P4 workspace prepared for submission.")
2037             print("To submit or revert, go to client workspace")
2038             print("  " + self.clientPath)
2039             print()
2040             print("To submit, use \"p4 submit\" to write a new description,")
2041             print("or \"p4 submit -i <%s\" to use the one prepared by" \
2042                   " \"git p4\"." % fileName)
2043             print("You can delete the file \"%s\" when finished." % fileName)
2044
2045             if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
2046                 print("To preserve change ownership by user %s, you must\n" \
2047                       "do \"p4 change -f <change>\" after submitting and\n" \
2048                       "edit the User field.")
2049             if pureRenameCopy:
2050                 print("After submitting, renamed files must be re-synced.")
2051                 print("Invoke \"p4 sync -f\" on each of these files:")
2052                 for f in pureRenameCopy:
2053                     print("  " + f)
2054
2055             print()
2056             print("To revert the changes, use \"p4 revert ...\", and delete")
2057             print("the submit template file \"%s\"" % fileName)
2058             if filesToAdd:
2059                 print("Since the commit adds new files, they must be deleted:")
2060                 for f in filesToAdd:
2061                     print("  " + f)
2062             print()
2063             return True
2064
2065         #
2066         # Let the user edit the change description, then submit it.
2067         #
2068         submitted = False
2069
2070         try:
2071             if self.edit_template(fileName):
2072                 # read the edited message and submit
2073                 tmpFile = open(fileName, "rb")
2074                 message = decode_text_stream(tmpFile.read())
2075                 tmpFile.close()
2076                 if self.isWindows:
2077                     message = message.replace("\r\n", "\n")
2078                 submitTemplate = message[:message.index(separatorLine)]
2079
2080                 if update_shelve:
2081                     p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2082                 elif self.shelve:
2083                     p4_write_pipe(['shelve', '-i'], submitTemplate)
2084                 else:
2085                     p4_write_pipe(['submit', '-i'], submitTemplate)
2086                     # The rename/copy happened by applying a patch that created a
2087                     # new file.  This leaves it writable, which confuses p4.
2088                     for f in pureRenameCopy:
2089                         p4_sync(f, "-f")
2090
2091                 if self.preserveUser:
2092                     if p4User:
2093                         # Get last changelist number. Cannot easily get it from
2094                         # the submit command output as the output is
2095                         # unmarshalled.
2096                         changelist = self.lastP4Changelist()
2097                         self.modifyChangelistUser(changelist, p4User)
2098
2099                 submitted = True
2100
2101         finally:
2102             # skip this patch
2103             if not submitted or self.shelve:
2104                 if self.shelve:
2105                     print ("Reverting shelved files.")
2106                 else:
2107                     print ("Submission cancelled, undoing p4 changes.")
2108                 for f in editedFiles | filesToDelete:
2109                     p4_revert(f)
2110                 for f in filesToAdd:
2111                     p4_revert(f)
2112                     os.remove(f)
2113
2114         os.remove(fileName)
2115         return submitted
2116
2117     # Export git tags as p4 labels. Create a p4 label and then tag
2118     # with that.
2119     def exportGitTags(self, gitTags):
2120         validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2121         if len(validLabelRegexp) == 0:
2122             validLabelRegexp = defaultLabelRegexp
2123         m = re.compile(validLabelRegexp)
2124
2125         for name in gitTags:
2126
2127             if not m.match(name):
2128                 if verbose:
2129                     print("tag %s does not match regexp %s" % (name, validLabelRegexp))
2130                 continue
2131
2132             # Get the p4 commit this corresponds to
2133             logMessage = extractLogMessageFromGitCommit(name)
2134             values = extractSettingsGitLog(logMessage)
2135
2136             if 'change' not in values:
2137                 # a tag pointing to something not sent to p4; ignore
2138                 if verbose:
2139                     print("git tag %s does not give a p4 commit" % name)
2140                 continue
2141             else:
2142                 changelist = values['change']
2143
2144             # Get the tag details.
2145             inHeader = True
2146             isAnnotated = False
2147             body = []
2148             for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2149                 l = l.strip()
2150                 if inHeader:
2151                     if re.match(r'tag\s+', l):
2152                         isAnnotated = True
2153                     elif re.match(r'\s*$', l):
2154                         inHeader = False
2155                         continue
2156                 else:
2157                     body.append(l)
2158
2159             if not isAnnotated:
2160                 body = ["lightweight tag imported by git p4\n"]
2161
2162             # Create the label - use the same view as the client spec we are using
2163             clientSpec = getClientSpec()
2164
2165             labelTemplate  = "Label: %s\n" % name
2166             labelTemplate += "Description:\n"
2167             for b in body:
2168                 labelTemplate += "\t" + b + "\n"
2169             labelTemplate += "View:\n"
2170             for depot_side in clientSpec.mappings:
2171                 labelTemplate += "\t%s\n" % depot_side
2172
2173             if self.dry_run:
2174                 print("Would create p4 label %s for tag" % name)
2175             elif self.prepare_p4_only:
2176                 print("Not creating p4 label %s for tag due to option" \
2177                       " --prepare-p4-only" % name)
2178             else:
2179                 p4_write_pipe(["label", "-i"], labelTemplate)
2180
2181                 # Use the label
2182                 p4_system(["tag", "-l", name] +
2183                           ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
2184
2185                 if verbose:
2186                     print("created p4 label for tag %s" % name)
2187
2188     def run(self, args):
2189         if len(args) == 0:
2190             self.master = currentGitBranch()
2191         elif len(args) == 1:
2192             self.master = args[0]
2193             if not branchExists(self.master):
2194                 die("Branch %s does not exist" % self.master)
2195         else:
2196             return False
2197
2198         for i in self.update_shelve:
2199             if i <= 0:
2200                 sys.exit("invalid changelist %d" % i)
2201
2202         if self.master:
2203             allowSubmit = gitConfig("git-p4.allowSubmit")
2204             if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2205                 die("%s is not in git-p4.allowSubmit" % self.master)
2206
2207         [upstream, settings] = findUpstreamBranchPoint()
2208         self.depotPath = settings['depot-paths'][0]
2209         if len(self.origin) == 0:
2210             self.origin = upstream
2211
2212         if len(self.update_shelve) > 0:
2213             self.shelve = True
2214
2215         if self.preserveUser:
2216             if not self.canChangeChangelists():
2217                 die("Cannot preserve user names without p4 super-user or admin permissions")
2218
2219         # if not set from the command line, try the config file
2220         if self.conflict_behavior is None:
2221             val = gitConfig("git-p4.conflict")
2222             if val:
2223                 if val not in self.conflict_behavior_choices:
2224                     die("Invalid value '%s' for config git-p4.conflict" % val)
2225             else:
2226                 val = "ask"
2227             self.conflict_behavior = val
2228
2229         if self.verbose:
2230             print("Origin branch is " + self.origin)
2231
2232         if len(self.depotPath) == 0:
2233             print("Internal error: cannot locate perforce depot path from existing branches")
2234             sys.exit(128)
2235
2236         self.useClientSpec = False
2237         if gitConfigBool("git-p4.useclientspec"):
2238             self.useClientSpec = True
2239         if self.useClientSpec:
2240             self.clientSpecDirs = getClientSpec()
2241
2242         # Check for the existence of P4 branches
2243         branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2244
2245         if self.useClientSpec and not branchesDetected:
2246             # all files are relative to the client spec
2247             self.clientPath = getClientRoot()
2248         else:
2249             self.clientPath = p4Where(self.depotPath)
2250
2251         if self.clientPath == "":
2252             die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
2253
2254         print("Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath))
2255         self.oldWorkingDirectory = os.getcwd()
2256
2257         # ensure the clientPath exists
2258         new_client_dir = False
2259         if not os.path.exists(self.clientPath):
2260             new_client_dir = True
2261             os.makedirs(self.clientPath)
2262
2263         chdir(self.clientPath, is_client_path=True)
2264         if self.dry_run:
2265             print("Would synchronize p4 checkout in %s" % self.clientPath)
2266         else:
2267             print("Synchronizing p4 checkout...")
2268             if new_client_dir:
2269                 # old one was destroyed, and maybe nobody told p4
2270                 p4_sync("...", "-f")
2271             else:
2272                 p4_sync("...")
2273         self.check()
2274
2275         commits = []
2276         if self.master:
2277             committish = self.master
2278         else:
2279             committish = 'HEAD'
2280
2281         if self.commit != "":
2282             if self.commit.find("..") != -1:
2283                 limits_ish = self.commit.split("..")
2284                 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2285                     commits.append(line.strip())
2286                 commits.reverse()
2287             else:
2288                 commits.append(self.commit)
2289         else:
2290             for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
2291                 commits.append(line.strip())
2292             commits.reverse()
2293
2294         if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
2295             self.checkAuthorship = False
2296         else:
2297             self.checkAuthorship = True
2298
2299         if self.preserveUser:
2300             self.checkValidP4Users(commits)
2301
2302         #
2303         # Build up a set of options to be passed to diff when
2304         # submitting each commit to p4.
2305         #
2306         if self.detectRenames:
2307             # command-line -M arg
2308             self.diffOpts = "-M"
2309         else:
2310             # If not explicitly set check the config variable
2311             detectRenames = gitConfig("git-p4.detectRenames")
2312
2313             if detectRenames.lower() == "false" or detectRenames == "":
2314                 self.diffOpts = ""
2315             elif detectRenames.lower() == "true":
2316                 self.diffOpts = "-M"
2317             else:
2318                 self.diffOpts = "-M%s" % detectRenames
2319
2320         # no command-line arg for -C or --find-copies-harder, just
2321         # config variables
2322         detectCopies = gitConfig("git-p4.detectCopies")
2323         if detectCopies.lower() == "false" or detectCopies == "":
2324             pass
2325         elif detectCopies.lower() == "true":
2326             self.diffOpts += " -C"
2327         else:
2328             self.diffOpts += " -C%s" % detectCopies
2329
2330         if gitConfigBool("git-p4.detectCopiesHarder"):
2331             self.diffOpts += " --find-copies-harder"
2332
2333         num_shelves = len(self.update_shelve)
2334         if num_shelves > 0 and num_shelves != len(commits):
2335             sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2336                      (len(commits), num_shelves))
2337
2338         hooks_path = gitConfig("core.hooksPath")
2339         if len(hooks_path) <= 0:
2340             hooks_path = os.path.join(os.environ.get("GIT_DIR", ".git"), "hooks")
2341
2342         hook_file = os.path.join(hooks_path, "p4-pre-submit")
2343         if os.path.isfile(hook_file) and os.access(hook_file, os.X_OK) and subprocess.call([hook_file]) != 0:
2344             sys.exit(1)
2345
2346         #
2347         # Apply the commits, one at a time.  On failure, ask if should
2348         # continue to try the rest of the patches, or quit.
2349         #
2350         if self.dry_run:
2351             print("Would apply")
2352         applied = []
2353         last = len(commits) - 1
2354         for i, commit in enumerate(commits):
2355             if self.dry_run:
2356                 print(" ", read_pipe(["git", "show", "-s",
2357                                       "--format=format:%h %s", commit]))
2358                 ok = True
2359             else:
2360                 ok = self.applyCommit(commit)
2361             if ok:
2362                 applied.append(commit)
2363             else:
2364                 if self.prepare_p4_only and i < last:
2365                     print("Processing only the first commit due to option" \
2366                           " --prepare-p4-only")
2367                     break
2368                 if i < last:
2369                     quit = False
2370                     while True:
2371                         # prompt for what to do, or use the option/variable
2372                         if self.conflict_behavior == "ask":
2373                             print("What do you want to do?")
2374                             response = raw_input("[s]kip this commit but apply"
2375                                                  " the rest, or [q]uit? ")
2376                             if not response:
2377                                 continue
2378                         elif self.conflict_behavior == "skip":
2379                             response = "s"
2380                         elif self.conflict_behavior == "quit":
2381                             response = "q"
2382                         else:
2383                             die("Unknown conflict_behavior '%s'" %
2384                                 self.conflict_behavior)
2385
2386                         if response[0] == "s":
2387                             print("Skipping this commit, but applying the rest")
2388                             break
2389                         if response[0] == "q":
2390                             print("Quitting")
2391                             quit = True
2392                             break
2393                     if quit:
2394                         break
2395
2396         chdir(self.oldWorkingDirectory)
2397         shelved_applied = "shelved" if self.shelve else "applied"
2398         if self.dry_run:
2399             pass
2400         elif self.prepare_p4_only:
2401             pass
2402         elif len(commits) == len(applied):
2403             print("All commits {0}!".format(shelved_applied))
2404
2405             sync = P4Sync()
2406             if self.branch:
2407                 sync.branch = self.branch
2408             if self.disable_p4sync:
2409                 sync.sync_origin_only()
2410             else:
2411                 sync.run([])
2412
2413                 if not self.disable_rebase:
2414                     rebase = P4Rebase()
2415                     rebase.rebase()
2416
2417         else:
2418             if len(applied) == 0:
2419                 print("No commits {0}.".format(shelved_applied))
2420             else:
2421                 print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
2422                 for c in commits:
2423                     if c in applied:
2424                         star = "*"
2425                     else:
2426                         star = " "
2427                     print(star, read_pipe(["git", "show", "-s",
2428                                            "--format=format:%h %s",  c]))
2429                 print("You will have to do 'git p4 sync' and rebase.")
2430
2431         if gitConfigBool("git-p4.exportLabels"):
2432             self.exportLabels = True
2433
2434         if self.exportLabels:
2435             p4Labels = getP4Labels(self.depotPath)
2436             gitTags = getGitTags()
2437
2438             missingGitTags = gitTags - p4Labels
2439             self.exportGitTags(missingGitTags)
2440
2441         # exit with error unless everything applied perfectly
2442         if len(commits) != len(applied):
2443                 sys.exit(1)
2444
2445         return True
2446
2447 class View(object):
2448     """Represent a p4 view ("p4 help views"), and map files in a
2449        repo according to the view."""
2450
2451     def __init__(self, client_name):
2452         self.mappings = []
2453         self.client_prefix = "//%s/" % client_name
2454         # cache results of "p4 where" to lookup client file locations
2455         self.client_spec_path_cache = {}
2456
2457     def append(self, view_line):
2458         """Parse a view line, splitting it into depot and client
2459            sides.  Append to self.mappings, preserving order.  This
2460            is only needed for tag creation."""
2461
2462         # Split the view line into exactly two words.  P4 enforces
2463         # structure on these lines that simplifies this quite a bit.
2464         #
2465         # Either or both words may be double-quoted.
2466         # Single quotes do not matter.
2467         # Double-quote marks cannot occur inside the words.
2468         # A + or - prefix is also inside the quotes.
2469         # There are no quotes unless they contain a space.
2470         # The line is already white-space stripped.
2471         # The two words are separated by a single space.
2472         #
2473         if view_line[0] == '"':
2474             # First word is double quoted.  Find its end.
2475             close_quote_index = view_line.find('"', 1)
2476             if close_quote_index <= 0:
2477                 die("No first-word closing quote found: %s" % view_line)
2478             depot_side = view_line[1:close_quote_index]
2479             # skip closing quote and space
2480             rhs_index = close_quote_index + 1 + 1
2481         else:
2482             space_index = view_line.find(" ")
2483             if space_index <= 0:
2484                 die("No word-splitting space found: %s" % view_line)
2485             depot_side = view_line[0:space_index]
2486             rhs_index = space_index + 1
2487
2488         # prefix + means overlay on previous mapping
2489         if depot_side.startswith("+"):
2490             depot_side = depot_side[1:]
2491
2492         # prefix - means exclude this path, leave out of mappings
2493         exclude = False
2494         if depot_side.startswith("-"):
2495             exclude = True
2496             depot_side = depot_side[1:]
2497
2498         if not exclude:
2499             self.mappings.append(depot_side)
2500
2501     def convert_client_path(self, clientFile):
2502         # chop off //client/ part to make it relative
2503         if not decode_path(clientFile).startswith(self.client_prefix):
2504             die("No prefix '%s' on clientFile '%s'" %
2505                 (self.client_prefix, clientFile))
2506         return clientFile[len(self.client_prefix):]
2507
2508     def update_client_spec_path_cache(self, files):
2509         """ Caching file paths by "p4 where" batch query """
2510
2511         # List depot file paths exclude that already cached
2512         fileArgs = [f['path'] for f in files if decode_path(f['path']) not in self.client_spec_path_cache]
2513
2514         if len(fileArgs) == 0:
2515             return  # All files in cache
2516
2517         where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2518         for res in where_result:
2519             if "code" in res and res["code"] == "error":
2520                 # assume error is "... file(s) not in client view"
2521                 continue
2522             if "clientFile" not in res:
2523                 die("No clientFile in 'p4 where' output")
2524             if "unmap" in res:
2525                 # it will list all of them, but only one not unmap-ped
2526                 continue
2527             depot_path = decode_path(res['depotFile'])
2528             if gitConfigBool("core.ignorecase"):
2529                 depot_path = depot_path.lower()
2530             self.client_spec_path_cache[depot_path] = self.convert_client_path(res["clientFile"])
2531
2532         # not found files or unmap files set to ""
2533         for depotFile in fileArgs:
2534             depotFile = decode_path(depotFile)
2535             if gitConfigBool("core.ignorecase"):
2536                 depotFile = depotFile.lower()
2537             if depotFile not in self.client_spec_path_cache:
2538                 self.client_spec_path_cache[depotFile] = b''
2539
2540     def map_in_client(self, depot_path):
2541         """Return the relative location in the client where this
2542            depot file should live.  Returns "" if the file should
2543            not be mapped in the client."""
2544
2545         if gitConfigBool("core.ignorecase"):
2546             depot_path = depot_path.lower()
2547
2548         if depot_path in self.client_spec_path_cache:
2549             return self.client_spec_path_cache[depot_path]
2550
2551         die( "Error: %s is not found in client spec path" % depot_path )
2552         return ""
2553
2554 def cloneExcludeCallback(option, opt_str, value, parser):
2555     # prepend "/" because the first "/" was consumed as part of the option itself.
2556     # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2557     parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2558
2559 class P4Sync(Command, P4UserMap):
2560
2561     def __init__(self):
2562         Command.__init__(self)
2563         P4UserMap.__init__(self)
2564         self.options = [
2565                 optparse.make_option("--branch", dest="branch"),
2566                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2567                 optparse.make_option("--changesfile", dest="changesFile"),
2568                 optparse.make_option("--silent", dest="silent", action="store_true"),
2569                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2570                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2571                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2572                                      help="Import into refs/heads/ , not refs/remotes"),
2573                 optparse.make_option("--max-changes", dest="maxChanges",
2574                                      help="Maximum number of changes to import"),
2575                 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2576                                      help="Internal block size to use when iteratively calling p4 changes"),
2577                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2578                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2579                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2580                                      help="Only sync files that are included in the Perforce Client Spec"),
2581                 optparse.make_option("-/", dest="cloneExclude",
2582                                      action="callback", callback=cloneExcludeCallback, type="string",
2583                                      help="exclude depot path"),
2584         ]
2585         self.description = """Imports from Perforce into a git repository.\n
2586     example:
2587     //depot/my/project/ -- to import the current head
2588     //depot/my/project/@all -- to import everything
2589     //depot/my/project/@1,6 -- to import only from revision 1 to 6
2590
2591     (a ... is not needed in the path p4 specification, it's added implicitly)"""
2592
2593         self.usage += " //depot/path[@revRange]"
2594         self.silent = False
2595         self.createdBranches = set()
2596         self.committedChanges = set()
2597         self.branch = ""
2598         self.detectBranches = False
2599         self.detectLabels = False
2600         self.importLabels = False
2601         self.changesFile = ""
2602         self.syncWithOrigin = True
2603         self.importIntoRemotes = True
2604         self.maxChanges = ""
2605         self.changes_block_size = None
2606         self.keepRepoPath = False
2607         self.depotPaths = None
2608         self.p4BranchesInGit = []
2609         self.cloneExclude = []
2610         self.useClientSpec = False
2611         self.useClientSpec_from_options = False
2612         self.clientSpecDirs = None
2613         self.tempBranches = []
2614         self.tempBranchLocation = "refs/git-p4-tmp"
2615         self.largeFileSystem = None
2616         self.suppress_meta_comment = False
2617
2618         if gitConfig('git-p4.largeFileSystem'):
2619             largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2620             self.largeFileSystem = largeFileSystemConstructor(
2621                 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2622             )
2623
2624         if gitConfig("git-p4.syncFromOrigin") == "false":
2625             self.syncWithOrigin = False
2626
2627         self.depotPaths = []
2628         self.changeRange = ""
2629         self.previousDepotPaths = []
2630         self.hasOrigin = False
2631
2632         # map from branch depot path to parent branch
2633         self.knownBranches = {}
2634         self.initialParents = {}
2635
2636         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2637         self.labels = {}
2638
2639     # Force a checkpoint in fast-import and wait for it to finish
2640     def checkpoint(self):
2641         self.gitStream.write("checkpoint\n\n")
2642         self.gitStream.write("progress checkpoint\n\n")
2643         out = self.gitOutput.readline()
2644         if self.verbose:
2645             print("checkpoint finished: " + out)
2646
2647     def isPathWanted(self, path):
2648         for p in self.cloneExclude:
2649             if p.endswith("/"):
2650                 if p4PathStartsWith(path, p):
2651                     return False
2652             # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2653             elif path.lower() == p.lower():
2654                 return False
2655         for p in self.depotPaths:
2656             if p4PathStartsWith(path, decode_path(p)):
2657                 return True
2658         return False
2659
2660     def extractFilesFromCommit(self, commit, shelved=False, shelved_cl = 0):
2661         files = []
2662         fnum = 0
2663         while "depotFile%s" % fnum in commit:
2664             path =  commit["depotFile%s" % fnum]
2665             found = self.isPathWanted(decode_path(path))
2666             if not found:
2667                 fnum = fnum + 1
2668                 continue
2669
2670             file = {}
2671             file["path"] = path
2672             file["rev"] = commit["rev%s" % fnum]
2673             file["action"] = commit["action%s" % fnum]
2674             file["type"] = commit["type%s" % fnum]
2675             if shelved:
2676                 file["shelved_cl"] = int(shelved_cl)
2677             files.append(file)
2678             fnum = fnum + 1
2679         return files
2680
2681     def extractJobsFromCommit(self, commit):
2682         jobs = []
2683         jnum = 0
2684         while "job%s" % jnum in commit:
2685             job = commit["job%s" % jnum]
2686             jobs.append(job)
2687             jnum = jnum + 1
2688         return jobs
2689
2690     def stripRepoPath(self, path, prefixes):
2691         """When streaming files, this is called to map a p4 depot path
2692            to where it should go in git.  The prefixes are either
2693            self.depotPaths, or self.branchPrefixes in the case of
2694            branch detection."""
2695
2696         if self.useClientSpec:
2697             # branch detection moves files up a level (the branch name)
2698             # from what client spec interpretation gives
2699             path = decode_path(self.clientSpecDirs.map_in_client(path))
2700             if self.detectBranches:
2701                 for b in self.knownBranches:
2702                     if p4PathStartsWith(path, b + "/"):
2703                         path = path[len(b)+1:]
2704
2705         elif self.keepRepoPath:
2706             # Preserve everything in relative path name except leading
2707             # //depot/; just look at first prefix as they all should
2708             # be in the same depot.
2709             depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2710             if p4PathStartsWith(path, depot):
2711                 path = path[len(depot):]
2712
2713         else:
2714             for p in prefixes:
2715                 if p4PathStartsWith(path, p):
2716                     path = path[len(p):]
2717                     break
2718
2719         path = wildcard_decode(path)
2720         return path
2721
2722     def splitFilesIntoBranches(self, commit):
2723         """Look at each depotFile in the commit to figure out to what
2724            branch it belongs."""
2725
2726         if self.clientSpecDirs:
2727             files = self.extractFilesFromCommit(commit)
2728             self.clientSpecDirs.update_client_spec_path_cache(files)
2729
2730         branches = {}
2731         fnum = 0
2732         while "depotFile%s" % fnum in commit:
2733             raw_path = commit["depotFile%s" % fnum]
2734             path = decode_path(raw_path)
2735             found = self.isPathWanted(path)
2736             if not found:
2737                 fnum = fnum + 1
2738                 continue
2739
2740             file = {}
2741             file["path"] = raw_path
2742             file["rev"] = commit["rev%s" % fnum]
2743             file["action"] = commit["action%s" % fnum]
2744             file["type"] = commit["type%s" % fnum]
2745             fnum = fnum + 1
2746
2747             # start with the full relative path where this file would
2748             # go in a p4 client
2749             if self.useClientSpec:
2750                 relPath = decode_path(self.clientSpecDirs.map_in_client(path))
2751             else:
2752                 relPath = self.stripRepoPath(path, self.depotPaths)
2753
2754             for branch in self.knownBranches.keys():
2755                 # add a trailing slash so that a commit into qt/4.2foo
2756                 # doesn't end up in qt/4.2, e.g.
2757                 if p4PathStartsWith(relPath, branch + "/"):
2758                     if branch not in branches:
2759                         branches[branch] = []
2760                     branches[branch].append(file)
2761                     break
2762
2763         return branches
2764
2765     def writeToGitStream(self, gitMode, relPath, contents):
2766         self.gitStream.write(encode_text_stream(u'M {} inline {}\n'.format(gitMode, relPath)))
2767         self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2768         for d in contents:
2769             self.gitStream.write(d)
2770         self.gitStream.write('\n')
2771
2772     def encodeWithUTF8(self, path):
2773         try:
2774             path.decode('ascii')
2775         except:
2776             encoding = 'utf8'
2777             if gitConfig('git-p4.pathEncoding'):
2778                 encoding = gitConfig('git-p4.pathEncoding')
2779             path = path.decode(encoding, 'replace').encode('utf8', 'replace')
2780             if self.verbose:
2781                 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
2782         return path
2783
2784     # output one file from the P4 stream
2785     # - helper for streamP4Files
2786
2787     def streamOneP4File(self, file, contents):
2788         file_path = file['depotFile']
2789         relPath = self.stripRepoPath(decode_path(file_path), self.branchPrefixes)
2790
2791         if verbose:
2792             if 'fileSize' in self.stream_file:
2793                 size = int(self.stream_file['fileSize'])
2794             else:
2795                 size = 0 # deleted files don't get a fileSize apparently
2796             sys.stdout.write('\r%s --> %s (%i MB)\n' % (file_path, relPath, size/1024/1024))
2797             sys.stdout.flush()
2798
2799         (type_base, type_mods) = split_p4_type(file["type"])
2800
2801         git_mode = "100644"
2802         if "x" in type_mods:
2803             git_mode = "100755"
2804         if type_base == "symlink":
2805             git_mode = "120000"
2806             # p4 print on a symlink sometimes contains "target\n";
2807             # if it does, remove the newline
2808             data = ''.join(decode_text_stream(c) for c in contents)
2809             if not data:
2810                 # Some version of p4 allowed creating a symlink that pointed
2811                 # to nothing.  This causes p4 errors when checking out such
2812                 # a change, and errors here too.  Work around it by ignoring
2813                 # the bad symlink; hopefully a future change fixes it.
2814                 print("\nIgnoring empty symlink in %s" % file_path)
2815                 return
2816             elif data[-1] == '\n':
2817                 contents = [data[:-1]]
2818             else:
2819                 contents = [data]
2820
2821         if type_base == "utf16":
2822             # p4 delivers different text in the python output to -G
2823             # than it does when using "print -o", or normal p4 client
2824             # operations.  utf16 is converted to ascii or utf8, perhaps.
2825             # But ascii text saved as -t utf16 is completely mangled.
2826             # Invoke print -o to get the real contents.
2827             #
2828             # On windows, the newlines will always be mangled by print, so put
2829             # them back too.  This is not needed to the cygwin windows version,
2830             # just the native "NT" type.
2831             #
2832             try:
2833                 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (decode_path(file['depotFile']), file['change'])], raw=True)
2834             except Exception as e:
2835                 if 'Translation of file content failed' in str(e):
2836                     type_base = 'binary'
2837                 else:
2838                     raise e
2839             else:
2840                 if p4_version_string().find('/NT') >= 0:
2841                     text = text.replace(b'\r\n', b'\n')
2842                 contents = [ text ]
2843
2844         if type_base == "apple":
2845             # Apple filetype files will be streamed as a concatenation of
2846             # its appledouble header and the contents.  This is useless
2847             # on both macs and non-macs.  If using "print -q -o xx", it
2848             # will create "xx" with the data, and "%xx" with the header.
2849             # This is also not very useful.
2850             #
2851             # Ideally, someday, this script can learn how to generate
2852             # appledouble files directly and import those to git, but
2853             # non-mac machines can never find a use for apple filetype.
2854             print("\nIgnoring apple filetype file %s" % file['depotFile'])
2855             return
2856
2857         # Note that we do not try to de-mangle keywords on utf16 files,
2858         # even though in theory somebody may want that.
2859         pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2860         if pattern:
2861             regexp = re.compile(pattern, re.VERBOSE)
2862             text = ''.join(decode_text_stream(c) for c in contents)
2863             text = regexp.sub(r'$\1$', text)
2864             contents = [ text ]
2865
2866         if self.largeFileSystem:
2867             (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
2868
2869         self.writeToGitStream(git_mode, relPath, contents)
2870
2871     def streamOneP4Deletion(self, file):
2872         relPath = self.stripRepoPath(decode_path(file['path']), self.branchPrefixes)
2873         if verbose:
2874             sys.stdout.write("delete %s\n" % relPath)
2875             sys.stdout.flush()
2876         self.gitStream.write(encode_text_stream(u'D {}\n'.format(relPath)))
2877
2878         if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2879             self.largeFileSystem.removeLargeFile(relPath)
2880
2881     # handle another chunk of streaming data
2882     def streamP4FilesCb(self, marshalled):
2883
2884         # catch p4 errors and complain
2885         err = None
2886         if "code" in marshalled:
2887             if marshalled["code"] == "error":
2888                 if "data" in marshalled:
2889                     err = marshalled["data"].rstrip()
2890
2891         if not err and 'fileSize' in self.stream_file:
2892             required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
2893             if required_bytes > 0:
2894                 err = 'Not enough space left on %s! Free at least %i MB.' % (
2895                     os.getcwd(), required_bytes/1024/1024
2896                 )
2897
2898         if err:
2899             f = None
2900             if self.stream_have_file_info:
2901                 if "depotFile" in self.stream_file:
2902                     f = self.stream_file["depotFile"]
2903             # force a failure in fast-import, else an empty
2904             # commit will be made
2905             self.gitStream.write("\n")
2906             self.gitStream.write("die-now\n")
2907             self.gitStream.close()
2908             # ignore errors, but make sure it exits first
2909             self.importProcess.wait()
2910             if f:
2911                 die("Error from p4 print for %s: %s" % (f, err))
2912             else:
2913                 die("Error from p4 print: %s" % err)
2914
2915         if 'depotFile' in marshalled and self.stream_have_file_info:
2916             # start of a new file - output the old one first
2917             self.streamOneP4File(self.stream_file, self.stream_contents)
2918             self.stream_file = {}
2919             self.stream_contents = []
2920             self.stream_have_file_info = False
2921
2922         # pick up the new file information... for the
2923         # 'data' field we need to append to our array
2924         for k in marshalled.keys():
2925             if k == 'data':
2926                 if 'streamContentSize' not in self.stream_file:
2927                     self.stream_file['streamContentSize'] = 0
2928                 self.stream_file['streamContentSize'] += len(marshalled['data'])
2929                 self.stream_contents.append(marshalled['data'])
2930             else:
2931                 self.stream_file[k] = marshalled[k]
2932
2933         if (verbose and
2934             'streamContentSize' in self.stream_file and
2935             'fileSize' in self.stream_file and
2936             'depotFile' in self.stream_file):
2937             size = int(self.stream_file["fileSize"])
2938             if size > 0:
2939                 progress = 100*self.stream_file['streamContentSize']/size
2940                 sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
2941                 sys.stdout.flush()
2942
2943         self.stream_have_file_info = True
2944
2945     # Stream directly from "p4 files" into "git fast-import"
2946     def streamP4Files(self, files):
2947         filesForCommit = []
2948         filesToRead = []
2949         filesToDelete = []
2950
2951         for f in files:
2952             filesForCommit.append(f)
2953             if f['action'] in self.delete_actions:
2954                 filesToDelete.append(f)
2955             else:
2956                 filesToRead.append(f)
2957
2958         # deleted files...
2959         for f in filesToDelete:
2960             self.streamOneP4Deletion(f)
2961
2962         if len(filesToRead) > 0:
2963             self.stream_file = {}
2964             self.stream_contents = []
2965             self.stream_have_file_info = False
2966
2967             # curry self argument
2968             def streamP4FilesCbSelf(entry):
2969                 self.streamP4FilesCb(entry)
2970
2971             fileArgs = []
2972             for f in filesToRead:
2973                 if 'shelved_cl' in f:
2974                     # Handle shelved CLs using the "p4 print file@=N" syntax to print
2975                     # the contents
2976                     fileArg = f['path'] + encode_text_stream('@={}'.format(f['shelved_cl']))
2977                 else:
2978                     fileArg = f['path'] + encode_text_stream('#{}'.format(f['rev']))
2979
2980                 fileArgs.append(fileArg)
2981
2982             p4CmdList(["-x", "-", "print"],
2983                       stdin=fileArgs,
2984                       cb=streamP4FilesCbSelf)
2985
2986             # do the last chunk
2987             if 'depotFile' in self.stream_file:
2988                 self.streamOneP4File(self.stream_file, self.stream_contents)
2989
2990     def make_email(self, userid):
2991         if userid in self.users:
2992             return self.users[userid]
2993         else:
2994             return "%s <a@b>" % userid
2995
2996     def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2997         """ Stream a p4 tag.
2998         commit is either a git commit, or a fast-import mark, ":<p4commit>"
2999         """
3000
3001         if verbose:
3002             print("writing tag %s for commit %s" % (labelName, commit))
3003         gitStream.write("tag %s\n" % labelName)
3004         gitStream.write("from %s\n" % commit)
3005
3006         if 'Owner' in labelDetails:
3007             owner = labelDetails["Owner"]
3008         else:
3009             owner = None
3010
3011         # Try to use the owner of the p4 label, or failing that,
3012         # the current p4 user id.
3013         if owner:
3014             email = self.make_email(owner)
3015         else:
3016             email = self.make_email(self.p4UserId())
3017         tagger = "%s %s %s" % (email, epoch, self.tz)
3018
3019         gitStream.write("tagger %s\n" % tagger)
3020
3021         print("labelDetails=",labelDetails)
3022         if 'Description' in labelDetails:
3023             description = labelDetails['Description']
3024         else:
3025             description = 'Label from git p4'
3026
3027         gitStream.write("data %d\n" % len(description))
3028         gitStream.write(description)
3029         gitStream.write("\n")
3030
3031     def inClientSpec(self, path):
3032         if not self.clientSpecDirs:
3033             return True
3034         inClientSpec = self.clientSpecDirs.map_in_client(path)
3035         if not inClientSpec and self.verbose:
3036             print('Ignoring file outside of client spec: {0}'.format(path))
3037         return inClientSpec
3038
3039     def hasBranchPrefix(self, path):
3040         if not self.branchPrefixes:
3041             return True
3042         hasPrefix = [p for p in self.branchPrefixes
3043                         if p4PathStartsWith(path, p)]
3044         if not hasPrefix and self.verbose:
3045             print('Ignoring file outside of prefix: {0}'.format(path))
3046         return hasPrefix
3047
3048     def commit(self, details, files, branch, parent = "", allow_empty=False):
3049         epoch = details["time"]
3050         author = details["user"]
3051         jobs = self.extractJobsFromCommit(details)
3052
3053         if self.verbose:
3054             print('commit into {0}'.format(branch))
3055
3056         if self.clientSpecDirs:
3057             self.clientSpecDirs.update_client_spec_path_cache(files)
3058
3059         files = [f for (f, path) in ((f, decode_path(f['path'])) for f in files)
3060             if self.inClientSpec(path) and self.hasBranchPrefix(path)]
3061
3062         if gitConfigBool('git-p4.keepEmptyCommits'):
3063             allow_empty = True
3064
3065         if not files and not allow_empty:
3066             print('Ignoring revision {0} as it would produce an empty commit.'
3067                 .format(details['change']))
3068             return
3069
3070         self.gitStream.write("commit %s\n" % branch)
3071         self.gitStream.write("mark :%s\n" % details["change"])
3072         self.committedChanges.add(int(details["change"]))
3073         committer = ""
3074         if author not in self.users:
3075             self.getUserMapFromPerforceServer()
3076         committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
3077
3078         self.gitStream.write("committer %s\n" % committer)
3079
3080         self.gitStream.write("data <<EOT\n")
3081         self.gitStream.write(details["desc"])
3082         if len(jobs) > 0:
3083             self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
3084
3085         if not self.suppress_meta_comment:
3086             self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3087                                 (','.join(self.branchPrefixes), details["change"]))
3088             if len(details['options']) > 0:
3089                 self.gitStream.write(": options = %s" % details['options'])
3090             self.gitStream.write("]\n")
3091
3092         self.gitStream.write("EOT\n\n")
3093
3094         if len(parent) > 0:
3095             if self.verbose:
3096                 print("parent %s" % parent)
3097             self.gitStream.write("from %s\n" % parent)
3098
3099         self.streamP4Files(files)
3100         self.gitStream.write("\n")
3101
3102         change = int(details["change"])
3103
3104         if change in self.labels:
3105             label = self.labels[change]
3106             labelDetails = label[0]
3107             labelRevisions = label[1]
3108             if self.verbose:
3109                 print("Change %s is labelled %s" % (change, labelDetails))
3110
3111             files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
3112                                                 for p in self.branchPrefixes])
3113
3114             if len(files) == len(labelRevisions):
3115
3116                 cleanedFiles = {}
3117                 for info in files:
3118                     if info["action"] in self.delete_actions:
3119                         continue
3120                     cleanedFiles[info["depotFile"]] = info["rev"]
3121
3122                 if cleanedFiles == labelRevisions:
3123                     self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
3124
3125                 else:
3126                     if not self.silent:
3127                         print("Tag %s does not match with change %s: files do not match."
3128                                % (labelDetails["label"], change))
3129
3130             else:
3131                 if not self.silent:
3132                     print("Tag %s does not match with change %s: file count is different."
3133                            % (labelDetails["label"], change))
3134
3135     # Build a dictionary of changelists and labels, for "detect-labels" option.
3136     def getLabels(self):
3137         self.labels = {}
3138
3139         l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
3140         if len(l) > 0 and not self.silent:
3141             print("Finding files belonging to labels in %s" % self.depotPaths)
3142
3143         for output in l:
3144             label = output["label"]
3145             revisions = {}
3146             newestChange = 0
3147             if self.verbose:
3148                 print("Querying files for label %s" % label)
3149             for file in p4CmdList(["files"] +
3150                                       ["%s...@%s" % (p, label)
3151                                           for p in self.depotPaths]):
3152                 revisions[file["depotFile"]] = file["rev"]
3153                 change = int(file["change"])
3154                 if change > newestChange:
3155                     newestChange = change
3156
3157             self.labels[newestChange] = [output, revisions]
3158
3159         if self.verbose:
3160             print("Label changes: %s" % self.labels.keys())
3161
3162     # Import p4 labels as git tags. A direct mapping does not
3163     # exist, so assume that if all the files are at the same revision
3164     # then we can use that, or it's something more complicated we should
3165     # just ignore.
3166     def importP4Labels(self, stream, p4Labels):
3167         if verbose:
3168             print("import p4 labels: " + ' '.join(p4Labels))
3169
3170         ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
3171         validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
3172         if len(validLabelRegexp) == 0:
3173             validLabelRegexp = defaultLabelRegexp
3174         m = re.compile(validLabelRegexp)
3175
3176         for name in p4Labels:
3177             commitFound = False
3178
3179             if not m.match(name):
3180                 if verbose:
3181                     print("label %s does not match regexp %s" % (name,validLabelRegexp))
3182                 continue
3183
3184             if name in ignoredP4Labels:
3185                 continue
3186
3187             labelDetails = p4CmdList(['label', "-o", name])[0]
3188
3189             # get the most recent changelist for each file in this label
3190             change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3191                                 for p in self.depotPaths])
3192
3193             if 'change' in change:
3194                 # find the corresponding git commit; take the oldest commit
3195                 changelist = int(change['change'])
3196                 if changelist in self.committedChanges:
3197                     gitCommit = ":%d" % changelist       # use a fast-import mark
3198                     commitFound = True
3199                 else:
3200                     gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3201                         "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3202                     if len(gitCommit) == 0:
3203                         print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
3204                     else:
3205                         commitFound = True
3206                         gitCommit = gitCommit.strip()
3207
3208                 if commitFound:
3209                     # Convert from p4 time format
3210                     try:
3211                         tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3212                     except ValueError:
3213                         print("Could not convert label time %s" % labelDetails['Update'])
3214                         tmwhen = 1
3215
3216                     when = int(time.mktime(tmwhen))
3217                     self.streamTag(stream, name, labelDetails, gitCommit, when)
3218                     if verbose:
3219                         print("p4 label %s mapped to git commit %s" % (name, gitCommit))
3220             else:
3221                 if verbose:
3222                     print("Label %s has no changelists - possibly deleted?" % name)
3223
3224             if not commitFound:
3225                 # We can't import this label; don't try again as it will get very
3226                 # expensive repeatedly fetching all the files for labels that will
3227                 # never be imported. If the label is moved in the future, the
3228                 # ignore will need to be removed manually.
3229                 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3230
3231     def guessProjectName(self):
3232         for p in self.depotPaths:
3233             if p.endswith("/"):
3234                 p = p[:-1]
3235             p = p[p.strip().rfind("/") + 1:]
3236             if not p.endswith("/"):
3237                p += "/"
3238             return p
3239
3240     def getBranchMapping(self):
3241         lostAndFoundBranches = set()
3242
3243         user = gitConfig("git-p4.branchUser")
3244         if len(user) > 0:
3245             command = "branches -u %s" % user
3246         else:
3247             command = "branches"
3248
3249         for info in p4CmdList(command):
3250             details = p4Cmd(["branch", "-o", info["branch"]])
3251             viewIdx = 0
3252             while "View%s" % viewIdx in details:
3253                 paths = details["View%s" % viewIdx].split(" ")
3254                 viewIdx = viewIdx + 1
3255                 # require standard //depot/foo/... //depot/bar/... mapping
3256                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3257                     continue
3258                 source = paths[0]
3259                 destination = paths[1]
3260                 ## HACK
3261                 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
3262                     source = source[len(self.depotPaths[0]):-4]
3263                     destination = destination[len(self.depotPaths[0]):-4]
3264
3265                     if destination in self.knownBranches:
3266                         if not self.silent:
3267                             print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3268                             print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
3269                         continue
3270
3271                     self.knownBranches[destination] = source
3272
3273                     lostAndFoundBranches.discard(destination)
3274
3275                     if source not in self.knownBranches:
3276                         lostAndFoundBranches.add(source)
3277
3278         # Perforce does not strictly require branches to be defined, so we also
3279         # check git config for a branch list.
3280         #
3281         # Example of branch definition in git config file:
3282         # [git-p4]
3283         #   branchList=main:branchA
3284         #   branchList=main:branchB
3285         #   branchList=branchA:branchC
3286         configBranches = gitConfigList("git-p4.branchList")
3287         for branch in configBranches:
3288             if branch:
3289                 (source, destination) = branch.split(":")
3290                 self.knownBranches[destination] = source
3291
3292                 lostAndFoundBranches.discard(destination)
3293
3294                 if source not in self.knownBranches:
3295                     lostAndFoundBranches.add(source)
3296
3297
3298         for branch in lostAndFoundBranches:
3299             self.knownBranches[branch] = branch
3300
3301     def getBranchMappingFromGitBranches(self):
3302         branches = p4BranchesInGit(self.importIntoRemotes)
3303         for branch in branches.keys():
3304             if branch == "master":
3305                 branch = "main"
3306             else:
3307                 branch = branch[len(self.projectName):]
3308             self.knownBranches[branch] = branch
3309
3310     def updateOptionDict(self, d):
3311         option_keys = {}
3312         if self.keepRepoPath:
3313             option_keys['keepRepoPath'] = 1
3314
3315         d["options"] = ' '.join(sorted(option_keys.keys()))
3316
3317     def readOptions(self, d):
3318         self.keepRepoPath = ('options' in d
3319                              and ('keepRepoPath' in d['options']))
3320
3321     def gitRefForBranch(self, branch):
3322         if branch == "main":
3323             return self.refPrefix + "master"
3324
3325         if len(branch) <= 0:
3326             return branch
3327
3328         return self.refPrefix + self.projectName + branch
3329
3330     def gitCommitByP4Change(self, ref, change):
3331         if self.verbose:
3332             print("looking in ref " + ref + " for change %s using bisect..." % change)
3333
3334         earliestCommit = ""
3335         latestCommit = parseRevision(ref)
3336
3337         while True:
3338             if self.verbose:
3339                 print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
3340             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
3341             if len(next) == 0:
3342                 if self.verbose:
3343                     print("argh")
3344                 return ""
3345             log = extractLogMessageFromGitCommit(next)
3346             settings = extractSettingsGitLog(log)
3347             currentChange = int(settings['change'])
3348             if self.verbose:
3349                 print("current change %s" % currentChange)
3350
3351             if currentChange == change:
3352                 if self.verbose:
3353                     print("found %s" % next)
3354                 return next
3355
3356             if currentChange < change:
3357                 earliestCommit = "^%s" % next
3358             else:
3359                 if next == latestCommit:
3360                     die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3361                 latestCommit = "%s^@" % next
3362
3363         return ""
3364
3365     def importNewBranch(self, branch, maxChange):
3366         # make fast-import flush all changes to disk and update the refs using the checkpoint
3367         # command so that we can try to find the branch parent in the git history
3368         self.gitStream.write("checkpoint\n\n");
3369         self.gitStream.flush();
3370         branchPrefix = self.depotPaths[0] + branch + "/"
3371         range = "@1,%s" % maxChange
3372         #print "prefix" + branchPrefix
3373         changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3374         if len(changes) <= 0:
3375             return False
3376         firstChange = changes[0]
3377         #print "first change in branch: %s" % firstChange
3378         sourceBranch = self.knownBranches[branch]
3379         sourceDepotPath = self.depotPaths[0] + sourceBranch
3380         sourceRef = self.gitRefForBranch(sourceBranch)
3381         #print "source " + sourceBranch
3382
3383         branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3384         #print "branch parent: %s" % branchParentChange
3385         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3386         if len(gitParent) > 0:
3387             self.initialParents[self.gitRefForBranch(branch)] = gitParent
3388             #print "parent git commit: %s" % gitParent
3389
3390         self.importChanges(changes)
3391         return True
3392
3393     def searchParent(self, parent, branch, target):
3394         parentFound = False
3395         for blob in read_pipe_lines(["git", "rev-list", "--reverse",
3396                                      "--no-merges", parent]):
3397             blob = blob.strip()
3398             if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
3399                 parentFound = True
3400                 if self.verbose:
3401                     print("Found parent of %s in commit %s" % (branch, blob))
3402                 break
3403         if parentFound:
3404             return blob
3405         else:
3406             return None
3407
3408     def importChanges(self, changes, origin_revision=0):
3409         cnt = 1
3410         for change in changes:
3411             description = p4_describe(change)
3412             self.updateOptionDict(description)
3413
3414             if not self.silent:
3415                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
3416                 sys.stdout.flush()
3417             cnt = cnt + 1
3418
3419             try:
3420                 if self.detectBranches:
3421                     branches = self.splitFilesIntoBranches(description)
3422                     for branch in branches.keys():
3423                         ## HACK  --hwn
3424                         branchPrefix = self.depotPaths[0] + branch + "/"
3425                         self.branchPrefixes = [ branchPrefix ]
3426
3427                         parent = ""
3428
3429                         filesForCommit = branches[branch]
3430
3431                         if self.verbose:
3432                             print("branch is %s" % branch)
3433
3434                         self.updatedBranches.add(branch)
3435
3436                         if branch not in self.createdBranches:
3437                             self.createdBranches.add(branch)
3438                             parent = self.knownBranches[branch]
3439                             if parent == branch:
3440                                 parent = ""
3441                             else:
3442                                 fullBranch = self.projectName + branch
3443                                 if fullBranch not in self.p4BranchesInGit:
3444                                     if not self.silent:
3445                                         print("\n    Importing new branch %s" % fullBranch);
3446                                     if self.importNewBranch(branch, change - 1):
3447                                         parent = ""
3448                                         self.p4BranchesInGit.append(fullBranch)
3449                                     if not self.silent:
3450                                         print("\n    Resuming with change %s" % change);
3451
3452                                 if self.verbose:
3453                                     print("parent determined through known branches: %s" % parent)
3454
3455                         branch = self.gitRefForBranch(branch)
3456                         parent = self.gitRefForBranch(parent)
3457
3458                         if self.verbose:
3459                             print("looking for initial parent for %s; current parent is %s" % (branch, parent))
3460
3461                         if len(parent) == 0 and branch in self.initialParents:
3462                             parent = self.initialParents[branch]
3463                             del self.initialParents[branch]
3464
3465                         blob = None
3466                         if len(parent) > 0:
3467                             tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3468                             if self.verbose:
3469                                 print("Creating temporary branch: " + tempBranch)
3470                             self.commit(description, filesForCommit, tempBranch)
3471                             self.tempBranches.append(tempBranch)
3472                             self.checkpoint()
3473                             blob = self.searchParent(parent, branch, tempBranch)
3474                         if blob:
3475                             self.commit(description, filesForCommit, branch, blob)
3476                         else:
3477                             if self.verbose:
3478                                 print("Parent of %s not found. Committing into head of %s" % (branch, parent))
3479                             self.commit(description, filesForCommit, branch, parent)
3480                 else:
3481                     files = self.extractFilesFromCommit(description)
3482                     self.commit(description, files, self.branch,
3483                                 self.initialParent)
3484                     # only needed once, to connect to the previous commit
3485                     self.initialParent = ""
3486             except IOError:
3487                 print(self.gitError.read())
3488                 sys.exit(1)
3489
3490     def sync_origin_only(self):
3491         if self.syncWithOrigin:
3492             self.hasOrigin = originP4BranchesExist()
3493             if self.hasOrigin:
3494                 if not self.silent:
3495                     print('Syncing with origin first, using "git fetch origin"')
3496                 system("git fetch origin")
3497
3498     def importHeadRevision(self, revision):
3499         print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
3500
3501         details = {}
3502         details["user"] = "git perforce import user"
3503         details["desc"] = ("Initial import of %s from the state at revision %s\n"
3504                            % (' '.join(self.depotPaths), revision))
3505         details["change"] = revision
3506         newestRevision = 0
3507
3508         fileCnt = 0
3509         fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3510
3511         for info in p4CmdList(["files"] + fileArgs):
3512
3513             if 'code' in info and info['code'] == 'error':
3514                 sys.stderr.write("p4 returned an error: %s\n"
3515                                  % info['data'])
3516                 if info['data'].find("must refer to client") >= 0:
3517                     sys.stderr.write("This particular p4 error is misleading.\n")
3518                     sys.stderr.write("Perhaps the depot path was misspelled.\n");
3519                     sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
3520                 sys.exit(1)
3521             if 'p4ExitCode' in info:
3522                 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3523                 sys.exit(1)
3524
3525
3526             change = int(info["change"])
3527             if change > newestRevision:
3528                 newestRevision = change
3529
3530             if info["action"] in self.delete_actions:
3531                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3532                 #fileCnt = fileCnt + 1
3533                 continue
3534
3535             for prop in ["depotFile", "rev", "action", "type" ]:
3536                 details["%s%s" % (prop, fileCnt)] = info[prop]
3537
3538             fileCnt = fileCnt + 1
3539
3540         details["change"] = newestRevision
3541
3542         # Use time from top-most change so that all git p4 clones of
3543         # the same p4 repo have the same commit SHA1s.
3544         res = p4_describe(newestRevision)
3545         details["time"] = res["time"]
3546
3547         self.updateOptionDict(details)
3548         try:
3549             self.commit(details, self.extractFilesFromCommit(details), self.branch)
3550         except IOError as err:
3551             print("IO error with git fast-import. Is your git version recent enough?")
3552             print("IO error details: {}".format(err))
3553             print(self.gitError.read())
3554
3555     def openStreams(self):
3556         self.importProcess = subprocess.Popen(["git", "fast-import"],
3557                                               stdin=subprocess.PIPE,
3558                                               stdout=subprocess.PIPE,
3559                                               stderr=subprocess.PIPE);
3560         self.gitOutput = self.importProcess.stdout
3561         self.gitStream = self.importProcess.stdin
3562         self.gitError = self.importProcess.stderr
3563
3564         if bytes is not str:
3565             # Wrap gitStream.write() so that it can be called using `str` arguments
3566             def make_encoded_write(write):
3567                 def encoded_write(s):
3568                     return write(s.encode() if isinstance(s, str) else s)
3569                 return encoded_write
3570
3571             self.gitStream.write = make_encoded_write(self.gitStream.write)
3572
3573     def closeStreams(self):
3574         self.gitStream.close()
3575         if self.importProcess.wait() != 0:
3576             die("fast-import failed: %s" % self.gitError.read())
3577         self.gitOutput.close()
3578         self.gitError.close()
3579
3580     def run(self, args):
3581         if self.importIntoRemotes:
3582             self.refPrefix = "refs/remotes/p4/"
3583         else:
3584             self.refPrefix = "refs/heads/p4/"
3585
3586         self.sync_origin_only()
3587
3588         branch_arg_given = bool(self.branch)
3589         if len(self.branch) == 0:
3590             self.branch = self.refPrefix + "master"
3591             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
3592                 system("git update-ref %s refs/heads/p4" % self.branch)
3593                 system("git branch -D p4")
3594
3595         # accept either the command-line option, or the configuration variable
3596         if self.useClientSpec:
3597             # will use this after clone to set the variable
3598             self.useClientSpec_from_options = True
3599         else:
3600             if gitConfigBool("git-p4.useclientspec"):
3601                 self.useClientSpec = True
3602         if self.useClientSpec:
3603             self.clientSpecDirs = getClientSpec()
3604
3605         # TODO: should always look at previous commits,
3606         # merge with previous imports, if possible.
3607         if args == []:
3608             if self.hasOrigin:
3609                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
3610
3611             # branches holds mapping from branch name to sha1
3612             branches = p4BranchesInGit(self.importIntoRemotes)
3613
3614             # restrict to just this one, disabling detect-branches
3615             if branch_arg_given:
3616                 short = self.branch.split("/")[-1]
3617                 if short in branches:
3618                     self.p4BranchesInGit = [ short ]
3619             else:
3620                 self.p4BranchesInGit = branches.keys()
3621
3622             if len(self.p4BranchesInGit) > 1:
3623                 if not self.silent:
3624                     print("Importing from/into multiple branches")
3625                 self.detectBranches = True
3626                 for branch in branches.keys():
3627                     self.initialParents[self.refPrefix + branch] = \
3628                         branches[branch]
3629
3630             if self.verbose:
3631                 print("branches: %s" % self.p4BranchesInGit)
3632
3633             p4Change = 0
3634             for branch in self.p4BranchesInGit:
3635                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
3636
3637                 settings = extractSettingsGitLog(logMsg)
3638
3639                 self.readOptions(settings)
3640                 if ('depot-paths' in settings
3641                     and 'change' in settings):
3642                     change = int(settings['change']) + 1
3643                     p4Change = max(p4Change, change)
3644
3645                     depotPaths = sorted(settings['depot-paths'])
3646                     if self.previousDepotPaths == []:
3647                         self.previousDepotPaths = depotPaths
3648                     else:
3649                         paths = []
3650                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
3651                             prev_list = prev.split("/")
3652                             cur_list = cur.split("/")
3653                             for i in range(0, min(len(cur_list), len(prev_list))):
3654                                 if cur_list[i] != prev_list[i]:
3655                                     i = i - 1
3656                                     break
3657
3658                             paths.append ("/".join(cur_list[:i + 1]))
3659
3660                         self.previousDepotPaths = paths
3661
3662             if p4Change > 0:
3663                 self.depotPaths = sorted(self.previousDepotPaths)
3664                 self.changeRange = "@%s,#head" % p4Change
3665                 if not self.silent and not self.detectBranches:
3666                     print("Performing incremental import into %s git branch" % self.branch)
3667
3668         # accept multiple ref name abbreviations:
3669         #    refs/foo/bar/branch -> use it exactly
3670         #    p4/branch -> prepend refs/remotes/ or refs/heads/
3671         #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
3672         if not self.branch.startswith("refs/"):
3673             if self.importIntoRemotes:
3674                 prepend = "refs/remotes/"
3675             else:
3676                 prepend = "refs/heads/"
3677             if not self.branch.startswith("p4/"):
3678                 prepend += "p4/"
3679             self.branch = prepend + self.branch
3680
3681         if len(args) == 0 and self.depotPaths:
3682             if not self.silent:
3683                 print("Depot paths: %s" % ' '.join(self.depotPaths))
3684         else:
3685             if self.depotPaths and self.depotPaths != args:
3686                 print("previous import used depot path %s and now %s was specified. "
3687                        "This doesn't work!" % (' '.join (self.depotPaths),
3688                                                ' '.join (args)))
3689                 sys.exit(1)
3690
3691             self.depotPaths = sorted(args)
3692
3693         revision = ""
3694         self.users = {}
3695
3696         # Make sure no revision specifiers are used when --changesfile
3697         # is specified.
3698         bad_changesfile = False
3699         if len(self.changesFile) > 0:
3700             for p in self.depotPaths:
3701                 if p.find("@") >= 0 or p.find("#") >= 0:
3702                     bad_changesfile = True
3703                     break
3704         if bad_changesfile:
3705             die("Option --changesfile is incompatible with revision specifiers")
3706
3707         newPaths = []
3708         for p in self.depotPaths:
3709             if p.find("@") != -1:
3710                 atIdx = p.index("@")
3711                 self.changeRange = p[atIdx:]
3712                 if self.changeRange == "@all":
3713                     self.changeRange = ""
3714                 elif ',' not in self.changeRange:
3715                     revision = self.changeRange
3716                     self.changeRange = ""
3717                 p = p[:atIdx]
3718             elif p.find("#") != -1:
3719                 hashIdx = p.index("#")
3720                 revision = p[hashIdx:]
3721                 p = p[:hashIdx]
3722             elif self.previousDepotPaths == []:
3723                 # pay attention to changesfile, if given, else import
3724                 # the entire p4 tree at the head revision
3725                 if len(self.changesFile) == 0:
3726                     revision = "#head"
3727
3728             p = re.sub ("\.\.\.$", "", p)
3729             if not p.endswith("/"):
3730                 p += "/"
3731
3732             newPaths.append(p)
3733
3734         self.depotPaths = newPaths
3735
3736         # --detect-branches may change this for each branch
3737         self.branchPrefixes = self.depotPaths
3738
3739         self.loadUserMapFromCache()
3740         self.labels = {}
3741         if self.detectLabels:
3742             self.getLabels();
3743
3744         if self.detectBranches:
3745             ## FIXME - what's a P4 projectName ?
3746             self.projectName = self.guessProjectName()
3747
3748             if self.hasOrigin:
3749                 self.getBranchMappingFromGitBranches()
3750             else:
3751                 self.getBranchMapping()
3752             if self.verbose:
3753                 print("p4-git branches: %s" % self.p4BranchesInGit)
3754                 print("initial parents: %s" % self.initialParents)
3755             for b in self.p4BranchesInGit:
3756                 if b != "master":
3757
3758                     ## FIXME
3759                     b = b[len(self.projectName):]
3760                 self.createdBranches.add(b)
3761
3762         self.openStreams()
3763
3764         if revision:
3765             self.importHeadRevision(revision)
3766         else:
3767             changes = []
3768
3769             if len(self.changesFile) > 0:
3770                 output = open(self.changesFile).readlines()
3771                 changeSet = set()
3772                 for line in output:
3773                     changeSet.add(int(line))
3774
3775                 for change in changeSet:
3776                     changes.append(change)
3777
3778                 changes.sort()
3779             else:
3780                 # catch "git p4 sync" with no new branches, in a repo that
3781                 # does not have any existing p4 branches
3782                 if len(args) == 0:
3783                     if not self.p4BranchesInGit:
3784                         die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
3785
3786                     # The default branch is master, unless --branch is used to
3787                     # specify something else.  Make sure it exists, or complain
3788                     # nicely about how to use --branch.
3789                     if not self.detectBranches:
3790                         if not branch_exists(self.branch):
3791                             if branch_arg_given:
3792                                 die("Error: branch %s does not exist." % self.branch)
3793                             else:
3794                                 die("Error: no branch %s; perhaps specify one with --branch." %
3795                                     self.branch)
3796
3797                 if self.verbose:
3798                     print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3799                                                               self.changeRange))
3800                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3801
3802                 if len(self.maxChanges) > 0:
3803                     changes = changes[:min(int(self.maxChanges), len(changes))]
3804
3805             if len(changes) == 0:
3806                 if not self.silent:
3807                     print("No changes to import!")
3808             else:
3809                 if not self.silent and not self.detectBranches:
3810                     print("Import destination: %s" % self.branch)
3811
3812                 self.updatedBranches = set()
3813
3814                 if not self.detectBranches:
3815                     if args:
3816                         # start a new branch
3817                         self.initialParent = ""
3818                     else:
3819                         # build on a previous revision
3820                         self.initialParent = parseRevision(self.branch)
3821
3822                 self.importChanges(changes)
3823
3824                 if not self.silent:
3825                     print("")
3826                     if len(self.updatedBranches) > 0:
3827                         sys.stdout.write("Updated branches: ")
3828                         for b in self.updatedBranches:
3829                             sys.stdout.write("%s " % b)
3830                         sys.stdout.write("\n")
3831
3832         if gitConfigBool("git-p4.importLabels"):
3833             self.importLabels = True
3834
3835         if self.importLabels:
3836             p4Labels = getP4Labels(self.depotPaths)
3837             gitTags = getGitTags()
3838
3839             missingP4Labels = p4Labels - gitTags
3840             self.importP4Labels(self.gitStream, missingP4Labels)
3841
3842         self.closeStreams()
3843
3844         # Cleanup temporary branches created during import
3845         if self.tempBranches != []:
3846             for branch in self.tempBranches:
3847                 read_pipe("git update-ref -d %s" % branch)
3848             os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3849
3850         # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3851         # a convenient shortcut refname "p4".
3852         if self.importIntoRemotes:
3853             head_ref = self.refPrefix + "HEAD"
3854             if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3855                 system(["git", "symbolic-ref", head_ref, self.branch])
3856
3857         return True
3858
3859 class P4Rebase(Command):
3860     def __init__(self):
3861         Command.__init__(self)
3862         self.options = [
3863                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3864         ]
3865         self.importLabels = False
3866         self.description = ("Fetches the latest revision from perforce and "
3867                             + "rebases the current work (branch) against it")
3868
3869     def run(self, args):
3870         sync = P4Sync()
3871         sync.importLabels = self.importLabels
3872         sync.run([])
3873
3874         return self.rebase()
3875
3876     def rebase(self):
3877         if os.system("git update-index --refresh") != 0:
3878             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.");
3879         if len(read_pipe("git diff-index HEAD --")) > 0:
3880             die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3881
3882         [upstream, settings] = findUpstreamBranchPoint()
3883         if len(upstream) == 0:
3884             die("Cannot find upstream branchpoint for rebase")
3885
3886         # the branchpoint may be p4/foo~3, so strip off the parent
3887         upstream = re.sub("~[0-9]+$", "", upstream)
3888
3889         print("Rebasing the current branch onto %s" % upstream)
3890         oldHead = read_pipe("git rev-parse HEAD").strip()
3891         system("git rebase %s" % upstream)
3892         system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3893         return True
3894
3895 class P4Clone(P4Sync):
3896     def __init__(self):
3897         P4Sync.__init__(self)
3898         self.description = "Creates a new git repository and imports from Perforce into it"
3899         self.usage = "usage: %prog [options] //depot/path[@revRange]"
3900         self.options += [
3901             optparse.make_option("--destination", dest="cloneDestination",
3902                                  action='store', default=None,
3903                                  help="where to leave result of the clone"),
3904             optparse.make_option("--bare", dest="cloneBare",
3905                                  action="store_true", default=False),
3906         ]
3907         self.cloneDestination = None
3908         self.needsGit = False
3909         self.cloneBare = False
3910
3911     def defaultDestination(self, args):
3912         ## TODO: use common prefix of args?
3913         depotPath = args[0]
3914         depotDir = re.sub("(@[^@]*)$", "", depotPath)
3915         depotDir = re.sub("(#[^#]*)$", "", depotDir)
3916         depotDir = re.sub(r"\.\.\.$", "", depotDir)
3917         depotDir = re.sub(r"/$", "", depotDir)
3918         return os.path.split(depotDir)[1]
3919
3920     def run(self, args):
3921         if len(args) < 1:
3922             return False
3923
3924         if self.keepRepoPath and not self.cloneDestination:
3925             sys.stderr.write("Must specify destination for --keep-path\n")
3926             sys.exit(1)
3927
3928         depotPaths = args
3929
3930         if not self.cloneDestination and len(depotPaths) > 1:
3931             self.cloneDestination = depotPaths[-1]
3932             depotPaths = depotPaths[:-1]
3933
3934         for p in depotPaths:
3935             if not p.startswith("//"):
3936                 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3937                 return False
3938
3939         if not self.cloneDestination:
3940             self.cloneDestination = self.defaultDestination(args)
3941
3942         print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
3943
3944         if not os.path.exists(self.cloneDestination):
3945             os.makedirs(self.cloneDestination)
3946         chdir(self.cloneDestination)
3947
3948         init_cmd = [ "git", "init" ]
3949         if self.cloneBare:
3950             init_cmd.append("--bare")
3951         retcode = subprocess.call(init_cmd)
3952         if retcode:
3953             raise CalledProcessError(retcode, init_cmd)
3954
3955         if not P4Sync.run(self, depotPaths):
3956             return False
3957
3958         # create a master branch and check out a work tree
3959         if gitBranchExists(self.branch):
3960             system([ "git", "branch", "master", self.branch ])
3961             if not self.cloneBare:
3962                 system([ "git", "checkout", "-f" ])
3963         else:
3964             print('Not checking out any branch, use ' \
3965                   '"git checkout -q -b master <branch>"')
3966
3967         # auto-set this variable if invoked with --use-client-spec
3968         if self.useClientSpec_from_options:
3969             system("git config --bool git-p4.useclientspec true")
3970
3971         return True
3972
3973 class P4Unshelve(Command):
3974     def __init__(self):
3975         Command.__init__(self)
3976         self.options = []
3977         self.origin = "HEAD"
3978         self.description = "Unshelve a P4 changelist into a git commit"
3979         self.usage = "usage: %prog [options] changelist"
3980         self.options += [
3981                 optparse.make_option("--origin", dest="origin",
3982                     help="Use this base revision instead of the default (%s)" % self.origin),
3983         ]
3984         self.verbose = False
3985         self.noCommit = False
3986         self.destbranch = "refs/remotes/p4-unshelved"
3987
3988     def renameBranch(self, branch_name):
3989         """ Rename the existing branch to branch_name.N
3990         """
3991
3992         found = True
3993         for i in range(0,1000):
3994             backup_branch_name = "{0}.{1}".format(branch_name, i)
3995             if not gitBranchExists(backup_branch_name):
3996                 gitUpdateRef(backup_branch_name, branch_name) # copy ref to backup
3997                 gitDeleteRef(branch_name)
3998                 found = True
3999                 print("renamed old unshelve branch to {0}".format(backup_branch_name))
4000                 break
4001
4002         if not found:
4003             sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
4004
4005     def findLastP4Revision(self, starting_point):
4006         """ Look back from starting_point for the first commit created by git-p4
4007             to find the P4 commit we are based on, and the depot-paths.
4008         """
4009
4010         for parent in (range(65535)):
4011             log = extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))
4012             settings = extractSettingsGitLog(log)
4013             if 'change' in settings:
4014                 return settings
4015
4016         sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4017
4018     def createShelveParent(self, change, branch_name, sync, origin):
4019         """ Create a commit matching the parent of the shelved changelist 'change'
4020         """
4021         parent_description = p4_describe(change, shelved=True)
4022         parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4023         files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4024
4025         parent_files = []
4026         for f in files:
4027             # if it was added in the shelved changelist, it won't exist in the parent
4028             if f['action'] in self.add_actions:
4029                 continue
4030
4031             # if it was deleted in the shelved changelist it must not be deleted
4032             # in the parent - we might even need to create it if the origin branch
4033             # does not have it
4034             if f['action'] in self.delete_actions:
4035                 f['action'] = 'add'
4036
4037             parent_files.append(f)
4038
4039         sync.commit(parent_description, parent_files, branch_name,
4040                 parent=origin, allow_empty=True)
4041         print("created parent commit for {0} based on {1} in {2}".format(
4042             change, self.origin, branch_name))
4043
4044     def run(self, args):
4045         if len(args) != 1:
4046             return False
4047
4048         if not gitBranchExists(self.origin):
4049             sys.exit("origin branch {0} does not exist".format(self.origin))
4050
4051         sync = P4Sync()
4052         changes = args
4053
4054         # only one change at a time
4055         change = changes[0]
4056
4057         # if the target branch already exists, rename it
4058         branch_name = "{0}/{1}".format(self.destbranch, change)
4059         if gitBranchExists(branch_name):
4060             self.renameBranch(branch_name)
4061         sync.branch = branch_name
4062
4063         sync.verbose = self.verbose
4064         sync.suppress_meta_comment = True
4065
4066         settings = self.findLastP4Revision(self.origin)
4067         sync.depotPaths = settings['depot-paths']
4068         sync.branchPrefixes = sync.depotPaths
4069
4070         sync.openStreams()
4071         sync.loadUserMapFromCache()
4072         sync.silent = True
4073
4074         # create a commit for the parent of the shelved changelist
4075         self.createShelveParent(change, branch_name, sync, self.origin)
4076
4077         # create the commit for the shelved changelist itself
4078         description = p4_describe(change, True)
4079         files = sync.extractFilesFromCommit(description, True, change)
4080
4081         sync.commit(description, files, branch_name, "")
4082         sync.closeStreams()
4083
4084         print("unshelved changelist {0} into {1}".format(change, branch_name))
4085
4086         return True
4087
4088 class P4Branches(Command):
4089     def __init__(self):
4090         Command.__init__(self)
4091         self.options = [ ]
4092         self.description = ("Shows the git branches that hold imports and their "
4093                             + "corresponding perforce depot paths")
4094         self.verbose = False
4095
4096     def run(self, args):
4097         if originP4BranchesExist():
4098             createOrUpdateBranchesFromOrigin()
4099
4100         cmdline = "git rev-parse --symbolic "
4101         cmdline += " --remotes"
4102
4103         for line in read_pipe_lines(cmdline):
4104             line = line.strip()
4105
4106             if not line.startswith('p4/') or line == "p4/HEAD":
4107                 continue
4108             branch = line
4109
4110             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4111             settings = extractSettingsGitLog(log)
4112
4113             print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
4114         return True
4115
4116 class HelpFormatter(optparse.IndentedHelpFormatter):
4117     def __init__(self):
4118         optparse.IndentedHelpFormatter.__init__(self)
4119
4120     def format_description(self, description):
4121         if description:
4122             return description + "\n"
4123         else:
4124             return ""
4125
4126 def printUsage(commands):
4127     print("usage: %s <command> [options]" % sys.argv[0])
4128     print("")
4129     print("valid commands: %s" % ", ".join(commands))
4130     print("")
4131     print("Try %s <command> --help for command specific help." % sys.argv[0])
4132     print("")
4133
4134 commands = {
4135     "debug" : P4Debug,
4136     "submit" : P4Submit,
4137     "commit" : P4Submit,
4138     "sync" : P4Sync,
4139     "rebase" : P4Rebase,
4140     "clone" : P4Clone,
4141     "rollback" : P4RollBack,
4142     "branches" : P4Branches,
4143     "unshelve" : P4Unshelve,
4144 }
4145
4146
4147 def main():
4148     if len(sys.argv[1:]) == 0:
4149         printUsage(commands.keys())
4150         sys.exit(2)
4151
4152     cmdName = sys.argv[1]
4153     try:
4154         klass = commands[cmdName]
4155         cmd = klass()
4156     except KeyError:
4157         print("unknown command %s" % cmdName)
4158         print("")
4159         printUsage(commands.keys())
4160         sys.exit(2)
4161
4162     options = cmd.options
4163     cmd.gitdir = os.environ.get("GIT_DIR", None)
4164
4165     args = sys.argv[2:]
4166
4167     options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
4168     if cmd.needsGit:
4169         options.append(optparse.make_option("--git-dir", dest="gitdir"))
4170
4171     parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4172                                    options,
4173                                    description = cmd.description,
4174                                    formatter = HelpFormatter())
4175
4176     (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
4177     global verbose
4178     verbose = cmd.verbose
4179     if cmd.needsGit:
4180         if cmd.gitdir == None:
4181             cmd.gitdir = os.path.abspath(".git")
4182             if not isValidGitDir(cmd.gitdir):
4183                 # "rev-parse --git-dir" without arguments will try $PWD/.git
4184                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
4185                 if os.path.exists(cmd.gitdir):
4186                     cdup = read_pipe("git rev-parse --show-cdup").strip()
4187                     if len(cdup) > 0:
4188                         chdir(cdup);
4189
4190         if not isValidGitDir(cmd.gitdir):
4191             if isValidGitDir(cmd.gitdir + "/.git"):
4192                 cmd.gitdir += "/.git"
4193             else:
4194                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
4195
4196         # so git commands invoked from the P4 workspace will succeed
4197         os.environ["GIT_DIR"] = cmd.gitdir
4198
4199     if not cmd.run(args):
4200         parser.print_help()
4201         sys.exit(2)
4202
4203
4204 if __name__ == '__main__':
4205     main()