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