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