feat(SublimeText2.GitPackages): cache packages

This commit is contained in:
Iristyle
2013-04-04 08:55:33 -04:00
parent c3efdad2c2
commit c0f9c6d45a
109 changed files with 15317 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
# editorconfig.org
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

View File

@@ -0,0 +1 @@
* text=auto

View File

@@ -0,0 +1,71 @@
import sublime_plugin
from editorconfig import get_properties, EditorConfigError
LINE_ENDINGS = {
'lf': 'unix',
'crlf': 'windows',
'cr': 'cr'
}
CHARSETS = {
'latin1': 'Western (ISO 8859-1)',
'utf-8': 'utf-8',
'utf-8-bom': 'utf-8 with bom',
'utf-16be': 'utf-16 be',
'utf-16le': 'utf-16 le'
}
class EditorConfig(sublime_plugin.EventListener):
def on_load(self, view):
self.init(view, False)
def on_pre_save(self, view):
self.init(view, True)
def init(self, view, pre_save):
path = view.file_name()
if not path:
return
try:
config = get_properties(path)
except EditorConfigError:
print 'Error occurred while getting EditorConfig properties'
else:
if config:
if pre_save:
self.apply_charset(view, config)
else:
self.apply_config(view, config)
def apply_charset(self, view, config):
charset = config.get('charset')
if charset in CHARSETS:
view.set_encoding(CHARSETS[charset])
def apply_config(self, view, config):
settings = view.settings()
indent_style = config.get('indent_style')
indent_size = config.get('indent_size')
end_of_line = config.get('end_of_line')
trim_trailing_whitespace = config.get('trim_trailing_whitespace')
insert_final_newline = config.get('insert_final_newline')
if indent_style == 'space':
settings.set('translate_tabs_to_spaces', True)
elif indent_style == 'tab':
settings.set('translate_tabs_to_spaces', False)
if indent_size:
try:
settings.set('tab_size', int(indent_size))
except ValueError:
pass
if end_of_line in LINE_ENDINGS:
view.set_line_endings(LINE_ENDINGS[end_of_line])
if trim_trailing_whitespace == 'true':
settings.set('trim_trailing_white_space_on_save', True)
elif trim_trailing_whitespace == 'false':
settings.set('trim_trailing_white_space_on_save', False)
if insert_final_newline == 'true':
settings.set('ensure_newline_at_eof_on_save', True)
elif insert_final_newline == 'false':
settings.set('ensure_newline_at_eof_on_save', False)

View File

@@ -0,0 +1,19 @@
"""EditorConfig Python Core"""
from editorconfig.versiontools import join_version
VERSION = (0, 11, 1, "final")
__all__ = ['get_properties', 'EditorConfigError', 'exceptions']
__version__ = join_version(VERSION)
def get_properties(filename):
"""Locate and parse EditorConfig files for the given filename"""
handler = EditorConfigHandler(filename)
return handler.get_configurations()
from editorconfig.handler import EditorConfigHandler
from editorconfig.exceptions import *

View File

@@ -0,0 +1,18 @@
"""EditorConfig Python2/Python3/Jython compatibility utilities"""
import sys
import types
__all__ = ['slice', 'u']
if sys.version_info[0] == 2:
slice = types.SliceType
else:
slice = slice
if sys.version_info[0] == 2:
import codecs
u = lambda s: codecs.unicode_escape_decode(s)[0]
else:
u = lambda s: s

View File

@@ -0,0 +1,27 @@
"""EditorConfig exception classes
Licensed under PSF License (see LICENSE.txt file).
"""
class EditorConfigError(Exception):
"""Parent class of all exceptions raised by EditorConfig"""
try:
from ConfigParser import ParsingError as _ParsingError
except:
from configparser import ParsingError as _ParsingError
class ParsingError(_ParsingError, EditorConfigError):
"""Error raised if an EditorConfig file could not be parsed"""
class PathError(ValueError, EditorConfigError):
"""Error raised if invalid filepath is specified"""
class VersionError(ValueError, EditorConfigError):
"""Error raised if invalid version number is specified"""

View File

@@ -0,0 +1,126 @@
"""Filename matching with shell patterns.
fnmatch(FILENAME, PATTERN) matches according to the local convention.
fnmatchcase(FILENAME, PATTERN) always takes case in account.
The functions operate by translating the pattern into a regular
expression. They cache the compiled regular expressions for speed.
The function translate(PATTERN) returns a regular expression
corresponding to PATTERN. (It does not compile it.)
Based on code from fnmatch.py file distributed with Python 2.6.
Licensed under PSF License (see LICENSE.txt file).
Changes to original fnmatch module:
- translate function supports ``*`` and ``**`` similarly to fnmatch C library
"""
import os
import re
__all__ = ["fnmatch", "fnmatchcase", "translate"]
_cache = {}
def fnmatch(name, pat):
"""Test whether FILENAME matches PATTERN.
Patterns are Unix shell style:
- ``*`` matches everything except path separator
- ``**`` matches everything
- ``?`` matches any single character
- ``[seq]`` matches any character in seq
- ``[!seq]`` matches any char not in seq
- ``{s1,s2,s3}`` matches any of the strings given (separated by commas)
An initial period in FILENAME is not special.
Both FILENAME and PATTERN are first case-normalized
if the operating system requires it.
If you don't want this, use fnmatchcase(FILENAME, PATTERN).
"""
name = os.path.normcase(name).replace(os.sep, "/")
return fnmatchcase(name, pat)
def fnmatchcase(name, pat):
"""Test whether FILENAME matches PATTERN, including case.
This is a version of fnmatch() which doesn't case-normalize
its arguments.
"""
if not pat in _cache:
res = translate(pat)
_cache[pat] = re.compile(res)
return _cache[pat].match(name) is not None
def translate(pat):
"""Translate a shell PATTERN to a regular expression.
There is no way to quote meta-characters.
"""
i, n = 0, len(pat)
res = ''
escaped = False
while i < n:
c = pat[i]
i = i + 1
if c == '*':
j = i
if j < n and pat[j] == '*':
res = res + '.*'
else:
res = res + '[^/]*'
elif c == '?':
res = res + '.'
elif c == '[':
j = i
if j < n and pat[j] == '!':
j = j + 1
if j < n and pat[j] == ']':
j = j + 1
while j < n and (pat[j] != ']' or escaped):
escaped = pat[j] == '\\' and not escaped
j = j + 1
if j >= n:
res = res + '\\['
else:
stuff = pat[i:j]
i = j + 1
if stuff[0] == '!':
stuff = '^' + stuff[1:]
elif stuff[0] == '^':
stuff = '\\' + stuff
res = '%s[%s]' % (res, stuff)
elif c == '{':
j = i
groups = []
while j < n and pat[j] != '}':
k = j
while k < n and (pat[k] not in (',', '}') or escaped):
escaped = pat[k] == '\\' and not escaped
k = k + 1
group = pat[j:k]
for char in (',', '}', '\\'):
group = group.replace('\\' + char, char)
groups.append(group)
j = k
if j < n and pat[j] == ',':
j = j + 1
if j < n and pat[j] == '}':
groups.append('')
if j >= n or len(groups) < 2:
res = res + '\\{'
else:
res = '%s(%s)' % (res, '|'.join(map(re.escape, groups)))
i = j + 1
else:
res = res + re.escape(c)
return res + '\Z(?ms)'

View File

@@ -0,0 +1,125 @@
"""EditorConfig file handler
Provides ``EditorConfigHandler`` class for locating and parsing
EditorConfig files relevant to a given filepath.
Licensed under PSF License (see LICENSE.txt file).
"""
import os
from editorconfig import VERSION
from editorconfig.ini import EditorConfigParser
from editorconfig.exceptions import PathError, VersionError
__all__ = ['EditorConfigHandler']
def get_filenames(path, filename):
"""Yield full filepath for filename in each directory in and above path"""
path_list = []
while True:
path_list.append(os.path.join(path, filename))
newpath = os.path.dirname(path)
if path == newpath:
break
path = newpath
return path_list
class EditorConfigHandler(object):
"""
Allows locating and parsing of EditorConfig files for given filename
In addition to the constructor a single public method is provided,
``get_configurations`` which returns the EditorConfig options for
the ``filepath`` specified to the constructor.
"""
def __init__(self, filepath, conf_filename='.editorconfig', version=None):
"""Create EditorConfigHandler for matching given filepath"""
self.filepath = filepath
self.conf_filename = conf_filename
self.version = version
self.options = None
def get_configurations(self):
"""
Find EditorConfig files and return all options matching filepath
Special exceptions that may be raised by this function include:
- ``VersionError``: self.version is invalid EditorConfig version
- ``PathError``: self.filepath is not a valid absolute filepath
- ``ParsingError``: improperly formatted EditorConfig file found
"""
self.check_assertions()
path, filename = os.path.split(self.filepath)
conf_files = get_filenames(path, self.conf_filename)
# Attempt to find and parse every EditorConfig file in filetree
for filename in conf_files:
parser = EditorConfigParser(self.filepath)
parser.read(filename)
# Merge new EditorConfig file's options into current options
old_options = self.options
self.options = parser.options
if old_options:
self.options.update(old_options)
# Stop parsing if parsed file has a ``root = true`` option
if parser.root_file:
break
self.preprocess_values()
return self.options
def check_assertions(self):
"""Raise error if filepath or version have invalid values"""
# Raise ``PathError`` if filepath isn't an absolute path
if not os.path.isabs(self.filepath):
raise PathError("Input file must be a full path name.")
# Raise ``VersionError`` if version specified is greater than current
if self.version is not None and self.version[:3] > VERSION[:3]:
raise VersionError(
"Required version is greater than the current version.")
def preprocess_values(self):
"""Preprocess option values for consumption by plugins"""
opts = self.options
# Lowercase option value for certain options
for name in ["end_of_line", "indent_style", "indent_size",
"insert_final_newline", "trim_trailing_whitespace", "charset"]:
if name in opts:
opts[name] = opts[name].lower()
# Set indent_size to "tab" if indent_size is unspecified and
# indent_style is set to "tab".
if (opts.get("indent_style") == "tab" and
not "indent_size" in opts and self.version >= VERSION[:3]):
opts["indent_size"] = "tab"
# Set tab_width to indent_size if indent_size is specified and
# tab_width is unspecified
if ("indent_size" in opts and "tab_width" not in opts and
opts["indent_size"] != "tab"):
opts["tab_width"] = opts["indent_size"]
# Set indent_size to tab_width if indent_size is "tab"
if ("indent_size" in opts and "tab_width" in opts and
opts["indent_size"] == "tab"):
opts["indent_size"] = opts["tab_width"]

View File

@@ -0,0 +1,150 @@
"""EditorConfig file parser
Based on code from ConfigParser.py file distributed with Python 2.6.
Licensed under PSF License (see LICENSE.txt file).
Changes to original ConfigParser:
- Special characters can be used in section names
- Octothorpe can be used for comments (not just at beginning of line)
- Only track INI options in sections that match target filename
- Stop parsing files with when ``root = true`` is found
"""
import re
from codecs import open
import posixpath
from os import sep
from os.path import normcase, dirname
from editorconfig.exceptions import ParsingError
from editorconfig.fnmatch import fnmatch
from editorconfig.odict import OrderedDict
from editorconfig.compat import u
__all__ = ["ParsingError", "EditorConfigParser"]
class EditorConfigParser(object):
"""Parser for EditorConfig-style configuration files
Based on RawConfigParser from ConfigParser.py in Python 2.6.
"""
# Regular expressions for parsing section headers and options.
# Allow ``]`` and escaped ``;`` and ``#`` characters in section headers
SECTCRE = re.compile(
r'\s*\[' # [
r'(?P<header>([^#;]|\\#|\\;)+)' # very permissive!
r'\]' # ]
)
# Regular expression for parsing option name/values.
# Allow any amount of whitespaces, followed by separator
# (either ``:`` or ``=``), followed by any amount of whitespace and then
# any characters to eol
OPTCRE = re.compile(
r'\s*(?P<option>[^:=\s][^:=]*)'
r'\s*(?P<vi>[:=])\s*'
r'(?P<value>.*)$'
)
def __init__(self, filename):
self.filename = filename
self.options = OrderedDict()
self.root_file = False
def matches_filename(self, config_filename, glob):
"""Return True if section glob matches filename"""
config_dirname = normcase(dirname(config_filename)).replace(sep, '/')
glob = glob.replace("\\#", "#")
glob = glob.replace("\\;", ";")
if '/' in glob:
if glob.find('/') == 0:
glob = glob[1:]
glob = posixpath.join(config_dirname, glob)
else:
glob = posixpath.join('**/', glob)
return fnmatch(self.filename, glob)
def read(self, filename):
"""Read and parse single EditorConfig file"""
try:
fp = open(filename, encoding='utf-8')
except IOError:
return
self._read(fp, filename)
fp.close()
def _read(self, fp, fpname):
"""Parse a sectioned setup file.
The sections in setup file contains a title line at the top,
indicated by a name in square brackets (`[]'), plus key/value
options lines, indicated by `name: value' format lines.
Continuations are represented by an embedded newline then
leading whitespace. Blank lines, lines beginning with a '#',
and just about everything else are ignored.
"""
in_section = False
matching_section = False
optname = None
lineno = 0
e = None # None, or an exception
while True:
line = fp.readline()
if not line:
break
if lineno == 0 and line.startswith(u('\ufeff')):
line = line[1:] # Strip UTF-8 BOM
lineno = lineno + 1
# comment or blank line?
if line.strip() == '' or line[0] in '#;':
continue
# a section header or option header?
else:
# is it a section header?
mo = self.SECTCRE.match(line)
if mo:
sectname = mo.group('header')
in_section = True
matching_section = self.matches_filename(fpname, sectname)
# So sections can't start with a continuation line
optname = None
# an option line?
else:
mo = self.OPTCRE.match(line)
if mo:
optname, vi, optval = mo.group('option', 'vi', 'value')
if ';' in optval or '#' in optval:
# ';' and '#' are comment delimiters only if
# preceeded by a spacing character
m = re.search('(.*?) [;#]', optval)
if m:
optval = m.group(1)
optval = optval.strip()
# allow empty values
if optval == '""':
optval = ''
optname = self.optionxform(optname.rstrip())
if not in_section and optname == 'root':
self.root_file = (optval.lower() == 'true')
if matching_section:
self.options[optname] = optval
else:
# a non-fatal parsing error occurred. set up the
# exception but keep going. the exception will be
# raised at the end of the file and will contain a
# list of all bogus lines
if not e:
e = ParsingError(fpname)
e.append(lineno, repr(line))
# if any parsing errors occurred, raise an exception
if e:
raise e
def optionxform(self, optionstr):
return optionstr.lower()

View File

@@ -0,0 +1,76 @@
"""EditorConfig command line interface
Licensed under PSF License (see LICENSE.txt file).
"""
import getopt
import sys
from editorconfig import __version__, VERSION
from editorconfig.versiontools import split_version
from editorconfig.handler import EditorConfigHandler
from editorconfig.exceptions import ParsingError, PathError, VersionError
def version():
print("Version %s" % __version__)
def usage(command, error=False):
if error:
out = sys.stderr
else:
out = sys.stdout
out.write("%s [OPTIONS] FILENAME\n" % command)
out.write('-f '
'Specify conf filename other than ".editorconfig".\n')
out.write("-b "
"Specify version (used by devs to test compatibility).\n")
out.write("-h OR --help Print this help message.\n")
out.write("-v OR --version Display version information.\n")
def main():
command_name = sys.argv[0]
try:
opts, args = getopt.getopt(sys.argv[1:], "vhb:f:", ["version", "help"])
except getopt.GetoptError:
print(str(sys.exc_info()[1])) # For Python 2/3 compatibility
usage(command_name, error=True)
sys.exit(2)
version_tuple = VERSION
conf_filename = '.editorconfig'
for option, arg in opts:
if option in ('-h', '--help'):
usage(command_name)
sys.exit()
if option in ('-v', '--version'):
version()
sys.exit()
if option == '-f':
conf_filename = arg
if option == '-b':
version_tuple = split_version(arg)
if version_tuple is None:
sys.exit("Invalid version number: %s" % arg)
if len(args) < 1:
usage(command_name, error=True)
sys.exit(2)
filenames = args
multiple_files = len(args) > 1
for filename in filenames:
handler = EditorConfigHandler(filename, conf_filename, version_tuple)
try:
options = handler.get_configurations()
except (ParsingError, PathError, VersionError):
print(str(sys.exc_info()[1])) # For Python 2/3 compatibility
sys.exit(2)
if multiple_files:
print("[%s]" % filename)
for key, value in options.items():
print("%s=%s" % (key, value))

View File

@@ -0,0 +1,899 @@
"""odict.py: An Ordered Dictionary object"""
# Copyright (C) 2005 Nicola Larosa, Michael Foord
# E-mail: nico AT tekNico DOT net, fuzzyman AT voidspace DOT org DOT uk
# Copyright (c) 2003-2010, Michael Foord
# E-mail : fuzzyman AT voidspace DOT org DOT uk
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# * Neither the name of Michael Foord nor the name of Voidspace
# may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import generators
import sys
import warnings
from editorconfig.compat import slice
__docformat__ = "restructuredtext en"
__all__ = ['OrderedDict']
INTP_VER = sys.version_info[:2]
if INTP_VER < (2, 2):
raise RuntimeError("Python v.2.2 or later required")
class OrderedDict(dict):
"""
A class of dictionary that keeps the insertion order of keys.
All appropriate methods return keys, items, or values in an ordered way.
All normal dictionary methods are available. Update and comparison is
restricted to other OrderedDict objects.
Various sequence methods are available, including the ability to explicitly
mutate the key ordering.
__contains__ tests:
>>> d = OrderedDict(((1, 3),))
>>> 1 in d
1
>>> 4 in d
0
__getitem__ tests:
>>> OrderedDict(((1, 3), (3, 2), (2, 1)))[2]
1
>>> OrderedDict(((1, 3), (3, 2), (2, 1)))[4]
Traceback (most recent call last):
KeyError: 4
__len__ tests:
>>> len(OrderedDict())
0
>>> len(OrderedDict(((1, 3), (3, 2), (2, 1))))
3
get tests:
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.get(1)
3
>>> d.get(4) is None
1
>>> d.get(4, 5)
5
>>> d
OrderedDict([(1, 3), (3, 2), (2, 1)])
has_key tests:
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.has_key(1)
1
>>> d.has_key(4)
0
"""
def __init__(self, init_val=(), strict=False):
"""
Create a new ordered dictionary. Cannot init from a normal dict,
nor from kwargs, since items order is undefined in those cases.
If the ``strict`` keyword argument is ``True`` (``False`` is the
default) then when doing slice assignment - the ``OrderedDict`` you are
assigning from *must not* contain any keys in the remaining dict.
>>> OrderedDict()
OrderedDict([])
>>> OrderedDict({1: 1})
Traceback (most recent call last):
TypeError: undefined order, cannot get items from dict
>>> OrderedDict({1: 1}.items())
OrderedDict([(1, 1)])
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d
OrderedDict([(1, 3), (3, 2), (2, 1)])
>>> OrderedDict(d)
OrderedDict([(1, 3), (3, 2), (2, 1)])
"""
self.strict = strict
dict.__init__(self)
if isinstance(init_val, OrderedDict):
self._sequence = init_val.keys()
dict.update(self, init_val)
elif isinstance(init_val, dict):
# we lose compatibility with other ordered dict types this way
raise TypeError('undefined order, cannot get items from dict')
else:
self._sequence = []
self.update(init_val)
### Special methods ###
def __delitem__(self, key):
"""
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> del d[3]
>>> d
OrderedDict([(1, 3), (2, 1)])
>>> del d[3]
Traceback (most recent call last):
KeyError: 3
>>> d[3] = 2
>>> d
OrderedDict([(1, 3), (2, 1), (3, 2)])
>>> del d[0:1]
>>> d
OrderedDict([(2, 1), (3, 2)])
"""
if isinstance(key, slice):
# FIXME: efficiency?
keys = self._sequence[key]
for entry in keys:
dict.__delitem__(self, entry)
del self._sequence[key]
else:
# do the dict.__delitem__ *first* as it raises
# the more appropriate error
dict.__delitem__(self, key)
self._sequence.remove(key)
def __eq__(self, other):
"""
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d == OrderedDict(d)
True
>>> d == OrderedDict(((1, 3), (2, 1), (3, 2)))
False
>>> d == OrderedDict(((1, 0), (3, 2), (2, 1)))
False
>>> d == OrderedDict(((0, 3), (3, 2), (2, 1)))
False
>>> d == dict(d)
False
>>> d == False
False
"""
if isinstance(other, OrderedDict):
# FIXME: efficiency?
# Generate both item lists for each compare
return (self.items() == other.items())
else:
return False
def __lt__(self, other):
"""
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
>>> c < d
True
>>> d < c
False
>>> d < dict(c)
Traceback (most recent call last):
TypeError: Can only compare with other OrderedDicts
"""
if not isinstance(other, OrderedDict):
raise TypeError('Can only compare with other OrderedDicts')
# FIXME: efficiency?
# Generate both item lists for each compare
return (self.items() < other.items())
def __le__(self, other):
"""
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
>>> e = OrderedDict(d)
>>> c <= d
True
>>> d <= c
False
>>> d <= dict(c)
Traceback (most recent call last):
TypeError: Can only compare with other OrderedDicts
>>> d <= e
True
"""
if not isinstance(other, OrderedDict):
raise TypeError('Can only compare with other OrderedDicts')
# FIXME: efficiency?
# Generate both item lists for each compare
return (self.items() <= other.items())
def __ne__(self, other):
"""
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d != OrderedDict(d)
False
>>> d != OrderedDict(((1, 3), (2, 1), (3, 2)))
True
>>> d != OrderedDict(((1, 0), (3, 2), (2, 1)))
True
>>> d == OrderedDict(((0, 3), (3, 2), (2, 1)))
False
>>> d != dict(d)
True
>>> d != False
True
"""
if isinstance(other, OrderedDict):
# FIXME: efficiency?
# Generate both item lists for each compare
return not (self.items() == other.items())
else:
return True
def __gt__(self, other):
"""
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
>>> d > c
True
>>> c > d
False
>>> d > dict(c)
Traceback (most recent call last):
TypeError: Can only compare with other OrderedDicts
"""
if not isinstance(other, OrderedDict):
raise TypeError('Can only compare with other OrderedDicts')
# FIXME: efficiency?
# Generate both item lists for each compare
return (self.items() > other.items())
def __ge__(self, other):
"""
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
>>> e = OrderedDict(d)
>>> c >= d
False
>>> d >= c
True
>>> d >= dict(c)
Traceback (most recent call last):
TypeError: Can only compare with other OrderedDicts
>>> e >= d
True
"""
if not isinstance(other, OrderedDict):
raise TypeError('Can only compare with other OrderedDicts')
# FIXME: efficiency?
# Generate both item lists for each compare
return (self.items() >= other.items())
def __repr__(self):
"""
Used for __repr__ and __str__
>>> r1 = repr(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f'))))
>>> r1
"OrderedDict([('a', 'b'), ('c', 'd'), ('e', 'f')])"
>>> r2 = repr(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd'))))
>>> r2
"OrderedDict([('a', 'b'), ('e', 'f'), ('c', 'd')])"
>>> r1 == str(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f'))))
True
>>> r2 == str(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd'))))
True
"""
return '%s([%s])' % (self.__class__.__name__, ', '.join(
['(%r, %r)' % (key, self[key]) for key in self._sequence]))
def __setitem__(self, key, val):
"""
Allows slice assignment, so long as the slice is an OrderedDict
>>> d = OrderedDict()
>>> d['a'] = 'b'
>>> d['b'] = 'a'
>>> d[3] = 12
>>> d
OrderedDict([('a', 'b'), ('b', 'a'), (3, 12)])
>>> d[:] = OrderedDict(((1, 2), (2, 3), (3, 4)))
>>> d
OrderedDict([(1, 2), (2, 3), (3, 4)])
>>> d[::2] = OrderedDict(((7, 8), (9, 10)))
>>> d
OrderedDict([(7, 8), (2, 3), (9, 10)])
>>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4)))
>>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8)))
>>> d
OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)])
>>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4)), strict=True)
>>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8)))
>>> d
OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)])
>>> a = OrderedDict(((0, 1), (1, 2), (2, 3)), strict=True)
>>> a[3] = 4
>>> a
OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> a
OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)])
Traceback (most recent call last):
ValueError: slice assignment must be from unique keys
>>> a = OrderedDict(((0, 1), (1, 2), (2, 3)))
>>> a[3] = 4
>>> a
OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> a
OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> a
OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> a[::-1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> a
OrderedDict([(3, 4), (2, 3), (1, 2), (0, 1)])
>>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> d[:1] = 3
Traceback (most recent call last):
TypeError: slice assignment requires an OrderedDict
>>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> d[:1] = OrderedDict([(9, 8)])
>>> d
OrderedDict([(9, 8), (1, 2), (2, 3), (3, 4)])
"""
if isinstance(key, slice):
if not isinstance(val, OrderedDict):
# FIXME: allow a list of tuples?
raise TypeError('slice assignment requires an OrderedDict')
keys = self._sequence[key]
# NOTE: Could use ``range(*key.indices(len(self._sequence)))``
indexes = range(len(self._sequence))[key]
if key.step is None:
# NOTE: new slice may not be the same size as the one being
# overwritten !
# NOTE: What is the algorithm for an impossible slice?
# e.g. d[5:3]
pos = key.start or 0
del self[key]
newkeys = val.keys()
for k in newkeys:
if k in self:
if self.strict:
raise ValueError('slice assignment must be from '
'unique keys')
else:
# NOTE: This removes duplicate keys *first*
# so start position might have changed?
del self[k]
self._sequence = (self._sequence[:pos] + newkeys +
self._sequence[pos:])
dict.update(self, val)
else:
# extended slice - length of new slice must be the same
# as the one being replaced
if len(keys) != len(val):
raise ValueError('attempt to assign sequence of size %s '
'to extended slice of size %s' % (len(val), len(keys)))
# FIXME: efficiency?
del self[key]
item_list = zip(indexes, val.items())
# smallest indexes first - higher indexes not guaranteed to
# exist
item_list.sort()
for pos, (newkey, newval) in item_list:
if self.strict and newkey in self:
raise ValueError('slice assignment must be from unique'
' keys')
self.insert(pos, newkey, newval)
else:
if key not in self:
self._sequence.append(key)
dict.__setitem__(self, key, val)
def __getitem__(self, key):
"""
Allows slicing. Returns an OrderedDict if you slice.
>>> b = OrderedDict([(7, 0), (6, 1), (5, 2), (4, 3), (3, 4), (2, 5), (1, 6)])
>>> b[::-1]
OrderedDict([(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1), (7, 0)])
>>> b[2:5]
OrderedDict([(5, 2), (4, 3), (3, 4)])
>>> type(b[2:4])
<class '__main__.OrderedDict'>
"""
if isinstance(key, slice):
# FIXME: does this raise the error we want?
keys = self._sequence[key]
# FIXME: efficiency?
return OrderedDict([(entry, self[entry]) for entry in keys])
else:
return dict.__getitem__(self, key)
__str__ = __repr__
def __setattr__(self, name, value):
"""
Implemented so that accesses to ``sequence`` raise a warning and are
diverted to the new ``setkeys`` method.
"""
if name == 'sequence':
warnings.warn('Use of the sequence attribute is deprecated.'
' Use the keys method instead.', DeprecationWarning)
# NOTE: doesn't return anything
self.setkeys(value)
else:
# FIXME: do we want to allow arbitrary setting of attributes?
# Or do we want to manage it?
object.__setattr__(self, name, value)
def __getattr__(self, name):
"""
Implemented so that access to ``sequence`` raises a warning.
>>> d = OrderedDict()
>>> d.sequence
[]
"""
if name == 'sequence':
warnings.warn('Use of the sequence attribute is deprecated.'
' Use the keys method instead.', DeprecationWarning)
# NOTE: Still (currently) returns a direct reference. Need to
# because code that uses sequence will expect to be able to
# mutate it in place.
return self._sequence
else:
# raise the appropriate error
raise AttributeError("OrderedDict has no '%s' attribute" % name)
def __deepcopy__(self, memo):
"""
To allow deepcopy to work with OrderedDict.
>>> from copy import deepcopy
>>> a = OrderedDict([(1, 1), (2, 2), (3, 3)])
>>> a['test'] = {}
>>> b = deepcopy(a)
>>> b == a
True
>>> b is a
False
>>> a['test'] is b['test']
False
"""
from copy import deepcopy
return self.__class__(deepcopy(self.items(), memo), self.strict)
### Read-only methods ###
def copy(self):
"""
>>> OrderedDict(((1, 3), (3, 2), (2, 1))).copy()
OrderedDict([(1, 3), (3, 2), (2, 1)])
"""
return OrderedDict(self)
def items(self):
"""
``items`` returns a list of tuples representing all the
``(key, value)`` pairs in the dictionary.
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.items()
[(1, 3), (3, 2), (2, 1)]
>>> d.clear()
>>> d.items()
[]
"""
return zip(self._sequence, self.values())
def keys(self):
"""
Return a list of keys in the ``OrderedDict``.
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.keys()
[1, 3, 2]
"""
return self._sequence[:]
def values(self, values=None):
"""
Return a list of all the values in the OrderedDict.
Optionally you can pass in a list of values, which will replace the
current list. The value list must be the same len as the OrderedDict.
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.values()
[3, 2, 1]
"""
return [self[key] for key in self._sequence]
def iteritems(self):
"""
>>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iteritems()
>>> ii.next()
(1, 3)
>>> ii.next()
(3, 2)
>>> ii.next()
(2, 1)
>>> ii.next()
Traceback (most recent call last):
StopIteration
"""
def make_iter(self=self):
keys = self.iterkeys()
while True:
key = keys.next()
yield (key, self[key])
return make_iter()
def iterkeys(self):
"""
>>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iterkeys()
>>> ii.next()
1
>>> ii.next()
3
>>> ii.next()
2
>>> ii.next()
Traceback (most recent call last):
StopIteration
"""
return iter(self._sequence)
__iter__ = iterkeys
def itervalues(self):
"""
>>> iv = OrderedDict(((1, 3), (3, 2), (2, 1))).itervalues()
>>> iv.next()
3
>>> iv.next()
2
>>> iv.next()
1
>>> iv.next()
Traceback (most recent call last):
StopIteration
"""
def make_iter(self=self):
keys = self.iterkeys()
while True:
yield self[keys.next()]
return make_iter()
### Read-write methods ###
def clear(self):
"""
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.clear()
>>> d
OrderedDict([])
"""
dict.clear(self)
self._sequence = []
def pop(self, key, *args):
"""
No dict.pop in Python 2.2, gotta reimplement it
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.pop(3)
2
>>> d
OrderedDict([(1, 3), (2, 1)])
>>> d.pop(4)
Traceback (most recent call last):
KeyError: 4
>>> d.pop(4, 0)
0
>>> d.pop(4, 0, 1)
Traceback (most recent call last):
TypeError: pop expected at most 2 arguments, got 3
"""
if len(args) > 1:
raise TypeError('pop expected at most 2 arguments, got %s' %
(len(args) + 1))
if key in self:
val = self[key]
del self[key]
else:
try:
val = args[0]
except IndexError:
raise KeyError(key)
return val
def popitem(self, i=-1):
"""
Delete and return an item specified by index, not a random one as in
dict. The index is -1 by default (the last item).
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.popitem()
(2, 1)
>>> d
OrderedDict([(1, 3), (3, 2)])
>>> d.popitem(0)
(1, 3)
>>> OrderedDict().popitem()
Traceback (most recent call last):
KeyError: 'popitem(): dictionary is empty'
>>> d.popitem(2)
Traceback (most recent call last):
IndexError: popitem(): index 2 not valid
"""
if not self._sequence:
raise KeyError('popitem(): dictionary is empty')
try:
key = self._sequence[i]
except IndexError:
raise IndexError('popitem(): index %s not valid' % i)
return (key, self.pop(key))
def setdefault(self, key, defval=None):
"""
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.setdefault(1)
3
>>> d.setdefault(4) is None
True
>>> d
OrderedDict([(1, 3), (3, 2), (2, 1), (4, None)])
>>> d.setdefault(5, 0)
0
>>> d
OrderedDict([(1, 3), (3, 2), (2, 1), (4, None), (5, 0)])
"""
if key in self:
return self[key]
else:
self[key] = defval
return defval
def update(self, from_od):
"""
Update from another OrderedDict or sequence of (key, value) pairs
>>> d = OrderedDict(((1, 0), (0, 1)))
>>> d.update(OrderedDict(((1, 3), (3, 2), (2, 1))))
>>> d
OrderedDict([(1, 3), (0, 1), (3, 2), (2, 1)])
>>> d.update({4: 4})
Traceback (most recent call last):
TypeError: undefined order, cannot get items from dict
>>> d.update((4, 4))
Traceback (most recent call last):
TypeError: cannot convert dictionary update sequence element "4" to a 2-item sequence
"""
if isinstance(from_od, OrderedDict):
for key, val in from_od.items():
self[key] = val
elif isinstance(from_od, dict):
# we lose compatibility with other ordered dict types this way
raise TypeError('undefined order, cannot get items from dict')
else:
# FIXME: efficiency?
# sequence of 2-item sequences, or error
for item in from_od:
try:
key, val = item
except TypeError:
raise TypeError('cannot convert dictionary update'
' sequence element "%s" to a 2-item sequence' % item)
self[key] = val
def rename(self, old_key, new_key):
"""
Rename the key for a given value, without modifying sequence order.
For the case where new_key already exists this raise an exception,
since if new_key exists, it is ambiguous as to what happens to the
associated values, and the position of new_key in the sequence.
>>> od = OrderedDict()
>>> od['a'] = 1
>>> od['b'] = 2
>>> od.items()
[('a', 1), ('b', 2)]
>>> od.rename('b', 'c')
>>> od.items()
[('a', 1), ('c', 2)]
>>> od.rename('c', 'a')
Traceback (most recent call last):
ValueError: New key already exists: 'a'
>>> od.rename('d', 'b')
Traceback (most recent call last):
KeyError: 'd'
"""
if new_key == old_key:
# no-op
return
if new_key in self:
raise ValueError("New key already exists: %r" % new_key)
# rename sequence entry
value = self[old_key]
old_idx = self._sequence.index(old_key)
self._sequence[old_idx] = new_key
# rename internal dict entry
dict.__delitem__(self, old_key)
dict.__setitem__(self, new_key, value)
def setitems(self, items):
"""
This method allows you to set the items in the dict.
It takes a list of tuples - of the same sort returned by the ``items``
method.
>>> d = OrderedDict()
>>> d.setitems(((3, 1), (2, 3), (1, 2)))
>>> d
OrderedDict([(3, 1), (2, 3), (1, 2)])
"""
self.clear()
# FIXME: this allows you to pass in an OrderedDict as well :-)
self.update(items)
def setkeys(self, keys):
"""
``setkeys`` all ows you to pass in a new list of keys which will
replace the current set. This must contain the same set of keys, but
need not be in the same order.
If you pass in new keys that don't match, a ``KeyError`` will be
raised.
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.keys()
[1, 3, 2]
>>> d.setkeys((1, 2, 3))
>>> d
OrderedDict([(1, 3), (2, 1), (3, 2)])
>>> d.setkeys(['a', 'b', 'c'])
Traceback (most recent call last):
KeyError: 'Keylist is not the same as current keylist.'
"""
# FIXME: Efficiency? (use set for Python 2.4 :-)
# NOTE: list(keys) rather than keys[:] because keys[:] returns
# a tuple, if keys is a tuple.
kcopy = list(keys)
kcopy.sort()
self._sequence.sort()
if kcopy != self._sequence:
raise KeyError('Keylist is not the same as current keylist.')
# NOTE: This makes the _sequence attribute a new object, instead
# of changing it in place.
# FIXME: efficiency?
self._sequence = list(keys)
def setvalues(self, values):
"""
You can pass in a list of values, which will replace the
current list. The value list must be the same len as the OrderedDict.
(Or a ``ValueError`` is raised.)
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.setvalues((1, 2, 3))
>>> d
OrderedDict([(1, 1), (3, 2), (2, 3)])
>>> d.setvalues([6])
Traceback (most recent call last):
ValueError: Value list is not the same length as the OrderedDict.
"""
if len(values) != len(self):
# FIXME: correct error to raise?
raise ValueError('Value list is not the same length as the '
'OrderedDict.')
self.update(zip(self, values))
### Sequence Methods ###
def index(self, key):
"""
Return the position of the specified key in the OrderedDict.
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.index(3)
1
>>> d.index(4)
Traceback (most recent call last):
ValueError: 4 is not in list
"""
return self._sequence.index(key)
def insert(self, index, key, value):
"""
Takes ``index``, ``key``, and ``value`` as arguments.
Sets ``key`` to ``value``, so that ``key`` is at position ``index`` in
the OrderedDict.
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.insert(0, 4, 0)
>>> d
OrderedDict([(4, 0), (1, 3), (3, 2), (2, 1)])
>>> d.insert(0, 2, 1)
>>> d
OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2)])
>>> d.insert(8, 8, 1)
>>> d
OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2), (8, 1)])
"""
if key in self:
# FIXME: efficiency?
del self[key]
self._sequence.insert(index, key)
dict.__setitem__(self, key, value)
def reverse(self):
"""
Reverse the order of the OrderedDict.
>>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
>>> d.reverse()
>>> d
OrderedDict([(2, 1), (3, 2), (1, 3)])
"""
self._sequence.reverse()
def sort(self, *args, **kwargs):
"""
Sort the key order in the OrderedDict.
This method takes the same arguments as the ``list.sort`` method on
your version of Python.
>>> d = OrderedDict(((4, 1), (2, 2), (3, 3), (1, 4)))
>>> d.sort()
>>> d
OrderedDict([(1, 4), (2, 2), (3, 3), (4, 1)])
"""
self._sequence.sort(*args, **kwargs)
if __name__ == '__main__':
# turn off warnings for tests
warnings.filterwarnings('ignore')
# run the code tests in doctest format
import doctest
m = sys.modules.get('__main__')
globs = m.__dict__.copy()
globs.update({
'INTP_VER': INTP_VER,
})
doctest.testmod(m, globs=globs)

View File

@@ -0,0 +1,35 @@
"""EditorConfig version tools
Provides ``join_version`` and ``split_version`` classes for converting
__version__ strings to VERSION tuples and vice versa.
"""
import re
__all__ = ['join_version', 'split_version']
_version_re = re.compile(r'^(\d+)\.(\d+)\.(\d+)(\..*)?$', re.VERBOSE)
def join_version(version_tuple):
"""Return a string representation of version from given VERSION tuple"""
version = "%s.%s.%s" % version_tuple[:3]
if version_tuple[3] != "final":
version += "-%s" % version_tuple[3]
return version
def split_version(version):
"""Return VERSION tuple for given string representation of version"""
match = _version_re.search(version)
if not match:
return None
else:
split_version = list(match.groups())
if split_version[3] is None:
split_version[3] = "final"
split_version = list(map(int, split_version[:3])) + split_version[3:]
return tuple(split_version)

View File

@@ -0,0 +1 @@
{"url": "http://sindresorhus.com", "version": "2013.03.18.18.13.22", "description": "Sublime Text plugin for EditorConfig - helps developers define and maintain consistent coding styles between different editors and IDEs"}

View File

@@ -0,0 +1,74 @@
# EditorConfig - Sublime Text plugin
> [EditorConfig](http://editorconfig.org) helps developers define and maintain consistent coding styles between different editors and IDEs. The EditorConfig project consists of a file format for defining coding styles and a collection of text editor plugins that enable editors to read the file format and adhere to defined styles. EditorConfig files are easily readibly and they work nicely with version control systems.
## Install
### Sublime Text 2
Install with [Package Control](http://wbond.net/sublime_packages/package_control)
### Sublime Text 3
[Download](https://github.com/sindresorhus/editorconfig-sublime/archive/st3.zip), unzip, and put the contents in `~/Library/Application Support/Sublime Text 3/Packages/EditorConfig`.
Will be easier when Package Control is fully compatible.
## Getting started
See the [EditorConfig site][] for documentation.
## Supported properties
- root
- indent_style
- indent_size
- end\_of\_line
- charset
- trim_trailing_whitespace
- insert_final_newline
Explanation of the properties can be found on the [EditorConfig site][].
## Example file
*My recommended default settings*
```ini
# editorconfig.org
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
```
## Tips
### Trailing whitespace
Even though there is a `trim_trailing_whitespace` property. I would still recommend you set `"draw_white_space": "all"` in your Sublime preferences to prevent you from accidentally committing whitespace garbage whenever a project is missing a .editorconfig file.
### Show changes
This plugin does its changes transparently in the background. I would recommend the excellent [Modific](https://github.com/gornostal/Modific) plugin if you would like to see what changed.
## License
[MIT License](http://en.wikipedia.org/wiki/MIT_License)
(c) [Sindre Sorhus](http://sindresorhus.com)
[EditorConfig site]: http://editorconfig.org

View File

@@ -0,0 +1,4 @@
*.pyc
*.tmLanguage.cache
.DS_Store
package-metadata.json

View File

@@ -0,0 +1,28 @@
These are the people who helped make this plugin:
David Lynch <kemayo@gmail.com>
Sheldon Els <sheldon.els@gmail.com>
Nick Fisher <spadgos@gmail.com>
Can Yilmaz <can@potatolondon.com>
Stefan Buhrmester <buhrmi@gmail.com>
Rafal Chlodnicki <rchlodnicki@opera.com>
Daniël de Kok <me@danieldk.eu>
David Baumgold <david@davidbaumgold.com>
Iuri de Silvio <iurisilvio@gmail.com>
joshuacc <josh@designpepper.com>
misfo <tedwardo2@gmail.com>
Kevin Smith <kevin@ilovecode.de>
Κώστας Καραχάλιος <kostas.karachalios@me.com>
Dominique Wahli <dominique.wahli@solvaxis.com>
Fraser Graham <frasergraham@me.com>
Hamid Nazari <hamidnazari@ymail.com>
Jeff Sandberg <paradox460@gmail.com>
Joshua Clanton <joshua.clanton@gmail.com>
Maxim Sukharev <max@smacker.ru>
Niklas Hambüchen <mail@nh2.me>
Patrik Ring <me@patrikring.se>
Scott Bowers <sbbowers@gmail.com>
Weslly Honorato <weslly.honorato@gmail.com>
brcooley <brcooley@cs.wm.edu>
jdc0589 <jdc0589@gmail.com>
Adam Venturella <aventurella@gmail.com>

View File

@@ -0,0 +1,213 @@
[
{ "caption": "Git: Init",
"command": "git_init"
}
,{
"caption": "Git: Blame",
"command": "git_blame"
}
,{
"caption": "Git: New Tag",
"command": "git_new_tag"
}
,{
"caption": "Git: Show Tags",
"command": "git_show_tags"
}
,{
"caption": "Git: Push Tags",
"command": "git_push_tags"
}
,{
"caption": "Git: Log Current File",
"command": "git_log"
}
,{
"caption": "Git: Log All",
"command": "git_log_all"
}
,{
"caption": "Git: Graph Current File",
"command": "git_graph"
}
,{
"caption": "Git: Graph All",
"command": "git_graph_all"
}
,{
"caption": "Git: Diff Current File",
"command": "git_diff"
}
,{
"caption": "Git: Diff All",
"command": "git_diff_all"
}
,{
"caption": "Git: Diff Staged",
"command": "git_diff_commit"
}
,{
"caption": "Git: Diff Tool Current File",
"command": "git_diff_tool"
}
,{
"caption": "Git: Diff Tool All",
"command": "git_diff_tool_all"
}
,{
"caption": "Git: Commit",
"command": "git_commit"
}
,{
"caption": "Git: Amend Commit",
"command": "git_commit_amend"
}
,{
"caption": "Git: Quick Commit",
"command": "git_quick_commit"
}
,{
"caption": "Git: Status",
"command": "git_status"
}
,{
"caption": "Git: Open Modified Files",
"command": "git_open_modified_files"
}
,{
"caption": "Git: New Branch",
"command": "git_new_branch"
}
,{
"caption": "Git: Change Branch",
"command": "git_branch"
}
,{
"caption": "Git: Merge Branch",
"command": "git_merge"
}
,{
"caption": "Git: Delete Branch",
"command": "git_delete_branch"
}
,{
"caption": "Git: Stash Changes",
"command": "git_stash"
}
,{
"caption": "Git: Stash Pop",
"command": "git_stash_pop"
}
,{
"caption": "Git: Stash Apply",
"command": "git_stash_apply"
}
,{
"caption": "Git: Stash Drop",
"command": "git_stash_drop"
}
,{
"caption": "Git: Add Current File",
"command": "git_add"
}
,{
"caption": "Git: Add...",
"command": "git_add_choice"
}
,{
"caption": "Git: Checkout Current File",
"command": "git_checkout"
}
,{
"caption": "Git: Fetch",
"command": "git_fetch"
}
,{
"caption": "Git: Pull",
"command": "git_pull"
}
,{
"caption": "Git: Pull Current Branch",
"command": "git_pull_current_branch"
}
,{
"caption": "Git: Push",
"command": "git_push"
}
,{
"caption": "Git: Push Current Branch",
"command": "git_push_current_branch"
}
,{
"caption": "Git: Show Current File",
"command": "git_show"
}
,{
"caption": "Git: Toggle Annotations",
"command": "git_toggle_annotations"
}
,{
"caption": "Git: Custom Command",
"command": "git_custom"
}
,{
"caption": "Git Flow: Feature Start",
"command": "git_flow_feature_start"
}
,{
"caption": "Git Flow: Feature Finish",
"command": "git_flow_feature_finish"
}
,{
"caption": "Git Flow: Release Start",
"command": "git_flow_release_start"
}
,{
"caption": "Git Flow: Release Finish",
"command": "git_flow_release_finish"
}
,{
"caption": "Git Flow: Hotfix Start",
"command": "git_flow_hotfix_start"
}
,{
"caption": "Git Flow: Hotfix Finish",
"command": "git_flow_hotfix_finish"
}
,{
"caption": "Git: Open...",
"command": "git_open_file"
}
,{
"caption": "Git: Reset (unstage) Current File",
"command": "git_reset_head"
}
,{
"caption": "Git: Reset (unstage) All",
"command": "git_reset_head_all"
}
,{
"caption": "Git: Reset (hard) HEAD",
"command": "git_reset_hard_head"
}
,{
"caption": "Git: Add Selected Hunk",
"command": "git_add_selected_hunk"
}
,{
"caption": "Git: Commit Selected Hunk",
"command": "git_commit_selected_hunk"
}
,{
"caption": "Git: Gui",
"command": "git_gui"
}
,{
"caption": "Git: Gitk",
"command": "git_gitk"
}
,{
"caption": "Git: Commit history",
"command": "git_commit_history"
}
]

View File

@@ -0,0 +1,6 @@
[
{"keys": ["enter"], "command": "git_goto_diff",
"context": [{"key": "selector", "operand": "markup.inserted.diff"}]},
{"keys": ["enter"], "command": "git_goto_diff",
"context": [{"key": "selector", "operand": "markup.deleted.diff"}]}
]

View File

@@ -0,0 +1,42 @@
{
// save before running commands
"save_first": true
// if present, use this command instead of plain "git"
// e.g. "/Users/kemayo/bin/git" or "C:\bin\git.exe"
,"git_command": false
// point this the installation location of git-flow
,"git_flow_command": "/usr/local/bin/git-flow"
// use the panel for diff output, rather than a new scratch window (new tab)
,"diff_panel": false
// affects blame command when no selection is made
// true: blame whole file
// false: blame only current line
,"blame_whole_file": true
// If you'd rather have your status command open files instead of show you a
// diff, set this to true. You can still do `Git: Status` followed by
// 'Git: Diff Current File' to get a file diff
,"status_opens_file": false
// Use --verbose flag for commit messages
,"verbose_commits": true
// How many commit messages to store in the history. Set to 0 to disable.
,"history_size": 5
// Show git flow commands
,"flow": false
// Annotations default to being on for all files. Can be slow in some cases.
,"annotations": false
// statusbar
,"statusbar_branch": true
// Symbols for quick git status in status bar
,"statusbar_status": true
,"statusbar_status_symbols" : {"modified": "≠", "added": "+", "deleted": "×", "untracked": "?", "conflicts": "‼", "renamed":"R", "copied":"C", "clean": "√", "separator": " "}
}

View File

@@ -0,0 +1,125 @@
[
{
"id": "tools",
"children":
[
{
"caption": "Git",
"children":
[
{
"caption": "This file",
"children":
[
{ "caption": "Log", "command": "git_log" }
,{ "caption": "Graph", "command": "git_graph" }
,{ "caption": "-" }
,{ "caption": "Diff", "command": "git_diff" }
,{ "caption": "DiffTool", "command": "git_diff_tool" }
,{ "caption": "-" }
,{ "caption": "Add", "command": "git_add" }
,{ "caption": "Add Selected Hunk", "command": "git_add_selected_hunk" }
,{ "caption": "-" }
,{ "caption": "Reset", "command": "git_reset_head" }
,{ "caption": "Checkout (Discard Changes)", "command": "git_checkout" }
,{ "caption": "-" }
,{ "caption": "Quick Commit Current File", "command": "git_quick_commit" }
,{ "caption": "Commit Selected Hunk", "command": "git_commit_selected_hunk" }
,{ "caption": "-" }
,{ "caption": "Blame", "command": "git_blame" }
,{ "caption": "-" }
,{ "caption": "Toggle Annotations", "command": "git_toggle_annotations" }
]
}
,{
"caption": "Whole repo",
"children":
[
{ "caption": "Log", "command": "git_log_all" }
,{ "caption": "Graph", "command": "git_graph_all" }
,{ "caption": "-" }
,{ "caption": "Diff", "command": "git_diff_all" }
,{ "caption": "Diff Staged", "command": "git_diff_commit" }
,{ "caption": "Diff Tool", "command": "git_diff_tool_all" }
,{ "caption": "Reset Hard", "command": "git_reset_hard_head" }
,{ "caption": "-" }
,{ "caption": "Add...", "command": "git_add_choice" }
,{ "caption": "-" }
,{ "caption": "Reset", "command": "git_reset_head_all" }
,{ "caption": "-" }
,{ "caption": "Commit", "command": "git_commit" }
,{ "caption": "Amend Last Commit", "command": "git_commit_amend" }
,{ "caption": "-" }
,{ "caption": "Open...", "command": "git_open_file" }
]
}
,{
"caption": "Stash",
"children":
[
{ "caption": "Save", "command": "git_stash" }
,{ "caption": "Pop", "command": "git_stash_pop" }
,{ "caption": "Apply", "command": "git_stash_apply" }
,{ "caption": "Drop", "command": "git_stash_drop" }
]
}
,{ "caption": "-" }
,{
"caption": "Flow",
"children":
[
{ "caption": "Feature Start", "command": "git_flow_feature_start"}
,{ "caption": "Feature Finish", "command": "git_flow_feature_finish"}
,{ "caption": "-"}
,{ "caption": "Release Start", "command": "git_flow_release_start"}
,{ "caption": "Release Finish", "command": "git_flow_release_finish"}
,{ "caption": "-"}
,{ "caption": "Hotfix Start", "command": "git_flow_hotfix_start"}
,{ "caption": "Hotfix Finish", "command": "git_flow_hotfix_finish"}
]
}
,{ "caption": "-" }
,{ "caption": "Init", "command": "git_init"}
,{ "caption": "Status...", "command": "git_status" }
,{ "caption": "Branches...", "command": "git_branch" }
,{ "caption": "Merge...", "command": "git_merge" }
,{ "caption": "See commit history...", "command": "git_commit_history"}
]
}
]
}
,{
"caption": "Preferences",
"mnemonic": "n",
"id": "preferences",
"children":
[
{
"caption": "Package Settings",
"mnemonic": "P",
"id": "package-settings",
"children":
[
{
"caption": "Git",
"children":
[
{
"command": "open_file",
"args": {"file": "${packages}/Git/Git.sublime-settings"},
"caption": "Settings Default"
},
{
"command": "open_file",
"args": {"file": "${packages}/User/Git.sublime-settings"},
"caption": "Settings User"
},
{ "caption": "-" }
]
}
]
}
]
}
]

View File

@@ -0,0 +1,22 @@
# Sublime Text 2 plugin: git
Git integration: it's pretty handy. Who knew, right?
For more information about what's supported, and how to install this, [check the wiki](https://github.com/kemayo/sublime-text-2-git/wiki).
## Install
### Package Control
The easiest way to install this is with [Package Control](http://wbond.net/sublime\_packages/package\_control).
* If you just went and installed Package Control, you probably need to restart Sublime Text 2 before doing this next bit.
* Bring up the Command Palette (Command+Shift+p on OS X, Control+Shift+p on Linux/Windows).
* Select "Package Control: Install Package" (it'll take a few seconds)
* Select Git when the list appears.
Package Control will automatically keep Git up to date with the latest version.
### The rest
If you don't want to use Package Control, [check the wiki](https://github.com/kemayo/sublime-text-2-git/wiki) for other installation methods on various platforms.

View File

@@ -0,0 +1,115 @@
import os
import re
import sublime
from git import GitTextCommand, GitWindowCommand, git_root
import status
class GitAddChoiceCommand(status.GitStatusCommand):
def status_filter(self, item):
return super(GitAddChoiceCommand, self).status_filter(item) and not item[1].isspace()
def show_status_list(self):
self.results = [[" + All Files", "apart from untracked files"], [" + All Files", "including untracked files"]] + self.results
return super(GitAddChoiceCommand, self).show_status_list()
def panel_followup(self, picked_status, picked_file, picked_index):
working_dir = git_root(self.get_working_dir())
if picked_index == 0:
command = ['git', 'add', '--update']
elif picked_index == 1:
command = ['git', 'add', '--all']
else:
command = ['git']
picked_file = picked_file.strip('"')
if os.path.isfile(working_dir + "/" + picked_file):
command += ['add']
else:
command += ['rm']
command += ['--', picked_file]
self.run_command(command, self.rerun,
working_dir=working_dir)
def rerun(self, result):
self.run()
class GitAdd(GitTextCommand):
def run(self, edit):
self.run_command(['git', 'add', self.get_file_name()])
class GitAddSelectedHunkCommand(GitTextCommand):
def run(self, edit):
self.run_command(['git', 'diff', '--no-color', '-U1', self.get_file_name()], self.cull_diff)
def cull_diff(self, result):
selection = []
for sel in self.view.sel():
selection.append({
"start": self.view.rowcol(sel.begin())[0] + 1,
"end": self.view.rowcol(sel.end())[0] + 1,
})
hunks = [{"diff":""}]
i = 0
matcher = re.compile('^@@ -([0-9]*)(?:,([0-9]*))? \+([0-9]*)(?:,([0-9]*))? @@')
for line in result.splitlines():
if line.startswith('@@'):
i += 1
match = matcher.match(line)
start = int(match.group(3))
end = match.group(4)
if end:
end = start + int(end)
else:
end = start
hunks.append({"diff": "", "start": start, "end": end})
hunks[i]["diff"] += line + "\n"
diffs = hunks[0]["diff"]
hunks.pop(0)
selection_is_hunky = False
for hunk in hunks:
for sel in selection:
if sel["end"] < hunk["start"]:
continue
if sel["start"] > hunk["end"]:
continue
diffs += hunk["diff"] # + "\n\nEND OF HUNK\n\n"
selection_is_hunky = True
if selection_is_hunky:
self.run_command(['git', 'apply', '--cached'], stdin=diffs)
else:
sublime.status_message("No selected hunk")
# Also, sometimes we want to undo adds
class GitResetHead(object):
def run(self, edit=None):
self.run_command(['git', 'reset', 'HEAD', self.get_file_name()])
def generic_done(self, result):
pass
class GitResetHeadCommand(GitResetHead, GitTextCommand):
pass
class GitResetHeadAllCommand(GitResetHead, GitWindowCommand):
pass
class GitResetHardHeadCommand(GitWindowCommand):
may_change_files = True
def run(self):
if sublime.ok_cancel_dialog("Warning: this will reset your index and revert all files, throwing away all your uncommitted changes with no way to recover. Consider stashing your changes instead if you'd like to set them aside safely.", "Continue"):
self.run_command(['git', 'reset', '--hard', 'HEAD'])

View File

@@ -0,0 +1,130 @@
import tempfile
import re
import os
import sublime
import sublime_plugin
from git import git_root, GitTextCommand
class GitClearAnnotationCommand(GitTextCommand):
def run(self, view):
self.active_view().settings().set('live_git_annotations', False)
self.view.erase_regions('git.changes.x')
self.view.erase_regions('git.changes.+')
self.view.erase_regions('git.changes.-')
class GitToggleAnnotationsCommand(GitTextCommand):
def run(self, view):
if self.active_view().settings().get('live_git_annotations'):
self.view.run_command('git_clear_annotation')
else:
self.view.run_command('git_annotate')
class GitAnnotationListener(sublime_plugin.EventListener):
def on_modified(self, view):
if not view.settings().get('live_git_annotations'):
return
view.run_command('git_annotate')
def on_load(self, view):
s = sublime.load_settings("Git.sublime-settings")
if s.get('annotations'):
view.run_command('git_annotate')
class GitAnnotateCommand(GitTextCommand):
# Unfortunately, git diff does not support text from stdin, making a *live*
# annotation difficult. Therefore I had to resort to the system diff
# command.
# This works as follows:
# 1. When the command is run for the first time for this file, a temporary
# file with the current state of the HEAD is being pulled from git.
# 2. All consecutive runs will pass the current buffer into diffs stdin.
# The resulting output is then parsed and regions are set accordingly.
def run(self, view):
# If the annotations are already running, we dont have to create a new
# tmpfile
if hasattr(self, "tmp"):
self.compare_tmp(None)
return
self.tmp = tempfile.NamedTemporaryFile()
self.active_view().settings().set('live_git_annotations', True)
root = git_root(self.get_working_dir())
repo_file = os.path.relpath(self.view.file_name(), root)
self.run_command(['git', 'show', 'HEAD:{0}'.format(repo_file)], show_status=False, no_save=True, callback=self.compare_tmp, stdout=self.tmp)
def compare_tmp(self, result, stdout=None):
all_text = self.view.substr(sublime.Region(0, self.view.size())).encode("utf-8")
self.run_command(['diff', '-u', self.tmp.name, '-'], stdin=all_text, no_save=True, show_status=False, callback=self.parse_diff)
# This is where the magic happens. At the moment, only one chunk format is supported. While
# the unified diff format theoritaclly supports more, I don't think git diff creates them.
def parse_diff(self, result, stdin=None):
lines = result.splitlines()
matcher = re.compile('^@@ -([0-9]*),([0-9]*) \+([0-9]*),([0-9]*) @@')
diff = []
for line_index in range(0, len(lines)):
line = lines[line_index]
if not line.startswith('@'):
continue
match = matcher.match(line)
if not match:
continue
line_before, len_before, line_after, len_after = [int(match.group(x)) for x in [1, 2, 3, 4]]
chunk_index = line_index + 1
tracked_line_index = line_after - 1
deletion = False
insertion = False
while True:
line = lines[chunk_index]
if line.startswith('@'):
break
elif line.startswith('-'):
if not line.strip() == '-':
deletion = True
tracked_line_index -= 1
elif line.startswith('+'):
if deletion and not line.strip() == '+':
diff.append(['x', tracked_line_index])
insertion = True
elif not deletion:
insertion = True
diff.append(['+', tracked_line_index])
else:
if not insertion and deletion:
diff.append(['-', tracked_line_index])
insertion = deletion = False
tracked_line_index += 1
chunk_index += 1
if chunk_index >= len(lines):
break
self.annotate(diff)
# Once we got all lines with their specific change types (either x, +, or - for
# modified, added, or removed) we can create our regions and do the actual annotation.
def annotate(self, diff):
self.view.erase_regions('git.changes.x')
self.view.erase_regions('git.changes.+')
self.view.erase_regions('git.changes.-')
typed_diff = {'x': [], '+': [], '-': []}
for change_type, line in diff:
if change_type == '-':
full_region = self.view.full_line(self.view.text_point(line - 1, 0))
position = full_region.begin()
for i in xrange(full_region.size()):
typed_diff[change_type].append(sublime.Region(position + i))
else:
point = self.view.text_point(line, 0)
region = self.view.full_line(point)
if change_type == '-':
region = sublime.Region(point, point + 5)
typed_diff[change_type].append(region)
for change in ['x', '+']:
self.view.add_regions("git.changes.{0}".format(change), typed_diff[change], 'git.changes.{0}'.format(change), 'dot', sublime.HIDDEN)
self.view.add_regions("git.changes.-", typed_diff['-'], 'git.changes.-', 'dot', sublime.DRAW_EMPTY_AS_OVERWRITE)

View File

@@ -0,0 +1,169 @@
import functools
import tempfile
import os
import sublime
import sublime_plugin
from git import GitTextCommand, GitWindowCommand, plugin_file, view_contents, _make_text_safeish
import add
history = []
class GitQuickCommitCommand(GitTextCommand):
def run(self, edit):
self.get_window().show_input_panel("Message", "",
self.on_input, None, None)
def on_input(self, message):
if message.strip() == "":
self.panel("No commit message provided")
return
self.run_command(['git', 'add', self.get_file_name()],
functools.partial(self.add_done, message))
def add_done(self, message, result):
if result.strip():
sublime.error_message("Error adding file:\n" + result)
return
self.run_command(['git', 'commit', '-m', message])
# Commit is complicated. It'd be easy if I just wanted to let it run
# on OSX, and assume that subl was in the $PATH. However... I can't do
# that. Second choice was to set $GIT_EDITOR to sublime text for the call
# to commit, and let that Just Work. However, on Windows you can't pass
# -w to sublime, which means the editor won't wait, and so the commit will fail
# with an empty message.
# Thus this flow:
# 1. `status --porcelain --untracked-files=no` to know whether files need
# to be committed
# 2. `status` to get a template commit message (not the exact one git uses; I
# can't see a way to ask it to output that, which is not quite ideal)
# 3. Create a scratch buffer containing the template
# 4. When this buffer is closed, get its contents with an event handler and
# pass execution back to the original command. (I feel that the way this
# is done is a total hack. Unfortunately, I cannot see a better way right
# now.)
# 5. Strip lines beginning with # from the message, and save in a temporary
# file
# 6. `commit -F [tempfile]`
class GitCommitCommand(GitWindowCommand):
active_message = False
extra_options = ""
def run(self):
self.lines = []
self.working_dir = self.get_working_dir()
self.run_command(
['git', 'status', '--untracked-files=no', '--porcelain'],
self.porcelain_status_done
)
def porcelain_status_done(self, result):
# todo: split out these status-parsing things... asdf
has_staged_files = False
result_lines = result.rstrip().split('\n')
for line in result_lines:
if line and not line[0].isspace():
has_staged_files = True
break
if not has_staged_files:
self.panel("Nothing to commit")
return
# Okay, get the template!
s = sublime.load_settings("Git.sublime-settings")
if s.get("verbose_commits"):
self.run_command(['git', 'diff', '--staged', '--no-color'], self.diff_done)
else:
self.run_command(['git', 'status'], self.diff_done)
def diff_done(self, result):
settings = sublime.load_settings("Git.sublime-settings")
historySize = settings.get('history_size')
def format(line):
return '# ' + line.replace("\n", " ")
if not len(self.lines):
self.lines = ["", ""]
self.lines.extend(map(format, history[:historySize]))
self.lines.extend([
"# --------------",
"# Please enter the commit message for your changes. Everything below",
"# this paragraph is ignored, and an empty message aborts the commit.",
"# Just close the window to accept your message.",
result.strip()
])
template = "\n".join(self.lines)
msg = self.window.new_file()
msg.set_scratch(True)
msg.set_name("COMMIT_EDITMSG")
self._output_to_view(msg, template, syntax=plugin_file("syntax/Git Commit Message.tmLanguage"))
msg.sel().clear()
msg.sel().add(sublime.Region(0, 0))
GitCommitCommand.active_message = self
def message_done(self, message):
# filter out the comments (git commit doesn't do this automatically)
settings = sublime.load_settings("Git.sublime-settings")
historySize = settings.get('history_size')
lines = [line for line in message.split("\n# --------------")[0].split("\n")
if not line.lstrip().startswith('#')]
message = '\n'.join(lines).strip()
if len(message) and historySize:
history.insert(0, message)
# write the temp file
message_file = tempfile.NamedTemporaryFile(delete=False)
message_file.write(_make_text_safeish(message, self.fallback_encoding, 'encode'))
message_file.close()
self.message_file = message_file
# and actually commit
with open(message_file.name, 'r') as fp:
self.run_command(['git', 'commit', '-F', '-', self.extra_options],
self.commit_done, working_dir=self.working_dir, stdin=fp.read())
def commit_done(self, result, **kwargs):
os.remove(self.message_file.name)
self.panel(result)
class GitCommitAmendCommand(GitCommitCommand):
extra_options = "--amend"
def diff_done(self, result):
self.after_show = result
self.run_command(['git', 'log', '-n', '1', '--format=format:%B'], self.amend_diff_done)
def amend_diff_done(self, result):
self.lines = result.split("\n")
super(GitCommitAmendCommand, self).diff_done(self.after_show)
class GitCommitMessageListener(sublime_plugin.EventListener):
def on_close(self, view):
if view.name() != "COMMIT_EDITMSG":
return
command = GitCommitCommand.active_message
if not command:
return
message = view_contents(view)
command.message_done(message)
class GitCommitHistoryCommand(sublime_plugin.TextCommand):
def run(self, edit):
self.edit = edit
self.view.window().show_quick_panel(history, self.panel_done, sublime.MONOSPACE_FONT)
def panel_done(self, index):
if index > -1:
self.view.replace(self.edit, self.view.sel()[0], history[index] + '\n')
class GitCommitSelectedHunk(add.GitAddSelectedHunkCommand):
def run(self, edit):
self.run_command(['git', 'diff', '--no-color', self.get_file_name()], self.cull_diff)
self.get_window().run_command('git_commit')

View File

@@ -0,0 +1,157 @@
import sublime
import re
from git import git_root, GitTextCommand, GitWindowCommand
import functools
def do_when(conditional, callback, *args, **kwargs):
if conditional():
return callback(*args, **kwargs)
sublime.set_timeout(functools.partial(do_when, conditional, callback, *args, **kwargs), 50)
def goto_xy(view, line, col):
view.run_command("goto_line", {"line": line})
for i in range(col):
view.run_command("move", {"by": "characters", "forward": True})
class GitDiff (object):
def run(self, edit=None):
self.run_command(['git', 'diff', '--no-color', '--', self.get_file_name()],
self.diff_done)
def diff_done(self, result):
if not result.strip():
self.panel("No output")
return
s = sublime.load_settings("Git.sublime-settings")
if s.get('diff_panel'):
view = self.panel(result)
else:
view = self.scratch(result, title="Git Diff")
lines_inserted = view.find_all(r'^\+[^+]{2} ')
lines_deleted = view.find_all(r'^-[^-]{2} ')
view.add_regions("inserted", lines_inserted, "markup.inserted.diff", "dot", sublime.HIDDEN)
view.add_regions("deleted", lines_deleted, "markup.deleted.diff", "dot", sublime.HIDDEN)
# Store the git root directory in the view so we can resolve relative paths
# when the user wants to navigate to the source file.
view.settings().set("git_root_dir", git_root(self.get_working_dir()))
class GitDiffCommit (object):
def run(self, edit=None):
self.run_command(['git', 'diff', '--cached', '--no-color'],
self.diff_done)
def diff_done(self, result):
if not result.strip():
self.panel("No output")
return
self.scratch(result, title="Git Diff")
class GitDiffCommand(GitDiff, GitTextCommand):
pass
class GitDiffAllCommand(GitDiff, GitWindowCommand):
pass
class GitDiffCommitCommand(GitDiffCommit, GitWindowCommand):
pass
class GitDiffTool(object):
def run(self, edit=None):
self.run_command(['git', 'difftool', '--', self.get_file_name()])
class GitDiffToolCommand(GitDiffTool, GitTextCommand):
pass
class GitDiffToolAll(GitWindowCommand):
def run(self):
self.run_command(['git', 'difftool'])
class GitGotoDiff(sublime_plugin.TextCommand):
def run(self, edit):
v = self.view
view_scope_name = v.scope_name(v.sel()[0].a)
scope_markup_inserted = ("markup.inserted.diff" in view_scope_name)
scope_markup_deleted = ("markup.deleted.diff" in view_scope_name)
if not scope_markup_inserted and not scope_markup_deleted:
return
beg = v.sel()[0].a # Current position in selection
pt = v.line(beg).a # First position in the current diff line
self.column = beg - pt - 1 # The current column (-1 because the first char in diff file)
self.file_name = None
hunk_line = None
line_offset = 0
while pt > 0:
line = v.line(pt)
lineContent = v.substr(line)
if lineContent.startswith("@@"):
if not hunk_line:
hunk_line = lineContent
elif lineContent.startswith("+++ b/"):
self.file_name = v.substr(sublime.Region(line.a+6, line.b)).strip()
break
elif not hunk_line and not lineContent.startswith("-"):
line_offset = line_offset+1
pt = v.line(pt-1).a
hunk = re.match(r"^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*", hunk_line)
if not hunk:
sublime.status_message("No hunk info")
return
hunk_start_line = hunk.group(3)
self.goto_line = int(hunk_start_line) + line_offset - 1
git_root_dir = v.settings().get("git_root_dir")
# Sanity check and see if the file we're going to try to open even
# exists. If it does not, prompt the user for the correct base directory
# to use for their diff.
full_path_file_name = self.file_name
if git_root_dir:
full_path_file_name = os.path.join(git_root_dir, self.file_name)
else:
git_root_dir = ""
if not os.path.isfile(full_path_file_name):
caption = "Enter base directory for file '%s':" % self.file_name
v.window().show_input_panel(caption,
git_root_dir,
self.on_path_confirmed,
None,
None)
else:
self.on_path_confirmed(git_root_dir)
def on_path_confirmed(self, git_root_dir):
v = self.view
old_git_root_dir = v.settings().get("git_root_dir")
# If the user provided a new git_root_dir, save it in the view settings
# so they only have to fix it once
if old_git_root_dir != git_root_dir:
v.settings().set("git_root_dir", git_root_dir)
full_path_file_name = os.path.join(git_root_dir, self.file_name)
new_view = v.window().open_file(full_path_file_name)
do_when(lambda: not new_view.is_loading(),
lambda: goto_xy(new_view, self.goto_line, self.column))

View File

@@ -0,0 +1,90 @@
import sublime
from git import GitWindowCommand
class GitFlowCommand(GitWindowCommand):
def is_visible(self):
s = sublime.load_settings("Git.sublime-settings")
if s.get('flow'):
return True
class GitFlowFeatureStartCommand(GitFlowCommand):
def run(self):
self.get_window().show_input_panel('Enter Feature Name:', '', self.on_done, None, None)
def on_done(self, feature_name):
self.run_command(['git-flow', 'feature', 'start', feature_name])
class GitFlowFeatureFinishCommand(GitFlowCommand):
def run(self):
self.run_command(['git-flow', 'feature'], self.feature_done)
def feature_done(self, result):
self.results = result.rstrip().split('\n')
self.quick_panel(self.results, self.panel_done,
sublime.MONOSPACE_FONT)
def panel_done(self, picked):
if 0 > picked < len(self.results):
return
picked_feature = self.results[picked]
if picked_feature.startswith("*"):
picked_feature = picked_feature.strip("*")
picked_feature = picked_feature.strip()
self.run_command(['git-flow', 'feature', 'finish', picked_feature])
class GitFlowReleaseStartCommand(GitFlowCommand):
def run(self):
self.get_window().show_input_panel('Enter Version Number:', '', self.on_done, None, None)
def on_done(self, release_name):
self.run_command(['git-flow', 'release', 'start', release_name])
class GitFlowReleaseFinishCommand(GitFlowCommand):
def run(self):
self.run_command(['git-flow', 'release'], self.release_done)
def release_done(self, result):
self.results = result.rstrip().split('\n')
self.quick_panel(self.results, self.panel_done,
sublime.MONOSPACE_FONT)
def panel_done(self, picked):
if 0 > picked < len(self.results):
return
picked_release = self.results[picked]
if picked_release.startswith("*"):
picked_release = picked_release.strip("*")
picked_release = picked_release.strip()
self.run_command(['git-flow', 'release', 'finish', picked_release])
class GitFlowHotfixStartCommand(GitFlowCommand):
def run(self):
self.get_window().show_input_panel('Enter hotfix name:', '', self.on_done, None, None)
def on_done(self, hotfix_name):
self.run_command(['git-flow', 'hotfix', 'start', hotfix_name])
class GitFlowHotfixFinishCommand(GitFlowCommand):
def run(self):
self.run_command(['git-flow', 'hotfix'], self.hotfix_done)
def hotfix_done(self, result):
self.results = result.rstrip().split('\n')
self.quick_panel(self.results, self.panel_done,
sublime.MONOSPACE_FONT)
def panel_done(self, picked):
if 0 > picked < len(self.results):
return
picked_hotfix = self.results[picked]
if picked_hotfix.startswith("*"):
picked_hotfix = picked_hotfix.strip("*")
picked_hotfix = picked_hotfix.strip()
self.run_command(['git-flow', 'hotfix', 'finish', picked_hotfix])

View File

@@ -0,0 +1,333 @@
import os
import sublime
import sublime_plugin
import threading
import subprocess
import functools
import os.path
import time
# when sublime loads a plugin it's cd'd into the plugin directory. Thus
# __file__ is useless for my purposes. What I want is "Packages/Git", but
# allowing for the possibility that someone has renamed the file.
# Fun discovery: Sublime on windows still requires posix path separators.
PLUGIN_DIRECTORY = os.getcwd().replace(os.path.normpath(os.path.join(os.getcwd(), '..', '..')) + os.path.sep, '').replace(os.path.sep, '/')
git_root_cache = {}
def main_thread(callback, *args, **kwargs):
# sublime.set_timeout gets used to send things onto the main thread
# most sublime.[something] calls need to be on the main thread
sublime.set_timeout(functools.partial(callback, *args, **kwargs), 0)
def open_url(url):
sublime.active_window().run_command('open_url', {"url": url})
def git_root(directory):
global git_root_cache
retval = False
leaf_dir = directory
if leaf_dir in git_root_cache and git_root_cache[leaf_dir]['expires'] > time.time():
return git_root_cache[leaf_dir]['retval']
while directory:
if os.path.exists(os.path.join(directory, '.git')):
retval = directory
break
parent = os.path.realpath(os.path.join(directory, os.path.pardir))
if parent == directory:
# /.. == /
retval = False
break
directory = parent
git_root_cache[leaf_dir] = {
'retval': retval,
'expires': time.time() + 5
}
return retval
# for readability code
def git_root_exist(directory):
return git_root(directory)
def view_contents(view):
region = sublime.Region(0, view.size())
return view.substr(region)
def plugin_file(name):
return os.path.join(PLUGIN_DIRECTORY, name)
def do_when(conditional, callback, *args, **kwargs):
if conditional():
return callback(*args, **kwargs)
sublime.set_timeout(functools.partial(do_when, conditional, callback, *args, **kwargs), 50)
def _make_text_safeish(text, fallback_encoding, method='decode'):
# The unicode decode here is because sublime converts to unicode inside
# insert in such a way that unknown characters will cause errors, which is
# distinctly non-ideal... and there's no way to tell what's coming out of
# git in output. So...
try:
unitext = getattr(text, method)('utf-8')
except (UnicodeEncodeError, UnicodeDecodeError):
unitext = getattr(text, method)(fallback_encoding)
return unitext
class CommandThread(threading.Thread):
def __init__(self, command, on_done, working_dir="", fallback_encoding="", **kwargs):
threading.Thread.__init__(self)
self.command = command
self.on_done = on_done
self.working_dir = working_dir
if "stdin" in kwargs:
self.stdin = kwargs["stdin"]
else:
self.stdin = None
if "stdout" in kwargs:
self.stdout = kwargs["stdout"]
else:
self.stdout = subprocess.PIPE
self.fallback_encoding = fallback_encoding
self.kwargs = kwargs
def run(self):
try:
# Ignore directories that no longer exist
if os.path.isdir(self.working_dir):
# Per http://bugs.python.org/issue8557 shell=True is required to
# get $PATH on Windows. Yay portable code.
shell = os.name == 'nt'
if self.working_dir != "":
os.chdir(self.working_dir)
proc = subprocess.Popen(self.command,
stdout=self.stdout, stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
shell=shell, universal_newlines=True)
output = proc.communicate(self.stdin)[0]
if not output:
output = ''
# if sublime's python gets bumped to 2.7 we can just do:
# output = subprocess.check_output(self.command)
main_thread(self.on_done,
_make_text_safeish(output, self.fallback_encoding), **self.kwargs)
except subprocess.CalledProcessError, e:
main_thread(self.on_done, e.returncode)
except OSError, e:
if e.errno == 2:
main_thread(sublime.error_message, "Git binary could not be found in PATH\n\nConsider using the git_command setting for the Git plugin\n\nPATH is: %s" % os.environ['PATH'])
else:
raise e
# A base for all commands
class GitCommand(object):
may_change_files = False
def run_command(self, command, callback=None, show_status=True,
filter_empty_args=True, no_save=False, **kwargs):
if filter_empty_args:
command = [arg for arg in command if arg]
if 'working_dir' not in kwargs:
kwargs['working_dir'] = self.get_working_dir()
if 'fallback_encoding' not in kwargs and self.active_view() and self.active_view().settings().get('fallback_encoding'):
kwargs['fallback_encoding'] = self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0]
s = sublime.load_settings("Git.sublime-settings")
if s.get('save_first') and self.active_view() and self.active_view().is_dirty() and not no_save:
self.active_view().run_command('save')
if command[0] == 'git' and s.get('git_command'):
command[0] = s.get('git_command')
if command[0] == 'git-flow' and s.get('git_flow_command'):
command[0] = s.get('git_flow_command')
if not callback:
callback = self.generic_done
thread = CommandThread(command, callback, **kwargs)
thread.start()
if show_status:
message = kwargs.get('status_message', False) or ' '.join(command)
sublime.status_message(message)
def generic_done(self, result):
if self.may_change_files and self.active_view() and self.active_view().file_name():
if self.active_view().is_dirty():
result = "WARNING: Current view is dirty.\n\n"
else:
# just asking the current file to be re-opened doesn't do anything
print "reverting"
position = self.active_view().viewport_position()
self.active_view().run_command('revert')
do_when(lambda: not self.active_view().is_loading(), lambda: self.active_view().set_viewport_position(position, False))
# self.active_view().show(position)
view = self.active_view()
if view and view.settings().get('live_git_annotations'):
self.view.run_command('git_annotate')
if not result.strip():
return
self.panel(result)
def _output_to_view(self, output_file, output, clear=False,
syntax="Packages/Diff/Diff.tmLanguage", **kwargs):
output_file.set_syntax_file(syntax)
edit = output_file.begin_edit()
if clear:
region = sublime.Region(0, self.output_view.size())
output_file.erase(edit, region)
output_file.insert(edit, 0, output)
output_file.end_edit(edit)
def scratch(self, output, title=False, position=None, **kwargs):
scratch_file = self.get_window().new_file()
if title:
scratch_file.set_name(title)
scratch_file.set_scratch(True)
self._output_to_view(scratch_file, output, **kwargs)
scratch_file.set_read_only(True)
if position:
sublime.set_timeout(lambda: scratch_file.set_viewport_position(position), 0)
return scratch_file
def panel(self, output, **kwargs):
if not hasattr(self, 'output_view'):
self.output_view = self.get_window().get_output_panel("git")
self.output_view.set_read_only(False)
self._output_to_view(self.output_view, output, clear=True, **kwargs)
self.output_view.set_read_only(True)
self.get_window().run_command("show_panel", {"panel": "output.git"})
def quick_panel(self, *args, **kwargs):
self.get_window().show_quick_panel(*args, **kwargs)
# A base for all git commands that work with the entire repository
class GitWindowCommand(GitCommand, sublime_plugin.WindowCommand):
def active_view(self):
return self.window.active_view()
def _active_file_name(self):
view = self.active_view()
if view and view.file_name() and len(view.file_name()) > 0:
return view.file_name()
@property
def fallback_encoding(self):
if self.active_view() and self.active_view().settings().get('fallback_encoding'):
return self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0]
# If there's no active view or the active view is not a file on the
# filesystem (e.g. a search results view), we can infer the folder
# that the user intends Git commands to run against when there's only
# only one.
def is_enabled(self):
if self._active_file_name() or len(self.window.folders()) == 1:
return git_root(self.get_working_dir())
def get_file_name(self):
return ''
def get_relative_file_name(self):
return ''
# If there is a file in the active view use that file's directory to
# search for the Git root. Otherwise, use the only folder that is
# open.
def get_working_dir(self):
file_name = self._active_file_name()
if file_name:
return os.path.realpath(os.path.dirname(file_name))
else:
try: # handle case with no open folder
return self.window.folders()[0]
except IndexError:
return ''
def get_window(self):
return self.window
# A base for all git commands that work with the file in the active view
class GitTextCommand(GitCommand, sublime_plugin.TextCommand):
def active_view(self):
return self.view
def is_enabled(self):
# First, is this actually a file on the file system?
if self.view.file_name() and len(self.view.file_name()) > 0:
return git_root(self.get_working_dir())
def get_file_name(self):
return os.path.basename(self.view.file_name())
def get_relative_file_name(self):
working_dir = self.get_working_dir()
file_path = working_dir.replace(git_root(working_dir), '')[1:]
file_name = os.path.join(file_path, self.get_file_name())
return file_name.replace('\\', '/') # windows issues
def get_working_dir(self):
return os.path.realpath(os.path.dirname(self.view.file_name()))
def get_window(self):
# Fun discovery: if you switch tabs while a command is working,
# self.view.window() is None. (Admittedly this is a consequence
# of my deciding to do async command processing... but, hey,
# got to live with that now.)
# I did try tracking the window used at the start of the command
# and using it instead of view.window() later, but that results
# panels on a non-visible window, which is especially useless in
# the case of the quick panel.
# So, this is not necessarily ideal, but it does work.
return self.view.window() or sublime.active_window()
# A few miscellaneous commands
class GitCustomCommand(GitWindowCommand):
may_change_files = True
def run(self):
self.get_window().show_input_panel("Git command", "",
self.on_input, None, None)
def on_input(self, command):
command = str(command) # avoiding unicode
if command.strip() == "":
self.panel("No git command provided")
return
import shlex
command_splitted = ['git'] + shlex.split(command)
print command_splitted
self.run_command(command_splitted)
class GitGuiCommand(GitTextCommand):
def run(self, edit):
command = ['git', 'gui']
self.run_command(command)
class GitGitkCommand(GitTextCommand):
def run(self, edit):
command = ['gitk']
self.run_command(command)

View File

@@ -0,0 +1,189 @@
import functools
import re
import sublime
from git import GitTextCommand, GitWindowCommand, plugin_file
class GitBlameCommand(GitTextCommand):
def run(self, edit):
# somewhat custom blame command:
# -w: ignore whitespace changes
# -M: retain blame when moving lines
# -C: retain blame when copying lines between files
command = ['git', 'blame', '-w', '-M', '-C']
s = sublime.load_settings("Git.sublime-settings")
selection = self.view.sel()[0] # todo: multi-select support?
if not selection.empty() or not s.get('blame_whole_file'):
# just the lines we have a selection on
begin_line, begin_column = self.view.rowcol(selection.begin())
end_line, end_column = self.view.rowcol(selection.end())
# blame will fail if last line is empty and is included in the selection
if end_line > begin_line and end_column == 0:
end_line -= 1
lines = str(begin_line + 1) + ',' + str(end_line + 1)
command.extend(('-L', lines))
callback = self.blame_done
else:
callback = functools.partial(self.blame_done,
position=self.view.viewport_position())
command.append(self.get_file_name())
self.run_command(command, callback)
def blame_done(self, result, position=None):
self.scratch(result, title="Git Blame", position=position,
syntax=plugin_file("syntax/Git Blame.tmLanguage"))
class GitLog(object):
def run(self, edit=None):
fn = self.get_file_name()
return self.run_log(fn != '', '--', fn)
def run_log(self, follow, *args):
# the ASCII bell (\a) is just a convenient character I'm pretty sure
# won't ever come up in the subject of the commit (and if it does then
# you positively deserve broken output...)
# 9000 is a pretty arbitrarily chosen limit; picked entirely because
# it's about the size of the largest repo I've tested this on... and
# there's a definite hiccup when it's loading that
command = ['git', 'log', '--pretty=%s\a%h %an <%aE>\a%ad (%ar)',
'--date=local', '--max-count=9000', '--follow' if follow else None]
command.extend(args)
self.run_command(
command,
self.log_done)
def log_done(self, result):
self.results = [r.split('\a', 2) for r in result.strip().split('\n')]
self.quick_panel(self.results, self.log_panel_done)
def log_panel_done(self, picked):
if 0 > picked < len(self.results):
return
item = self.results[picked]
# the commit hash is the first thing on the second line
self.log_result(item[1].split(' ')[0])
def log_result(self, ref):
# I'm not certain I should have the file name here; it restricts the
# details to just the current file. Depends on what the user expects...
# which I'm not sure of.
self.run_command(
['git', 'log', '-p', '-1', ref, '--', self.get_file_name()],
self.details_done)
def details_done(self, result):
self.scratch(result, title="Git Commit Details", syntax=plugin_file("syntax/Git Commit Message.tmLanguage"))
class GitLogCommand(GitLog, GitTextCommand):
pass
class GitLogAllCommand(GitLog, GitWindowCommand):
pass
class GitShow(object):
def run(self, edit=None):
# GitLog Copy-Past
self.run_command(
['git', 'log', '--pretty=%s\a%h %an <%aE>\a%ad (%ar)',
'--date=local', '--max-count=9000', '--', self.get_file_name()],
self.show_done)
def show_done(self, result):
# GitLog Copy-Past
self.results = [r.split('\a', 2) for r in result.strip().split('\n')]
self.quick_panel(self.results, self.panel_done)
def panel_done(self, picked):
if 0 > picked < len(self.results):
return
item = self.results[picked]
# the commit hash is the first thing on the second line
ref = item[1].split(' ')[0]
self.run_command(
['git', 'show', '%s:%s' % (ref, self.get_relative_file_name())],
self.details_done,
ref=ref)
def details_done(self, result, ref):
syntax = self.view.settings().get('syntax')
self.scratch(result, title="%s:%s" % (ref, self.get_file_name()), syntax=syntax)
class GitShowCommand(GitShow, GitTextCommand):
pass
class GitShowAllCommand(GitShow, GitWindowCommand):
pass
class GitGraph(object):
def run(self, edit=None):
filename = self.get_file_name()
self.run_command(
['git', 'log', '--graph', '--pretty=%h -%d (%cr) (%ci) <%an> %s', '--abbrev-commit', '--no-color', '--decorate', '--date=relative', '--follow' if filename else None, '--', filename],
self.log_done
)
def log_done(self, result):
self.scratch(result, title="Git Log Graph", syntax=plugin_file("syntax/Git Graph.tmLanguage"))
class GitGraphCommand(GitGraph, GitTextCommand):
pass
class GitGraphAllCommand(GitGraph, GitWindowCommand):
pass
class GitOpenFileCommand(GitLog, GitWindowCommand):
def run(self):
self.run_command(['git', 'branch', '-a', '--no-color'], self.branch_done)
def branch_done(self, result):
self.results = result.rstrip().split('\n')
self.quick_panel(self.results, self.branch_panel_done,
sublime.MONOSPACE_FONT)
def branch_panel_done(self, picked):
if 0 > picked < len(self.results):
return
self.branch = self.results[picked].split(' ')[-1]
self.run_log(False, self.branch)
def log_result(self, result_hash):
# the commit hash is the first thing on the second line
self.ref = result_hash
self.run_command(
['git', 'ls-tree', '-r', '--full-tree', self.ref],
self.ls_done)
def ls_done(self, result):
# Last two items are the ref and the file name
# p.s. has to be a list of lists; tuples cause errors later
self.results = [[match.group(2), match.group(1)] for match in re.finditer(r"\S+\s(\S+)\t(.+)", result)]
self.quick_panel(self.results, self.ls_panel_done)
def ls_panel_done(self, picked):
if 0 > picked < len(self.results):
return
item = self.results[picked]
self.filename = item[0]
self.fileRef = item[1]
self.run_command(
['git', 'show', self.fileRef],
self.show_done)
def show_done(self, result):
self.scratch(result, title="%s:%s" % (self.fileRef, self.filename))

View File

@@ -0,0 +1,159 @@
import os
import sublime
from git import GitTextCommand, GitWindowCommand, git_root_exist
class GitInit(object):
def git_init(self, directory):
if os.path.exists(directory):
self.run_command(['git', 'init'], self.git_inited, working_dir=directory)
else:
sublime.status_message("Directory does not exist.")
def git_inited(self, result):
sublime.status_message(result)
class GitInitCommand(GitInit, GitWindowCommand):
def run(self):
self.get_window().show_input_panel("Git directory", self.get_working_dir(), self.git_init, None, None)
def is_enabled(self):
if not git_root_exist(self.get_working_dir()):
return True
else:
return False
class GitBranchCommand(GitWindowCommand):
may_change_files = True
command_to_run_after_branch = ['checkout']
extra_flags = []
def run(self):
self.run_command(['git', 'branch', '--no-color'] + self.extra_flags, self.branch_done)
def branch_done(self, result):
self.results = result.rstrip().split('\n')
self.quick_panel(self.results, self.panel_done,
sublime.MONOSPACE_FONT)
def panel_done(self, picked):
if 0 > picked < len(self.results):
return
picked_branch = self.results[picked]
if picked_branch.startswith("*"):
return
picked_branch = picked_branch.strip()
self.run_command(['git'] + self.command_to_run_after_branch + [picked_branch], self.update_status)
def update_status(self, result):
global branch
branch = ""
for view in self.window.views():
view.run_command("git_branch_status")
class GitMergeCommand(GitBranchCommand):
command_to_run_after_branch = ['merge']
extra_flags = ['--no-merge']
class GitDeleteBranchCommand(GitBranchCommand):
command_to_run_after_branch = ['branch', '-d']
class GitNewBranchCommand(GitWindowCommand):
def run(self):
self.get_window().show_input_panel("Branch name", "",
self.on_input, None, None)
def on_input(self, branchname):
if branchname.strip() == "":
self.panel("No branch name provided")
return
self.run_command(['git', 'checkout', '-b', branchname])
class GitNewTagCommand(GitWindowCommand):
def run(self):
self.get_window().show_input_panel("Tag name", "", self.on_input, None, None)
def on_input(self, tagname):
if not tagname.strip():
self.panel("No branch name provided")
return
self.run_command(['git', 'tag', tagname])
class GitShowTagsCommand(GitWindowCommand):
def run(self):
self.run_command(['git', 'tag'], self.fetch_tag)
def fetch_tag(self, result):
self.results = result.rstrip().split('\n')
self.quick_panel(self.results, self.panel_done)
def panel_done(self, picked):
if 0 > picked < len(self.results):
return
picked_tag = self.results[picked]
picked_tag = picked_tag.strip()
self.run_command(['git', 'show', picked_tag])
class GitPushTagsCommand(GitWindowCommand):
def run(self):
self.run_command(['git', 'push', '--tags'])
class GitCheckoutCommand(GitTextCommand):
may_change_files = True
def run(self, edit):
self.run_command(['git', 'checkout', self.get_file_name()])
class GitFetchCommand(GitWindowCommand):
def run(self):
self.run_command(['git', 'fetch'], callback=self.panel)
class GitPullCommand(GitWindowCommand):
def run(self):
self.run_command(['git', 'pull'], callback=self.panel)
class GitPullCurrentBranchCommand(GitWindowCommand):
command_to_run_after_describe = 'pull'
def run(self):
self.run_command(['git', 'describe', '--contains', '--all', 'HEAD'], callback=self.describe_done)
def describe_done(self, result):
self.current_branch = result.strip()
self.run_command(['git', 'remote'], callback=self.remote_done)
def remote_done(self, result):
self.remotes = result.rstrip().split('\n')
if len(self.remotes) == 1:
self.panel_done()
else:
self.quick_panel(self.remotes, self.panel_done, sublime.MONOSPACE_FONT)
def panel_done(self, picked=0):
if picked < 0 or picked >= len(self.remotes):
return
self.picked_remote = self.remotes[picked]
self.picked_remote = self.picked_remote.strip()
self.run_command(['git', self.command_to_run_after_describe, self.picked_remote, self.current_branch])
class GitPushCommand(GitWindowCommand):
def run(self):
self.run_command(['git', 'push'], callback=self.panel)
class GitPushCurrentBranchCommand(GitPullCurrentBranchCommand):
command_to_run_after_describe = 'push'

View File

@@ -0,0 +1,47 @@
from git import GitWindowCommand
class GitStashCommand(GitWindowCommand):
may_change_files = True
def run(self):
self.run_command(['git', 'stash'])
class GitStashPopCommand(GitWindowCommand):
def run(self):
self.run_command(['git', 'stash', 'pop'])
class GitStashApplyCommand(GitWindowCommand):
may_change_files = True
command_to_run_after_list = 'apply'
def run(self):
self.run_command(['git', 'stash', 'list'], self.stash_list_done)
def stash_list_done(self, result):
# No stash list at all
if not result:
self.panel('No stash found')
return
self.results = result.rstrip().split('\n')
# If there is only one, apply it
if len(self.results) == 1:
self.stash_list_panel_done()
else:
self.quick_panel(self.results, self.stash_list_panel_done)
def stash_list_panel_done(self, picked=0):
if 0 > picked < len(self.results):
return
# get the stash ref (e.g. stash@{3})
self.stash = self.results[picked].split(':')[0]
self.run_command(['git', 'stash', self.command_to_run_after_list, self.stash])
class GitStashDropCommand(GitStashApplyCommand):
command_to_run_after_list = 'drop'

View File

@@ -0,0 +1,63 @@
import os
import re
import sublime
from git import GitWindowCommand, git_root
class GitStatusCommand(GitWindowCommand):
force_open = False
def run(self):
self.run_command(['git', 'status', '--porcelain'], self.status_done)
def status_done(self, result):
self.results = filter(self.status_filter, result.rstrip().split('\n'))
if len(self.results):
self.show_status_list()
else:
sublime.status_message("Nothing to show")
def show_status_list(self):
self.quick_panel(self.results, self.panel_done,
sublime.MONOSPACE_FONT)
def status_filter(self, item):
# for this class we don't actually care
if not re.match(r'^[ MADRCU?!]{1,2}\s+.*', item):
return False
return len(item) > 0
def panel_done(self, picked):
if 0 > picked < len(self.results):
return
picked_file = self.results[picked]
# first 2 characters are status codes, the third is a space
picked_status = picked_file[:2]
picked_file = picked_file[3:]
self.panel_followup(picked_status, picked_file, picked)
def panel_followup(self, picked_status, picked_file, picked_index):
# split out solely so I can override it for laughs
s = sublime.load_settings("Git.sublime-settings")
root = git_root(self.get_working_dir())
if picked_status == '??' or s.get('status_opens_file') or self.force_open:
if(os.path.isfile(os.path.join(root, picked_file))):
self.window.open_file(os.path.join(root, picked_file))
else:
self.run_command(['git', 'diff', '--no-color', '--', picked_file.strip('"')],
self.diff_done, working_dir=root)
def diff_done(self, result):
if not result.strip():
return
self.scratch(result, title="Git Diff")
class GitOpenModifiedFilesCommand(GitStatusCommand):
force_open = True
def show_status_list(self):
for line_index in range(0, len(self.results)):
self.panel_done(line_index)

View File

@@ -0,0 +1,58 @@
import re
import sublime
import sublime_plugin
from git import GitTextCommand
class GitBranchStatusListener(sublime_plugin.EventListener):
def on_activated(self, view):
view.run_command("git_branch_status")
def on_post_save(self, view):
view.run_command("git_branch_status")
class GitBranchStatusCommand(GitTextCommand):
def run(self, view):
s = sublime.load_settings("Git.sublime-settings")
if s.get("statusbar_branch"):
self.run_command(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], self.branch_done, show_status=False, no_save=True)
else:
self.view.set_status("git-branch", "")
if (s.get("statusbar_status")):
self.run_command(['git', 'status', '--porcelain'], self.status_done, show_status=False, no_save=True)
else:
self.view.set_status("git-status", "")
def branch_done(self, result):
self.view.set_status("git-branch", "git branch: " + result.strip())
def status_done(self, result):
lines = [line for line in result.splitlines() if re.match(r'^[ MADRCU?!]{1,2}\s+.*', line)]
index = [line[0] for line in lines if not line[0].isspace()]
working = [line[1] for line in lines if not line[1].isspace()]
self.view.set_status("git-status-index", "index: " + self.status_string(index))
self.view.set_status("git-status-working", "working: " + self.status_string(working))
def status_string(self, statuses):
s = sublime.load_settings("Git.sublime-settings")
symbols = s.get("statusbar_status_symbols")
if not statuses:
return symbols['clean']
status = []
if statuses.count('M'):
status.append("%d%s" % (statuses.count('M'), symbols['modified']))
if statuses.count('A'):
status.append("%d%s" % (statuses.count('A'), symbols['added']))
if statuses.count('D'):
status.append("%d%s" % (statuses.count('D'), symbols['deleted']))
if statuses.count('?'):
status.append("%d%s" % (statuses.count('?'), symbols['untracked']))
if statuses.count('U'):
status.append("%d%s" % (statuses.count('U'), symbols['conflicts']))
if statuses.count('R'):
status.append("%d%s" % (statuses.count('R'), symbols['renamed']))
if statuses.count('C'):
status.append("%d%s" % (statuses.count('C'), symbols['copied']))
return symbols['separator'].join(status)

View File

@@ -0,0 +1,18 @@
{ "name": "Git Blame",
"scopeName": "text.git-blame",
"fileTypes": ["git-blame"],
"patterns": [
{
"match": "^(\\^?[a-f0-9]+)\\s+([\\w\\-\\d\\.\\/]*)\\s*\\((.*?)\\s+(\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d [+-]\\d{4})\\s+(\\d+)\\)",
"name": "line.comment.git-blame",
"captures": {
"1": {"name": "string.sha.git-blame"},
"2": {"name": "string.path.git-blame"},
"3": {"name": "support.function.author.git-blame"},
"4": {"name": "constant.numeric.date.git-blame"},
"5": {"name": "variable.parameter.line-number.git-blame"}
}
}
],
"uuid": "5d37add9-889e-4174-b232-4bd423b84c0a"
}

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>fileTypes</key>
<array>
<string>git-blame</string>
</array>
<key>name</key>
<string>Git Blame</string>
<key>patterns</key>
<array>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>string.sha.git-blame</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>string.path.git-blame</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>support.function.author.git-blame</string>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>constant.numeric.date.git-blame</string>
</dict>
<key>5</key>
<dict>
<key>name</key>
<string>variable.parameter.line-number.git-blame</string>
</dict>
</dict>
<key>match</key>
<string>^(\^?[a-f0-9]+)\s+([\w\-\d\.\/]*)\s*\((.*?)\s+(\d{4}-\d\d-\d\d( \d\d:\d\d:\d\d [+-]\d{4})?)\s+(\d+)\)</string>
<key>name</key>
<string>line.comment.git-blame</string>
</dict>
</array>
<key>scopeName</key>
<string>text.git-blame</string>
<key>uuid</key>
<string>5d37add9-889e-4174-b232-4bd423b84c0a</string>
</dict>
</plist>

View File

@@ -0,0 +1,21 @@
{ "name": "Git Commit Message",
"scopeName": "text.git-commit",
"fileTypes": ["COMMIT_EDITMSG"],
"patterns": [
{ "name": "comment.line.number-sign.git-commit",
"match": "^\\s*(#).*$\n?",
"captures": {
"1": { "name": "punctuation.definition.comment.git-commit" }
}
},
{ "name": "meta.diff.git-commit",
"comment": "diff at the end of the commit message when using commit -v, or viewing a log. End pattern is just something to be never matched so that the meta continues untill the end of the file.",
"begin": "diff\\ \\-\\-git",
"end": "(?=xxxxxx)123457",
"patterns": [
{ "include": "source.diff" }
]
}
],
"uuid": "de3fb2fc-e564-4a31-9813-5ee26967c5c8"
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>fileTypes</key>
<array>
<string>COMMIT_EDITMSG</string>
</array>
<key>name</key>
<string>Git Commit Message</string>
<key>patterns</key>
<array>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.comment.git-commit</string>
</dict>
</dict>
<key>match</key>
<string>^\s*(#).*$
?</string>
<key>name</key>
<string>comment.line.number-sign.git-commit</string>
</dict>
<dict>
<key>begin</key>
<string>diff\ \-\-git</string>
<key>comment</key>
<string>diff at the end of the commit message when using commit -v, or viewing a log. End pattern is just something to be never matched so that the meta continues untill the end of the file.</string>
<key>end</key>
<string>(?=xxxxxx)123457</string>
<key>name</key>
<string>meta.diff.git-commit</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>source.diff</string>
</dict>
</array>
</dict>
</array>
<key>scopeName</key>
<string>text.git-commit</string>
<key>uuid</key>
<string>de3fb2fc-e564-4a31-9813-5ee26967c5c8</string>
</dict>
</plist>

View File

@@ -0,0 +1,31 @@
{ "name": "Git Graph",
"scopeName": "text.git-graph",
"fileTypes": ["git-graph"],
"patterns": [
{ "match": "^([| *\\\\]+)([0-9a-f]{4,40}) (.*?) (\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d [+-]\\d{4}) (?:\\(((?:[a-zA-Z0-9._\\-\\/]+(?:, )?)+)\\) )?",
"name": "log-entry.git-graph",
"captures": {
"1": {"name": "comment.git-graph" },
"2": {"name": "string.git-graph" },
"3": {"name": "support.function.git-graph" },
"4": {"name": "constant.numeric.git-graph" },
"5": {"name": "variable.parameter.git-graph" }
}
},
{ "match": "^\\|[\\|_\\/\\\\ ]+\n?$",
"name": "comment.git-graph",
"comment": "lines with no commit details"
},
{ "match": "(?:[Ff]ix(?:e[ds])?|[Rr]esolve[ds]?|[Cc]lose[ds]?)?\\s*(?:#\\d+|\\[.*?\\])",
"name": "keyword.git-graph",
"comment": "issue numbers"
},
{ "match": "Merge branch '(.*?)' of .*?\n?$",
"name": "comment.git-graph",
"captures": {
"1": {"name": "variable.parameter.git-graph"}
}
}
],
"uuid": "b900521e-af64-471b-aec8-1ecf88aab595"
}

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>fileTypes</key>
<array>
<string>git-graph</string>
</array>
<key>name</key>
<string>Git Graph</string>
<key>patterns</key>
<array>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>comment.git-graph</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>string.git-graph</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>support.function.git-graph</string>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>constant.numeric.git-graph</string>
</dict>
<key>5</key>
<dict>
<key>name</key>
<string>variable.parameter.git-graph</string>
</dict>
<key>6</key>
<dict>
<key>name</key>
<string>keyword.git-graph</string>
</dict>
</dict>
<key>match</key>
<string>^([| *\\]+)([0-9a-f]{4,40}) -( \(.*?\))? (.*) (\(.*) (&lt;.*&gt;) .*</string>
<key>name</key>
<string>log-entry.git-graph</string>
</dict>
<dict>
<key>comment</key>
<string>lines with no commit details</string>
<key>match</key>
<string>^\|[\|_\/\\ ]+
?$</string>
<key>name</key>
<string>comment.git-graph</string>
</dict>
<dict>
<key>comment</key>
<string>issue numbers</string>
<key>match</key>
<string>(?:[Ff]ix(?:e[ds])?|[Rr]esolve[ds]?|[Cc]lose[ds]?)?\s*(?:#\d+|\[.*?\])</string>
<key>name</key>
<string>keyword.git-graph</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.parameter.git-graph</string>
</dict>
</dict>
<key>match</key>
<string>Merge branch '(.*?)' of .*?
?$</string>
<key>name</key>
<string>comment.git-graph</string>
</dict>
</array>
<key>scopeName</key>
<string>text.git-graph</string>
<key>uuid</key>
<string>b900521e-af64-471b-aec8-1ecf88aab595</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
*.pyc

View File

@@ -0,0 +1,6 @@
[
{ "command": "githubinator", "caption": "GitHubinator", "args": { "permalink": false } },
{ "command": "githubinator", "caption": "GitHubinator Permalink", "args": { "permalink": true } },
{ "command": "githubinator", "caption": "GitHubinator Blame", "args": { "permalink": false, "mode": "blame" } },
{ "command": "githubinator", "caption": "GitHubinator Blame Permalink", "args": { "permalink": true, "mode": "blame" } }
]

View File

@@ -0,0 +1,4 @@
[
{ "keys": ["super+\\"], "command": "githubinator", "args" : { "permalink": false }},
{ "keys": ["shift+super+\\"], "command": "githubinator", "args" : { "permalink": true }}
]

View File

@@ -0,0 +1,22 @@
[
{
"caption": "GitHubinator",
"command": "githubinator",
"args": { "permalink": false }
},
{
"caption": "GitHubinator Permalink",
"command": "githubinator",
"args": { "permalink": true }
},
{
"caption": "GitHubinator Blame",
"command": "githubinator",
"args": { "permalink": false, "mode": "blame" }
},
{
"caption": "GitHubinator Blame Permalink",
"command": "githubinator",
"args": { "permalink": true, "mode": "blame" }
}
]

View File

@@ -0,0 +1,3 @@
{
"default_remote": "origin"
}

View File

@@ -0,0 +1,34 @@
# GitHubinator*
*_With regards to [Dr. Heinz Doofenshmirtz](http://en.wikipedia.org/wiki/Dr._Heinz_Doofenshmirtz)_
This will allow you to select text in a Sublime Text 2 file, and see the highlighted lines on GitHub's remote repo, if one exists.
![Screenshot](http://i.imgur.com/lcJ78.png)
## Installation
If you use [Package Control](http://wbond.net/sublime_packages/package_control), just install it from there. If not:
Clone this repo to your Sublime Text 2 Packages folder:
cd ~/"Library/Application Support/Sublime Text 2/Packages/"
git clone https://github.com/ehamiter/ST2-GitHubinator.git
The plugin should be picked up automatically. If not, restart Sublime Text.
## Configuration
The defaults should work for most setups, but if you have a different remote name, you can configure it in the `Githubinator.sublime-settings` file:
{
"default_remote": "origin"
}
## Usage
Select some text.
Activate the context menu and select "GitHubinator" or by keypress (&#8984;\\ by default, configurable in .sublime-keymap file).

View File

@@ -0,0 +1,76 @@
import os
import re
import sublime
import sublime_plugin
class GithubinatorCommand(sublime_plugin.TextCommand):
'''This will allow you to highlight your code, activate the plugin, then see the
highlighted results on GitHub.
'''
def load_config(self):
s = sublime.load_settings("Githubinator.sublime-settings")
global DEFAULT_GIT_REMOTE; DEFAULT_GIT_REMOTE = s.get("default_remote")
def run(self, edit, permalink = False, mode = 'blob'):
self.load_config()
if not self.view.file_name():
return
full_name = os.path.realpath(self.view.file_name())
folder_name, file_name = os.path.split(full_name)
git_path = self.recurse_dir(folder_name, '.git')
if not git_path:
sublime.status_message('Could not find .git directory.')
print('Could not find .git directory.')
return
git_config_path = os.path.join(git_path, '.git', 'config')
new_git_path = folder_name[len(git_path):]
with open(git_config_path, "r") as git_config_file:
config = git_config_file.read()
sel = self.view.sel()[0]
begin_line = self.view.rowcol(sel.begin())[0] + 1
end_line = self.view.rowcol(sel.end())[0] + 1
if begin_line == end_line:
lines = begin_line
else:
lines = '%s-%s' % (begin_line, end_line)
for remote in [DEFAULT_GIT_REMOTE]:
regex = r'.*\s.*(?:https://github\.com/|github\.com:|git://github\.com/)(.*)/(.*?)(?:\.git)?\r?\n'
result = re.search(remote + regex, config)
if not result:
continue
matches = result.groups()
ref_path = open(os.path.join(git_path, '.git', 'HEAD'), "r").read().replace('ref: ', '')[:-1]
branch = ref_path.replace('refs/heads/','')
sha = open(os.path.join(git_path, '.git', ref_path), "r").read()[:-1]
target = sha if permalink else branch
full_link = 'https://github.com/%s/%s/%s/%s%s/%s#L%s' % \
(matches[0], matches[1], mode, target, new_git_path, file_name, lines)
sublime.set_clipboard(full_link)
sublime.status_message('Copied %s to clipboard.' % full_link)
print('Copied %s to clipboard.' % full_link)
self.view.window().run_command('open_url', {"url": full_link})
break
def recurse_dir(self, path, folder):
items = os.listdir(path)
if folder in items and os.path.isdir(os.path.join(path, folder)):
return path
dirname = os.path.dirname(path)
if dirname == path:
return None
return self.recurse_dir(dirname, folder)
def is_enabled(self):
return self.view.file_name() and len(self.view.file_name()) > 0

View File

@@ -0,0 +1 @@
{"url": "https://github.com/ehamiter/ST2-GitHubinator", "version": "2013.03.02.08.48.58", "description": "Sublime Text 2 plugin that shows selected ST2 text on GitHub"}

View File

@@ -0,0 +1,3 @@
*.pyc
images
!.gitignore

View File

@@ -0,0 +1,9 @@
[
{ "keys": ["ctrl+alt+c"], "command": "show_original_part" },
{ "keys": ["ctrl+alt+r"], "command": "replace_modified_part" },
{ "keys": ["ctrl+alt+d"], "command": "show_diff" },
{ "keys": ["ctrl+alt+u"], "command": "uncommitted_files" },
{ "keys": ["ctrl+shift+pageup"], "command": "jump_between_changes", "args": {"direction": "prev"} },
{ "keys": ["ctrl+shift+pagedown"], "command": "jump_between_changes", "args": {"direction": "next"} }
]

View File

@@ -0,0 +1,9 @@
[
{ "keys": ["ctrl+super+c"], "command": "show_original_part" },
{ "keys": ["ctrl+super+r"], "command": "replace_modified_part" },
{ "keys": ["ctrl+alt+d"], "command": "show_diff" },
{ "keys": ["ctrl+super+u"], "command": "uncommitted_files" },
{ "keys": ["ctrl+shift+pageup"], "command": "jump_between_changes", "args": {"direction": "prev"} },
{ "keys": ["ctrl+shift+pagedown"], "command": "jump_between_changes", "args": {"direction": "next"} }
]

View File

@@ -0,0 +1,9 @@
[
{ "keys": ["ctrl+alt+c"], "command": "show_original_part" },
{ "keys": ["ctrl+alt+r"], "command": "replace_modified_part" },
{ "keys": ["ctrl+alt+d"], "command": "show_diff" },
{ "keys": ["ctrl+alt+u"], "command": "uncommitted_files" },
{ "keys": ["ctrl+shift+pageup"], "command": "jump_between_changes", "args": {"direction": "prev"} },
{ "keys": ["ctrl+shift+pagedown"], "command": "jump_between_changes", "args": {"direction": "next"} }
]

View File

@@ -0,0 +1,90 @@
[
{
"caption": "Preferences",
"mnemonic": "n",
"id": "preferences",
"children":
[
{
"caption": "Package Settings",
"mnemonic": "P",
"id": "package-settings",
"children":
[
{
"caption": "Modific",
"children":
[
{
"command": "open_file",
"args": {"file": "${packages}/Modific/README.md"},
"caption": "README"
},
{ "caption": "-" },
{
"command": "open_file",
"args": {"file": "${packages}/Modific/Modific.sublime-settings"},
"caption": "Settings Default"
},
{
"command": "open_file",
"args": {"file": "${packages}/User/Modific.sublime-settings"},
"caption": "Settings User"
},
{ "caption": "-" },
{
"command": "open_file",
"args": {
"file": "${packages}/Modific/Default (OSX).sublime-keymap",
"platform": "OSX"
},
"caption": "Key Bindings Default"
},
{
"command": "open_file",
"args": {
"file": "${packages}/Modific/Default (Linux).sublime-keymap",
"platform": "Linux"
},
"caption": "Key Bindings Default"
},
{
"command": "open_file",
"args": {
"file": "${packages}/Modific/Default (Windows).sublime-keymap",
"platform": "Windows"
},
"caption": "Key Bindings Default"
},
{
"command": "open_file",
"args": {
"file": "${packages}/User/Default (OSX).sublime-keymap",
"platform": "OSX"
},
"caption": "Key Bindings User"
},
{
"command": "open_file",
"args": {
"file": "${packages}/User/Default (Linux).sublime-keymap",
"platform": "Linux"
},
"caption": "Key Bindings User"
},
{
"command": "open_file",
"args": {
"file": "${packages}/User/Default (Windows).sublime-keymap",
"platform": "Windows"
},
"caption": "Key Bindings User"
},
{ "caption": "-" }
]
}
]
}
]
}
]

View File

@@ -0,0 +1,618 @@
# -*- coding: utf-8 -*-
import sublime
import sublime_plugin
import os
import threading
import subprocess
import functools
import re
IS_ST3 = sublime.version().startswith('3')
def get_settings():
return sublime.load_settings("Modific.sublime-settings")
def get_vcs_settings():
return get_settings().get('vcs', [
["git", "git"],
["svn", "svn"],
["bzr", "bzr"],
["hg", "hg"]
])
def vcs_root(directory):
"""
Determines root directory for VCS
"""
vcs_check = [(lambda vcs: lambda dir: os.path.exists(os.path.join(dir, '.' + vcs))
and {'root': dir, 'name': vcs})(vcs)
for vcs, _ in get_vcs_settings()]
while directory:
available = list(filter(lambda x: x, [check(directory) for check in vcs_check]))
if available:
return directory, available[0]
parent = os.path.realpath(os.path.join(directory, os.path.pardir))
if parent == directory:
# /.. == /
return None, None
directory = parent
return None, None
def get_vcs(directory):
"""
Determines, which of VCS systems we should use for given folder.
Currently, uses priority of definitions in get_vcs_settings()
"""
root_dir, vcs = vcs_root(directory)
return vcs
def main_thread(callback, *args, **kwargs):
# sublime.set_timeout gets used to send things onto the main thread
# most sublime.[something] calls need to be on the main thread
sublime.set_timeout(functools.partial(callback, *args, **kwargs), 0)
def _make_text_safeish(text, fallback_encoding, method='decode'):
# The unicode decode here is because sublime converts to unicode inside
# insert in such a way that unknown characters will cause errors, which is
# distinctly non-ideal... and there's no way to tell what's coming out of
# git in output. So...
try:
unitext = getattr(text, method)('utf-8')
except (UnicodeEncodeError, UnicodeDecodeError):
unitext = getattr(text, method)(fallback_encoding)
except AttributeError:
# strongly implies we're already unicode, but just in case let's cast
# to string
unitext = str(text)
return unitext
def do_when(conditional, callback, *args, **kwargs):
if conditional():
return callback(*args, **kwargs)
sublime.set_timeout(functools.partial(do_when, conditional, callback, *args, **kwargs), 50)
class CommandThread(threading.Thread):
def __init__(self, command, on_done, working_dir="", fallback_encoding="", console_encoding="", **kwargs):
threading.Thread.__init__(self)
self.command = command
self.on_done = on_done
self.working_dir = working_dir
if 'stdin' in kwargs:
self.stdin = kwargs['stdin'].encode()
else:
self.stdin = None
self.stdout = kwargs.get('stdout', subprocess.PIPE)
self.console_encoding = console_encoding
self.fallback_encoding = fallback_encoding
self.kwargs = kwargs
def run(self):
try:
# Per http://bugs.python.org/issue8557 shell=True is required to
# get $PATH on Windows. Yay portable code.
shell = os.name == 'nt'
if self.working_dir != "":
os.chdir(self.working_dir)
if self.console_encoding:
self.command = [s.encode(self.console_encoding) for s in self.command]
proc = subprocess.Popen(self.command,
stdout=self.stdout, stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
shell=shell, universal_newlines=True)
output = proc.communicate(self.stdin)[0]
if not output:
output = ''
# if sublime's python gets bumped to 2.7 we can just do:
# output = subprocess.check_output(self.command)
main_thread(self.on_done,
_make_text_safeish(output, self.fallback_encoding), **self.kwargs)
except subprocess.CalledProcessError as e:
main_thread(self.on_done, e.returncode)
except OSError as e:
if e.errno == 2:
main_thread(sublime.error_message,
"'%s' binary could not be found in PATH\n\nConsider using `vcs` property to specify PATH\n\nPATH is: %s" % (self.command[0], os.environ['PATH']))
else:
raise e
class EditViewCommand(sublime_plugin.TextCommand):
def run(self, edit, command=None, output='', begin=0, region=None):
"""
For some reason Sublime's view.run_command() doesn't allow to pass tuples,
therefore region must be a list
"""
region = sublime.Region(int(region[0]), int(region[1])) if region else None
if command == 'insert':
self.view.insert(edit, int(begin), output)
elif command == 'replace':
self.view.replace(edit, region, output)
elif command == 'erase':
self.view.erase(edit, region)
else:
print('Invalid command: ', command)
raise
class VcsCommand(object):
may_change_files = False
def __init__(self, *args, **kwargs):
self.settings = get_settings()
super(VcsCommand, self).__init__(*args, **kwargs)
def run_command(self, command, callback=None, show_status=True,
filter_empty_args=True, **kwargs):
if filter_empty_args:
command = [arg for arg in command if arg]
if 'working_dir' not in kwargs:
kwargs['working_dir'] = self.get_working_dir()
if 'fallback_encoding' not in kwargs and self.active_view() and self.active_view().settings().get('fallback_encoding'):
kwargs['fallback_encoding'] = self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0]
kwargs['console_encoding'] = self.settings.get('console_encoding')
autosave = self.settings.get('autosave', True)
if self.active_view() and self.active_view().is_dirty() and autosave:
self.active_view().run_command('save')
if not callback:
callback = self.generic_done
thread = CommandThread(command, callback, **kwargs)
thread.start()
if show_status:
message = kwargs.get('status_message', False) or ' '.join(command)
sublime.status_message(message)
def generic_done(self, result):
if self.may_change_files and self.active_view() and self.active_view().file_name():
if self.active_view().is_dirty():
result = "WARNING: Current view is dirty.\n\n"
else:
# just asking the current file to be re-opened doesn't do anything
print("reverting")
position = self.active_view().viewport_position()
self.active_view().run_command('revert')
do_when(lambda: not self.active_view().is_loading(), lambda: self.active_view().set_viewport_position(position, False))
if not result.strip():
return
self.panel(result)
def _output_to_view(self, output_file, output, clear=False,
syntax="Packages/Diff/Diff.tmLanguage"):
output_file.set_syntax_file(syntax)
if clear:
output_file.run_command('edit_view', dict(command='replace', region=[0, self.output_view.size()], output=output))
else:
output_file.run_command('edit_view', dict(command='insert', output=output))
def scratch(self, output, title=False, position=None, **kwargs):
scratch_file = self.get_window().new_file()
if title:
scratch_file.set_name(title)
scratch_file.set_scratch(True)
self._output_to_view(scratch_file, output, **kwargs)
scratch_file.set_read_only(True)
if position:
sublime.set_timeout(lambda: scratch_file.set_viewport_position(position), 0)
return scratch_file
def panel(self, output, **kwargs):
if not hasattr(self, 'output_view'):
self.output_view = self.get_window().get_output_panel("vcs")
self.output_view.set_read_only(False)
self._output_to_view(self.output_view, output, clear=True, **kwargs)
self.output_view.set_read_only(True)
self.get_window().run_command("show_panel", {"panel": "output.vcs"})
def _active_file_name(self):
view = self.active_view()
if view and view.file_name() and len(view.file_name()) > 0:
return view.file_name()
def active_view(self):
return self.view
def get_window(self):
if (hasattr(self, 'view') and hasattr(self.view, 'window')):
return self.view.window()
else:
return sublime.active_window()
def get_working_dir(self):
return os.path.dirname(self._active_file_name())
def is_enabled(self):
file_name = self._active_file_name()
if file_name and os.path.exists(file_name):
return bool(get_vcs(self.get_working_dir()))
return False
def get_user_command(self, vcs_name):
return dict(get_vcs_settings()).get(vcs_name, False)
class DiffCommand(VcsCommand):
""" Here you can define diff commands for your VCS
method name pattern: %(vcs_name)s_diff_command
"""
def run(self, edit):
vcs = get_vcs(self.get_working_dir())
filepath = self.view.file_name()
filename = os.path.basename(filepath)
max_file_size = self.settings.get('max_file_size', 1024) * 1024
if not os.path.exists(filepath) or os.path.getsize(filepath) > max_file_size:
# skip large files
return
get_command = getattr(self, '{0}_diff_command'.format(vcs['name']), None)
if get_command:
self.run_command(get_command(filename), self.diff_done)
def diff_done(self, result):
pass
def git_diff_command(self, file_name):
return [self.get_user_command('git') or 'git', 'diff', '--no-color', '--no-ext-diff', '--', file_name]
def svn_diff_command(self, file_name):
params = [self.get_user_command('svn') or 'svn', 'diff']
if self.settings.get('svn_use_internal_diff', True):
params.append('--internal-diff')
if file_name.find('@') != -1:
file_name += '@'
params.extend(['--revision', 'HEAD'])
params.extend([file_name])
return params
def bzr_diff_command(self, file_name):
return [self.get_user_command('bzr') or 'bzr', 'diff', file_name]
def hg_diff_command(self, file_name):
return [self.get_user_command('hg') or 'hg', 'diff', file_name]
class ShowDiffCommand(DiffCommand, sublime_plugin.TextCommand):
def diff_done(self, result):
if not result.strip():
return
file_name = re.findall(r'([^\\\/]+)$', self.view.file_name())
self.scratch(result, title="Diff - " + file_name[0])
class DiffParser(object):
instance = None
def __init__(self, diff):
self.diff = diff
self.chunks = None
self.__class__.instance = self
def _append_to_chunks(self, start, lines):
self.chunks.append({
"start": start,
"end": start + len(lines),
"lines": lines
})
def get_chunks(self):
if self.chunks is None:
self.chunks = []
diff = self.diff.strip()
if diff:
re_header = re.compile(r'^@@[0-9\-, ]+\+(\d+)', re.S)
current = None
lines = []
for line in diff.splitlines():
# ignore lines with '\' at the beginning
if line.startswith('\\'):
continue
matches = re.findall(re_header, line)
if matches:
if current is not None:
self._append_to_chunks(current, lines)
current = int(matches[0])
lines = []
elif current:
lines.append(line)
if current is not None and lines:
self._append_to_chunks(current, lines)
return self.chunks
def get_lines_to_hl(self):
inserted = []
changed = []
deleted = []
for chunk in self.get_chunks():
current = chunk['start']
deleted_line = None
for line in chunk['lines']:
if line.startswith('-'):
if (not deleted_line or deleted_line not in deleted):
deleted.append(current)
deleted_line = current
elif line.startswith('+'):
if deleted_line:
deleted.pop()
deleted_line = None
changed.append(current)
elif current - 1 in changed:
changed.append(current)
else:
inserted.append(current)
current += 1
else:
deleted_line = None
current += 1
return inserted, changed, deleted
def get_original_part(self, line_num):
""" returns a chunk of code that relates to the given line
and was there before modifications
return (lines list, start_line int, replace_lines int)
"""
# for each chunk from diff:
for chunk in self.get_chunks():
# if line_num is within that chunk
if chunk['start'] <= line_num <= chunk['end']:
ret_lines = []
current = chunk['start'] # line number that corresponds to current version of file
first = None # number of the first line to change
replace_lines = 0 # number of lines to change
return_this_lines = False # flag shows whether we can return accumulated lines
for line in chunk['lines']:
if line.startswith('-') or line.startswith('+'):
first = first or current
if current == line_num:
return_this_lines = True
if line.startswith('-'):
# if line starts with '-' we have previous version
ret_lines.append(line[1:])
else:
# if line starts with '+' we only increment numbers
replace_lines += 1
current += 1
elif return_this_lines:
break
else:
# gap between modifications
# reset our variables
current += 1
first = current
replace_lines = 0
ret_lines = []
if return_this_lines:
return ret_lines, first, replace_lines
return None, None, None
class HlChangesCommand(DiffCommand, sublime_plugin.TextCommand):
def hl_lines(self, lines, hl_key):
if (not len(lines)):
self.view.erase_regions(hl_key)
return
icon = self.settings.get('region_icon') or 'modific'
if icon == 'modific':
if IS_ST3:
icon = 'Packages/Modific/icons/' + hl_key + '.png'
else:
icon = '../Modific/icons/' + hl_key
points = [self.view.text_point(l - 1, 0) for l in lines]
regions = [sublime.Region(p, p) for p in points]
self.view.add_regions(hl_key, regions, "markup.%s.diff" % hl_key,
icon, sublime.HIDDEN | sublime.DRAW_EMPTY)
def diff_done(self, diff):
if diff and '@@' not in diff:
# probably this is an error message
# if print raise UnicodeEncodeError, try to encode string to utf-8 (issue #35)
try:
print(diff)
except UnicodeEncodeError:
print(diff.encode('utf-8'))
diff_parser = DiffParser(diff)
(inserted, changed, deleted) = diff_parser.get_lines_to_hl()
if self.settings.get('debug'):
print(inserted, changed, deleted)
self.hl_lines(inserted, 'inserted')
self.hl_lines(deleted, 'deleted')
self.hl_lines(changed, 'changed')
class ShowOriginalPartCommand(DiffCommand, sublime_plugin.TextCommand):
def run(self, edit):
diff_parser = DiffParser.instance
if not diff_parser:
return
(row, col) = self.view.rowcol(self.view.sel()[0].begin())
(lines, start, replace_lines) = diff_parser.get_original_part(row + 1)
if lines is not None:
self.panel(os.linesep.join(lines))
class ReplaceModifiedPartCommand(DiffCommand, sublime_plugin.TextCommand):
def run(self, edit):
self.view.run_command('save')
diff_parser = DiffParser.instance
if not diff_parser:
return
(row, col) = self.view.rowcol(self.view.sel()[0].begin())
(lines, current, replace_lines) = diff_parser.get_original_part(row + 1)
if self.settings.get('debug'):
print('replace', (lines, current, replace_lines))
if lines is not None:
begin = self.view.text_point(current - 1, 0)
content = os.linesep.join(lines)
if replace_lines:
end = self.view.line(self.view.text_point(replace_lines + current - 2, 0)).end()
region = sublime.Region(begin, end)
if lines:
self.view.run_command('edit_view', dict(command='replace', region=[region.begin(), region.end()], output=content))
else:
region = self.view.full_line(region)
self.view.run_command('edit_view', dict(command='erase', region=[region.begin(), region.end()]))
else:
self.view.run_command('edit_view', dict(command='insert', begin=begin, output=content + os.linesep))
self.view.run_command('save')
class HlChangesBackground(sublime_plugin.EventListener):
def on_load(self, view):
view.run_command('hl_changes')
def on_activated(self, view):
view.run_command('hl_changes')
def on_post_save(self, view):
view.run_command('hl_changes')
class JumpBetweenChangesCommand(DiffCommand, sublime_plugin.TextCommand):
def run(self, edit, direction='next'):
lines = self._get_lines()
if not lines:
return
if direction == 'prev':
lines.reverse()
(current_line, col) = self.view.rowcol(self.view.sel()[0].begin())
current_line += 1
jump_to = None
for line in lines:
if direction == 'next' and current_line < line:
jump_to = line
break
if direction == 'prev' and current_line > line:
jump_to = line
break
if not jump_to:
jump_to = lines[0]
self.goto_line(edit, jump_to)
def goto_line(self, edit, line):
# Convert from 1 based to a 0 based line number
line = int(line) - 1
# Negative line numbers count from the end of the buffer
if line < 0:
lines, _ = self.view.rowcol(self.view.size())
line = lines + line + 1
pt = self.view.text_point(line, 0)
self.view.sel().clear()
self.view.sel().add(sublime.Region(pt))
self.view.show(pt)
def _get_lines(self):
diff_parser = DiffParser.instance
if not diff_parser:
return
(inserted, changed, deleted) = diff_parser.get_lines_to_hl()
lines = list(set(inserted + changed + deleted))
lines.sort()
prev = None
ret_lines = []
for line in lines:
if prev != line - 1:
ret_lines.append(line)
prev = line
return ret_lines
class UncommittedFilesCommand(VcsCommand, sublime_plugin.WindowCommand):
def active_view(self):
return self.window.active_view()
def run(self):
self.root, self.vcs = vcs_root(self.get_working_dir())
status_command = getattr(self, '{0}_status_command'.format(self.vcs['name']), None)
if status_command:
self.run_command(status_command(), self.status_done, working_dir=self.root)
def git_status_command(self):
return [self.get_user_command('git') or 'git', 'status', '--porcelain']
def svn_status_command(self):
return [self.get_user_command('svn') or 'svn', 'status', '--quiet']
def bzr_status_command(self):
return [self.get_user_command('bzr') or 'bzr', 'status', '-S', '--no-pending', '-V']
def hg_status_command(self):
return [self.get_user_command('hg') or 'hg', 'status']
def git_status_file(self, file_name):
# first 2 characters are status codes, the third is a space
return file_name[3:]
def svn_status_file(self, file_name):
return file_name[8:]
def bzr_status_file(self, file_name):
return file_name[4:]
def hg_status_file(self, file_name):
return file_name[2:]
def status_done(self, result):
self.results = list(filter(lambda x: len(x) > 0 and not x.lstrip().startswith('>'),
result.rstrip().split('\n')))
if len(self.results):
self.show_status_list()
else:
sublime.status_message("Nothing to show")
def show_status_list(self):
self.get_window().show_quick_panel(self.results, self.panel_done,
sublime.MONOSPACE_FONT)
def panel_done(self, picked):
if 0 > picked < len(self.results):
return
picked_file = self.results[picked]
get_file = getattr(self, '{0}_status_file'.format(self.vcs['name']), None)
if (get_file):
self.open_file(get_file(picked_file))
def open_file(self, picked_file):
if os.path.isfile(os.path.join(self.root, picked_file)):
self.window.open_file(os.path.join(self.root, picked_file))
else:
sublime.status_message("File doesn't exist")

View File

@@ -0,0 +1,6 @@
[
{
"caption": "Modific: Show diff",
"command": "show_diff"
}
]

View File

@@ -0,0 +1,33 @@
// Modific default settings
{
// Name of a region icon
// Valid icon names are: modific, dot, circle, bookmark and cross
// WARNING: if you set value different than 'modific',
// you may experience issues with UI of Sublime.
// See https://github.com/gornostal/Modific/issues/9
"region_icon": "modific",
// You can use your commands instead of plain "git" or "svn"
// e.g. "/usr/bin/git" or "C:\bin\git.exe"
"vcs": [
["git", "git"],
["svn", "svn"],
["bzr", "bzr"],
["hg" , "hg"]
],
//if you have some weird OS, that has non-unicode console
//place its console encoding here
"console_encoding" : "",
// if true, plugin prints some debug information to the console window
"debug": false,
// set to false to disable automatic saving
"autosave": true,
"svn_use_internal_diff": true,
// File size limit (in KB) for drawing icons on the gutter
"max_file_size": 1024
}

View File

@@ -0,0 +1,86 @@
Modific
=========
Modific is a ST2(3) plugin for highlighting lines changed since the last commit (you know what I mean if you used Netbeans).
For now it supports **Git**, **SVN**, **Bazaar** and **Mercurial**.
Install
-------
The easiest way to install is through **[Package Control](http://wbond.net/sublime\_packages/package\_control)**.
Once you install Package Control, restart ST3 and bring up the Command Palette (`Ctrl+Shift+P` on Linux/Windows, `Cmd+Shift+P` on OS X). Select "Package Control: Install Package", wait while Package Control fetches the latest package list, then select *Modific* when the list appears. The advantage of using this method is that Package Control will automatically keep *Modific* up to date with the latest version.
Or you can **download** the latest source from [GitHub](https://github.com/gornostal/Modific/zipball/master) and copy the *Modific* folder to your Sublime Text "Packages" directory.
Or **clone** the repository to your Sublime Text "Packages" directory:
git clone git://github.com/gornostal/Modific.git
The "Packages" directory is located at:
* OS X:
~/Library/Application Support/Sublime Text 2/Packages/
* Linux:
~/.config/sublime-text-2/Packages/
* Windows:
%APPDATA%/Sublime Text 2/Packages/
Please, make sure your VCS binaries is in the PATH (**especially if you are on Windows**).
To do that on Windows, open `Controll Panel -> System -> Advanced system settings -> Environment variables -> System Variables`, find PATH, click "Edit" and append `;C:\path\to\VCS\binaries` for every VCS you will use (or make sure it's already there).
Features / Usage
----------------
**Highlight changes** *(automatically: on save or when window gets focus)*
[![Highlight changes](http://i.imgur.com/FgpyRl.jpg)](http://i.imgur.com/FgpyR.jpg)
**Show diff** `Ctrl+Alt+D` on Linux/Windows and OS X
[![Show diff](http://i.imgur.com/csCw7l.jpg)](http://i.imgur.com/csCw7.jpg)
**Preview of the commited code for current line** `Ctrl+Alt+C` on Linux/Windows, `Ctlr+Super+C` on OS X
[![Preview](http://i.imgur.com/siVOXl.jpg)](http://i.imgur.com/siVOX.jpg)
**Revert modification** `Ctrl+Alt+R` on Linux/Windows, `Ctlr+Super+R` on OS X
This command reverts modifications if your cursor stays on modified line (or if on group of lines, then whole group will be reverted)
**View uncommitted files in a quick panel** `Ctrl+Alt+U` on Linux/Windows, `Ctlr+Super+U` on OS X
[![Preview](http://i.imgur.com/sldHNl.jpg)](http://i.imgur.com/sldHN.jpg)
**Go through changed lines** `Ctrl+Shift+Page Up(Down)`
For those who expected to see a clone of Netbeans feature - unfortunately, with existing Sublime Text API that is impossible :(
[Discussion on the forum](http://www.sublimetext.com/forum/viewtopic.php?f=5&t=7468)
Configuring
-----------
Open `Prefrences -> Package Settings -> Modific -> Settings - Default` and look for available settings.
If you want to change something, don't do it in this file. Open `Preferences -> Package Settings -> Modific -> Settings - User` and put there your configuration.
You can configure is a type of icon (dot, circle or bookmark) and path for your VCS binaries (or leave them as is, if you have them in your PATH). It's also possible to set priority for VCS used (when you have more than one simultaneously) by reordering their definitions.
If some sacred punishment has been bestowed upon you, and you have no other choice but to use OS, where console has non-UTF8 encoding, you can set console_encoding parameter to the name of your beloved encoding. This parameter is specifically designed for Windows XP users, who have their git repositories in folders with cyrillic path. Since russian XP uses CP1251 as default encoding (including console), VCS diff commands will be encoded appropriately, when using this parameter.
If you use different than the default theme, you can customize colors of bullets on the gutter by adding [this](https://gist.github.com/3692073) chunk of code to your theme.
Thanks to
---------
@beefsack for purchasing a license
License
-------
Released under the [WTFPLv2](http://sam.zoy.org/wtfpl/COPYING).

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

View File

@@ -0,0 +1 @@
{"url": "https://github.com/gornostal/Modific", "version": "2013.03.01.06.02.08", "description": "Highlight lines changed since the last commit (supports Git, SVN, Bazaar and Mercurial) / ST2(3) plugin"}

View File

@@ -0,0 +1,4 @@
*.pyc
*.cache
*.sublime-project
package-metadata.json

View File

@@ -0,0 +1,6 @@
[
{
"caption": "File: Refresh",
"command": "side_bar_git_refresh_tab_contents_by_running_command_again"
}
]

View File

@@ -0,0 +1,177 @@
[
{ "caption": "-" , "id":"side-bar-end-separator"},
{
"caption": "Git ", "id":"side-bar-git",
"children":
[
{ "caption": "Add & Commit…", "command": "side_bar_git_add_commit", "args": {"paths": []} },
{ "caption": "Add & Commit & Push…", "command": "side_bar_git_add_commit_push", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Commit…", "command": "side_bar_git_commit", "args": {"paths": []} },
{ "caption": "Commit Undo", "command": "side_bar_git_commit_undo", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Commit Amend", "command": "side_bar_git_commit_amend", "args": {"paths": []} },
{ "caption": "Commit All…", "command": "side_bar_git_commit_all", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Status", "command": "side_bar_git_status", "args": {"paths": []} },
{ "caption": "-"},
{
"caption": "Diff", "id":"side-bar-git-diff",
"children":
[
{ "caption": "all changes since the last commit", "command": "side_bar_git_diff_all_changes_since_last_commit", "args": {"paths": []} },
{ "caption": "all changes since the last commit ( ignore whitespace )", "command": "side_bar_git_diff_all_changes_since_last_commit_ignore_white_space", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "changes that have not been staged", "command": "side_bar_git_diff_changes_not_staged", "args": {"paths": []} },
{ "caption": "changes that are staged but not committed", "command": "side_bar_git_diff_changes_staged_not_commited", "args": {"paths": []} },
{ "caption": "between the index and last commit", "command": "side_bar_git_diff_between_index_and_last_commit", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "between remote and last local commit (origin/master..)", "command": "side_bar_git_diff_between_remote_and_last_local_commit", "args": {"paths": []} },
{ "caption": "between last local commit and remote (..origin/master)", "command": "side_bar_git_diff_between_last_local_commit_and_remote", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "between the two latest tags", "command": "xxxx", "args": {"paths": []} }
]
},
{
"caption": "Difftool", "id":"side-bar-git-difftool",
"children":
[
{ "caption": "all changes since the last commit", "command": "side_bar_git_difftool_all_changes_since_last_commit", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "changes that have not been staged", "command": "side_bar_git_difftool_changes_not_staged", "args": {"paths": []} },
{ "caption": "changes that are staged but not committed", "command": "side_bar_git_difftool_changes_staged_not_commited", "args": {"paths": []} },
{ "caption": "between the index and last commit", "command": "side_bar_git_difftool_between_index_and_last_commit", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "between remote and last local commit (origin/master..)", "command": "side_bar_git_difftool_between_remote_and_last_local_commit", "args": {"paths": []} },
{ "caption": "between last local commit and remote (..origin/master)", "command": "side_bar_git_difftool_between_last_local_commit_and_remote", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "between the two latest tags", "command": "xxxx", "args": {"paths": []} }
]
},
{ "caption": "-"},
{
"caption": "Log", "id":"side-bar-git-log",
"children":
[
{ "caption": "short summary of changes last 30", "command": "side_bar_git_log_stat_short_latest", "args": {"paths": []} },
{ "caption": "short summary of changes full", "command": "side_bar_git_log_stat_short_full", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "summary of changes last 30", "command": "side_bar_git_log_stat_latest", "args": {"paths": []} },
{ "caption": "summary of changes full", "command": "side_bar_git_log_stat_full", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "list of changes last 50", "command": "side_bar_git_log_stat_list_latest", "args": {"paths": []} },
{ "caption": "list of changes last 50 with commit", "command": "side_bar_git_log_stat_list_commit_latest", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "diffs of changes last 30", "command": "side_bar_git_log_extended_latest", "args": {"paths": []} },
{ "caption": "diffs of changes full", "command": "side_bar_git_log_extended_full", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "summary of changes since latest tag", "command": "xxxx", "args": {"paths": []} },
{ "caption": "summary of changes since latest push", "command": "side_bar_git_log_since_latest_push", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "between the two latest tags", "command": "xxxx", "args": {"paths": []} }
]
},
{ "caption": "Reflog", "command": "side_bar_git_reflog", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Blame", "command": "side_bar_git_blame", "args": {"paths": []} },
{ "caption": "-"},
{
"caption": "Tags", "id":"side-bar-git-tags",
"children":
[
{ "caption": "Auto-Tag", "command": "xxxx", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Add…", "command": "xxxx", "args": {"paths": []} },
{ "caption": "Remove…", "command": "xxxx", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "List", "command": "xxxx", "args": {"paths": []} }
]
},
{ "caption": "-"},
{
"caption": "Revert", "id":"side-bar-git-revert",
"children":
[
{ "caption": "discard changes to tracked", "command": "side_bar_git_revert_tracked", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "discard changes to tracked, clean untracked", "command": "side_bar_git_revert_tracked_clean_untracked", "args": {"paths": []} },
{ "caption": "discard changes to tracked, clean untracked, unstage", "command": "side_bar_git_revert_tracked_clean_untracked_unstage", "args": {"paths": []} },
{ "caption": "discard changes to tracked, unstage, clean untracked", "command": "side_bar_git_revert_tracked_unstage_clean_untracked", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "unstage", "command": "side_bar_git_revert_unstage", "args": {"paths": []} }
]
},
{ "caption": "-"},
{
"caption": "Branch",
"children":
[
{ "caption": "new from current and switch to…", "command": "side_bar_git_branch_new_from_current", "args": {"paths": []} },
{ "caption": "new from master and switch to…", "command": "side_bar_git_branch_new_from_master", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "new from cleaned current and switch to…", "command": "side_bar_git_branch_new_from_clean_current", "args": {"paths": []} },
{ "caption": "new from cleaned master and switch to…", "command": "side_bar_git_branch_new_from_clean_master", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "switch to master", "command": "side_bar_git_branch_switch_to_master", "args": {"paths": []} },
{ "caption": "switch to…", "command": "side_bar_git_branch_switch_to", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "rebase current into master", "command": "side_bar_git_rebase_current_into_master", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "merge changes to current from…", "command": "side_bar_git_merge_to_current_from", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "delete…", "command": "side_bar_git_branch_delete", "args": {"paths": []} },
{ "caption": "delete even if unmerged (force deletion)…","command": "side_bar_git_branch_delete_force", "args": {"paths": []} }
]
},
{ "caption": "Checkout to…", "command": "side_bar_git_checkout_to", "args": {"paths": []} },
{ "caption": "Checkout repository to…", "command": "side_bar_git_checkout_repository_to", "args": {"paths": []} },
{ "caption": "-"},
{
"caption": "Push, Pull, Fetch", "id":"side-bar-git-pull-push-fetch",
"children":
[
{ "caption": "Push", "command": "side_bar_git_push", "args": {"paths": []} },
{ "caption": "Push All Branches", "command": "side_bar_git_push_all_branches", "args": {"paths": []} },
{ "caption": "Push, Push Tags", "command": "side_bar_git_push_and_push_tags", "args": {"paths": []} },
{ "caption": "Push Tags", "command": "side_bar_git_push_tags", "args": {"paths": []} },
{ "caption": "Push with options…", "command": "side_bar_git_push_with_options", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Pull", "command": "side_bar_git_pull", "args": {"paths": []} },
{ "caption": "Pull with options…", "command": "side_bar_git_pull_with_options", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Fetch", "command": "side_bar_git_fetch", "args": {"paths": []} },
{ "caption": "Fetch with options…", "command": "side_bar_git_fetch_with_options", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Remote add…", "command": "side_bar_git_remote_add", "args": {"paths": []} }
]
},
{ "caption": "-"},
{ "caption": "Clone…", "command": "side_bar_git_clone", "args": {"paths": []} },
{ "caption": "Init", "command": "side_bar_git_init", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Add", "command": "side_bar_git_add", "args": {"paths": []} },
{ "caption": "Remove", "command": "side_bar_git_remove", "args": {"paths": []} },
{ "caption": "Remove Keep Local", "command": "side_bar_git_remove_keep_local", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Add to Git Ignore", "command": "side_bar_git_ignore_add", "args": {"paths": []} },
{ "caption": "Open Git Ignore", "command": "side_bar_git_ignore_open", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Git GUI", "command": "side_bar_git_gui", "args": {"paths": []} },
{ "caption": "Gitk", "command": "side_bar_git_gitk", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Liberal Git Command", "command": "side_bar_git_liberal", "args": {"paths": []} }
]
},
{ "caption": "-", "id": "side-bar-end-separator" }
]

View File

@@ -0,0 +1,3 @@
[
{ "keys": ["f5"], "command": "side_bar_git_refresh_tab_contents_by_running_command_again" }
]

View File

@@ -0,0 +1,3 @@
[
{ "keys": ["f5"], "command": "side_bar_git_refresh_tab_contents_by_running_command_again" }
]

View File

@@ -0,0 +1,3 @@
[
{ "keys": ["f5"], "command": "side_bar_git_refresh_tab_contents_by_running_command_again" }
]

View File

@@ -0,0 +1,39 @@
[
{
"caption": "Preferences",
"mnemonic": "n",
"id": "preferences",
"children":
[
{
"caption": "Package Settings",
"mnemonic": "P",
"id": "package-settings",
"children":
[
{
"caption": "Side Bar Git",
"children":
[
{
"command": "open_file", "args":
{
"file": "${packages}/SideBarGit/SideBarGit.sublime-settings"
},
"caption": "Settings Default"
},
{
"command": "open_file", "args":
{
"file": "${packages}/User/SideBarGit.sublime-settings"
},
"caption": "Settings User"
},
{ "caption": "-" }
]
}
]
}
]
}
]

View File

@@ -0,0 +1,177 @@
[
{ "caption": "-" , "id":"side-bar-end-separator"},
{
"caption": "Git ", "id":"side-bar-git",
"children":
[
{ "caption": "Add & Commit…", "command": "side_bar_git_add_commit", "args": {"paths": []} },
{ "caption": "Add & Commit & Push…", "command": "side_bar_git_add_commit_push", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Commit…", "command": "side_bar_git_commit", "args": {"paths": []} },
{ "caption": "Commit Undo", "command": "side_bar_git_commit_undo", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Commit Amend", "command": "side_bar_git_commit_amend", "args": {"paths": []} },
{ "caption": "Commit All…", "command": "side_bar_git_commit_all", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Status", "command": "side_bar_git_status", "args": {"paths": []} },
{ "caption": "-"},
{
"caption": "Diff", "id":"side-bar-git-diff",
"children":
[
{ "caption": "all changes since the last commit", "command": "side_bar_git_diff_all_changes_since_last_commit", "args": {"paths": []} },
{ "caption": "all changes since the last commit ( ignore whitespace )", "command": "side_bar_git_diff_all_changes_since_last_commit_ignore_white_space", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "changes that have not been staged", "command": "side_bar_git_diff_changes_not_staged", "args": {"paths": []} },
{ "caption": "changes that are staged but not committed", "command": "side_bar_git_diff_changes_staged_not_commited", "args": {"paths": []} },
{ "caption": "between the index and last commit", "command": "side_bar_git_diff_between_index_and_last_commit", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "between remote and last local commit (origin/master..)", "command": "side_bar_git_diff_between_remote_and_last_local_commit", "args": {"paths": []} },
{ "caption": "between last local commit and remote (..origin/master)", "command": "side_bar_git_diff_between_last_local_commit_and_remote", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "between the two latest tags", "command": "xxxx", "args": {"paths": []} }
]
},
{
"caption": "Difftool", "id":"side-bar-git-difftool",
"children":
[
{ "caption": "all changes since the last commit", "command": "side_bar_git_difftool_all_changes_since_last_commit", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "changes that have not been staged", "command": "side_bar_git_difftool_changes_not_staged", "args": {"paths": []} },
{ "caption": "changes that are staged but not committed", "command": "side_bar_git_difftool_changes_staged_not_commited", "args": {"paths": []} },
{ "caption": "between the index and last commit", "command": "side_bar_git_difftool_between_index_and_last_commit", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "between remote and last local commit (origin/master..)", "command": "side_bar_git_difftool_between_remote_and_last_local_commit", "args": {"paths": []} },
{ "caption": "between last local commit and remote (..origin/master)", "command": "side_bar_git_difftool_between_last_local_commit_and_remote", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "between the two latest tags", "command": "xxxx", "args": {"paths": []} }
]
},
{ "caption": "-"},
{
"caption": "Log", "id":"side-bar-git-log",
"children":
[
{ "caption": "short summary of changes last 30", "command": "side_bar_git_log_stat_short_latest", "args": {"paths": []} },
{ "caption": "short summary of changes full", "command": "side_bar_git_log_stat_short_full", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "summary of changes last 30", "command": "side_bar_git_log_stat_latest", "args": {"paths": []} },
{ "caption": "summary of changes full", "command": "side_bar_git_log_stat_full", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "list of changes last 50", "command": "side_bar_git_log_stat_list_latest", "args": {"paths": []} },
{ "caption": "list of changes last 50 with commit", "command": "side_bar_git_log_stat_list_commit_latest", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "diffs of changes last 30", "command": "side_bar_git_log_extended_latest", "args": {"paths": []} },
{ "caption": "diffs of changes full", "command": "side_bar_git_log_extended_full", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "summary of changes since latest tag", "command": "xxxx", "args": {"paths": []} },
{ "caption": "summary of changes since latest push", "command": "side_bar_git_log_since_latest_push", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "between the two latest tags", "command": "xxxx", "args": {"paths": []} }
]
},
{ "caption": "Reflog", "command": "side_bar_git_reflog", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Blame", "command": "side_bar_git_blame", "args": {"paths": []} },
{ "caption": "-"},
{
"caption": "Tags", "id":"side-bar-git-tags",
"children":
[
{ "caption": "Auto-Tag", "command": "xxxx", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Add…", "command": "xxxx", "args": {"paths": []} },
{ "caption": "Remove…", "command": "xxxx", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "List", "command": "xxxx", "args": {"paths": []} }
]
},
{ "caption": "-"},
{
"caption": "Revert", "id":"side-bar-git-revert",
"children":
[
{ "caption": "discard changes to tracked", "command": "side_bar_git_revert_tracked", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "discard changes to tracked, clean untracked", "command": "side_bar_git_revert_tracked_clean_untracked", "args": {"paths": []} },
{ "caption": "discard changes to tracked, clean untracked, unstage", "command": "side_bar_git_revert_tracked_clean_untracked_unstage", "args": {"paths": []} },
{ "caption": "discard changes to tracked, unstage, clean untracked", "command": "side_bar_git_revert_tracked_unstage_clean_untracked", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "unstage", "command": "side_bar_git_revert_unstage", "args": {"paths": []} }
]
},
{ "caption": "-"},
{
"caption": "Branch",
"children":
[
{ "caption": "new from current and switch to…", "command": "side_bar_git_branch_new_from_current", "args": {"paths": []} },
{ "caption": "new from master and switch to…", "command": "side_bar_git_branch_new_from_master", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "new from cleaned current and switch to…", "command": "side_bar_git_branch_new_from_clean_current", "args": {"paths": []} },
{ "caption": "new from cleaned master and switch to…", "command": "side_bar_git_branch_new_from_clean_master", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "switch to master", "command": "side_bar_git_branch_switch_to_master", "args": {"paths": []} },
{ "caption": "switch to…", "command": "side_bar_git_branch_switch_to", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "rebase current into master", "command": "side_bar_git_rebase_current_into_master", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "merge changes to current from…", "command": "side_bar_git_merge_to_current_from", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "delete…", "command": "side_bar_git_branch_delete", "args": {"paths": []} },
{ "caption": "delete even if unmerged (force deletion)…","command": "side_bar_git_branch_delete_force", "args": {"paths": []} }
]
},
{ "caption": "Checkout to…", "command": "side_bar_git_checkout_to", "args": {"paths": []} },
{ "caption": "Checkout repository to…", "command": "side_bar_git_checkout_repository_to", "args": {"paths": []} },
{ "caption": "-"},
{
"caption": "Push, Pull, Fetch", "id":"side-bar-git-pull-push-fetch",
"children":
[
{ "caption": "Push", "command": "side_bar_git_push", "args": {"paths": []} },
{ "caption": "Push All Branches", "command": "side_bar_git_push_all_branches", "args": {"paths": []} },
{ "caption": "Push, Push Tags", "command": "side_bar_git_push_and_push_tags", "args": {"paths": []} },
{ "caption": "Push Tags", "command": "side_bar_git_push_tags", "args": {"paths": []} },
{ "caption": "Push with options…", "command": "side_bar_git_push_with_options", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Pull", "command": "side_bar_git_pull", "args": {"paths": []} },
{ "caption": "Pull with options…", "command": "side_bar_git_pull_with_options", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Fetch", "command": "side_bar_git_fetch", "args": {"paths": []} },
{ "caption": "Fetch with options…", "command": "side_bar_git_fetch_with_options", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Remote add…", "command": "side_bar_git_remote_add", "args": {"paths": []} }
]
},
{ "caption": "-"},
{ "caption": "Clone…", "command": "side_bar_git_clone", "args": {"paths": []} },
{ "caption": "Init", "command": "side_bar_git_init", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Add", "command": "side_bar_git_add", "args": {"paths": []} },
{ "caption": "Remove", "command": "side_bar_git_remove", "args": {"paths": []} },
{ "caption": "Remove Keep Local", "command": "side_bar_git_remove_keep_local", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Add to Git Ignore", "command": "side_bar_git_ignore_add", "args": {"paths": []} },
{ "caption": "Open Git Ignore", "command": "side_bar_git_ignore_open", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Git GUI", "command": "side_bar_git_gui", "args": {"paths": []} },
{ "caption": "Gitk", "command": "side_bar_git_gitk", "args": {"paths": []} },
{ "caption": "-"},
{ "caption": "Liberal Git Command", "command": "side_bar_git_liberal", "args": {"paths": []} }
]
},
{ "caption": "-", "id": "side-bar-end-separator" }
]

View File

@@ -0,0 +1,4 @@
{
"statusbar_branch" : true,
"path_to_git_unixes":"" //example: /usr/local/git/bin/git
}

View File

@@ -0,0 +1,170 @@
import sublime
import sublime_plugin
import functools
import threading
import subprocess
import sys
import re
import tempfile
import codecs
import os
import time
from SideBarGit import SideBarGit
from SideBarItem import SideBarItem
from Utils import Object
Object.running = False
Object.timing = time.time()
class SideBarGitGutterDiff(sublime_plugin.EventListener):
def on_load(self, view):
self.run(view)
def on_modified(self, view):
now = time.time()
if now - Object.timing > 0.1:
Object.timing = now
self.run(view)
else:
Object.timing = now
def on_post_save(self, view):
self.run(view)
def run(self, view):
if Object.running == False and view.file_name() != None and view.file_name() != '':
Object.running = True
# cache file repository ( if any )
if not view.settings().has('SideBarGitGutterRepository'):
item = SideBarItem(view.file_name(), False)
_item = SideBarItem(view.file_name(), False)
repos = SideBarGit().getSelectedRepos([_item])
if len(repos) > 0:
view.settings().set('SideBarGitGutterRepository', repos[0].repository.path())
view.settings().set('SideBarGitGutterCWD', repos[0].repository.path())
view.settings().set('SideBarGitGutterPath', item.forCwdSystemPathRelativeFrom(repos[0].repository.path()))
else:
view.settings().set('SideBarGitGutterRepository', '')
# if in a repo check for modifications
repo = view.settings().get('SideBarGitGutterRepository')
if repo != '':
SideBarGitGutterDiffThread(
view,
repo,
view.settings().get('SideBarGitGutterCWD'),
view.settings().get('SideBarGitGutterPath'),
view.substr(sublime.Region(0, view.size()))
).start()
else:
Object.running = False
class SideBarGitGutterDiffThread(threading.Thread):
def __init__(self, view, repo, cwd, path, content):
threading.Thread.__init__(self)
self.view = view
self.repo = repo
self.cwd = cwd
self.path = path
self.content = content
def run(self):
tmp = tempfile.NamedTemporaryFile(delete=False)
codecs.open(tmp.name, 'w+', 'utf-8').write(self.content)
comand = ['git', 'diff', '-p', '--unified=0', '--no-color', '--ignore-all-space', '--ignore-space-at-eol', '--ignore-space-change', 'HEAD:'+self.path, tmp.name]
process = subprocess.Popen(
comand,
cwd=self.cwd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=sys.platform == 'win32',
universal_newlines=True)
stdout, stderr = process.communicate()
if stdout != '' and stdout.find('fatal:') != 0:
hunk = re.finditer('\n@@ -([0-9]+),?([0-9]*) \+([0-9]+),?([0-9]*) @@', stdout)
additions = []
deletions = []
changes = []
for change in hunk:
g = []
for group in change.groups():
if group == '':
g.append(1)
else:
g.append(int(group))
deleted = g[1]
added = g[3]
if deleted == added and added == 1:
changes.append([g[2]-1, g[2]]);
else:
if deleted > 0:
if deleted == 1:
if added > deleted:
deletions.append([g[2], g[2]+deleted-added]);
else:
deletions.append([g[2], g[2]+1]);
else:
deletions.append([g[2]-1, g[2]+deleted-1])
if added > 0:
if added == 1:
additions.append([g[2], g[2]+added]);
else:
additions.append([g[2]-1, g[2]+added-1])
tmp.close();
os.remove(tmp.name)
sublime.set_timeout(functools.partial(self.add_regions, additions, deletions, changes), 0)
else:
tmp.close();
os.remove(tmp.name)
sublime.set_timeout(functools.partial(self.erase_regions), 0)
if stdout.find('fatal:'):
sublime.set_timeout(functools.partial(self.mark_not_in_a_repository), 0)
Object.running = False
def add_regions(self, additions, deletions, changes):
self.erase_regions()
rs = []
for r in additions:
while r[0] != r[1]:
rs.append(sublime.Region(self.view.text_point(r[0], 0)))
r[0] = r[0]+1
if len(rs):
self.view.add_regions("git.diff.additions", rs, "number", "dot", sublime.HIDDEN)
rs = []
for r in deletions:
while r[0] != r[1]:
rs.append(sublime.Region(self.view.text_point(r[0], 0)))
r[0] = r[0]+1
if len(rs):
self.view.add_regions("git.diff.deletions", rs, "entity.name.class", "dot", sublime.HIDDEN)
rs = []
for r in changes:
while r[0] != r[1]:
rs.append(sublime.Region(self.view.text_point(r[0], 0)))
r[0] = r[0]+1
if len(rs):
self.view.add_regions("git.diff.changes", rs, "string", "dot", sublime.HIDDEN)
def erase_regions(self):
self.view.erase_regions("git.diff.additions")
self.view.erase_regions("git.diff.deletions")
self.view.erase_regions("git.diff.changes")
def mark_not_in_a_repository(self):
self.view.settings().set('SideBarGitGutterRepository', '')

View File

@@ -0,0 +1,43 @@
import sublime, sublime_plugin
from sidebar.SideBarGit import SideBarGit
from sidebar.SideBarSelection import SideBarSelection
import threading
class Object():
pass
s = sublime.load_settings('SideBarGit.sublime-settings')
class StatusBarBranch(sublime_plugin.EventListener):
def on_load(self, v):
if s.get('statusbar_branch') and v.file_name():
StatusBarBranchGet(v.file_name(), v).start()
def on_activated(self, v):
if s.get('statusbar_branch') and v.file_name():
StatusBarBranchGet(v.file_name(), v).start()
class StatusBarBranchGet(threading.Thread):
def __init__(self, file_name, v):
threading.Thread.__init__(self)
self.file_name = file_name
self.v = v
def run(self):
for repo in SideBarGit().getSelectedRepos(SideBarSelection([self.file_name]).getSelectedItems()):
object = Object()
object.item = repo.repository
object.command = ['git', 'branch']
object.silent = True
SideBarGit().run(object)
sublime.set_timeout(lambda:self.on_done(SideBarGit.last_stdout.decode('utf-8')), 0)
return
def on_done(self, branches):
branches = branches.split('\n')
for branch in branches:
if branch.startswith("*"):
self.v.set_status('statusbar_sidebargit_branch', branch)
return

View File

@@ -0,0 +1,19 @@
"None are so hopelessly enslaved as those who falsely believe they are free."
Johann Wolfgang von Goethe
Copyright (C) 2012 Tito Bouzout <tito.bouzout@gmail.com>
This license apply to all the files inside this program unless noted
different for some files or portions of code inside these files.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation. http://www.gnu.org/licenses/gpl.html
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses/gpl.html

View File

@@ -0,0 +1,182 @@
Description
------------------
Provides git commands on Side Bar of Files and Folders for Sublime Text 2. For sublime text see: http://www.sublimetext.com/
It should work with files names in any language.
This plugin is a port of the "Komodin" extension for komodo edit. See: https://github.com/titoBouzout/komodo-komodin-git
Usage
------------------
* Right click on "multiple/single" "files/folders" of the "tree" sidebar to apply commands on selected files which maybe are from different repositories.
* To apply commands to focused document use the document context menu.
* Tip: If a command sends output to a tab ( example a diff ), pressing F5 on that tab: will execute the command again and refresh the tab with the new contents ( example the new computed diff )
Provides the following commands
------------------
<pre>
Add & Commit
o git add -- "/selected/paths/files/or/and/folders"
o git commit -m "promptMessage" -- "/selected/paths/files/or/and/folders"
Add & Commit & Push
o git add -- "/selected/paths/files/or/and/folders"
o git commit -m "promptMessage" -- "/selected/paths/files/or/and/folders"
o git push
Commit
o git commit -m "promptMessage" -- "/selected/paths/files/or/and/folders"
Commit Undo
o git reset --soft HEAD~1
Commit Amend
o git commit --amend -C HEAD -- "/selected/paths/files/or/and/folders"
Commit All
o git commit -a -m "promptMessage"
Status
o git status --untracked-files=all -- "/selected/paths/files/or/and/folders"
Diff
all changes since the last commit
o git diff HEAD -- "/selected/paths/files/or/and/folders"
changes that have not been staged
o git diff -- "/selected/paths/files/or/and/folders"
changes that are staged but not committed
o git diff --staged -- "/selected/paths/files/or/and/folders"
between the index and last commit
o git diff --cached -- "/selected/paths/files/or/and/folders"
between remote and last local commit (origin/master..)
o git diff origin/master.. -- "/selected/paths/files/or/and/folders"
between last local commit and remote (..origin/master)
o git diff ..origin/master -- "/selected/paths/files/or/and/folders"
between the two latest tags
o git diff "previousTag".."lastTag" -- "/selected/paths/files/or/and/folders"
Log stat last 30
o git log -n 30 --stat --graph -- "/selected/paths/files/or/and/folders"
Log stat full
o git log --stat --graph -- "/selected/paths/files/or/and/folders"
Log extended last 30
o git log -n 30 -p -- "/selected/paths/files/or/and/folders"
Log extended full
o git log -p -- "/selected/paths/files/or/and/folders"
Log since last tag
o git log "lastTag"... --stat --graph -- "/selected/paths/files/or/and/folders"
Log since last push
o git log origin/master... --stat --graph -- "/selected/paths/files/or/and/folders"
Log between the two latest tags
o git log "prevToLastTag".."lastTag" --stat --graph -- "/selected/paths/files/or/and/folders"
Blame
o git blame -- "/selected/paths/files/NOT/folders"
Auto-Tag
o git tag "YYMMDD.Version"
Tag Add
o git tag "promptMessage"
Tag Remove
o git tag -d "promptMessage"
Tag List
o git tag -l
Revert Discard changes to tracked
o git checkout HEAD -- "/selected/paths/files/or/and/folders"
Revert Discard changes to tracked, clean untracked
o git checkout HEAD -- "/selected/paths/files/or/and/folders"
o git clean -f -d -- "/selected/paths/files/or/and/folders"
Revert Discard changes to tracked, clean untracked, unstage
o git checkout HEAD -- "/selected/paths/files/or/and/folders"
o git clean -f -d -- "/selected/paths/files/or/and/folders"
o git reset HEAD -- "/selected/paths/files/or/and/folders"
Revert Discard changes to tracked, unstage, clean untracked
o git checkout HEAD -- "/selected/paths/files/or/and/folders"
o git reset HEAD -- "/selected/paths/files/or/and/folders"
o git clean -f -d -- "/selected/paths/files/or/and/folders"
Revert Unstage
o git reset HEAD -- "/selected/paths/files/or/and/folders"
Checkout to
o git checkout promptMessage -- "/selected/paths/files/or/and/folders"
Checkout repo to
o cd repoPath
o git checkout promptMessage
Push
o git push
Push, Push Tags
o git push && git push --tags
Push Tags
o git push --tags
Push with options…
o promptMessage
Pull
o git pull
Pull with options…
o promptMessage
Fetch
o git fetch
Fetch with options…
o promptMessage
Remote add
o git remote add promptMessage
Configure default remote
o git config branch.promptBranch.remote promptRemoteName
Clone
o git clone promptMessage
Init
o git init
Add
o git add -- "/selected/paths/files/or/and/folders"
Remove
o git rm -r -f -- "/selected/paths/files/or/and/folders"
Remove Keep Local
o git rm -r --cached -- "/selected/paths/files/or/and/folders"
Open Git Ignore
Add to Git Ignore
Git GUI
Gitk
Liberal Git Command
</pre>
Installation
------------------
* Install this repository via "Package Control" http://wbond.net/sublime_packages/package_control
Todo
------------------
* Tag commands not yet ported
Source-code
------------------
https://github.com/SublimeText/SideBarGit
Forum Thread
------------------
http://www.sublimetext.com/forum/viewtopic.php?f=5&t=3405
Contribute
------------------
[Consider make a contribution](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=extensiondevelopment%40gmail%2ecom&lc=UY&item_name=Tito&item_number=sublime%2dtext%2dside%2dbar%2dplugin&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted )

View File

@@ -0,0 +1,317 @@
# coding=utf8
import sublime
import os
import subprocess
from SideBarItem import SideBarItem
class Object():
pass
s = sublime.load_settings('SideBarGit.sublime-settings')
path_to_git_unixes = s.get('path_to_git_unixes');
class SideBarGit:
last_stdout = ''
def run(
self,
object,
modal = False,
background = False,
refresh_funct_view = False,
refresh_funct_command = False,
refresh_funct_item = False,
refresh_funct_to_status_bar = False,
refresh_funct_title = False,
refresh_funct_no_results = False,
refresh_funct_syntax_file = False
):
if not refresh_funct_view:
pass
else:
object = Object()
object.command = refresh_funct_command
object.item = SideBarItem(refresh_funct_item, os.path.isdir(refresh_funct_item))
object.to_status_bar = refresh_funct_to_status_bar
object.title = refresh_funct_title
object.no_results = refresh_funct_no_results
object.syntax_file = refresh_funct_syntax_file
debug = False
if debug:
print '----------------------------------------------------------'
print 'GIT:'
print object.command
print 'CWD:'
print object.item.forCwdSystemPath()
print 'PATH:'
print object.item.forCwdSystemName()
failed = False
if sublime.platform() == 'windows':
object.command = map(self.escapeCMDWindows, object.command)
if sublime.platform() is not 'windows' and object.command[0] == 'git':
if path_to_git_unixes != '':
object.command[0] = s.get('path_to_git_unixes')
elif os.path.exists('/usr/local/git/bin'):
object.command[0] = '/usr/local/git/bin/git'
cwd = object.item.forCwdSystemPath()
try:
if sublime.platform() == 'windows':
process = subprocess.Popen(
#" ".join(object.command),
object.command,
cwd=cwd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
universal_newlines=True)
else:
process = subprocess.Popen(
object.command,
cwd=cwd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=False,
universal_newlines=True)
if background:
if debug:
print 'SUCCESS'
print '----------------------------------------------------------'
return True
stdout, stderr = process.communicate()
SideBarGit.last_stdout = str(stdout).rstrip()
self.last_stdout = str(stdout).rstrip()
stdout = stdout.strip()
if stdout.find('fatal:') == 0 or stdout.find('error:') == 0 or stdout.find('Permission denied') == 0 or stderr:
print 'FAILED'
failed = True
else:
if debug:
print 'SUCCESS'
if stdout:
if debug:
print 'STDOUT'
print stdout
if stderr:
print 'STDERR'
print stderr
except OSError as (errno, strerror):
print 'FAILED'
failed = True
print errno
print strerror
SideBarGit.last_stdout = ''
self.last_stdout = ''
except IOError as (errno, strerror):
print 'FAILED'
failed = True
print errno
print strerror
SideBarGit.last_stdout = ''
self.last_stdout = ''
if debug:
print '----------------------------------------------------------'
try:
object.to_status_bar
except:
object.to_status_bar = False
try:
object.silent
return
except:
pass
if failed:
try:
strerror
if errno == 2:
self.alert(strerror+'\nPossible error:\n'+object.command[0]+' not found on $PATH')
else:
self.alert(strerror)
return False
except:
if not stdout and not stderr:
return False
if stdout.find('Permission denied') == 0 or stdout.find('fatal: The remote end hung up unexpectedly') == 0:
self.alert((stdout or '')+'\n'+(stderr or '')+'\nPossible error:\nssh keys not in .ssh/ directory or keys not opened')
else:
self.alert((stdout or '')+'\n'+(stderr or ''))
return False
else:
if stdout != '' and refresh_funct_view == False and (object.to_status_bar or " ".join(object.command).find('git push') == 0 or stdout.find('nothing to commit') == 0):
self.status(stdout)
else:
if stdout == '' and refresh_funct_view == False:
try:
self.status(object.no_results)
except:
self.status('No output to show')
return True
if stdout == '' and refresh_funct_view != False:
try:
stdout = object.no_results
except:
stdout = 'No output to show'
if stdout == '':
return True
if refresh_funct_view == False:
view = sublime.active_window().new_file()
else:
view = refresh_funct_view
try:
view.set_name(object.title.decode('utf-8'))
except:
view.set_name('No Title')
try:
if object.syntax_file != False:
view.set_syntax_file(object.syntax_file)
except:
pass
try:
object.word_wrap
view.settings().set('word_wrap', False)
except:
pass
view.settings().set('fallback_encoding', 'UTF-8')
view.settings().set('encoding', 'UTF-8')
view.settings().set('default_dir', object.item.dirname())
view.set_scratch(True)
if refresh_funct_view == False:
view.settings().set('SideBarGitIsASideBarGitTab', True)
view.settings().set('SideBarGitCommand', object.command)
view.settings().set('SideBarGitModal', modal)
view.settings().set('SideBarGitBackground', background)
view.settings().set('SideBarGitItem', object.item.path())
try:
view.settings().set('SideBarGitToStatusBar', object.to_status_bar)
except:
view.settings().set('SideBarGitToStatusBar', False)
try:
view.settings().set('SideBarGitTitle', object.title)
except:
view.settings().set('SideBarGitTitle', 'No Title')
try:
view.settings().set('SideBarGitNoResults', object.no_results)
except:
view.settings().set('SideBarGitNoResults', 'No output to show')
try:
view.settings().set('SideBarGitSyntaxFile', object.syntax_file)
except:
view.settings().set('SideBarGitSyntaxFile', False)
content = "[SideBarGit@SublimeText "
content += object.item.name().decode('utf-8')
content += "/] "
content += (" ".join(object.command)).decode('utf-8')
content += "\n\n"
content += "# Improve this command, the output or the tab title by posting here:"
content += "\n"
content += "# http://www.sublimetext.com/forum/viewtopic.php?f=5&t=3405"
content += "\n"
content += "# Tip: F5 will run the command again and refresh the contents of this tab"
content += "\n\n"
try:
content += stdout
except:
content += unicode(stdout, 'UTF-8', errors='ignore')
edit = view.begin_edit()
view.replace(edit, sublime.Region(0, view.size()), content);
view.sel().clear()
view.sel().add(sublime.Region(0))
view.end_edit(edit)
return True
def confirm(self, message, function, arg1):
if int(sublime.version()) >= 2186:
if sublime.ok_cancel_dialog(u'Side Bar Git : '+message):
function(arg1, True)
else:
import functools
sublime.active_window().run_command('hide_panel');
sublime.active_window().show_input_panel("Confirmation Required:", message.decode('utf-8'), functools.partial(function, arg1, True), None, None)
def prompt(self, message, default, function, arg1):
import functools
sublime.active_window().run_command('hide_panel');
sublime.active_window().show_input_panel(message.decode('utf-8'), default.decode('utf-8'), functools.partial(function, arg1, True), None, None)
def alert(self, message):
try:
sublime.error_message('Git : '+(message.decode('utf-8')))
except:
try:
sublime.error_message('Git : '+message)
except:
print message
def status(self, message):
message = message[:200] + (message[200:] and '')
message = message.replace('\n', ' ')
try:
v = sublime.active_window().active_view()
v.set_status('SideBarGit', 'Git : '+(message.decode('utf-8')))
sublime.set_timeout(lambda: SideBarGit().statusRemove(v), 16000)
except:#there is no tabs opened
sublime.status_message('Git : '+(message.decode('utf-8')))
def statusRemove(self, v):
try:
v.erase_status('SideBarGit')
except:#this view is not there
pass
def quickPanel(self, function, extra, data):
import functools
window = sublime.active_window()
# window.show_input_panel("BUG!", '', '', None, None)
# window.run_command('hide_panel');
data = [item[:70] for item in data]
window.show_quick_panel(data, functools.partial(self.quickPanelDone, function, extra, data))
def quickPanelDone(self, function, extra, data, result):
if result != -1:
function(extra, data, result)
def getSelectedRepos(self, items):
repos = []
reposTemp = []
for item in items:
original = item.path()
while not os.path.exists(item.join('.git')):
if item.dirname() == item.path():
break;
item.path(item.dirname())
if os.path.exists(item.join('.git')):
try:
index = reposTemp.index(item.path())
except ValueError:
reposTemp.append(item.path())
index = reposTemp.index(item.path())
repos.append(Object())
repos[index].repository = item
repos[index].items = []
repos[index].items.append(SideBarItem(original, os.path.isdir(original)))
return repos
def escapeCMDWindows(self, string):
return string.replace('^', '^^')

View File

@@ -0,0 +1,480 @@
# coding=utf8
import sublime
import os
import re
import shutil
from SideBarProject import SideBarProject
try:
import desktop
except:
pass
class Object():
pass
def expand_vars(path):
for k, v in os.environ.iteritems():
try:
# dirty hack, this should be autofixed in python3
k = unicode(k.encode('utf8'))
v = unicode(v.encode('utf8'))
path = path.replace(u'%'+k+'%', v).replace(u'%'+k.lower()+'%', v)
except:
pass
return path
class SideBarItem:
def __init__(self, path, is_directory):
self._path = path
self._is_directory = is_directory
def path(self, path = ''):
if path == '':
return self._path
else:
self._path = path
self._is_directory = os.path.isdir(path)
return path
def pathSystem(self):
import sys
return self.path().encode(sys.getfilesystemencoding())
def pathWithoutProject(self):
path = self.path()
for directory in SideBarProject().getDirectories():
path = path.replace(directory, '', 1)
return path.replace('\\', '/')
def pathProject(self):
path = self.path()
for directory in SideBarProject().getDirectories():
path2 = path.replace(directory, '', 1)
if path2 != path:
return directory
return False
def projectURL(self, type):
filename = os.path.normpath(os.path.join(sublime.packages_path(), '..', 'Settings', 'SideBarEnhancements.json'))
if os.path.lexists(filename):
#try:
import json
data = file(filename, 'r').read()
data = data.replace('\t', ' ').replace('\\', '/').replace('\\', '/').replace('//', '/').replace('//', '/').replace('http:/', 'http://').replace('https:/', 'https://')
data = json.loads(data, strict=False)
for path in data.keys():
path2 = expand_vars(path)
print '-------------------------------------------------------'
print 'searching:'
path2 = path2.replace('\\', '/').replace('\\', '/').replace('//', '/').replace('//', '/')
print path2
print 'in:'
path3 = self.path().replace('\\', '/').replace('\\', '/').replace('//', '/').replace('//', '/')
print path3
print '-------------------------------------------------------'
path4 = re.sub(re.compile("^"+re.escape(path2), re.IGNORECASE), '', path3);
print path4
if path4 != path3:
url = data[path][type]
if url:
if url[-1:] != '/':
url = url+'/'
import urllib
return url+(re.sub("^/", '', urllib.quote(path4.encode('utf-8'))));
#except:
# return False
else:
return False
def isUnderCurrentProject(self):
path = self.path()
path2 = self.path()
for directory in SideBarProject().getDirectories():
path2 = path2.replace(directory, '', 1)
return path != path2
def pathRelativeFromProject(self):
return re.sub('^/+', '', self.pathWithoutProject())
def pathRelativeFromProjectEncoded(self):
import urllib
return urllib.quote(self.pathRelativeFromProject().encode('utf-8'))
def pathRelativeFromView(self):
return os.path.relpath(self.path(), os.path.dirname(sublime.active_window().active_view().file_name())).replace('\\', '/')
def pathRelativeFromViewEncoded(self):
import urllib
return urllib.quote(os.path.relpath(self.path(), os.path.dirname(sublime.active_window().active_view().file_name())).replace('\\', '/').encode('utf-8'))
def pathAbsoluteFromProject(self):
return self.pathWithoutProject()
def pathAbsoluteFromProjectEncoded(self):
import urllib
return urllib.quote(self.pathAbsoluteFromProject().encode('utf-8'))
def uri(self):
import urllib
return 'file:'+urllib.pathname2url(self.path().encode('utf-8'));
def join(self, name):
return os.path.join(self.path(), name)
def dirname(self):
branch, leaf = os.path.split(self.path())
return branch;
def forCwdSystemPath(self):
if self.isDirectory():
return self.pathSystem()
else:
return self.dirnameSystem()
def forCwdSystemName(self):
if self.isDirectory():
return '.'
else:
path = self.pathSystem()
branch = self.dirnameSystem()
leaf = path.replace(branch, '', 1).replace('\\', '').replace('/', '')
return leaf
def forCwdSystemPathRelativeFrom(self, relativeFrom):
relative = SideBarItem(relativeFrom, os.path.isdir(relativeFrom))
path = self.pathSystem().replace(relative.pathSystem(), '', 1).replace('\\', '/')
if path == '':
return '.'
else:
return re.sub('^/+', '', path)
def forCwdSystemPathRelativeFromRecursive(self, relativeFrom):
relative = SideBarItem(relativeFrom, os.path.isdir(relativeFrom))
path = self.pathSystem().replace(relative.pathSystem(), '', 1).replace('\\', '/')
if path == '':
return '.'
else:
if self.isDirectory():
return re.sub('^/+', '', path)+'/'
else:
return re.sub('^/+', '', path)
def dirnameSystem(self):
import sys
return self.dirname().encode(sys.getfilesystemencoding())
def dirnameCreate(self):
try:
os.makedirs(self.dirname())
except:
pass
def name(self):
branch, leaf = os.path.split(self.path())
return leaf;
def nameSystem(self):
import sys
return self.name().encode(sys.getfilesystemencoding())
def nameEncoded(self):
import urllib
return urllib.quote(self.name().encode('utf-8'));
def namePretty(self):
return self.name().replace(self.extension(), '').replace('-', ' ').replace('_', ' ').strip();
def open(self):
if sublime.platform() == 'osx':
import subprocess
subprocess.Popen(['open', '-a', self.nameSystem()], cwd=self.dirnameSystem())
elif sublime.platform() == 'windows':
import subprocess
subprocess.Popen([self.nameSystem()], cwd=self.dirnameSystem(), shell=True)
else:
desktop.open(self.path())
def edit(self):
return sublime.active_window().open_file(self.path())
def isDirectory(self):
return self._is_directory
def isFile(self):
return self.isDirectory() == False
def contentUTF8(self):
import codecs
return codecs.open(self.path(), 'r', 'utf-8').read()
def contentBinary(self):
return file(self.path(), "rb").read()
def contentBase64(self):
return 'data:'+self.mime()+';base64,'+(file(self.path(), "rb").read().encode("base64").replace('\n', ''))
def reveal(self):
sublime.active_window().run_command("open_dir", {"dir": self.dirname(), "file": self.name()} )
def write(self, content):
file(self.path(), 'w+').write(content)
def mime(self):
import mimetypes
return mimetypes.guess_type(self.path())[0] or 'application/octet-stream'
def extension(self):
return os.path.splitext('name'+self.name())[1].lower()
def exists(self):
return os.path.isdir(self.path()) or os.path.isfile(self.path())
def create(self):
if self.isDirectory():
self.dirnameCreate()
os.makedirs(self.path())
else:
self.dirnameCreate()
self.write('')
def copy(self, location, replace = False):
location = SideBarItem(location, os.path.isdir(location));
if location.exists() and replace == False:
return False
elif location.exists() and location.isFile():
os.remove(location.path())
location.dirnameCreate();
if self.isDirectory():
if location.exists():
self.copy_recursive(self.path(), location.path())
else:
shutil.copytree(self.path(), location.path())
else:
shutil.copy2(self.path(), location.path())
return True
def copy_recursive(self, _from, _to):
if os.path.isfile(_from) or os.path.islink(_from):
try:
os.makedirs(os.path.dirname(_to));
except:
pass
if os.path.exists(_to):
os.remove(_to)
shutil.copy2(_from, _to)
else:
try:
os.makedirs(_to);
except:
pass
for content in os.listdir(_from):
__from = os.path.join(_from, content)
__to = os.path.join(_to, content)
self.copy_recursive(__from, __to)
def move(self, location, replace = False):
location = SideBarItem(location, os.path.isdir(location));
if location.exists() and replace == False:
if self.path().lower() == location.path().lower():
pass
else:
return False
elif location.exists() and location.isFile():
os.remove(location.path())
if self.path().lower() == location.path().lower():
location.dirnameCreate();
os.rename(self.path(), location.path()+'.sublime-temp')
os.rename(location.path()+'.sublime-temp', location.path())
self._move_moveViews(self.path(), location.path())
else:
location.dirnameCreate();
if location.exists():
self.move_recursive(self.path(), location.path())
else:
os.rename(self.path(), location.path())
self._move_moveViews(self.path(), location.path())
return True
def move_recursive(self, _from, _to):
if os.path.isfile(_from) or os.path.islink(_from):
try:
os.makedirs(os.path.dirname(_to));
except:
pass
if os.path.exists(_to):
os.remove(_to)
os.rename(_from, _to)
else:
try:
os.makedirs(_to);
except:
pass
for content in os.listdir(_from):
__from = os.path.join(_from, content)
__to = os.path.join(_to, content)
self.move_recursive(__from, __to)
os.rmdir(_from)
def _move_moveViews(self, old, location):
for window in sublime.windows():
active_view = window.active_view()
views = []
for view in window.views():
if view.file_name():
views.append(view)
views.reverse();
for view in views:
if old == view.file_name():
active_view = self._move_moveView(window, view, location, active_view)
elif view.file_name().find(old+'\\') == 0:
active_view = self._move_moveView(window, view, view.file_name().replace(old+'\\', location+'\\', 1), active_view)
elif view.file_name().find(old+'/') == 0:
active_view = self._move_moveView(window, view, view.file_name().replace(old+'/', location+'/', 1), active_view)
def _move_moveView(self, window, view, location, active_view):
if active_view == view:
is_active_view = True
else:
is_active_view = False
options = Object()
options.scroll = view.viewport_position()
options.selections = [[item.a, item.b] for item in view.sel()]
options.marks = [[item.a, item.b] for item in view.get_regions("mark")]
options.bookmarks = [[item.a, item.b] for item in view.get_regions("bookmarks")]
if int(sublime.version()) >= 2167:
options.folds = [[item.a, item.b] for item in view.folded_regions()]
else:
options.folds = [[item.a, item.b] for item in view.unfold(sublime.Region(0, view.size()))]
options.syntax = view.settings().get('syntax')
try:
_window = window or view.window() or sublime.active_window()
options.position = _window.get_view_index(view)
except:
options.position = False
window.focus_view(view)
if view.is_dirty():
options.content = view.substr(sublime.Region(0, view.size()))
view.window().run_command('revert')
else:
options.content = False
_view = view
view = window.open_file(location)
window.focus_view(_view)
window.run_command('close')
sublime.set_timeout(lambda: self._move_restoreView(view, options, window), 200)
if is_active_view:
window.focus_view(view)
return view
else:
window.focus_view(active_view)
return active_view
def _move_restoreView(self, view, options, window):
if view.is_loading():
sublime.set_timeout(lambda: self._move_restoreView(view, options, window), 100)
else:
if options.content != False:
edit = view.begin_edit()
view.replace(edit, sublime.Region(0, view.size()), options.content);
view.sel().clear()
view.sel().add(sublime.Region(0))
view.end_edit(edit)
if options.position != False:
try:
_window = window or view.window() or sublime.active_window()
group, index = options.position
_window.set_view_index(view, group, index)
except:
pass
if options.syntax:
view.settings().set('syntax', options.syntax);
for r in options.folds:
view.fold(sublime.Region(r[0], r[1]))
view.sel().clear()
for r in options.selections:
view.sel().add(sublime.Region(r[0], r[1]))
rs = []
for r in options.marks:
rs.append(sublime.Region(r[0], r[1]))
if len(rs):
view.add_regions("mark", rs, "mark", "dot", sublime.HIDDEN | sublime.PERSISTENT)
rs = []
for r in options.bookmarks:
rs.append(sublime.Region(r[0], r[1]))
if len(rs):
view.add_regions("bookmarks", rs, "bookmarks", "bookmark", sublime.HIDDEN | sublime.PERSISTENT)
view.set_viewport_position(options.scroll, False)
def close_associated_buffers(self):
path = self.path()
closed_items = []
for window in sublime.windows():
active_view = window.active_view()
views = []
for view in window.views():
if view.file_name():
views.append(view)
views.reverse();
for view in views:
if path == view.file_name():
if view.window():
closed_items.append([view.file_name(), view.window(), view.window().get_view_index(view)])
if len(window.views()) == 1:
window.new_file()
window.focus_view(view)
window.run_command('revert')
window.run_command('close')
elif view.file_name().find(path+'\\') == 0:
if view.window():
closed_items.append([view.file_name(), view.window(), view.window().get_view_index(view)])
if len(window.views()) == 1:
window.new_file()
window.focus_view(view)
window.run_command('revert')
window.run_command('close')
elif view.file_name().find(path+'/') == 0:
if view.window():
closed_items.append([view.file_name(), view.window(), view.window().get_view_index(view)])
if len(window.views()) == 1:
window.new_file()
window.focus_view(view)
window.run_command('revert')
window.run_command('close')
# try to repaint
try:
window.focus_view(active_view)
window.focus_view(window.active_view())
except:
try:
window.focus_view(window.active_view())
except:
pass
return closed_items

View File

@@ -0,0 +1,119 @@
import sublime
import re
import os
class SideBarProject:
def getDirectories(self):
return sublime.active_window().folders()
def hasOpenedProject(self):
return self.getProjectFile() != None
def getDirectoryFromPath(self, path):
for directory in self.getDirectories():
maybe_path = path.replace(directory, '', 1)
if maybe_path != path:
return directory
def getProjectFile(self):
if not self.getDirectories():
return None
import json
data = file(os.path.normpath(os.path.join(sublime.packages_path(), '..', 'Settings', 'Session.sublime_session')), 'r').read()
data = data.replace('\t', ' ')
data = json.loads(data, strict=False)
projects = data['workspaces']['recent_workspaces']
if os.path.lexists(os.path.join(sublime.packages_path(), '..', 'Settings', 'Auto Save Session.sublime_session')):
data = file(os.path.normpath(os.path.join(sublime.packages_path(), '..', 'Settings', 'Auto Save Session.sublime_session')), 'r').read()
data = data.replace('\t', ' ')
data = json.loads(data, strict=False)
if 'workspaces' in data and 'recent_workspaces' in data['workspaces'] and data['workspaces']['recent_workspaces']:
projects += data['workspaces']['recent_workspaces']
projects = list(set(projects))
for project_file in projects:
project_file = re.sub(r'^/([^/])/', '\\1:/', project_file);
project_json = json.loads(file(project_file, 'r').read(), strict=False)
if 'folders' in project_json:
folders = project_json['folders']
found_all = True
for directory in self.getDirectories():
found = False
for folder in folders:
folder_path = re.sub(r'^/([^/])/', '\\1:/', folder['path']);
if folder_path == directory.replace('\\', '/'):
found = True
break;
if found == False:
found_all = False
break;
if found_all:
return project_file
return None
def getProjectJson(self):
if not self.hasOpenedProject():
return None
import json
return json.loads(file(self.getProjectFile(), 'r').read(), strict=False)
def excludeDirectory(self, path):
import json
project_file = self.getProjectFile();
project = self.getProjectJson()
path = re.sub(r'^([^/])\:/', '/\\1/', path.replace('\\', '/'))
for folder in project['folders']:
if path.find(folder['path']) == 0:
try:
folder['folder_exclude_patterns'].append(re.sub(r'/+$', '', path.replace(folder['path']+'/', '', 1)))
except:
folder['folder_exclude_patterns'] = [re.sub(r'/+$', '', path.replace(folder['path']+'/', '', 1))]
file(project_file, 'w+').write(json.dumps(project, indent=1))
return
def excludeFile(self, path):
import json
project_file = self.getProjectFile();
project = self.getProjectJson()
path = re.sub(r'^([^/])\:/', '/\\1/', path.replace('\\', '/'))
for folder in project['folders']:
if path.find(folder['path']) == 0:
try:
folder['file_exclude_patterns'].append(path.replace(folder['path']+'/', '', 1))
except:
folder['file_exclude_patterns'] = [path.replace(folder['path']+'/', '', 1)]
file(project_file, 'w+').write(json.dumps(project, indent=1))
return
def rootAdd(self, path):
import json
project_file = self.getProjectFile();
project = self.getProjectJson()
path = re.sub(r'^([^/])\:/', '/\\1/', path.replace('\\', '/'))
project['folders'].append({'path':path});
file(project_file, 'w+').write(json.dumps(project, indent=1))
def refresh(self):
try:
sublime.set_timeout(lambda:sublime.active_window().run_command('refresh_folder_list'), 200);
sublime.set_timeout(lambda:sublime.active_window().run_command('refresh_folder_list'), 600);
sublime.set_timeout(lambda:sublime.active_window().run_command('refresh_folder_list'), 1300);
sublime.set_timeout(lambda:sublime.active_window().run_command('refresh_folder_list'), 2300);
except:
pass
def getPreference(self, name):
if not self.hasOpenedProject():
return None
project = self.getProjectJson()
try:
return project[name]
except:
return None

View File

@@ -0,0 +1,186 @@
# coding=utf8
import sublime
import os
import re
from SideBarProject import SideBarProject
from SideBarItem import SideBarItem
class SideBarSelection:
def __init__(self, paths = []):
if len(paths) < 1:
try:
path = sublime.active_window().active_view().file_name()
if self.isNone(path):
paths = []
else:
paths = [path]
except:
paths = []
self._paths = paths
self._paths.sort()
self._obtained_selection_information_basic = False
self._obtained_selection_information_extended = False
def len(self):
return len(self._paths)
def hasDirectories(self):
self._obtainSelectionInformationBasic()
return self._has_directories
def hasFiles(self):
self._obtainSelectionInformationBasic()
return self._has_files
def hasOnlyDirectories(self):
self._obtainSelectionInformationBasic()
return self._only_directories
def hasOnlyFiles(self):
self._obtainSelectionInformationBasic()
return self._only_files
def hasProjectDirectories(self):
if self.hasDirectories():
project_directories = SideBarProject().getDirectories()
for item in self.getSelectedDirectories():
if item.path() in project_directories:
return True
return False
else:
return False
def hasItemsUnderProject(self):
for item in self.getSelectedItems():
if item.isUnderCurrentProject():
return True
return False
def hasImages(self):
return self.hasFilesWithExtension('gif|jpg|jpeg|png')
def hasFilesWithExtension(self, extensions):
extensions = re.compile('('+extensions+')$', re.I);
for item in self.getSelectedFiles():
if extensions.search(item.path()):
return True;
return False
def getSelectedItems(self):
self._obtainSelectionInformationExtended()
return self._files + self._directories;
def getSelectedItemsWithoutChildItems(self):
self._obtainSelectionInformationExtended()
items = []
for item in self._items_without_containing_child_items:
items.append(SideBarItem(item, os.path.isdir(item)))
return items
def getSelectedDirectories(self):
self._obtainSelectionInformationExtended()
return self._directories;
def getSelectedFiles(self):
self._obtainSelectionInformationExtended()
return self._files;
def getSelectedDirectoriesOrDirnames(self):
self._obtainSelectionInformationExtended()
return self._directories_or_dirnames;
def getSelectedImages(self):
return self.getSelectedFilesWithExtension('gif|jpg|jpeg|png')
def getSelectedFilesWithExtension(self, extensions):
items = []
extensions = re.compile('('+extensions+')$', re.I);
for item in self.getSelectedFiles():
if extensions.search(item.path()):
items.append(item)
return items
def _obtainSelectionInformationBasic(self):
if not self._obtained_selection_information_basic:
self._obtained_selection_information_basic = True
self._has_directories = False
self._has_files = False
self._only_directories = False
self._only_files = False
for path in self._paths:
if self._has_directories == False and os.path.isdir(path):
self._has_directories = True
if self._has_files == False and os.path.isdir(path) == False:
self._has_files = True
if self._has_files and self._has_directories:
break
if self._has_files and self._has_directories:
self._only_directories = False
self._only_files = False
elif self._has_files:
self._only_files = True
elif self._has_directories:
self._only_directories = True
def _obtainSelectionInformationExtended(self):
if not self._obtained_selection_information_extended:
self._obtained_selection_information_extended = True
self._directories = []
self._files = []
self._directories_or_dirnames = []
self._items_without_containing_child_items = []
_directories = []
_files = []
_directories_or_dirnames = []
_items_without_containing_child_items = []
for path in self._paths:
if os.path.isdir(path):
item = SideBarItem(path, True)
if item.path() not in _directories:
_directories.append(item.path())
self._directories.append(item)
if item.path() not in _directories_or_dirnames:
_directories_or_dirnames.append(item.path())
self._directories_or_dirnames.append(item)
_items_without_containing_child_items = self._itemsWithoutContainingChildItems(_items_without_containing_child_items, item.path())
else:
item = SideBarItem(path, False)
if item.path() not in _files:
_files.append(item.path())
self._files.append(item)
_items_without_containing_child_items = self._itemsWithoutContainingChildItems(_items_without_containing_child_items, item.path())
item = SideBarItem(os.path.dirname(path), True)
if item.path() not in _directories_or_dirnames:
_directories_or_dirnames.append(item.path())
self._directories_or_dirnames.append(item)
self._items_without_containing_child_items = _items_without_containing_child_items
def _itemsWithoutContainingChildItems(self, items, item):
new_list = []
add = True
for i in items:
if i.find(item+'\\') == 0 or i.find(item+'/') == 0:
continue
else:
new_list.append(i)
if (item+'\\').find(i+'\\') == 0 or (item+'/').find(i+'/') == 0:
add = False
if add:
new_list.append(item)
return new_list
def isNone(self, path):
if path == None or path == '' or path == '.' or path == '..' or path == './' or path == '/' or path == '//' or path == '\\' or path == '\\\\' or path == '\\\\\\\\':
return True
else:
return False

View File

@@ -0,0 +1,34 @@
[
// Git Chords - https://github.com/kemayo/sublime-text-2-git
{ "keys": ["ctrl+shift+g", "ctrl+shift+a"], "command": "git_add_choice" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+b"], "command": "git_branch" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+c"], "command": "git_commit" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+d"], "command": "git_diff" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+f"], "command": "git_fetch" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+g"], "command": "git_graph" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+h"], "command": "git_commit_history" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+l"], "command": "git_log" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+m"], "command": "git_merge" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+n"], "command": "git_new_branch" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+p"], "command": "git_pull_current_branch" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+q"], "command": "git_quick_commit" },
// dangerous
// { "keys": ["ctrl+shift+g", "ctrl+shift+r"], "command": "git_reset_hard_head" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+s"], "command": "git_status" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+t"], "command": "git_new_tag" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+z"], "command": "git_commit_amend" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+up"], "command": "git_push" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+down"], "command": "git_pull" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+right"], "command": "git_stash" },
{ "keys": ["ctrl+shift+g", "ctrl+shift+left"], "command": "git_stash_pop" },
//Modific - https://github.com/gornostal/Modific
{ "keys": ["ctrl+alt+c"], "command": "show_original_part" },
//using this binding with https://github.com/braindamageinc/SublimeHttpRequester
//{ "keys": ["ctrl+alt+r"], "command": "replace_modified_part" },
//Git plugin does this already
//{ "keys": ["ctrl+alt+d"], "command": "show_diff" },
{ "keys": ["ctrl+alt+u"], "command": "uncommitted_files" }
//{ "keys": ["ctrl+shift+pageup"], "command": "jump_between_changes", "args": {"direction": "prev"} },
//{ "keys": ["ctrl+shift+pagedown"], "command": "jump_between_changes", "args": {"direction": "next"} }
]

View File

@@ -0,0 +1,27 @@
SublimeKeyMap.Git
=================
A simple repository used to host / share my customized Sublime Text 2 key bindings for Git plugins
Designed to be incorporated into `Package Control.sublime-settings` like:
```json
{
"installed_packages":
[
"EditorConfig",
"Git",
"GitHubinator",
"Modific",
"SideBarGit",
"sublime-github"
],
"package_name_map": {
"SublimeKeyMap.Git": "ZZZ.EthanBrown.SublimeKeyMap.Git"
},
"repositories":
[
"https://github.com/Iristyle/SublimeKeyMap.Git"
]
}
```

View File

@@ -0,0 +1 @@
{"url": "https://github.com/Iristyle/SublimeKeyMap.Git", "version": "2013.03.17.19.57.57", "description": "A simple repository used to host / share my customized Sublime Text 2 key bindings for Git plugins"}

View File

@@ -0,0 +1 @@
*.pyc

View File

@@ -0,0 +1,6 @@
[
{ "keys": ["super+g", "super+n"], "command": "public_gist_from_selection" },
{ "keys": ["super+g", "super+p","super+n"], "command": "private_gist_from_selection" },
{ "keys": ["super+g", "super+o"], "command": "open_gist_in_editor" },
{ "keys": ["super+g", "super+c"], "command": "open_gist_url" }
]

View File

@@ -0,0 +1,37 @@
/* Sublime GitHub default settings */
{
"accounts": {
"GitHub": {
"base_uri": "https://api.github.com",
"github_token": ""
}
},
// You can add support for a private GitHub installation by adding another entry
// to the accounts entry in the User settings file (Preferences ->
// Package Settings -> GitHub -> Settings - User). You can then switch between
// accounts via the GitHub: Switch Accounts command
//
// "YourCo": {
// "base_uri": "https://github.yourco.com/api/v3",
// "github_token": ""
// }
// The format of the each line in the list of gists.
// The value is either a Python format string, or a list of Python format
// strings. In the latter case, each element of the list will be a separate
// line in the select list.
// Valid parameters:
// filename - filename of [first file in] gist
// description - description of gist
// index - 1-based index of gist in the list
//
// Some things to try:
// "gist_list_format": "%(index)d. %(filename)s: %(description)s",
// "gist_list_format": ["%(filename)s", "%(description)s"],
"gist_list_format": "%(filename)s: %(description)s",
// If true, display the list of gists in a monospace font
"gist_list_monospace": false,
// output debug info to the console
"debug": false
}

View File

@@ -0,0 +1,15 @@
[
{ "caption": "GitHub: Private Gist from Selection", "command": "private_gist_from_selection" },
{ "caption": "GitHub: Public Gist from Selection", "command": "public_gist_from_selection" },
{ "caption": "GitHub: Copy Gist to Clipboard", "command": "open_gist" },
{ "caption": "GitHub: Copy Starred Gist to Clipboard", "command": "open_starred_gist" },
{ "caption": "GitHub: Open Gist in Editor", "command": "open_gist_in_editor" },
{ "caption": "GitHub: Open Starred Gist in Editor", "command": "open_starred_gist_in_editor" },
{ "caption": "GitHub: Copy Gist URL to Clipboard", "command": "open_gist_url" },
{ "caption": "GitHub: Open Gist in Browser", "command": "open_gist_in_browser" },
{ "caption": "GitHub: Open Starred Gist in Browser", "command": "open_starred_gist_in_browser" },
{ "caption": "GitHub: Update Gist", "command": "update_gist" },
{ "caption": "GitHub: Switch Accounts", "command": "switch_accounts" },
{ "caption": "GitHub: Copy Remote URL to Clipboard", "command": "copy_remote_url" },
{ "caption": "GitHub: Open Remote URL in Browser", "command": "open_remote_url" }
]

View File

@@ -0,0 +1,20 @@
Copyright (c) 2011 Brad Greenlee
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,34 @@
[
{
"caption": "Preferences",
"mnemonic": "n",
"id": "preferences",
"children":
[
{
"caption": "Package Settings",
"mnemonic": "P",
"id": "package-settings",
"children":
[
{
"caption": "GitHub",
"children":
[
{
"command": "open_file",
"args": {"file": "${packages}/sublime-github/GitHub.sublime-settings"},
"caption": "Settings Default"
},
{
"command": "open_file",
"args": {"file": "${packages}/User/GitHub.sublime-settings"},
"caption": "Settings User"
}
]
}
]
}
]
}
]

View File

@@ -0,0 +1,18 @@
Sublime GitHub includes some external libraries to simplify installation.
Requests License
================
Copyright (c) 2012 Kenneth Reitz.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -0,0 +1,169 @@
# Sublime GitHub
This is a plugin for the [Sublime Text 2](http://www.sublimetext.com/) text
editor that allows you to create and browse your [GitHub Gists](http://gist.github.com).
## Installation
**The easiest way to install is via the** [**Sublime Package Control**](http://wbond.net/sublime_packages/package_control) **plugin.**
Just open "Package Control: Install Package" in your Command Palette and search for
"sublime-github" (or, if you already have it installed, select "Package Control: Upgrade Package"
to upgrade).
To install it manually in a shell/Terminal (on OS X, Linux or Cygwin), via git:
cd ~/"Library/Application Support/Sublime Text 2/Packages/" # location on OS X; will be different on Linux & Windows
git clone https://github.com/bgreenlee/sublime-github.git
or, if you don't have git installed:
cd ~/"Library/Application Support/Sublime Text 2/Packages/"
rm -rf bgreenlee-sublime-github* # remove any old versions
curl -L https://github.com/bgreenlee/sublime-github/tarball/master | tar xf -
The plugin should be picked up automatically. If not, restart Sublime Text.
## Usage
The first time you run one of the commands, it will ask you for your GitHub
username and password in order to create a GitHub API access token, which gets saved
in the Sublime GitHub user settings file. Your username and password are not
stored anywhere, but if you would rather generate the access token yourself, see
the "Generating Your Own Access Token" section below.
The following commands are available in the Command Palette:
* **GitHub: Private Gist from Selection**
Create a private gist from the currently selected text (or, if nothing is selected,
the contents of the active editor.
* **GitHub: Public Gist from Selection**
Create a public gist from the currently selected text (or, if nothing is selected,
the contents of the active editor.
* **GitHub: Copy Gist to Clipboard**
Displays a quick select panel listing all of your gists, and selecting one will
copy the contents of that gist to your clipboard.
* **GitHub: Copy Starred Gist to Clipboard**
Displays a quick select panel listing only your starred gists, and selecting one will
copy the contents of that gist to your clipboard.
* **GitHub: Open Gist in Editor**
Displays a quick select panel listing all of your gists, and selecting one will
open a new editor tab with the contents of that gist.
* **GitHub: Open Starred Gist in Editor**
Displays a quick select panel listing only your starred gists, and selecting one will
open a new editor tab with the contents of that gist.
* **GitHub: Open Gist in Browser**
Displays a quick select panel listing all of your gists, and selecting one will
open that gist in your default web browser.
* **GitHub: Open Starred Gist in Browser**
Displays a quick select panel listing only your starred gists, and selecting one will
open that gist in your default web browser.
* **GitHub: Update Gist**
Update the gist open in the current editor.
* **GitHub: Switch Accounts**
Switch to another GitHub account (see Adding Additional Accounts below)
* **GitHub: Open Remote URL in Browser**
Open the current file's location in the repository in the browser. *Note:* Requires
the Git plugin, available through the Package Manager. After installing, restart
Sublime Text.
* **GitHub: Copy Remote URL to Clipboard**
Put the url of the current file's location in the repository into the clipboard.
*Note:* Requires the Git plugin, available through the Package Manager. After
installing, restart Sublime Text.
## Adding Additional Accounts
If have multiple GitHub accounts, or have a private GitHub installation, you can add the other
accounts and switch between them whenever you like.
Go to the GitHub user settings file (Preferences -> Package Settings -> GitHub -> Settings - User),
and add another entry to the `accounts` dictionary. If it is another GitHub account, copy the
`base_uri` for the default GitHub entry (if you don't see it, you can get it from Preferences ->
Package Settings -> GitHub -> Settings - Default, or in the example below), and just give the
account a different name. If you're adding a private GitHub installation, the `base_uri` will be
whatever the base url is for your private GitHub, plus "/api/v3". For example:
"accounts":
{
"GitHub":
{
"base_uri": "https://api.github.com",
"github_token": "..."
},
"YourCo":
{
"base_uri": "https://github.yourco.com/api/v3",
"github_token": ""
},
}
Don't worry about setting the `github_token`--that will be set for you automatically, after you
switch accounts (Shift-Cmd-P, "GitHub: Switch Accounts").
## Issues
* Linux requires the [curl](http://curl.haxx.se/) binary to be installed on your system (in one of:
`/usr/local/sbin`, `/usr/local/bin`, `/usr/sbin`, `/usr/bin`, `/sbin`, or `/bin`).
* Depending on the number of gists you have, there can be a considerable delay the first time
your list of gists is fetched. Subsequent requests will be cached and should be a bit faster
(although the GitHub API's ETags are currently not correct; once that fix that, it should speed
things up). In the meantime, if there are gists that you open frequently, open them on GitHub and
"Star" them, then access them via the Open/Copy Starred Gist commands.
* Setting the file type for syntax highlighting when opening a gist in the editor does not work
in Linux. I could get it to work with significant effort, so if you desperately want it, open
an issue.
## Generating Your Own Access Token
If you feel uncomfortable giving your GitHub username and password to the
plugin, you can generate a GitHub API access token yourself. Just open up
a Terminal window/shell (on OS X, Linux or Cygwin), and run:
curl -u username -d '{"scopes":["gist"]}' https://api.github.com/authorizations
where `username` is your GitHub username. You'll be prompt for your password first. Then you'll get back
a response that includes a 40-digit "token" value (e.g. `6423ba8429a152ff4a7279d1e8f4674029d3ef87`).
Go to Sublime Text 2 -> Preferences -> Package Settings -> GitHub -> Settings - User,
and insert the token there. It should look like:
{
"github_token": "6423ba8429a152ff4a7279d1e8f4674029d3ef87"
}
Restart Sublime.
That's it!
## Bugs and Feature Requests
<http://github.com/bgreenlee/sublime-github/issues>
## Copyright
Copyright &copy; 2011+ Brad Greenlee. See LICENSE for details.

View File

@@ -0,0 +1,28 @@
# adapted from https://github.com/wbond/sublime_package_control/blob/master/Package%20Control.py
import os.path
import subprocess
class BinaryNotFoundError(Exception):
pass
def find_binary(name):
dirs = ['/usr/local/sbin', '/usr/local/bin', '/usr/sbin', '/usr/bin',
'/sbin', '/bin']
for dir in dirs:
path = os.path.join(dir, name)
if os.path.exists(path):
return path
raise BinaryNotFoundError('The binary ' + name + ' could not be ' + \
'located')
def execute(args):
proc = subprocess.Popen(args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = proc.stdout.read()
proc.wait()
return output

View File

@@ -0,0 +1,131 @@
import sublime
import os.path
import json
import sublime_requests as requests
import sys
import logging
logging.basicConfig(format='%(asctime)s %(message)s')
logger = logging.getLogger()
class GitHubApi(object):
"Encapsulates the GitHub API"
PER_PAGE = 100
etags = {}
cache = {}
class UnauthorizedException(Exception):
"Raised if we get a 401 from GitHub"
pass
class UnknownException(Exception):
"Raised if we get a response code we don't recognize from GitHub"
pass
def __init__(self, base_uri="https://api.github.com", token=None, debug=False):
self.base_uri = base_uri
self.token = token
self.debug = debug
if debug:
logger.setLevel(logging.DEBUG)
# set up requests session with the root CA cert bundle
cert_path = os.path.join(sublime.packages_path(), "sublime-github", "ca-bundle.crt")
if not os.path.isfile(cert_path):
logger.warning("Root CA cert bundle not found at %s! Not verifying requests." % cert_path)
cert_path = None
self.rsession = requests.session(verify=cert_path,
config={'verbose': sys.stderr if self.debug else None})
def get_token(self, username, password):
auth_data = {
"scopes": ["gist"],
"note": "Sublime GitHub",
"note_url": "https://github.com/bgreenlee/sublime-github"
}
resp = self.rsession.post(self.base_uri + "/authorizations",
auth=(username, password),
data=json.dumps(auth_data))
if resp.status_code == requests.codes.CREATED:
data = json.loads(resp.text)
return data["token"]
elif resp.status_code == requests.codes.UNAUTHORIZED:
raise self.UnauthorizedException()
else:
raise self.UnknownException("%d %s" % (resp.status_code, resp.text))
def post(self, endpoint, data=None, content_type='application/json'):
return self.request('post', endpoint, data=data, content_type=content_type)
def patch(self, endpoint, data=None, content_type='application/json'):
return self.request('patch', endpoint, data=data, content_type=content_type)
def get(self, endpoint, params=None):
return self.request('get', endpoint, params=params)
def request(self, method, url, params=None, data=None, content_type=None):
if not url.startswith("http"):
url = self.base_uri + url
if data:
data = json.dumps(data)
headers = {"Authorization": "token %s" % self.token}
if content_type:
headers["Content-Type"] = content_type
# add an etag to the header if we have one
if method == 'get' and url in self.etags:
headers["If-None-Match"] = self.etags[url]
logger.debug("request: %s %s %s %s" % (method, url, headers, params))
resp = self.rsession.request(method, url,
headers=headers,
params=params,
data=data,
allow_redirects=True)
full_url = resp.url
logger.debug("response: %s" % resp.headers)
if resp.status_code in [requests.codes.OK,
requests.codes.CREATED,
requests.codes.FOUND,
requests.codes.CONTINUE]:
if 'application/json' in resp.headers['content-type']:
resp_data = json.loads(resp.text)
else:
resp_data = resp.text
if method == 'get': # cache the response
etag = resp.headers['etag']
self.etags[full_url] = etag
self.cache[etag] = resp_data
return resp_data
elif resp.status_code == requests.codes.NOT_MODIFIED:
return self.cache[resp.headers['etag']]
elif resp.status_code == requests.codes.UNAUTHORIZED:
raise self.UnauthorizedException()
else:
raise self.UnknownException("%d %s" % (resp.status_code, resp.text))
def create_gist(self, description="", filename="", content="", public=False):
return self.post("/gists", {"description": description,
"public": public,
"files": {filename: {"content": content}}})
def update_gist(self, gist, content):
filename = gist["files"].keys()[0]
return self.patch("/gists/" + gist["id"],
{"description": gist["description"],
"files": {filename: {"content": content}}})
def list_gists(self, starred=False):
page = 1
data = []
# fetch all pages
while True:
endpoint = "/gists" + ("/starred" if starred else "")
page_data = self.get(endpoint, params={'page': page, 'per_page': self.PER_PAGE})
data.extend(page_data)
if len(page_data) < self.PER_PAGE:
break
page += 1
return data

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# __
# /__) _ _ _ _ _/ _
# / ( (- (/ (/ (- _) / _)
# /
"""
requests
~~~~~~~~
:copyright: (c) 2012 by Kenneth Reitz.
:license: ISC, see LICENSE for more details.
"""
__title__ = 'requests'
__version__ = '0.10.2'
__build__ = 0x001002
__author__ = 'Kenneth Reitz'
__license__ = 'ISC'
__copyright__ = 'Copyright 2012 Kenneth Reitz'
from . import utils
from .models import Request, Response
from .api import request, get, head, post, patch, put, delete, options
from .sessions import session, Session
from .status_codes import codes
from .exceptions import (
RequestException, Timeout, URLRequired,
TooManyRedirects, HTTPError, ConnectionError
)

View File

@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
"""
requests.api
~~~~~~~~~~~~
This module implements the Requests API.
:copyright: (c) 2012 by Kenneth Reitz.
:license: ISC, see LICENSE for more details.
"""
from . import sessions
def request(method, url, **kwargs):
"""Constructs and sends a :class:`Request <Request>`.
Returns :class:`Response <Response>` object.
:param method: method for the new :class:`Request` object.
:param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
:param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
:param files: (optional) Dictionary of 'name': file-like-objects (or {'name': ('filename', fileobj)}) for multipart encoding upload.
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
:param timeout: (optional) Float describing the timeout of the request.
:param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed.
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
:param return_response: (optional) If False, an un-sent Request object will returned.
:param session: (optional) A :class:`Session` object to be used for the request.
:param config: (optional) A configuration dictionary.
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
:param prefetch: (optional) if ``True``, the response content will be immediately downloaded.
"""
s = kwargs.pop('session') if 'session' in kwargs else sessions.session()
return s.request(method=method, url=url, **kwargs)
def get(url, **kwargs):
"""Sends a GET request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param **kwargs: Optional arguments that ``request`` takes.
"""
kwargs.setdefault('allow_redirects', True)
return request('get', url, **kwargs)
def options(url, **kwargs):
"""Sends a OPTIONS request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param **kwargs: Optional arguments that ``request`` takes.
"""
kwargs.setdefault('allow_redirects', True)
return request('options', url, **kwargs)
def head(url, **kwargs):
"""Sends a HEAD request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param **kwargs: Optional arguments that ``request`` takes.
"""
kwargs.setdefault('allow_redirects', True)
return request('head', url, **kwargs)
def post(url, data=None, **kwargs):
"""Sends a POST request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
:param **kwargs: Optional arguments that ``request`` takes.
"""
return request('post', url, data=data, **kwargs)
def put(url, data=None, **kwargs):
"""Sends a PUT request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
:param **kwargs: Optional arguments that ``request`` takes.
"""
return request('put', url, data=data, **kwargs)
def patch(url, data=None, **kwargs):
"""Sends a PATCH request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
:param **kwargs: Optional arguments that ``request`` takes.
"""
return request('patch', url, data=data, **kwargs)
def delete(url, **kwargs):
"""Sends a DELETE request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param **kwargs: Optional arguments that ``request`` takes.
"""
return request('delete', url, **kwargs)

View File

@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
"""
requests.async
~~~~~~~~~~~~~~
This module contains an asynchronous replica of ``requests.api``, powered
by gevent. All API methods return a ``Request`` instance (as opposed to
``Response``). A list of requests can be sent with ``map()``.
"""
try:
import gevent
from gevent import monkey as curious_george
from gevent.pool import Pool
except ImportError:
raise RuntimeError('Gevent is required for requests.async.')
# Monkey-patch.
curious_george.patch_all(thread=False)
from . import api
__all__ = (
'map',
'get', 'options', 'head', 'post', 'put', 'patch', 'delete', 'request'
)
def patched(f):
"""Patches a given API function to not send."""
def wrapped(*args, **kwargs):
kwargs['return_response'] = False
kwargs['prefetch'] = True
config = kwargs.get('config', {})
config.update(safe_mode=True)
kwargs['config'] = config
return f(*args, **kwargs)
return wrapped
def send(r, pool=None, prefetch=False):
"""Sends the request object using the specified pool. If a pool isn't
specified this method blocks. Pools are useful because you can specify size
and can hence limit concurrency."""
if pool != None:
return pool.spawn(r.send, prefetch=prefetch)
return gevent.spawn(r.send, prefetch=prefetch)
# Patched requests.api functions.
get = patched(api.get)
options = patched(api.options)
head = patched(api.head)
post = patched(api.post)
put = patched(api.put)
patch = patched(api.patch)
delete = patched(api.delete)
request = patched(api.request)
def map(requests, prefetch=True, size=None):
"""Concurrently converts a list of Requests to Responses.
:param requests: a collection of Request objects.
:param prefetch: If False, the content will not be downloaded immediately.
:param size: Specifies the number of requests to make at a time. If None, no throttling occurs.
"""
requests = list(requests)
pool = Pool(size) if size else None
jobs = [send(r, pool, prefetch=prefetch) for r in requests]
gevent.joinall(jobs)
return [r.response for r in requests]

View File

@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
"""
requests.auth
~~~~~~~~~~~~~
This module contains the authentication handlers for Requests.
"""
from __future__ import unicode_literals
import time
import hashlib
from base64 import b64encode
from .compat import urlparse, str, bytes
from .utils import randombytes, parse_dict_header
def _basic_auth_str(username, password):
"""Returns a Basic Auth string."""
return 'Basic ' + b64encode(("%s:%s" % (username, password)).encode('utf-8')).strip().decode('utf-8')
class AuthBase(object):
"""Base class that all auth implementations derive from"""
def __call__(self, r):
raise NotImplementedError('Auth hooks must be callable.')
class HTTPBasicAuth(AuthBase):
"""Attaches HTTP Basic Authentication to the given Request object."""
def __init__(self, username, password):
self.username = username
self.password = password
def __call__(self, r):
r.headers['Authorization'] = _basic_auth_str(self.username, self.password)
return r
class HTTPProxyAuth(HTTPBasicAuth):
"""Attaches HTTP Proxy Authenetication to a given Request object."""
def __call__(self, r):
r.headers['Proxy-Authorization'] = _basic_auth_str(self.username, self.password)
return r
class HTTPDigestAuth(AuthBase):
"""Attaches HTTP Digest Authentication to the given Request object."""
def __init__(self, username, password):
self.username = username
self.password = password
def handle_401(self, r):
"""Takes the given response and tries digest-auth, if needed."""
s_auth = r.headers.get('www-authenticate', '')
if 'digest' in s_auth.lower():
last_nonce = ''
nonce_count = 0
chal = parse_dict_header(s_auth.replace('Digest ', ''))
realm = chal['realm']
nonce = chal['nonce']
qop = chal.get('qop')
algorithm = chal.get('algorithm', 'MD5')
opaque = chal.get('opaque', None)
algorithm = algorithm.upper()
# lambdas assume digest modules are imported at the top level
if algorithm == 'MD5':
def h(x):
if isinstance(x, str):
x = x.encode('utf-8')
return hashlib.md5(x).hexdigest()
H = h
elif algorithm == 'SHA':
def h(x):
if isinstance(x, str):
x = x.encode('utf-8')
return hashlib.sha1(x).hexdigest()
H = h
# XXX MD5-sess
KD = lambda s, d: H("%s:%s" % (s, d))
if H is None:
return None
# XXX not implemented yet
entdig = None
p_parsed = urlparse(r.request.url)
path = p_parsed.path
if p_parsed.query:
path += '?' + p_parsed.query
A1 = '%s:%s:%s' % (self.username, realm, self.password)
A2 = '%s:%s' % (r.request.method, path)
if qop == 'auth':
if nonce == last_nonce:
nonce_count += 1
else:
nonce_count = 1
last_nonce = nonce
ncvalue = '%08x' % nonce_count
s = str(nonce_count).encode('utf-8')
s += nonce.encode('utf-8')
s += time.ctime().encode('utf-8')
s += randombytes(8)
cnonce = (hashlib.sha1(s).hexdigest()[:16])
noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, H(A2))
respdig = KD(H(A1), noncebit)
elif qop is None:
respdig = KD(H(A1), "%s:%s" % (nonce, H(A2)))
else:
# XXX handle auth-int.
return None
# XXX should the partial digests be encoded too?
base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
'response="%s"' % (self.username, realm, nonce, path, respdig)
if opaque:
base += ', opaque="%s"' % opaque
if entdig:
base += ', digest="%s"' % entdig
base += ', algorithm="%s"' % algorithm
if qop:
base += ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce)
r.request.headers['Authorization'] = 'Digest %s' % (base)
r.request.send(anyway=True)
_r = r.request.response
_r.history.append(r)
return _r
return r
def __call__(self, r):
r.register_hook('response', self.handle_401)
return r

View File

@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
"""
pythoncompat
"""
import sys
# -------
# Pythons
# -------
# Syntax sugar.
_ver = sys.version_info
#: Python 2.x?
is_py2 = (_ver[0] == 2)
#: Python 3.x?
is_py3 = (_ver[0] == 3)
#: Python 3.0.x
is_py30 = (is_py3 and _ver[1] == 0)
#: Python 3.1.x
is_py31 = (is_py3 and _ver[1] == 1)
#: Python 3.2.x
is_py32 = (is_py3 and _ver[1] == 2)
#: Python 3.3.x
is_py33 = (is_py3 and _ver[1] == 3)
#: Python 3.4.x
is_py34 = (is_py3 and _ver[1] == 4)
#: Python 2.7.x
is_py27 = (is_py2 and _ver[1] == 7)
#: Python 2.6.x
is_py26 = (is_py2 and _ver[1] == 6)
#: Python 2.5.x
is_py25 = (is_py2 and _ver[1] == 5)
#: Python 2.4.x
is_py24 = (is_py2 and _ver[1] == 4) # I'm assuming this is not by choice.
# ---------
# Platforms
# ---------
# Syntax sugar.
_ver = sys.version.lower()
is_pypy = ('pypy' in _ver)
is_jython = ('jython' in _ver)
is_ironpython = ('iron' in _ver)
# Assume CPython, if nothing else.
is_cpython = not any((is_pypy, is_jython, is_ironpython))
# Windows-based system.
is_windows = 'win32' in str(sys.platform).lower()
# Standard Linux 2+ system.
is_linux = ('linux' in str(sys.platform).lower())
is_osx = ('darwin' in str(sys.platform).lower())
is_hpux = ('hpux' in str(sys.platform).lower()) # Complete guess.
is_solaris = ('solar==' in str(sys.platform).lower()) # Complete guess.
# ---------
# Specifics
# ---------
if is_py2:
from urllib import quote, unquote, urlencode
from urlparse import urlparse, urlunparse, urljoin, urlsplit
from urllib2 import parse_http_list
import cookielib
from .packages.oreos.monkeys import SimpleCookie
from StringIO import StringIO
bytes = str
str = unicode
basestring = basestring
elif is_py3:
from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote
from urllib.request import parse_http_list
from http import cookiejar as cookielib
from http.cookies import SimpleCookie
from io import StringIO
str = str
bytes = bytes
basestring = (str,bytes)

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
"""
requests.defaults
~~~~~~~~~~~~~~~~~
This module provides the Requests configuration defaults.
Configurations:
:base_headers: Default HTTP headers.
:verbose: Stream to write request logging to.
:max_redirects: Maximum number of redirects allowed within a request.s
:keep_alive: Reuse HTTP Connections?
:max_retries: The number of times a request should be retried in the event of a connection failure.
:danger_mode: If true, Requests will raise errors immediately.
:safe_mode: If true, Requests will catch all errors.
:pool_maxsize: The maximium size of an HTTP connection pool.
:pool_connections: The number of active HTTP connection pools to use.
"""
from . import __version__
defaults = dict()
defaults['base_headers'] = {
'User-Agent': 'python-requests/%s' % __version__,
'Accept-Encoding': ', '.join(('identity', 'deflate', 'compress', 'gzip')),
'Accept': '*/*'
}
defaults['verbose'] = None
defaults['max_redirects'] = 30
defaults['pool_connections'] = 10
defaults['pool_maxsize'] = 10
defaults['max_retries'] = 0
defaults['danger_mode'] = False
defaults['safe_mode'] = False
defaults['keep_alive'] = True

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
"""
requests.exceptions
~~~~~~~~~~~~~~~~~~~
This module contains the set of Requests' exceptions.
"""
class RequestException(Exception):
"""There was an ambiguous exception that occurred while handling your
request."""
class HTTPError(RequestException):
"""An HTTP error occurred."""
class ConnectionError(RequestException):
"""A Connection error occurred."""
class SSLError(ConnectionError):
"""An SSL error occurred."""
class Timeout(RequestException):
"""The request timed out."""
class URLRequired(RequestException):
"""A valid URL is required to make a request."""
class TooManyRedirects(RequestException):
"""Too many redirects."""

Some files were not shown because too many files have changed in this diff Show More