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