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