blob: 1f004883374fc841e25ee96cdfd99785e02bff4d [file] [log] [blame]
# Copyright (C) 2018 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. 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.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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.
import os
import re
import sublime
import sublime_plugin
import subprocess
import webbrowser
global s_settings
def plugin_loaded():
global s_settings
s_settings = Settings()
def plugin_unloaded():
s_settings.unload()
class Settings(object):
def __init__(self):
self._settings = sublime.load_settings('CopyWebKitPermalink.sublime-settings')
self._cache = {}
def unload(self):
for key in self._cache:
self._settings.clear_on_change(key)
@property
def include_revision(self):
return self._get('include_revision', default=True)
@include_revision.setter
def include_revision(self, value):
self._set('include_revision', value)
@property
def automatically_open_in_browser(self):
return self._get('automatically_open_in_browser', default=False)
@automatically_open_in_browser.setter
def automatically_open_in_browser(self, value):
self._set('automatically_open_in_browser', value)
def _get(self, key, default=None):
if key not in self._cache:
self._settings.add_on_change(key, lambda : self._cache.pop(key, None))
self._cache[key] = self._settings.get(key, default)
return self._cache[key]
def _set(self, key, value):
if key in self._cache and self._cache[key] == value:
return
self._cache[key] = value
self._settings.set(key, value)
class CopyWebKitPermalinkCommand(sublime_plugin.TextCommand):
def run(self, edit, annotate_blame=False):
if not self.is_enabled():
return
document_path = self.view.file_name()
self._last_svn_info = None
self._directory_in_checkout = document_path if os.path.isdir(document_path) else os.path.dirname(document_path)
self.determine_vcs_from_path(document_path)
if s_settings.include_revision and not self.path_is_in_webkit_checkout(document_path):
return
line_number, _ = self.view.rowcol(self.view.sel()[0].begin()) # Zero-based
line_number = line_number + 1
path = self.path_relative_to_repository_root_for_path(document_path)
revision_info = self.revision_info_for_path(document_path)
permalink = self.permalink_for_path(path, line_number, revision_info, annotate_blame)
sublime.set_clipboard(permalink)
if s_settings.automatically_open_in_browser:
webbrowser.open(permalink)
def is_enabled(self):
return len(self.view.sel()) > 0 and bool(self.view.file_name())
def is_visible(self):
return self.is_enabled()
def description(self):
return 'Copy WebKit Permalink'
def determine_vcs_from_path(self, path):
if not os.path.isdir(path):
path = os.path.dirname(path)
self._is_svn = False
self._is_git = False
self._is_git_svn = False
if self.is_svn_directory(path):
self._is_svn = True
return
if self.is_git_svn_directory(path):
self._is_git = True
self._is_git_svn = True
return
if self.is_git_directory(path):
self._is_git = True
return
def path_is_in_webkit_checkout(self, path):
repository_url = self.revision_info_for_path(path).get('repository_url', '')
return bool(re.match(r'\w+:\/\/\w+\.webkit.org', repository_url))
def git_path_relative_to_repository_root_for_path(self, path):
return subprocess.check_output(['git', 'ls-tree', '--full-name', '--name-only', 'HEAD', path], cwd=self._directory_in_checkout).decode('utf-8').rstrip()
def svn_path_relative_to_repository_root_for_path(self, path):
return self.svn_info_for_path(path)['path']
def path_relative_to_repository_root_for_path(self, path):
if self._is_svn:
return self.svn_path_relative_to_repository_root_for_path(path)
if self._is_git:
return self.git_path_relative_to_repository_root_for_path(path)
return ''
def revision_info_for_path(self, path):
if s_settings.include_revision:
if self._is_svn or self._is_git_svn:
return self.svn_revision_info_for_path(path)
if self._is_git:
return self.git_revision_info_for_path(path)
return {}
def svn_revision_info_for_path(self, path):
svn_info = self.svn_info_for_path(path)
return {'branch': svn_info['branch'], 'revision': svn_info['revision'], 'repository_url': svn_info['repositoryRoot']}
def git_revision_info_for_path(self, path):
repository_url = subprocess.check_output(['git', 'remote', 'get-url', 'origin'], cwd=self._directory_in_checkout).decode('utf-8').rstrip()
revision = subprocess.check_output(['git', 'log', '-1', '--format', '%H', path], cwd=self._directory_in_checkout).decode('utf-8').rstrip()
branch = subprocess.check_output(['git', 'symbolic-ref', '-q', 'HEAD'], cwd=self._directory_in_checkout).decode('utf-8').rstrip()
branch = re.sub(r'^refs\/heads\/', '', branch) or 'master'
return {branch, revision, repository_url}
def svn_info_for_path(self, path):
if self._last_svn_info and self._last_svn_info['path'] == path:
# FIXME: We should also ensure that the checkout directory for the cached SVN info is
# the same as the specified checkout directory.
return self._last_svn_info
svn_info_command = ['svn', 'info']
if self._is_git_svn:
svn_info_command = ['git'] + svn_info_command
output = subprocess.check_output(svn_info_command + [path], cwd=self._directory_in_checkout).decode('utf-8').rstrip()
if not output:
return {}
temp = {}
lines = output.splitlines()
for line in lines:
key, value = line.split(': ', 1)
if key and value:
temp[key] = value
svn_info = {
'pathAsURL': temp['URL'],
'repositoryRoot': temp['Repository Root'],
'revision': temp['Revision'],
}
branch = svn_info['pathAsURL'].replace(svn_info['repositoryRoot'] + '/', '')
branch = branch[0:branch.find('/')]
svn_info['branch'] = branch
# Although tempting to use temp['Path'] we cannot because it is relative to self._directory_in_checkout.
# And self._directory_in_checkout may not be the top-level checkout directory. We need to compute the
# relative path with respect to the top-level checkout directory.
svn_info['path'] = svn_info['pathAsURL'].replace('{}/{}/'.format(svn_info['repositoryRoot'], branch), '')
self._last_svn_info = svn_info
return svn_info
@staticmethod
def permalink_for_path(path, line_number, revision_info, annotate_blame):
revision = '&rev=' + str(revision_info['revision']) if 'revision' in revision_info else ''
line_number = '#L' + str(line_number) if line_number else ''
branch = revision_info.get('branch', 'trunk')
annotate_blame = '&annotate=blame' if annotate_blame else ''
return 'https://trac.webkit.org/browser/{}/{}?{}{}{}'.format(branch, path, revision, annotate_blame, line_number)
@staticmethod
def is_svn_directory(directory):
try:
subprocess.check_call(['svn', 'info'], cwd=directory)
except:
return False
return True
@staticmethod
def is_git_directory(directory):
try:
subprocess.check_call(['git', 'rev-parse'], cwd=directory)
except:
return False
return True
@staticmethod
def is_git_svn_directory(directory):
try:
return bool(subprocess.check_output(['git', 'config', '--get', 'svn-remote.svn.fetch'], cwd=directory, stderr=subprocess.STDOUT).decode('utf-8').rstrip())
except:
return False