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