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