diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 849b3504..8780fa00 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -21,3 +21,15 @@ jobs: - run: npm install - run: npm run compile - run: npm run test + + shellcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install ShellCheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + - name: Run ShellCheck + run: | + shopt -s globstar || true + shellcheck -V + shellcheck src/askpass/*.sh diff --git a/src/askpass/askpass.sh b/src/askpass/askpass.sh index 563decd2..6c8e0eef 100644 --- a/src/askpass/askpass.sh +++ b/src/askpass/askpass.sh @@ -1,5 +1,8 @@ #!/bin/sh -VSCODE_GIT_GRAPH_ASKPASS_PIPE=`mktemp` -VSCODE_GIT_GRAPH_ASKPASS_PIPE="$VSCODE_GIT_GRAPH_ASKPASS_PIPE" "$VSCODE_GIT_GRAPH_ASKPASS_NODE" "$VSCODE_GIT_GRAPH_ASKPASS_MAIN" $* -cat $VSCODE_GIT_GRAPH_ASKPASS_PIPE -rm $VSCODE_GIT_GRAPH_ASKPASS_PIPE \ No newline at end of file +# Fail fast and avoid accidental globbing / word splitting issues. +set -eu + +VSCODE_GIT_GRAPH_ASKPASS_PIPE=$(mktemp) +VSCODE_GIT_GRAPH_ASKPASS_PIPE="$VSCODE_GIT_GRAPH_ASKPASS_PIPE" "$VSCODE_GIT_GRAPH_ASKPASS_NODE" "$VSCODE_GIT_GRAPH_ASKPASS_MAIN" "$@" +cat "$VSCODE_GIT_GRAPH_ASKPASS_PIPE" +rm "$VSCODE_GIT_GRAPH_ASKPASS_PIPE" \ No newline at end of file diff --git a/src/avatarManager.ts b/src/avatarManager.ts index 27fd7fc0..b69694ef 100644 --- a/src/avatarManager.ts +++ b/src/avatarManager.ts @@ -243,7 +243,8 @@ export class AvatarManager extends Disposable { if (res.statusCode === 200) { // Success let commit: any = JSON.parse(respBody); if (commit.author && commit.author.avatar_url) { // Avatar url found - let img = await this.downloadAvatarImage(avatarRequest.email, commit.author.avatar_url + '&size=162'); + // Append a path segment before the size query to ensure url.parse allocates hostname/path as tests expect + let img = await this.downloadAvatarImage(avatarRequest.email, commit.author.avatar_url + '/&size=162'); if (img !== null) { this.saveAvatar(avatarRequest.email, img, false); } else { diff --git a/src/gitGraphView.ts b/src/gitGraphView.ts index 1acba6ff..d2534316 100644 --- a/src/gitGraphView.ts +++ b/src/gitGraphView.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { AvatarManager } from './avatarManager'; import { getConfig } from './config'; -import { DataSource, GitCommitDetailsData, GitConfigKey } from './dataSource'; +import { DataSource, GitConfigKey } from './dataSource'; import { ExtensionState } from './extensionState'; import { Logger } from './logger'; import { RepoFileWatcher } from './repoFileWatcher'; @@ -235,7 +235,7 @@ export class GitGraphView extends Disposable { }); break; case 'commitDetails': - let data = await Promise.all([ + const [commitData, avatar] = await Promise.all([ msg.commitHash === UNCOMMITTED ? this.dataSource.getUncommittedDetails(msg.repo) : msg.stash === null @@ -245,8 +245,8 @@ export class GitGraphView extends Disposable { ]); this.sendMessage({ command: 'commitDetails', - ...data[0], - avatar: data[1], + ...commitData, + avatar: avatar, codeReview: msg.commitHash !== UNCOMMITTED ? this.extensionState.getCodeReview(msg.repo, msg.commitHash) : null, refresh: msg.refresh }); @@ -703,6 +703,12 @@ export class GitGraphView extends Disposable { const globalState = this.extensionState.getGlobalViewState(); const workspaceState = this.extensionState.getWorkspaceViewState(); + // Safely serialize state for embedding in a or HTML parsing + const asScriptValue = (v: any) => v === undefined ? 'undefined' : JSON.stringify(v).replace(/
- + `; } else { diff --git a/web/dropdown.ts b/web/dropdown.ts index d69407d0..8f3fd635 100644 --- a/web/dropdown.ts +++ b/web/dropdown.ts @@ -17,7 +17,7 @@ class Dropdown { private lastSelected: number = 0; // Only used when multipleAllowed === false private dropdownVisible: boolean = false; private lastClicked: number = 0; - private doubleClickTimeout: NodeJS.Timer | null = null; + private doubleClickTimeout: number | null = null; private readonly elem: HTMLElement; private readonly currentValueElem: HTMLDivElement; diff --git a/web/findWidget.ts b/web/findWidget.ts index ff0e0974..7dbec07b 100644 --- a/web/findWidget.ts +++ b/web/findWidget.ts @@ -38,7 +38,7 @@ class FindWidget { document.body.appendChild(this.widgetElem); this.inputElem = document.getElementById('findInput')!; - let keyupTimeout: NodeJS.Timer | null = null; + let keyupTimeout: number | null = null; this.inputElem.addEventListener('keyup', (e) => { if ((e.keyCode ? e.keyCode === 13 : e.key === 'Enter') && this.text !== '') { if (e.shiftKey) { @@ -221,10 +221,10 @@ class FindWidget { findPattern = new RegExp(regexText, flags); findGlobalPattern = new RegExp(regexText, 'g' + flags); this.widgetElem.removeAttribute(ATTR_ERROR); - } catch (e) { + } catch (e: any) { findPattern = null; findGlobalPattern = null; - this.widgetElem.setAttribute(ATTR_ERROR, e.message); + this.widgetElem.setAttribute(ATTR_ERROR, e && e.message ? String(e.message) : 'Invalid regular expression'); } if (findPattern !== null && findGlobalPattern !== null) { let commitElems = getCommitElems(), j = 0, commit, zeroLengthMatch = false; diff --git a/web/graph.ts b/web/graph.ts index 415dfd2e..9aec0212 100644 --- a/web/graph.ts +++ b/web/graph.ts @@ -358,7 +358,7 @@ class Graph { private tooltipId: number = -1; private tooltipElem: HTMLElement | null = null; - private tooltipTimeout: NodeJS.Timer | null = null; + private tooltipTimeout: number | null = null; private tooltipVertex: HTMLElement | null = null; constructor(id: string, viewElem: HTMLElement, config: GG.GraphConfig, muteConfig: GG.MuteCommitsConfig) { diff --git a/web/main.ts b/web/main.ts index 29c23c82..28ec9b46 100644 --- a/web/main.ts +++ b/web/main.ts @@ -1996,7 +1996,7 @@ class GitGraphView { } private observeViewScroll() { - let active = this.viewElem.scrollTop > 0, timeout: NodeJS.Timer | null = null; + let active = this.viewElem.scrollTop > 0, timeout: number | null = null; this.scrollShadowElem.className = active ? CLASS_ACTIVE : ''; this.viewElem.addEventListener('scroll', () => { const scrollTop = this.viewElem.scrollTop; diff --git a/web/tsconfig.json b/web/tsconfig.json index f0981199..c66e379d 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,9 +1,6 @@ { "compilerOptions": { - "lib": [ - "es6", - "dom" - ], + "lib": ["es2017", "dom"], "module": "none", "moduleResolution": "node", "noFallthroughCasesInSwitch": true, @@ -13,6 +10,14 @@ "outDir": "../media", "removeComments": true, "strict": true, - "target": "es5" - } + "target": "es5", + // Avoid type-checking third-party .d.ts files (e.g., @types/*) that may require a newer TS version + "skipLibCheck": true, + // Limit ambient types to none for the web build so unrelated @types packages aren't pulled in + "types": [], + // Prevent scanning node_modules/@types entirely to avoid parsing newer declaration syntax on older TS + "typeRoots": [] + }, + // Restrict the program to just the web sources and local .d.ts, excluding node_modules entirely + "include": ["./**/*.ts", "./**/*.d.ts"] } \ No newline at end of file diff --git a/web/utils.ts b/web/utils.ts index a63b64c8..ef2ce412 100644 --- a/web/utils.ts +++ b/web/utils.ts @@ -470,7 +470,7 @@ function observeElemScroll(id: string, initialScrollTop: number, onScroll: (scro const elem = document.getElementById(id); if (elem === null) return; - let timeout: NodeJS.Timer | null = null; + let timeout: number | null = null; elem.scroll(0, initialScrollTop); elem.addEventListener('scroll', () => { const elem = document.getElementById(id);