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