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