Created
May 27, 2023 07:52
-
-
Save theY4Kman/7233d15dd976a524e8f287055c0f0451 to your computer and use it in GitHub Desktop.
Branch Changed Files line status tracker
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
package com.github.rewstapp.packauthoring.openapi.vcs.impl | |
import com.github.rewstapp.packauthoring.psi.search.scope.packageSet.BranchChangedFilesCustomScopesProvider | |
import com.github.rewstapp.packauthoring.settings.PackAuthoringSettings | |
import com.intellij.diff.DiffApplicationSettings | |
import com.intellij.diff.DiffContentFactory | |
import com.intellij.diff.DiffManager | |
import com.intellij.diff.comparison.ByWord | |
import com.intellij.diff.comparison.ComparisonPolicy | |
import com.intellij.diff.contents.DiffContent | |
import com.intellij.diff.requests.SimpleDiffRequest | |
import com.intellij.diff.util.DiffUtil | |
import com.intellij.openapi.Disposable | |
import com.intellij.openapi.LineNumberConstants | |
import com.intellij.openapi.actionSystem.AnAction | |
import com.intellij.openapi.components.service | |
import com.intellij.openapi.diagnostic.Logger | |
import com.intellij.openapi.diff.DefaultFlagsProvider | |
import com.intellij.openapi.diff.DefaultLineFlags | |
import com.intellij.openapi.diff.DiffBundle | |
import com.intellij.openapi.diff.LineStatusMarkerDrawUtil | |
import com.intellij.openapi.editor.Document | |
import com.intellij.openapi.editor.Editor | |
import com.intellij.openapi.ide.CopyPasteManager | |
import com.intellij.openapi.progress.ProgressIndicator | |
import com.intellij.openapi.progress.util.BackgroundTaskUtil | |
import com.intellij.openapi.project.Project | |
import com.intellij.openapi.util.Disposer | |
import com.intellij.openapi.util.TextRange | |
import com.intellij.openapi.vcs.ex.* | |
import com.intellij.openapi.vfs.VirtualFile | |
import com.intellij.ui.EditorTextField | |
import java.awt.Graphics | |
import java.awt.Point | |
import java.awt.datatransfer.StringSelection | |
import java.util.* | |
import javax.swing.JComponent | |
class BranchChangedFileRange( | |
line1: Int, | |
line2: Int, | |
vcsLine1: Int, | |
vcsLine2: Int, | |
innerRanges: List<InnerRange>? | |
) : Range(line1, line2, vcsLine1, vcsLine2, innerRanges) | |
class MergeBaseLocalLineStatusTracker( | |
project: Project, | |
document: Document, | |
virtualFile: VirtualFile, | |
) : LocalLineStatusTrackerImpl<BranchChangedFileRange>(project, document, virtualFile) { | |
override val renderer = MergeBaseLocalLineStatusMarkerRenderer(this) | |
@Suppress("UNCHECKED_CAST") | |
override var DocumentTracker.Block.innerRanges: List<Range.InnerRange>? | |
get() = data as List<Range.InnerRange>? | |
set(value) { data = value } | |
override fun setBaseRevision(vcsContent: CharSequence) { | |
setBaseRevisionContent(vcsContent, null) | |
} | |
override fun toRange(block: DocumentTracker.Block): BranchChangedFileRange = | |
BranchChangedFileRange(block.start, block.end, block.vcsStart, block.vcsEnd, block.innerRanges) | |
override fun isRangeModified(startLine: Int, endLine: Int): Boolean = false | |
override fun isLineModified(line: Int): Boolean = false | |
protected class MergeBaseLocalLineStatusMarkerRenderer( | |
tracker: LocalLineStatusTrackerImpl<*> | |
) : LocalLineStatusMarkerRenderer(tracker) { | |
override fun paint(editor: Editor, g: Graphics) { | |
LineStatusMarkerDrawUtil.paintDefault( | |
editor, g, myTracker, BranchChangeFilesFlagsProvider, 0 | |
) | |
} | |
} | |
} | |
class BranchChangedFileLocalLineStatusTracker( | |
project: Project, | |
document: Document, | |
virtualFile: VirtualFile | |
) : LocalLineStatusTrackerImpl<Range>(project, document, virtualFile) { | |
private val logger = Logger.getInstance(BranchChangedFileLocalLineStatusTracker::class.java) | |
override val renderer = BranchChangedFileLineStatusMarkerRenderer(this) | |
private val settings = project.service<PackAuthoringSettings>() | |
private val branchChangeListScopeProvider = BranchChangedFilesCustomScopesProvider.getInstance(project) | |
private var isPaneActive = branchChangeListScopeProvider.isPaneActive() | |
private val isMergeBaseEnabled: Boolean | |
get() = settings.enableBranchChangedFilesLineStatuses && | |
when (settings.branchChangedFilesLineStatusDisplayMode) { | |
BranchChangedFilesLineStatusDisplayMode.NEVER_SHOW -> false | |
BranchChangedFilesLineStatusDisplayMode.SHOW_WHEN_PANE_ACTIVE -> isPaneActive | |
BranchChangedFilesLineStatusDisplayMode.ALWAYS_SHOW -> true | |
} | |
private val isChangelistEnabled: Boolean | |
get() = settings.enableBranchChangedFilesLineStatuses && | |
when (settings.changelistLineStatusDisplayMode) { | |
ChangelistLineStatusDisplayMode.NEVER_SHOW -> false | |
ChangelistLineStatusDisplayMode.HIDE_WHEN_PANE_ACTIVE -> !isPaneActive | |
ChangelistLineStatusDisplayMode.ALWAYS_SHOW -> true | |
} | |
private val mergeBaseTracker = MergeBaseLocalLineStatusTracker(project, document, virtualFile) | |
private val changelistTracker = ChangelistsLocalLineStatusTracker(project, document, virtualFile) | |
private var trackers = listOf(mergeBaseTracker, changelistTracker) | |
init { | |
listOf(mergeBaseTracker, changelistTracker) | |
.forEach { Disposer.register(disposable, it.disposable) } | |
settings.addBranchChangedFilesTrackerListener( | |
object : PackAuthoringSettings.BranchChangedFilesSettingsListener { | |
override fun changed() { reconcileTrackerVisibility() } | |
}, | |
disposable, | |
) | |
branchChangeListScopeProvider.addListener( | |
object : BranchChangedFilesCustomScopesProvider.ViewListener { | |
override fun changed() { reconcileTrackerVisibility() } | |
}, | |
disposable, | |
) | |
reconcileTrackerVisibility() | |
} | |
private fun setTrackerVisibility(tracker: LocalLineStatusTrackerImpl<*>, isVisible: Boolean): Boolean { | |
if (tracker.mode.isVisible != isVisible) { | |
tracker.mode = LocalLineStatusTracker.Mode( | |
isVisible, | |
tracker.mode.showErrorStripeMarkers, | |
tracker.mode.detectWhitespaceChangedLines | |
) | |
return true | |
} | |
return false | |
} | |
private fun reconcileTrackerVisibility() { | |
if (!settings.enableBranchChangedFilesLineStatuses) { | |
this.release() | |
logger.debug("Branch Changed Files line statuses disabled. Releasing tracker for $virtualFile") | |
return | |
} | |
isPaneActive = branchChangeListScopeProvider.isPaneActive() | |
trackers = listOfNotNull( | |
if (isMergeBaseEnabled) mergeBaseTracker else null, | |
if (isChangelistEnabled) changelistTracker else null, | |
) | |
if ( | |
setTrackerVisibility(mergeBaseTracker, isMergeBaseEnabled) || | |
setTrackerVisibility(changelistTracker, isChangelistEnabled) | |
) { | |
updateHighlighters() | |
} | |
} | |
@Suppress("UNCHECKED_CAST") | |
override var DocumentTracker.Block.innerRanges: List<Range.InnerRange>? | |
get() = data as List<Range.InnerRange>? | |
set(value) { data = value } | |
override fun setBaseRevision(vcsContent: CharSequence) { | |
setBaseRevisionContent(vcsContent, null) | |
} | |
fun setBaseRevision(mergeBaseContent: CharSequence, headDocument: Document?) { | |
if (headDocument != null) { | |
val headText = headDocument.text | |
setBaseRevision(headText) | |
mergeBaseTracker.setBaseRevision(mergeBaseContent) | |
changelistTracker.setBaseRevision(headText) | |
} | |
} | |
override fun findRange(range: Range): Range? = | |
when (range) { | |
is BranchChangedFileRange -> mergeBaseTracker.findRange(range) | |
else -> changelistTracker.findRange(range) | |
} | |
override fun getNextRange(line: Int): Range? = | |
trackers.mapNotNull { it.getNextRange(line) }.minByOrNull { it.line1 } | |
override fun getPrevRange(line: Int): Range? = | |
trackers.mapNotNull { it.getPrevRange(line) }.maxByOrNull { it.line2 } | |
override fun getRangeForLine(line: Int): Range? = | |
trackers.firstNotNullOfOrNull { it.getRangeForLine(line) } | |
override fun getRangesForLines(lines: BitSet): List<Range> = | |
combinedTrackerResults { it.getRangesForLines(lines) } | |
override fun isRangeModified(startLine: Int, endLine: Int): Boolean = | |
// NB(zk): highlights are hidden if this returns true, and we want to hide them ONLY if the | |
// line is currently changed in the working tree | |
isChangelistEnabled && changelistTracker.isRangeModified(startLine, endLine) | |
override fun isLineModified(line: Int): Boolean = isRangeModified(line, line + 1) | |
override fun transferLineFromVcs(line: Int, approximate: Boolean): Int = | |
trackers | |
.firstOrNull { it.getRangeForLine(line) != null } | |
?.transferLineFromVcs(line, approximate) | |
?: line | |
override fun transferLineToVcs(line: Int, approximate: Boolean): Int = | |
// NB(zk): highlights are also hidden if this returns a negative number, so we always try | |
// to provide a value here. | |
trackers | |
.firstNotNullOfOrNull { | |
when (val vcsLine = it.transferLineToVcs(line, approximate)) { | |
LineNumberConstants.ABSENT_LINE_NUMBER, | |
LineNumberConstants.FAKE_LINE_NUMBER -> null | |
else -> vcsLine | |
} | |
} | |
?: LineNumberConstants.ABSENT_LINE_NUMBER | |
override fun toRange(block: DocumentTracker.Block): Range = | |
BranchChangedFileRange(block.start, block.end, block.vcsStart, block.vcsEnd, block.innerRanges) | |
override fun getRanges(): List<Range> = | |
combinedTrackerResults { it.getRanges() } | |
override fun rollbackChanges(range: Range) { | |
when (range) { | |
is BranchChangedFileRange -> mergeBaseTracker.rollbackChanges(range) | |
else -> changelistTracker.rollbackChanges(range) | |
} | |
} | |
override fun rollbackChanges(lines: BitSet) { | |
trackers | |
.firstOrNull { it.getRangesForLines(lines)?.isNotEmpty() ?: false } | |
?.rollbackChanges(lines) | |
} | |
fun getVcsContentForRange(range: Range): CharSequence = | |
DiffUtil.getLinesContent(getVcsDocumentForRange(range), range.vcsLine1, range.vcsLine2) | |
fun getVcsDocumentForRange(range: Range): Document = | |
when (range) { | |
is BranchChangedFileRange -> mergeBaseTracker.vcsDocument | |
else -> changelistTracker.vcsDocument | |
} | |
private fun <T> combinedTrackerResults(func: (LocalLineStatusTrackerImpl<out Range>) -> List<T>?): List<T> = | |
trackers | |
.mapNotNull(func) | |
.fold(emptyList()) { acc, ranges -> | |
acc + ranges | |
} | |
protected class BranchChangedFileLineStatusMarkerRenderer( | |
private val branchChangedFileTracker: BranchChangedFileLocalLineStatusTracker | |
) : LocalLineStatusMarkerRenderer(branchChangedFileTracker) { | |
override fun paint(editor: Editor, g: Graphics) { | |
// no-op — the delegated trackers' renderers will handle their own painting | |
} | |
/** | |
* Lifted from [LocalLineStatusTrackerImpl.LocalLineStatusMarkerRenderer] and changed to | |
* use the [vcsDocument] from the appropriate tracker (changelist or mergeBase), | |
* depending on the type of [Range]. | |
*/ | |
override fun showHintAt(editor: Editor, range: Range, mousePosition: Point?) { | |
if (!myTracker.isValid()) return | |
val disposable = Disposer.newDisposable() | |
var editorComponent: JComponent? = null | |
if (range.hasVcsLines()) { | |
val vcsDocument = branchChangedFileTracker.getVcsDocumentForRange(range) | |
val content = DiffUtil.getLinesContent(vcsDocument, range.vcsLine1, range.vcsLine2).toString() | |
val textField = LineStatusMarkerPopupPanel.createTextField(editor, content) | |
val vcsTextRange = DiffUtil.getLinesRange(vcsDocument, range.vcsLine1, range.vcsLine2) | |
LineStatusMarkerPopupPanel.installBaseEditorSyntaxHighlighters( | |
myTracker.project, textField, vcsDocument, vcsTextRange, fileType | |
) | |
installWordDiff(editor, textField, range, disposable) | |
editorComponent = LineStatusMarkerPopupPanel.createEditorComponent(editor, textField) | |
} | |
val actions = createToolbarActions(editor, range, mousePosition) | |
val toolbar = LineStatusMarkerPopupPanel.buildToolbar(editor, actions, disposable) | |
val additionalInfoPanel = createAdditionalInfoPanel(editor, range, mousePosition, disposable) | |
LineStatusMarkerPopupPanel.showPopupAt( | |
editor, | |
toolbar, | |
editorComponent, | |
additionalInfoPanel, | |
mousePosition, | |
disposable, | |
null | |
) | |
} | |
/** | |
* Lifted directly from [LocalLineStatusTrackerImpl.LocalLineStatusMarkerRenderer], and | |
* changed to use vcsContent from the appropriate tracker (changelist or mergeBase), | |
* depending on the type of [Range]. | |
*/ | |
private fun installWordDiff( | |
editor: Editor, textField: EditorTextField, range: Range, disposable: Disposable | |
) { | |
if (!DiffApplicationSettings.getInstance().SHOW_LST_WORD_DIFFERENCES) return | |
if (!range.hasLines() || !range.hasVcsLines()) return | |
val vcsContent = branchChangedFileTracker.getVcsContentForRange(range) | |
val currentContent = LineStatusMarkerPopupActions.getCurrentContent(myTracker, range) | |
val wordDiff = BackgroundTaskUtil.tryComputeFast( | |
{ indicator: ProgressIndicator -> | |
ByWord.compare(vcsContent, currentContent, ComparisonPolicy.DEFAULT, indicator) | |
}, 200 | |
) ?: return | |
LineStatusMarkerPopupPanel.installMasterEditorWordHighlighters( | |
editor, range.line1, range.line2, wordDiff, disposable | |
) | |
LineStatusMarkerPopupPanel.installPopupEditorWordHighlighters(textField, wordDiff) | |
} | |
/** | |
* Install our own toolbar actions, which calculate diffs appropriately for merge-base changes | |
*/ | |
override fun createToolbarActions( | |
editor: Editor, | |
range: Range, | |
mousePosition: Point? | |
): List<AnAction> = | |
super.createToolbarActions(editor, range, mousePosition) | |
.map { action -> | |
when (action) { | |
is ShowLineStatusRangeDiffAction -> RoutedShowLineStatusRangeDiffAction(editor, range) | |
is CopyLineStatusRangeAction -> RoutedCopyLineStatusRangeAction(editor, range) | |
else -> action | |
} | |
} | |
private inner class RoutedShowLineStatusRangeDiffAction( | |
editor: Editor, range: Range | |
) : ShowLineStatusRangeDiffAction(editor, range) { | |
override fun actionPerformed(editor: Editor, range: Range) { | |
BranchChangedFilesMarkerPopupActions.showDiff(branchChangedFileTracker, range) | |
} | |
} | |
private inner class RoutedCopyLineStatusRangeAction( | |
editor: Editor, range: Range | |
) : CopyLineStatusRangeAction(editor, range) { | |
override fun actionPerformed(editor: Editor, range: Range) { | |
BranchChangedFilesMarkerPopupActions.copyVcsContent(branchChangedFileTracker, range) | |
} | |
} | |
} | |
} | |
/** | |
* Lifted from [LineStatusMarkerPopupActions], and changed to use the vcsDocument from the | |
* appropriate tracker (changelist or mergeBase), depending on the type of [Range]. | |
*/ | |
object BranchChangedFilesMarkerPopupActions { | |
fun showDiff(tracker: BranchChangedFileLocalLineStatusTracker, range: Range) { | |
val vcsDocument = tracker.getVcsDocumentForRange(range) | |
val project = tracker.project | |
val ourRange = expand(range, tracker.document, vcsDocument) | |
val vcsContent = createDiffContent( | |
project, | |
vcsDocument, | |
tracker.virtualFile, | |
DiffUtil.getLinesRange(vcsDocument, ourRange.vcsLine1, ourRange.vcsLine2) | |
) | |
val currentContent = createDiffContent( | |
project, | |
tracker.document, | |
tracker.virtualFile, | |
LineStatusMarkerPopupActions.getCurrentTextRange(tracker, ourRange) | |
) | |
val request = SimpleDiffRequest( | |
DiffBundle.message("dialog.title.diff.for.range"), | |
vcsContent, currentContent, | |
DiffBundle.message("diff.content.title.up.to.date"), | |
DiffBundle.message("diff.content.title.current.range") | |
) | |
DiffManager.getInstance().showDiff(project, request) | |
} | |
private fun createDiffContent( | |
project: Project?, | |
document: Document, | |
highlightFile: VirtualFile?, | |
textRange: TextRange | |
): DiffContent { | |
val content = DiffContentFactory.getInstance().create(project, document, highlightFile) | |
return DiffContentFactory.getInstance().createFragment(project, content, textRange) | |
} | |
private fun expand(range: Range, document: Document, uDocument: Document): Range { | |
val canExpandBefore = range.line1 != 0 && range.vcsLine1 != 0 | |
val canExpandAfter = | |
range.line2 < DiffUtil.getLineCount(document) && range.vcsLine2 < DiffUtil.getLineCount( | |
uDocument | |
) | |
val offset1 = range.line1 - if (canExpandBefore) 1 else 0 | |
val uOffset1 = range.vcsLine1 - if (canExpandBefore) 1 else 0 | |
val offset2 = range.line2 + if (canExpandAfter) 1 else 0 | |
val uOffset2 = range.vcsLine2 + if (canExpandAfter) 1 else 0 | |
return Range(offset1, offset2, uOffset1, uOffset2) | |
} | |
fun copyVcsContent(tracker: BranchChangedFileLocalLineStatusTracker, range: Range) { | |
val content = tracker.getVcsContentForRange(range).toString() + "\n" | |
CopyPasteManager.getInstance().setContents(StringSelection(content)) | |
} | |
} | |
object BranchChangeFilesFlagsProvider : DefaultFlagsProvider() { | |
override fun getFlags(range: Range): DefaultLineFlags = | |
when (range) { | |
// NB(zk): IGNORED will paint annotations as borders only, without filling the background, | |
// which is perfect for showing non-working-tree, merge-base changes | |
is BranchChangedFileRange -> DefaultLineFlags.IGNORED | |
else -> DefaultLineFlags.DEFAULT | |
} | |
override fun shouldIgnoreInnerRanges(flag: DefaultLineFlags): Boolean = false | |
} |
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
package com.github.rewstapp.packauthoring.openapi.vcs.impl | |
import com.github.rewstapp.packauthoring.services.PackAuthoringProjectService | |
import com.github.rewstapp.packauthoring.settings.PackAuthoringSettings | |
import com.intellij.openapi.application.ApplicationManager | |
import com.intellij.openapi.components.service | |
import com.intellij.openapi.diagnostic.Logger | |
import com.intellij.openapi.editor.Document | |
import com.intellij.openapi.editor.EditorFactory | |
import com.intellij.openapi.fileEditor.FileDocumentManager | |
import com.intellij.openapi.project.Project | |
import com.intellij.openapi.util.text.StringUtil | |
import com.intellij.openapi.vcs.VcsException | |
import com.intellij.openapi.vcs.ex.LocalLineStatusTracker | |
import com.intellij.openapi.vcs.impl.LineStatusTrackerContentLoader | |
import com.intellij.openapi.vcs.impl.LineStatusTrackerManager | |
import com.intellij.openapi.vfs.VirtualFile | |
import com.intellij.vcsUtil.VcsFileUtil | |
import com.intellij.vcsUtil.VcsImplUtil | |
import com.intellij.vcsUtil.VcsUtil | |
import git4idea.GitContentRevision | |
import git4idea.index.* | |
import git4idea.index.vfs.GitIndexFileSystemRefresher | |
import git4idea.repo.GitRepositoryManager | |
import git4idea.util.GitFileUtils | |
import vendored.com.intellij.openapi.application.runReadAction | |
import vendored.com.intellij.util.asSafely | |
import java.nio.charset.Charset | |
enum class BranchChangedFilesLineStatusDisplayMode(private val displayName: String) { | |
/** Never show line statuses for merge-base changes */ | |
NEVER_SHOW("Never show"), | |
/** Show line statuses for merge-base changes only when the Branch Changed Files pane is active */ | |
SHOW_WHEN_PANE_ACTIVE("Show when pane is active"), | |
/** Always show line statuses for merge-base changes */ | |
ALWAYS_SHOW("Always show"); | |
override fun toString(): String = displayName | |
} | |
enum class ChangelistLineStatusDisplayMode(private val displayName: String) { | |
/** Never show line statuses for changelist changes */ | |
NEVER_SHOW("Never show"), | |
/** Hide line statuses for changelist changes only when the Branch Changed Files pane is active */ | |
HIDE_WHEN_PANE_ACTIVE("Hide when pane is active"), | |
/** Always show line statuses for changelist changes */ | |
ALWAYS_SHOW("Always show"); | |
override fun toString(): String = displayName | |
} | |
class BranchChangedFilesLocalLineStatusTrackerProvider : LineStatusTrackerContentLoader { | |
private val logger = Logger.getInstance(BranchChangedFilesLocalLineStatusTrackerProvider::class.java) | |
private val trackedProjects = mutableSetOf<Project>() | |
private fun addProjectListeners(project: Project) { | |
val settings = project.service<PackAuthoringSettings>() | |
val packAuthoringService = project.service<PackAuthoringProjectService>() | |
settings.addBranchChangedFilesTrackerListener( | |
object : PackAuthoringSettings.BranchChangedFilesSettingsListener { | |
override fun enableBranchChangedFilesLineStatusesChanged() { | |
if (settings.enableBranchChangedFilesLineStatuses) { | |
// Release existing trackers after enabling our own, so | |
// next request for a tracker will use our own | |
val lstm = LineStatusTrackerManager.getInstance(project) | |
val editorFactory = EditorFactory.getInstance() | |
val editorTrackers = | |
editorFactory.allEditors | |
.filter { editor -> editor.project == project } | |
.mapNotNull { editor -> | |
lstm.getLineStatusTracker(editor.document) | |
?.asSafely<LocalLineStatusTracker<*>>() | |
?.let { tracker -> editor to tracker } | |
} | |
.filter { (_, tracker) -> !isMyTracker(tracker) } | |
logger.debug("Releasing ${editorTrackers.size} other line status trackers after enabling Branch Changed Files line status tracking ...") | |
editorTrackers.forEach { (editor, tracker) -> | |
tracker.release() | |
ApplicationManager.getApplication().invokeLater { | |
lstm.requestTrackerFor(tracker.document, editor) | |
} | |
} | |
logger.info("Released ${editorTrackers.size} other line status trackers after enabling Branch Changed Files line status tracking") | |
} | |
} | |
}, | |
packAuthoringService, | |
) | |
} | |
override fun createTracker(project: Project, file: VirtualFile): LocalLineStatusTracker<*>? { | |
val document = FileDocumentManager.getInstance().getDocument(file) ?: return null | |
if (project !in trackedProjects) { | |
addProjectListeners(project) | |
trackedProjects.add(project) | |
} | |
return BranchChangedFileLocalLineStatusTracker(project, document, file) | |
} | |
private class BranchChangedContentInfo( | |
val currentRevision: String?, | |
val mergeBaseRevision: String?, | |
val charset: Charset, | |
val virtualFile: VirtualFile, | |
) : | |
LineStatusTrackerContentLoader.ContentInfo | |
private class BranchChangedTrackerContent( | |
val mergeBaseContent: CharSequence, | |
val headDocument: Document?, | |
) : | |
LineStatusTrackerContentLoader.TrackerContent | |
override fun getContentInfo( | |
project: Project, | |
file: VirtualFile | |
): LineStatusTrackerContentLoader.ContentInfo? { | |
val repository = GitRepositoryManager.getInstance(project).getRepositoryForFile(file) ?: return null | |
val branchChangedFilesTracker = project.service<BranchChangedFilesTracker>() | |
return BranchChangedContentInfo(repository.currentRevision, branchChangedFilesTracker.mergeBaseRevision, file.charset, file) | |
} | |
override fun handleLoadingError(tracker: LocalLineStatusTracker<*>) { | |
tracker as BranchChangedFileLocalLineStatusTracker | |
tracker.dropBaseRevision() | |
} | |
override fun isMyTracker(tracker: LocalLineStatusTracker<*>): Boolean = | |
tracker is BranchChangedFileLocalLineStatusTracker | |
override fun isTrackedFile(project: Project, file: VirtualFile): Boolean { | |
// NB(zk): this is mostly lifted from GitStageLineStatusTrackerProvider | |
if (!file.isInLocalFileSystem) return false | |
val settings = project.service<PackAuthoringSettings>() | |
if (!settings.enableBranchChangedFilesLineStatuses) return false | |
val repository = GitRepositoryManager.getInstance(project).getRepositoryForFileQuick(file) | |
return repository != null | |
} | |
override fun loadContent( | |
project: Project, | |
info: LineStatusTrackerContentLoader.ContentInfo | |
): LineStatusTrackerContentLoader.TrackerContent? { | |
// NB(zk): this logic is mostly lifted from GitStageLineStatusTrackerProvider | |
info as BranchChangedContentInfo | |
val branchChangedFilesTracker = project.service<BranchChangedFilesTracker>() | |
val file = info.virtualFile | |
val filePath = VcsUtil.getFilePath(file) | |
val status = GitStageTracker.getInstance(project).status(file) ?: return null | |
if (GitContentRevision.getRepositoryIfSubmodule(project, filePath) != null) return null | |
val repository = GitRepositoryManager.getInstance(project).getRepositoryForFile(file) ?: return null | |
val indexFileRefresher = GitIndexFileSystemRefresher.getInstance(project) | |
val indexFile = indexFileRefresher.getFile(repository.root, status.path(ContentVersion.STAGED)) ?: return null | |
val indexDocument = runReadAction { FileDocumentManager.getInstance().getDocument(indexFile) } ?: return null | |
if (!status.has(ContentVersion.LOCAL)) { | |
return BranchChangedTrackerContent("", null) | |
} | |
val fileRev = branchChangedFilesTracker.getEarliestMergeBaseRevisionForFile(repository, file) ?: return null | |
try { | |
val bytes = GitFileUtils.getFileContent( | |
project, | |
repository.root, | |
fileRev, | |
VcsFileUtil.relativePath( | |
repository.root, status.path(ContentVersion.HEAD) | |
), | |
) | |
val headContent = VcsImplUtil.loadTextFromBytes(project, bytes, filePath) | |
val correctedText = StringUtil.convertLineSeparators(headContent) | |
return BranchChangedTrackerContent(correctedText, indexDocument) | |
} catch (e: VcsException) { | |
logger.warn("Can't load base revision content for ${file.path} with status $status", e) | |
return null | |
} | |
} | |
override fun setLoadedContent( | |
tracker: LocalLineStatusTracker<*>, | |
content: LineStatusTrackerContentLoader.TrackerContent | |
) { | |
tracker as BranchChangedFileLocalLineStatusTracker | |
content as BranchChangedTrackerContent | |
tracker.setBaseRevision(content.mergeBaseContent, content.headDocument) | |
} | |
override fun shouldBeUpdated( | |
oldInfo: LineStatusTrackerContentLoader.ContentInfo?, | |
newInfo: LineStatusTrackerContentLoader.ContentInfo | |
): Boolean { | |
newInfo as BranchChangedContentInfo | |
return oldInfo == null || | |
oldInfo !is BranchChangedContentInfo || | |
oldInfo.currentRevision != newInfo.currentRevision || | |
oldInfo.mergeBaseRevision != newInfo.mergeBaseRevision || | |
oldInfo.charset != newInfo.charset | |
} | |
} |
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
package com.github.rewstapp.packauthoring.openapi.vcs.impl | |
import com.github.rewstapp.packauthoring.services.PackAuthoringProjectService | |
import com.github.rewstapp.packauthoring.settings.PackAuthoringSettings | |
import com.github.rewstapp.packauthoring.settings.PackAuthoringSettings.BranchChangedFilesSettingsListener | |
import com.github.rewstapp.packauthoring.util.profile | |
import com.intellij.openapi.Disposable | |
import com.intellij.openapi.components.service | |
import com.intellij.openapi.diagnostic.Logger | |
import com.intellij.openapi.progress.EmptyProgressIndicator | |
import com.intellij.openapi.progress.ProgressIndicator | |
import com.intellij.openapi.progress.ProgressManager | |
import com.intellij.openapi.progress.Task | |
import com.intellij.openapi.project.Project | |
import com.intellij.openapi.util.text.StringUtil | |
import com.intellij.openapi.vcs.VcsException | |
import com.intellij.openapi.vcs.changes.ChangeListAdapter | |
import com.intellij.openapi.vcs.changes.ChangeListListener | |
import com.intellij.openapi.vcs.changes.ChangeListManager | |
import com.intellij.openapi.vfs.VirtualFile | |
import com.intellij.util.EventDispatcher | |
import git4idea.GitUtil | |
import git4idea.commands.Git | |
import git4idea.commands.GitCommand | |
import git4idea.commands.GitLineHandler | |
import git4idea.history.GitHistoryUtils | |
import git4idea.repo.GitRepository | |
import git4idea.repo.GitRepositoryChangeListener | |
import git4idea.repo.GitRepositoryManager | |
import git4idea.util.StringScanner | |
import org.jetbrains.annotations.NonNls | |
import java.util.* | |
class BranchChangedFilesTracker(val project: Project) { | |
private val logger = Logger.getInstance(BranchChangedFilesTracker::class.java) | |
private val eventDispatcher = EventDispatcher.create(BranchChangedFilesTrackerListener::class.java) | |
/** Revision where the current branch meets the main branch */ | |
private var _mergeBaseRevision: String? = null | |
val mergeBaseRevision: String? | |
get() = _mergeBaseRevision | |
/** Files changed since the branch deviated from master */ | |
private var mergeBaseChangedPaths = emptySet<String>() | |
/** Currently-modified files */ | |
private var currentAffectedPaths = emptySet<String>() | |
/** Combined set of the above two */ | |
private var _branchChangedPaths = emptySet<String>() | |
var branchChangedPaths: Set<String> | |
get() = _branchChangedPaths | |
private set(value) { | |
_branchChangedPaths = value | |
} | |
private val settings = project.service<PackAuthoringSettings>() | |
private val packAuthoringService = project.service<PackAuthoringProjectService>() | |
private val git = Git.getInstance() | |
private val gitRepoManager = GitRepositoryManager.getInstance(project) | |
private val changeListManager = ChangeListManager.getInstance(project) | |
private val progressManager = ProgressManager.getInstance() | |
init { | |
// Explicitly attach our connection to our own plugin&project-level parent disposable, so | |
// the connection will be disposed when our plugin unloads (a prerequisite for dynamic reloading) | |
val busConnection = project.messageBus.connect(packAuthoringService) | |
// When repo is first loaded, or is fetched/pulled, recalculate file changes since the | |
// merge-base ref (the point at which the current branch diverges from main branch) | |
busConnection.subscribe(GitRepository.GIT_REPO_CHANGE, GitRepositoryChangeListener { repo -> | |
onRepositoryChanged(repo) | |
}) | |
// Refresh the scope view pane whenever a change is made to the stage (i.e. whenever a file | |
// is added/removed from the stage, or a staged file is modified) | |
busConnection.subscribe(ChangeListListener.TOPIC, object : ChangeListAdapter() { | |
override fun changeListsChanged() = onAffectedFilesChanged() | |
override fun changeListUpdateDone() = onAffectedFilesChanged() | |
}) | |
// Recalculate merge-base changes when main branch is changed | |
settings.addBranchChangedFilesTrackerListener( | |
object : BranchChangedFilesSettingsListener { | |
override fun mainBranchChanged() { | |
gitRepoManager.repositories.filter { it.project == project }.forEach { repo -> | |
onRepositoryChanged(repo) | |
} | |
} | |
}, | |
packAuthoringService, | |
) | |
} | |
fun addListener(listener: BranchChangedFilesTrackerListener, disposable: Disposable) { | |
eventDispatcher.addListener(listener, disposable) | |
} | |
private fun onRepositoryChanged(repo: GitRepository) { | |
val branch = repo.currentBranch ?: return | |
val branchName = branch.name | |
val mainBranch = getMainBranchName() | |
val bgTask = object : Task.Backgroundable(project, "Updating branch changed files") { | |
override fun run(indicator: ProgressIndicator) { | |
profile(logger, "Determining merge base and calculating file diffs") { | |
_mergeBaseRevision = | |
GitHistoryUtils.getMergeBase(project, repo.root, branchName, mainBranch)?.rev | |
?: mainBranch | |
mergeBaseChangedPaths = getPathsDiffBetweenRefs(git, repo, branchName, mergeBaseRevision!!) | |
} | |
onAffectedFilesChanged() | |
} | |
} | |
progressManager.runProcessWithProgressAsynchronously(bgTask, EmptyProgressIndicator()) | |
} | |
private fun onAffectedFilesChanged() { | |
currentAffectedPaths = changeListManager.affectedPaths.map { it.path }.toSet() | |
branchChangedPaths = mergeBaseChangedPaths.union(currentAffectedPaths) | |
eventDispatcher.multicaster.update() | |
} | |
private fun getMainBranchName(): String = settings.branchChangedFilesMainBranch | |
/** | |
* Returns absolute paths which have changed locally comparing to the current branch, i.e. performs | |
* `git diff --name-only origin/master...master` | |
* | |
* Paths are absolute, Git-formatted (i.e. with forward slashes). | |
*/ | |
@Throws(VcsException::class) | |
fun getPathsDiffBetweenRefs( | |
git: Git, repository: GitRepository, | |
beforeRef: @NonNls String, afterRef: @NonNls String, | |
): Set<String> { | |
val parameters: List<String> = mutableListOf("--name-only", "--pretty=format:") | |
val range = "$afterRef...$beforeRef" | |
val result = git.diff(repository, parameters, range) | |
if (!result.success()) { | |
logger.info("Couldn't get diff in range [$range] for repository [${repository.toLogString()}]") | |
return emptySet() | |
} | |
val remoteChanges = HashSet<String>() | |
val s = StringScanner(result.outputAsJoinedString) | |
while (s.hasMoreData()) { | |
val relative = s.line() | |
if (StringUtil.isEmptyOrSpaces(relative)) { | |
continue | |
} | |
val path = repository.root.path + "/" + GitUtil.unescapePath(relative) | |
remoteChanges.add(path) | |
} | |
return remoteChanges | |
} | |
/** | |
* Find the earliest commit since mergeBaseRevision which includes the given file | |
*/ | |
fun getEarliestMergeBaseRevisionForFile(repo: GitRepository, file: VirtualFile): String? { | |
val baseRevision = mergeBaseRevision | |
?: getMainBranchName().takeIf { repo.branches.findBranchByName(it) != null } | |
?: return null | |
val h = GitLineHandler(project, repo.root, GitCommand.LOG) | |
h.setSilent(true) | |
h.addParameters("--pretty=format:%H") | |
h.addParameters("--diff-filter=A") | |
h.addParameters("--follow") | |
h.addParameters(baseRevision, "HEAD") | |
h.endOptions() | |
h.addRelativeFiles(listOf(file)) | |
val result = git.runCommand(h).getOutputOrThrow() | |
if (result.isEmpty()) { | |
return baseRevision | |
} | |
// NOTE: due to --follow chasing commits across renames, the commit returned by git log | |
// may be earlier than the merge-base commit. We must therefore check that the | |
// returned commit is actually a descendant of the merge-base commit. | |
val earliestFollowedRev = result.trim() | |
if (isAncestor(repo, earliestFollowedRev, baseRevision)) { | |
return baseRevision | |
} | |
return earliestFollowedRev | |
} | |
fun isAncestor(repo: GitRepository, ancestorRev: String, descendantRev: String): Boolean { | |
val descHandler = GitLineHandler(project, repo.root, GitCommand.MERGE_BASE) | |
descHandler.setSilent(true) | |
descHandler.addParameters("--is-ancestor") | |
descHandler.addParameters(ancestorRev) | |
descHandler.addParameters(descendantRev) | |
return git.runCommandWithoutCollectingOutput(descHandler).success() | |
} | |
} | |
interface BranchChangedFilesTrackerListener : EventListener { | |
fun update() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment