Skip to content

Commit

Permalink
🔥 Pick the closest reasonable commit if the current one was not pushed
Browse files Browse the repository at this point in the history
After this change there are now 3 actions:

- generating a link to the currently checked out commit
- generating a link to the latest commit from the remote's default branch
- generating a link to some parent commit that is definitely available at the remote

...with the last one becoming the default assigned to `Cmd`+`Shift`+`L`.

The main use cases are generating a link to whatever I see in my IDE (so,
close commits tend to also be acceptable since the code hasn't changed
much) or showing something that is currently in the latest master. Also, I
expect there to be not that many users relying on the plugin generating
a link to exactly the current commit in scenarios when it has not been
pushed, thought I don't really have any statistics. I assume no one has
tried automating anything by calling the action or the default shortcut.
  • Loading branch information
lunakoly committed Sep 28, 2024
1 parent 222217e commit af484bc
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 62 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

### Changed
- Now the plugin allows generating links to modified files, even though the lines may be inaccurate
- If the current commit is not reachable from remote branches, the plugin will look for the closest parent commit that is

### Added
- Two new actions: "Copy Current Commit Line Link" (the old behavior) and "Copy Latest Default Line Link"

## [1.0.10] - 2024-06-29

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginGroup = com.github.lunakoly.quicklink
pluginName = Quick Link
pluginRepositoryUrl = https://github.com/lunakoly/QuickLink
# SemVer format -> https://semver.org
pluginVersion = 1.0.10
pluginVersion = 1.0.11

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 211
Expand Down
117 changes: 71 additions & 46 deletions src/main/kotlin/org/lunakoly/quicklink/actions/CopyLineLinkAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,49 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.changes.ChangeListManager
import com.intellij.openapi.vfs.VfsUtilCore
import org.lunakoly.quicklink.repository.getRepositoryInfo
import org.lunakoly.quicklink.ui.warn
import git4idea.repo.GitRepository
import org.lunakoly.quicklink.repository.*
import org.lunakoly.quicklink.ui.showClickableListIfNeeded
import org.lunakoly.quicklink.ui.toast
import org.lunakoly.quicklink.ui.warn
import org.lunakoly.quicklink.urlbuilder.LineOffset
import org.lunakoly.quicklink.urlbuilder.Selection
import org.lunakoly.quicklink.urlbuilder.UrlBuilders
import org.lunakoly.quicklink.utils.PopupException
import org.lunakoly.quicklink.utils.catchingPopupExceptions
import org.lunakoly.quicklink.utils.runInBackground
import org.lunakoly.quicklink.utils.toDomain
import java.awt.datatransfer.StringSelection

class CopyLineLinkAction : DumbAwareAction() {
override fun actionPerformed(event: AnActionEvent) {
catchingPopupExceptions {
generateLineLink(event, ::getReasonablePublishedRepositoryInfo)
}
}
}

class CopyCurrentCommitLineLinkAction : DumbAwareAction() {
override fun actionPerformed(event: AnActionEvent) {
catchingPopupExceptions {
generateLineLink(event) { repo, _, _ ->
getCurrentCommitRepositoryInfo(repo)
}
}
}
}

class CopyLatestDefaultLineLinkAction : DumbAwareAction() {
override fun actionPerformed(event: AnActionEvent) {
catchingPopupExceptions {
generateLineLink(event, ::getLatestDefaultRepositoryInfo)
}
}
}

class NoProjectException : PopupException(
"Please, open a project",
"No Open Project",
Expand All @@ -38,49 +67,51 @@ class RelativePathException : PopupException(
"No Relative Path",
)

class CopyLineLinkAction : DumbAwareAction() {
@Suppress("ThrowsCount")
private fun generateLineLink(event: AnActionEvent) {
val project = event.project
?: throw NoProjectException()
val editor = event.getData(CommonDataKeys.EDITOR)
?: throw NoEditorException()
val currentFile = event.dataContext.getData(CommonDataKeys.VIRTUAL_FILE)?.canonicalFile
?: throw NoActiveFileException()

val selection = if (editor.selectionModel.hasSelection()) {
val startLine = 1 + editor.document.getLineNumber(editor.selectionModel.selectionStart)
val endLine = 1 + editor.document.getLineNumber(editor.selectionModel.selectionEnd)
val startColumn = 1 + editor.selectionModel.selectionStartPosition!!.getColumn()
val endColumn = 1 + editor.selectionModel.selectionEndPosition!!.getColumn()
val startOffset = LineOffset(startLine, startColumn)
val endOffset = LineOffset(endLine, endColumn)
Selection.MultilineSelection(startOffset, endOffset)
} else {
val lineOffset = LineOffset(1 + editor.document.getLineNumber(editor.caretModel.offset), 0)
Selection.SingleLinkSelection(lineOffset)
}
private inline fun generateLineLink(
event: AnActionEvent,
crossinline getInfo: (GitRepository, Project, String) -> RepositoryInfo,
) {
val project = event.project
?: throw NoProjectException()
val editor = event.getData(CommonDataKeys.EDITOR)
?: throw NoEditorException()
val currentFile = event.dataContext.getData(CommonDataKeys.VIRTUAL_FILE)?.canonicalFile
?: throw NoActiveFileException()

val currentFileIsModified = ChangeListManager
.getInstance(project)
.getChange(currentFile) != null

if (currentFileIsModified) {
project.warn("This is a locally modified file, the line number may be inaccurate")
}

val repositoryInfo = getRepositoryInfo(project, currentFile)
val selection = if (editor.selectionModel.hasSelection()) {
val startLine = 1 + editor.document.getLineNumber(editor.selectionModel.selectionStart)
val endLine = 1 + editor.document.getLineNumber(editor.selectionModel.selectionEnd)
val startColumn = 1 + editor.selectionModel.selectionStartPosition!!.getColumn()
val endColumn = 1 + editor.selectionModel.selectionEndPosition!!.getColumn()
val startOffset = LineOffset(startLine, startColumn)
val endOffset = LineOffset(endLine, endColumn)
Selection.MultilineSelection(startOffset, endOffset)
} else {
val lineOffset = LineOffset(1 + editor.document.getLineNumber(editor.caretModel.offset), 0)
Selection.SingleLinkSelection(lineOffset)
}

val filePath = VfsUtilCore
.findRelativePath(repositoryInfo.root, currentFile, VfsUtilCore.VFS_SEPARATOR_CHAR)
?: throw RelativePathException()
val repository = getRepositoryFor(currentFile, project)
val remotesMap = repository.getRemotesMap()

val currentFileIsModified = ChangeListManager
.getInstance(project)
.getChange(currentFile) != null
editor.showClickableListIfNeeded(things = remotesMap.keys.toList(), title = "Select a Remote") { remote ->
project.runInBackground("Generating the line link") {
val repositoryInfo = getInfo(repository, project, remote)

if (currentFileIsModified) {
project.warn("This is a locally modified file, the line number may be inaccurate")
}
val filePath = VfsUtilCore
.findRelativePath(repositoryInfo.root, currentFile, VfsUtilCore.VFS_SEPARATOR_CHAR)
?: throw RelativePathException()

editor.showClickableListIfNeeded(
repositoryInfo.remotesNames,
title = "Select a Remote",
) {
val remoteLink = repositoryInfo.remotes[it]
?: return@showClickableListIfNeeded
val remoteLink = remotesMap[remote]
?: return@runInBackground

val urlBuilder = UrlBuilders.fromDomain(remoteLink.toDomain())
val url = urlBuilder.buildUrl(remoteLink, repositoryInfo, filePath, selection)
Expand All @@ -89,10 +120,4 @@ class CopyLineLinkAction : DumbAwareAction() {
project.toast("Line link copied: $url")
}
}

override fun actionPerformed(event: AnActionEvent) {
catchingPopupExceptions {
generateLineLink(event)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,4 @@ class RepositoryInfo(
@Suppress("unused")
val branch: String?,
val commitHash: String,
val remotes: Map<String, String>,
) {
val remotesNames get() = remotes.keys.toList()
}
)
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package org.lunakoly.quicklink.repository

import org.lunakoly.quicklink.utils.PopupException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import git4idea.config.GitExecutableManager
import git4idea.history.GitHistoryUtils
import git4idea.repo.GitRepository
import git4idea.repo.GitRepositoryManager
import org.lunakoly.quicklink.ui.warn
import org.lunakoly.quicklink.utils.PopupException

class InvalidRemotesException : PopupException(
"No valid remotes found",
Expand All @@ -21,10 +26,45 @@ class GitRepositoryNeededException : PopupException(
"No Git Repository Found",
)

fun getRepositoryInfoAsGit(repo: GitRepository): RepositoryInfo {
fun getCurrentCommitRepositoryInfo(repo: GitRepository): RepositoryInfo =
getRepositoryInfo(repo) { repo.currentRevision }

fun getLatestDefaultRepositoryInfo(repo: GitRepository, project: Project, remote: String): RepositoryInfo =
getRepositoryInfo(repo) {
val defaultBranch = detectDefaultBranchFor(remote, repo, project)
GitHistoryUtils.collectTimedCommits(project, repo.root, "$remote/$defaultBranch").firstOrNull()?.id?.asString()
}

fun getReasonablePublishedRepositoryInfo(repo: GitRepository, project: Project, remote: String): RepositoryInfo =
getRepositoryInfo(repo) {
val remoteBranch = repo.currentBranch?.findTrackedBranch(repo)?.takeIf { it.remote.name == remote }
val commitHash = repo.currentRevision ?: throw NoRevisionException()

val publishedCommit = when {
remoteBranch != null -> GitHistoryUtils.getMergeBase(project, repo.root, remoteBranch.name, commitHash)?.rev
else -> findClosestDefaultBranchCommitTo(commitHash, repo, project, remote)
}

if (publishedCommit != commitHash) {
val commits = GitHistoryUtils.collectCommitsMetadata(project, repo.root, commitHash, publishedCommit)
val current = commits?.first()?.subject?.let { " (${commitHash.take(8)} \"$it\")" } ?: ""
val published = commits?.last()?.subject?.let { " (${publishedCommit?.take(8)} \"$it\")" } ?: ""
project.warn("The current commit$current is not available at the remote, picking the closest parent instead$published")
}

publishedCommit
}

fun getRepositoryInfo(repo: GitRepository, getCommitHash: () -> String?): RepositoryInfo {
val branch = repo.currentBranch?.name
val commitHash = getCommitHash() ?: throw NoRevisionException()
return RepositoryInfo(repo.root, branch, commitHash)
}

fun GitRepository.getRemotesMap(): Map<String, String> {
val map = mutableMapOf<String, String>()

repo.remotes.forEach {
remotes.forEach {
val url = it.firstUrl
val name = it.name

Expand All @@ -37,20 +77,43 @@ fun getRepositoryInfoAsGit(repo: GitRepository): RepositoryInfo {
throw InvalidRemotesException()
}

val branch = repo.currentBranch?.name
return map
}

fun findClosestDefaultBranchCommitTo(commitHash: String, repo: GitRepository, project: Project, remote: String): String {
val git = GitExecutableManager.getInstance().getExecutable(project)

val isPresentOnRemote = listOf(git.exePath, "branch", "-r", "--contains", commitHash)
.let(::GeneralCommandLine).withWorkDirectory(repo.root.path)
.let(::CapturingProcessHandler).runProcess()
.stdout.split("\n").any { it.startsWith("$remote/") }

val commitHash = repo.currentRevision
?: throw NoRevisionException()
if (isPresentOnRemote) {
return commitHash
}

val defaultBranch = detectDefaultBranchFor(remote, repo, project)
val branch = repo.branches.findRemoteBranch("$remote/$defaultBranch")
?: return commitHash

return GitHistoryUtils.getMergeBase(project, repo.root, branch.name, commitHash)?.rev ?: commitHash
}

private fun detectDefaultBranchFor(remote: String, repo: GitRepository, project: Project): String {
val git = GitExecutableManager.getInstance().getExecutable(project)

return RepositoryInfo(repo.root, branch, commitHash, map)
return listOf(git.exePath, "symbolic-ref", "refs/remotes/origin/HEAD")
.let(::GeneralCommandLine).withWorkDirectory(repo.root.path)
.let(::CapturingProcessHandler).runProcess()
.stdout.trim().removePrefix("refs/remotes/$remote/")
}

fun getRepositoryInfo(project: Project, file: VirtualFile): RepositoryInfo {
fun getRepositoryFor(file: VirtualFile, project: Project): GitRepository {
// a project can have multiple repositories reported, if there are
// submodules in it. info about each of those repositories will be
// fetched and returned
val repositories = GitRepositoryManager.getInstance(project)
.repositories.map { getRepositoryInfoAsGit(it) }
val repositories = GitRepositoryManager.getInstance(project).repositories

if (repositories.isEmpty()) {
throw GitRepositoryNeededException()
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/org/lunakoly/quicklink/utils/ThreadingHelpers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.lunakoly.quicklink.utils

import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.ThrowableComputable

fun Project?.runInBackground(progressTitle: String, block: () -> Unit) {
ProgressManager.getInstance().runProcessWithProgressSynchronously(
ThrowableComputable(block), progressTitle, true, this,
)
}
18 changes: 17 additions & 1 deletion src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,28 @@
<actions>
<action id="org.lunakoly.quicklink.actions.CopyLineLinkAction"
class="org.lunakoly.quicklink.actions.CopyLineLinkAction" text="Copy Line Link"
description="Generates an URL link to the current line with respect to the selected git remote">
description="Generates an URL link to the current line at some reasonably close commit with respect to the selected git remote. 'Reasonably close' means the current commit if it has been pushed to the remote, the closest parent commit with the remote branch that has been pushed or the closest parent with the remote default branch if no remote branch available.">
<add-to-group group-id="CopyFileReference" anchor="first"/>
<keyboard-shortcut keymap="$default" first-keystroke="shift control L"/>
<synonym text="Create Line Link"/>
<synonym text="Generate Line Link"/>
<synonym text="Link Line"/>
</action>
<action id="org.lunakoly.quicklink.actions.CopyCurrentCommitLineLinkAction"
class="org.lunakoly.quicklink.actions.CopyCurrentCommitLineLinkAction" text="Copy Current Commit Line Link"
description="Generates an URL link to the current line at the currently checked out commit with respect to the selected git remote">
<add-to-group group-id="CopyFileReference" anchor="first"/>
<synonym text="Create Current Line Link"/>
<synonym text="Generate Current Line Link"/>
<synonym text="Exact Current Line"/>
</action>
<action id="org.lunakoly.quicklink.actions.CopyLatestDefaultLineLinkAction"
class="org.lunakoly.quicklink.actions.CopyLatestDefaultLineLinkAction" text="Copy Latest Default Line Link"
description="Generates an URL link to the current line at the latest default branch commit with respect to the selected git remote">
<add-to-group group-id="CopyFileReference" anchor="first"/>
<synonym text="Create Latest Default Line Link"/>
<synonym text="Generate Latest Default Line Link"/>
<synonym text="Exact Latest Default Line"/>
</action>
</actions>
</idea-plugin>

0 comments on commit af484bc

Please sign in to comment.