feat(SublimeText2.GitPackages): cache packages
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
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")
|
Reference in New Issue
Block a user