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