├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── encodings.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml └── vcs.xml ├── IDEA-GitLab-Integration.iml ├── LICENSE ├── META-INF └── plugin.xml ├── README.md ├── resources └── ru │ └── trylogic │ └── idea │ └── gitlab │ └── gitlab_icon.png └── src ├── icons └── GitlabIcons.java └── ru └── trylogic └── idea └── gitlab └── integration ├── actions └── GitLabOpenInBrowserAction.java └── utils └── GitlabUrlUtil.java /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /out 3 | IDEA-GitLab-Integration.jar 4 | 5 | .idea/workspace.xml 6 | .idea/rebel_project.xml -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | IDEA-GitLab-Integration -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /IDEA-GitLab-Integration.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Sergey Egorov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | ru.trylogic.idea.gitlab.integration 3 | GitLab integration 4 | 1.0.4 5 | Sergei BSiDeUp Egorov 6 | 7 | GitLab integration plugin. 9 | Support "Open file in browser" command. 10 | ]]> 11 | 12 | 15 | 16 | 17 | 18 | 19 | com.intellij.modules.vcs 20 | com.intellij.modules.lang 21 | Git4Idea 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | IDEA-GitLab-Integration 2 | ======================= 3 | 4 | This plugin adds an `Open on GitLab` entry in the right-click menu of files. 5 | -------------------------------------------------------------------------------- /resources/ru/trylogic/idea/gitlab/gitlab_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsideup/IDEA-GitLab-Integration/db5cae0d76cd70fae172bb9533bc0b6ec35c1672/resources/ru/trylogic/idea/gitlab/gitlab_icon.png -------------------------------------------------------------------------------- /src/icons/GitlabIcons.java: -------------------------------------------------------------------------------- 1 | package icons; 2 | 3 | import com.intellij.openapi.util.IconLoader; 4 | 5 | import javax.swing.*; 6 | 7 | /** 8 | * NOTE THIS FILE IS AUTO-GENERATED by the build/scripts/icons.gant 9 | * Don't repeat mistakes of others ;-) 10 | */ 11 | public class GitlabIcons { 12 | private static Icon load(String path) { 13 | return IconLoader.getIcon(path, GitlabIcons.class); 14 | } 15 | 16 | public static final Icon Gitlab_icon = load("/ru/trylogic/idea/gitlab/gitlab_icon.png"); // 16x16 17 | } -------------------------------------------------------------------------------- /src/ru/trylogic/idea/gitlab/integration/actions/GitLabOpenInBrowserAction.java: -------------------------------------------------------------------------------- 1 | package ru.trylogic.idea.gitlab.integration.actions; 2 | 3 | import com.intellij.ide.BrowserUtil; 4 | import com.intellij.openapi.actionSystem.*; 5 | import com.intellij.openapi.diagnostic.Logger; 6 | import com.intellij.openapi.editor.Editor; 7 | import com.intellij.openapi.editor.SelectionModel; 8 | import com.intellij.openapi.project.DumbAwareAction; 9 | import com.intellij.openapi.project.Project; 10 | import com.intellij.openapi.ui.popup.JBPopupFactory; 11 | import com.intellij.openapi.ui.popup.ListPopup; 12 | import com.intellij.openapi.vcs.VcsNotifier; 13 | import com.intellij.openapi.vcs.changes.Change; 14 | import com.intellij.openapi.vcs.changes.ChangeListManager; 15 | import com.intellij.openapi.vfs.VirtualFile; 16 | import git4idea.GitLocalBranch; 17 | import git4idea.GitRemoteBranch; 18 | import git4idea.GitUtil; 19 | import git4idea.repo.GitRemote; 20 | import git4idea.repo.GitRepository; 21 | import git4idea.repo.GitRepositoryManager; 22 | import icons.GitlabIcons; 23 | import org.jetbrains.annotations.NotNull; 24 | import org.jetbrains.annotations.Nullable; 25 | import ru.trylogic.idea.gitlab.integration.utils.GitlabUrlUtil; 26 | 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | public class GitLabOpenInBrowserAction extends DumbAwareAction { 31 | 32 | public static final Logger LOG = Logger.getInstance("gitlab"); 33 | 34 | public static final String CANNOT_OPEN_IN_BROWSER = "Cannot open in browser"; 35 | 36 | protected GitLabOpenInBrowserAction() { 37 | super("Open on GitLab", "Open corresponding link in browser", GitlabIcons.Gitlab_icon); 38 | } 39 | 40 | static void setVisibleEnabled(AnActionEvent e, boolean visible, boolean enabled) { 41 | e.getPresentation().setVisible(visible); 42 | e.getPresentation().setEnabled(enabled); 43 | } 44 | 45 | static void showError(Project project, String title, String message) { 46 | showError(project, title, message, null); 47 | } 48 | 49 | static void showError(Project project, String title, String message, String debugInfo) { 50 | LOG.warn(title + "; " + message); 51 | if (debugInfo != null) { 52 | LOG.warn(debugInfo); 53 | } 54 | VcsNotifier.getInstance(project).notifyError(title, message); 55 | } 56 | 57 | @Nullable 58 | static String makeUrlToOpen(@Nullable Editor editor, 59 | @NotNull String relativePath, 60 | @NotNull String branch, 61 | @NotNull String remoteUrl) { 62 | final StringBuilder builder = new StringBuilder(); 63 | final String repoUrl = GitlabUrlUtil.makeRepoUrlFromRemoteUrl(remoteUrl); 64 | if (repoUrl == null) { 65 | return null; 66 | } 67 | builder.append(repoUrl).append("/blob/").append(branch).append(relativePath); 68 | 69 | if (editor != null && editor.getDocument().getLineCount() >= 1) { 70 | // lines are counted internally from 0, but from 1 on gitlab 71 | SelectionModel selectionModel = editor.getSelectionModel(); 72 | final int begin = editor.getDocument().getLineNumber(selectionModel.getSelectionStart()) + 1; 73 | final int selectionEnd = selectionModel.getSelectionEnd(); 74 | int end = editor.getDocument().getLineNumber(selectionEnd) + 1; 75 | if (editor.getDocument().getLineStartOffset(end - 1) == selectionEnd) { 76 | end -= 1; 77 | } 78 | builder.append("#L").append(begin).append('-').append(end); 79 | } 80 | 81 | return builder.toString(); 82 | } 83 | 84 | @Nullable 85 | public static String getBranchNameOnRemote(@NotNull Project project, @NotNull GitRepository repository) { 86 | GitLocalBranch currentBranch = repository.getCurrentBranch(); 87 | if (currentBranch == null) { 88 | showError(project, CANNOT_OPEN_IN_BROWSER, 89 | "Can't open the file on GitLab when repository is on detached HEAD. Please checkout a branch."); 90 | return null; 91 | } 92 | 93 | GitRemoteBranch tracked = currentBranch.findTrackedBranch(repository); 94 | if (tracked == null) { 95 | showError(project, CANNOT_OPEN_IN_BROWSER, "Can't open the file on GitLab when current branch doesn't have a tracked branch.", 96 | "Current branch: " + currentBranch + ", tracked info: " + repository.getBranchTrackInfos()); 97 | return null; 98 | } 99 | 100 | return tracked.getNameForRemoteOperations(); 101 | } 102 | 103 | @Override 104 | public void update(final AnActionEvent e) { 105 | Project project = e.getData(PlatformDataKeys.PROJECT); 106 | VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE); 107 | if (project == null || project.isDefault() || virtualFile == null) { 108 | setVisibleEnabled(e, false, false); 109 | return; 110 | } 111 | 112 | final GitRepository gitRepository = GitUtil.getRepositoryManager(project).getRepositoryForFile(virtualFile); 113 | if (gitRepository == null) { 114 | setVisibleEnabled(e, false, false); 115 | return; 116 | } 117 | 118 | ChangeListManager changeListManager = ChangeListManager.getInstance(project); 119 | if (changeListManager.isUnversioned(virtualFile)) { 120 | setVisibleEnabled(e, true, false); 121 | return; 122 | } 123 | 124 | Change change = changeListManager.getChange(virtualFile); 125 | if (change != null && change.getType() == Change.Type.NEW) { 126 | setVisibleEnabled(e, true, false); 127 | return; 128 | } 129 | 130 | setVisibleEnabled(e, true, true); 131 | } 132 | 133 | @Override 134 | public void actionPerformed(final AnActionEvent e) { 135 | final Project project = e.getData(PlatformDataKeys.PROJECT); 136 | final VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE); 137 | final Editor editor = e.getData(PlatformDataKeys.EDITOR); 138 | if (virtualFile == null || project == null || project.isDisposed()) { 139 | return; 140 | } 141 | 142 | GitRepositoryManager manager = GitUtil.getRepositoryManager(project); 143 | final GitRepository repository = manager.getRepositoryForFile(virtualFile); 144 | if (repository == null) { 145 | StringBuilder details = new StringBuilder("file: " + virtualFile.getPresentableUrl() + "; Git repositories: "); 146 | for (GitRepository repo : manager.getRepositories()) { 147 | details.append(repo.getPresentableUrl()).append("; "); 148 | } 149 | showError(project, CANNOT_OPEN_IN_BROWSER, "Can't find git repository", details.toString()); 150 | return; 151 | } 152 | 153 | final String rootPath = repository.getRoot().getPath(); 154 | final String path = virtualFile.getPath(); 155 | 156 | List remoteSelectedActions = new ArrayList(); 157 | 158 | for (GitRemote remote : repository.getRemotes()) { 159 | remoteSelectedActions.add(new RemoteSelectedAction(project, repository, editor, remote, rootPath, path)); 160 | } 161 | 162 | if (remoteSelectedActions.size() > 1) { 163 | DefaultActionGroup remotesActionGroup = new DefaultActionGroup(); 164 | remotesActionGroup.addAll(remoteSelectedActions); 165 | DataContext dataContext = e.getDataContext(); 166 | final ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup( 167 | "Select remote", 168 | remotesActionGroup, 169 | dataContext, 170 | JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, 171 | true); 172 | 173 | popup.showInBestPositionFor(dataContext); 174 | } else if (remoteSelectedActions.size() == 1) { 175 | remoteSelectedActions.get(0).actionPerformed(null); 176 | } else { 177 | showError(project, CANNOT_OPEN_IN_BROWSER, "Can't find gitlab remote"); 178 | } 179 | } 180 | 181 | 182 | } 183 | 184 | class RemoteSelectedAction extends AnAction { 185 | 186 | private final Editor editor; 187 | 188 | private final GitRemote remote; 189 | 190 | private final String rootPath; 191 | 192 | private final String path; 193 | 194 | private final Project project; 195 | 196 | private final GitRepository repository; 197 | 198 | public RemoteSelectedAction(@NotNull Project project, @NotNull GitRepository repository, @Nullable Editor editor, 199 | @NotNull GitRemote remote, @NotNull String rootPath, @NotNull String path) { 200 | super(remote.getName()); 201 | this.project = project; 202 | this.repository = repository; 203 | this.editor = editor; 204 | this.remote = remote; 205 | this.rootPath = rootPath; 206 | this.path = path; 207 | } 208 | 209 | @Override 210 | public void update(AnActionEvent e) { 211 | super.update(e); 212 | setEnabledInModalContext(true); 213 | e.getPresentation().setEnabled(true); 214 | } 215 | 216 | @Override 217 | public void actionPerformed(@Nullable AnActionEvent unused) { 218 | if (!path.startsWith(rootPath)) { 219 | GitLabOpenInBrowserAction.showError(project, GitLabOpenInBrowserAction.CANNOT_OPEN_IN_BROWSER, 220 | "File is not under repository root", "Root: " + rootPath + ", file: " + path); 221 | return; 222 | } 223 | 224 | String branch = GitLabOpenInBrowserAction.getBranchNameOnRemote(project, repository); 225 | if (branch == null) { 226 | return; 227 | } 228 | 229 | String remoteUrl = remote.getFirstUrl(); 230 | 231 | if (remoteUrl == null) { 232 | GitLabOpenInBrowserAction.showError(project, GitLabOpenInBrowserAction.CANNOT_OPEN_IN_BROWSER, 233 | "Can't obtain url for remote", remote.getName()); 234 | return; 235 | } 236 | 237 | String relativePath = path.substring(rootPath.length()); 238 | String urlToOpen = GitLabOpenInBrowserAction.makeUrlToOpen(editor, relativePath, branch, remoteUrl); 239 | if (urlToOpen == null) { 240 | GitLabOpenInBrowserAction.showError(project, GitLabOpenInBrowserAction.CANNOT_OPEN_IN_BROWSER, 241 | "Can't create properly url", remote.getFirstUrl()); 242 | return; 243 | } 244 | 245 | BrowserUtil.launchBrowser(urlToOpen); 246 | } 247 | } -------------------------------------------------------------------------------- /src/ru/trylogic/idea/gitlab/integration/utils/GitlabUrlUtil.java: -------------------------------------------------------------------------------- 1 | package ru.trylogic.idea.gitlab.integration.utils; 2 | 3 | import com.intellij.openapi.ui.Messages; 4 | import com.intellij.openapi.util.text.StringUtil; 5 | import git4idea.repo.GitRemote; 6 | import git4idea.repo.GitRepository; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class GitlabUrlUtil { 14 | 15 | @Nullable 16 | public static String findRemoteUrl(@NotNull GitRepository repository) { 17 | List remotes = new ArrayList(); 18 | for (GitRemote remote : repository.getRemotes()) { 19 | if (remote.getName().equals("origin")) { 20 | return remote.getFirstUrl(); 21 | } 22 | } 23 | return null; 24 | } 25 | 26 | public static String makeRepoUrlFromRemoteUrl(@NotNull String remoteUrl) { 27 | String cleanedFromDotGit = StringUtil.trimEnd(remoteUrl, ".git"); 28 | 29 | if (remoteUrl.startsWith("http://") || remoteUrl.startsWith("https://")) { 30 | return cleanedFromDotGit; 31 | } else if (remoteUrl.startsWith("git@")) { 32 | String cleanedFromGitAt = StringUtil.trimStart(cleanedFromDotGit, "git@"); 33 | 34 | return "http://" + StringUtil.replace(cleanedFromGitAt, ":", "/"); 35 | } else { 36 | throw new IllegalStateException("Invalid remote Gitlab url: " + remoteUrl); 37 | } 38 | } 39 | } 40 | --------------------------------------------------------------------------------