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