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