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