Merge branch 'jc/push-reject-reasons'
[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
11 import sys
12 if sys.hexversion < 0x02040000:
13     # The limiter is the subprocess module
14     sys.stderr.write("git-p4: requires Python 2.4 or later.\n")
15     sys.exit(1)
16
17 import optparse, os, marshal, subprocess, shelve
18 import tempfile, getopt, os.path, time, platform
19 import re, shutil
20
21 try:
22     from subprocess import CalledProcessError
23 except ImportError:
24     # from python2.7:subprocess.py
25     # Exception classes used by this module.
26     class CalledProcessError(Exception):
27         """This exception is raised when a process run by check_call() returns
28         a non-zero exit status.  The exit status will be stored in the
29         returncode attribute."""
30         def __init__(self, returncode, cmd):
31             self.returncode = returncode
32             self.cmd = cmd
33         def __str__(self):
34             return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
35
36 verbose = False
37
38 # Only labels/tags matching this will be imported/exported
39 defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
40
41 def p4_build_cmd(cmd):
42     """Build a suitable p4 command line.
43
44     This consolidates building and returning a p4 command line into one
45     location. It means that hooking into the environment, or other configuration
46     can be done more easily.
47     """
48     real_cmd = ["p4"]
49
50     user = gitConfig("git-p4.user")
51     if len(user) > 0:
52         real_cmd += ["-u",user]
53
54     password = gitConfig("git-p4.password")
55     if len(password) > 0:
56         real_cmd += ["-P", password]
57
58     port = gitConfig("git-p4.port")
59     if len(port) > 0:
60         real_cmd += ["-p", port]
61
62     host = gitConfig("git-p4.host")
63     if len(host) > 0:
64         real_cmd += ["-H", host]
65
66     client = gitConfig("git-p4.client")
67     if len(client) > 0:
68         real_cmd += ["-c", client]
69
70
71     if isinstance(cmd,basestring):
72         real_cmd = ' '.join(real_cmd) + ' ' + cmd
73     else:
74         real_cmd += cmd
75     return real_cmd
76
77 def chdir(dir):
78     # P4 uses the PWD environment variable rather than getcwd(). Since we're
79     # not using the shell, we have to set it ourselves.  This path could
80     # be relative, so go there first, then figure out where we ended up.
81     os.chdir(dir)
82     os.environ['PWD'] = os.getcwd()
83
84 def die(msg):
85     if verbose:
86         raise Exception(msg)
87     else:
88         sys.stderr.write(msg + "\n")
89         sys.exit(1)
90
91 def write_pipe(c, stdin):
92     if verbose:
93         sys.stderr.write('Writing pipe: %s\n' % str(c))
94
95     expand = isinstance(c,basestring)
96     p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
97     pipe = p.stdin
98     val = pipe.write(stdin)
99     pipe.close()
100     if p.wait():
101         die('Command failed: %s' % str(c))
102
103     return val
104
105 def p4_write_pipe(c, stdin):
106     real_cmd = p4_build_cmd(c)
107     return write_pipe(real_cmd, stdin)
108
109 def read_pipe(c, ignore_error=False):
110     if verbose:
111         sys.stderr.write('Reading pipe: %s\n' % str(c))
112
113     expand = isinstance(c,basestring)
114     p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
115     pipe = p.stdout
116     val = pipe.read()
117     if p.wait() and not ignore_error:
118         die('Command failed: %s' % str(c))
119
120     return val
121
122 def p4_read_pipe(c, ignore_error=False):
123     real_cmd = p4_build_cmd(c)
124     return read_pipe(real_cmd, ignore_error)
125
126 def read_pipe_lines(c):
127     if verbose:
128         sys.stderr.write('Reading pipe: %s\n' % str(c))
129
130     expand = isinstance(c, basestring)
131     p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
132     pipe = p.stdout
133     val = pipe.readlines()
134     if pipe.close() or p.wait():
135         die('Command failed: %s' % str(c))
136
137     return val
138
139 def p4_read_pipe_lines(c):
140     """Specifically invoke p4 on the command supplied. """
141     real_cmd = p4_build_cmd(c)
142     return read_pipe_lines(real_cmd)
143
144 def p4_has_command(cmd):
145     """Ask p4 for help on this command.  If it returns an error, the
146        command does not exist in this version of p4."""
147     real_cmd = p4_build_cmd(["help", cmd])
148     p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
149                                    stderr=subprocess.PIPE)
150     p.communicate()
151     return p.returncode == 0
152
153 def p4_has_move_command():
154     """See if the move command exists, that it supports -k, and that
155        it has not been administratively disabled.  The arguments
156        must be correct, but the filenames do not have to exist.  Use
157        ones with wildcards so even if they exist, it will fail."""
158
159     if not p4_has_command("move"):
160         return False
161     cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
162     p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
163     (out, err) = p.communicate()
164     # return code will be 1 in either case
165     if err.find("Invalid option") >= 0:
166         return False
167     if err.find("disabled") >= 0:
168         return False
169     # assume it failed because @... was invalid changelist
170     return True
171
172 def system(cmd):
173     expand = isinstance(cmd,basestring)
174     if verbose:
175         sys.stderr.write("executing %s\n" % str(cmd))
176     retcode = subprocess.call(cmd, shell=expand)
177     if retcode:
178         raise CalledProcessError(retcode, cmd)
179
180 def p4_system(cmd):
181     """Specifically invoke p4 as the system command. """
182     real_cmd = p4_build_cmd(cmd)
183     expand = isinstance(real_cmd, basestring)
184     retcode = subprocess.call(real_cmd, shell=expand)
185     if retcode:
186         raise CalledProcessError(retcode, real_cmd)
187
188 def p4_integrate(src, dest):
189     p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
190
191 def p4_sync(f, *options):
192     p4_system(["sync"] + list(options) + [wildcard_encode(f)])
193
194 def p4_add(f):
195     # forcibly add file names with wildcards
196     if wildcard_present(f):
197         p4_system(["add", "-f", f])
198     else:
199         p4_system(["add", f])
200
201 def p4_delete(f):
202     p4_system(["delete", wildcard_encode(f)])
203
204 def p4_edit(f):
205     p4_system(["edit", wildcard_encode(f)])
206
207 def p4_revert(f):
208     p4_system(["revert", wildcard_encode(f)])
209
210 def p4_reopen(type, f):
211     p4_system(["reopen", "-t", type, wildcard_encode(f)])
212
213 def p4_move(src, dest):
214     p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
215
216 def p4_describe(change):
217     """Make sure it returns a valid result by checking for
218        the presence of field "time".  Return a dict of the
219        results."""
220
221     ds = p4CmdList(["describe", "-s", str(change)])
222     if len(ds) != 1:
223         die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
224
225     d = ds[0]
226
227     if "p4ExitCode" in d:
228         die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
229                                                       str(d)))
230     if "code" in d:
231         if d["code"] == "error":
232             die("p4 describe -s %d returned error code: %s" % (change, str(d)))
233
234     if "time" not in d:
235         die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
236
237     return d
238
239 #
240 # Canonicalize the p4 type and return a tuple of the
241 # base type, plus any modifiers.  See "p4 help filetypes"
242 # for a list and explanation.
243 #
244 def split_p4_type(p4type):
245
246     p4_filetypes_historical = {
247         "ctempobj": "binary+Sw",
248         "ctext": "text+C",
249         "cxtext": "text+Cx",
250         "ktext": "text+k",
251         "kxtext": "text+kx",
252         "ltext": "text+F",
253         "tempobj": "binary+FSw",
254         "ubinary": "binary+F",
255         "uresource": "resource+F",
256         "uxbinary": "binary+Fx",
257         "xbinary": "binary+x",
258         "xltext": "text+Fx",
259         "xtempobj": "binary+Swx",
260         "xtext": "text+x",
261         "xunicode": "unicode+x",
262         "xutf16": "utf16+x",
263     }
264     if p4type in p4_filetypes_historical:
265         p4type = p4_filetypes_historical[p4type]
266     mods = ""
267     s = p4type.split("+")
268     base = s[0]
269     mods = ""
270     if len(s) > 1:
271         mods = s[1]
272     return (base, mods)
273
274 #
275 # return the raw p4 type of a file (text, text+ko, etc)
276 #
277 def p4_type(file):
278     results = p4CmdList(["fstat", "-T", "headType", file])
279     return results[0]['headType']
280
281 #
282 # Given a type base and modifier, return a regexp matching
283 # the keywords that can be expanded in the file
284 #
285 def p4_keywords_regexp_for_type(base, type_mods):
286     if base in ("text", "unicode", "binary"):
287         kwords = None
288         if "ko" in type_mods:
289             kwords = 'Id|Header'
290         elif "k" in type_mods:
291             kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
292         else:
293             return None
294         pattern = r"""
295             \$              # Starts with a dollar, followed by...
296             (%s)            # one of the keywords, followed by...
297             (:[^$\n]+)?     # possibly an old expansion, followed by...
298             \$              # another dollar
299             """ % kwords
300         return pattern
301     else:
302         return None
303
304 #
305 # Given a file, return a regexp matching the possible
306 # RCS keywords that will be expanded, or None for files
307 # with kw expansion turned off.
308 #
309 def p4_keywords_regexp_for_file(file):
310     if not os.path.exists(file):
311         return None
312     else:
313         (type_base, type_mods) = split_p4_type(p4_type(file))
314         return p4_keywords_regexp_for_type(type_base, type_mods)
315
316 def setP4ExecBit(file, mode):
317     # Reopens an already open file and changes the execute bit to match
318     # the execute bit setting in the passed in mode.
319
320     p4Type = "+x"
321
322     if not isModeExec(mode):
323         p4Type = getP4OpenedType(file)
324         p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
325         p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
326         if p4Type[-1] == "+":
327             p4Type = p4Type[0:-1]
328
329     p4_reopen(p4Type, file)
330
331 def getP4OpenedType(file):
332     # Returns the perforce file type for the given file.
333
334     result = p4_read_pipe(["opened", wildcard_encode(file)])
335     match = re.match(".*\((.+)\)\r?$", result)
336     if match:
337         return match.group(1)
338     else:
339         die("Could not determine file type for %s (result: '%s')" % (file, result))
340
341 # Return the set of all p4 labels
342 def getP4Labels(depotPaths):
343     labels = set()
344     if isinstance(depotPaths,basestring):
345         depotPaths = [depotPaths]
346
347     for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
348         label = l['label']
349         labels.add(label)
350
351     return labels
352
353 # Return the set of all git tags
354 def getGitTags():
355     gitTags = set()
356     for line in read_pipe_lines(["git", "tag"]):
357         tag = line.strip()
358         gitTags.add(tag)
359     return gitTags
360
361 def diffTreePattern():
362     # This is a simple generator for the diff tree regex pattern. This could be
363     # a class variable if this and parseDiffTreeEntry were a part of a class.
364     pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
365     while True:
366         yield pattern
367
368 def parseDiffTreeEntry(entry):
369     """Parses a single diff tree entry into its component elements.
370
371     See git-diff-tree(1) manpage for details about the format of the diff
372     output. This method returns a dictionary with the following elements:
373
374     src_mode - The mode of the source file
375     dst_mode - The mode of the destination file
376     src_sha1 - The sha1 for the source file
377     dst_sha1 - The sha1 fr the destination file
378     status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
379     status_score - The score for the status (applicable for 'C' and 'R'
380                    statuses). This is None if there is no score.
381     src - The path for the source file.
382     dst - The path for the destination file. This is only present for
383           copy or renames. If it is not present, this is None.
384
385     If the pattern is not matched, None is returned."""
386
387     match = diffTreePattern().next().match(entry)
388     if match:
389         return {
390             'src_mode': match.group(1),
391             'dst_mode': match.group(2),
392             'src_sha1': match.group(3),
393             'dst_sha1': match.group(4),
394             'status': match.group(5),
395             'status_score': match.group(6),
396             'src': match.group(7),
397             'dst': match.group(10)
398         }
399     return None
400
401 def isModeExec(mode):
402     # Returns True if the given git mode represents an executable file,
403     # otherwise False.
404     return mode[-3:] == "755"
405
406 def isModeExecChanged(src_mode, dst_mode):
407     return isModeExec(src_mode) != isModeExec(dst_mode)
408
409 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
410
411     if isinstance(cmd,basestring):
412         cmd = "-G " + cmd
413         expand = True
414     else:
415         cmd = ["-G"] + cmd
416         expand = False
417
418     cmd = p4_build_cmd(cmd)
419     if verbose:
420         sys.stderr.write("Opening pipe: %s\n" % str(cmd))
421
422     # Use a temporary file to avoid deadlocks without
423     # subprocess.communicate(), which would put another copy
424     # of stdout into memory.
425     stdin_file = None
426     if stdin is not None:
427         stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
428         if isinstance(stdin,basestring):
429             stdin_file.write(stdin)
430         else:
431             for i in stdin:
432                 stdin_file.write(i + '\n')
433         stdin_file.flush()
434         stdin_file.seek(0)
435
436     p4 = subprocess.Popen(cmd,
437                           shell=expand,
438                           stdin=stdin_file,
439                           stdout=subprocess.PIPE)
440
441     result = []
442     try:
443         while True:
444             entry = marshal.load(p4.stdout)
445             if cb is not None:
446                 cb(entry)
447             else:
448                 result.append(entry)
449     except EOFError:
450         pass
451     exitCode = p4.wait()
452     if exitCode != 0:
453         entry = {}
454         entry["p4ExitCode"] = exitCode
455         result.append(entry)
456
457     return result
458
459 def p4Cmd(cmd):
460     list = p4CmdList(cmd)
461     result = {}
462     for entry in list:
463         result.update(entry)
464     return result;
465
466 def p4Where(depotPath):
467     if not depotPath.endswith("/"):
468         depotPath += "/"
469     depotPath = depotPath + "..."
470     outputList = p4CmdList(["where", depotPath])
471     output = None
472     for entry in outputList:
473         if "depotFile" in entry:
474             if entry["depotFile"] == depotPath:
475                 output = entry
476                 break
477         elif "data" in entry:
478             data = entry.get("data")
479             space = data.find(" ")
480             if data[:space] == depotPath:
481                 output = entry
482                 break
483     if output == None:
484         return ""
485     if output["code"] == "error":
486         return ""
487     clientPath = ""
488     if "path" in output:
489         clientPath = output.get("path")
490     elif "data" in output:
491         data = output.get("data")
492         lastSpace = data.rfind(" ")
493         clientPath = data[lastSpace + 1:]
494
495     if clientPath.endswith("..."):
496         clientPath = clientPath[:-3]
497     return clientPath
498
499 def currentGitBranch():
500     return read_pipe("git name-rev HEAD").split(" ")[1].strip()
501
502 def isValidGitDir(path):
503     if (os.path.exists(path + "/HEAD")
504         and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
505         return True;
506     return False
507
508 def parseRevision(ref):
509     return read_pipe("git rev-parse %s" % ref).strip()
510
511 def branchExists(ref):
512     rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
513                      ignore_error=True)
514     return len(rev) > 0
515
516 def extractLogMessageFromGitCommit(commit):
517     logMessage = ""
518
519     ## fixme: title is first line of commit, not 1st paragraph.
520     foundTitle = False
521     for log in read_pipe_lines("git cat-file commit %s" % commit):
522        if not foundTitle:
523            if len(log) == 1:
524                foundTitle = True
525            continue
526
527        logMessage += log
528     return logMessage
529
530 def extractSettingsGitLog(log):
531     values = {}
532     for line in log.split("\n"):
533         line = line.strip()
534         m = re.search (r"^ *\[git-p4: (.*)\]$", line)
535         if not m:
536             continue
537
538         assignments = m.group(1).split (':')
539         for a in assignments:
540             vals = a.split ('=')
541             key = vals[0].strip()
542             val = ('='.join (vals[1:])).strip()
543             if val.endswith ('\"') and val.startswith('"'):
544                 val = val[1:-1]
545
546             values[key] = val
547
548     paths = values.get("depot-paths")
549     if not paths:
550         paths = values.get("depot-path")
551     if paths:
552         values['depot-paths'] = paths.split(',')
553     return values
554
555 def gitBranchExists(branch):
556     proc = subprocess.Popen(["git", "rev-parse", branch],
557                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);
558     return proc.wait() == 0;
559
560 _gitConfig = {}
561 def gitConfig(key, args = None): # set args to "--bool", for instance
562     if not _gitConfig.has_key(key):
563         argsFilter = ""
564         if args != None:
565             argsFilter = "%s " % args
566         cmd = "git config %s%s" % (argsFilter, key)
567         _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
568     return _gitConfig[key]
569
570 def gitConfigList(key):
571     if not _gitConfig.has_key(key):
572         _gitConfig[key] = read_pipe("git config --get-all %s" % key, ignore_error=True).strip().split(os.linesep)
573     return _gitConfig[key]
574
575 def p4BranchesInGit(branchesAreInRemotes=True):
576     """Find all the branches whose names start with "p4/", looking
577        in remotes or heads as specified by the argument.  Return
578        a dictionary of { branch: revision } for each one found.
579        The branch names are the short names, without any
580        "p4/" prefix."""
581
582     branches = {}
583
584     cmdline = "git rev-parse --symbolic "
585     if branchesAreInRemotes:
586         cmdline += "--remotes"
587     else:
588         cmdline += "--branches"
589
590     for line in read_pipe_lines(cmdline):
591         line = line.strip()
592
593         # only import to p4/
594         if not line.startswith('p4/'):
595             continue
596         # special symbolic ref to p4/master
597         if line == "p4/HEAD":
598             continue
599
600         # strip off p4/ prefix
601         branch = line[len("p4/"):]
602
603         branches[branch] = parseRevision(line)
604
605     return branches
606
607 def branch_exists(branch):
608     """Make sure that the given ref name really exists."""
609
610     cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
611     p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
612     out, _ = p.communicate()
613     if p.returncode:
614         return False
615     # expect exactly one line of output: the branch name
616     return out.rstrip() == branch
617
618 def findUpstreamBranchPoint(head = "HEAD"):
619     branches = p4BranchesInGit()
620     # map from depot-path to branch name
621     branchByDepotPath = {}
622     for branch in branches.keys():
623         tip = branches[branch]
624         log = extractLogMessageFromGitCommit(tip)
625         settings = extractSettingsGitLog(log)
626         if settings.has_key("depot-paths"):
627             paths = ",".join(settings["depot-paths"])
628             branchByDepotPath[paths] = "remotes/p4/" + branch
629
630     settings = None
631     parent = 0
632     while parent < 65535:
633         commit = head + "~%s" % parent
634         log = extractLogMessageFromGitCommit(commit)
635         settings = extractSettingsGitLog(log)
636         if settings.has_key("depot-paths"):
637             paths = ",".join(settings["depot-paths"])
638             if branchByDepotPath.has_key(paths):
639                 return [branchByDepotPath[paths], settings]
640
641         parent = parent + 1
642
643     return ["", settings]
644
645 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
646     if not silent:
647         print ("Creating/updating branch(es) in %s based on origin branch(es)"
648                % localRefPrefix)
649
650     originPrefix = "origin/p4/"
651
652     for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
653         line = line.strip()
654         if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
655             continue
656
657         headName = line[len(originPrefix):]
658         remoteHead = localRefPrefix + headName
659         originHead = line
660
661         original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
662         if (not original.has_key('depot-paths')
663             or not original.has_key('change')):
664             continue
665
666         update = False
667         if not gitBranchExists(remoteHead):
668             if verbose:
669                 print "creating %s" % remoteHead
670             update = True
671         else:
672             settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
673             if settings.has_key('change') > 0:
674                 if settings['depot-paths'] == original['depot-paths']:
675                     originP4Change = int(original['change'])
676                     p4Change = int(settings['change'])
677                     if originP4Change > p4Change:
678                         print ("%s (%s) is newer than %s (%s). "
679                                "Updating p4 branch from origin."
680                                % (originHead, originP4Change,
681                                   remoteHead, p4Change))
682                         update = True
683                 else:
684                     print ("Ignoring: %s was imported from %s while "
685                            "%s was imported from %s"
686                            % (originHead, ','.join(original['depot-paths']),
687                               remoteHead, ','.join(settings['depot-paths'])))
688
689         if update:
690             system("git update-ref %s %s" % (remoteHead, originHead))
691
692 def originP4BranchesExist():
693         return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
694
695 def p4ChangesForPaths(depotPaths, changeRange):
696     assert depotPaths
697     cmd = ['changes']
698     for p in depotPaths:
699         cmd += ["%s...%s" % (p, changeRange)]
700     output = p4_read_pipe_lines(cmd)
701
702     changes = {}
703     for line in output:
704         changeNum = int(line.split(" ")[1])
705         changes[changeNum] = True
706
707     changelist = changes.keys()
708     changelist.sort()
709     return changelist
710
711 def p4PathStartsWith(path, prefix):
712     # This method tries to remedy a potential mixed-case issue:
713     #
714     # If UserA adds  //depot/DirA/file1
715     # and UserB adds //depot/dira/file2
716     #
717     # we may or may not have a problem. If you have core.ignorecase=true,
718     # we treat DirA and dira as the same directory
719     ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
720     if ignorecase:
721         return path.lower().startswith(prefix.lower())
722     return path.startswith(prefix)
723
724 def getClientSpec():
725     """Look at the p4 client spec, create a View() object that contains
726        all the mappings, and return it."""
727
728     specList = p4CmdList("client -o")
729     if len(specList) != 1:
730         die('Output from "client -o" is %d lines, expecting 1' %
731             len(specList))
732
733     # dictionary of all client parameters
734     entry = specList[0]
735
736     # just the keys that start with "View"
737     view_keys = [ k for k in entry.keys() if k.startswith("View") ]
738
739     # hold this new View
740     view = View()
741
742     # append the lines, in order, to the view
743     for view_num in range(len(view_keys)):
744         k = "View%d" % view_num
745         if k not in view_keys:
746             die("Expected view key %s missing" % k)
747         view.append(entry[k])
748
749     return view
750
751 def getClientRoot():
752     """Grab the client directory."""
753
754     output = p4CmdList("client -o")
755     if len(output) != 1:
756         die('Output from "client -o" is %d lines, expecting 1' % len(output))
757
758     entry = output[0]
759     if "Root" not in entry:
760         die('Client has no "Root"')
761
762     return entry["Root"]
763
764 #
765 # P4 wildcards are not allowed in filenames.  P4 complains
766 # if you simply add them, but you can force it with "-f", in
767 # which case it translates them into %xx encoding internally.
768 #
769 def wildcard_decode(path):
770     # Search for and fix just these four characters.  Do % last so
771     # that fixing it does not inadvertently create new %-escapes.
772     # Cannot have * in a filename in windows; untested as to
773     # what p4 would do in such a case.
774     if not platform.system() == "Windows":
775         path = path.replace("%2A", "*")
776     path = path.replace("%23", "#") \
777                .replace("%40", "@") \
778                .replace("%25", "%")
779     return path
780
781 def wildcard_encode(path):
782     # do % first to avoid double-encoding the %s introduced here
783     path = path.replace("%", "%25") \
784                .replace("*", "%2A") \
785                .replace("#", "%23") \
786                .replace("@", "%40")
787     return path
788
789 def wildcard_present(path):
790     m = re.search("[*#@%]", path)
791     return m is not None
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.isWindows = (platform.system() == "Windows")
1993         self.keepRepoPath = False
1994         self.depotPaths = None
1995         self.p4BranchesInGit = []
1996         self.cloneExclude = []
1997         self.useClientSpec = False
1998         self.useClientSpec_from_options = False
1999         self.clientSpecDirs = None
2000         self.tempBranches = []
2001         self.tempBranchLocation = "git-p4-tmp"
2002
2003         if gitConfig("git-p4.syncFromOrigin") == "false":
2004             self.syncWithOrigin = False
2005
2006     # Force a checkpoint in fast-import and wait for it to finish
2007     def checkpoint(self):
2008         self.gitStream.write("checkpoint\n\n")
2009         self.gitStream.write("progress checkpoint\n\n")
2010         out = self.gitOutput.readline()
2011         if self.verbose:
2012             print "checkpoint finished: " + out
2013
2014     def extractFilesFromCommit(self, commit):
2015         self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2016                              for path in self.cloneExclude]
2017         files = []
2018         fnum = 0
2019         while commit.has_key("depotFile%s" % fnum):
2020             path =  commit["depotFile%s" % fnum]
2021
2022             if [p for p in self.cloneExclude
2023                 if p4PathStartsWith(path, p)]:
2024                 found = False
2025             else:
2026                 found = [p for p in self.depotPaths
2027                          if p4PathStartsWith(path, p)]
2028             if not found:
2029                 fnum = fnum + 1
2030                 continue
2031
2032             file = {}
2033             file["path"] = path
2034             file["rev"] = commit["rev%s" % fnum]
2035             file["action"] = commit["action%s" % fnum]
2036             file["type"] = commit["type%s" % fnum]
2037             files.append(file)
2038             fnum = fnum + 1
2039         return files
2040
2041     def stripRepoPath(self, path, prefixes):
2042         """When streaming files, this is called to map a p4 depot path
2043            to where it should go in git.  The prefixes are either
2044            self.depotPaths, or self.branchPrefixes in the case of
2045            branch detection."""
2046
2047         if self.useClientSpec:
2048             # branch detection moves files up a level (the branch name)
2049             # from what client spec interpretation gives
2050             path = self.clientSpecDirs.map_in_client(path)
2051             if self.detectBranches:
2052                 for b in self.knownBranches:
2053                     if path.startswith(b + "/"):
2054                         path = path[len(b)+1:]
2055
2056         elif self.keepRepoPath:
2057             # Preserve everything in relative path name except leading
2058             # //depot/; just look at first prefix as they all should
2059             # be in the same depot.
2060             depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2061             if p4PathStartsWith(path, depot):
2062                 path = path[len(depot):]
2063
2064         else:
2065             for p in prefixes:
2066                 if p4PathStartsWith(path, p):
2067                     path = path[len(p):]
2068                     break
2069
2070         path = wildcard_decode(path)
2071         return path
2072
2073     def splitFilesIntoBranches(self, commit):
2074         """Look at each depotFile in the commit to figure out to what
2075            branch it belongs."""
2076
2077         branches = {}
2078         fnum = 0
2079         while commit.has_key("depotFile%s" % fnum):
2080             path =  commit["depotFile%s" % fnum]
2081             found = [p for p in self.depotPaths
2082                      if p4PathStartsWith(path, p)]
2083             if not found:
2084                 fnum = fnum + 1
2085                 continue
2086
2087             file = {}
2088             file["path"] = path
2089             file["rev"] = commit["rev%s" % fnum]
2090             file["action"] = commit["action%s" % fnum]
2091             file["type"] = commit["type%s" % fnum]
2092             fnum = fnum + 1
2093
2094             # start with the full relative path where this file would
2095             # go in a p4 client
2096             if self.useClientSpec:
2097                 relPath = self.clientSpecDirs.map_in_client(path)
2098             else:
2099                 relPath = self.stripRepoPath(path, self.depotPaths)
2100
2101             for branch in self.knownBranches.keys():
2102                 # add a trailing slash so that a commit into qt/4.2foo
2103                 # doesn't end up in qt/4.2, e.g.
2104                 if relPath.startswith(branch + "/"):
2105                     if branch not in branches:
2106                         branches[branch] = []
2107                     branches[branch].append(file)
2108                     break
2109
2110         return branches
2111
2112     # output one file from the P4 stream
2113     # - helper for streamP4Files
2114
2115     def streamOneP4File(self, file, contents):
2116         relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2117         if verbose:
2118             sys.stderr.write("%s\n" % relPath)
2119
2120         (type_base, type_mods) = split_p4_type(file["type"])
2121
2122         git_mode = "100644"
2123         if "x" in type_mods:
2124             git_mode = "100755"
2125         if type_base == "symlink":
2126             git_mode = "120000"
2127             # p4 print on a symlink contains "target\n"; remove the newline
2128             data = ''.join(contents)
2129             contents = [data[:-1]]
2130
2131         if type_base == "utf16":
2132             # p4 delivers different text in the python output to -G
2133             # than it does when using "print -o", or normal p4 client
2134             # operations.  utf16 is converted to ascii or utf8, perhaps.
2135             # But ascii text saved as -t utf16 is completely mangled.
2136             # Invoke print -o to get the real contents.
2137             text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
2138             contents = [ text ]
2139
2140         if type_base == "apple":
2141             # Apple filetype files will be streamed as a concatenation of
2142             # its appledouble header and the contents.  This is useless
2143             # on both macs and non-macs.  If using "print -q -o xx", it
2144             # will create "xx" with the data, and "%xx" with the header.
2145             # This is also not very useful.
2146             #
2147             # Ideally, someday, this script can learn how to generate
2148             # appledouble files directly and import those to git, but
2149             # non-mac machines can never find a use for apple filetype.
2150             print "\nIgnoring apple filetype file %s" % file['depotFile']
2151             return
2152
2153         # Perhaps windows wants unicode, utf16 newlines translated too;
2154         # but this is not doing it.
2155         if self.isWindows and type_base == "text":
2156             mangled = []
2157             for data in contents:
2158                 data = data.replace("\r\n", "\n")
2159                 mangled.append(data)
2160             contents = mangled
2161
2162         # Note that we do not try to de-mangle keywords on utf16 files,
2163         # even though in theory somebody may want that.
2164         pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2165         if pattern:
2166             regexp = re.compile(pattern, re.VERBOSE)
2167             text = ''.join(contents)
2168             text = regexp.sub(r'$\1$', text)
2169             contents = [ text ]
2170
2171         self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2172
2173         # total length...
2174         length = 0
2175         for d in contents:
2176             length = length + len(d)
2177
2178         self.gitStream.write("data %d\n" % length)
2179         for d in contents:
2180             self.gitStream.write(d)
2181         self.gitStream.write("\n")
2182
2183     def streamOneP4Deletion(self, file):
2184         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2185         if verbose:
2186             sys.stderr.write("delete %s\n" % relPath)
2187         self.gitStream.write("D %s\n" % relPath)
2188
2189     # handle another chunk of streaming data
2190     def streamP4FilesCb(self, marshalled):
2191
2192         # catch p4 errors and complain
2193         err = None
2194         if "code" in marshalled:
2195             if marshalled["code"] == "error":
2196                 if "data" in marshalled:
2197                     err = marshalled["data"].rstrip()
2198         if err:
2199             f = None
2200             if self.stream_have_file_info:
2201                 if "depotFile" in self.stream_file:
2202                     f = self.stream_file["depotFile"]
2203             # force a failure in fast-import, else an empty
2204             # commit will be made
2205             self.gitStream.write("\n")
2206             self.gitStream.write("die-now\n")
2207             self.gitStream.close()
2208             # ignore errors, but make sure it exits first
2209             self.importProcess.wait()
2210             if f:
2211                 die("Error from p4 print for %s: %s" % (f, err))
2212             else:
2213                 die("Error from p4 print: %s" % err)
2214
2215         if marshalled.has_key('depotFile') and self.stream_have_file_info:
2216             # start of a new file - output the old one first
2217             self.streamOneP4File(self.stream_file, self.stream_contents)
2218             self.stream_file = {}
2219             self.stream_contents = []
2220             self.stream_have_file_info = False
2221
2222         # pick up the new file information... for the
2223         # 'data' field we need to append to our array
2224         for k in marshalled.keys():
2225             if k == 'data':
2226                 self.stream_contents.append(marshalled['data'])
2227             else:
2228                 self.stream_file[k] = marshalled[k]
2229
2230         self.stream_have_file_info = True
2231
2232     # Stream directly from "p4 files" into "git fast-import"
2233     def streamP4Files(self, files):
2234         filesForCommit = []
2235         filesToRead = []
2236         filesToDelete = []
2237
2238         for f in files:
2239             # if using a client spec, only add the files that have
2240             # a path in the client
2241             if self.clientSpecDirs:
2242                 if self.clientSpecDirs.map_in_client(f['path']) == "":
2243                     continue
2244
2245             filesForCommit.append(f)
2246             if f['action'] in self.delete_actions:
2247                 filesToDelete.append(f)
2248             else:
2249                 filesToRead.append(f)
2250
2251         # deleted files...
2252         for f in filesToDelete:
2253             self.streamOneP4Deletion(f)
2254
2255         if len(filesToRead) > 0:
2256             self.stream_file = {}
2257             self.stream_contents = []
2258             self.stream_have_file_info = False
2259
2260             # curry self argument
2261             def streamP4FilesCbSelf(entry):
2262                 self.streamP4FilesCb(entry)
2263
2264             fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2265
2266             p4CmdList(["-x", "-", "print"],
2267                       stdin=fileArgs,
2268                       cb=streamP4FilesCbSelf)
2269
2270             # do the last chunk
2271             if self.stream_file.has_key('depotFile'):
2272                 self.streamOneP4File(self.stream_file, self.stream_contents)
2273
2274     def make_email(self, userid):
2275         if userid in self.users:
2276             return self.users[userid]
2277         else:
2278             return "%s <a@b>" % userid
2279
2280     # Stream a p4 tag
2281     def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2282         if verbose:
2283             print "writing tag %s for commit %s" % (labelName, commit)
2284         gitStream.write("tag %s\n" % labelName)
2285         gitStream.write("from %s\n" % commit)
2286
2287         if labelDetails.has_key('Owner'):
2288             owner = labelDetails["Owner"]
2289         else:
2290             owner = None
2291
2292         # Try to use the owner of the p4 label, or failing that,
2293         # the current p4 user id.
2294         if owner:
2295             email = self.make_email(owner)
2296         else:
2297             email = self.make_email(self.p4UserId())
2298         tagger = "%s %s %s" % (email, epoch, self.tz)
2299
2300         gitStream.write("tagger %s\n" % tagger)
2301
2302         print "labelDetails=",labelDetails
2303         if labelDetails.has_key('Description'):
2304             description = labelDetails['Description']
2305         else:
2306             description = 'Label from git p4'
2307
2308         gitStream.write("data %d\n" % len(description))
2309         gitStream.write(description)
2310         gitStream.write("\n")
2311
2312     def commit(self, details, files, branch, parent = ""):
2313         epoch = details["time"]
2314         author = details["user"]
2315
2316         if self.verbose:
2317             print "commit into %s" % branch
2318
2319         # start with reading files; if that fails, we should not
2320         # create a commit.
2321         new_files = []
2322         for f in files:
2323             if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2324                 new_files.append (f)
2325             else:
2326                 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2327
2328         self.gitStream.write("commit %s\n" % branch)
2329 #        gitStream.write("mark :%s\n" % details["change"])
2330         self.committedChanges.add(int(details["change"]))
2331         committer = ""
2332         if author not in self.users:
2333             self.getUserMapFromPerforceServer()
2334         committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2335
2336         self.gitStream.write("committer %s\n" % committer)
2337
2338         self.gitStream.write("data <<EOT\n")
2339         self.gitStream.write(details["desc"])
2340         self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2341                              (','.join(self.branchPrefixes), details["change"]))
2342         if len(details['options']) > 0:
2343             self.gitStream.write(": options = %s" % details['options'])
2344         self.gitStream.write("]\nEOT\n\n")
2345
2346         if len(parent) > 0:
2347             if self.verbose:
2348                 print "parent %s" % parent
2349             self.gitStream.write("from %s\n" % parent)
2350
2351         self.streamP4Files(new_files)
2352         self.gitStream.write("\n")
2353
2354         change = int(details["change"])
2355
2356         if self.labels.has_key(change):
2357             label = self.labels[change]
2358             labelDetails = label[0]
2359             labelRevisions = label[1]
2360             if self.verbose:
2361                 print "Change %s is labelled %s" % (change, labelDetails)
2362
2363             files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2364                                                 for p in self.branchPrefixes])
2365
2366             if len(files) == len(labelRevisions):
2367
2368                 cleanedFiles = {}
2369                 for info in files:
2370                     if info["action"] in self.delete_actions:
2371                         continue
2372                     cleanedFiles[info["depotFile"]] = info["rev"]
2373
2374                 if cleanedFiles == labelRevisions:
2375                     self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2376
2377                 else:
2378                     if not self.silent:
2379                         print ("Tag %s does not match with change %s: files do not match."
2380                                % (labelDetails["label"], change))
2381
2382             else:
2383                 if not self.silent:
2384                     print ("Tag %s does not match with change %s: file count is different."
2385                            % (labelDetails["label"], change))
2386
2387     # Build a dictionary of changelists and labels, for "detect-labels" option.
2388     def getLabels(self):
2389         self.labels = {}
2390
2391         l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2392         if len(l) > 0 and not self.silent:
2393             print "Finding files belonging to labels in %s" % `self.depotPaths`
2394
2395         for output in l:
2396             label = output["label"]
2397             revisions = {}
2398             newestChange = 0
2399             if self.verbose:
2400                 print "Querying files for label %s" % label
2401             for file in p4CmdList(["files"] +
2402                                       ["%s...@%s" % (p, label)
2403                                           for p in self.depotPaths]):
2404                 revisions[file["depotFile"]] = file["rev"]
2405                 change = int(file["change"])
2406                 if change > newestChange:
2407                     newestChange = change
2408
2409             self.labels[newestChange] = [output, revisions]
2410
2411         if self.verbose:
2412             print "Label changes: %s" % self.labels.keys()
2413
2414     # Import p4 labels as git tags. A direct mapping does not
2415     # exist, so assume that if all the files are at the same revision
2416     # then we can use that, or it's something more complicated we should
2417     # just ignore.
2418     def importP4Labels(self, stream, p4Labels):
2419         if verbose:
2420             print "import p4 labels: " + ' '.join(p4Labels)
2421
2422         ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2423         validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2424         if len(validLabelRegexp) == 0:
2425             validLabelRegexp = defaultLabelRegexp
2426         m = re.compile(validLabelRegexp)
2427
2428         for name in p4Labels:
2429             commitFound = False
2430
2431             if not m.match(name):
2432                 if verbose:
2433                     print "label %s does not match regexp %s" % (name,validLabelRegexp)
2434                 continue
2435
2436             if name in ignoredP4Labels:
2437                 continue
2438
2439             labelDetails = p4CmdList(['label', "-o", name])[0]
2440
2441             # get the most recent changelist for each file in this label
2442             change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2443                                 for p in self.depotPaths])
2444
2445             if change.has_key('change'):
2446                 # find the corresponding git commit; take the oldest commit
2447                 changelist = int(change['change'])
2448                 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2449                      "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2450                 if len(gitCommit) == 0:
2451                     print "could not find git commit for changelist %d" % changelist
2452                 else:
2453                     gitCommit = gitCommit.strip()
2454                     commitFound = True
2455                     # Convert from p4 time format
2456                     try:
2457                         tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2458                     except ValueError:
2459                         print "Could not convert label time %s" % labelDetails['Update']
2460                         tmwhen = 1
2461
2462                     when = int(time.mktime(tmwhen))
2463                     self.streamTag(stream, name, labelDetails, gitCommit, when)
2464                     if verbose:
2465                         print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2466             else:
2467                 if verbose:
2468                     print "Label %s has no changelists - possibly deleted?" % name
2469
2470             if not commitFound:
2471                 # We can't import this label; don't try again as it will get very
2472                 # expensive repeatedly fetching all the files for labels that will
2473                 # never be imported. If the label is moved in the future, the
2474                 # ignore will need to be removed manually.
2475                 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2476
2477     def guessProjectName(self):
2478         for p in self.depotPaths:
2479             if p.endswith("/"):
2480                 p = p[:-1]
2481             p = p[p.strip().rfind("/") + 1:]
2482             if not p.endswith("/"):
2483                p += "/"
2484             return p
2485
2486     def getBranchMapping(self):
2487         lostAndFoundBranches = set()
2488
2489         user = gitConfig("git-p4.branchUser")
2490         if len(user) > 0:
2491             command = "branches -u %s" % user
2492         else:
2493             command = "branches"
2494
2495         for info in p4CmdList(command):
2496             details = p4Cmd(["branch", "-o", info["branch"]])
2497             viewIdx = 0
2498             while details.has_key("View%s" % viewIdx):
2499                 paths = details["View%s" % viewIdx].split(" ")
2500                 viewIdx = viewIdx + 1
2501                 # require standard //depot/foo/... //depot/bar/... mapping
2502                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2503                     continue
2504                 source = paths[0]
2505                 destination = paths[1]
2506                 ## HACK
2507                 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2508                     source = source[len(self.depotPaths[0]):-4]
2509                     destination = destination[len(self.depotPaths[0]):-4]
2510
2511                     if destination in self.knownBranches:
2512                         if not self.silent:
2513                             print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2514                             print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2515                         continue
2516
2517                     self.knownBranches[destination] = source
2518
2519                     lostAndFoundBranches.discard(destination)
2520
2521                     if source not in self.knownBranches:
2522                         lostAndFoundBranches.add(source)
2523
2524         # Perforce does not strictly require branches to be defined, so we also
2525         # check git config for a branch list.
2526         #
2527         # Example of branch definition in git config file:
2528         # [git-p4]
2529         #   branchList=main:branchA
2530         #   branchList=main:branchB
2531         #   branchList=branchA:branchC
2532         configBranches = gitConfigList("git-p4.branchList")
2533         for branch in configBranches:
2534             if branch:
2535                 (source, destination) = branch.split(":")
2536                 self.knownBranches[destination] = source
2537
2538                 lostAndFoundBranches.discard(destination)
2539
2540                 if source not in self.knownBranches:
2541                     lostAndFoundBranches.add(source)
2542
2543
2544         for branch in lostAndFoundBranches:
2545             self.knownBranches[branch] = branch
2546
2547     def getBranchMappingFromGitBranches(self):
2548         branches = p4BranchesInGit(self.importIntoRemotes)
2549         for branch in branches.keys():
2550             if branch == "master":
2551                 branch = "main"
2552             else:
2553                 branch = branch[len(self.projectName):]
2554             self.knownBranches[branch] = branch
2555
2556     def updateOptionDict(self, d):
2557         option_keys = {}
2558         if self.keepRepoPath:
2559             option_keys['keepRepoPath'] = 1
2560
2561         d["options"] = ' '.join(sorted(option_keys.keys()))
2562
2563     def readOptions(self, d):
2564         self.keepRepoPath = (d.has_key('options')
2565                              and ('keepRepoPath' in d['options']))
2566
2567     def gitRefForBranch(self, branch):
2568         if branch == "main":
2569             return self.refPrefix + "master"
2570
2571         if len(branch) <= 0:
2572             return branch
2573
2574         return self.refPrefix + self.projectName + branch
2575
2576     def gitCommitByP4Change(self, ref, change):
2577         if self.verbose:
2578             print "looking in ref " + ref + " for change %s using bisect..." % change
2579
2580         earliestCommit = ""
2581         latestCommit = parseRevision(ref)
2582
2583         while True:
2584             if self.verbose:
2585                 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2586             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2587             if len(next) == 0:
2588                 if self.verbose:
2589                     print "argh"
2590                 return ""
2591             log = extractLogMessageFromGitCommit(next)
2592             settings = extractSettingsGitLog(log)
2593             currentChange = int(settings['change'])
2594             if self.verbose:
2595                 print "current change %s" % currentChange
2596
2597             if currentChange == change:
2598                 if self.verbose:
2599                     print "found %s" % next
2600                 return next
2601
2602             if currentChange < change:
2603                 earliestCommit = "^%s" % next
2604             else:
2605                 latestCommit = "%s" % next
2606
2607         return ""
2608
2609     def importNewBranch(self, branch, maxChange):
2610         # make fast-import flush all changes to disk and update the refs using the checkpoint
2611         # command so that we can try to find the branch parent in the git history
2612         self.gitStream.write("checkpoint\n\n");
2613         self.gitStream.flush();
2614         branchPrefix = self.depotPaths[0] + branch + "/"
2615         range = "@1,%s" % maxChange
2616         #print "prefix" + branchPrefix
2617         changes = p4ChangesForPaths([branchPrefix], range)
2618         if len(changes) <= 0:
2619             return False
2620         firstChange = changes[0]
2621         #print "first change in branch: %s" % firstChange
2622         sourceBranch = self.knownBranches[branch]
2623         sourceDepotPath = self.depotPaths[0] + sourceBranch
2624         sourceRef = self.gitRefForBranch(sourceBranch)
2625         #print "source " + sourceBranch
2626
2627         branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2628         #print "branch parent: %s" % branchParentChange
2629         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2630         if len(gitParent) > 0:
2631             self.initialParents[self.gitRefForBranch(branch)] = gitParent
2632             #print "parent git commit: %s" % gitParent
2633
2634         self.importChanges(changes)
2635         return True
2636
2637     def searchParent(self, parent, branch, target):
2638         parentFound = False
2639         for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]):
2640             blob = blob.strip()
2641             if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2642                 parentFound = True
2643                 if self.verbose:
2644                     print "Found parent of %s in commit %s" % (branch, blob)
2645                 break
2646         if parentFound:
2647             return blob
2648         else:
2649             return None
2650
2651     def importChanges(self, changes):
2652         cnt = 1
2653         for change in changes:
2654             description = p4_describe(change)
2655             self.updateOptionDict(description)
2656
2657             if not self.silent:
2658                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2659                 sys.stdout.flush()
2660             cnt = cnt + 1
2661
2662             try:
2663                 if self.detectBranches:
2664                     branches = self.splitFilesIntoBranches(description)
2665                     for branch in branches.keys():
2666                         ## HACK  --hwn
2667                         branchPrefix = self.depotPaths[0] + branch + "/"
2668                         self.branchPrefixes = [ branchPrefix ]
2669
2670                         parent = ""
2671
2672                         filesForCommit = branches[branch]
2673
2674                         if self.verbose:
2675                             print "branch is %s" % branch
2676
2677                         self.updatedBranches.add(branch)
2678
2679                         if branch not in self.createdBranches:
2680                             self.createdBranches.add(branch)
2681                             parent = self.knownBranches[branch]
2682                             if parent == branch:
2683                                 parent = ""
2684                             else:
2685                                 fullBranch = self.projectName + branch
2686                                 if fullBranch not in self.p4BranchesInGit:
2687                                     if not self.silent:
2688                                         print("\n    Importing new branch %s" % fullBranch);
2689                                     if self.importNewBranch(branch, change - 1):
2690                                         parent = ""
2691                                         self.p4BranchesInGit.append(fullBranch)
2692                                     if not self.silent:
2693                                         print("\n    Resuming with change %s" % change);
2694
2695                                 if self.verbose:
2696                                     print "parent determined through known branches: %s" % parent
2697
2698                         branch = self.gitRefForBranch(branch)
2699                         parent = self.gitRefForBranch(parent)
2700
2701                         if self.verbose:
2702                             print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2703
2704                         if len(parent) == 0 and branch in self.initialParents:
2705                             parent = self.initialParents[branch]
2706                             del self.initialParents[branch]
2707
2708                         blob = None
2709                         if len(parent) > 0:
2710                             tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change))
2711                             if self.verbose:
2712                                 print "Creating temporary branch: " + tempBranch
2713                             self.commit(description, filesForCommit, tempBranch)
2714                             self.tempBranches.append(tempBranch)
2715                             self.checkpoint()
2716                             blob = self.searchParent(parent, branch, tempBranch)
2717                         if blob:
2718                             self.commit(description, filesForCommit, branch, blob)
2719                         else:
2720                             if self.verbose:
2721                                 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2722                             self.commit(description, filesForCommit, branch, parent)
2723                 else:
2724                     files = self.extractFilesFromCommit(description)
2725                     self.commit(description, files, self.branch,
2726                                 self.initialParent)
2727                     # only needed once, to connect to the previous commit
2728                     self.initialParent = ""
2729             except IOError:
2730                 print self.gitError.read()
2731                 sys.exit(1)
2732
2733     def importHeadRevision(self, revision):
2734         print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2735
2736         details = {}
2737         details["user"] = "git perforce import user"
2738         details["desc"] = ("Initial import of %s from the state at revision %s\n"
2739                            % (' '.join(self.depotPaths), revision))
2740         details["change"] = revision
2741         newestRevision = 0
2742
2743         fileCnt = 0
2744         fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2745
2746         for info in p4CmdList(["files"] + fileArgs):
2747
2748             if 'code' in info and info['code'] == 'error':
2749                 sys.stderr.write("p4 returned an error: %s\n"
2750                                  % info['data'])
2751                 if info['data'].find("must refer to client") >= 0:
2752                     sys.stderr.write("This particular p4 error is misleading.\n")
2753                     sys.stderr.write("Perhaps the depot path was misspelled.\n");
2754                     sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2755                 sys.exit(1)
2756             if 'p4ExitCode' in info:
2757                 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2758                 sys.exit(1)
2759
2760
2761             change = int(info["change"])
2762             if change > newestRevision:
2763                 newestRevision = change
2764
2765             if info["action"] in self.delete_actions:
2766                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2767                 #fileCnt = fileCnt + 1
2768                 continue
2769
2770             for prop in ["depotFile", "rev", "action", "type" ]:
2771                 details["%s%s" % (prop, fileCnt)] = info[prop]
2772
2773             fileCnt = fileCnt + 1
2774
2775         details["change"] = newestRevision
2776
2777         # Use time from top-most change so that all git p4 clones of
2778         # the same p4 repo have the same commit SHA1s.
2779         res = p4_describe(newestRevision)
2780         details["time"] = res["time"]
2781
2782         self.updateOptionDict(details)
2783         try:
2784             self.commit(details, self.extractFilesFromCommit(details), self.branch)
2785         except IOError:
2786             print "IO error with git fast-import. Is your git version recent enough?"
2787             print self.gitError.read()
2788
2789
2790     def run(self, args):
2791         self.depotPaths = []
2792         self.changeRange = ""
2793         self.previousDepotPaths = []
2794         self.hasOrigin = False
2795
2796         # map from branch depot path to parent branch
2797         self.knownBranches = {}
2798         self.initialParents = {}
2799
2800         if self.importIntoRemotes:
2801             self.refPrefix = "refs/remotes/p4/"
2802         else:
2803             self.refPrefix = "refs/heads/p4/"
2804
2805         if self.syncWithOrigin:
2806             self.hasOrigin = originP4BranchesExist()
2807             if self.hasOrigin:
2808                 if not self.silent:
2809                     print 'Syncing with origin first, using "git fetch origin"'
2810                 system("git fetch origin")
2811
2812         branch_arg_given = bool(self.branch)
2813         if len(self.branch) == 0:
2814             self.branch = self.refPrefix + "master"
2815             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2816                 system("git update-ref %s refs/heads/p4" % self.branch)
2817                 system("git branch -D p4")
2818
2819         # accept either the command-line option, or the configuration variable
2820         if self.useClientSpec:
2821             # will use this after clone to set the variable
2822             self.useClientSpec_from_options = True
2823         else:
2824             if gitConfig("git-p4.useclientspec", "--bool") == "true":
2825                 self.useClientSpec = True
2826         if self.useClientSpec:
2827             self.clientSpecDirs = getClientSpec()
2828
2829         # TODO: should always look at previous commits,
2830         # merge with previous imports, if possible.
2831         if args == []:
2832             if self.hasOrigin:
2833                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2834
2835             # branches holds mapping from branch name to sha1
2836             branches = p4BranchesInGit(self.importIntoRemotes)
2837
2838             # restrict to just this one, disabling detect-branches
2839             if branch_arg_given:
2840                 short = self.branch.split("/")[-1]
2841                 if short in branches:
2842                     self.p4BranchesInGit = [ short ]
2843             else:
2844                 self.p4BranchesInGit = branches.keys()
2845
2846             if len(self.p4BranchesInGit) > 1:
2847                 if not self.silent:
2848                     print "Importing from/into multiple branches"
2849                 self.detectBranches = True
2850                 for branch in branches.keys():
2851                     self.initialParents[self.refPrefix + branch] = \
2852                         branches[branch]
2853
2854             if self.verbose:
2855                 print "branches: %s" % self.p4BranchesInGit
2856
2857             p4Change = 0
2858             for branch in self.p4BranchesInGit:
2859                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2860
2861                 settings = extractSettingsGitLog(logMsg)
2862
2863                 self.readOptions(settings)
2864                 if (settings.has_key('depot-paths')
2865                     and settings.has_key ('change')):
2866                     change = int(settings['change']) + 1
2867                     p4Change = max(p4Change, change)
2868
2869                     depotPaths = sorted(settings['depot-paths'])
2870                     if self.previousDepotPaths == []:
2871                         self.previousDepotPaths = depotPaths
2872                     else:
2873                         paths = []
2874                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2875                             prev_list = prev.split("/")
2876                             cur_list = cur.split("/")
2877                             for i in range(0, min(len(cur_list), len(prev_list))):
2878                                 if cur_list[i] <> prev_list[i]:
2879                                     i = i - 1
2880                                     break
2881
2882                             paths.append ("/".join(cur_list[:i + 1]))
2883
2884                         self.previousDepotPaths = paths
2885
2886             if p4Change > 0:
2887                 self.depotPaths = sorted(self.previousDepotPaths)
2888                 self.changeRange = "@%s,#head" % p4Change
2889                 if not self.silent and not self.detectBranches:
2890                     print "Performing incremental import into %s git branch" % self.branch
2891
2892         # accept multiple ref name abbreviations:
2893         #    refs/foo/bar/branch -> use it exactly
2894         #    p4/branch -> prepend refs/remotes/ or refs/heads/
2895         #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2896         if not self.branch.startswith("refs/"):
2897             if self.importIntoRemotes:
2898                 prepend = "refs/remotes/"
2899             else:
2900                 prepend = "refs/heads/"
2901             if not self.branch.startswith("p4/"):
2902                 prepend += "p4/"
2903             self.branch = prepend + self.branch
2904
2905         if len(args) == 0 and self.depotPaths:
2906             if not self.silent:
2907                 print "Depot paths: %s" % ' '.join(self.depotPaths)
2908         else:
2909             if self.depotPaths and self.depotPaths != args:
2910                 print ("previous import used depot path %s and now %s was specified. "
2911                        "This doesn't work!" % (' '.join (self.depotPaths),
2912                                                ' '.join (args)))
2913                 sys.exit(1)
2914
2915             self.depotPaths = sorted(args)
2916
2917         revision = ""
2918         self.users = {}
2919
2920         # Make sure no revision specifiers are used when --changesfile
2921         # is specified.
2922         bad_changesfile = False
2923         if len(self.changesFile) > 0:
2924             for p in self.depotPaths:
2925                 if p.find("@") >= 0 or p.find("#") >= 0:
2926                     bad_changesfile = True
2927                     break
2928         if bad_changesfile:
2929             die("Option --changesfile is incompatible with revision specifiers")
2930
2931         newPaths = []
2932         for p in self.depotPaths:
2933             if p.find("@") != -1:
2934                 atIdx = p.index("@")
2935                 self.changeRange = p[atIdx:]
2936                 if self.changeRange == "@all":
2937                     self.changeRange = ""
2938                 elif ',' not in self.changeRange:
2939                     revision = self.changeRange
2940                     self.changeRange = ""
2941                 p = p[:atIdx]
2942             elif p.find("#") != -1:
2943                 hashIdx = p.index("#")
2944                 revision = p[hashIdx:]
2945                 p = p[:hashIdx]
2946             elif self.previousDepotPaths == []:
2947                 # pay attention to changesfile, if given, else import
2948                 # the entire p4 tree at the head revision
2949                 if len(self.changesFile) == 0:
2950                     revision = "#head"
2951
2952             p = re.sub ("\.\.\.$", "", p)
2953             if not p.endswith("/"):
2954                 p += "/"
2955
2956             newPaths.append(p)
2957
2958         self.depotPaths = newPaths
2959
2960         # --detect-branches may change this for each branch
2961         self.branchPrefixes = self.depotPaths
2962
2963         self.loadUserMapFromCache()
2964         self.labels = {}
2965         if self.detectLabels:
2966             self.getLabels();
2967
2968         if self.detectBranches:
2969             ## FIXME - what's a P4 projectName ?
2970             self.projectName = self.guessProjectName()
2971
2972             if self.hasOrigin:
2973                 self.getBranchMappingFromGitBranches()
2974             else:
2975                 self.getBranchMapping()
2976             if self.verbose:
2977                 print "p4-git branches: %s" % self.p4BranchesInGit
2978                 print "initial parents: %s" % self.initialParents
2979             for b in self.p4BranchesInGit:
2980                 if b != "master":
2981
2982                     ## FIXME
2983                     b = b[len(self.projectName):]
2984                 self.createdBranches.add(b)
2985
2986         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2987
2988         self.importProcess = subprocess.Popen(["git", "fast-import"],
2989                                               stdin=subprocess.PIPE,
2990                                               stdout=subprocess.PIPE,
2991                                               stderr=subprocess.PIPE);
2992         self.gitOutput = self.importProcess.stdout
2993         self.gitStream = self.importProcess.stdin
2994         self.gitError = self.importProcess.stderr
2995
2996         if revision:
2997             self.importHeadRevision(revision)
2998         else:
2999             changes = []
3000
3001             if len(self.changesFile) > 0:
3002                 output = open(self.changesFile).readlines()
3003                 changeSet = set()
3004                 for line in output:
3005                     changeSet.add(int(line))
3006
3007                 for change in changeSet:
3008                     changes.append(change)
3009
3010                 changes.sort()
3011             else:
3012                 # catch "git p4 sync" with no new branches, in a repo that
3013                 # does not have any existing p4 branches
3014                 if len(args) == 0:
3015                     if not self.p4BranchesInGit:
3016                         die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
3017
3018                     # The default branch is master, unless --branch is used to
3019                     # specify something else.  Make sure it exists, or complain
3020                     # nicely about how to use --branch.
3021                     if not self.detectBranches:
3022                         if not branch_exists(self.branch):
3023                             if branch_arg_given:
3024                                 die("Error: branch %s does not exist." % self.branch)
3025                             else:
3026                                 die("Error: no branch %s; perhaps specify one with --branch." %
3027                                     self.branch)
3028
3029                 if self.verbose:
3030                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3031                                                               self.changeRange)
3032                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
3033
3034                 if len(self.maxChanges) > 0:
3035                     changes = changes[:min(int(self.maxChanges), len(changes))]
3036
3037             if len(changes) == 0:
3038                 if not self.silent:
3039                     print "No changes to import!"
3040             else:
3041                 if not self.silent and not self.detectBranches:
3042                     print "Import destination: %s" % self.branch
3043
3044                 self.updatedBranches = set()
3045
3046                 if not self.detectBranches:
3047                     if args:
3048                         # start a new branch
3049                         self.initialParent = ""
3050                     else:
3051                         # build on a previous revision
3052                         self.initialParent = parseRevision(self.branch)
3053
3054                 self.importChanges(changes)
3055
3056                 if not self.silent:
3057                     print ""
3058                     if len(self.updatedBranches) > 0:
3059                         sys.stdout.write("Updated branches: ")
3060                         for b in self.updatedBranches:
3061                             sys.stdout.write("%s " % b)
3062                         sys.stdout.write("\n")
3063
3064         if gitConfig("git-p4.importLabels", "--bool") == "true":
3065             self.importLabels = True
3066
3067         if self.importLabels:
3068             p4Labels = getP4Labels(self.depotPaths)
3069             gitTags = getGitTags()
3070
3071             missingP4Labels = p4Labels - gitTags
3072             self.importP4Labels(self.gitStream, missingP4Labels)
3073
3074         self.gitStream.close()
3075         if self.importProcess.wait() != 0:
3076             die("fast-import failed: %s" % self.gitError.read())
3077         self.gitOutput.close()
3078         self.gitError.close()
3079
3080         # Cleanup temporary branches created during import
3081         if self.tempBranches != []:
3082             for branch in self.tempBranches:
3083                 read_pipe("git update-ref -d %s" % branch)
3084             os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3085
3086         # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3087         # a convenient shortcut refname "p4".
3088         if self.importIntoRemotes:
3089             head_ref = self.refPrefix + "HEAD"
3090             if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3091                 system(["git", "symbolic-ref", head_ref, self.branch])
3092
3093         return True
3094
3095 class P4Rebase(Command):
3096     def __init__(self):
3097         Command.__init__(self)
3098         self.options = [
3099                 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3100         ]
3101         self.importLabels = False
3102         self.description = ("Fetches the latest revision from perforce and "
3103                             + "rebases the current work (branch) against it")
3104
3105     def run(self, args):
3106         sync = P4Sync()
3107         sync.importLabels = self.importLabels
3108         sync.run([])
3109
3110         return self.rebase()
3111
3112     def rebase(self):
3113         if os.system("git update-index --refresh") != 0:
3114             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.");
3115         if len(read_pipe("git diff-index HEAD --")) > 0:
3116             die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
3117
3118         [upstream, settings] = findUpstreamBranchPoint()
3119         if len(upstream) == 0:
3120             die("Cannot find upstream branchpoint for rebase")
3121
3122         # the branchpoint may be p4/foo~3, so strip off the parent
3123         upstream = re.sub("~[0-9]+$", "", upstream)
3124
3125         print "Rebasing the current branch onto %s" % upstream
3126         oldHead = read_pipe("git rev-parse HEAD").strip()
3127         system("git rebase %s" % upstream)
3128         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
3129         return True
3130
3131 class P4Clone(P4Sync):
3132     def __init__(self):
3133         P4Sync.__init__(self)
3134         self.description = "Creates a new git repository and imports from Perforce into it"
3135         self.usage = "usage: %prog [options] //depot/path[@revRange]"
3136         self.options += [
3137             optparse.make_option("--destination", dest="cloneDestination",
3138                                  action='store', default=None,
3139                                  help="where to leave result of the clone"),
3140             optparse.make_option("-/", dest="cloneExclude",
3141                                  action="append", type="string",
3142                                  help="exclude depot path"),
3143             optparse.make_option("--bare", dest="cloneBare",
3144                                  action="store_true", default=False),
3145         ]
3146         self.cloneDestination = None
3147         self.needsGit = False
3148         self.cloneBare = False
3149
3150     # This is required for the "append" cloneExclude action
3151     def ensure_value(self, attr, value):
3152         if not hasattr(self, attr) or getattr(self, attr) is None:
3153             setattr(self, attr, value)
3154         return getattr(self, attr)
3155
3156     def defaultDestination(self, args):
3157         ## TODO: use common prefix of args?
3158         depotPath = args[0]
3159         depotDir = re.sub("(@[^@]*)$", "", depotPath)
3160         depotDir = re.sub("(#[^#]*)$", "", depotDir)
3161         depotDir = re.sub(r"\.\.\.$", "", depotDir)
3162         depotDir = re.sub(r"/$", "", depotDir)
3163         return os.path.split(depotDir)[1]
3164
3165     def run(self, args):
3166         if len(args) < 1:
3167             return False
3168
3169         if self.keepRepoPath and not self.cloneDestination:
3170             sys.stderr.write("Must specify destination for --keep-path\n")
3171             sys.exit(1)
3172
3173         depotPaths = args
3174
3175         if not self.cloneDestination and len(depotPaths) > 1:
3176             self.cloneDestination = depotPaths[-1]
3177             depotPaths = depotPaths[:-1]
3178
3179         self.cloneExclude = ["/"+p for p in self.cloneExclude]
3180         for p in depotPaths:
3181             if not p.startswith("//"):
3182                 return False
3183
3184         if not self.cloneDestination:
3185             self.cloneDestination = self.defaultDestination(args)
3186
3187         print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3188
3189         if not os.path.exists(self.cloneDestination):
3190             os.makedirs(self.cloneDestination)
3191         chdir(self.cloneDestination)
3192
3193         init_cmd = [ "git", "init" ]
3194         if self.cloneBare:
3195             init_cmd.append("--bare")
3196         retcode = subprocess.call(init_cmd)
3197         if retcode:
3198             raise CalledProcessError(retcode, init_cmd)
3199
3200         if not P4Sync.run(self, depotPaths):
3201             return False
3202
3203         # create a master branch and check out a work tree
3204         if gitBranchExists(self.branch):
3205             system([ "git", "branch", "master", self.branch ])
3206             if not self.cloneBare:
3207                 system([ "git", "checkout", "-f" ])
3208         else:
3209             print 'Not checking out any branch, use ' \
3210                   '"git checkout -q -b master <branch>"'
3211
3212         # auto-set this variable if invoked with --use-client-spec
3213         if self.useClientSpec_from_options:
3214             system("git config --bool git-p4.useclientspec true")
3215
3216         return True
3217
3218 class P4Branches(Command):
3219     def __init__(self):
3220         Command.__init__(self)
3221         self.options = [ ]
3222         self.description = ("Shows the git branches that hold imports and their "
3223                             + "corresponding perforce depot paths")
3224         self.verbose = False
3225
3226     def run(self, args):
3227         if originP4BranchesExist():
3228             createOrUpdateBranchesFromOrigin()
3229
3230         cmdline = "git rev-parse --symbolic "
3231         cmdline += " --remotes"
3232
3233         for line in read_pipe_lines(cmdline):
3234             line = line.strip()
3235
3236             if not line.startswith('p4/') or line == "p4/HEAD":
3237                 continue
3238             branch = line
3239
3240             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3241             settings = extractSettingsGitLog(log)
3242
3243             print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3244         return True
3245
3246 class HelpFormatter(optparse.IndentedHelpFormatter):
3247     def __init__(self):
3248         optparse.IndentedHelpFormatter.__init__(self)
3249
3250     def format_description(self, description):
3251         if description:
3252             return description + "\n"
3253         else:
3254             return ""
3255
3256 def printUsage(commands):
3257     print "usage: %s <command> [options]" % sys.argv[0]
3258     print ""
3259     print "valid commands: %s" % ", ".join(commands)
3260     print ""
3261     print "Try %s <command> --help for command specific help." % sys.argv[0]
3262     print ""
3263
3264 commands = {
3265     "debug" : P4Debug,
3266     "submit" : P4Submit,
3267     "commit" : P4Submit,
3268     "sync" : P4Sync,
3269     "rebase" : P4Rebase,
3270     "clone" : P4Clone,
3271     "rollback" : P4RollBack,
3272     "branches" : P4Branches
3273 }
3274
3275
3276 def main():
3277     if len(sys.argv[1:]) == 0:
3278         printUsage(commands.keys())
3279         sys.exit(2)
3280
3281     cmdName = sys.argv[1]
3282     try:
3283         klass = commands[cmdName]
3284         cmd = klass()
3285     except KeyError:
3286         print "unknown command %s" % cmdName
3287         print ""
3288         printUsage(commands.keys())
3289         sys.exit(2)
3290
3291     options = cmd.options
3292     cmd.gitdir = os.environ.get("GIT_DIR", None)
3293
3294     args = sys.argv[2:]
3295
3296     options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3297     if cmd.needsGit:
3298         options.append(optparse.make_option("--git-dir", dest="gitdir"))
3299
3300     parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3301                                    options,
3302                                    description = cmd.description,
3303                                    formatter = HelpFormatter())
3304
3305     (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3306     global verbose
3307     verbose = cmd.verbose
3308     if cmd.needsGit:
3309         if cmd.gitdir == None:
3310             cmd.gitdir = os.path.abspath(".git")
3311             if not isValidGitDir(cmd.gitdir):
3312                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3313                 if os.path.exists(cmd.gitdir):
3314                     cdup = read_pipe("git rev-parse --show-cdup").strip()
3315                     if len(cdup) > 0:
3316                         chdir(cdup);
3317
3318         if not isValidGitDir(cmd.gitdir):
3319             if isValidGitDir(cmd.gitdir + "/.git"):
3320                 cmd.gitdir += "/.git"
3321             else:
3322                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3323
3324         os.environ["GIT_DIR"] = cmd.gitdir
3325
3326     if not cmd.run(args):
3327         parser.print_help()
3328         sys.exit(2)
3329
3330
3331 if __name__ == '__main__':
3332     main()