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