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