import sublime import sublime_plugin import os import subprocess from .common import ( SETTINGS_FILE_BASENAME, SETTINGS_KEY_COMMIT_SKIPPING_MODE, SETTINGS_KEY_TEMPORARY_COMMIT_SKIPPING_MODE, ) from .util import view_is_suitable, communicate_error, platform_startupinfo from .templates import blame_phantom_html_template, blame_phantom_css from .commit_skipping import BlameSetCommitSkippingMode # @todo #0 Add a [Prev] button to the phantom, that causes it to reflect the previous commit that changed the line. # This has some overlap with the "commit-skipping" feature and possibly obsoletes it? class Blame(sublime_plugin.TextCommand): # Overrides -------------------------------------------------- def __init__(self, view): super().__init__(view) self.phantom_set = sublime.PhantomSet(view, "git-blame") def run(self, edit): if not view_is_suitable(self.view): return phantoms = [] self.view.erase_phantoms("git-blame") # Before adding the phantom, see if the current phantom that is displayed is at the same spot at the selection if self.phantom_set.phantoms: phantom_exists = self.view.line(self.view.sel()[0]) == self.view.line( self.phantom_set.phantoms[0].region ) if phantom_exists: self.phantom_set.update(phantoms) return for region in self.view.sel(): line = self.view.line(region) (row, col) = self.view.rowcol(region.begin()) full_path = self.view.file_name() try: blame_output = self.get_blame(int(row) + 1, full_path) except Exception as e: communicate_error(e) return sha, user, date, time = self.parse_blame(blame_output) phantom = sublime.Phantom( line, blame_phantom_html_template.format( css=blame_phantom_css, sha=sha, user=user, date=date, time=time ), sublime.LAYOUT_BLOCK, self.on_phantom_close, ) phantoms.append(phantom) self.phantom_set.update(phantoms) def on_phantom_close(self, href): href_parts = href.split("-") if len(href_parts) > 1: intent = href_parts[0] sha = href_parts[1] # The SHA output by git-blame may have a leading caret to indicate # that it is a "boundary commit". That useful information has # already been shown in the phantom, so strip it before going on to # use the SHA programmatically. sha = sha.strip("^") if intent == "copy": sublime.set_clipboard(sha) sublime.status_message("Git SHA copied to clipboard") elif intent == "show": try: desc = self.get_commit(sha, self.view.file_name()) except Exception as e: communicate_error(e) return buf = self.view.window().new_file() buf.run_command( "blame_insert_commit_description", {"desc": desc, "scratch_view_name": "commit " + sha}, ) else: self.view.erase_phantoms("git-blame") else: self.view.erase_phantoms("git-blame") # ------------------------------------------------------------ def get_blame(self, line, path): cmd_line = [ "git", "blame", "--minimal", "-w", "-L {0},{0}".format(line), os.path.basename(path), ] # @todo #21 Factor out loading of the commit-skipping mode so that the BlameShowAll command can use it too. skipping_mode = self.view.settings().get( SETTINGS_KEY_TEMPORARY_COMMIT_SKIPPING_MODE, None ) if skipping_mode is None: settings_file = sublime.load_settings(SETTINGS_FILE_BASENAME) skipping_mode = settings_file.get( SETTINGS_KEY_COMMIT_SKIPPING_MODE, BlameSetCommitSkippingMode.MODE_NONE ) try: cmd_line += BlameSetCommitSkippingMode.DETAIL[skipping_mode]["git_args"] except KeyError as e: communicate_error("Unexpected commit skipping mode: {0}".format(e)) # print(cmd_line) return subprocess.check_output( cmd_line, cwd=os.path.dirname(os.path.realpath(path)), startupinfo=platform_startupinfo(), stderr=subprocess.STDOUT, ).decode("utf-8") def parse_blame(self, blame): sha, file_path, user, date, time, tz_offset, *_ = blame.split() # Was part of the inital commit so no updates if file_path[0] == "(": user, date, time, tz_offset = file_path, user, date, time file_path = None # Fix an issue where the username has a space # Im going to need to do something better though if people # start to have multiple spaces in their names. if not date[0].isdigit(): user = "{0} {1}".format(user, date) date, time = time, tz_offset return (sha, user[1:], date, time) def get_commit(self, sha, path): return subprocess.check_output( ["git", "show", sha], cwd=os.path.dirname(os.path.realpath(path)), startupinfo=platform_startupinfo(), stderr=subprocess.STDOUT, ).decode("utf-8") class BlameInsertCommitDescription(sublime_plugin.TextCommand): # Overrides -------------------------------------------------- def run(self, edit, desc, scratch_view_name): view = self.view view.set_scratch(True) view.assign_syntax("Packages/Diff/Diff.sublime-syntax") view.insert(edit, 0, desc) view.set_name(scratch_view_name)