git-p4: reduce number of server queries for fetches
[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):
257     p4_system(["edit", 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         pointerContents = [i+'\n' for i in pointerFile.split('\n')[2:][:-1]]
1068         oid = pointerContents[1].split(' ')[1].split(':')[1][:-1]
1069         localLargeFile = os.path.join(
1070             os.getcwd(),
1071             '.git', 'lfs', 'objects', oid[:2], oid[2:4],
1072             oid,
1073         )
1074         # LFS Spec states that pointer files should not have the executable bit set.
1075         gitMode = '100644'
1076         return (gitMode, pointerContents, localLargeFile)
1077
1078     def pushFile(self, localLargeFile):
1079         uploadProcess = subprocess.Popen(
1080             ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1081         )
1082         if uploadProcess.wait():
1083             die('git-lfs push command failed. Did you define a remote?')
1084
1085     def generateGitAttributes(self):
1086         return (
1087             self.baseGitAttributes +
1088             [
1089                 '\n',
1090                 '#\n',
1091                 '# Git LFS (see https://git-lfs.github.com/)\n',
1092                 '#\n',
1093             ] +
1094             ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\n'
1095                 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1096             ] +
1097             ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\n'
1098                 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1099             ]
1100         )
1101
1102     def addLargeFile(self, relPath):
1103         LargeFileSystem.addLargeFile(self, relPath)
1104         self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1105
1106     def removeLargeFile(self, relPath):
1107         LargeFileSystem.removeLargeFile(self, relPath)
1108         self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1109
1110     def processContent(self, git_mode, relPath, contents):
1111         if relPath == '.gitattributes':
1112             self.baseGitAttributes = contents
1113             return (git_mode, self.generateGitAttributes())
1114         else:
1115             return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1116
1117 class Command:
1118     def __init__(self):
1119         self.usage = "usage: %prog [options]"
1120         self.needsGit = True
1121         self.verbose = False
1122
1123 class P4UserMap:
1124     def __init__(self):
1125         self.userMapFromPerforceServer = False
1126         self.myP4UserId = None
1127
1128     def p4UserId(self):
1129         if self.myP4UserId:
1130             return self.myP4UserId
1131
1132         results = p4CmdList("user -o")
1133         for r in results:
1134             if r.has_key('User'):
1135                 self.myP4UserId = r['User']
1136                 return r['User']
1137         die("Could not find your p4 user id")
1138
1139     def p4UserIsMe(self, p4User):
1140         # return True if the given p4 user is actually me
1141         me = self.p4UserId()
1142         if not p4User or p4User != me:
1143             return False
1144         else:
1145             return True
1146
1147     def getUserCacheFilename(self):
1148         home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1149         return home + "/.gitp4-usercache.txt"
1150
1151     def getUserMapFromPerforceServer(self):
1152         if self.userMapFromPerforceServer:
1153             return
1154         self.users = {}
1155         self.emails = {}
1156
1157         for output in p4CmdList("users"):
1158             if not output.has_key("User"):
1159                 continue
1160             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1161             self.emails[output["Email"]] = output["User"]
1162
1163
1164         s = ''
1165         for (key, val) in self.users.items():
1166             s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1167
1168         open(self.getUserCacheFilename(), "wb").write(s)
1169         self.userMapFromPerforceServer = True
1170
1171     def loadUserMapFromCache(self):
1172         self.users = {}
1173         self.userMapFromPerforceServer = False
1174         try:
1175             cache = open(self.getUserCacheFilename(), "rb")
1176             lines = cache.readlines()
1177             cache.close()
1178             for line in lines:
1179                 entry = line.strip().split("\t")
1180                 self.users[entry[0]] = entry[1]
1181         except IOError:
1182             self.getUserMapFromPerforceServer()
1183
1184 class P4Debug(Command):
1185     def __init__(self):
1186         Command.__init__(self)
1187         self.options = []
1188         self.description = "A tool to debug the output of p4 -G."
1189         self.needsGit = False
1190
1191     def run(self, args):
1192         j = 0
1193         for output in p4CmdList(args):
1194             print 'Element: %d' % j
1195             j += 1
1196             print output
1197         return True
1198
1199 class P4RollBack(Command):
1200     def __init__(self):
1201         Command.__init__(self)
1202         self.options = [
1203             optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
1204         ]
1205         self.description = "A tool to debug the multi-branch import. Don't use :)"
1206         self.rollbackLocalBranches = False
1207
1208     def run(self, args):
1209         if len(args) != 1:
1210             return False
1211         maxChange = int(args[0])
1212
1213         if "p4ExitCode" in p4Cmd("changes -m 1"):
1214             die("Problems executing p4");
1215
1216         if self.rollbackLocalBranches:
1217             refPrefix = "refs/heads/"
1218             lines = read_pipe_lines("git rev-parse --symbolic --branches")
1219         else:
1220             refPrefix = "refs/remotes/"
1221             lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1222
1223         for line in lines:
1224             if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1225                 line = line.strip()
1226                 ref = refPrefix + line
1227                 log = extractLogMessageFromGitCommit(ref)
1228                 settings = extractSettingsGitLog(log)
1229
1230                 depotPaths = settings['depot-paths']
1231                 change = settings['change']
1232
1233                 changed = False
1234
1235                 if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
1236                                                            for p in depotPaths]))) == 0:
1237                     print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
1238                     system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1239                     continue
1240
1241                 while change and int(change) > maxChange:
1242                     changed = True
1243                     if self.verbose:
1244                         print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
1245                     system("git update-ref %s \"%s^\"" % (ref, ref))
1246                     log = extractLogMessageFromGitCommit(ref)
1247                     settings =  extractSettingsGitLog(log)
1248
1249
1250                     depotPaths = settings['depot-paths']
1251                     change = settings['change']
1252
1253                 if changed:
1254                     print "%s rewound to %s" % (ref, change)
1255
1256         return True
1257
1258 class P4Submit(Command, P4UserMap):
1259
1260     conflict_behavior_choices = ("ask", "skip", "quit")
1261
1262     def __init__(self):
1263         Command.__init__(self)
1264         P4UserMap.__init__(self)
1265         self.options = [
1266                 optparse.make_option("--origin", dest="origin"),
1267                 optparse.make_option("-M", dest="detectRenames", action="store_true"),
1268                 # preserve the user, requires relevant p4 permissions
1269                 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1270                 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1271                 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1272                 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1273                 optparse.make_option("--conflict", dest="conflict_behavior",
1274                                      choices=self.conflict_behavior_choices),
1275                 optparse.make_option("--branch", dest="branch"),
1276         ]
1277         self.description = "Submit changes from git to the perforce depot."
1278         self.usage += " [name of git branch to submit into perforce depot]"
1279         self.origin = ""
1280         self.detectRenames = False
1281         self.preserveUser = gitConfigBool("git-p4.preserveUser")
1282         self.dry_run = False
1283         self.prepare_p4_only = False
1284         self.conflict_behavior = None
1285         self.isWindows = (platform.system() == "Windows")
1286         self.exportLabels = False
1287         self.p4HasMoveCommand = p4_has_move_command()
1288         self.branch = None
1289
1290         if gitConfig('git-p4.largeFileSystem'):
1291             die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1292
1293     def check(self):
1294         if len(p4CmdList("opened ...")) > 0:
1295             die("You have files opened with perforce! Close them before starting the sync.")
1296
1297     def separate_jobs_from_description(self, message):
1298         """Extract and return a possible Jobs field in the commit
1299            message.  It goes into a separate section in the p4 change
1300            specification.
1301
1302            A jobs line starts with "Jobs:" and looks like a new field
1303            in a form.  Values are white-space separated on the same
1304            line or on following lines that start with a tab.
1305
1306            This does not parse and extract the full git commit message
1307            like a p4 form.  It just sees the Jobs: line as a marker
1308            to pass everything from then on directly into the p4 form,
1309            but outside the description section.
1310
1311            Return a tuple (stripped log message, jobs string)."""
1312
1313         m = re.search(r'^Jobs:', message, re.MULTILINE)
1314         if m is None:
1315             return (message, None)
1316
1317         jobtext = message[m.start():]
1318         stripped_message = message[:m.start()].rstrip()
1319         return (stripped_message, jobtext)
1320
1321     def prepareLogMessage(self, template, message, jobs):
1322         """Edits the template returned from "p4 change -o" to insert
1323            the message in the Description field, and the jobs text in
1324            the Jobs field."""
1325         result = ""
1326
1327         inDescriptionSection = False
1328
1329         for line in template.split("\n"):
1330             if line.startswith("#"):
1331                 result += line + "\n"
1332                 continue
1333
1334             if inDescriptionSection:
1335                 if line.startswith("Files:") or line.startswith("Jobs:"):
1336                     inDescriptionSection = False
1337                     # insert Jobs section
1338                     if jobs:
1339                         result += jobs + "\n"
1340                 else:
1341                     continue
1342             else:
1343                 if line.startswith("Description:"):
1344                     inDescriptionSection = True
1345                     line += "\n"
1346                     for messageLine in message.split("\n"):
1347                         line += "\t" + messageLine + "\n"
1348
1349             result += line + "\n"
1350
1351         return result
1352
1353     def patchRCSKeywords(self, file, pattern):
1354         # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1355         (handle, outFileName) = tempfile.mkstemp(dir='.')
1356         try:
1357             outFile = os.fdopen(handle, "w+")
1358             inFile = open(file, "r")
1359             regexp = re.compile(pattern, re.VERBOSE)
1360             for line in inFile.readlines():
1361                 line = regexp.sub(r'$\1$', line)
1362                 outFile.write(line)
1363             inFile.close()
1364             outFile.close()
1365             # Forcibly overwrite the original file
1366             os.unlink(file)
1367             shutil.move(outFileName, file)
1368         except:
1369             # cleanup our temporary file
1370             os.unlink(outFileName)
1371             print "Failed to strip RCS keywords in %s" % file
1372             raise
1373
1374         print "Patched up RCS keywords in %s" % file
1375
1376     def p4UserForCommit(self,id):
1377         # Return the tuple (perforce user,git email) for a given git commit id
1378         self.getUserMapFromPerforceServer()
1379         gitEmail = read_pipe(["git", "log", "--max-count=1",
1380                               "--format=%ae", id])
1381         gitEmail = gitEmail.strip()
1382         if not self.emails.has_key(gitEmail):
1383             return (None,gitEmail)
1384         else:
1385             return (self.emails[gitEmail],gitEmail)
1386
1387     def checkValidP4Users(self,commits):
1388         # check if any git authors cannot be mapped to p4 users
1389         for id in commits:
1390             (user,email) = self.p4UserForCommit(id)
1391             if not user:
1392                 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1393                 if gitConfigBool("git-p4.allowMissingP4Users"):
1394                     print "%s" % msg
1395                 else:
1396                     die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1397
1398     def lastP4Changelist(self):
1399         # Get back the last changelist number submitted in this client spec. This
1400         # then gets used to patch up the username in the change. If the same
1401         # client spec is being used by multiple processes then this might go
1402         # wrong.
1403         results = p4CmdList("client -o")        # find the current client
1404         client = None
1405         for r in results:
1406             if r.has_key('Client'):
1407                 client = r['Client']
1408                 break
1409         if not client:
1410             die("could not get client spec")
1411         results = p4CmdList(["changes", "-c", client, "-m", "1"])
1412         for r in results:
1413             if r.has_key('change'):
1414                 return r['change']
1415         die("Could not get changelist number for last submit - cannot patch up user details")
1416
1417     def modifyChangelistUser(self, changelist, newUser):
1418         # fixup the user field of a changelist after it has been submitted.
1419         changes = p4CmdList("change -o %s" % changelist)
1420         if len(changes) != 1:
1421             die("Bad output from p4 change modifying %s to user %s" %
1422                 (changelist, newUser))
1423
1424         c = changes[0]
1425         if c['User'] == newUser: return   # nothing to do
1426         c['User'] = newUser
1427         input = marshal.dumps(c)
1428
1429         result = p4CmdList("change -f -i", stdin=input)
1430         for r in result:
1431             if r.has_key('code'):
1432                 if r['code'] == 'error':
1433                     die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1434             if r.has_key('data'):
1435                 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1436                 return
1437         die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1438
1439     def canChangeChangelists(self):
1440         # check to see if we have p4 admin or super-user permissions, either of
1441         # which are required to modify changelists.
1442         results = p4CmdList(["protects", self.depotPath])
1443         for r in results:
1444             if r.has_key('perm'):
1445                 if r['perm'] == 'admin':
1446                     return 1
1447                 if r['perm'] == 'super':
1448                     return 1
1449         return 0
1450
1451     def prepareSubmitTemplate(self):
1452         """Run "p4 change -o" to grab a change specification template.
1453            This does not use "p4 -G", as it is nice to keep the submission
1454            template in original order, since a human might edit it.
1455
1456            Remove lines in the Files section that show changes to files
1457            outside the depot path we're committing into."""
1458
1459         [upstream, settings] = findUpstreamBranchPoint()
1460
1461         template = ""
1462         inFilesSection = False
1463         for line in p4_read_pipe_lines(['change', '-o']):
1464             if line.endswith("\r\n"):
1465                 line = line[:-2] + "\n"
1466             if inFilesSection:
1467                 if line.startswith("\t"):
1468                     # path starts and ends with a tab
1469                     path = line[1:]
1470                     lastTab = path.rfind("\t")
1471                     if lastTab != -1:
1472                         path = path[:lastTab]
1473                         if settings.has_key('depot-paths'):
1474                             if not [p for p in settings['depot-paths']
1475                                     if p4PathStartsWith(path, p)]:
1476                                 continue
1477                         else:
1478                             if not p4PathStartsWith(path, self.depotPath):
1479                                 continue
1480                 else:
1481                     inFilesSection = False
1482             else:
1483                 if line.startswith("Files:"):
1484                     inFilesSection = True
1485
1486             template += line
1487
1488         return template
1489
1490     def edit_template(self, template_file):
1491         """Invoke the editor to let the user change the submission
1492            message.  Return true if okay to continue with the submit."""
1493
1494         # if configured to skip the editing part, just submit
1495         if gitConfigBool("git-p4.skipSubmitEdit"):
1496             return True
1497
1498         # look at the modification time, to check later if the user saved
1499         # the file
1500         mtime = os.stat(template_file).st_mtime
1501
1502         # invoke the editor
1503         if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1504             editor = os.environ.get("P4EDITOR")
1505         else:
1506             editor = read_pipe("git var GIT_EDITOR").strip()
1507         system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1508
1509         # If the file was not saved, prompt to see if this patch should
1510         # be skipped.  But skip this verification step if configured so.
1511         if gitConfigBool("git-p4.skipSubmitEditCheck"):
1512             return True
1513
1514         # modification time updated means user saved the file
1515         if os.stat(template_file).st_mtime > mtime:
1516             return True
1517
1518         while True:
1519             response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1520             if response == 'y':
1521                 return True
1522             if response == 'n':
1523                 return False
1524
1525     def get_diff_description(self, editedFiles, filesToAdd):
1526         # diff
1527         if os.environ.has_key("P4DIFF"):
1528             del(os.environ["P4DIFF"])
1529         diff = ""
1530         for editedFile in editedFiles:
1531             diff += p4_read_pipe(['diff', '-du',
1532                                   wildcard_encode(editedFile)])
1533
1534         # new file diff
1535         newdiff = ""
1536         for newFile in filesToAdd:
1537             newdiff += "==== new file ====\n"
1538             newdiff += "--- /dev/null\n"
1539             newdiff += "+++ %s\n" % newFile
1540             f = open(newFile, "r")
1541             for line in f.readlines():
1542                 newdiff += "+" + line
1543             f.close()
1544
1545         return (diff + newdiff).replace('\r\n', '\n')
1546
1547     def applyCommit(self, id):
1548         """Apply one commit, return True if it succeeded."""
1549
1550         print "Applying", read_pipe(["git", "show", "-s",
1551                                      "--format=format:%h %s", id])
1552
1553         (p4User, gitEmail) = self.p4UserForCommit(id)
1554
1555         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1556         filesToAdd = set()
1557         filesToDelete = set()
1558         editedFiles = set()
1559         pureRenameCopy = set()
1560         filesToChangeExecBit = {}
1561
1562         for line in diff:
1563             diff = parseDiffTreeEntry(line)
1564             modifier = diff['status']
1565             path = diff['src']
1566             if modifier == "M":
1567                 p4_edit(path)
1568                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1569                     filesToChangeExecBit[path] = diff['dst_mode']
1570                 editedFiles.add(path)
1571             elif modifier == "A":
1572                 filesToAdd.add(path)
1573                 filesToChangeExecBit[path] = diff['dst_mode']
1574                 if path in filesToDelete:
1575                     filesToDelete.remove(path)
1576             elif modifier == "D":
1577                 filesToDelete.add(path)
1578                 if path in filesToAdd:
1579                     filesToAdd.remove(path)
1580             elif modifier == "C":
1581                 src, dest = diff['src'], diff['dst']
1582                 p4_integrate(src, dest)
1583                 pureRenameCopy.add(dest)
1584                 if diff['src_sha1'] != diff['dst_sha1']:
1585                     p4_edit(dest)
1586                     pureRenameCopy.discard(dest)
1587                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1588                     p4_edit(dest)
1589                     pureRenameCopy.discard(dest)
1590                     filesToChangeExecBit[dest] = diff['dst_mode']
1591                 if self.isWindows:
1592                     # turn off read-only attribute
1593                     os.chmod(dest, stat.S_IWRITE)
1594                 os.unlink(dest)
1595                 editedFiles.add(dest)
1596             elif modifier == "R":
1597                 src, dest = diff['src'], diff['dst']
1598                 if self.p4HasMoveCommand:
1599                     p4_edit(src)        # src must be open before move
1600                     p4_move(src, dest)  # opens for (move/delete, move/add)
1601                 else:
1602                     p4_integrate(src, dest)
1603                     if diff['src_sha1'] != diff['dst_sha1']:
1604                         p4_edit(dest)
1605                     else:
1606                         pureRenameCopy.add(dest)
1607                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1608                     if not self.p4HasMoveCommand:
1609                         p4_edit(dest)   # with move: already open, writable
1610                     filesToChangeExecBit[dest] = diff['dst_mode']
1611                 if not self.p4HasMoveCommand:
1612                     if self.isWindows:
1613                         os.chmod(dest, stat.S_IWRITE)
1614                     os.unlink(dest)
1615                     filesToDelete.add(src)
1616                 editedFiles.add(dest)
1617             else:
1618                 die("unknown modifier %s for %s" % (modifier, path))
1619
1620         diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1621         patchcmd = diffcmd + " | git apply "
1622         tryPatchCmd = patchcmd + "--check -"
1623         applyPatchCmd = patchcmd + "--check --apply -"
1624         patch_succeeded = True
1625
1626         if os.system(tryPatchCmd) != 0:
1627             fixed_rcs_keywords = False
1628             patch_succeeded = False
1629             print "Unfortunately applying the change failed!"
1630
1631             # Patch failed, maybe it's just RCS keyword woes. Look through
1632             # the patch to see if that's possible.
1633             if gitConfigBool("git-p4.attemptRCSCleanup"):
1634                 file = None
1635                 pattern = None
1636                 kwfiles = {}
1637                 for file in editedFiles | filesToDelete:
1638                     # did this file's delta contain RCS keywords?
1639                     pattern = p4_keywords_regexp_for_file(file)
1640
1641                     if pattern:
1642                         # this file is a possibility...look for RCS keywords.
1643                         regexp = re.compile(pattern, re.VERBOSE)
1644                         for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1645                             if regexp.search(line):
1646                                 if verbose:
1647                                     print "got keyword match on %s in %s in %s" % (pattern, line, file)
1648                                 kwfiles[file] = pattern
1649                                 break
1650
1651                 for file in kwfiles:
1652                     if verbose:
1653                         print "zapping %s with %s" % (line,pattern)
1654                     # File is being deleted, so not open in p4.  Must
1655                     # disable the read-only bit on windows.
1656                     if self.isWindows and file not in editedFiles:
1657                         os.chmod(file, stat.S_IWRITE)
1658                     self.patchRCSKeywords(file, kwfiles[file])
1659                     fixed_rcs_keywords = True
1660
1661             if fixed_rcs_keywords:
1662                 print "Retrying the patch with RCS keywords cleaned up"
1663                 if os.system(tryPatchCmd) == 0:
1664                     patch_succeeded = True
1665
1666         if not patch_succeeded:
1667             for f in editedFiles:
1668                 p4_revert(f)
1669             return False
1670
1671         #
1672         # Apply the patch for real, and do add/delete/+x handling.
1673         #
1674         system(applyPatchCmd)
1675
1676         for f in filesToAdd:
1677             p4_add(f)
1678         for f in filesToDelete:
1679             p4_revert(f)
1680             p4_delete(f)
1681
1682         # Set/clear executable bits
1683         for f in filesToChangeExecBit.keys():
1684             mode = filesToChangeExecBit[f]
1685             setP4ExecBit(f, mode)
1686
1687         #
1688         # Build p4 change description, starting with the contents
1689         # of the git commit message.
1690         #
1691         logMessage = extractLogMessageFromGitCommit(id)
1692         logMessage = logMessage.strip()
1693         (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1694
1695         template = self.prepareSubmitTemplate()
1696         submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1697
1698         if self.preserveUser:
1699            submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1700
1701         if self.checkAuthorship and not self.p4UserIsMe(p4User):
1702             submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1703             submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1704             submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1705
1706         separatorLine = "######## everything below this line is just the diff #######\n"
1707         if not self.prepare_p4_only:
1708             submitTemplate += separatorLine
1709             submitTemplate += self.get_diff_description(editedFiles, filesToAdd)
1710
1711         (handle, fileName) = tempfile.mkstemp()
1712         tmpFile = os.fdopen(handle, "w+b")
1713         if self.isWindows:
1714             submitTemplate = submitTemplate.replace("\n", "\r\n")
1715         tmpFile.write(submitTemplate)
1716         tmpFile.close()
1717
1718         if self.prepare_p4_only:
1719             #
1720             # Leave the p4 tree prepared, and the submit template around
1721             # and let the user decide what to do next
1722             #
1723             print
1724             print "P4 workspace prepared for submission."
1725             print "To submit or revert, go to client workspace"
1726             print "  " + self.clientPath
1727             print
1728             print "To submit, use \"p4 submit\" to write a new description,"
1729             print "or \"p4 submit -i <%s\" to use the one prepared by" \
1730                   " \"git p4\"." % fileName
1731             print "You can delete the file \"%s\" when finished." % fileName
1732
1733             if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1734                 print "To preserve change ownership by user %s, you must\n" \
1735                       "do \"p4 change -f <change>\" after submitting and\n" \
1736                       "edit the User field."
1737             if pureRenameCopy:
1738                 print "After submitting, renamed files must be re-synced."
1739                 print "Invoke \"p4 sync -f\" on each of these files:"
1740                 for f in pureRenameCopy:
1741                     print "  " + f
1742
1743             print
1744             print "To revert the changes, use \"p4 revert ...\", and delete"
1745             print "the submit template file \"%s\"" % fileName
1746             if filesToAdd:
1747                 print "Since the commit adds new files, they must be deleted:"
1748                 for f in filesToAdd:
1749                     print "  " + f
1750             print
1751             return True
1752
1753         #
1754         # Let the user edit the change description, then submit it.
1755         #
1756         submitted = False
1757
1758         try:
1759             if self.edit_template(fileName):
1760                 # read the edited message and submit
1761                 tmpFile = open(fileName, "rb")
1762                 message = tmpFile.read()
1763                 tmpFile.close()
1764                 if self.isWindows:
1765                     message = message.replace("\r\n", "\n")
1766                 submitTemplate = message[:message.index(separatorLine)]
1767                 p4_write_pipe(['submit', '-i'], submitTemplate)
1768
1769                 if self.preserveUser:
1770                     if p4User:
1771                         # Get last changelist number. Cannot easily get it from
1772                         # the submit command output as the output is
1773                         # unmarshalled.
1774                         changelist = self.lastP4Changelist()
1775                         self.modifyChangelistUser(changelist, p4User)
1776
1777                 # The rename/copy happened by applying a patch that created a
1778                 # new file.  This leaves it writable, which confuses p4.
1779                 for f in pureRenameCopy:
1780                     p4_sync(f, "-f")
1781                 submitted = True
1782
1783         finally:
1784             # skip this patch
1785             if not submitted:
1786                 print "Submission cancelled, undoing p4 changes."
1787                 for f in editedFiles:
1788                     p4_revert(f)
1789                 for f in filesToAdd:
1790                     p4_revert(f)
1791                     os.remove(f)
1792                 for f in filesToDelete:
1793                     p4_revert(f)
1794
1795         os.remove(fileName)
1796         return submitted
1797
1798     # Export git tags as p4 labels. Create a p4 label and then tag
1799     # with that.
1800     def exportGitTags(self, gitTags):
1801         validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1802         if len(validLabelRegexp) == 0:
1803             validLabelRegexp = defaultLabelRegexp
1804         m = re.compile(validLabelRegexp)
1805
1806         for name in gitTags:
1807
1808             if not m.match(name):
1809                 if verbose:
1810                     print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1811                 continue
1812
1813             # Get the p4 commit this corresponds to
1814             logMessage = extractLogMessageFromGitCommit(name)
1815             values = extractSettingsGitLog(logMessage)
1816
1817             if not values.has_key('change'):
1818                 # a tag pointing to something not sent to p4; ignore
1819                 if verbose:
1820                     print "git tag %s does not give a p4 commit" % name
1821                 continue
1822             else:
1823                 changelist = values['change']
1824
1825             # Get the tag details.
1826             inHeader = True
1827             isAnnotated = False
1828             body = []
1829             for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1830                 l = l.strip()
1831                 if inHeader:
1832                     if re.match(r'tag\s+', l):
1833                         isAnnotated = True
1834                     elif re.match(r'\s*$', l):
1835                         inHeader = False
1836                         continue
1837                 else:
1838                     body.append(l)
1839
1840             if not isAnnotated:
1841                 body = ["lightweight tag imported by git p4\n"]
1842
1843             # Create the label - use the same view as the client spec we are using
1844             clientSpec = getClientSpec()
1845
1846             labelTemplate  = "Label: %s\n" % name
1847             labelTemplate += "Description:\n"
1848             for b in body:
1849                 labelTemplate += "\t" + b + "\n"
1850             labelTemplate += "View:\n"
1851             for depot_side in clientSpec.mappings:
1852                 labelTemplate += "\t%s\n" % depot_side
1853
1854             if self.dry_run:
1855                 print "Would create p4 label %s for tag" % name
1856             elif self.prepare_p4_only:
1857                 print "Not creating p4 label %s for tag due to option" \
1858                       " --prepare-p4-only" % name
1859             else:
1860                 p4_write_pipe(["label", "-i"], labelTemplate)
1861
1862                 # Use the label
1863                 p4_system(["tag", "-l", name] +
1864                           ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1865
1866                 if verbose:
1867                     print "created p4 label for tag %s" % name
1868
1869     def run(self, args):
1870         if len(args) == 0:
1871             self.master = currentGitBranch()
1872         elif len(args) == 1:
1873             self.master = args[0]
1874             if not branchExists(self.master):
1875                 die("Branch %s does not exist" % self.master)
1876         else:
1877             return False
1878
1879         if self.master:
1880             allowSubmit = gitConfig("git-p4.allowSubmit")
1881             if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1882                 die("%s is not in git-p4.allowSubmit" % self.master)
1883
1884         [upstream, settings] = findUpstreamBranchPoint()
1885         self.depotPath = settings['depot-paths'][0]
1886         if len(self.origin) == 0:
1887             self.origin = upstream
1888
1889         if self.preserveUser:
1890             if not self.canChangeChangelists():
1891                 die("Cannot preserve user names without p4 super-user or admin permissions")
1892
1893         # if not set from the command line, try the config file
1894         if self.conflict_behavior is None:
1895             val = gitConfig("git-p4.conflict")
1896             if val:
1897                 if val not in self.conflict_behavior_choices:
1898                     die("Invalid value '%s' for config git-p4.conflict" % val)
1899             else:
1900                 val = "ask"
1901             self.conflict_behavior = val
1902
1903         if self.verbose:
1904             print "Origin branch is " + self.origin
1905
1906         if len(self.depotPath) == 0:
1907             print "Internal error: cannot locate perforce depot path from existing branches"
1908             sys.exit(128)
1909
1910         self.useClientSpec = False
1911         if gitConfigBool("git-p4.useclientspec"):
1912             self.useClientSpec = True
1913         if self.useClientSpec:
1914             self.clientSpecDirs = getClientSpec()
1915
1916         # Check for the existance of P4 branches
1917         branchesDetected = (len(p4BranchesInGit().keys()) > 1)
1918
1919         if self.useClientSpec and not branchesDetected:
1920             # all files are relative to the client spec
1921             self.clientPath = getClientRoot()
1922         else:
1923             self.clientPath = p4Where(self.depotPath)
1924
1925         if self.clientPath == "":
1926             die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1927
1928         print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1929         self.oldWorkingDirectory = os.getcwd()
1930
1931         # ensure the clientPath exists
1932         new_client_dir = False
1933         if not os.path.exists(self.clientPath):
1934             new_client_dir = True
1935             os.makedirs(self.clientPath)
1936
1937         chdir(self.clientPath, is_client_path=True)
1938         if self.dry_run:
1939             print "Would synchronize p4 checkout in %s" % self.clientPath
1940         else:
1941             print "Synchronizing p4 checkout..."
1942             if new_client_dir:
1943                 # old one was destroyed, and maybe nobody told p4
1944                 p4_sync("...", "-f")
1945             else:
1946                 p4_sync("...")
1947         self.check()
1948
1949         commits = []
1950         if self.master:
1951             commitish = self.master
1952         else:
1953             commitish = 'HEAD'
1954
1955         for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, commitish)]):
1956             commits.append(line.strip())
1957         commits.reverse()
1958
1959         if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1960             self.checkAuthorship = False
1961         else:
1962             self.checkAuthorship = True
1963
1964         if self.preserveUser:
1965             self.checkValidP4Users(commits)
1966
1967         #
1968         # Build up a set of options to be passed to diff when
1969         # submitting each commit to p4.
1970         #
1971         if self.detectRenames:
1972             # command-line -M arg
1973             self.diffOpts = "-M"
1974         else:
1975             # If not explicitly set check the config variable
1976             detectRenames = gitConfig("git-p4.detectRenames")
1977
1978             if detectRenames.lower() == "false" or detectRenames == "":
1979                 self.diffOpts = ""
1980             elif detectRenames.lower() == "true":
1981                 self.diffOpts = "-M"
1982             else:
1983                 self.diffOpts = "-M%s" % detectRenames
1984
1985         # no command-line arg for -C or --find-copies-harder, just
1986         # config variables
1987         detectCopies = gitConfig("git-p4.detectCopies")
1988         if detectCopies.lower() == "false" or detectCopies == "":
1989             pass
1990         elif detectCopies.lower() == "true":
1991             self.diffOpts += " -C"
1992         else:
1993             self.diffOpts += " -C%s" % detectCopies
1994
1995         if gitConfigBool("git-p4.detectCopiesHarder"):
1996             self.diffOpts += " --find-copies-harder"
1997
1998         #
1999         # Apply the commits, one at a time.  On failure, ask if should
2000         # continue to try the rest of the patches, or quit.
2001         #
2002         if self.dry_run:
2003             print "Would apply"
2004         applied = []
2005         last = len(commits) - 1
2006         for i, commit in enumerate(commits):
2007             if self.dry_run:
2008                 print " ", read_pipe(["git", "show", "-s",
2009                                       "--format=format:%h %s", commit])
2010                 ok = True
2011             else:
2012                 ok = self.applyCommit(commit)
2013             if ok:
2014                 applied.append(commit)
2015             else:
2016                 if self.prepare_p4_only and i < last:
2017                     print "Processing only the first commit due to option" \
2018                           " --prepare-p4-only"
2019                     break
2020                 if i < last:
2021                     quit = False
2022                     while True:
2023                         # prompt for what to do, or use the option/variable
2024                         if self.conflict_behavior == "ask":
2025                             print "What do you want to do?"
2026                             response = raw_input("[s]kip this commit but apply"
2027                                                  " the rest, or [q]uit? ")
2028                             if not response:
2029                                 continue
2030                         elif self.conflict_behavior == "skip":
2031                             response = "s"
2032                         elif self.conflict_behavior == "quit":
2033                             response = "q"
2034                         else:
2035                             die("Unknown conflict_behavior '%s'" %
2036                                 self.conflict_behavior)
2037
2038                         if response[0] == "s":
2039                             print "Skipping this commit, but applying the rest"
2040                             break
2041                         if response[0] == "q":
2042                             print "Quitting"
2043                             quit = True
2044                             break
2045                     if quit:
2046                         break
2047
2048         chdir(self.oldWorkingDirectory)
2049
2050         if self.dry_run:
2051             pass
2052         elif self.prepare_p4_only:
2053             pass
2054         elif len(commits) == len(applied):
2055             print "All commits applied!"
2056
2057             sync = P4Sync()
2058             if self.branch:
2059                 sync.branch = self.branch
2060             sync.run([])
2061
2062             rebase = P4Rebase()
2063             rebase.rebase()
2064
2065         else:
2066             if len(applied) == 0:
2067                 print "No commits applied."
2068             else:
2069                 print "Applied only the commits marked with '*':"
2070                 for c in commits:
2071                     if c in applied:
2072                         star = "*"
2073                     else:
2074                         star = " "
2075                     print star, read_pipe(["git", "show", "-s",
2076                                            "--format=format:%h %s",  c])
2077                 print "You will have to do 'git p4 sync' and rebase."
2078
2079         if gitConfigBool("git-p4.exportLabels"):
2080             self.exportLabels = True
2081
2082         if self.exportLabels:
2083             p4Labels = getP4Labels(self.depotPath)
2084             gitTags = getGitTags()
2085
2086             missingGitTags = gitTags - p4Labels
2087             self.exportGitTags(missingGitTags)
2088
2089         # exit with error unless everything applied perfectly
2090         if len(commits) != len(applied):
2091                 sys.exit(1)
2092
2093         return True
2094
2095 class View(object):
2096     """Represent a p4 view ("p4 help views"), and map files in a
2097        repo according to the view."""
2098
2099     def __init__(self, client_name):
2100         self.mappings = []
2101         self.client_prefix = "//%s/" % client_name
2102         # cache results of "p4 where" to lookup client file locations
2103         self.client_spec_path_cache = {}
2104
2105     def append(self, view_line):
2106         """Parse a view line, splitting it into depot and client
2107            sides.  Append to self.mappings, preserving order.  This
2108            is only needed for tag creation."""
2109
2110         # Split the view line into exactly two words.  P4 enforces
2111         # structure on these lines that simplifies this quite a bit.
2112         #
2113         # Either or both words may be double-quoted.
2114         # Single quotes do not matter.
2115         # Double-quote marks cannot occur inside the words.
2116         # A + or - prefix is also inside the quotes.
2117         # There are no quotes unless they contain a space.
2118         # The line is already white-space stripped.
2119         # The two words are separated by a single space.
2120         #
2121         if view_line[0] == '"':
2122             # First word is double quoted.  Find its end.
2123             close_quote_index = view_line.find('"', 1)
2124             if close_quote_index <= 0:
2125                 die("No first-word closing quote found: %s" % view_line)
2126             depot_side = view_line[1:close_quote_index]
2127             # skip closing quote and space
2128             rhs_index = close_quote_index + 1 + 1
2129         else:
2130             space_index = view_line.find(" ")
2131             if space_index <= 0:
2132                 die("No word-splitting space found: %s" % view_line)
2133             depot_side = view_line[0:space_index]
2134             rhs_index = space_index + 1
2135
2136         # prefix + means overlay on previous mapping
2137         if depot_side.startswith("+"):
2138             depot_side = depot_side[1:]
2139
2140         # prefix - means exclude this path, leave out of mappings
2141         exclude = False
2142         if depot_side.startswith("-"):
2143             exclude = True
2144             depot_side = depot_side[1:]
2145
2146         if not exclude:
2147             self.mappings.append(depot_side)
2148
2149     def convert_client_path(self, clientFile):
2150         # chop off //client/ part to make it relative
2151         if not clientFile.startswith(self.client_prefix):
2152             die("No prefix '%s' on clientFile '%s'" %
2153                 (self.client_prefix, clientFile))
2154         return clientFile[len(self.client_prefix):]
2155
2156     def update_client_spec_path_cache(self, files):
2157         """ Caching file paths by "p4 where" batch query """
2158
2159         # List depot file paths exclude that already cached
2160         fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
2161
2162         if len(fileArgs) == 0:
2163             return  # All files in cache
2164
2165         where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2166         for res in where_result:
2167             if "code" in res and res["code"] == "error":
2168                 # assume error is "... file(s) not in client view"
2169                 continue
2170             if "clientFile" not in res:
2171                 die("No clientFile in 'p4 where' output")
2172             if "unmap" in res:
2173                 # it will list all of them, but only one not unmap-ped
2174                 continue
2175             if gitConfigBool("core.ignorecase"):
2176                 res['depotFile'] = res['depotFile'].lower()
2177             self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
2178
2179         # not found files or unmap files set to ""
2180         for depotFile in fileArgs:
2181             if gitConfigBool("core.ignorecase"):
2182                 depotFile = depotFile.lower()
2183             if depotFile not in self.client_spec_path_cache:
2184                 self.client_spec_path_cache[depotFile] = ""
2185
2186     def map_in_client(self, depot_path):
2187         """Return the relative location in the client where this
2188            depot file should live.  Returns "" if the file should
2189            not be mapped in the client."""
2190
2191         if gitConfigBool("core.ignorecase"):
2192             depot_path = depot_path.lower()
2193
2194         if depot_path in self.client_spec_path_cache:
2195             return self.client_spec_path_cache[depot_path]
2196
2197         die( "Error: %s is not found in client spec path" % depot_path )
2198         return ""
2199
2200 class P4Sync(Command, P4UserMap):
2201     delete_actions = ( "delete", "move/delete", "purge" )
2202
2203     def __init__(self):
2204         Command.__init__(self)
2205         P4UserMap.__init__(self)
2206         self.options = [
2207                 optparse.make_option("--branch", dest="branch"),
2208                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2209                 optparse.make_option("--changesfile", dest="changesFile"),
2210                 optparse.make_option("--silent", dest="silent", action="store_true"),
2211                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2212                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2213                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2214                                      help="Import into refs/heads/ , not refs/remotes"),
2215                 optparse.make_option("--max-changes", dest="maxChanges",
2216                                      help="Maximum number of changes to import"),
2217                 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2218                                      help="Internal block size to use when iteratively calling p4 changes"),
2219                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2220                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2221                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2222                                      help="Only sync files that are included in the Perforce Client Spec"),
2223                 optparse.make_option("-/", dest="cloneExclude",
2224                                      action="append", type="string",
2225                                      help="exclude depot path"),
2226         ]
2227         self.description = """Imports from Perforce into a git repository.\n
2228     example:
2229     //depot/my/project/ -- to import the current head
2230     //depot/my/project/@all -- to import everything
2231     //depot/my/project/@1,6 -- to import only from revision 1 to 6
2232
2233     (a ... is not needed in the path p4 specification, it's added implicitly)"""
2234
2235         self.usage += " //depot/path[@revRange]"
2236         self.silent = False
2237         self.createdBranches = set()
2238         self.committedChanges = set()
2239         self.branch = ""
2240         self.detectBranches = False
2241         self.detectLabels = False
2242         self.importLabels = False
2243         self.changesFile = ""
2244         self.syncWithOrigin = True
2245         self.importIntoRemotes = True
2246         self.maxChanges = ""
2247         self.changes_block_size = None
2248         self.keepRepoPath = False
2249         self.depotPaths = None
2250         self.p4BranchesInGit = []
2251         self.cloneExclude = []
2252         self.useClientSpec = False
2253         self.useClientSpec_from_options = False
2254         self.clientSpecDirs = None
2255         self.tempBranches = []
2256         self.tempBranchLocation = "git-p4-tmp"
2257         self.largeFileSystem = None
2258
2259         if gitConfig('git-p4.largeFileSystem'):
2260             largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2261             self.largeFileSystem = largeFileSystemConstructor(
2262                 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2263             )
2264
2265         if gitConfig("git-p4.syncFromOrigin") == "false":
2266             self.syncWithOrigin = False
2267
2268     # This is required for the "append" cloneExclude action
2269     def ensure_value(self, attr, value):
2270         if not hasattr(self, attr) or getattr(self, attr) is None:
2271             setattr(self, attr, value)
2272         return getattr(self, attr)
2273
2274     # Force a checkpoint in fast-import and wait for it to finish
2275     def checkpoint(self):
2276         self.gitStream.write("checkpoint\n\n")
2277         self.gitStream.write("progress checkpoint\n\n")
2278         out = self.gitOutput.readline()
2279         if self.verbose:
2280             print "checkpoint finished: " + out
2281
2282     def extractFilesFromCommit(self, commit):
2283         self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2284                              for path in self.cloneExclude]
2285         files = []
2286         fnum = 0
2287         while commit.has_key("depotFile%s" % fnum):
2288             path =  commit["depotFile%s" % fnum]
2289
2290             if [p for p in self.cloneExclude
2291                 if p4PathStartsWith(path, p)]:
2292                 found = False
2293             else:
2294                 found = [p for p in self.depotPaths
2295                          if p4PathStartsWith(path, p)]
2296             if not found:
2297                 fnum = fnum + 1
2298                 continue
2299
2300             file = {}
2301             file["path"] = path
2302             file["rev"] = commit["rev%s" % fnum]
2303             file["action"] = commit["action%s" % fnum]
2304             file["type"] = commit["type%s" % fnum]
2305             files.append(file)
2306             fnum = fnum + 1
2307         return files
2308
2309     def stripRepoPath(self, path, prefixes):
2310         """When streaming files, this is called to map a p4 depot path
2311            to where it should go in git.  The prefixes are either
2312            self.depotPaths, or self.branchPrefixes in the case of
2313            branch detection."""
2314
2315         if self.useClientSpec:
2316             # branch detection moves files up a level (the branch name)
2317             # from what client spec interpretation gives
2318             path = self.clientSpecDirs.map_in_client(path)
2319             if self.detectBranches:
2320                 for b in self.knownBranches:
2321                     if path.startswith(b + "/"):
2322                         path = path[len(b)+1:]
2323
2324         elif self.keepRepoPath:
2325             # Preserve everything in relative path name except leading
2326             # //depot/; just look at first prefix as they all should
2327             # be in the same depot.
2328             depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2329             if p4PathStartsWith(path, depot):
2330                 path = path[len(depot):]
2331
2332         else:
2333             for p in prefixes:
2334                 if p4PathStartsWith(path, p):
2335                     path = path[len(p):]
2336                     break
2337
2338         path = wildcard_decode(path)
2339         return path
2340
2341     def splitFilesIntoBranches(self, commit):
2342         """Look at each depotFile in the commit to figure out to what
2343            branch it belongs."""
2344
2345         if self.clientSpecDirs:
2346             files = self.extractFilesFromCommit(commit)
2347             self.clientSpecDirs.update_client_spec_path_cache(files)
2348
2349         branches = {}
2350         fnum = 0
2351         while commit.has_key("depotFile%s" % fnum):
2352             path =  commit["depotFile%s" % fnum]
2353             found = [p for p in self.depotPaths
2354                      if p4PathStartsWith(path, p)]
2355             if not found:
2356                 fnum = fnum + 1
2357                 continue
2358
2359             file = {}
2360             file["path"] = path
2361             file["rev"] = commit["rev%s" % fnum]
2362             file["action"] = commit["action%s" % fnum]
2363             file["type"] = commit["type%s" % fnum]
2364             fnum = fnum + 1
2365
2366             # start with the full relative path where this file would
2367             # go in a p4 client
2368             if self.useClientSpec:
2369                 relPath = self.clientSpecDirs.map_in_client(path)
2370             else:
2371                 relPath = self.stripRepoPath(path, self.depotPaths)
2372
2373             for branch in self.knownBranches.keys():
2374                 # add a trailing slash so that a commit into qt/4.2foo
2375                 # doesn't end up in qt/4.2, e.g.
2376                 if relPath.startswith(branch + "/"):
2377                     if branch not in branches:
2378                         branches[branch] = []
2379                     branches[branch].append(file)
2380                     break
2381
2382         return branches
2383
2384     def writeToGitStream(self, gitMode, relPath, contents):
2385         self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
2386         self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2387         for d in contents:
2388             self.gitStream.write(d)
2389         self.gitStream.write('\n')
2390
2391     # output one file from the P4 stream
2392     # - helper for streamP4Files
2393
2394     def streamOneP4File(self, file, contents):
2395         relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2396         if verbose:
2397             size = int(self.stream_file['fileSize'])
2398             sys.stdout.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath, size/1024/1024))
2399             sys.stdout.flush()
2400
2401         (type_base, type_mods) = split_p4_type(file["type"])
2402
2403         git_mode = "100644"
2404         if "x" in type_mods:
2405             git_mode = "100755"
2406         if type_base == "symlink":
2407             git_mode = "120000"
2408             # p4 print on a symlink sometimes contains "target\n";
2409             # if it does, remove the newline
2410             data = ''.join(contents)
2411             if not data:
2412                 # Some version of p4 allowed creating a symlink that pointed
2413                 # to nothing.  This causes p4 errors when checking out such
2414                 # a change, and errors here too.  Work around it by ignoring
2415                 # the bad symlink; hopefully a future change fixes it.
2416                 print "\nIgnoring empty symlink in %s" % file['depotFile']
2417                 return
2418             elif data[-1] == '\n':
2419                 contents = [data[:-1]]
2420             else:
2421                 contents = [data]
2422
2423         if type_base == "utf16":
2424             # p4 delivers different text in the python output to -G
2425             # than it does when using "print -o", or normal p4 client
2426             # operations.  utf16 is converted to ascii or utf8, perhaps.
2427             # But ascii text saved as -t utf16 is completely mangled.
2428             # Invoke print -o to get the real contents.
2429             #
2430             # On windows, the newlines will always be mangled by print, so put
2431             # them back too.  This is not needed to the cygwin windows version,
2432             # just the native "NT" type.
2433             #
2434             try:
2435                 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2436             except Exception as e:
2437                 if 'Translation of file content failed' in str(e):
2438                     type_base = 'binary'
2439                 else:
2440                     raise e
2441             else:
2442                 if p4_version_string().find('/NT') >= 0:
2443                     text = text.replace('\r\n', '\n')
2444                 contents = [ text ]
2445
2446         if type_base == "apple":
2447             # Apple filetype files will be streamed as a concatenation of
2448             # its appledouble header and the contents.  This is useless
2449             # on both macs and non-macs.  If using "print -q -o xx", it
2450             # will create "xx" with the data, and "%xx" with the header.
2451             # This is also not very useful.
2452             #
2453             # Ideally, someday, this script can learn how to generate
2454             # appledouble files directly and import those to git, but
2455             # non-mac machines can never find a use for apple filetype.
2456             print "\nIgnoring apple filetype file %s" % file['depotFile']
2457             return
2458
2459         # Note that we do not try to de-mangle keywords on utf16 files,
2460         # even though in theory somebody may want that.
2461         pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2462         if pattern:
2463             regexp = re.compile(pattern, re.VERBOSE)
2464             text = ''.join(contents)
2465             text = regexp.sub(r'$\1$', text)
2466             contents = [ text ]
2467
2468         try:
2469             relPath.decode('ascii')
2470         except:
2471             encoding = 'utf8'
2472             if gitConfig('git-p4.pathEncoding'):
2473                 encoding = gitConfig('git-p4.pathEncoding')
2474             relPath = relPath.decode(encoding, 'replace').encode('utf8', 'replace')
2475             if self.verbose:
2476                 print 'Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, relPath)
2477
2478         if self.largeFileSystem:
2479             (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
2480
2481         self.writeToGitStream(git_mode, relPath, contents)
2482
2483     def streamOneP4Deletion(self, file):
2484         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2485         if verbose:
2486             sys.stdout.write("delete %s\n" % relPath)
2487             sys.stdout.flush()
2488         self.gitStream.write("D %s\n" % relPath)
2489
2490         if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2491             self.largeFileSystem.removeLargeFile(relPath)
2492
2493     # handle another chunk of streaming data
2494     def streamP4FilesCb(self, marshalled):
2495
2496         # catch p4 errors and complain
2497         err = None
2498         if "code" in marshalled:
2499             if marshalled["code"] == "error":
2500                 if "data" in marshalled:
2501                     err = marshalled["data"].rstrip()
2502
2503         if not err and 'fileSize' in self.stream_file:
2504             required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
2505             if required_bytes > 0:
2506                 err = 'Not enough space left on %s! Free at least %i MB.' % (
2507                     os.getcwd(), required_bytes/1024/1024
2508                 )
2509
2510         if err:
2511             f = None
2512             if self.stream_have_file_info:
2513                 if "depotFile" in self.stream_file:
2514                     f = self.stream_file["depotFile"]
2515             # force a failure in fast-import, else an empty
2516             # commit will be made
2517             self.gitStream.write("\n")
2518             self.gitStream.write("die-now\n")
2519             self.gitStream.close()
2520             # ignore errors, but make sure it exits first
2521             self.importProcess.wait()
2522             if f:
2523                 die("Error from p4 print for %s: %s" % (f, err))
2524             else:
2525                 die("Error from p4 print: %s" % err)
2526
2527         if marshalled.has_key('depotFile') and self.stream_have_file_info:
2528             # start of a new file - output the old one first
2529             self.streamOneP4File(self.stream_file, self.stream_contents)
2530             self.stream_file = {}
2531             self.stream_contents = []
2532             self.stream_have_file_info = False
2533
2534         # pick up the new file information... for the
2535         # 'data' field we need to append to our array
2536         for k in marshalled.keys():
2537             if k == 'data':
2538                 if 'streamContentSize' not in self.stream_file:
2539                     self.stream_file['streamContentSize'] = 0
2540                 self.stream_file['streamContentSize'] += len(marshalled['data'])
2541                 self.stream_contents.append(marshalled['data'])
2542             else:
2543                 self.stream_file[k] = marshalled[k]
2544
2545         if (verbose and
2546             'streamContentSize' in self.stream_file and
2547             'fileSize' in self.stream_file and
2548             'depotFile' in self.stream_file):
2549             size = int(self.stream_file["fileSize"])
2550             if size > 0:
2551                 progress = 100*self.stream_file['streamContentSize']/size
2552                 sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
2553                 sys.stdout.flush()
2554
2555         self.stream_have_file_info = True
2556
2557     # Stream directly from "p4 files" into "git fast-import"
2558     def streamP4Files(self, files):
2559         filesForCommit = []
2560         filesToRead = []
2561         filesToDelete = []
2562
2563         for f in files:
2564             # if using a client spec, only add the files that have
2565             # a path in the client
2566             if self.clientSpecDirs:
2567                 if self.clientSpecDirs.map_in_client(f['path']) == "":
2568                     continue
2569
2570             filesForCommit.append(f)
2571             if f['action'] in self.delete_actions:
2572                 filesToDelete.append(f)
2573             else:
2574                 filesToRead.append(f)
2575
2576         # deleted files...
2577         for f in filesToDelete:
2578             self.streamOneP4Deletion(f)
2579
2580         if len(filesToRead) > 0:
2581             self.stream_file = {}
2582             self.stream_contents = []
2583             self.stream_have_file_info = False
2584
2585             # curry self argument
2586             def streamP4FilesCbSelf(entry):
2587                 self.streamP4FilesCb(entry)
2588
2589             fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2590
2591             p4CmdList(["-x", "-", "print"],
2592                       stdin=fileArgs,
2593                       cb=streamP4FilesCbSelf)
2594
2595             # do the last chunk
2596             if self.stream_file.has_key('depotFile'):
2597                 self.streamOneP4File(self.stream_file, self.stream_contents)
2598
2599     def make_email(self, userid):
2600         if userid in self.users:
2601             return self.users[userid]
2602         else:
2603             return "%s <a@b>" % userid
2604
2605     def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2606         """ Stream a p4 tag.
2607         commit is either a git commit, or a fast-import mark, ":<p4commit>"
2608         """
2609
2610         if verbose:
2611             print "writing tag %s for commit %s" % (labelName, commit)
2612         gitStream.write("tag %s\n" % labelName)
2613         gitStream.write("from %s\n" % commit)
2614
2615         if labelDetails.has_key('Owner'):
2616             owner = labelDetails["Owner"]
2617         else:
2618             owner = None
2619
2620         # Try to use the owner of the p4 label, or failing that,
2621         # the current p4 user id.
2622         if owner:
2623             email = self.make_email(owner)
2624         else:
2625             email = self.make_email(self.p4UserId())
2626         tagger = "%s %s %s" % (email, epoch, self.tz)
2627
2628         gitStream.write("tagger %s\n" % tagger)
2629
2630         print "labelDetails=",labelDetails
2631         if labelDetails.has_key('Description'):
2632             description = labelDetails['Description']
2633         else:
2634             description = 'Label from git p4'
2635
2636         gitStream.write("data %d\n" % len(description))
2637         gitStream.write(description)
2638         gitStream.write("\n")
2639
2640     def commit(self, details, files, branch, parent = ""):
2641         epoch = details["time"]
2642         author = details["user"]
2643
2644         if self.verbose:
2645             print "commit into %s" % branch
2646
2647         # start with reading files; if that fails, we should not
2648         # create a commit.
2649         new_files = []
2650         for f in files:
2651             if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2652                 new_files.append (f)
2653             else:
2654                 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2655
2656         if self.clientSpecDirs:
2657             self.clientSpecDirs.update_client_spec_path_cache(files)
2658
2659         self.gitStream.write("commit %s\n" % branch)
2660         self.gitStream.write("mark :%s\n" % details["change"])
2661         self.committedChanges.add(int(details["change"]))
2662         committer = ""
2663         if author not in self.users:
2664             self.getUserMapFromPerforceServer()
2665         committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2666
2667         self.gitStream.write("committer %s\n" % committer)
2668
2669         self.gitStream.write("data <<EOT\n")
2670         self.gitStream.write(details["desc"])
2671         self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2672                              (','.join(self.branchPrefixes), details["change"]))
2673         if len(details['options']) > 0:
2674             self.gitStream.write(": options = %s" % details['options'])
2675         self.gitStream.write("]\nEOT\n\n")
2676
2677         if len(parent) > 0:
2678             if self.verbose:
2679                 print "parent %s" % parent
2680             self.gitStream.write("from %s\n" % parent)
2681
2682         self.streamP4Files(new_files)
2683         self.gitStream.write("\n")
2684
2685         change = int(details["change"])
2686
2687         if self.labels.has_key(change):
2688             label = self.labels[change]
2689             labelDetails = label[0]
2690             labelRevisions = label[1]
2691             if self.verbose:
2692                 print "Change %s is labelled %s" % (change, labelDetails)
2693
2694             files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2695                                                 for p in self.branchPrefixes])
2696
2697             if len(files) == len(labelRevisions):
2698
2699                 cleanedFiles = {}
2700                 for info in files:
2701                     if info["action"] in self.delete_actions:
2702                         continue
2703                     cleanedFiles[info["depotFile"]] = info["rev"]
2704
2705                 if cleanedFiles == labelRevisions:
2706                     self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2707
2708                 else:
2709                     if not self.silent:
2710                         print ("Tag %s does not match with change %s: files do not match."
2711                                % (labelDetails["label"], change))
2712
2713             else:
2714                 if not self.silent:
2715                     print ("Tag %s does not match with change %s: file count is different."
2716                            % (labelDetails["label"], change))
2717
2718     # Build a dictionary of changelists and labels, for "detect-labels" option.
2719     def getLabels(self):
2720         self.labels = {}
2721
2722         l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2723         if len(l) > 0 and not self.silent:
2724             print "Finding files belonging to labels in %s" % `self.depotPaths`
2725
2726         for output in l:
2727             label = output["label"]
2728             revisions = {}
2729             newestChange = 0
2730             if self.verbose:
2731                 print "Querying files for label %s" % label
2732             for file in p4CmdList(["files"] +
2733                                       ["%s...@%s" % (p, label)
2734                                           for p in self.depotPaths]):
2735                 revisions[file["depotFile"]] = file["rev"]
2736                 change = int(file["change"])
2737                 if change > newestChange:
2738                     newestChange = change
2739
2740             self.labels[newestChange] = [output, revisions]
2741
2742         if self.verbose:
2743             print "Label changes: %s" % self.labels.keys()
2744
2745     # Import p4 labels as git tags. A direct mapping does not
2746     # exist, so assume that if all the files are at the same revision
2747     # then we can use that, or it's something more complicated we should
2748     # just ignore.
2749     def importP4Labels(self, stream, p4Labels):
2750         if verbose:
2751             print "import p4 labels: " + ' '.join(p4Labels)
2752
2753         ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2754         validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2755         if len(validLabelRegexp) == 0:
2756             validLabelRegexp = defaultLabelRegexp
2757         m = re.compile(validLabelRegexp)
2758
2759         for name in p4Labels:
2760             commitFound = False
2761
2762             if not m.match(name):
2763                 if verbose:
2764                     print "label %s does not match regexp %s" % (name,validLabelRegexp)
2765                 continue
2766
2767             if name in ignoredP4Labels:
2768                 continue
2769
2770             labelDetails = p4CmdList(['label', "-o", name])[0]
2771
2772             # get the most recent changelist for each file in this label
2773             change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2774                                 for p in self.depotPaths])
2775
2776             if change.has_key('change'):
2777                 # find the corresponding git commit; take the oldest commit
2778                 changelist = int(change['change'])
2779                 if changelist in self.committedChanges:
2780                     gitCommit = ":%d" % changelist       # use a fast-import mark
2781                     commitFound = True
2782                 else:
2783                     gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2784                         "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
2785                     if len(gitCommit) == 0:
2786                         print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
2787                     else:
2788                         commitFound = True
2789                         gitCommit = gitCommit.strip()
2790
2791                 if commitFound:
2792                     # Convert from p4 time format
2793                     try:
2794                         tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2795                     except ValueError:
2796                         print "Could not convert label time %s" % labelDetails['Update']
2797                         tmwhen = 1
2798
2799                     when = int(time.mktime(tmwhen))
2800                     self.streamTag(stream, name, labelDetails, gitCommit, when)
2801                     if verbose:
2802                         print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2803             else:
2804                 if verbose:
2805                     print "Label %s has no changelists - possibly deleted?" % name
2806
2807             if not commitFound:
2808                 # We can't import this label; don't try again as it will get very
2809                 # expensive repeatedly fetching all the files for labels that will
2810                 # never be imported. If the label is moved in the future, the
2811                 # ignore will need to be removed manually.
2812                 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2813
2814     def guessProjectName(self):
2815         for p in self.depotPaths:
2816             if p.endswith("/"):
2817                 p = p[:-1]
2818             p = p[p.strip().rfind("/") + 1:]
2819             if not p.endswith("/"):
2820                p += "/"
2821             return p
2822
2823     def getBranchMapping(self):
2824         lostAndFoundBranches = set()
2825
2826         user = gitConfig("git-p4.branchUser")
2827         if len(user) > 0:
2828             command = "branches -u %s" % user
2829         else:
2830             command = "branches"
2831
2832         for info in p4CmdList(command):
2833             details = p4Cmd(["branch", "-o", info["branch"]])
2834             viewIdx = 0
2835             while details.has_key("View%s" % viewIdx):
2836                 paths = details["View%s" % viewIdx].split(" ")
2837                 viewIdx = viewIdx + 1
2838                 # require standard //depot/foo/... //depot/bar/... mapping
2839                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2840                     continue
2841                 source = paths[0]
2842                 destination = paths[1]
2843                 ## HACK
2844                 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2845                     source = source[len(self.depotPaths[0]):-4]
2846                     destination = destination[len(self.depotPaths[0]):-4]
2847
2848                     if destination in self.knownBranches:
2849                         if not self.silent:
2850                             print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2851                             print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2852                         continue
2853
2854                     self.knownBranches[destination] = source
2855
2856                     lostAndFoundBranches.discard(destination)
2857
2858                     if source not in self.knownBranches:
2859                         lostAndFoundBranches.add(source)
2860
2861         # Perforce does not strictly require branches to be defined, so we also
2862         # check git config for a branch list.
2863         #
2864         # Example of branch definition in git config file:
2865         # [git-p4]
2866         #   branchList=main:branchA
2867         #   branchList=main:branchB
2868         #   branchList=branchA:branchC
2869         configBranches = gitConfigList("git-p4.branchList")
2870         for branch in configBranches:
2871             if branch:
2872                 (source, destination) = branch.split(":")
2873                 self.knownBranches[destination] = source
2874
2875                 lostAndFoundBranches.discard(destination)
2876
2877                 if source not in self.knownBranches:
2878                     lostAndFoundBranches.add(source)
2879
2880
2881         for branch in lostAndFoundBranches:
2882             self.knownBranches[branch] = branch
2883
2884     def getBranchMappingFromGitBranches(self):
2885         branches = p4BranchesInGit(self.importIntoRemotes)
2886         for branch in branches.keys():
2887             if branch == "master":
2888                 branch = "main"
2889             else:
2890                 branch = branch[len(self.projectName):]
2891             self.knownBranches[branch] = branch
2892
2893     def updateOptionDict(self, d):
2894         option_keys = {}
2895         if self.keepRepoPath:
2896             option_keys['keepRepoPath'] = 1
2897
2898         d["options"] = ' '.join(sorted(option_keys.keys()))
2899
2900     def readOptions(self, d):
2901         self.keepRepoPath = (d.has_key('options')
2902                              and ('keepRepoPath' in d['options']))
2903
2904     def gitRefForBranch(self, branch):
2905         if branch == "main":
2906             return self.refPrefix + "master"
2907
2908         if len(branch) <= 0:
2909             return branch
2910
2911         return self.refPrefix + self.projectName + branch
2912
2913     def gitCommitByP4Change(self, ref, change):
2914         if self.verbose:
2915             print "looking in ref " + ref + " for change %s using bisect..." % change
2916
2917         earliestCommit = ""
2918         latestCommit = parseRevision(ref)
2919
2920         while True:
2921             if self.verbose:
2922                 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2923             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2924             if len(next) == 0:
2925                 if self.verbose:
2926                     print "argh"
2927                 return ""
2928             log = extractLogMessageFromGitCommit(next)
2929             settings = extractSettingsGitLog(log)
2930             currentChange = int(settings['change'])
2931             if self.verbose:
2932                 print "current change %s" % currentChange
2933
2934             if currentChange == change:
2935                 if self.verbose:
2936                     print "found %s" % next
2937                 return next
2938
2939             if currentChange < change:
2940                 earliestCommit = "^%s" % next
2941             else:
2942                 latestCommit = "%s" % next
2943
2944         return ""
2945
2946     def importNewBranch(self, branch, maxChange):
2947         # make fast-import flush all changes to disk and update the refs using the checkpoint
2948         # command so that we can try to find the branch parent in the git history
2949         self.gitStream.write("checkpoint\n\n");
2950         self.gitStream.flush();
2951         branchPrefix = self.depotPaths[0] + branch + "/"
2952         range = "@1,%s" % maxChange
2953         #print "prefix" + branchPrefix
2954         changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
2955         if len(changes) <= 0:
2956             return False
2957         firstChange = changes[0]
2958         #print "first change in branch: %s" % firstChange
2959         sourceBranch = self.knownBranches[branch]
2960         sourceDepotPath = self.depotPaths[0] + sourceBranch
2961         sourceRef = self.gitRefForBranch(sourceBranch)
2962         #print "source " + sourceBranch
2963
2964         branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2965         #print "branch parent: %s" % branchParentChange
2966         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2967         if len(gitParent) > 0:
2968             self.initialParents[self.gitRefForBranch(branch)] = gitParent
2969             #print "parent git commit: %s" % gitParent
2970
2971         self.importChanges(changes)
2972         return True
2973
2974     def searchParent(self, parent, branch, target):
2975         parentFound = False
2976         for blob in read_pipe_lines(["git", "rev-list", "--reverse",
2977                                      "--no-merges", parent]):
2978             blob = blob.strip()
2979             if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2980                 parentFound = True
2981                 if self.verbose:
2982                     print "Found parent of %s in commit %s" % (branch, blob)
2983                 break
2984         if parentFound:
2985             return blob
2986         else:
2987             return None
2988
2989     def importChanges(self, changes):
2990         cnt = 1
2991         for change in changes:
2992             description = p4_describe(change)
2993             self.updateOptionDict(description)
2994
2995             if not self.silent:
2996                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2997                 sys.stdout.flush()
2998             cnt = cnt + 1
2999
3000             try:
3001                 if self.detectBranches:
3002                     branches = self.splitFilesIntoBranches(description)
3003                     for branch in branches.keys():
3004                         ## HACK  --hwn
3005                         branchPrefix = self.depotPaths[0] + branch + "/"
3006                         self.branchPrefixes = [ branchPrefix ]
3007
3008                         parent = ""
3009
3010                         filesForCommit = branches[branch]
3011
3012                         if self.verbose:
3013                             print "branch is %s" % branch
3014
3015                         self.updatedBranches.add(branch)
3016
3017                         if branch not in self.createdBranches:
3018                             self.createdBranches.add(branch)
3019                             parent = self.knownBranches[branch]
3020                             if parent == branch:
3021                                 parent = ""
3022                             else:
3023                                 fullBranch = self.projectName + branch
3024                                 if fullBranch not in self.p4BranchesInGit:
3025                                     if not self.silent:
3026                                         print("\n    Importing new branch %s" % fullBranch);
3027                                     if self.importNewBranch(branch, change - 1):
3028                                         parent = ""
3029                                         self.p4BranchesInGit.append(fullBranch)
3030                                     if not self.silent:
3031                                         print("\n    Resuming with change %s" % change);
3032
3033                                 if self.verbose:
3034                                     print "parent determined through known branches: %s" % parent
3035
3036                         branch = self.gitRefForBranch(branch)
3037                         parent = self.gitRefForBranch(parent)
3038
3039                         if self.verbose:
3040                             print "looking for initial parent for %s; current parent is %s" % (branch, parent)
3041
3042                         if len(parent) == 0 and branch in self.initialParents:
3043                             parent = self.initialParents[branch]
3044                             del self.initialParents[branch]
3045
3046                         blob = None
3047                         if len(parent) > 0:
3048                             tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3049                             if self.verbose:
3050                                 print "Creating temporary branch: " + tempBranch
3051                             self.commit(description, filesForCommit, tempBranch)
3052                             self.tempBranches.append(tempBranch)
3053                             self.checkpoint()
3054                             blob = self.searchParent(parent, branch, tempBranch)
3055                         if blob:
3056                             self.commit(description, filesForCommit, branch, blob)
3057                         else:
3058                             if self.verbose:
3059                                 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
3060                             self.commit(description, filesForCommit, branch, parent)
3061                 else:
3062                     files = self.extractFilesFromCommit(description)
3063                     self.commit(description, files, self.branch,
3064                                 self.initialParent)
3065                     # only needed once, to connect to the previous commit
3066                     self.initialParent = ""
3067             except IOError:
3068                 print self.gitError.read()
3069                 sys.exit(1)
3070
3071     def importHeadRevision(self, revision):
3072         print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
3073
3074         details = {}
3075         details["user"] = "git perforce import user"
3076         details["desc"] = ("Initial import of %s from the state at revision %s\n"
3077                            % (' '.join(self.depotPaths), revision))
3078         details["change"] = revision
3079         newestRevision = 0
3080
3081         fileCnt = 0
3082         fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3083
3084         for info in p4CmdList(["files"] + fileArgs):
3085
3086             if 'code' in info and info['code'] == 'error':
3087                 sys.stderr.write("p4 returned an error: %s\n"
3088                                  % info['data'])
3089                 if info['data'].find("must refer to client") >= 0:
3090                     sys.stderr.write("This particular p4 error is misleading.\n")
3091                     sys.stderr.write("Perhaps the depot path was misspelled.\n");
3092                     sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
3093                 sys.exit(1)
3094             if 'p4ExitCode' in info:
3095                 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3096                 sys.exit(1)
3097
3098
3099             change = int(info["change"])
3100             if change > newestRevision:
3101                 newestRevision = change
3102
3103             if info["action"] in self.delete_actions:
3104                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3105                 #fileCnt = fileCnt + 1
3106                 continue
3107
3108             for prop in ["depotFile", "rev", "action", "type" ]:
3109                 details["%s%s" % (prop, fileCnt)] = info[prop]
3110
3111             fileCnt = fileCnt + 1
3112
3113         details["change"] = newestRevision
3114
3115         # Use time from top-most change so that all git p4 clones of
3116         # the same p4 repo have the same commit SHA1s.
3117         res = p4_describe(newestRevision)
3118         details["time"] = res["time"]
3119
3120         self.updateOptionDict(details)
3121         try:
3122             self.commit(details, self.extractFilesFromCommit(details), self.branch)
3123         except IOError:
3124             print "IO error with git fast-import. Is your git version recent enough?"
3125             print self.gitError.read()
3126
3127
3128     def run(self, args):
3129         self.depotPaths = []
3130         self.changeRange = ""
3131         self.previousDepotPaths = []
3132         self.hasOrigin = False
3133
3134         # map from branch depot path to parent branch
3135         self.knownBranches = {}
3136         self.initialParents = {}
3137
3138         if self.importIntoRemotes:
3139             self.refPrefix = "refs/remotes/p4/"
3140         else:
3141             self.refPrefix = "refs/heads/p4/"
3142
3143         if self.syncWithOrigin:
3144             self.hasOrigin = originP4BranchesExist()
3145             if self.hasOrigin:
3146                 if not self.silent:
3147                     print 'Syncing with origin first, using "git fetch origin"'
3148                 system("git fetch origin")
3149
3150         branch_arg_given = bool(self.branch)
3151         if len(self.branch) == 0:
3152             self.branch = self.refPrefix + "master"
3153             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
3154                 system("git update-ref %s refs/heads/p4" % self.branch)
3155                 system("git branch -D p4")
3156
3157         # accept either the command-line option, or the configuration variable
3158         if self.useClientSpec:
3159             # will use this after clone to set the variable
3160             self.useClientSpec_from_options = True
3161         else:
3162             if gitConfigBool("git-p4.useclientspec"):
3163                 self.useClientSpec = True
3164         if self.useClientSpec:
3165             self.clientSpecDirs = getClientSpec()
3166
3167         # TODO: should always look at previous commits,
3168         # merge with previous imports, if possible.
3169         if args == []:
3170             if self.hasOrigin:
3171                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
3172
3173             # branches holds mapping from branch name to sha1
3174             branches = p4BranchesInGit(self.importIntoRemotes)
3175
3176             # restrict to just this one, disabling detect-branches
3177             if branch_arg_given:
3178                 short = self.branch.split("/")[-1]
3179                 if short in branches:
3180                     self.p4BranchesInGit = [ short ]
3181             else:
3182                 self.p4BranchesInGit = branches.keys()
3183
3184             if len(self.p4BranchesInGit) > 1:
3185                 if not self.silent:
3186                     print "Importing from/into multiple branches"
3187                 self.detectBranches = True
3188                 for branch in branches.keys():
3189                     self.initialParents[self.refPrefix + branch] = \
3190                         branches[branch]
3191
3192             if self.verbose:
3193                 print "branches: %s" % self.p4BranchesInGit
3194
3195             p4Change = 0
3196             for branch in self.p4BranchesInGit:
3197                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
3198
3199                 settings = extractSettingsGitLog(logMsg)
3200
3201                 self.readOptions(settings)
3202                 if (settings.has_key('depot-paths')
3203                     and settings.has_key ('change')):
3204                     change = int(settings['change']) + 1
3205                     p4Change = max(p4Change, change)
3206
3207                     depotPaths = sorted(settings['depot-paths'])
3208                     if self.previousDepotPaths == []:
3209                         self.previousDepotPaths = depotPaths
3210                     else:
3211                         paths = []
3212                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
3213                             prev_list = prev.split("/")
3214                             cur_list = cur.split("/")
3215                             for i in range(0, min(len(cur_list), len(prev_list))):
3216                                 if cur_list[i] <> prev_list[i]:
3217                                     i = i - 1
3218                                     break
3219
3220                             paths.append ("/".join(cur_list[:i + 1]))
3221
3222                         self.previousDepotPaths = paths
3223
3224             if p4Change > 0:
3225                 self.depotPaths = sorted(self.previousDepotPaths)
3226                 self.changeRange = "@%s,#head" % p4Change
3227                 if not self.silent and not self.detectBranches:
3228                     print "Performing incremental import into %s git branch" % self.branch
3229
3230         # accept multiple ref name abbreviations:
3231         #    refs/foo/bar/branch -> use it exactly
3232         #    p4/branch -> prepend refs/remotes/ or refs/heads/
3233         #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
3234         if not self.branch.startswith("refs/"):
3235             if self.importIntoRemotes:
3236                 prepend = "refs/remotes/"
3237             else:
3238                 prepend = "refs/heads/"
3239             if not self.branch.startswith("p4/"):
3240                 prepend += "p4/"
3241             self.branch = prepend + self.branch
3242
3243         if len(args) == 0 and self.depotPaths:
3244             if not self.silent:
3245                 print "Depot paths: %s" % ' '.join(self.depotPaths)
3246         else:
3247             if self.depotPaths and self.depotPaths != args:
3248                 print ("previous import used depot path %s and now %s was specified. "
3249                        "This doesn't work!" % (' '.join (self.depotPaths),
3250                                                ' '.join (args)))
3251                 sys.exit(1)
3252
3253             self.depotPaths = sorted(args)
3254
3255         revision = ""
3256         self.users = {}
3257
3258         # Make sure no revision specifiers are used when --changesfile
3259         # is specified.
3260         bad_changesfile = False
3261         if len(self.changesFile) > 0:
3262             for p in self.depotPaths:
3263                 if p.find("@") >= 0 or p.find("#") >= 0:
3264                     bad_changesfile = True
3265                     break
3266         if bad_changesfile:
3267             die("Option --changesfile is incompatible with revision specifiers")
3268
3269         newPaths = []
3270         for p in self.depotPaths:
3271             if p.find("@") != -1:
3272                 atIdx = p.index("@")
3273                 self.changeRange = p[atIdx:]
3274                 if self.changeRange == "@all":
3275                     self.changeRange = ""
3276                 elif ',' not in self.changeRange:
3277                     revision = self.changeRange
3278                     self.changeRange = ""
3279                 p = p[:atIdx]
3280             elif p.find("#") != -1:
3281                 hashIdx = p.index("#")
3282                 revision = p[hashIdx:]
3283                 p = p[:hashIdx]
3284             elif self.previousDepotPaths == []:
3285                 # pay attention to changesfile, if given, else import
3286                 # the entire p4 tree at the head revision
3287                 if len(self.changesFile) == 0:
3288                     revision = "#head"
3289
3290             p = re.sub ("\.\.\.$", "", p)
3291             if not p.endswith("/"):
3292                 p += "/"
3293
3294             newPaths.append(p)
3295
3296         self.depotPaths = newPaths
3297
3298         # --detect-branches may change this for each branch
3299         self.branchPrefixes = self.depotPaths
3300
3301         self.loadUserMapFromCache()
3302         self.labels = {}
3303         if self.detectLabels:
3304             self.getLabels();
3305
3306         if self.detectBranches:
3307             ## FIXME - what's a P4 projectName ?
3308             self.projectName = self.guessProjectName()
3309
3310             if self.hasOrigin:
3311                 self.getBranchMappingFromGitBranches()
3312             else:
3313                 self.getBranchMapping()
3314             if self.verbose:
3315                 print "p4-git branches: %s" % self.p4BranchesInGit
3316                 print "initial parents: %s" % self.initialParents
3317             for b in self.p4BranchesInGit:
3318                 if b != "master":
3319
3320                     ## FIXME
3321                     b = b[len(self.projectName):]
3322                 self.createdBranches.add(b)
3323
3324         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
3325
3326         self.importProcess = subprocess.Popen(["git", "fast-import"],
3327                                               stdin=subprocess.PIPE,
3328                                               stdout=subprocess.PIPE,
3329                                               stderr=subprocess.PIPE);
3330         self.gitOutput = self.importProcess.stdout
3331         self.gitStream = self.importProcess.stdin
3332         self.gitError = self.importProcess.stderr
3333
3334         if revision:
3335             self.importHeadRevision(revision)
3336         else:
3337             changes = []
3338
3339             if len(self.changesFile) > 0:
3340                 output = open(self.changesFile).readlines()
3341                 changeSet = set()
3342                 for line in output:
3343                     changeSet.add(int(line))
3344
3345                 for change in changeSet:
3346                     changes.append(change)
3347
3348                 changes.sort()
3349             else:
3350                 # catch "git p4 sync" with no new branches, in a repo that
3351                 # does not have any existing p4 branches
3352                 if len(args) == 0:
3353                     if not self.p4BranchesInGit:
3354                         die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
3355
3356                     # The default branch is master, unless --branch is used to
3357                     # specify something else.  Make sure it exists, or complain
3358                     # nicely about how to use --branch.
3359                     if not self.detectBranches:
3360                         if not branch_exists(self.branch):
3361                             if branch_arg_given:
3362                                 die("Error: branch %s does not exist." % self.branch)
3363                             else:
3364                                 die("Error: no branch %s; perhaps specify one with --branch." %
3365                                     self.branch)
3366
3367                 if self.verbose:
3368                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3369                                                               self.changeRange)
3370                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3371
3372                 if len(self.maxChanges) > 0:
3373                     changes = changes[:min(int(self.maxChanges), len(changes))]
3374
3375             if len(changes) == 0:
3376                 if not self.silent:
3377                     print "No changes to import!"
3378             else:
3379                 if not self.silent and not self.detectBranches:
3380                     print "Import destination: %s" % self.branch
3381
3382                 self.updatedBranches = set()
3383
3384                 if not self.detectBranches:
3385                     if args:
3386                         # start a new branch
3387                         self.initialParent = ""
3388                     else:
3389                         # build on a previous revision
3390                         self.initialParent = parseRevision(self.branch)
3391
3392                 self.importChanges(changes)
3393
3394                 if not self.silent:
3395                     print ""
3396                     if len(self.updatedBranches) > 0:
3397                         sys.stdout.write("Updated branches: ")
3398                         for b in self.updatedBranches:
3399                             sys.stdout.write("%s " % b)
3400                         sys.stdout.write("\n")
3401
3402         if gitConfigBool("git-p4.importLabels"):
3403             self.importLabels = True
3404
3405         if self.importLabels:
3406             p4Labels = getP4Labels(self.depotPaths)
3407             gitTags = getGitTags()
3408
3409             missingP4Labels = p4Labels - gitTags
3410             self.importP4Labels(self.gitStream, missingP4Labels)
3411
3412         self.gitStream.close()
3413         if self.importProcess.wait() != 0:
3414             die("fast-import failed: %s" % self.gitError.read())
3415         self.gitOutput.close()
3416         self.gitError.close()
3417
3418         # Cleanup temporary branches created during import
3419         if self.tempBranches != []:
3420             for branch in self.tempBranches:
3421                 read_pipe("git update-ref -d %s" % branch)
3422             os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3423
3424         # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3425         # a convenient shortcut refname "p4".
3426         if self.importIntoRemotes:
3427             head_ref = self.refPrefix + "HEAD"
3428             if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3429                 system(["git", "symbolic-ref", head_ref, self.branch])
3430
3431         return True
3432
3433 class P4Rebase(Command):
3434     def __init__(self):
3435         Command.__init__(self)
3436         self.options = [
3437                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3438         ]
3439         self.importLabels = False
3440         self.description = ("Fetches the latest revision from perforce and "
3441                             + "rebases the current work (branch) against it")
3442
3443     def run(self, args):
3444         sync = P4Sync()
3445         sync.importLabels = self.importLabels
3446         sync.run([])
3447
3448         return self.rebase()
3449
3450     def rebase(self):
3451         if os.system("git update-index --refresh") != 0:
3452             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.");
3453         if len(read_pipe("git diff-index HEAD --")) > 0:
3454             die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3455
3456         [upstream, settings] = findUpstreamBranchPoint()
3457         if len(upstream) == 0:
3458             die("Cannot find upstream branchpoint for rebase")
3459
3460         # the branchpoint may be p4/foo~3, so strip off the parent
3461         upstream = re.sub("~[0-9]+$", "", upstream)
3462
3463         print "Rebasing the current branch onto %s" % upstream
3464         oldHead = read_pipe("git rev-parse HEAD").strip()
3465         system("git rebase %s" % upstream)
3466         system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3467         return True
3468
3469 class P4Clone(P4Sync):
3470     def __init__(self):
3471         P4Sync.__init__(self)
3472         self.description = "Creates a new git repository and imports from Perforce into it"
3473         self.usage = "usage: %prog [options] //depot/path[@revRange]"
3474         self.options += [
3475             optparse.make_option("--destination", dest="cloneDestination",
3476                                  action='store', default=None,
3477                                  help="where to leave result of the clone"),
3478             optparse.make_option("--bare", dest="cloneBare",
3479                                  action="store_true", default=False),
3480         ]
3481         self.cloneDestination = None
3482         self.needsGit = False
3483         self.cloneBare = False
3484
3485     def defaultDestination(self, args):
3486         ## TODO: use common prefix of args?
3487         depotPath = args[0]
3488         depotDir = re.sub("(@[^@]*)$", "", depotPath)
3489         depotDir = re.sub("(#[^#]*)$", "", depotDir)
3490         depotDir = re.sub(r"\.\.\.$", "", depotDir)
3491         depotDir = re.sub(r"/$", "", depotDir)
3492         return os.path.split(depotDir)[1]
3493
3494     def run(self, args):
3495         if len(args) < 1:
3496             return False
3497
3498         if self.keepRepoPath and not self.cloneDestination:
3499             sys.stderr.write("Must specify destination for --keep-path\n")
3500             sys.exit(1)
3501
3502         depotPaths = args
3503
3504         if not self.cloneDestination and len(depotPaths) > 1:
3505             self.cloneDestination = depotPaths[-1]
3506             depotPaths = depotPaths[:-1]
3507
3508         self.cloneExclude = ["/"+p for p in self.cloneExclude]
3509         for p in depotPaths:
3510             if not p.startswith("//"):
3511                 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3512                 return False
3513
3514         if not self.cloneDestination:
3515             self.cloneDestination = self.defaultDestination(args)
3516
3517         print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3518
3519         if not os.path.exists(self.cloneDestination):
3520             os.makedirs(self.cloneDestination)
3521         chdir(self.cloneDestination)
3522
3523         init_cmd = [ "git", "init" ]
3524         if self.cloneBare:
3525             init_cmd.append("--bare")
3526         retcode = subprocess.call(init_cmd)
3527         if retcode:
3528             raise CalledProcessError(retcode, init_cmd)
3529
3530         if not P4Sync.run(self, depotPaths):
3531             return False
3532
3533         # create a master branch and check out a work tree
3534         if gitBranchExists(self.branch):
3535             system([ "git", "branch", "master", self.branch ])
3536             if not self.cloneBare:
3537                 system([ "git", "checkout", "-f" ])
3538         else:
3539             print 'Not checking out any branch, use ' \
3540                   '"git checkout -q -b master <branch>"'
3541
3542         # auto-set this variable if invoked with --use-client-spec
3543         if self.useClientSpec_from_options:
3544             system("git config --bool git-p4.useclientspec true")
3545
3546         return True
3547
3548 class P4Branches(Command):
3549     def __init__(self):
3550         Command.__init__(self)
3551         self.options = [ ]
3552         self.description = ("Shows the git branches that hold imports and their "
3553                             + "corresponding perforce depot paths")
3554         self.verbose = False
3555
3556     def run(self, args):
3557         if originP4BranchesExist():
3558             createOrUpdateBranchesFromOrigin()
3559
3560         cmdline = "git rev-parse --symbolic "
3561         cmdline += " --remotes"
3562
3563         for line in read_pipe_lines(cmdline):
3564             line = line.strip()
3565
3566             if not line.startswith('p4/') or line == "p4/HEAD":
3567                 continue
3568             branch = line
3569
3570             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3571             settings = extractSettingsGitLog(log)
3572
3573             print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3574         return True
3575
3576 class HelpFormatter(optparse.IndentedHelpFormatter):
3577     def __init__(self):
3578         optparse.IndentedHelpFormatter.__init__(self)
3579
3580     def format_description(self, description):
3581         if description:
3582             return description + "\n"
3583         else:
3584             return ""
3585
3586 def printUsage(commands):
3587     print "usage: %s <command> [options]" % sys.argv[0]
3588     print ""
3589     print "valid commands: %s" % ", ".join(commands)
3590     print ""
3591     print "Try %s <command> --help for command specific help." % sys.argv[0]
3592     print ""
3593
3594 commands = {
3595     "debug" : P4Debug,
3596     "submit" : P4Submit,
3597     "commit" : P4Submit,
3598     "sync" : P4Sync,
3599     "rebase" : P4Rebase,
3600     "clone" : P4Clone,
3601     "rollback" : P4RollBack,
3602     "branches" : P4Branches
3603 }
3604
3605
3606 def main():
3607     if len(sys.argv[1:]) == 0:
3608         printUsage(commands.keys())
3609         sys.exit(2)
3610
3611     cmdName = sys.argv[1]
3612     try:
3613         klass = commands[cmdName]
3614         cmd = klass()
3615     except KeyError:
3616         print "unknown command %s" % cmdName
3617         print ""
3618         printUsage(commands.keys())
3619         sys.exit(2)
3620
3621     options = cmd.options
3622     cmd.gitdir = os.environ.get("GIT_DIR", None)
3623
3624     args = sys.argv[2:]
3625
3626     options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3627     if cmd.needsGit:
3628         options.append(optparse.make_option("--git-dir", dest="gitdir"))
3629
3630     parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3631                                    options,
3632                                    description = cmd.description,
3633                                    formatter = HelpFormatter())
3634
3635     (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3636     global verbose
3637     verbose = cmd.verbose
3638     if cmd.needsGit:
3639         if cmd.gitdir == None:
3640             cmd.gitdir = os.path.abspath(".git")
3641             if not isValidGitDir(cmd.gitdir):
3642                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3643                 if os.path.exists(cmd.gitdir):
3644                     cdup = read_pipe("git rev-parse --show-cdup").strip()
3645                     if len(cdup) > 0:
3646                         chdir(cdup);
3647
3648         if not isValidGitDir(cmd.gitdir):
3649             if isValidGitDir(cmd.gitdir + "/.git"):
3650                 cmd.gitdir += "/.git"
3651             else:
3652                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3653
3654         os.environ["GIT_DIR"] = cmd.gitdir
3655
3656     if not cmd.run(args):
3657         parser.print_help()
3658         sys.exit(2)
3659
3660
3661 if __name__ == '__main__':
3662     main()