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