feat(SublimeText2.GitPackages): cache packages
This commit is contained in:
4
EthanBrown.SublimeText2.GitPackages/tools/PackageCache/Git/.gitignore
vendored
Normal file
4
EthanBrown.SublimeText2.GitPackages/tools/PackageCache/Git/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.tmLanguage.cache
|
||||
.DS_Store
|
||||
package-metadata.json
|
@@ -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>
|
@@ -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"
|
||||
}
|
||||
]
|
@@ -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"}]}
|
||||
]
|
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rulers": [70]
|
||||
}
|
@@ -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": " "}
|
||||
}
|
@@ -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": "-" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@@ -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.
|
@@ -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'])
|
@@ -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)
|
@@ -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')
|
@@ -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))
|
@@ -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])
|
@@ -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)
|
@@ -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))
|
@@ -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'
|
@@ -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'
|
@@ -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)
|
@@ -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)
|
@@ -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"
|
||||
}
|
@@ -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>
|
@@ -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"
|
||||
}
|
@@ -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>
|
@@ -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"
|
||||
}
|
@@ -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}) -( \(.*?\))? (.*) (\(.*) (<.*>) .*</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>
|
Reference in New Issue
Block a user