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);
+ }
+ }
+}