diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce6dfb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +.DS_Store +/out +IDEA-GitLab-Integration.jar diff --git a/IDEA-GitLab-Integration.iml b/IDEA-GitLab-Integration.iml new file mode 100644 index 0000000..63565d1 --- /dev/null +++ b/IDEA-GitLab-Integration.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/META-INF/plugin.xml b/META-INF/plugin.xml new file mode 100644 index 0000000..663d1ef --- /dev/null +++ b/META-INF/plugin.xml @@ -0,0 +1,44 @@ + + ru.trylogic.idea.gitlab.integration + GitLab integration + 1.0 + Sergei Egorov + + GitLab integration plugin. + Support "Open file in browser" command. + ]]> + + + + + + + com.intellij.modules.vcs + com.intellij.modules.lang + Git4Idea + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/ru/trylogic/idea/gitlab/gitlab_icon.png b/resources/ru/trylogic/idea/gitlab/gitlab_icon.png new file mode 100644 index 0000000..ac27cbe Binary files /dev/null and b/resources/ru/trylogic/idea/gitlab/gitlab_icon.png differ diff --git a/src/icons/GitlabIcons.java b/src/icons/GitlabIcons.java new file mode 100644 index 0000000..a5ed20d --- /dev/null +++ b/src/icons/GitlabIcons.java @@ -0,0 +1,17 @@ +package icons; + +import com.intellij.openapi.util.IconLoader; + +import javax.swing.*; + +/** + * NOTE THIS FILE IS AUTO-GENERATED by the build/scripts/icons.gant + * Don't repeat mistakes of others ;-) + */ +public class GitlabIcons { + private static Icon load(String path) { + return IconLoader.getIcon(path, GitlabIcons.class); + } + + public static final Icon Gitlab_icon = load("/ru/trylogic/idea/gitlab/gitlab_icon.png"); // 16x16 +} \ No newline at end of file diff --git a/src/ru/trylogic/idea/gitlab/integration/actions/AbstractGitLabShowCommitInBrowserAction.java b/src/ru/trylogic/idea/gitlab/integration/actions/AbstractGitLabShowCommitInBrowserAction.java new file mode 100644 index 0000000..5d26c22 --- /dev/null +++ b/src/ru/trylogic/idea/gitlab/integration/actions/AbstractGitLabShowCommitInBrowserAction.java @@ -0,0 +1,25 @@ +package ru.trylogic.idea.gitlab.integration.actions; + +import com.intellij.ide.BrowserUtil; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import git4idea.repo.GitRepository; +import icons.GitlabIcons; +import ru.trylogic.idea.gitlab.integration.utils.GitlabUrlUtil; + +abstract class AbstractGitLabShowCommitInBrowserAction extends DumbAwareAction { + + public AbstractGitLabShowCommitInBrowserAction() { + super("Open on GitLab", "Open the selected commit in browser", GitlabIcons.Gitlab_icon); + } + + protected static void openInBrowser(GitRepository repository, String revisionHash) { + + String remote = GitlabUrlUtil.findRemoteUrl(repository); + final String repoUrl = GitlabUrlUtil.makeRepoUrlFromRemoteUrl(remote); + + String url = repoUrl + "/commit/" + revisionHash; + BrowserUtil.launchBrowser(url); + } + +} \ No newline at end of file diff --git a/src/ru/trylogic/idea/gitlab/integration/actions/GitLabOpenInBrowserAction.java b/src/ru/trylogic/idea/gitlab/integration/actions/GitLabOpenInBrowserAction.java new file mode 100644 index 0000000..d377d5a --- /dev/null +++ b/src/ru/trylogic/idea/gitlab/integration/actions/GitLabOpenInBrowserAction.java @@ -0,0 +1,183 @@ +package ru.trylogic.idea.gitlab.integration.actions; + +import com.intellij.ide.BrowserUtil; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.SelectionModel; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ChangeListManager; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import git4idea.GitLocalBranch; +import git4idea.GitRemoteBranch; +import git4idea.GitUtil; +import git4idea.repo.GitRepository; +import git4idea.repo.GitRepositoryManager; +import ru.trylogic.idea.gitlab.integration.utils.GitlabUrlUtil; +import icons.GitlabIcons; + +public class GitLabOpenInBrowserAction extends DumbAwareAction { + + public static final String CANNOT_OPEN_IN_BROWSER = "Cannot open in browser"; + + static void setVisibleEnabled(AnActionEvent e, boolean visible, boolean enabled) { + e.getPresentation().setVisible(visible); + e.getPresentation().setEnabled(enabled); + } + + protected GitLabOpenInBrowserAction() { + super("Open on GitLab", "Open corresponding link in browser", GitlabIcons.Gitlab_icon); + } + + @Override + public void update(final AnActionEvent e) { + Project project = e.getData(PlatformDataKeys.PROJECT); + VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE); + if (project == null || project.isDefault() || virtualFile == null) { + setVisibleEnabled(e, false, false); + return; + } + GitRepositoryManager manager = GitUtil.getRepositoryManager(project); + + final GitRepository gitRepository = manager.getRepositoryForFile(virtualFile); + if (gitRepository == null) { + setVisibleEnabled(e, false, false); + return; + } + + ChangeListManager changeListManager = ChangeListManager.getInstance(project); + if (changeListManager.isUnversioned(virtualFile)) { + setVisibleEnabled(e, true, false); + return; + } + + Change change = changeListManager.getChange(virtualFile); + if (change != null && change.getType() == Change.Type.NEW) { + setVisibleEnabled(e, true, false); + return; + } + + setVisibleEnabled(e, true, true); + } + + @Override + public void actionPerformed(final AnActionEvent e) { + final Project project = e.getData(PlatformDataKeys.PROJECT); + final VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE); + final Editor editor = e.getData(PlatformDataKeys.EDITOR); + if (virtualFile == null || project == null || project.isDisposed()) { + return; + } + + String urlToOpen = getUrl(project, virtualFile, editor); + if (urlToOpen != null) { + BrowserUtil.launchBrowser(urlToOpen); + } + } + + @Nullable + public static String getUrl(@NotNull Project project, @NotNull VirtualFile virtualFile, @Nullable Editor editor) { + + GitRepositoryManager manager = GitUtil.getRepositoryManager(project); + final GitRepository repository = manager.getRepositoryForFile(virtualFile); + if (repository == null) { + StringBuilder details = new StringBuilder("file: " + virtualFile.getPresentableUrl() + "; Git repositories: "); + for (GitRepository repo : manager.getRepositories()) { + details.append(repo.getPresentableUrl()).append("; "); + } + showError(project, CANNOT_OPEN_IN_BROWSER, "Can't find git repository", details.toString()); + return null; + } + + final String remoteUrl = GitlabUrlUtil.findRemoteUrl(repository); + if (remoteUrl == null) { + showError(project, CANNOT_OPEN_IN_BROWSER, "Can't find gitlab remote"); + return null; + } + + final String rootPath = repository.getRoot().getPath(); + final String path = virtualFile.getPath(); + if (!path.startsWith(rootPath)) { + showError(project, CANNOT_OPEN_IN_BROWSER, "File is not under repository root", "Root: " + rootPath + ", file: " + path); + return null; + } + + String branch = getBranchNameOnRemote(project, repository); + if (branch == null) { + return null; + } + + String relativePath = path.substring(rootPath.length()); + String urlToOpen = makeUrlToOpen(editor, relativePath, branch, remoteUrl); + if (urlToOpen == null) { + showError(project, CANNOT_OPEN_IN_BROWSER, "Can't create properly url", remoteUrl); + return null; + } + + return urlToOpen; + } + + private static void showError(Project project, String cannotOpenInBrowser) { + showError(project, cannotOpenInBrowser, null); + } + + private static void showError(Project project, String cannotOpenInBrowser, String s) { + showError(project, cannotOpenInBrowser, s, null); + } + + private static void showError(Project project, String cannotOpenInBrowser, String s, String s1) { + System.out.println(cannotOpenInBrowser + ";" + s + ";" + s1); + } + + @Nullable + private static String makeUrlToOpen(@Nullable Editor editor, + @NotNull String relativePath, + @NotNull String branch, + @NotNull String remoteUrl) { + final StringBuilder builder = new StringBuilder(); + final String repoUrl = GitlabUrlUtil.makeRepoUrlFromRemoteUrl(remoteUrl); + if (repoUrl == null) { + return null; + } + builder.append(repoUrl).append("/blob/").append(branch).append(relativePath); + + if (editor != null && editor.getDocument().getLineCount() >= 1) { + // lines are counted internally from 0, but from 1 on gitlab + SelectionModel selectionModel = editor.getSelectionModel(); + final int begin = editor.getDocument().getLineNumber(selectionModel.getSelectionStart()) + 1; + final int selectionEnd = selectionModel.getSelectionEnd(); + int end = editor.getDocument().getLineNumber(selectionEnd) + 1; + if (editor.getDocument().getLineStartOffset(end - 1) == selectionEnd) { + end -= 1; + } + builder.append("#L").append(begin).append('-').append(end); + } + + return builder.toString(); + } + + @Nullable + public static String getBranchNameOnRemote(@NotNull Project project, @NotNull GitRepository repository) { + GitLocalBranch currentBranch = repository.getCurrentBranch(); + if (currentBranch == null) { + showError(project, CANNOT_OPEN_IN_BROWSER, + "Can't open the file on GitLab when repository is on detached HEAD. Please checkout a branch."); + return null; + } + + GitRemoteBranch tracked = currentBranch.findTrackedBranch(repository); + if (tracked == null) { + showError(project, CANNOT_OPEN_IN_BROWSER, "Can't open the file on GitLab when current branch doesn't have a tracked branch.", + "Current branch: " + currentBranch + ", tracked info: " + repository.getBranchTrackInfos()); + return null; + } + + return tracked.getNameForRemoteOperations(); + } + + +} diff --git a/src/ru/trylogic/idea/gitlab/integration/actions/GitLabShowCommitInBrowserFromLogAction.java b/src/ru/trylogic/idea/gitlab/integration/actions/GitLabShowCommitInBrowserFromLogAction.java new file mode 100644 index 0000000..c9bc817 --- /dev/null +++ b/src/ru/trylogic/idea/gitlab/integration/actions/GitLabShowCommitInBrowserFromLogAction.java @@ -0,0 +1,75 @@ +package ru.trylogic.idea.gitlab.integration.actions; + + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import git4idea.GitUtil; +import git4idea.GitVcs; +import git4idea.history.browser.GitCommit; +import git4idea.repo.GitRepository; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class GitLabShowCommitInBrowserFromLogAction extends AbstractGitLabShowCommitInBrowserAction { + + @Nullable + private static EventData collectData(AnActionEvent e) { + Project project = e.getData(PlatformDataKeys.PROJECT); + if (project == null || project.isDefault()) { + return null; + } + + GitCommit commit = e.getData(GitVcs.GIT_COMMIT); + if (commit == null) { + return null; + } + + VirtualFile root = commit.getRoot(); + GitRepository repository = GitUtil.getRepositoryManager(project).getRepositoryForRoot(root); + if (repository == null) { + return null; + } + + return new EventData(repository, commit); + } + + @Override + public void update(AnActionEvent e) { + EventData eventData = collectData(e); + e.getPresentation().setVisible(eventData != null); + e.getPresentation().setEnabled(eventData != null); + } + + @Override + public void actionPerformed(AnActionEvent e) { + EventData eventData = collectData(e); + if (eventData != null) { + openInBrowser(eventData.getRepository(), eventData.getCommit().getHash().getValue()); + } + } + + private static class EventData { + @NotNull + private final GitRepository myRepository; + @NotNull + private final GitCommit myCommit; + + private EventData(@NotNull GitRepository repository, @NotNull GitCommit commit) { + myRepository = repository; + myCommit = commit; + } + + @NotNull + public GitRepository getRepository() { + return myRepository; + } + + @NotNull + public GitCommit getCommit() { + return myCommit; + } + } + +} \ No newline at end of file diff --git a/src/ru/trylogic/idea/gitlab/integration/utils/GitlabUrlUtil.java b/src/ru/trylogic/idea/gitlab/integration/utils/GitlabUrlUtil.java new file mode 100644 index 0000000..4fb9639 --- /dev/null +++ b/src/ru/trylogic/idea/gitlab/integration/utils/GitlabUrlUtil.java @@ -0,0 +1,34 @@ +package ru.trylogic.idea.gitlab.integration.utils; + +import com.intellij.openapi.util.text.StringUtil; +import git4idea.repo.GitRemote; +import git4idea.repo.GitRepository; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class GitlabUrlUtil { + + @Nullable + public static String findRemoteUrl(@NotNull GitRepository repository) { + for (GitRemote remote : repository.getRemotes()) { + if (remote.getName().equals("origin")) { + return remote.getFirstUrl(); + } + } + return null; + } + + public static String makeRepoUrlFromRemoteUrl(@NotNull String remoteUrl) { + String cleanedFromDotGit = StringUtil.trimEnd(remoteUrl, ".git"); + + if (remoteUrl.startsWith("http://")) { + return cleanedFromDotGit; + } else if (remoteUrl.startsWith("git@")) { + String cleanedFromGitAt = StringUtil.trimStart(cleanedFromDotGit, "git@"); + + return "http://" + StringUtil.replace(cleanedFromGitAt, ":", "/"); + } else { + throw new IllegalStateException("Invalid remote Gitlab url: " + remoteUrl); + } + } +}