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