Skip to content

Commit

Permalink
use vscode.git extension api for folder/repo management, move extensi…
Browse files Browse the repository at this point in the history
…on git logic into separate file, add new `verbose-logging` option for message logging, add new output channel for dedicated log output

now we don't ship our own file exclude handling anymore (constraint e.g. by nesting depth) and instead just ask VSCode directly for the list of repositories the user has opened. This means that said list now also updates automatically on change as you'd expect.
removes the need for `glob` package, reducing bundle size significantly
fixes detection of working tree changes or head moves e.g. on `commit --amend` (#9)

closes #9
  • Loading branch information
phil294 committed May 17, 2023
1 parent c272003 commit a5cfa38
Show file tree
Hide file tree
Showing 14 changed files with 544 additions and 284 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ web-dist
*.ttf
todo*
*.vsix
extension.js
src/*.js
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ Please consider opening an issue or PR if you think a certain action warrants a
"git-log--graph.folder": {
"description": "Use this to overwrite the desired *absolute* path in which a .git folder is located. You usually don't need to do this as folder selection is available from the interface.",
"type": "string"
},
"git-log--graph.verbose-logging": {
"type": "boolean",
"default": false
}
}
```
Expand Down
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
"onStartupFinished",
"onWebviewPanel:git-log--graph"
],
"extensionDependencies": [
"vscode.git"
],
"main": "./src/extension.js",
"contributes": {
"commands": [
Expand Down Expand Up @@ -433,6 +436,10 @@
"description": "If active, the mouse wheel event on the scroller will not be caught and instead behave normally. This comes at the expense of the dotted connection lines at the top being offset wrongly more often.",
"type": "boolean",
"default": false
},
"git-log--graph.verbose-logging": {
"type": "boolean",
"default": false
}
}
}
Expand All @@ -445,7 +452,5 @@
"coffeescript": "^2.7.0",
"esbuild": "^0.17.18"
},
"dependencies": {
"glob": "^10.2.2"
}
"dependencies": {}
}
123 changes: 46 additions & 77 deletions src/extension.coffee
Original file line number Diff line number Diff line change
@@ -1,85 +1,51 @@
vscode = require 'vscode'
{ join, dirname } = require('path')
util = require('util')
{ glob } = require('glob')
exec = util.promisify(require('child_process').exec)

{ get_git } = require './git'

``###* @typedef {{ type: 'response' | 'request' | 'push', command?: string, data?: any, error?: any, id: number | string }} BridgeMessage ###

EXT_NAME = 'git log --graph'
EXT_ID = 'git-log--graph'
START_CMD = 'git-log--graph.start'

selected_folder_path = ''
``###* @type {{name:string,path:string}[]} ###
folders = []
refresh_folders = =>
folders = await Promise.all((vscode.workspace.workspaceFolders or []).map (root) =>
paths = try await glob '**/.git',
ignore: 'node_modules/**' # TODO maybe use some kind of vscode setting for this
maxDepth: 3
cwd: root.uri.fsPath
signal: AbortSignal.timeout(2000)
if not paths
(vscode.workspace.workspaceFolders or []).map (folder) =>
name: folder.uri.fsPath
path: folder.uri.fsPath
else
paths.map (path) =>
path = dirname(path)
if path == '.' then path = ''
name: if path then "#{root.name}/#{path}" else root.name
path: join(root.uri.fsPath, path)
).then (x) => x.flat()

``###* @type {vscode.FileSystemWatcher | null} ###
index_watcher = null
restart_index_watcher = =>
if index_watcher
index_watcher.dispose()
index_watcher = null
return if not selected_folder_path
index_watcher = vscode.workspace.createFileSystemWatcher "#{selected_folder_path}/.git/index"
index_change = =>
return if Date.now() - last_git_execution < 1500
console.info 'file watcher: git INDEX change' # from external, e.g. cli
post_message
type: 'push'
id: 'git-index-change'
index_watcher.onDidChange index_change
index_watcher.onDidCreate index_change
index_watcher.onDidDelete index_change

last_git_execution = 0
git = (###* @type string ### args) =>
{ stdout, stderr } = await exec 'git ' + args,
cwd: vscode.workspace.getConfiguration(EXT_ID).get('folder') or selected_folder_path
# 35 MB. For scale, Linux kernel git graph (1 mio commits) in extension format is 538 MB or 7.4 MB for the first 15k commits
maxBuffer: 1024 * 1024 * 35
last_git_execution = Date.now()
stdout

``###* @type {vscode.WebviewPanel | null} ###
panel = null

post_message = (###* @type BridgeMessage ### msg) =>
panel?.webview.postMessage msg
log = vscode.window.createOutputChannel EXT_NAME
module.exports.log = log

# When you convert a folder into a workspace by adding another folder, the extension is de- and reactivated
# but the webview panel isn't destroyed even though we instruct it to (with subscriptions).
# This is an unresolved bug in VSCode and it seems there is nothing you can do. https://github.com/microsoft/vscode/issues/158839
module.exports.activate = (###* @type vscode.ExtensionContext ### context) =>
log.appendLine "extension activate"

post_message = (###* @type BridgeMessage ### msg) =>
log.appendLine "send to webview: "+JSON.stringify(msg) if vscode.workspace.getConfiguration(EXT_ID).get('verbose-logging')
panel?.webview.postMessage msg

git = get_git log,
on_repo_external_state_change: =>
post_message
type: 'push'
id: 'repo-external-state-change'
on_repo_names_change: =>
post_message
type: 'push'
id: 'repo-names-change'
data: git.get_repo_names()

populate_panel = =>
return if not panel
view = panel.webview
view.options = { enableScripts: true, localResourceRoots: [ vscode.Uri.joinPath(context.extensionUri, 'web-dist') ] }
panel.onDidDispose =>
panel = null
panel.onDidDispose => panel = null
context.subscriptions.push panel

await refresh_folders()
selected_folder_path = context.workspaceState.get('selected_folder_path') or ''
if not folders.some (folder) => folder.path == selected_folder_path
selected_folder_path = folders[0]?.path or ''
restart_index_watcher()
git.set_selected_repo_index(context.workspaceState.get('selected_repo_index') or 0)

view.onDidReceiveMessage (###* @type BridgeMessage ### message) =>
log.appendLine "receive from webview: "+JSON.stringify(message) if vscode.workspace.getConfiguration(EXT_ID).get('verbose-logging')
d = message.data
h = (###* @type {() => any} ### func) =>
``###* @type BridgeMessage ###
Expand All @@ -95,7 +61,7 @@ module.exports.activate = (###* @type vscode.ExtensionContext ### context) =>
when 'request'
switch message.command
when 'git' then h =>
git d
git.run d
when 'show-error-message' then h =>
vscode.window.showErrorMessage d
when 'show-information-message' then h =>
Expand All @@ -114,15 +80,14 @@ module.exports.activate = (###* @type vscode.ExtensionContext ### context) =>
vscode.commands.executeCommand 'vscode.diff', uri_1, uri_2, "#{d.filename} #{d.hashes[0]} vs. #{d.hashes[1]}"
when 'get-config' then h =>
vscode.workspace.getConfiguration(EXT_ID).get d
when 'get-folder-names' then h =>
folders.map (f) => f.name
when 'set-selected-folder-index' then h =>
selected_folder_path = folders[Number(d)]?.path or ''
context.workspaceState.update 'selected_folder_path', selected_folder_path
restart_index_watcher()
when 'get-selected-folder-index' then h =>
folders.findIndex (folder) =>
folder.path == selected_folder_path
when 'get-repo-names' then h =>
git.get_repo_names()
when 'set-selected-repo-index' then h =>
index = Number(d) or 0
context.workspaceState.update 'selected_repo_index', index
git.set_selected_repo_index(index)
when 'get-selected-repo-index' then h =>
git.get_selected_repo_index()
vscode.workspace.onDidChangeConfiguration (event) =>
if event.affectsConfiguration EXT_ID
post_message
Expand Down Expand Up @@ -170,19 +135,20 @@ module.exports.activate = (###* @type vscode.ExtensionContext ### context) =>

context.subscriptions.push vscode.workspace.registerTextDocumentContentProvider "#{EXT_ID}-git-show",
provideTextDocumentContent: (uri) ->
(try await git "show \"#{uri.path}\"") or ''
(try await git.run "show \"#{uri.path}\"") or ''

context.subscriptions.push vscode.commands.registerCommand START_CMD, =>
if panel
panel.reveal()
return
log.appendLine "start command"
return panel.reveal() if panel
panel = vscode.window.createWebviewPanel(EXT_ID, EXT_NAME, vscode.window.activeTextEditor?.viewColumn or 1, { retainContextWhenHidden: true })
panel.iconPath = vscode.Uri.joinPath(context.extensionUri, "logo.png")
populate_panel()

# This bit is needed so the webview can keep open around restarts
vscode.window.registerWebviewPanelSerializer EXT_ID,
deserializeWebviewPanel: (deserialized_panel) ->
panel = deserialized_panel
log.appendLine "deserialize web panel (rebuild editor tab from last session)"
await populate_panel()
undefined

Expand All @@ -191,4 +157,7 @@ module.exports.activate = (###* @type vscode.ExtensionContext ### context) =>
context.subscriptions.push status_bar_item
status_bar_item.text = "$(git-branch) Git Log"
status_bar_item.tooltip = "Open up the main view of the git-log--graph extension"
status_bar_item.show()
status_bar_item.show()

module.exports.deactivate = =>
log.appendLine("extension deactivate")
77 changes: 77 additions & 0 deletions src/git.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
vscode = require 'vscode'
util = require('util')
{ basename } = require('path')
exec = util.promisify(require('child_process').exec)
VSCodeGit = require('./vscode.git')

``###*
# @param log {vscode.OutputChannel}
# @param args {{on_repo_external_state_change:()=>any, on_repo_names_change:()=>any}}
###
module.exports.get_git = (log, { on_repo_external_state_change, on_repo_names_change }) =>
#
###* @type {VSCodeGit.API} ###
api = vscode.extensions.getExtension('vscode.git')?.exports.getAPI(1) or throw 'VSCode official Git Extension not found, did you disable it?'
last_git_execution = 0

``###* @type {Record<string,string>} ###
repo_state_cache = {}
start_observing_repo = (###* @type {VSCodeGit.Repository} ### repo) =>
log.appendLine "start observing repo "+repo.rootUri.fsPath
repo.state.onDidChange =>
# There's no event info available so we need to compare. (https://github.com/microsoft/vscode/issues/142313#issuecomment-1056939973)
state_cache = [repo.state.workingTreeChanges.map((c)=>c.uri.fsPath).join(','), repo.state.mergeChanges.map((c)=>c.uri.fsPath).join(','), repo.state.indexChanges.map((c)=>c.uri.fsPath).join(','), repo.state.HEAD?.commit].join(';')
is_initial_change = not repo_state_cache[repo.rootUri.fsPath]
return if repo_state_cache[repo.rootUri.fsPath] == state_cache
repo_state_cache[repo.rootUri.fsPath] = state_cache
return if is_initial_change
# Changes done via interface already do a refresh afterwards, prevent a second one.
# This could probably be done a bit more elegantly though...
return if Date.now() - last_git_execution < 1500
# We have to observe all repos even if they aren't the selected one because
# there is no apparent way to unsubscribe from repo state changes. So filter:
return if api.repositories.findIndex((r)=>r.rootUri.path==repo.rootUri.path) != selected_repo_index
log.appendLine 'repo watcher: external index/head change' # from external, e.g. cli or committed via vscode ui
on_repo_external_state_change()

``###* @type {VSCodeGit.Repository[]} ###
repos_cache = []
``###* @type {NodeJS.Timeout|null} ###
repos_changed_debouncer = null
repos_changed = =>
# onDidOpenRepository fires multiple times. At first, there isn't even a repos change..
return if api.repositories.length == repos_cache.length
clearTimeout repos_changed_debouncer if repos_changed_debouncer
repos_changed_debouncer = setTimeout (=>
log.appendLine 'workspace: repo(s) added/removed'
api.repositories
.filter (repo) => not repos_cache.includes(repo)
.forEach (repo) =>
start_observing_repo repo
repos_cache = api.repositories.slice()
on_repo_names_change()
), 200
api.onDidOpenRepository repos_changed
api.onDidCloseRepository repos_changed
api.onDidChangeState repos_changed
do repos_changed

selected_repo_index = 0
{
get_repo_names: =>
api.repositories.map (f) => basename f.rootUri.path
run: (###* @type string ### args) =>
repo = api.repositories[selected_repo_index]
throw 'No repository found/selected' if not repo
{ stdout, stderr: _ } = await exec 'git ' + args,
cwd: repo.rootUri.fsPath
# 35 MB. For scale, Linux kernel git graph (1 mio commits) in extension format
# is 538 MB or 7.4 MB for the first 15k commits
maxBuffer: 1024 * 1024 * 35
last_git_execution = Date.now()
stdout
set_selected_repo_index: (###* @type number ### index) =>
log.appendLine "set selected repo index "+index
selected_repo_index = index
get_selected_repo_index: => selected_repo_index
}
Loading

0 comments on commit a5cfa38

Please sign in to comment.