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