import os import sys import os.path import re import sublime import sublime_plugin import webbrowser import plistlib from github import GitHubApi import logging as logger try: import xml.parsers.expat as expat except ImportError: expat = None try: sys.path.append(os.path.join(sublime.packages_path(), 'Git')) git = __import__("git") sys.path.remove(os.path.join(sublime.packages_path(), 'Git')) except ImportError: git = None logger.basicConfig(format='[sublime-github] %(levelname)s: %(message)s') class BaseGitHubCommand(sublime_plugin.TextCommand): """ Base class for all GitHub commands. Handles getting an auth token. """ MSG_USERNAME = "GitHub username:" MSG_PASSWORD = "GitHub password:" MSG_TOKEN_SUCCESS = "Your access token has been saved. We'll now resume your command." ERR_NO_USER_TOKEN = "Your GitHub Gist access token needs to be configured.\n\n"\ "Click OK and then enter your GitHub username and password below (neither will "\ "be stored; they are only used to generate an access token)." ERR_UNAUTHORIZED = "Your Github username or password appears to be incorrect. "\ "Please try again." ERR_UNAUTHORIZED_TOKEN = "Your Github token appears to be incorrect. Please re-enter your "\ "username and password to generate a new token." def run(self, edit): self.settings = sublime.load_settings("GitHub.sublime-settings") self.github_user = None self.accounts = self.settings.get("accounts") self.active_account = self.settings.get("active_account") if not self.active_account: self.active_account = self.accounts.keys()[0] self.github_token = self.accounts[self.active_account]["github_token"] if not self.github_token: self.github_token = self.settings.get("github_token") if self.github_token: # migrate to new structure self.settings.set("accounts", {"GitHub": {"base_uri": "https://api.github.com", "github_token": self.github_token}}) self.settings.set("active_account", "GitHub") self.active_account = self.settings.get("active_account") self.settings.erase("github_token") sublime.save_settings("GitHub.sublime-settings") self.base_uri = self.accounts[self.active_account]["base_uri"] self.debug = self.settings.get('debug') self.gistapi = GitHubApi(self.base_uri, self.github_token, debug=self.debug) def get_token(self): sublime.error_message(self.ERR_NO_USER_TOKEN) self.get_username() def get_username(self): self.view.window().show_input_panel(self.MSG_USERNAME, self.github_user or "", self.on_done_username, None, None) def get_password(self): self.view.window().show_input_panel(self.MSG_PASSWORD, "", self.on_done_password, None, None) def on_done_username(self, value): "Callback for the username show_input_panel." self.github_user = value # need to do this or the input panel doesn't show sublime.set_timeout(self.get_password, 50) def on_done_password(self, value): "Callback for the password show_input_panel" try: self.github_token = GitHubApi(self.base_uri, debug=self.debug).get_token(self.github_user, value) self.accounts[self.active_account]["github_token"] = self.github_token self.settings.set("accounts", self.accounts) sublime.save_settings("GitHub.sublime-settings") self.gistapi = GitHubApi(self.base_uri, self.github_token, debug=self.debug) try: if self.callback: sublime.error_message(self.MSG_TOKEN_SUCCESS) callback = self.callback self.callback = None sublime.set_timeout(callback, 50) except AttributeError: pass except GitHubApi.UnauthorizedException: sublime.error_message(self.ERR_UNAUTHORIZED) sublime.set_timeout(self.get_username, 50) except GitHubApi.UnknownException, e: sublime.error_message(e.message) class OpenGistCommand(BaseGitHubCommand): """ Open a gist. Defaults to all gists and copying it to the clipboard """ MSG_SUCCESS = "Contents of '%s' copied to the clipboard." starred = False open_in_editor = False syntax_file_map = None copy_gist_id = False def run(self, edit): super(OpenGistCommand, self).run(edit) if self.github_token: self.get_gists() else: self.callback = self.get_gists self.get_token() def get_gists(self): try: self.gists = self.gistapi.list_gists(starred=self.starred) format = self.settings.get("gist_list_format") packed_gists = [] for idx, gist in enumerate(self.gists): attribs = {"index": idx + 1, "filename": gist["files"].keys()[0], "description": gist["description"] or ''} if isinstance(format, basestring): item = format % attribs else: item = [(format_str % attribs) for format_str in format] packed_gists.append(item) args = [packed_gists, self.on_done] if self.settings.get("gist_list_monospace"): args.append(sublime.MONOSPACE_FONT) self.view.window().show_quick_panel(*args) except GitHubApi.UnauthorizedException: sublime.error_message(self.ERR_UNAUTHORIZED_TOKEN) sublime.set_timeout(self.get_username, 50) except GitHubApi.UnknownException, e: sublime.error_message(e.message) def on_done(self, idx): if idx == -1: return gist = self.gists[idx] filename = gist["files"].keys()[0] filedata = gist["files"][filename] content = self.gistapi.get(filedata["raw_url"]) if self.open_in_editor: new_view = self.view.window().new_file() if expat: # not present in Linux # set syntax file if not self.syntax_file_map: self.syntax_file_map = self._generate_syntax_file_map() try: extension = os.path.splitext(filename)[1][1:].lower() syntax_file = self.syntax_file_map[extension] new_view.set_syntax_file(syntax_file) except KeyError: logger.warn("no mapping for '%s'" % extension) pass # insert the gist edit = new_view.begin_edit('gist') new_view.insert(edit, 0, content) new_view.end_edit(edit) new_view.set_name(filename) new_view.settings().set('gist', gist) elif self.copy_gist_id: sublime.set_clipboard(gist["html_url"]) else: sublime.set_clipboard(content) sublime.status_message(self.MSG_SUCCESS % filename) @staticmethod def _generate_syntax_file_map(): """ Generate a map of all file types to their syntax files. """ syntax_file_map = {} packages_path = sublime.packages_path() packages = [f for f in os.listdir(packages_path) if os.path.isdir(os.path.join(packages_path, f))] for package in packages: package_dir = os.path.join(packages_path, package) syntax_files = [os.path.join(package_dir, f) for f in os.listdir(package_dir) if f.endswith(".tmLanguage")] for syntax_file in syntax_files: try: plist = plistlib.readPlist(syntax_file) if plist: for file_type in plist['fileTypes']: syntax_file_map[file_type.lower()] = syntax_file except expat.ExpatError: # can't parse logger.warn("could not parse '%s'" % syntax_file) except KeyError: # no file types pass return syntax_file_map class OpenStarredGistCommand(OpenGistCommand): """ Browse starred gists """ starred = True class OpenGistInEditorCommand(OpenGistCommand): """ Open a gist in a new editor. """ open_in_editor = True class OpenGistUrlCommand(OpenGistCommand): """ Open a gist url in a new editor. """ copy_gist_id = True class OpenStarredGistInEditorCommand(OpenGistCommand): """ Open a starred gist in a new editor. """ starred = True open_in_editor = True class OpenGistInBrowserCommand(OpenGistCommand): """ Open a gist in a browser """ def on_done(self, idx): if idx == -1: return gist = self.gists[idx] webbrowser.open(gist["html_url"]) class OpenStarredGistInBrowserCommand(OpenGistInBrowserCommand): """ Open a gist in a browser """ starred = True class GistFromSelectionCommand(BaseGitHubCommand): """ Base class for creating a Github Gist from the current selection. """ MSG_DESCRIPTION = "Gist description:" MSG_FILENAME = "Gist filename:" MSG_SUCCESS = "Gist created and url copied to the clipboard." def run(self, edit): self.description = None self.filename = None super(GistFromSelectionCommand, self).run(edit) if self.github_token: self.get_description() else: self.callback = self.get_description self.get_token() def get_description(self): self.view.window().show_input_panel(self.MSG_DESCRIPTION, "", self.on_done_description, None, None) def get_filename(self): # use the current filename as the default current_filename = self.view.file_name() or "snippet.txt" filename = os.path.basename(current_filename) self.view.window().show_input_panel(self.MSG_FILENAME, filename, self.on_done_filename, None, None) def on_done_description(self, value): "Callback for description show_input_panel." self.description = value # need to do this or the input panel doesn't show sublime.set_timeout(self.get_filename, 50) def on_done_filename(self, value): self.filename = value # get selected text, or the whole file if nothing selected if all([region.empty() for region in self.view.sel()]): text = self.view.substr(sublime.Region(0, self.view.size())) else: text = "\n".join([self.view.substr(region) for region in self.view.sel()]) try: gist = self.gistapi.create_gist(description=self.description, filename=self.filename, content=text, public=self.public) self.view.settings().set('gist', gist) sublime.set_clipboard(gist["html_url"]) sublime.status_message(self.MSG_SUCCESS) except GitHubApi.UnauthorizedException: # clear out the bad token so we can reset it self.settings.set("github_token", "") sublime.save_settings("GitHub.sublime-settings") sublime.error_message(self.ERR_UNAUTHORIZED_TOKEN) sublime.set_timeout(self.get_username, 50) except GitHubApi.UnknownException, e: sublime.error_message(e.message) class PrivateGistFromSelectionCommand(GistFromSelectionCommand): """ Command to create a private Github gist from the current selection. """ public = False class PublicGistFromSelectionCommand(GistFromSelectionCommand): """ Command to create a public Github gist from the current selection. """ public = True class UpdateGistCommand(BaseGitHubCommand): MSG_SUCCESS = "Gist updated and url copied to the clipboard." def run(self, edit): super(UpdateGistCommand, self).run(edit) self.gist = self.view.settings().get('gist') if not self.gist: sublime.error_message("Can't update: this doesn't appear to be a valid gist.") return if self.github_token: self.update() else: self.callback = self.update self.get_token() def update(self): text = self.view.substr(sublime.Region(0, self.view.size())) try: updated_gist = self.gistapi.update_gist(self.gist, text) sublime.set_clipboard(updated_gist["html_url"]) sublime.status_message(self.MSG_SUCCESS) except GitHubApi.UnauthorizedException: # clear out the bad token so we can reset it self.settings.set("github_token", "") sublime.save_settings("GitHub.sublime-settings") sublime.error_message(self.ERR_UNAUTHORIZED_TOKEN) sublime.set_timeout(self.get_username, 50) except GitHubApi.UnknownException, e: sublime.error_message(e.message) class SwitchAccountsCommand(BaseGitHubCommand): def run(self, edit): super(SwitchAccountsCommand, self).run(edit) accounts = self.accounts.keys() self.view.window().show_quick_panel(accounts, self.account_selected) def account_selected(self, index): if index == -1: return # canceled else: self.active_account = self.accounts.keys()[index] self.settings.set("active_account", self.active_account) sublime.save_settings("GitHub.sublime-settings") self.base_uri = self.accounts[self.active_account]["base_uri"] self.github_token = self.accounts[self.active_account]["github_token"] if git: class RemoteUrlCommand(git.GitTextCommand): def run(self, edit): self.run_command("git remote -v".split(), self.done_remote) def done_remote(self, result): remote_origin = [r for r in result.split("\n") if "origin" in r][0] remote_loc = re.split('\s+', remote_origin)[1] repo_url = re.sub('^git@', 'https://', remote_loc) repo_url = re.sub('\.com:', '.com/', repo_url) repo_url = re.sub('\.git$', '', repo_url) self.repo_url = repo_url self.run_command("git rev-parse --abbrev-ref HEAD".split(), self.done_rev_parse) def done_rev_parse(self, result): # get current branch current_branch = result.strip() # get file path within repo repo_name = self.repo_url.split("/").pop() relative_path = self.view.file_name().split(repo_name).pop() self.url = "%s/blob/%s%s" % (self.repo_url, current_branch, relative_path) self.on_done() else: class RemoteUrlCommand(sublime_plugin.TextCommand): def run(self, edit): sublime.error_message("I couldn't find the Git plugin. Please install it, restart Sublime Text, and try again.") class OpenRemoteUrlCommand(RemoteUrlCommand): def run(self, edit): super(OpenRemoteUrlCommand, self).run(edit) def on_done(self): webbrowser.open(self.url) class CopyRemoteUrlCommand(RemoteUrlCommand): def run(self, edit): super(CopyRemoteUrlCommand, self).run(edit) def on_done(self): sublime.set_clipboard(self.url) sublime.status_message("Remote URL copied to clipboard")