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