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