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