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