-
-
Notifications
You must be signed in to change notification settings - Fork 137
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
387 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
""" | ||
Bitbucket methods that are functionally separate from anything Sublime-related. | ||
""" | ||
|
||
import re | ||
from collections import namedtuple | ||
from functools import partial, lru_cache | ||
from webbrowser import open as open_in_browser | ||
|
||
from ..common import interwebs, util | ||
from ..core.exceptions import FailedBitbucketRequest | ||
from ..core.settings import GitSavvySettings | ||
|
||
|
||
BITBUCKET_PER_PAGE_MAX = 100 | ||
BITBUCKET_ERROR_TEMPLATE = "Error {action} Bitbucket: {payload}" | ||
AUTH_ERROR_TEMPLATE = """Error {action} Bitbucket, access was denied! | ||
Please ensure you have created a Bitbucket API token and added it to | ||
your settings, as described in the documentation: | ||
https://github.com/divmain/GitSavvy/blob/master/docs/bitbucket.md#setup | ||
""" | ||
|
||
BitbucketRepo = namedtuple("BitbucketRepo", ("url", "fqdn", "owner", "repo", "token")) | ||
|
||
|
||
@lru_cache() | ||
def remote_to_url(remote): | ||
""" | ||
Parse out a Bitbucket HTTP URL from a remote URI: | ||
>>> remote_to_url("git://bitbucket.org/pasha_savchenko/GitSavvy.git") | ||
'https://bitbucket.org/pasha_savchenko/GitSavvy' | ||
>>> remote_to_url("git@bitbucket.org:pasha_savchenko/gitsavvy.git") | ||
'https://bitbucket.org/pasha_savchenko/gitsavvy' | ||
>>> remote_to_url("https://pasha_savchenko@bitbucket.org/pasha_savchenko/GitSavvy.git") | ||
'https://pasha_savchenko@bitbucket.org/pasha_savchenko/GitSavvy' | ||
""" | ||
|
||
if remote.endswith(".git"): | ||
remote = remote[:-4] | ||
|
||
if remote.startswith("git@"): | ||
return remote.replace(":", "/").replace("git@", "https://") | ||
elif remote.startswith("git://"): | ||
return remote.replace("git://", "https://") | ||
elif remote.startswith("http"): | ||
return remote | ||
else: | ||
util.debug.log_error('Cannot parse remote "%s" to url' % remote) | ||
return None | ||
|
||
|
||
@lru_cache() | ||
def parse_remote(remote): | ||
""" | ||
Given a line of output from `git remote -v`, parse the string and return | ||
an object with original url, FQDN, owner, repo, and the token to use for | ||
this particular FQDN (if available). | ||
""" | ||
url = remote_to_url(remote) | ||
if not url: | ||
return None | ||
|
||
match = re.match(r"https?://([a-zA-Z-\.0-9]+)/([a-zA-Z-\._0-9]+)/([a-zA-Z-\._0-9]+)/?", url) | ||
|
||
if not match: | ||
util.log_error('Invalid Bitbucket url: %s' % url) | ||
return None | ||
|
||
fqdn, owner, repo = match.groups() | ||
|
||
api_tokens = GitSavvySettings().get("api_tokens") | ||
token = api_tokens and api_tokens.get(fqdn, None) or None | ||
|
||
return BitbucketRepo(url, fqdn, owner, repo, token) | ||
|
||
|
||
def open_file_in_browser(rel_path, remote, commit_hash, start_line=None, end_line=None): | ||
""" | ||
Open the URL corresponding to the provided `rel_path` on `remote`. | ||
""" | ||
bitbucket_repo = parse_remote(remote) | ||
if not bitbucket_repo: | ||
return None | ||
|
||
line_numbers = "#lines-{}:{}".format(start_line, end_line) if start_line is not None else "" | ||
|
||
url = "{repo_url}/src/{commit_hash}/{path}{lines}".format( | ||
repo_url=bitbucket_repo.url, | ||
commit_hash=commit_hash, | ||
path=rel_path, | ||
lines=line_numbers | ||
) | ||
|
||
open_in_browser(url) | ||
|
||
|
||
def open_repo(remote): | ||
""" | ||
Open the GitHub repo in a new browser window, given the specified remote. | ||
""" | ||
bitbucket_repo = parse_remote(remote) | ||
if not bitbucket_repo: | ||
return None | ||
open_in_browser(bitbucket_repo.url) | ||
|
||
|
||
def open_issues(remote): | ||
""" | ||
Open the GitHub issues in a new browser window, given the specified remote. | ||
""" | ||
bitbucket_repo = parse_remote(remote) | ||
if not bitbucket_repo: | ||
return None | ||
open_in_browser("{}/issues".format(bitbucket_repo.url)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .open_on_remote import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import sublime | ||
from sublime_plugin import TextCommand | ||
|
||
from ...core.git_command import GitCommand | ||
from ..bitbucket import open_file_in_browser # , open_repo, open_issues | ||
from ..bitbucket import open_repo | ||
from ..bitbucket import open_issues | ||
|
||
from .. import git_mixins | ||
from ...core.ui_mixins.quick_panel import show_remote_panel | ||
|
||
|
||
EARLIER_COMMIT_PROMPT = ("The remote chosen may not contain the commit. " | ||
"Open the file {} before?") | ||
|
||
|
||
class GsBitbucketOpenFileOnRemoteCommand(TextCommand, GitCommand, git_mixins.BitbucketRemotesMixin): | ||
|
||
""" | ||
Open a new browser window to the web-version of the currently opened | ||
(or specified) file. If `preselect` is `True`, include the selected | ||
lines in the request. If the active tracked remote is the same as the | ||
integrated remote, open browser directly, if not, display a list to remotes | ||
to choose from. | ||
At present, this only supports bitbucket.org and Bitbucket enterprise. | ||
""" | ||
|
||
def run(self, edit, remote=None, preselect=False, fpath=None): | ||
sublime.set_timeout_async( | ||
lambda: self.run_async(remote, preselect, fpath)) | ||
|
||
def run_async(self, remote, preselect, fpath): | ||
self.fpath = fpath or self.get_rel_path() | ||
self.preselect = preselect | ||
|
||
self.remotes = self.get_remotes() | ||
|
||
if not remote: | ||
remote = self.guess_bitbucket_remote() | ||
|
||
if remote: | ||
self.open_file_on_remote(remote) | ||
else: | ||
show_remote_panel(self.open_file_on_remote) | ||
|
||
def open_file_on_remote(self, remote): | ||
if not remote: | ||
return | ||
|
||
fpath = self.fpath | ||
if isinstance(fpath, str): | ||
fpath = [fpath] | ||
remote_url = self.remotes[remote] | ||
|
||
if self.view.settings().get("git_savvy.show_file_at_commit_view"): | ||
# if it is a show_file_at_commit_view, get the hash from settings | ||
commit_hash = self.view.settings().get("git_savvy.show_file_at_commit_view.commit") | ||
else: | ||
commit_hash = self.get_commit_hash_for_head() | ||
|
||
base_hash = commit_hash | ||
|
||
# check if the remote contains the commit hash | ||
if remote not in self.remotes_containing_commit(commit_hash): | ||
upstream = self.get_upstream_for_active_branch() | ||
if upstream: | ||
merge_base = self.git("merge-base", commit_hash, upstream).strip() | ||
if merge_base and remote in self.remotes_containing_commit(merge_base): | ||
count = self.git( | ||
"rev-list", "--count", "{}..{}".format(merge_base, commit_hash)).strip() | ||
if not sublime.ok_cancel_dialog(EARLIER_COMMIT_PROMPT.format( | ||
count + (" commit" if count == "1" else " commits"))): | ||
return | ||
|
||
commit_hash = merge_base | ||
|
||
start_line = None | ||
end_line = None | ||
|
||
if self.preselect and len(fpath) == 1: | ||
selections = self.view.sel() | ||
if len(selections) >= 1: | ||
first_selection = selections[0] | ||
last_selection = selections[-1] | ||
# Git lines are 1-indexed; Sublime rows are 0-indexed. | ||
start_line = self.view.rowcol(first_selection.begin())[0] + 1 | ||
end_line = self.view.rowcol(last_selection.end())[0] + 1 | ||
|
||
# forward line number if the opening commit is the merge base | ||
if base_hash != commit_hash: | ||
start_line = self.find_matching_lineno( | ||
base_hash, commit_hash, line=start_line, file_path=fpath[0]) | ||
end_line = self.find_matching_lineno( | ||
base_hash, commit_hash, line=end_line, file_path=fpath[0]) | ||
|
||
for p in fpath: | ||
open_file_in_browser( | ||
p, | ||
remote_url, | ||
commit_hash, | ||
start_line=start_line, | ||
end_line=end_line | ||
) | ||
|
||
|
||
class GsBitbucketOpenRepoCommand(TextCommand, GitCommand, git_mixins.BitbucketRemotesMixin): | ||
|
||
""" | ||
Open a new browser window to the Bitbucket remote repository. | ||
""" | ||
|
||
def run(self, edit, remote=None): | ||
sublime.set_timeout_async(lambda: self.run_async(remote)) | ||
|
||
def run_async(self, remote): | ||
self.remotes = self.get_remotes() | ||
|
||
if not remote: | ||
remote = self.guess_bitbucket_remote() | ||
|
||
if remote: | ||
open_repo(self.remotes[remote]) | ||
else: | ||
show_remote_panel(self.on_remote_selection) | ||
|
||
def on_remote_selection(self, remote): | ||
if not remote: | ||
return | ||
open_repo(self.remotes[remote]) | ||
|
||
|
||
class GsBitbucketOpenIssuesCommand(TextCommand, GitCommand, git_mixins.BitbucketRemotesMixin): | ||
|
||
""" | ||
Open a new browser window to the Bitbucket remote repository's issues page. | ||
""" | ||
|
||
def run(self, edit): | ||
open_issues(self.get_integrated_remote_url()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .remotes import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
class BitbucketRemotesMixin(): | ||
|
||
def get_integrated_branch_name(self): | ||
configured_branch_name = self.git( | ||
"config", | ||
"--local", | ||
"--get", | ||
"GitSavvy.bbBranch", | ||
throw_on_stderr=False | ||
).strip() | ||
if configured_branch_name: | ||
return configured_branch_name | ||
else: | ||
return "master" | ||
|
||
def get_integrated_remote_name(self): | ||
configured_remote_name = self.git( | ||
"config", | ||
"--local", | ||
"--get", | ||
"GitSavvy.bbRemote", | ||
throw_on_stderr=False | ||
).strip() | ||
remotes = self.get_remotes() | ||
|
||
if len(remotes) == 0: | ||
raise ValueError("Bitbucket integration will not function when no remotes defined.") | ||
|
||
if configured_remote_name and configured_remote_name in remotes: | ||
return configured_remote_name | ||
elif len(remotes) == 1: | ||
return list(remotes.keys())[0] | ||
elif "origin" in remotes: | ||
return "origin" | ||
elif self.get_upstream_for_active_branch(): | ||
# fall back to the current active remote | ||
return self.get_upstream_for_active_branch().split("/")[0] | ||
else: | ||
raise ValueError("Cannot determine Bitbucket integrated remote.") | ||
|
||
def get_integrated_remote_url(self): | ||
configured_remote_name = self.get_integrated_remote_name() | ||
remotes = self.get_remotes() | ||
return remotes[configured_remote_name] | ||
|
||
def guess_bitbucket_remote(self): | ||
upstream = self.get_upstream_for_active_branch() | ||
integrated_remote = self.get_integrated_remote_name() | ||
remotes = self.get_remotes() | ||
|
||
if len(self.remotes) == 1: | ||
return list(remotes.keys())[0] | ||
elif upstream: | ||
tracked_remote = upstream.split("/")[0] if upstream else None | ||
|
||
if tracked_remote and tracked_remote == integrated_remote: | ||
return tracked_remote | ||
else: | ||
return None | ||
else: | ||
return integrated_remote |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.