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