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