Code

attr: fix leak in free_attr_elem
[git.git] / contrib / fast-import / git-p4
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 #
11 import optparse, sys, os, marshal, subprocess, shelve
12 import tempfile, getopt, os.path, time, platform
13 import re
15 verbose = False
18 def p4_build_cmd(cmd):
19     """Build a suitable p4 command line.
21     This consolidates building and returning a p4 command line into one
22     location. It means that hooking into the environment, or other configuration
23     can be done more easily.
24     """
25     real_cmd = "%s " % "p4"
27     user = gitConfig("git-p4.user")
28     if len(user) > 0:
29         real_cmd += "-u %s " % user
31     password = gitConfig("git-p4.password")
32     if len(password) > 0:
33         real_cmd += "-P %s " % password
35     port = gitConfig("git-p4.port")
36     if len(port) > 0:
37         real_cmd += "-p %s " % port
39     host = gitConfig("git-p4.host")
40     if len(host) > 0:
41         real_cmd += "-h %s " % host
43     client = gitConfig("git-p4.client")
44     if len(client) > 0:
45         real_cmd += "-c %s " % client
47     real_cmd += "%s" % (cmd)
48     if verbose:
49         print real_cmd
50     return real_cmd
52 def chdir(dir):
53     if os.name == 'nt':
54         os.environ['PWD']=dir
55     os.chdir(dir)
57 def die(msg):
58     if verbose:
59         raise Exception(msg)
60     else:
61         sys.stderr.write(msg + "\n")
62         sys.exit(1)
64 def write_pipe(c, str):
65     if verbose:
66         sys.stderr.write('Writing pipe: %s\n' % c)
68     pipe = os.popen(c, 'w')
69     val = pipe.write(str)
70     if pipe.close():
71         die('Command failed: %s' % c)
73     return val
75 def p4_write_pipe(c, str):
76     real_cmd = p4_build_cmd(c)
77     return write_pipe(real_cmd, str)
79 def read_pipe(c, ignore_error=False):
80     if verbose:
81         sys.stderr.write('Reading pipe: %s\n' % c)
83     pipe = os.popen(c, 'rb')
84     val = pipe.read()
85     if pipe.close() and not ignore_error:
86         die('Command failed: %s' % c)
88     return val
90 def p4_read_pipe(c, ignore_error=False):
91     real_cmd = p4_build_cmd(c)
92     return read_pipe(real_cmd, ignore_error)
94 def read_pipe_lines(c):
95     if verbose:
96         sys.stderr.write('Reading pipe: %s\n' % c)
97     ## todo: check return status
98     pipe = os.popen(c, 'rb')
99     val = pipe.readlines()
100     if pipe.close():
101         die('Command failed: %s' % c)
103     return val
105 def p4_read_pipe_lines(c):
106     """Specifically invoke p4 on the command supplied. """
107     real_cmd = p4_build_cmd(c)
108     return read_pipe_lines(real_cmd)
110 def system(cmd):
111     if verbose:
112         sys.stderr.write("executing %s\n" % cmd)
113     if os.system(cmd) != 0:
114         die("command failed: %s" % cmd)
116 def p4_system(cmd):
117     """Specifically invoke p4 as the system command. """
118     real_cmd = p4_build_cmd(cmd)
119     return system(real_cmd)
121 def isP4Exec(kind):
122     """Determine if a Perforce 'kind' should have execute permission
124     'p4 help filetypes' gives a list of the types.  If it starts with 'x',
125     or x follows one of a few letters.  Otherwise, if there is an 'x' after
126     a plus sign, it is also executable"""
127     return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
129 def setP4ExecBit(file, mode):
130     # Reopens an already open file and changes the execute bit to match
131     # the execute bit setting in the passed in mode.
133     p4Type = "+x"
135     if not isModeExec(mode):
136         p4Type = getP4OpenedType(file)
137         p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
138         p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
139         if p4Type[-1] == "+":
140             p4Type = p4Type[0:-1]
142     p4_system("reopen -t %s %s" % (p4Type, file))
144 def getP4OpenedType(file):
145     # Returns the perforce file type for the given file.
147     result = p4_read_pipe("opened %s" % file)
148     match = re.match(".*\((.+)\)\r?$", result)
149     if match:
150         return match.group(1)
151     else:
152         die("Could not determine file type for %s (result: '%s')" % (file, result))
154 def diffTreePattern():
155     # This is a simple generator for the diff tree regex pattern. This could be
156     # a class variable if this and parseDiffTreeEntry were a part of a class.
157     pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
158     while True:
159         yield pattern
161 def parseDiffTreeEntry(entry):
162     """Parses a single diff tree entry into its component elements.
164     See git-diff-tree(1) manpage for details about the format of the diff
165     output. This method returns a dictionary with the following elements:
167     src_mode - The mode of the source file
168     dst_mode - The mode of the destination file
169     src_sha1 - The sha1 for the source file
170     dst_sha1 - The sha1 fr the destination file
171     status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
172     status_score - The score for the status (applicable for 'C' and 'R'
173                    statuses). This is None if there is no score.
174     src - The path for the source file.
175     dst - The path for the destination file. This is only present for
176           copy or renames. If it is not present, this is None.
178     If the pattern is not matched, None is returned."""
180     match = diffTreePattern().next().match(entry)
181     if match:
182         return {
183             'src_mode': match.group(1),
184             'dst_mode': match.group(2),
185             'src_sha1': match.group(3),
186             'dst_sha1': match.group(4),
187             'status': match.group(5),
188             'status_score': match.group(6),
189             'src': match.group(7),
190             'dst': match.group(10)
191         }
192     return None
194 def isModeExec(mode):
195     # Returns True if the given git mode represents an executable file,
196     # otherwise False.
197     return mode[-3:] == "755"
199 def isModeExecChanged(src_mode, dst_mode):
200     return isModeExec(src_mode) != isModeExec(dst_mode)
202 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
203     cmd = p4_build_cmd("-G %s" % (cmd))
204     if verbose:
205         sys.stderr.write("Opening pipe: %s\n" % cmd)
207     # Use a temporary file to avoid deadlocks without
208     # subprocess.communicate(), which would put another copy
209     # of stdout into memory.
210     stdin_file = None
211     if stdin is not None:
212         stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
213         stdin_file.write(stdin)
214         stdin_file.flush()
215         stdin_file.seek(0)
217     p4 = subprocess.Popen(cmd, shell=True,
218                           stdin=stdin_file,
219                           stdout=subprocess.PIPE)
221     result = []
222     try:
223         while True:
224             entry = marshal.load(p4.stdout)
225             if cb is not None:
226                 cb(entry)
227             else:
228                 result.append(entry)
229     except EOFError:
230         pass
231     exitCode = p4.wait()
232     if exitCode != 0:
233         entry = {}
234         entry["p4ExitCode"] = exitCode
235         result.append(entry)
237     return result
239 def p4Cmd(cmd):
240     list = p4CmdList(cmd)
241     result = {}
242     for entry in list:
243         result.update(entry)
244     return result;
246 def p4Where(depotPath):
247     if not depotPath.endswith("/"):
248         depotPath += "/"
249     depotPath = depotPath + "..."
250     outputList = p4CmdList("where %s" % depotPath)
251     output = None
252     for entry in outputList:
253         if "depotFile" in entry:
254             if entry["depotFile"] == depotPath:
255                 output = entry
256                 break
257         elif "data" in entry:
258             data = entry.get("data")
259             space = data.find(" ")
260             if data[:space] == depotPath:
261                 output = entry
262                 break
263     if output == None:
264         return ""
265     if output["code"] == "error":
266         return ""
267     clientPath = ""
268     if "path" in output:
269         clientPath = output.get("path")
270     elif "data" in output:
271         data = output.get("data")
272         lastSpace = data.rfind(" ")
273         clientPath = data[lastSpace + 1:]
275     if clientPath.endswith("..."):
276         clientPath = clientPath[:-3]
277     return clientPath
279 def currentGitBranch():
280     return read_pipe("git name-rev HEAD").split(" ")[1].strip()
282 def isValidGitDir(path):
283     if (os.path.exists(path + "/HEAD")
284         and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
285         return True;
286     return False
288 def parseRevision(ref):
289     return read_pipe("git rev-parse %s" % ref).strip()
291 def extractLogMessageFromGitCommit(commit):
292     logMessage = ""
294     ## fixme: title is first line of commit, not 1st paragraph.
295     foundTitle = False
296     for log in read_pipe_lines("git cat-file commit %s" % commit):
297        if not foundTitle:
298            if len(log) == 1:
299                foundTitle = True
300            continue
302        logMessage += log
303     return logMessage
305 def extractSettingsGitLog(log):
306     values = {}
307     for line in log.split("\n"):
308         line = line.strip()
309         m = re.search (r"^ *\[git-p4: (.*)\]$", line)
310         if not m:
311             continue
313         assignments = m.group(1).split (':')
314         for a in assignments:
315             vals = a.split ('=')
316             key = vals[0].strip()
317             val = ('='.join (vals[1:])).strip()
318             if val.endswith ('\"') and val.startswith('"'):
319                 val = val[1:-1]
321             values[key] = val
323     paths = values.get("depot-paths")
324     if not paths:
325         paths = values.get("depot-path")
326     if paths:
327         values['depot-paths'] = paths.split(',')
328     return values
330 def gitBranchExists(branch):
331     proc = subprocess.Popen(["git", "rev-parse", branch],
332                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);
333     return proc.wait() == 0;
335 _gitConfig = {}
336 def gitConfig(key, args = None): # set args to "--bool", for instance
337     if not _gitConfig.has_key(key):
338         argsFilter = ""
339         if args != None:
340             argsFilter = "%s " % args
341         cmd = "git config %s%s" % (argsFilter, key)
342         _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
343     return _gitConfig[key]
345 def p4BranchesInGit(branchesAreInRemotes = True):
346     branches = {}
348     cmdline = "git rev-parse --symbolic "
349     if branchesAreInRemotes:
350         cmdline += " --remotes"
351     else:
352         cmdline += " --branches"
354     for line in read_pipe_lines(cmdline):
355         line = line.strip()
357         ## only import to p4/
358         if not line.startswith('p4/') or line == "p4/HEAD":
359             continue
360         branch = line
362         # strip off p4
363         branch = re.sub ("^p4/", "", line)
365         branches[branch] = parseRevision(line)
366     return branches
368 def findUpstreamBranchPoint(head = "HEAD"):
369     branches = p4BranchesInGit()
370     # map from depot-path to branch name
371     branchByDepotPath = {}
372     for branch in branches.keys():
373         tip = branches[branch]
374         log = extractLogMessageFromGitCommit(tip)
375         settings = extractSettingsGitLog(log)
376         if settings.has_key("depot-paths"):
377             paths = ",".join(settings["depot-paths"])
378             branchByDepotPath[paths] = "remotes/p4/" + branch
380     settings = None
381     parent = 0
382     while parent < 65535:
383         commit = head + "~%s" % parent
384         log = extractLogMessageFromGitCommit(commit)
385         settings = extractSettingsGitLog(log)
386         if settings.has_key("depot-paths"):
387             paths = ",".join(settings["depot-paths"])
388             if branchByDepotPath.has_key(paths):
389                 return [branchByDepotPath[paths], settings]
391         parent = parent + 1
393     return ["", settings]
395 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
396     if not silent:
397         print ("Creating/updating branch(es) in %s based on origin branch(es)"
398                % localRefPrefix)
400     originPrefix = "origin/p4/"
402     for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
403         line = line.strip()
404         if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
405             continue
407         headName = line[len(originPrefix):]
408         remoteHead = localRefPrefix + headName
409         originHead = line
411         original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
412         if (not original.has_key('depot-paths')
413             or not original.has_key('change')):
414             continue
416         update = False
417         if not gitBranchExists(remoteHead):
418             if verbose:
419                 print "creating %s" % remoteHead
420             update = True
421         else:
422             settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
423             if settings.has_key('change') > 0:
424                 if settings['depot-paths'] == original['depot-paths']:
425                     originP4Change = int(original['change'])
426                     p4Change = int(settings['change'])
427                     if originP4Change > p4Change:
428                         print ("%s (%s) is newer than %s (%s). "
429                                "Updating p4 branch from origin."
430                                % (originHead, originP4Change,
431                                   remoteHead, p4Change))
432                         update = True
433                 else:
434                     print ("Ignoring: %s was imported from %s while "
435                            "%s was imported from %s"
436                            % (originHead, ','.join(original['depot-paths']),
437                               remoteHead, ','.join(settings['depot-paths'])))
439         if update:
440             system("git update-ref %s %s" % (remoteHead, originHead))
442 def originP4BranchesExist():
443         return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
445 def p4ChangesForPaths(depotPaths, changeRange):
446     assert depotPaths
447     output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
448                                                         for p in depotPaths]))
450     changes = {}
451     for line in output:
452         changeNum = int(line.split(" ")[1])
453         changes[changeNum] = True
455     changelist = changes.keys()
456     changelist.sort()
457     return changelist
459 def p4PathStartsWith(path, prefix):
460     # This method tries to remedy a potential mixed-case issue:
461     #
462     # If UserA adds  //depot/DirA/file1
463     # and UserB adds //depot/dira/file2
464     #
465     # we may or may not have a problem. If you have core.ignorecase=true,
466     # we treat DirA and dira as the same directory
467     ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
468     if ignorecase:
469         return path.lower().startswith(prefix.lower())
470     return path.startswith(prefix)
472 class Command:
473     def __init__(self):
474         self.usage = "usage: %prog [options]"
475         self.needsGit = True
477 class P4UserMap:
478     def __init__(self):
479         self.userMapFromPerforceServer = False
481     def getUserCacheFilename(self):
482         home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
483         return home + "/.gitp4-usercache.txt"
485     def getUserMapFromPerforceServer(self):
486         if self.userMapFromPerforceServer:
487             return
488         self.users = {}
489         self.emails = {}
491         for output in p4CmdList("users"):
492             if not output.has_key("User"):
493                 continue
494             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
495             self.emails[output["Email"]] = output["User"]
498         s = ''
499         for (key, val) in self.users.items():
500             s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
502         open(self.getUserCacheFilename(), "wb").write(s)
503         self.userMapFromPerforceServer = True
505     def loadUserMapFromCache(self):
506         self.users = {}
507         self.userMapFromPerforceServer = False
508         try:
509             cache = open(self.getUserCacheFilename(), "rb")
510             lines = cache.readlines()
511             cache.close()
512             for line in lines:
513                 entry = line.strip().split("\t")
514                 self.users[entry[0]] = entry[1]
515         except IOError:
516             self.getUserMapFromPerforceServer()
518 class P4Debug(Command):
519     def __init__(self):
520         Command.__init__(self)
521         self.options = [
522             optparse.make_option("--verbose", dest="verbose", action="store_true",
523                                  default=False),
524             ]
525         self.description = "A tool to debug the output of p4 -G."
526         self.needsGit = False
527         self.verbose = False
529     def run(self, args):
530         j = 0
531         for output in p4CmdList(" ".join(args)):
532             print 'Element: %d' % j
533             j += 1
534             print output
535         return True
537 class P4RollBack(Command):
538     def __init__(self):
539         Command.__init__(self)
540         self.options = [
541             optparse.make_option("--verbose", dest="verbose", action="store_true"),
542             optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
543         ]
544         self.description = "A tool to debug the multi-branch import. Don't use :)"
545         self.verbose = False
546         self.rollbackLocalBranches = False
548     def run(self, args):
549         if len(args) != 1:
550             return False
551         maxChange = int(args[0])
553         if "p4ExitCode" in p4Cmd("changes -m 1"):
554             die("Problems executing p4");
556         if self.rollbackLocalBranches:
557             refPrefix = "refs/heads/"
558             lines = read_pipe_lines("git rev-parse --symbolic --branches")
559         else:
560             refPrefix = "refs/remotes/"
561             lines = read_pipe_lines("git rev-parse --symbolic --remotes")
563         for line in lines:
564             if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
565                 line = line.strip()
566                 ref = refPrefix + line
567                 log = extractLogMessageFromGitCommit(ref)
568                 settings = extractSettingsGitLog(log)
570                 depotPaths = settings['depot-paths']
571                 change = settings['change']
573                 changed = False
575                 if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
576                                                            for p in depotPaths]))) == 0:
577                     print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
578                     system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
579                     continue
581                 while change and int(change) > maxChange:
582                     changed = True
583                     if self.verbose:
584                         print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
585                     system("git update-ref %s \"%s^\"" % (ref, ref))
586                     log = extractLogMessageFromGitCommit(ref)
587                     settings =  extractSettingsGitLog(log)
590                     depotPaths = settings['depot-paths']
591                     change = settings['change']
593                 if changed:
594                     print "%s rewound to %s" % (ref, change)
596         return True
598 class P4Submit(Command, P4UserMap):
599     def __init__(self):
600         Command.__init__(self)
601         P4UserMap.__init__(self)
602         self.options = [
603                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
604                 optparse.make_option("--origin", dest="origin"),
605                 optparse.make_option("-M", dest="detectRenames", action="store_true"),
606                 # preserve the user, requires relevant p4 permissions
607                 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
608         ]
609         self.description = "Submit changes from git to the perforce depot."
610         self.usage += " [name of git branch to submit into perforce depot]"
611         self.interactive = True
612         self.origin = ""
613         self.detectRenames = False
614         self.verbose = False
615         self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
616         self.isWindows = (platform.system() == "Windows")
617         self.myP4UserId = None
619     def check(self):
620         if len(p4CmdList("opened ...")) > 0:
621             die("You have files opened with perforce! Close them before starting the sync.")
623     # replaces everything between 'Description:' and the next P4 submit template field with the
624     # commit message
625     def prepareLogMessage(self, template, message):
626         result = ""
628         inDescriptionSection = False
630         for line in template.split("\n"):
631             if line.startswith("#"):
632                 result += line + "\n"
633                 continue
635             if inDescriptionSection:
636                 if line.startswith("Files:") or line.startswith("Jobs:"):
637                     inDescriptionSection = False
638                 else:
639                     continue
640             else:
641                 if line.startswith("Description:"):
642                     inDescriptionSection = True
643                     line += "\n"
644                     for messageLine in message.split("\n"):
645                         line += "\t" + messageLine + "\n"
647             result += line + "\n"
649         return result
651     def p4UserForCommit(self,id):
652         # Return the tuple (perforce user,git email) for a given git commit id
653         self.getUserMapFromPerforceServer()
654         gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
655         gitEmail = gitEmail.strip()
656         if not self.emails.has_key(gitEmail):
657             return (None,gitEmail)
658         else:
659             return (self.emails[gitEmail],gitEmail)
661     def checkValidP4Users(self,commits):
662         # check if any git authors cannot be mapped to p4 users
663         for id in commits:
664             (user,email) = self.p4UserForCommit(id)
665             if not user:
666                 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
667                 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
668                     print "%s" % msg
669                 else:
670                     die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
672     def lastP4Changelist(self):
673         # Get back the last changelist number submitted in this client spec. This
674         # then gets used to patch up the username in the change. If the same
675         # client spec is being used by multiple processes then this might go
676         # wrong.
677         results = p4CmdList("client -o")        # find the current client
678         client = None
679         for r in results:
680             if r.has_key('Client'):
681                 client = r['Client']
682                 break
683         if not client:
684             die("could not get client spec")
685         results = p4CmdList("changes -c %s -m 1" % client)
686         for r in results:
687             if r.has_key('change'):
688                 return r['change']
689         die("Could not get changelist number for last submit - cannot patch up user details")
691     def modifyChangelistUser(self, changelist, newUser):
692         # fixup the user field of a changelist after it has been submitted.
693         changes = p4CmdList("change -o %s" % changelist)
694         if len(changes) != 1:
695             die("Bad output from p4 change modifying %s to user %s" %
696                 (changelist, newUser))
698         c = changes[0]
699         if c['User'] == newUser: return   # nothing to do
700         c['User'] = newUser
701         input = marshal.dumps(c)
703         result = p4CmdList("change -f -i", stdin=input)
704         for r in result:
705             if r.has_key('code'):
706                 if r['code'] == 'error':
707                     die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
708             if r.has_key('data'):
709                 print("Updated user field for changelist %s to %s" % (changelist, newUser))
710                 return
711         die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
713     def canChangeChangelists(self):
714         # check to see if we have p4 admin or super-user permissions, either of
715         # which are required to modify changelists.
716         results = p4CmdList("protects %s" % self.depotPath)
717         for r in results:
718             if r.has_key('perm'):
719                 if r['perm'] == 'admin':
720                     return 1
721                 if r['perm'] == 'super':
722                     return 1
723         return 0
725     def p4UserId(self):
726         if self.myP4UserId:
727             return self.myP4UserId
729         results = p4CmdList("user -o")
730         for r in results:
731             if r.has_key('User'):
732                 self.myP4UserId = r['User']
733                 return r['User']
734         die("Could not find your p4 user id")
736     def p4UserIsMe(self, p4User):
737         # return True if the given p4 user is actually me
738         me = self.p4UserId()
739         if not p4User or p4User != me:
740             return False
741         else:
742             return True
744     def prepareSubmitTemplate(self):
745         # remove lines in the Files section that show changes to files outside the depot path we're committing into
746         template = ""
747         inFilesSection = False
748         for line in p4_read_pipe_lines("change -o"):
749             if line.endswith("\r\n"):
750                 line = line[:-2] + "\n"
751             if inFilesSection:
752                 if line.startswith("\t"):
753                     # path starts and ends with a tab
754                     path = line[1:]
755                     lastTab = path.rfind("\t")
756                     if lastTab != -1:
757                         path = path[:lastTab]
758                         if not p4PathStartsWith(path, self.depotPath):
759                             continue
760                 else:
761                     inFilesSection = False
762             else:
763                 if line.startswith("Files:"):
764                     inFilesSection = True
766             template += line
768         return template
770     def applyCommit(self, id):
771         print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
773         (p4User, gitEmail) = self.p4UserForCommit(id)
775         if not self.detectRenames:
776             # If not explicitly set check the config variable
777             self.detectRenames = gitConfig("git-p4.detectRenames").lower() == "true"
779         if self.detectRenames:
780             diffOpts = "-M"
781         else:
782             diffOpts = ""
784         if gitConfig("git-p4.detectCopies").lower() == "true":
785             diffOpts += " -C"
787         if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
788             diffOpts += " --find-copies-harder"
790         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
791         filesToAdd = set()
792         filesToDelete = set()
793         editedFiles = set()
794         filesToChangeExecBit = {}
795         for line in diff:
796             diff = parseDiffTreeEntry(line)
797             modifier = diff['status']
798             path = diff['src']
799             if modifier == "M":
800                 p4_system("edit \"%s\"" % path)
801                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
802                     filesToChangeExecBit[path] = diff['dst_mode']
803                 editedFiles.add(path)
804             elif modifier == "A":
805                 filesToAdd.add(path)
806                 filesToChangeExecBit[path] = diff['dst_mode']
807                 if path in filesToDelete:
808                     filesToDelete.remove(path)
809             elif modifier == "D":
810                 filesToDelete.add(path)
811                 if path in filesToAdd:
812                     filesToAdd.remove(path)
813             elif modifier == "C":
814                 src, dest = diff['src'], diff['dst']
815                 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
816                 if diff['src_sha1'] != diff['dst_sha1']:
817                     p4_system("edit \"%s\"" % (dest))
818                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
819                     p4_system("edit \"%s\"" % (dest))
820                     filesToChangeExecBit[dest] = diff['dst_mode']
821                 os.unlink(dest)
822                 editedFiles.add(dest)
823             elif modifier == "R":
824                 src, dest = diff['src'], diff['dst']
825                 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
826                 if diff['src_sha1'] != diff['dst_sha1']:
827                     p4_system("edit \"%s\"" % (dest))
828                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
829                     p4_system("edit \"%s\"" % (dest))
830                     filesToChangeExecBit[dest] = diff['dst_mode']
831                 os.unlink(dest)
832                 editedFiles.add(dest)
833                 filesToDelete.add(src)
834             else:
835                 die("unknown modifier %s for %s" % (modifier, path))
837         diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
838         patchcmd = diffcmd + " | git apply "
839         tryPatchCmd = patchcmd + "--check -"
840         applyPatchCmd = patchcmd + "--check --apply -"
842         if os.system(tryPatchCmd) != 0:
843             print "Unfortunately applying the change failed!"
844             print "What do you want to do?"
845             response = "x"
846             while response != "s" and response != "a" and response != "w":
847                 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
848                                      "and with .rej files / [w]rite the patch to a file (patch.txt) ")
849             if response == "s":
850                 print "Skipping! Good luck with the next patches..."
851                 for f in editedFiles:
852                     p4_system("revert \"%s\"" % f);
853                 for f in filesToAdd:
854                     system("rm %s" %f)
855                 return
856             elif response == "a":
857                 os.system(applyPatchCmd)
858                 if len(filesToAdd) > 0:
859                     print "You may also want to call p4 add on the following files:"
860                     print " ".join(filesToAdd)
861                 if len(filesToDelete):
862                     print "The following files should be scheduled for deletion with p4 delete:"
863                     print " ".join(filesToDelete)
864                 die("Please resolve and submit the conflict manually and "
865                     + "continue afterwards with git-p4 submit --continue")
866             elif response == "w":
867                 system(diffcmd + " > patch.txt")
868                 print "Patch saved to patch.txt in %s !" % self.clientPath
869                 die("Please resolve and submit the conflict manually and "
870                     "continue afterwards with git-p4 submit --continue")
872         system(applyPatchCmd)
874         for f in filesToAdd:
875             p4_system("add \"%s\"" % f)
876         for f in filesToDelete:
877             p4_system("revert \"%s\"" % f)
878             p4_system("delete \"%s\"" % f)
880         # Set/clear executable bits
881         for f in filesToChangeExecBit.keys():
882             mode = filesToChangeExecBit[f]
883             setP4ExecBit(f, mode)
885         logMessage = extractLogMessageFromGitCommit(id)
886         logMessage = logMessage.strip()
888         template = self.prepareSubmitTemplate()
890         if self.interactive:
891             submitTemplate = self.prepareLogMessage(template, logMessage)
893             if self.preserveUser:
894                submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
896             if os.environ.has_key("P4DIFF"):
897                 del(os.environ["P4DIFF"])
898             diff = ""
899             for editedFile in editedFiles:
900                 diff += p4_read_pipe("diff -du %r" % editedFile)
902             newdiff = ""
903             for newFile in filesToAdd:
904                 newdiff += "==== new file ====\n"
905                 newdiff += "--- /dev/null\n"
906                 newdiff += "+++ %s\n" % newFile
907                 f = open(newFile, "r")
908                 for line in f.readlines():
909                     newdiff += "+" + line
910                 f.close()
912             if self.checkAuthorship and not self.p4UserIsMe(p4User):
913                 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
914                 submitTemplate += "######## Use git-p4 option --preserve-user to modify authorship\n"
915                 submitTemplate += "######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n"
917             separatorLine = "######## everything below this line is just the diff #######\n"
919             [handle, fileName] = tempfile.mkstemp()
920             tmpFile = os.fdopen(handle, "w+")
921             if self.isWindows:
922                 submitTemplate = submitTemplate.replace("\n", "\r\n")
923                 separatorLine = separatorLine.replace("\n", "\r\n")
924                 newdiff = newdiff.replace("\n", "\r\n")
925             tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
926             tmpFile.close()
927             mtime = os.stat(fileName).st_mtime
928             if os.environ.has_key("P4EDITOR"):
929                 editor = os.environ.get("P4EDITOR")
930             else:
931                 editor = read_pipe("git var GIT_EDITOR").strip()
932             system(editor + " " + fileName)
934             if gitConfig("git-p4.skipSubmitEditCheck") == "true":
935                 checkModTime = False
936             else:
937                 checkModTime = True
939             response = "y"
940             if checkModTime and (os.stat(fileName).st_mtime <= mtime):
941                 response = "x"
942                 while response != "y" and response != "n":
943                     response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
945             if response == "y":
946                 tmpFile = open(fileName, "rb")
947                 message = tmpFile.read()
948                 tmpFile.close()
949                 submitTemplate = message[:message.index(separatorLine)]
950                 if self.isWindows:
951                     submitTemplate = submitTemplate.replace("\r\n", "\n")
952                 p4_write_pipe("submit -i", submitTemplate)
954                 if self.preserveUser:
955                     if p4User:
956                         # Get last changelist number. Cannot easily get it from
957                         # the submit command output as the output is unmarshalled.
958                         changelist = self.lastP4Changelist()
959                         self.modifyChangelistUser(changelist, p4User)
961             else:
962                 for f in editedFiles:
963                     p4_system("revert \"%s\"" % f);
964                 for f in filesToAdd:
965                     p4_system("revert \"%s\"" % f);
966                     system("rm %s" %f)
968             os.remove(fileName)
969         else:
970             fileName = "submit.txt"
971             file = open(fileName, "w+")
972             file.write(self.prepareLogMessage(template, logMessage))
973             file.close()
974             print ("Perforce submit template written as %s. "
975                    + "Please review/edit and then use p4 submit -i < %s to submit directly!"
976                    % (fileName, fileName))
978     def run(self, args):
979         if len(args) == 0:
980             self.master = currentGitBranch()
981             if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
982                 die("Detecting current git branch failed!")
983         elif len(args) == 1:
984             self.master = args[0]
985         else:
986             return False
988         allowSubmit = gitConfig("git-p4.allowSubmit")
989         if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
990             die("%s is not in git-p4.allowSubmit" % self.master)
992         [upstream, settings] = findUpstreamBranchPoint()
993         self.depotPath = settings['depot-paths'][0]
994         if len(self.origin) == 0:
995             self.origin = upstream
997         if self.preserveUser:
998             if not self.canChangeChangelists():
999                 die("Cannot preserve user names without p4 super-user or admin permissions")
1001         if self.verbose:
1002             print "Origin branch is " + self.origin
1004         if len(self.depotPath) == 0:
1005             print "Internal error: cannot locate perforce depot path from existing branches"
1006             sys.exit(128)
1008         self.clientPath = p4Where(self.depotPath)
1010         if len(self.clientPath) == 0:
1011             print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
1012             sys.exit(128)
1014         print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1015         self.oldWorkingDirectory = os.getcwd()
1017         chdir(self.clientPath)
1018         print "Synchronizing p4 checkout..."
1019         p4_system("sync ...")
1021         self.check()
1023         commits = []
1024         for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1025             commits.append(line.strip())
1026         commits.reverse()
1028         if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1029             self.checkAuthorship = False
1030         else:
1031             self.checkAuthorship = True
1033         if self.preserveUser:
1034             self.checkValidP4Users(commits)
1036         while len(commits) > 0:
1037             commit = commits[0]
1038             commits = commits[1:]
1039             self.applyCommit(commit)
1040             if not self.interactive:
1041                 break
1043         if len(commits) == 0:
1044             print "All changes applied!"
1045             chdir(self.oldWorkingDirectory)
1047             sync = P4Sync()
1048             sync.run([])
1050             rebase = P4Rebase()
1051             rebase.rebase()
1053         return True
1055 class P4Sync(Command, P4UserMap):
1056     delete_actions = ( "delete", "move/delete", "purge" )
1058     def __init__(self):
1059         Command.__init__(self)
1060         P4UserMap.__init__(self)
1061         self.options = [
1062                 optparse.make_option("--branch", dest="branch"),
1063                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1064                 optparse.make_option("--changesfile", dest="changesFile"),
1065                 optparse.make_option("--silent", dest="silent", action="store_true"),
1066                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1067                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
1068                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1069                                      help="Import into refs/heads/ , not refs/remotes"),
1070                 optparse.make_option("--max-changes", dest="maxChanges"),
1071                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1072                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1073                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1074                                      help="Only sync files that are included in the Perforce Client Spec")
1075         ]
1076         self.description = """Imports from Perforce into a git repository.\n
1077     example:
1078     //depot/my/project/ -- to import the current head
1079     //depot/my/project/@all -- to import everything
1080     //depot/my/project/@1,6 -- to import only from revision 1 to 6
1082     (a ... is not needed in the path p4 specification, it's added implicitly)"""
1084         self.usage += " //depot/path[@revRange]"
1085         self.silent = False
1086         self.createdBranches = set()
1087         self.committedChanges = set()
1088         self.branch = ""
1089         self.detectBranches = False
1090         self.detectLabels = False
1091         self.changesFile = ""
1092         self.syncWithOrigin = True
1093         self.verbose = False
1094         self.importIntoRemotes = True
1095         self.maxChanges = ""
1096         self.isWindows = (platform.system() == "Windows")
1097         self.keepRepoPath = False
1098         self.depotPaths = None
1099         self.p4BranchesInGit = []
1100         self.cloneExclude = []
1101         self.useClientSpec = False
1102         self.clientSpecDirs = []
1104         if gitConfig("git-p4.syncFromOrigin") == "false":
1105             self.syncWithOrigin = False
1107     #
1108     # P4 wildcards are not allowed in filenames.  P4 complains
1109     # if you simply add them, but you can force it with "-f", in
1110     # which case it translates them into %xx encoding internally.
1111     # Search for and fix just these four characters.  Do % last so
1112     # that fixing it does not inadvertently create new %-escapes.
1113     #
1114     def wildcard_decode(self, path):
1115         # Cannot have * in a filename in windows; untested as to
1116         # what p4 would do in such a case.
1117         if not self.isWindows:
1118             path = path.replace("%2A", "*")
1119         path = path.replace("%23", "#") \
1120                    .replace("%40", "@") \
1121                    .replace("%25", "%")
1122         return path
1124     def extractFilesFromCommit(self, commit):
1125         self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1126                              for path in self.cloneExclude]
1127         files = []
1128         fnum = 0
1129         while commit.has_key("depotFile%s" % fnum):
1130             path =  commit["depotFile%s" % fnum]
1132             if [p for p in self.cloneExclude
1133                 if p4PathStartsWith(path, p)]:
1134                 found = False
1135             else:
1136                 found = [p for p in self.depotPaths
1137                          if p4PathStartsWith(path, p)]
1138             if not found:
1139                 fnum = fnum + 1
1140                 continue
1142             file = {}
1143             file["path"] = path
1144             file["rev"] = commit["rev%s" % fnum]
1145             file["action"] = commit["action%s" % fnum]
1146             file["type"] = commit["type%s" % fnum]
1147             files.append(file)
1148             fnum = fnum + 1
1149         return files
1151     def stripRepoPath(self, path, prefixes):
1152         if self.useClientSpec:
1154             # if using the client spec, we use the output directory
1155             # specified in the client.  For example, a view
1156             #   //depot/foo/branch/... //client/branch/foo/...
1157             # will end up putting all foo/branch files into
1158             #  branch/foo/
1159             for val in self.clientSpecDirs:
1160                 if path.startswith(val[0]):
1161                     # replace the depot path with the client path
1162                     path = path.replace(val[0], val[1][1])
1163                     # now strip out the client (//client/...)
1164                     path = re.sub("^(//[^/]+/)", '', path)
1165                     # the rest is all path
1166                     return path
1168         if self.keepRepoPath:
1169             prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1171         for p in prefixes:
1172             if p4PathStartsWith(path, p):
1173                 path = path[len(p):]
1175         return path
1177     def splitFilesIntoBranches(self, commit):
1178         branches = {}
1179         fnum = 0
1180         while commit.has_key("depotFile%s" % fnum):
1181             path =  commit["depotFile%s" % fnum]
1182             found = [p for p in self.depotPaths
1183                      if p4PathStartsWith(path, p)]
1184             if not found:
1185                 fnum = fnum + 1
1186                 continue
1188             file = {}
1189             file["path"] = path
1190             file["rev"] = commit["rev%s" % fnum]
1191             file["action"] = commit["action%s" % fnum]
1192             file["type"] = commit["type%s" % fnum]
1193             fnum = fnum + 1
1195             relPath = self.stripRepoPath(path, self.depotPaths)
1197             for branch in self.knownBranches.keys():
1199                 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1200                 if relPath.startswith(branch + "/"):
1201                     if branch not in branches:
1202                         branches[branch] = []
1203                     branches[branch].append(file)
1204                     break
1206         return branches
1208     # output one file from the P4 stream
1209     # - helper for streamP4Files
1211     def streamOneP4File(self, file, contents):
1212         if file["type"] == "apple":
1213             print "\nfile %s is a strange apple file that forks. Ignoring" % \
1214                 file['depotFile']
1215             return
1217         relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1218         relPath = self.wildcard_decode(relPath)
1219         if verbose:
1220             sys.stderr.write("%s\n" % relPath)
1222         mode = "644"
1223         if isP4Exec(file["type"]):
1224             mode = "755"
1225         elif file["type"] == "symlink":
1226             mode = "120000"
1227             # p4 print on a symlink contains "target\n", so strip it off
1228             data = ''.join(contents)
1229             contents = [data[:-1]]
1231         if self.isWindows and file["type"].endswith("text"):
1232             mangled = []
1233             for data in contents:
1234                 data = data.replace("\r\n", "\n")
1235                 mangled.append(data)
1236             contents = mangled
1238         if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1239             contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1240         elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1241             contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1243         self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1245         # total length...
1246         length = 0
1247         for d in contents:
1248             length = length + len(d)
1250         self.gitStream.write("data %d\n" % length)
1251         for d in contents:
1252             self.gitStream.write(d)
1253         self.gitStream.write("\n")
1255     def streamOneP4Deletion(self, file):
1256         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1257         if verbose:
1258             sys.stderr.write("delete %s\n" % relPath)
1259         self.gitStream.write("D %s\n" % relPath)
1261     # handle another chunk of streaming data
1262     def streamP4FilesCb(self, marshalled):
1264         if marshalled.has_key('depotFile') and self.stream_have_file_info:
1265             # start of a new file - output the old one first
1266             self.streamOneP4File(self.stream_file, self.stream_contents)
1267             self.stream_file = {}
1268             self.stream_contents = []
1269             self.stream_have_file_info = False
1271         # pick up the new file information... for the
1272         # 'data' field we need to append to our array
1273         for k in marshalled.keys():
1274             if k == 'data':
1275                 self.stream_contents.append(marshalled['data'])
1276             else:
1277                 self.stream_file[k] = marshalled[k]
1279         self.stream_have_file_info = True
1281     # Stream directly from "p4 files" into "git fast-import"
1282     def streamP4Files(self, files):
1283         filesForCommit = []
1284         filesToRead = []
1285         filesToDelete = []
1287         for f in files:
1288             includeFile = True
1289             for val in self.clientSpecDirs:
1290                 if f['path'].startswith(val[0]):
1291                     if val[1][0] <= 0:
1292                         includeFile = False
1293                     break
1295             if includeFile:
1296                 filesForCommit.append(f)
1297                 if f['action'] in self.delete_actions:
1298                     filesToDelete.append(f)
1299                 else:
1300                     filesToRead.append(f)
1302         # deleted files...
1303         for f in filesToDelete:
1304             self.streamOneP4Deletion(f)
1306         if len(filesToRead) > 0:
1307             self.stream_file = {}
1308             self.stream_contents = []
1309             self.stream_have_file_info = False
1311             # curry self argument
1312             def streamP4FilesCbSelf(entry):
1313                 self.streamP4FilesCb(entry)
1315             p4CmdList("-x - print",
1316                 '\n'.join(['%s#%s' % (f['path'], f['rev'])
1317                                                   for f in filesToRead]),
1318                 cb=streamP4FilesCbSelf)
1320             # do the last chunk
1321             if self.stream_file.has_key('depotFile'):
1322                 self.streamOneP4File(self.stream_file, self.stream_contents)
1324     def commit(self, details, files, branch, branchPrefixes, parent = ""):
1325         epoch = details["time"]
1326         author = details["user"]
1327         self.branchPrefixes = branchPrefixes
1329         if self.verbose:
1330             print "commit into %s" % branch
1332         # start with reading files; if that fails, we should not
1333         # create a commit.
1334         new_files = []
1335         for f in files:
1336             if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1337                 new_files.append (f)
1338             else:
1339                 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1341         self.gitStream.write("commit %s\n" % branch)
1342 #        gitStream.write("mark :%s\n" % details["change"])
1343         self.committedChanges.add(int(details["change"]))
1344         committer = ""
1345         if author not in self.users:
1346             self.getUserMapFromPerforceServer()
1347         if author in self.users:
1348             committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1349         else:
1350             committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1352         self.gitStream.write("committer %s\n" % committer)
1354         self.gitStream.write("data <<EOT\n")
1355         self.gitStream.write(details["desc"])
1356         self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1357                              % (','.join (branchPrefixes), details["change"]))
1358         if len(details['options']) > 0:
1359             self.gitStream.write(": options = %s" % details['options'])
1360         self.gitStream.write("]\nEOT\n\n")
1362         if len(parent) > 0:
1363             if self.verbose:
1364                 print "parent %s" % parent
1365             self.gitStream.write("from %s\n" % parent)
1367         self.streamP4Files(new_files)
1368         self.gitStream.write("\n")
1370         change = int(details["change"])
1372         if self.labels.has_key(change):
1373             label = self.labels[change]
1374             labelDetails = label[0]
1375             labelRevisions = label[1]
1376             if self.verbose:
1377                 print "Change %s is labelled %s" % (change, labelDetails)
1379             files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1380                                                     for p in branchPrefixes]))
1382             if len(files) == len(labelRevisions):
1384                 cleanedFiles = {}
1385                 for info in files:
1386                     if info["action"] in self.delete_actions:
1387                         continue
1388                     cleanedFiles[info["depotFile"]] = info["rev"]
1390                 if cleanedFiles == labelRevisions:
1391                     self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1392                     self.gitStream.write("from %s\n" % branch)
1394                     owner = labelDetails["Owner"]
1395                     tagger = ""
1396                     if author in self.users:
1397                         tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1398                     else:
1399                         tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1400                     self.gitStream.write("tagger %s\n" % tagger)
1401                     self.gitStream.write("data <<EOT\n")
1402                     self.gitStream.write(labelDetails["Description"])
1403                     self.gitStream.write("EOT\n\n")
1405                 else:
1406                     if not self.silent:
1407                         print ("Tag %s does not match with change %s: files do not match."
1408                                % (labelDetails["label"], change))
1410             else:
1411                 if not self.silent:
1412                     print ("Tag %s does not match with change %s: file count is different."
1413                            % (labelDetails["label"], change))
1415     def getLabels(self):
1416         self.labels = {}
1418         l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1419         if len(l) > 0 and not self.silent:
1420             print "Finding files belonging to labels in %s" % `self.depotPaths`
1422         for output in l:
1423             label = output["label"]
1424             revisions = {}
1425             newestChange = 0
1426             if self.verbose:
1427                 print "Querying files for label %s" % label
1428             for file in p4CmdList("files "
1429                                   +  ' '.join (["%s...@%s" % (p, label)
1430                                                 for p in self.depotPaths])):
1431                 revisions[file["depotFile"]] = file["rev"]
1432                 change = int(file["change"])
1433                 if change > newestChange:
1434                     newestChange = change
1436             self.labels[newestChange] = [output, revisions]
1438         if self.verbose:
1439             print "Label changes: %s" % self.labels.keys()
1441     def guessProjectName(self):
1442         for p in self.depotPaths:
1443             if p.endswith("/"):
1444                 p = p[:-1]
1445             p = p[p.strip().rfind("/") + 1:]
1446             if not p.endswith("/"):
1447                p += "/"
1448             return p
1450     def getBranchMapping(self):
1451         lostAndFoundBranches = set()
1453         for info in p4CmdList("branches"):
1454             details = p4Cmd("branch -o %s" % info["branch"])
1455             viewIdx = 0
1456             while details.has_key("View%s" % viewIdx):
1457                 paths = details["View%s" % viewIdx].split(" ")
1458                 viewIdx = viewIdx + 1
1459                 # require standard //depot/foo/... //depot/bar/... mapping
1460                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1461                     continue
1462                 source = paths[0]
1463                 destination = paths[1]
1464                 ## HACK
1465                 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
1466                     source = source[len(self.depotPaths[0]):-4]
1467                     destination = destination[len(self.depotPaths[0]):-4]
1469                     if destination in self.knownBranches:
1470                         if not self.silent:
1471                             print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1472                             print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1473                         continue
1475                     self.knownBranches[destination] = source
1477                     lostAndFoundBranches.discard(destination)
1479                     if source not in self.knownBranches:
1480                         lostAndFoundBranches.add(source)
1483         for branch in lostAndFoundBranches:
1484             self.knownBranches[branch] = branch
1486     def getBranchMappingFromGitBranches(self):
1487         branches = p4BranchesInGit(self.importIntoRemotes)
1488         for branch in branches.keys():
1489             if branch == "master":
1490                 branch = "main"
1491             else:
1492                 branch = branch[len(self.projectName):]
1493             self.knownBranches[branch] = branch
1495     def listExistingP4GitBranches(self):
1496         # branches holds mapping from name to commit
1497         branches = p4BranchesInGit(self.importIntoRemotes)
1498         self.p4BranchesInGit = branches.keys()
1499         for branch in branches.keys():
1500             self.initialParents[self.refPrefix + branch] = branches[branch]
1502     def updateOptionDict(self, d):
1503         option_keys = {}
1504         if self.keepRepoPath:
1505             option_keys['keepRepoPath'] = 1
1507         d["options"] = ' '.join(sorted(option_keys.keys()))
1509     def readOptions(self, d):
1510         self.keepRepoPath = (d.has_key('options')
1511                              and ('keepRepoPath' in d['options']))
1513     def gitRefForBranch(self, branch):
1514         if branch == "main":
1515             return self.refPrefix + "master"
1517         if len(branch) <= 0:
1518             return branch
1520         return self.refPrefix + self.projectName + branch
1522     def gitCommitByP4Change(self, ref, change):
1523         if self.verbose:
1524             print "looking in ref " + ref + " for change %s using bisect..." % change
1526         earliestCommit = ""
1527         latestCommit = parseRevision(ref)
1529         while True:
1530             if self.verbose:
1531                 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1532             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1533             if len(next) == 0:
1534                 if self.verbose:
1535                     print "argh"
1536                 return ""
1537             log = extractLogMessageFromGitCommit(next)
1538             settings = extractSettingsGitLog(log)
1539             currentChange = int(settings['change'])
1540             if self.verbose:
1541                 print "current change %s" % currentChange
1543             if currentChange == change:
1544                 if self.verbose:
1545                     print "found %s" % next
1546                 return next
1548             if currentChange < change:
1549                 earliestCommit = "^%s" % next
1550             else:
1551                 latestCommit = "%s" % next
1553         return ""
1555     def importNewBranch(self, branch, maxChange):
1556         # make fast-import flush all changes to disk and update the refs using the checkpoint
1557         # command so that we can try to find the branch parent in the git history
1558         self.gitStream.write("checkpoint\n\n");
1559         self.gitStream.flush();
1560         branchPrefix = self.depotPaths[0] + branch + "/"
1561         range = "@1,%s" % maxChange
1562         #print "prefix" + branchPrefix
1563         changes = p4ChangesForPaths([branchPrefix], range)
1564         if len(changes) <= 0:
1565             return False
1566         firstChange = changes[0]
1567         #print "first change in branch: %s" % firstChange
1568         sourceBranch = self.knownBranches[branch]
1569         sourceDepotPath = self.depotPaths[0] + sourceBranch
1570         sourceRef = self.gitRefForBranch(sourceBranch)
1571         #print "source " + sourceBranch
1573         branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1574         #print "branch parent: %s" % branchParentChange
1575         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1576         if len(gitParent) > 0:
1577             self.initialParents[self.gitRefForBranch(branch)] = gitParent
1578             #print "parent git commit: %s" % gitParent
1580         self.importChanges(changes)
1581         return True
1583     def importChanges(self, changes):
1584         cnt = 1
1585         for change in changes:
1586             description = p4Cmd("describe %s" % change)
1587             self.updateOptionDict(description)
1589             if not self.silent:
1590                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1591                 sys.stdout.flush()
1592             cnt = cnt + 1
1594             try:
1595                 if self.detectBranches:
1596                     branches = self.splitFilesIntoBranches(description)
1597                     for branch in branches.keys():
1598                         ## HACK  --hwn
1599                         branchPrefix = self.depotPaths[0] + branch + "/"
1601                         parent = ""
1603                         filesForCommit = branches[branch]
1605                         if self.verbose:
1606                             print "branch is %s" % branch
1608                         self.updatedBranches.add(branch)
1610                         if branch not in self.createdBranches:
1611                             self.createdBranches.add(branch)
1612                             parent = self.knownBranches[branch]
1613                             if parent == branch:
1614                                 parent = ""
1615                             else:
1616                                 fullBranch = self.projectName + branch
1617                                 if fullBranch not in self.p4BranchesInGit:
1618                                     if not self.silent:
1619                                         print("\n    Importing new branch %s" % fullBranch);
1620                                     if self.importNewBranch(branch, change - 1):
1621                                         parent = ""
1622                                         self.p4BranchesInGit.append(fullBranch)
1623                                     if not self.silent:
1624                                         print("\n    Resuming with change %s" % change);
1626                                 if self.verbose:
1627                                     print "parent determined through known branches: %s" % parent
1629                         branch = self.gitRefForBranch(branch)
1630                         parent = self.gitRefForBranch(parent)
1632                         if self.verbose:
1633                             print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1635                         if len(parent) == 0 and branch in self.initialParents:
1636                             parent = self.initialParents[branch]
1637                             del self.initialParents[branch]
1639                         self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1640                 else:
1641                     files = self.extractFilesFromCommit(description)
1642                     self.commit(description, files, self.branch, self.depotPaths,
1643                                 self.initialParent)
1644                     self.initialParent = ""
1645             except IOError:
1646                 print self.gitError.read()
1647                 sys.exit(1)
1649     def importHeadRevision(self, revision):
1650         print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1652         details = { "user" : "git perforce import user", "time" : int(time.time()) }
1653         details["desc"] = ("Initial import of %s from the state at revision %s\n"
1654                            % (' '.join(self.depotPaths), revision))
1655         details["change"] = revision
1656         newestRevision = 0
1658         fileCnt = 0
1659         for info in p4CmdList("files "
1660                               +  ' '.join(["%s...%s"
1661                                            % (p, revision)
1662                                            for p in self.depotPaths])):
1664             if 'code' in info and info['code'] == 'error':
1665                 sys.stderr.write("p4 returned an error: %s\n"
1666                                  % info['data'])
1667                 if info['data'].find("must refer to client") >= 0:
1668                     sys.stderr.write("This particular p4 error is misleading.\n")
1669                     sys.stderr.write("Perhaps the depot path was misspelled.\n");
1670                     sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
1671                 sys.exit(1)
1672             if 'p4ExitCode' in info:
1673                 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
1674                 sys.exit(1)
1677             change = int(info["change"])
1678             if change > newestRevision:
1679                 newestRevision = change
1681             if info["action"] in self.delete_actions:
1682                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1683                 #fileCnt = fileCnt + 1
1684                 continue
1686             for prop in ["depotFile", "rev", "action", "type" ]:
1687                 details["%s%s" % (prop, fileCnt)] = info[prop]
1689             fileCnt = fileCnt + 1
1691         details["change"] = newestRevision
1692         self.updateOptionDict(details)
1693         try:
1694             self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1695         except IOError:
1696             print "IO error with git fast-import. Is your git version recent enough?"
1697             print self.gitError.read()
1700     def getClientSpec(self):
1701         specList = p4CmdList( "client -o" )
1702         temp = {}
1703         for entry in specList:
1704             for k,v in entry.iteritems():
1705                 if k.startswith("View"):
1707                     # p4 has these %%1 to %%9 arguments in specs to
1708                     # reorder paths; which we can't handle (yet :)
1709                     if re.match('%%\d', v) != None:
1710                         print "Sorry, can't handle %%n arguments in client specs"
1711                         sys.exit(1)
1713                     if v.startswith('"'):
1714                         start = 1
1715                     else:
1716                         start = 0
1717                     index = v.find("...")
1719                     # save the "client view"; i.e the RHS of the view
1720                     # line that tells the client where to put the
1721                     # files for this view.
1722                     cv = v[index+3:].strip() # +3 to remove previous '...'
1724                     # if the client view doesn't end with a
1725                     # ... wildcard, then we're going to mess up the
1726                     # output directory, so fail gracefully.
1727                     if not cv.endswith('...'):
1728                         print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1729                         sys.exit(1)
1730                     cv=cv[:-3]
1732                     # now save the view; +index means included, -index
1733                     # means it should be filtered out.
1734                     v = v[start:index]
1735                     if v.startswith("-"):
1736                         v = v[1:]
1737                         include = -len(v)
1738                     else:
1739                         include = len(v)
1741                     temp[v] = (include, cv)
1743         self.clientSpecDirs = temp.items()
1744         self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1746     def run(self, args):
1747         self.depotPaths = []
1748         self.changeRange = ""
1749         self.initialParent = ""
1750         self.previousDepotPaths = []
1752         # map from branch depot path to parent branch
1753         self.knownBranches = {}
1754         self.initialParents = {}
1755         self.hasOrigin = originP4BranchesExist()
1756         if not self.syncWithOrigin:
1757             self.hasOrigin = False
1759         if self.importIntoRemotes:
1760             self.refPrefix = "refs/remotes/p4/"
1761         else:
1762             self.refPrefix = "refs/heads/p4/"
1764         if self.syncWithOrigin and self.hasOrigin:
1765             if not self.silent:
1766                 print "Syncing with origin first by calling git fetch origin"
1767             system("git fetch origin")
1769         if len(self.branch) == 0:
1770             self.branch = self.refPrefix + "master"
1771             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1772                 system("git update-ref %s refs/heads/p4" % self.branch)
1773                 system("git branch -D p4");
1774             # create it /after/ importing, when master exists
1775             if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1776                 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1778         if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1779             self.getClientSpec()
1781         # TODO: should always look at previous commits,
1782         # merge with previous imports, if possible.
1783         if args == []:
1784             if self.hasOrigin:
1785                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1786             self.listExistingP4GitBranches()
1788             if len(self.p4BranchesInGit) > 1:
1789                 if not self.silent:
1790                     print "Importing from/into multiple branches"
1791                 self.detectBranches = True
1793             if self.verbose:
1794                 print "branches: %s" % self.p4BranchesInGit
1796             p4Change = 0
1797             for branch in self.p4BranchesInGit:
1798                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1800                 settings = extractSettingsGitLog(logMsg)
1802                 self.readOptions(settings)
1803                 if (settings.has_key('depot-paths')
1804                     and settings.has_key ('change')):
1805                     change = int(settings['change']) + 1
1806                     p4Change = max(p4Change, change)
1808                     depotPaths = sorted(settings['depot-paths'])
1809                     if self.previousDepotPaths == []:
1810                         self.previousDepotPaths = depotPaths
1811                     else:
1812                         paths = []
1813                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1814                             for i in range(0, min(len(cur), len(prev))):
1815                                 if cur[i] <> prev[i]:
1816                                     i = i - 1
1817                                     break
1819                             paths.append (cur[:i + 1])
1821                         self.previousDepotPaths = paths
1823             if p4Change > 0:
1824                 self.depotPaths = sorted(self.previousDepotPaths)
1825                 self.changeRange = "@%s,#head" % p4Change
1826                 if not self.detectBranches:
1827                     self.initialParent = parseRevision(self.branch)
1828                 if not self.silent and not self.detectBranches:
1829                     print "Performing incremental import into %s git branch" % self.branch
1831         if not self.branch.startswith("refs/"):
1832             self.branch = "refs/heads/" + self.branch
1834         if len(args) == 0 and self.depotPaths:
1835             if not self.silent:
1836                 print "Depot paths: %s" % ' '.join(self.depotPaths)
1837         else:
1838             if self.depotPaths and self.depotPaths != args:
1839                 print ("previous import used depot path %s and now %s was specified. "
1840                        "This doesn't work!" % (' '.join (self.depotPaths),
1841                                                ' '.join (args)))
1842                 sys.exit(1)
1844             self.depotPaths = sorted(args)
1846         revision = ""
1847         self.users = {}
1849         newPaths = []
1850         for p in self.depotPaths:
1851             if p.find("@") != -1:
1852                 atIdx = p.index("@")
1853                 self.changeRange = p[atIdx:]
1854                 if self.changeRange == "@all":
1855                     self.changeRange = ""
1856                 elif ',' not in self.changeRange:
1857                     revision = self.changeRange
1858                     self.changeRange = ""
1859                 p = p[:atIdx]
1860             elif p.find("#") != -1:
1861                 hashIdx = p.index("#")
1862                 revision = p[hashIdx:]
1863                 p = p[:hashIdx]
1864             elif self.previousDepotPaths == []:
1865                 revision = "#head"
1867             p = re.sub ("\.\.\.$", "", p)
1868             if not p.endswith("/"):
1869                 p += "/"
1871             newPaths.append(p)
1873         self.depotPaths = newPaths
1876         self.loadUserMapFromCache()
1877         self.labels = {}
1878         if self.detectLabels:
1879             self.getLabels();
1881         if self.detectBranches:
1882             ## FIXME - what's a P4 projectName ?
1883             self.projectName = self.guessProjectName()
1885             if self.hasOrigin:
1886                 self.getBranchMappingFromGitBranches()
1887             else:
1888                 self.getBranchMapping()
1889             if self.verbose:
1890                 print "p4-git branches: %s" % self.p4BranchesInGit
1891                 print "initial parents: %s" % self.initialParents
1892             for b in self.p4BranchesInGit:
1893                 if b != "master":
1895                     ## FIXME
1896                     b = b[len(self.projectName):]
1897                 self.createdBranches.add(b)
1899         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1901         importProcess = subprocess.Popen(["git", "fast-import"],
1902                                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1903                                          stderr=subprocess.PIPE);
1904         self.gitOutput = importProcess.stdout
1905         self.gitStream = importProcess.stdin
1906         self.gitError = importProcess.stderr
1908         if revision:
1909             self.importHeadRevision(revision)
1910         else:
1911             changes = []
1913             if len(self.changesFile) > 0:
1914                 output = open(self.changesFile).readlines()
1915                 changeSet = set()
1916                 for line in output:
1917                     changeSet.add(int(line))
1919                 for change in changeSet:
1920                     changes.append(change)
1922                 changes.sort()
1923             else:
1924                 # catch "git-p4 sync" with no new branches, in a repo that
1925                 # does not have any existing git-p4 branches
1926                 if len(args) == 0 and not self.p4BranchesInGit:
1927                     die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
1928                 if self.verbose:
1929                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1930                                                               self.changeRange)
1931                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1933                 if len(self.maxChanges) > 0:
1934                     changes = changes[:min(int(self.maxChanges), len(changes))]
1936             if len(changes) == 0:
1937                 if not self.silent:
1938                     print "No changes to import!"
1939                 return True
1941             if not self.silent and not self.detectBranches:
1942                 print "Import destination: %s" % self.branch
1944             self.updatedBranches = set()
1946             self.importChanges(changes)
1948             if not self.silent:
1949                 print ""
1950                 if len(self.updatedBranches) > 0:
1951                     sys.stdout.write("Updated branches: ")
1952                     for b in self.updatedBranches:
1953                         sys.stdout.write("%s " % b)
1954                     sys.stdout.write("\n")
1956         self.gitStream.close()
1957         if importProcess.wait() != 0:
1958             die("fast-import failed: %s" % self.gitError.read())
1959         self.gitOutput.close()
1960         self.gitError.close()
1962         return True
1964 class P4Rebase(Command):
1965     def __init__(self):
1966         Command.__init__(self)
1967         self.options = [ ]
1968         self.description = ("Fetches the latest revision from perforce and "
1969                             + "rebases the current work (branch) against it")
1970         self.verbose = False
1972     def run(self, args):
1973         sync = P4Sync()
1974         sync.run([])
1976         return self.rebase()
1978     def rebase(self):
1979         if os.system("git update-index --refresh") != 0:
1980             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.");
1981         if len(read_pipe("git diff-index HEAD --")) > 0:
1982             die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1984         [upstream, settings] = findUpstreamBranchPoint()
1985         if len(upstream) == 0:
1986             die("Cannot find upstream branchpoint for rebase")
1988         # the branchpoint may be p4/foo~3, so strip off the parent
1989         upstream = re.sub("~[0-9]+$", "", upstream)
1991         print "Rebasing the current branch onto %s" % upstream
1992         oldHead = read_pipe("git rev-parse HEAD").strip()
1993         system("git rebase %s" % upstream)
1994         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1995         return True
1997 class P4Clone(P4Sync):
1998     def __init__(self):
1999         P4Sync.__init__(self)
2000         self.description = "Creates a new git repository and imports from Perforce into it"
2001         self.usage = "usage: %prog [options] //depot/path[@revRange]"
2002         self.options += [
2003             optparse.make_option("--destination", dest="cloneDestination",
2004                                  action='store', default=None,
2005                                  help="where to leave result of the clone"),
2006             optparse.make_option("-/", dest="cloneExclude",
2007                                  action="append", type="string",
2008                                  help="exclude depot path"),
2009             optparse.make_option("--bare", dest="cloneBare",
2010                                  action="store_true", default=False),
2011         ]
2012         self.cloneDestination = None
2013         self.needsGit = False
2014         self.cloneBare = False
2016     # This is required for the "append" cloneExclude action
2017     def ensure_value(self, attr, value):
2018         if not hasattr(self, attr) or getattr(self, attr) is None:
2019             setattr(self, attr, value)
2020         return getattr(self, attr)
2022     def defaultDestination(self, args):
2023         ## TODO: use common prefix of args?
2024         depotPath = args[0]
2025         depotDir = re.sub("(@[^@]*)$", "", depotPath)
2026         depotDir = re.sub("(#[^#]*)$", "", depotDir)
2027         depotDir = re.sub(r"\.\.\.$", "", depotDir)
2028         depotDir = re.sub(r"/$", "", depotDir)
2029         return os.path.split(depotDir)[1]
2031     def run(self, args):
2032         if len(args) < 1:
2033             return False
2035         if self.keepRepoPath and not self.cloneDestination:
2036             sys.stderr.write("Must specify destination for --keep-path\n")
2037             sys.exit(1)
2039         depotPaths = args
2041         if not self.cloneDestination and len(depotPaths) > 1:
2042             self.cloneDestination = depotPaths[-1]
2043             depotPaths = depotPaths[:-1]
2045         self.cloneExclude = ["/"+p for p in self.cloneExclude]
2046         for p in depotPaths:
2047             if not p.startswith("//"):
2048                 return False
2050         if not self.cloneDestination:
2051             self.cloneDestination = self.defaultDestination(args)
2053         print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2055         if not os.path.exists(self.cloneDestination):
2056             os.makedirs(self.cloneDestination)
2057         chdir(self.cloneDestination)
2059         init_cmd = [ "git", "init" ]
2060         if self.cloneBare:
2061             init_cmd.append("--bare")
2062         subprocess.check_call(init_cmd)
2064         if not P4Sync.run(self, depotPaths):
2065             return False
2066         if self.branch != "master":
2067             if self.importIntoRemotes:
2068                 masterbranch = "refs/remotes/p4/master"
2069             else:
2070                 masterbranch = "refs/heads/p4/master"
2071             if gitBranchExists(masterbranch):
2072                 system("git branch master %s" % masterbranch)
2073                 if not self.cloneBare:
2074                     system("git checkout -f")
2075             else:
2076                 print "Could not detect main branch. No checkout/master branch created."
2078         return True
2080 class P4Branches(Command):
2081     def __init__(self):
2082         Command.__init__(self)
2083         self.options = [ ]
2084         self.description = ("Shows the git branches that hold imports and their "
2085                             + "corresponding perforce depot paths")
2086         self.verbose = False
2088     def run(self, args):
2089         if originP4BranchesExist():
2090             createOrUpdateBranchesFromOrigin()
2092         cmdline = "git rev-parse --symbolic "
2093         cmdline += " --remotes"
2095         for line in read_pipe_lines(cmdline):
2096             line = line.strip()
2098             if not line.startswith('p4/') or line == "p4/HEAD":
2099                 continue
2100             branch = line
2102             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2103             settings = extractSettingsGitLog(log)
2105             print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2106         return True
2108 class HelpFormatter(optparse.IndentedHelpFormatter):
2109     def __init__(self):
2110         optparse.IndentedHelpFormatter.__init__(self)
2112     def format_description(self, description):
2113         if description:
2114             return description + "\n"
2115         else:
2116             return ""
2118 def printUsage(commands):
2119     print "usage: %s <command> [options]" % sys.argv[0]
2120     print ""
2121     print "valid commands: %s" % ", ".join(commands)
2122     print ""
2123     print "Try %s <command> --help for command specific help." % sys.argv[0]
2124     print ""
2126 commands = {
2127     "debug" : P4Debug,
2128     "submit" : P4Submit,
2129     "commit" : P4Submit,
2130     "sync" : P4Sync,
2131     "rebase" : P4Rebase,
2132     "clone" : P4Clone,
2133     "rollback" : P4RollBack,
2134     "branches" : P4Branches
2138 def main():
2139     if len(sys.argv[1:]) == 0:
2140         printUsage(commands.keys())
2141         sys.exit(2)
2143     cmd = ""
2144     cmdName = sys.argv[1]
2145     try:
2146         klass = commands[cmdName]
2147         cmd = klass()
2148     except KeyError:
2149         print "unknown command %s" % cmdName
2150         print ""
2151         printUsage(commands.keys())
2152         sys.exit(2)
2154     options = cmd.options
2155     cmd.gitdir = os.environ.get("GIT_DIR", None)
2157     args = sys.argv[2:]
2159     if len(options) > 0:
2160         options.append(optparse.make_option("--git-dir", dest="gitdir"))
2162         parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2163                                        options,
2164                                        description = cmd.description,
2165                                        formatter = HelpFormatter())
2167         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2168     global verbose
2169     verbose = cmd.verbose
2170     if cmd.needsGit:
2171         if cmd.gitdir == None:
2172             cmd.gitdir = os.path.abspath(".git")
2173             if not isValidGitDir(cmd.gitdir):
2174                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2175                 if os.path.exists(cmd.gitdir):
2176                     cdup = read_pipe("git rev-parse --show-cdup").strip()
2177                     if len(cdup) > 0:
2178                         chdir(cdup);
2180         if not isValidGitDir(cmd.gitdir):
2181             if isValidGitDir(cmd.gitdir + "/.git"):
2182                 cmd.gitdir += "/.git"
2183             else:
2184                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2186         os.environ["GIT_DIR"] = cmd.gitdir
2188     if not cmd.run(args):
2189         parser.print_help()
2192 if __name__ == '__main__':
2193     main()