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