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