Merge branch 'main' of github.com:tjgurwara99/vscode-codeql
This commit is contained in:
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -11,7 +11,7 @@ updates:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: ".github"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "thursday" # Thursday is arbitrary
|
||||
|
||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v1
|
||||
uses: actions/dependency-review-action@v3
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -116,7 +116,7 @@ jobs:
|
||||
perl -i -pe 's/^/## \[UNRELEASED\]\n\n/ if($.==3)' CHANGELOG.md
|
||||
|
||||
- name: Create version bump PR
|
||||
uses: peter-evans/create-pull-request@c7f493a8000b8aeb17a1332e326ba76b57cb83eb # v3.4.1
|
||||
uses: peter-evans/create-pull-request@2b011faafdcbc9ceb11414d64d0573f37c774b04 # v4.2.3
|
||||
if: success()
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"args": [
|
||||
"--projects",
|
||||
"test"
|
||||
"test/unit-tests"
|
||||
],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
@@ -94,7 +94,7 @@
|
||||
"cwd": "${workspaceFolder}/extensions/ql-vscode",
|
||||
"args": [
|
||||
"--projects",
|
||||
"src/vscode-tests/no-workspace"
|
||||
"test/vscode-tests/no-workspace"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"console": "integratedTerminal",
|
||||
@@ -110,7 +110,7 @@
|
||||
"cwd": "${workspaceFolder}/extensions/ql-vscode",
|
||||
"args": [
|
||||
"--projects",
|
||||
"src/vscode-tests/minimal-workspace"
|
||||
"test/vscode-tests/minimal-workspace"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"console": "integratedTerminal",
|
||||
@@ -126,7 +126,7 @@
|
||||
"cwd": "${workspaceFolder}/extensions/ql-vscode",
|
||||
"args": [
|
||||
"--projects",
|
||||
"src/vscode-tests/cli-integration"
|
||||
"test/vscode-tests/cli-integration"
|
||||
],
|
||||
"env": {
|
||||
// Optionally, set the version to use for the integration tests.
|
||||
|
||||
@@ -95,14 +95,17 @@ More information about Storybook can be found inside the **Overview** page once
|
||||
|
||||
We have several types of tests:
|
||||
|
||||
* Unit tests: these live in the `tests/pure-tests/` directory
|
||||
* Unit tests: these live in the `tests/unit-tests/` directory
|
||||
* View tests: these live in `src/view/variant-analysis/__tests__/`
|
||||
* VSCode integration tests: these live in `src/vscode-tests/no-workspace` and `src/vscode-tests/minimal-workspace`
|
||||
* CLI integration tests: these live in `src/vscode-tests/cli-integration`
|
||||
* VSCode integration tests:
|
||||
* `test/vscode-tests/no-workspace` tests: These are intended to cover functionality that is meant to work before you even have a workspace open.
|
||||
* `test/vscode-tests/minimal-workspace` tests: These are intended to cover functionality that need a workspace but don't require the full extension to be activated.
|
||||
* CLI integration tests: these live in `test/vscode-tests/cli-integration`
|
||||
* These tests are intendended to be cover functionality that is related to the integration between the CodeQL CLI and the extension.
|
||||
|
||||
The CLI integration tests require an instance of the CodeQL CLI to run so they will require some extra setup steps. When adding new tests to our test suite, please be mindful of whether they need to be in the cli-integration folder. If the tests don't depend on the CLI, they are better suited to being a VSCode integration test.
|
||||
|
||||
Any test data you're using (sample projects, config files, etc.) must go in a `src/vscode-tests/*/data` directory. When you run the tests, the test runner will copy the data directory to `out/vscode-tests/*/data`.
|
||||
Any test data you're using (sample projects, config files, etc.) must go in a `test/vscode-tests/*/data` directory. When you run the tests, the test runner will copy the data directory to `out/vscode-tests/*/data`.
|
||||
|
||||
#### Running the tests
|
||||
|
||||
@@ -155,16 +158,16 @@ The CLI integration tests require the CodeQL standard libraries in order to run
|
||||
##### 1. From the terminal
|
||||
|
||||
The easiest way to run a single test is to change the `it` of the test to `it.only` and then run the test command with some additional options
|
||||
to only run tests for this specific file. For example, to run the test `src/vscode-tests/cli-integration/run-queries.test.ts`:
|
||||
to only run tests for this specific file. For example, to run the test `test/vscode-tests/cli-integration/run-queries.test.ts`:
|
||||
|
||||
```shell
|
||||
npm run cli-integration -- --runTestsByPath src/vscode-tests/cli-integration/run-queries.test.ts
|
||||
npm run cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts
|
||||
```
|
||||
|
||||
You can also use the `--testNamePattern` option to run a specific test within a file. For example, to run the test `src/vscode-tests/cli-integration/run-queries.test.ts`:
|
||||
You can also use the `--testNamePattern` option to run a specific test within a file. For example, to run the test `test/vscode-tests/cli-integration/run-queries.test.ts`:
|
||||
|
||||
```shell
|
||||
npm run cli-integration -- --runTestsByPath src/vscode-tests/cli-integration/run-queries.test.ts --testNamePattern "should create a QueryEvaluationInfo"
|
||||
npm run cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts --testNamePattern "should create a QueryEvaluationInfo"
|
||||
```
|
||||
|
||||
##### 2. From VSCode
|
||||
@@ -221,6 +224,7 @@ Pre-recorded scenarios are stored in `./src/mocks/scenarios`. However, it's poss
|
||||
|
||||
## Releasing (write access required)
|
||||
|
||||
1. Go through [our test plan](/extensions/ql-vscode/docs/test-plan.md) to ensure that the extension is working as expected.
|
||||
1. Double-check the `CHANGELOG.md` contains all desired change comments and has the version to be released with date at the top.
|
||||
* Go through all recent PRs and make sure they are properly accounted for.
|
||||
* Make sure all changelog entries have links back to their PR(s) if appropriate.
|
||||
|
||||
@@ -3,7 +3,7 @@ module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: "module",
|
||||
project: ["tsconfig.json", "./src/**/tsconfig.json", "./gulpfile.ts/tsconfig.json", "./scripts/tsconfig.json", "./.storybook/tsconfig.json"],
|
||||
project: ["tsconfig.json", "./src/**/tsconfig.json", "./test/**/tsconfig.json", "./gulpfile.ts/tsconfig.json", "./scripts/tsconfig.json", "./.storybook/tsconfig.json"],
|
||||
},
|
||||
plugins: [
|
||||
"github",
|
||||
|
||||
@@ -14,3 +14,4 @@ gulpfile.js/**
|
||||
tsconfig.json
|
||||
.prettierrc
|
||||
vsc-extension-quickstart.md
|
||||
node_modules/**
|
||||
|
||||
BIN
extensions/ql-vscode/docs/images/highlighted-code-snippet.png
Normal file
BIN
extensions/ql-vscode/docs/images/highlighted-code-snippet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
extensions/ql-vscode/docs/images/results-table.png
Normal file
BIN
extensions/ql-vscode/docs/images/results-table.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
280
extensions/ql-vscode/docs/test-plan.md
Normal file
280
extensions/ql-vscode/docs/test-plan.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# Test Plan
|
||||
|
||||
This document describes the manual test plan for the QL extension for Visual Studio Code.
|
||||
|
||||
The plan will be executed manually to start with but the goal is to eventually automate parts of the process (based on
|
||||
effort vs value basis).
|
||||
|
||||
#### What this doesn't cover
|
||||
We don't need to test features (and permutations of features) that are covered by automated tests.
|
||||
|
||||
### Before releasing the VS Code extension
|
||||
- Go through the required test cases listed below
|
||||
- Check major PRs since the previous release for specific one-off things to test. Based on that, you might want to
|
||||
choose to go through some of the Optional Test Cases.
|
||||
- Run a query using the existing version of the extension (to generate an "old" query history item)
|
||||
|
||||
## Required Test Cases
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
- Flip the `codeQL.canary` flag. This will enable MRVA in the extension.
|
||||
|
||||
### Test Case 1: MRVA - Running a problem path query and viewing results
|
||||
|
||||
1. Open the [UnsafeJQueryPlugin query](https://github.com/github/codeql/blob/main/javascript/ql/src/Security/CWE-079/UnsafeJQueryPlugin.ql).
|
||||
2. Run a MRVA against the following repo list:
|
||||
```
|
||||
"test-repo-list": [
|
||||
"angular-cn/ng-nice",
|
||||
"apache/hadoop",
|
||||
"apache/hive"
|
||||
]
|
||||
```
|
||||
3. Check that a notification message pops up and the results view is opened.
|
||||
4. Check the query history. It should:
|
||||
- Show that an item has been added to the query history
|
||||
- The item should be marked as "in progress".
|
||||
5. Once the query starts:
|
||||
- Check the results view
|
||||
- Check the code paths view, including the code paths drop down menu.
|
||||
- Check that the repository filter box works
|
||||
- Click links to files/locations on GitHub
|
||||
- Check that the query history item is updated to show the number of results
|
||||
6. Once the query completes:
|
||||
- Check that the query history item is updated to show the query status as "complete"
|
||||
|
||||
### Test Case 2: MRVA - Running a problem query and viewing results
|
||||
|
||||
1. Open the [ReDoS query](https://github.com/github/codeql/blob/main/javascript/ql/src/Performance/ReDoS.ql).
|
||||
2. Run a MRVA against the "Top 10" repositories.
|
||||
3. Check the notification message. It should:
|
||||
- Show the number of repos that are going to be queried
|
||||
- Provide a link to the actions workflow
|
||||
4. Check the query history. It should:
|
||||
- Show that an item has been added to the query history
|
||||
- The item should be marked as "in progress".
|
||||
5. Once the query starts:
|
||||
- Check that a notification is shown with a link to the results view
|
||||
- Check that the results are rendered with an alert message and a highlighted code snippet:
|
||||

|
||||
|
||||
### Test Case 3: MRVA - Running a non-problem query and viewing results
|
||||
|
||||
1. Open the [FunLinesOfCode query](https://github.com/github/codeql/blob/main/cpp/ql/src/Metrics/Functions/FunLinesOfCode.ql).
|
||||
2. Run a MRVA against a single repository (e.g. `google/brotli`).
|
||||
3. Once the query starts:
|
||||
- Open the query results
|
||||
- Check that the results show up in a table:
|
||||

|
||||
|
||||
### Test Case 4: MRVA - Interacting with query history
|
||||
|
||||
1. Click a history item (for MRVA):
|
||||
- Check that exporting results works
|
||||
- Check that sorting results works
|
||||
- Check that copying repo lists works
|
||||
2. Open the query directory (containing results):
|
||||
- Check that the correct directory is opened and there are results in it
|
||||
3. Open variant analysis on GitHub
|
||||
- Check that the correct workflow is opened
|
||||
|
||||
### Test Case 5: MRVA - Canceling a variant analysis run
|
||||
|
||||
Run one of the above MRVAs, but cancel it from within VS Code:
|
||||
- Check that the query is canceled and the query history item is updated.
|
||||
- Check that the workflow run is also canceled.
|
||||
- Check that any available results are visible in VS Code.
|
||||
|
||||
### Test Case 6: MRVA - Change to a different colour theme
|
||||
|
||||
Open one of the above MRVAs, try changing to a different colour theme and check that everything looks sensible.
|
||||
Are there any components that are not showing up?
|
||||
|
||||
## Optional Test Cases
|
||||
|
||||
These are mostly aimed at MRVA, but some of them are also applicable to non-MRVA queries.
|
||||
|
||||
### Selecting repositories to run on
|
||||
|
||||
#### Test case 1: Running a query on a single repository
|
||||
1. When the repository exists and is public
|
||||
1. Has a CodeQL database for the correct language
|
||||
2. Has a CodeQL database for another language
|
||||
3. Does not have any CodeQL databases
|
||||
2. When the repository exists and is private
|
||||
1. Is accessible and has a CodeQL database
|
||||
2. Is not accessible
|
||||
3. When the repository does not exist
|
||||
|
||||
#### Test case 2: Running a query on a custom repository list
|
||||
1. The repository list is non-empty
|
||||
1. All repositories in the list have a CodeQL database
|
||||
2. Some but not all repositories in the list have a CodeQL database
|
||||
3. No repositories in the list have a CodeQL database
|
||||
2. The repository list is empty
|
||||
|
||||
#### Test case 3: Running a query on all repositories in an organization
|
||||
1. The org exists
|
||||
1. The org contains repositories that have CodeQL databases
|
||||
2. The org contains repositories of the right language but without CodeQL databases
|
||||
3. The org contains repositories not of the right language
|
||||
4. The org contains private repositories that are inaccessible
|
||||
2. The org does not exist
|
||||
|
||||
### Using different types of controller repos
|
||||
|
||||
#### Test case 1: Running a query when the controller repository is public
|
||||
1. Can run queries on public repositories
|
||||
2. Can not run queries on private repositories
|
||||
|
||||
#### Test case 2: Running a query when the controller repository is private
|
||||
1. Can run queries on public repositories
|
||||
2. Can run queries on private repositories
|
||||
|
||||
#### Test case 3: Running a query when the controller repo exists but you do not have write access
|
||||
1. Cannot run queries
|
||||
|
||||
#### Test case 4: Running a query when the controller repo doesn’t exist
|
||||
1. Cannot run queries
|
||||
|
||||
#### Test case 5: Running a query when the "config field" for the controller repo is not set
|
||||
1. Cannot run queries
|
||||
|
||||
### Query History
|
||||
|
||||
This requires running a MRVA query and viewing the query history.
|
||||
|
||||
The first test case specifies actions that you can do when the query is first run and is in "pending" state. We start
|
||||
with this since it has quite a limited number of actions you can do.
|
||||
|
||||
#### Test case 1: When variant analysis state is "pending"
|
||||
1. Starts monitoring variant analysis
|
||||
2. Cannot open query history item
|
||||
3. Can delete a query history item
|
||||
1. Item is removed from list in UI
|
||||
2. Files on dist are deleted (can get to files using "open query directory")
|
||||
4. Can sort query history items
|
||||
1. By name
|
||||
2. By query date
|
||||
3. By result count
|
||||
5. Cannot open query directory
|
||||
6. Can open query that produced these results
|
||||
1. When the file still exists and has not moved
|
||||
2. When the file does not exist
|
||||
7. Cannot open variant analysis on github
|
||||
8. Cannot copy repository list
|
||||
9. Cannot export results
|
||||
10. Cannot select to create a gist
|
||||
11. Cannot select to save as markdown
|
||||
12. Cannot cancel analysis
|
||||
|
||||
#### Test case 2: When the variant analysis state is not "pending"
|
||||
1. Query history is loaded when VSCode starts
|
||||
2. Handles when action workflow was canceled while VSCode was closed
|
||||
3. Can open query history item
|
||||
1. Manually by clicking on them
|
||||
2. Automatically when VSCode starts (if they were open when VSCode was last used)
|
||||
4. Can delete a query history item
|
||||
1. Item is removed from list in UI
|
||||
2. Files on dist are deleted (can get to files using "open query directory")
|
||||
5. Can sort query history items
|
||||
1. By name
|
||||
2. By query date
|
||||
3. By result count
|
||||
6. Can open query directory
|
||||
7. Can open query that produced these results
|
||||
1. When the file still exists and has not moved
|
||||
2. When the file does not exist
|
||||
8. Can open variant analysis on github
|
||||
9. Can copy repository list
|
||||
1. Text is copied to clipboard
|
||||
2. Text is a valid repository list
|
||||
10. Can export results
|
||||
11. Can select to create gist
|
||||
1. A gist is created
|
||||
2. The first thing in the gist is a summary
|
||||
3. Contains a file for each repository with results
|
||||
4. A popup links you to the gist
|
||||
12. Can select to save as markdown
|
||||
1. A directory is created on disk
|
||||
2. Contains a summary file
|
||||
3. Contains a file for each repository with results
|
||||
4. A popup allows you to open the directory
|
||||
|
||||
#### Test case 3: When variant analysis state is "in_progress"
|
||||
1. Starts monitoring variant analysis
|
||||
1. Ready results are downloaded
|
||||
2. Can cancel analysis
|
||||
1. Causes the actions run to be canceled
|
||||
|
||||
#### Test case 4: When variant analysis state is in final state ("succeeded"/"failed"/"canceled")
|
||||
1. Stops monitoring variant analysis
|
||||
1. All results are downloaded if state is succeeded
|
||||
2. Otherwise, ready results are downloaded, if any are available
|
||||
2. Cannot cancel analysis
|
||||
|
||||
### MRVA results view
|
||||
|
||||
This requires running a MRVA query and seeing the results view.
|
||||
|
||||
#### Test case 1: When variant analysis state is "pending"
|
||||
1. Can open a results view
|
||||
2. Results view opens automatically
|
||||
- When starting variant analysis run
|
||||
- When VSCode opens (if view was open when VSCode was closed)
|
||||
3. Results view is empty
|
||||
|
||||
#### Test case 2: When variant analysis state is not "pending"
|
||||
1. Can open a results view
|
||||
2. Results view opens automatically
|
||||
1. When starting variant analysis run
|
||||
2. When VSCode opens (if view was open when VSCode was closed)
|
||||
3. Can copy repository list
|
||||
1. Text is copied to clipboard
|
||||
2. Text is a valid repository list
|
||||
4. Can export results
|
||||
1. Only includes repos that you have selected (also see section from query history)
|
||||
5. Can cancel analysis
|
||||
6. Can open query file
|
||||
1. When the file still exists and has not moved
|
||||
2. When the file does not exist
|
||||
7. Can open query text
|
||||
8. Can sort repos
|
||||
1. By name
|
||||
2. By results
|
||||
3. By stars
|
||||
4. By last commit
|
||||
9. Can filter repos
|
||||
10. Shows correct statistics
|
||||
1. Total number of results
|
||||
2. Total number of repositories
|
||||
3. Duration
|
||||
11. Can see live results
|
||||
1. Results appear in extension as soon as each query is completed
|
||||
12. Can view interpreted results (i.e. for a "problem" query)
|
||||
1. Can view non-path results
|
||||
2. Can view code paths for "path-problem" queries
|
||||
13. Can view raw results (i.e. for a non "problem" query)
|
||||
1. Renders a table
|
||||
14. Can see skipped repositories
|
||||
1. Can see repos with no db in a tab
|
||||
1. Shown warning that explains the tab
|
||||
2. Can see repos with no access in a tab
|
||||
1. Shown warning that explains the tab
|
||||
3. Only shows tab when there are skipped repos
|
||||
15. Result downloads
|
||||
1. All results are downloaded automatically
|
||||
2. Download status is indicated by a spinner (Not currently any indication of progress beyond "downloading" and "not downloading")
|
||||
3. Only 3 items are downloaded at a time
|
||||
4. Results for completed queries are still downloaded when
|
||||
1. Some but not all queries failed
|
||||
2. The variant analysis was canceled after some queries completed
|
||||
|
||||
#### Test case 3: When variant analysis state is in "succeeded" state
|
||||
1. Can view logs
|
||||
2. All results are downloaded
|
||||
|
||||
#### Test case 4: When variant analysis is in "failed" or "canceled" state
|
||||
1. Can view logs
|
||||
1. Results for finished queries are still downloaded.
|
||||
@@ -13,7 +13,7 @@ export function injectAppInsightsKey() {
|
||||
}
|
||||
|
||||
// replace the key
|
||||
return src(["out/telemetry.js"])
|
||||
return src(["out/extension.js"])
|
||||
.pipe(replace(/REPLACE-APP-INSIGHTS-KEY/, process.env.APP_INSIGHTS_KEY))
|
||||
.pipe(dest("out/"));
|
||||
}
|
||||
|
||||
@@ -22,21 +22,27 @@ const packageFiles = [
|
||||
"language-configuration.json",
|
||||
"snippets.json",
|
||||
"media",
|
||||
"node_modules",
|
||||
"out",
|
||||
"workspace-databases-schema.json",
|
||||
];
|
||||
|
||||
async function copyDirectory(
|
||||
sourcePath: string,
|
||||
destPath: string,
|
||||
): Promise<void> {
|
||||
console.log(`copying ${sourcePath} to ${destPath}`);
|
||||
await copy(sourcePath, destPath);
|
||||
}
|
||||
|
||||
async function copyPackage(
|
||||
sourcePath: string,
|
||||
destPath: string,
|
||||
): Promise<void> {
|
||||
for (const file of packageFiles) {
|
||||
console.log(
|
||||
`copying ${resolve(sourcePath, file)} to ${resolve(destPath, file)}`,
|
||||
);
|
||||
await copy(resolve(sourcePath, file), resolve(destPath, file));
|
||||
}
|
||||
await Promise.all(
|
||||
packageFiles.map((file) =>
|
||||
copyDirectory(resolve(sourcePath, file), resolve(destPath, file)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function deployPackage(
|
||||
@@ -88,6 +94,12 @@ export async function deployPackage(
|
||||
);
|
||||
await copyPackage(sourcePath, distPath);
|
||||
|
||||
// This is necessary for vsce to know the dependencies
|
||||
await copyDirectory(
|
||||
resolve(sourcePath, "node_modules"),
|
||||
resolve(distPath, "node_modules"),
|
||||
);
|
||||
|
||||
return {
|
||||
distPath,
|
||||
name: packageJson.name,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { series, parallel } from "gulp";
|
||||
import { compileTypeScript, watchTypeScript, cleanOutput } from "./typescript";
|
||||
import { parallel, series } from "gulp";
|
||||
import {
|
||||
compileEsbuild,
|
||||
watchEsbuild,
|
||||
checkTypeScript,
|
||||
watchCheckTypeScript,
|
||||
cleanOutput,
|
||||
copyWasmFiles,
|
||||
} from "./typescript";
|
||||
import { compileTextMateGrammar } from "./textmate";
|
||||
import { copyTestData, watchTestData } from "./tests";
|
||||
import { compileView, watchView } from "./webpack";
|
||||
import { packageExtension } from "./package";
|
||||
import { injectAppInsightsKey } from "./appInsights";
|
||||
@@ -9,21 +15,25 @@ import { injectAppInsightsKey } from "./appInsights";
|
||||
export const buildWithoutPackage = series(
|
||||
cleanOutput,
|
||||
parallel(
|
||||
compileTypeScript,
|
||||
compileEsbuild,
|
||||
copyWasmFiles,
|
||||
checkTypeScript,
|
||||
compileTextMateGrammar,
|
||||
compileView,
|
||||
copyTestData,
|
||||
),
|
||||
);
|
||||
|
||||
export const watch = parallel(watchEsbuild, watchCheckTypeScript, watchView);
|
||||
|
||||
export {
|
||||
cleanOutput,
|
||||
compileTextMateGrammar,
|
||||
watchTypeScript,
|
||||
watchEsbuild,
|
||||
watchCheckTypeScript,
|
||||
watchView,
|
||||
compileTypeScript,
|
||||
copyTestData,
|
||||
watchTestData,
|
||||
compileEsbuild,
|
||||
copyWasmFiles,
|
||||
checkTypeScript,
|
||||
injectAppInsightsKey,
|
||||
compileView,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,9 @@ import { deployPackage } from "./deploy";
|
||||
import { spawn } from "child-process-promise";
|
||||
|
||||
export async function packageExtension(): Promise<void> {
|
||||
const deployedPackage = await deployPackage(resolve("package.json"));
|
||||
const deployedPackage = await deployPackage(
|
||||
resolve(__dirname, "../package.json"),
|
||||
);
|
||||
console.log(
|
||||
`Packaging extension '${deployedPackage.name}@${deployedPackage.version}'...`,
|
||||
);
|
||||
@@ -16,7 +18,7 @@ export async function packageExtension(): Promise<void> {
|
||||
`${deployedPackage.name}-${deployedPackage.version}.vsix`,
|
||||
),
|
||||
];
|
||||
const proc = spawn("./node_modules/.bin/vsce", args, {
|
||||
const proc = spawn(resolve(__dirname, "../node_modules/.bin/vsce"), args, {
|
||||
cwd: deployedPackage.distPath,
|
||||
});
|
||||
proc.childProcess.stdout!.on("data", (data) => {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { watch, src, dest } from "gulp";
|
||||
|
||||
export function copyTestData() {
|
||||
return Promise.all([copyNoWorkspaceData(), copyCliIntegrationData()]);
|
||||
}
|
||||
|
||||
export function watchTestData() {
|
||||
return watch(["src/vscode-tests/*/data/**/*"], copyTestData);
|
||||
}
|
||||
|
||||
function copyNoWorkspaceData() {
|
||||
return src("src/vscode-tests/no-workspace/data/**/*").pipe(
|
||||
dest("out/vscode-tests/no-workspace/data"),
|
||||
);
|
||||
}
|
||||
|
||||
function copyCliIntegrationData() {
|
||||
return src("src/vscode-tests/cli-integration/data/**/*").pipe(
|
||||
dest("out/vscode-tests/cli-integration/data"),
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { src, dest } from "gulp";
|
||||
import { dest, src } from "gulp";
|
||||
import { load } from "js-yaml";
|
||||
import { obj } from "through2";
|
||||
import * as PluginError from "plugin-error";
|
||||
import PluginError from "plugin-error";
|
||||
import * as Vinyl from "vinyl";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { gray, red } from "ansi-colors";
|
||||
import { dest, watch } from "gulp";
|
||||
import { init, write } from "gulp-sourcemaps";
|
||||
import * as ts from "gulp-typescript";
|
||||
import * as del from "del";
|
||||
import { dest, src, watch } from "gulp";
|
||||
import esbuild from "gulp-esbuild";
|
||||
import ts from "gulp-typescript";
|
||||
import del from "del";
|
||||
|
||||
function goodReporter(): ts.reporter.Reporter {
|
||||
return {
|
||||
@@ -35,20 +35,46 @@ export function cleanOutput() {
|
||||
: Promise.resolve();
|
||||
}
|
||||
|
||||
export function compileTypeScript() {
|
||||
return tsProject
|
||||
.src()
|
||||
.pipe(init())
|
||||
.pipe(tsProject(goodReporter()))
|
||||
export function compileEsbuild() {
|
||||
return src("./src/extension.ts")
|
||||
.pipe(
|
||||
write(".", {
|
||||
includeContent: false,
|
||||
sourceRoot: ".",
|
||||
esbuild({
|
||||
outfile: "extension.js",
|
||||
bundle: true,
|
||||
external: ["vscode", "fsevents"],
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
target: "es2020",
|
||||
sourcemap: "linked",
|
||||
sourceRoot: "..",
|
||||
loader: {
|
||||
".node": "copy",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.pipe(dest("out"));
|
||||
}
|
||||
|
||||
export function watchTypeScript() {
|
||||
watch("src/**/*.ts", compileTypeScript);
|
||||
export function watchEsbuild() {
|
||||
watch("src/**/*.ts", compileEsbuild);
|
||||
}
|
||||
|
||||
export function checkTypeScript() {
|
||||
// This doesn't actually output the TypeScript files, it just
|
||||
// runs the TypeScript compiler and reports any errors.
|
||||
return tsProject.src().pipe(tsProject(goodReporter()));
|
||||
}
|
||||
|
||||
export function watchCheckTypeScript() {
|
||||
watch("src/**/*.ts", checkTypeScript);
|
||||
}
|
||||
|
||||
export function copyWasmFiles() {
|
||||
// We need to copy this file for the source-map package to work. Without this fie, the source-map
|
||||
// package is not able to load the WASM file because we are not including the full node_modules
|
||||
// directory. In version 0.7.4, it is not possible to call SourceMapConsumer.initialize in Node environments
|
||||
// to configure the path to the WASM file. So, source-map will always load the file from `__dirname/mappings.wasm`.
|
||||
// In version 0.8.0, it may be possible to do this properly by calling SourceMapConsumer.initialize by
|
||||
// using the "browser" field in source-map's package.json to load the WASM file from a given file path.
|
||||
return src("node_modules/source-map/lib/mappings.wasm").pipe(dest("out"));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolve } from "path";
|
||||
import * as webpack from "webpack";
|
||||
import * as MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
|
||||
export const config: webpack.Configuration = {
|
||||
mode: "development",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as webpack from "webpack";
|
||||
import webpack from "webpack";
|
||||
import { config } from "./webpack.config";
|
||||
|
||||
export function compileView(cb: (err?: Error) => void) {
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
module.exports = {
|
||||
projects: [
|
||||
"<rootDir>/src/view",
|
||||
"<rootDir>/test",
|
||||
"<rootDir>/src/vscode-tests/cli-integration",
|
||||
"<rootDir>/src/vscode-tests/no-workspace",
|
||||
"<rootDir>/src/vscode-tests/minimal-workspace",
|
||||
"<rootDir>/test/unit-tests",
|
||||
"<rootDir>/test/vscode-tests/cli-integration",
|
||||
"<rootDir>/test/vscode-tests/no-workspace",
|
||||
"<rootDir>/test/vscode-tests/minimal-workspace",
|
||||
],
|
||||
};
|
||||
|
||||
1502
extensions/ql-vscode/package-lock.json
generated
1502
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@
|
||||
"onLanguage:ql",
|
||||
"onLanguage:ql-summary",
|
||||
"onView:codeQLDatabases",
|
||||
"onView:codeQLDatabasesExperimental",
|
||||
"onView:codeQLVariantAnalysisRepositories",
|
||||
"onView:codeQLQueryHistory",
|
||||
"onView:codeQLAstViewer",
|
||||
"onView:codeQLEvalLogViewer",
|
||||
@@ -59,9 +59,14 @@
|
||||
"onCommand:codeQL.chooseDatabaseGithub",
|
||||
"onCommand:codeQLDatabases.chooseDatabase",
|
||||
"onCommand:codeQLDatabases.setCurrentDatabase",
|
||||
"onCommand:codeQLDatabasesExperimental.openConfigFile",
|
||||
"onCommand:codeQLDatabasesExperimental.addNewList",
|
||||
"onCommand:codeQLDatabasesExperimental.setSelectedItem",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.openConfigFile",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.addNewDatabase",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.addNewList",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.setSelectedItem",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.setSelectedItemContextMenu",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.renameItemContextMenu",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.openOnGitHubContextMenu",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.removeItemContextMenu",
|
||||
"onCommand:codeQL.quickQuery",
|
||||
"onCommand:codeQL.restartQueryServer",
|
||||
"onWebviewPanel:resultsView",
|
||||
@@ -358,19 +363,39 @@
|
||||
"title": "CodeQL: Copy Version Information"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabasesExperimental.openConfigFile",
|
||||
"command": "codeQLVariantAnalysisRepositories.openConfigFile",
|
||||
"title": "Open Database Configuration File",
|
||||
"icon": "$(edit)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabasesExperimental.addNewList",
|
||||
"command": "codeQLVariantAnalysisRepositories.addNewDatabase",
|
||||
"title": "Add new database",
|
||||
"icon": "$(add)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.addNewList",
|
||||
"title": "Add new list",
|
||||
"icon": "$(new-folder)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabasesExperimental.setSelectedItem",
|
||||
"title": "Select Item",
|
||||
"icon": "$(circle-small-filled)"
|
||||
"command": "codeQLVariantAnalysisRepositories.setSelectedItem",
|
||||
"title": "✓"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.setSelectedItemContextMenu",
|
||||
"title": "Select"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.renameItemContextMenu",
|
||||
"title": "Rename"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.openOnGitHubContextMenu",
|
||||
"title": "Open on GitHub"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.removeItemContextMenu",
|
||||
"title": "Remove"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseFolder",
|
||||
@@ -755,17 +780,38 @@
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabasesExperimental.openConfigFile",
|
||||
"when": "view == codeQLDatabasesExperimental",
|
||||
"command": "codeQLVariantAnalysisRepositories.openConfigFile",
|
||||
"when": "view == codeQLVariantAnalysisRepositories",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabasesExperimental.addNewList",
|
||||
"when": "view == codeQLDatabasesExperimental && codeQLDatabasesExperimental.configError == false",
|
||||
"command": "codeQLVariantAnalysisRepositories.addNewDatabase",
|
||||
"when": "view == codeQLVariantAnalysisRepositories && codeQLVariantAnalysisRepositories.configError == false",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.addNewList",
|
||||
"when": "view == codeQLVariantAnalysisRepositories && codeQLVariantAnalysisRepositories.configError == false",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.removeItemContextMenu",
|
||||
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canBeRemoved/"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.setSelectedItemContextMenu",
|
||||
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canBeSelected/"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.renameItemContextMenu",
|
||||
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canBeRenamed/"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.openOnGitHubContextMenu",
|
||||
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canBeOpenedOnGitHub/"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.setCurrentDatabase",
|
||||
"group": "inline",
|
||||
@@ -792,8 +838,8 @@
|
||||
"when": "view == codeQLDatabases"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabasesExperimental.setSelectedItem",
|
||||
"when": "view == codeQLDatabasesExperimental && viewItem == selectableDbItem",
|
||||
"command": "codeQLVariantAnalysisRepositories.setSelectedItem",
|
||||
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canBeSelected/",
|
||||
"group": "inline"
|
||||
},
|
||||
{
|
||||
@@ -979,15 +1025,35 @@
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabasesExperimental.openConfigFile",
|
||||
"command": "codeQLVariantAnalysisRepositories.openConfigFile",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabasesExperimental.addNewList",
|
||||
"command": "codeQLVariantAnalysisRepositories.addNewDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabasesExperimental.setSelectedItem",
|
||||
"command": "codeQLVariantAnalysisRepositories.addNewList",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.setSelectedItem",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.setSelectedItemContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.renameItemContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.openOnGitHubContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.removeItemContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
@@ -1226,9 +1292,9 @@
|
||||
"name": "Databases"
|
||||
},
|
||||
{
|
||||
"id": "codeQLDatabasesExperimental",
|
||||
"name": "Databases",
|
||||
"when": "config.codeQL.canary && config.codeQL.newQueryRunExperience"
|
||||
"id": "codeQLVariantAnalysisRepositories",
|
||||
"name": "Variant Analysis Repositories",
|
||||
"when": "config.codeQL.canary && config.codeQL.variantAnalysis.repositoriesPanel"
|
||||
},
|
||||
{
|
||||
"id": "codeQLQueryHistory",
|
||||
@@ -1266,17 +1332,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:extension": "tsc --watch",
|
||||
"watch:webpack": "gulp watchView",
|
||||
"watch:files": "gulp watchTestData",
|
||||
"watch": "gulp watch",
|
||||
"test": "npm-run-all -p test:*",
|
||||
"test:unit": "cross-env TZ=UTC LANG=en-US jest --projects test",
|
||||
"test:unit": "cross-env TZ=UTC LANG=en-US jest --projects test/unit-tests",
|
||||
"test:view": "jest --projects src/view",
|
||||
"integration": "npm-run-all integration:*",
|
||||
"integration:no-workspace": "jest --projects src/vscode-tests/no-workspace",
|
||||
"integration:minimal-workspace": "jest --projects src/vscode-tests/minimal-workspace",
|
||||
"cli-integration": "jest --projects src/vscode-tests/cli-integration",
|
||||
"integration:no-workspace": "jest --projects test/vscode-tests/no-workspace",
|
||||
"integration:minimal-workspace": "jest --projects test/vscode-tests/minimal-workspace",
|
||||
"cli-integration": "jest --projects test/vscode-tests/cli-integration",
|
||||
"update-vscode": "node ./node_modules/vscode/bin/install",
|
||||
"format": "prettier --write **/*.{ts,tsx} && eslint . --ext .ts,.tsx --fix",
|
||||
"lint": "eslint . --ext .ts,.tsx --max-warnings=0",
|
||||
@@ -1299,13 +1362,13 @@
|
||||
"chokidar": "^3.5.3",
|
||||
"classnames": "~2.2.6",
|
||||
"d3": "^7.6.1",
|
||||
"d3-graphviz": "^2.6.1",
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"fs-extra": "^10.0.1",
|
||||
"glob-promise": "^4.2.2",
|
||||
"immutable": "^4.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimist": "~1.2.6",
|
||||
"msw": "^0.47.4",
|
||||
"msw": "^0.49.0",
|
||||
"nanoid": "^3.2.0",
|
||||
"node-fetch": "~2.6.7",
|
||||
"p-queue": "^6.0.0",
|
||||
@@ -1387,6 +1450,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "~3.1.0",
|
||||
"del": "^6.0.0",
|
||||
"esbuild": "^0.15.15",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-github": "^4.4.1",
|
||||
@@ -1398,6 +1462,7 @@
|
||||
"file-loader": "^6.2.0",
|
||||
"glob": "^7.1.4",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-esbuild": "^0.10.5",
|
||||
"gulp-replace": "^1.1.3",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
|
||||
@@ -13,102 +13,59 @@ const SCOPES = ["repo", "gist"];
|
||||
* Handles authentication to GitHub, using the VS Code [authentication API](https://code.visualstudio.com/api/references/vscode-api#authentication).
|
||||
*/
|
||||
export class Credentials {
|
||||
/**
|
||||
* A specific octokit to return, otherwise a new authenticated octokit will be created when needed.
|
||||
*/
|
||||
private octokit: Octokit.Octokit | undefined;
|
||||
|
||||
// Explicitly make the constructor private, so that we can't accidentally call the constructor from outside the class
|
||||
// without also initializing the class.
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
private constructor(octokit?: Octokit.Octokit) {
|
||||
this.octokit = octokit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an instance of credentials with an octokit instance.
|
||||
* Initializes a Credentials instance. This will generate octokit instances
|
||||
* authenticated as the user. If there is not already an authenticated GitHub
|
||||
* session available then the user will be prompted to log in.
|
||||
*
|
||||
* Do not call this method until you know you actually need an instance of credentials.
|
||||
* since calling this method will require the user to log in.
|
||||
*
|
||||
* @param context The extension context.
|
||||
* @returns An instance of credentials.
|
||||
*/
|
||||
static async initialize(
|
||||
context: vscode.ExtensionContext,
|
||||
): Promise<Credentials> {
|
||||
const c = new Credentials();
|
||||
c.registerListeners(context);
|
||||
c.octokit = await c.createOctokit(false);
|
||||
return c;
|
||||
static async initialize(): Promise<Credentials> {
|
||||
return new Credentials();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an instance of credentials with an octokit instance using
|
||||
* a token from the user's GitHub account. This method is meant to be
|
||||
* used non-interactive environments such as tests.
|
||||
* a specific known token. This method is meant to be used in
|
||||
* non-interactive environments such as tests.
|
||||
*
|
||||
* @param overrideToken The GitHub token to use for authentication.
|
||||
* @returns An instance of credentials.
|
||||
*/
|
||||
static async initializeWithToken(overrideToken: string) {
|
||||
const c = new Credentials();
|
||||
c.octokit = await c.createOctokit(false, overrideToken);
|
||||
return c;
|
||||
}
|
||||
|
||||
private async createOctokit(
|
||||
createIfNone: boolean,
|
||||
overrideToken?: string,
|
||||
): Promise<Octokit.Octokit | undefined> {
|
||||
if (overrideToken) {
|
||||
return new Octokit.Octokit({ auth: overrideToken, retry });
|
||||
}
|
||||
|
||||
const session = await vscode.authentication.getSession(
|
||||
GITHUB_AUTH_PROVIDER_ID,
|
||||
SCOPES,
|
||||
{ createIfNone },
|
||||
);
|
||||
|
||||
if (session) {
|
||||
return new Octokit.Octokit({
|
||||
auth: session.accessToken,
|
||||
retry,
|
||||
});
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
registerListeners(context: vscode.ExtensionContext): void {
|
||||
// Sessions are changed when a user logs in or logs out.
|
||||
context.subscriptions.push(
|
||||
vscode.authentication.onDidChangeSessions(async (e) => {
|
||||
if (e.provider.id === GITHUB_AUTH_PROVIDER_ID) {
|
||||
this.octokit = await this.createOctokit(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return new Credentials(new Octokit.Octokit({ auth: overrideToken, retry }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or returns an instance of Octokit.
|
||||
*
|
||||
* @param requireAuthentication Whether the Octokit instance needs to be authenticated as user.
|
||||
* @returns An instance of Octokit.
|
||||
*/
|
||||
async getOctokit(requireAuthentication = true): Promise<Octokit.Octokit> {
|
||||
async getOctokit(): Promise<Octokit.Octokit> {
|
||||
if (this.octokit) {
|
||||
return this.octokit;
|
||||
}
|
||||
|
||||
this.octokit = await this.createOctokit(requireAuthentication);
|
||||
const session = await vscode.authentication.getSession(
|
||||
GITHUB_AUTH_PROVIDER_ID,
|
||||
SCOPES,
|
||||
{ createIfNone: true },
|
||||
);
|
||||
|
||||
if (!this.octokit) {
|
||||
if (requireAuthentication) {
|
||||
throw new Error("Did not initialize Octokit.");
|
||||
}
|
||||
|
||||
// We don't want to set this in this.octokit because that would prevent
|
||||
// authenticating when requireCredentials is true.
|
||||
return new Octokit.Octokit({ retry });
|
||||
}
|
||||
return this.octokit;
|
||||
return new Octokit.Octokit({
|
||||
auth: session.accessToken,
|
||||
retry,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as sarif from "sarif";
|
||||
import { SemVer } from "semver";
|
||||
import { Readable } from "stream";
|
||||
import { StringDecoder } from "string_decoder";
|
||||
import * as tk from "tree-kill";
|
||||
import tk from "tree-kill";
|
||||
import { promisify } from "util";
|
||||
import { CancellationToken, commands, Disposable, Uri } from "vscode";
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ export function commandRunner(
|
||||
return undefined;
|
||||
} finally {
|
||||
const executionTime = Date.now() - startTime;
|
||||
telemetryListener.sendCommandUsage(commandId, executionTime, error);
|
||||
telemetryListener?.sendCommandUsage(commandId, executionTime, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -201,7 +201,7 @@ export function commandRunnerWithProgress<R>(
|
||||
return undefined;
|
||||
} finally {
|
||||
const executionTime = Date.now() - startTime;
|
||||
telemetryListener.sendCommandUsage(commandId, executionTime, error);
|
||||
telemetryListener?.sendCommandUsage(commandId, executionTime, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Disposable } from "../pure/disposable-object";
|
||||
import { AppEventEmitter } from "./events";
|
||||
import { Logger } from "./logging";
|
||||
import { Memento } from "./memento";
|
||||
|
||||
export interface App {
|
||||
createEventEmitter<T>(): AppEventEmitter<T>;
|
||||
@@ -11,6 +12,7 @@ export interface App {
|
||||
extensionPath: string;
|
||||
globalStoragePath: string;
|
||||
workspaceStoragePath?: string;
|
||||
workspaceState: Memento;
|
||||
}
|
||||
|
||||
export enum AppMode {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { OWNER_REGEX, REPO_REGEX } from "../pure/helpers-pure";
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid GitHub NWO.
|
||||
* @param identifier The GitHub NWO
|
||||
* @returns
|
||||
*/
|
||||
export function isValidGitHubNwo(identifier: string): boolean {
|
||||
return validGitHubNwoOrOwner(identifier, "nwo");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid GitHub owner.
|
||||
* @param identifier The GitHub owner
|
||||
* @returns
|
||||
*/
|
||||
export function isValidGitHubOwner(identifier: string): boolean {
|
||||
return validGitHubNwoOrOwner(identifier, "owner");
|
||||
}
|
||||
|
||||
function validGitHubNwoOrOwner(
|
||||
identifier: string,
|
||||
kind: "owner" | "nwo",
|
||||
): boolean {
|
||||
return kind === "owner"
|
||||
? OWNER_REGEX.test(identifier)
|
||||
: REPO_REGEX.test(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts an NWO from a GitHub URL.
|
||||
* @param githubUrl The GitHub repository URL
|
||||
* @return The corresponding NWO, or undefined if the URL is not valid
|
||||
*/
|
||||
export function getNwoFromGitHubUrl(githubUrl: string): string | undefined {
|
||||
return getNwoOrOwnerFromGitHubUrl(githubUrl, "nwo");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts an owner from a GitHub URL.
|
||||
* @param githubUrl The GitHub repository URL
|
||||
* @return The corresponding Owner, or undefined if the URL is not valid
|
||||
*/
|
||||
export function getOwnerFromGitHubUrl(githubUrl: string): string | undefined {
|
||||
return getNwoOrOwnerFromGitHubUrl(githubUrl, "owner");
|
||||
}
|
||||
|
||||
function getNwoOrOwnerFromGitHubUrl(
|
||||
githubUrl: string,
|
||||
kind: "owner" | "nwo",
|
||||
): string | undefined {
|
||||
try {
|
||||
const uri = new URL(githubUrl);
|
||||
if (uri.protocol !== "https:") {
|
||||
return;
|
||||
}
|
||||
if (uri.hostname !== "github.com" && uri.hostname !== "www.github.com") {
|
||||
return;
|
||||
}
|
||||
const paths = uri.pathname.split("/").filter((segment: string) => segment);
|
||||
const owner = `${paths[0]}`;
|
||||
if (kind === "owner") {
|
||||
return owner ? owner : undefined;
|
||||
}
|
||||
const nwo = `${paths[0]}/${paths[1]}`;
|
||||
return paths[1] ? nwo : undefined;
|
||||
} catch (e) {
|
||||
// Ignore the error here, since we catch failures at a higher level.
|
||||
return;
|
||||
}
|
||||
}
|
||||
44
extensions/ql-vscode/src/common/memento.ts
Normal file
44
extensions/ql-vscode/src/common/memento.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* A memento represents a storage utility. It can store and retrieve
|
||||
* values.
|
||||
*
|
||||
* It is an interface used by the VS Code API. We replicate it here
|
||||
* to avoid the dependency to the VS Code API.
|
||||
*/
|
||||
export interface Memento {
|
||||
/**
|
||||
* Returns the stored keys.
|
||||
*
|
||||
* @return The stored keys.
|
||||
*/
|
||||
keys(): readonly string[];
|
||||
|
||||
/**
|
||||
* Return a value.
|
||||
*
|
||||
* @param key A string.
|
||||
* @return The stored value or `undefined`.
|
||||
*/
|
||||
get<T>(key: string): T | undefined;
|
||||
|
||||
/**
|
||||
* Return a value.
|
||||
*
|
||||
* @param key A string.
|
||||
* @param defaultValue A value that should be returned when there is no
|
||||
* value (`undefined`) with the given key.
|
||||
* @return The stored value or the defaultValue.
|
||||
*/
|
||||
get<T>(key: string, defaultValue: T): T;
|
||||
|
||||
/**
|
||||
* Store a value. The value must be JSON-stringifyable.
|
||||
*
|
||||
* *Note* that using `undefined` as value removes the key from the underlying
|
||||
* storage.
|
||||
*
|
||||
* @param key A string.
|
||||
* @param value A value. MUST not contain cyclic references.
|
||||
*/
|
||||
update(key: string, value: any): Thenable<void>;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Disposable } from "../../pure/disposable-object";
|
||||
import { App, AppMode } from "../app";
|
||||
import { AppEventEmitter } from "../events";
|
||||
import { extLogger, Logger } from "../logging";
|
||||
import { Memento } from "../memento";
|
||||
import { VSCodeAppEventEmitter } from "./events";
|
||||
|
||||
export class ExtensionApp implements App {
|
||||
@@ -22,6 +23,10 @@ export class ExtensionApp implements App {
|
||||
return this.extensionContext.storageUri?.fsPath;
|
||||
}
|
||||
|
||||
public get workspaceState(): Memento {
|
||||
return this.extensionContext.workspaceState;
|
||||
}
|
||||
|
||||
public get subscriptions(): Disposable[] {
|
||||
return this.extensionContext.subscriptions;
|
||||
}
|
||||
|
||||
@@ -476,7 +476,7 @@ export const NO_CACHE_AST_VIEWER = new Setting(
|
||||
);
|
||||
|
||||
// Settings for variant analysis
|
||||
const REMOTE_QUERIES_SETTING = new Setting("variantAnalysis", ROOT_SETTING);
|
||||
const VARIANT_ANALYSIS_SETTING = new Setting("variantAnalysis", ROOT_SETTING);
|
||||
|
||||
/**
|
||||
* Lists of GitHub repositories that you want to query remotely via the "Run Variant Analysis" command.
|
||||
@@ -487,7 +487,7 @@ const REMOTE_QUERIES_SETTING = new Setting("variantAnalysis", ROOT_SETTING);
|
||||
*/
|
||||
const REMOTE_REPO_LISTS = new Setting(
|
||||
"repositoryLists",
|
||||
REMOTE_QUERIES_SETTING,
|
||||
VARIANT_ANALYSIS_SETTING,
|
||||
);
|
||||
|
||||
export function getRemoteRepositoryLists():
|
||||
@@ -513,7 +513,7 @@ export async function setRemoteRepositoryLists(
|
||||
*/
|
||||
const REPO_LISTS_PATH = new Setting(
|
||||
"repositoryListsPath",
|
||||
REMOTE_QUERIES_SETTING,
|
||||
VARIANT_ANALYSIS_SETTING,
|
||||
);
|
||||
|
||||
export function getRemoteRepositoryListsPath(): string | undefined {
|
||||
@@ -528,7 +528,7 @@ export function getRemoteRepositoryListsPath(): string | undefined {
|
||||
*/
|
||||
const REMOTE_CONTROLLER_REPO = new Setting(
|
||||
"controllerRepo",
|
||||
REMOTE_QUERIES_SETTING,
|
||||
VARIANT_ANALYSIS_SETTING,
|
||||
);
|
||||
|
||||
export function getRemoteControllerRepo(): string | undefined {
|
||||
@@ -544,7 +544,7 @@ export async function setRemoteControllerRepo(repo: string | undefined) {
|
||||
* Default value is "main".
|
||||
* Note: This command is only available for internal users.
|
||||
*/
|
||||
const ACTION_BRANCH = new Setting("actionBranch", REMOTE_QUERIES_SETTING);
|
||||
const ACTION_BRANCH = new Setting("actionBranch", VARIANT_ANALYSIS_SETTING);
|
||||
|
||||
export function getActionBranch(): string {
|
||||
return ACTION_BRANCH.getValue<string>() || "main";
|
||||
@@ -559,16 +559,15 @@ export function isVariantAnalysisLiveResultsEnabled(): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* A flag indicating whether to use the new query run experience which involves
|
||||
* using a new database panel.
|
||||
* A flag indicating whether to use the new "variant analysis repositories" panel.
|
||||
*/
|
||||
const NEW_QUERY_RUN_EXPERIENCE = new Setting(
|
||||
"newQueryRunExperience",
|
||||
ROOT_SETTING,
|
||||
const VARIANT_ANALYSIS_REPOS_PANEL = new Setting(
|
||||
"repositoriesPanel",
|
||||
VARIANT_ANALYSIS_SETTING,
|
||||
);
|
||||
|
||||
export function isNewQueryRunExperienceEnabled(): boolean {
|
||||
return !!NEW_QUERY_RUN_EXPERIENCE.getValue<boolean>();
|
||||
export function isVariantAnalysisReposPanelEnabled(): boolean {
|
||||
return !!VARIANT_ANALYSIS_REPOS_PANEL.getValue<boolean>();
|
||||
}
|
||||
|
||||
// Settings for mocking the GitHub API.
|
||||
|
||||
@@ -23,9 +23,9 @@ import { extLogger } from "./common";
|
||||
import { Credentials } from "./authentication";
|
||||
import { getErrorMessage } from "./pure/helpers-pure";
|
||||
import {
|
||||
convertGitHubUrlToNwo,
|
||||
looksLikeGithubRepo,
|
||||
} from "./databases/github-nwo";
|
||||
getNwoFromGitHubUrl,
|
||||
isValidGitHubNwo,
|
||||
} from "./common/github-url-identifier-helper";
|
||||
|
||||
/**
|
||||
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
|
||||
@@ -100,19 +100,16 @@ export async function promptImportGithubDatabase(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!looksLikeGithubRepo(githubRepo)) {
|
||||
const nwo = getNwoFromGitHubUrl(githubRepo) || githubRepo;
|
||||
if (!isValidGitHubNwo(nwo)) {
|
||||
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
|
||||
}
|
||||
|
||||
const octokit = credentials
|
||||
? await credentials.getOctokit(true)
|
||||
? await credentials.getOctokit()
|
||||
: new Octokit.Octokit({ retry });
|
||||
|
||||
const result = await convertGithubNwoToDatabaseUrl(
|
||||
githubRepo,
|
||||
octokit,
|
||||
progress,
|
||||
);
|
||||
const result = await convertGithubNwoToDatabaseUrl(nwo, octokit, progress);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
@@ -446,7 +443,7 @@ export async function findDirWithFile(
|
||||
}
|
||||
|
||||
export async function convertGithubNwoToDatabaseUrl(
|
||||
githubRepo: string,
|
||||
nwo: string,
|
||||
octokit: Octokit.Octokit,
|
||||
progress: ProgressCallback,
|
||||
): Promise<
|
||||
@@ -458,7 +455,6 @@ export async function convertGithubNwoToDatabaseUrl(
|
||||
| undefined
|
||||
> {
|
||||
try {
|
||||
const nwo = convertGitHubUrlToNwo(githubRepo) || githubRepo;
|
||||
const [owner, repo] = nwo.split("/");
|
||||
|
||||
const response = await octokit.request(
|
||||
@@ -480,7 +476,7 @@ export async function convertGithubNwoToDatabaseUrl(
|
||||
};
|
||||
} catch (e) {
|
||||
void extLogger.log(`Error: ${getErrorMessage(e)}`);
|
||||
throw new Error(`Unable to get database for '${githubRepo}'`);
|
||||
throw new Error(`Unable to get database for '${nwo}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { pathExists, stat, remove } from "fs-extra";
|
||||
import * as glob from "glob-promise";
|
||||
import { promise as glob } from "glob-promise";
|
||||
import { join, basename, resolve, relative, dirname, extname } from "path";
|
||||
import * as vscode from "vscode";
|
||||
import * as cli from "./cli";
|
||||
@@ -21,6 +21,7 @@ import { DisposableObject } from "./pure/disposable-object";
|
||||
import { Logger, extLogger } from "./common";
|
||||
import { getErrorMessage } from "./pure/helpers-pure";
|
||||
import { QueryRunner } from "./queryRunner";
|
||||
import { pathsEqual } from "./pure/files";
|
||||
|
||||
/**
|
||||
* databases.ts
|
||||
@@ -523,7 +524,11 @@ export class DatabaseItemImpl implements DatabaseItem {
|
||||
// database for /one/two/three/test.ql is at /one/two/three/three.testproj
|
||||
const testdir = dirname(testPath);
|
||||
const testdirbase = basename(testdir);
|
||||
return databasePath == join(testdir, `${testdirbase}.testproj`);
|
||||
return pathsEqual(
|
||||
databasePath,
|
||||
join(testdir, `${testdirbase}.testproj`),
|
||||
process.platform,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// No information available for test path - assume database is unaffected.
|
||||
@@ -924,7 +929,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
// Delete folder from file system only if it is controlled by the extension
|
||||
if (this.isExtensionControlledLocation(item.databaseUri)) {
|
||||
void extLogger.log("Deleting database from filesystem.");
|
||||
remove(item.databaseUri.fsPath).then(
|
||||
await remove(item.databaseUri.fsPath).then(
|
||||
() => void extLogger.log(`Deleted '${item.databaseUri.fsPath}'`),
|
||||
(e) =>
|
||||
void extLogger.log(
|
||||
|
||||
@@ -3,7 +3,14 @@ import { join } from "path";
|
||||
import {
|
||||
cloneDbConfig,
|
||||
DbConfig,
|
||||
ExpandedDbItem,
|
||||
removeLocalDb,
|
||||
removeLocalList,
|
||||
removeRemoteList,
|
||||
removeRemoteOwner,
|
||||
removeRemoteRepo,
|
||||
renameLocalDb,
|
||||
renameLocalList,
|
||||
renameRemoteList,
|
||||
SelectedDbItem,
|
||||
} from "./db-config";
|
||||
import * as chokidar from "chokidar";
|
||||
@@ -16,6 +23,13 @@ import {
|
||||
DbConfigValidationErrorKind,
|
||||
} from "../db-validation-errors";
|
||||
import { ValueResult } from "../../common/value-result";
|
||||
import {
|
||||
LocalDatabaseDbItem,
|
||||
LocalListDbItem,
|
||||
VariantAnalysisUserDefinedListDbItem,
|
||||
DbItem,
|
||||
DbItemKind,
|
||||
} from "../db-item";
|
||||
|
||||
export class DbConfigStore extends DisposableObject {
|
||||
public readonly onDidChangeConfig: AppEvent<void>;
|
||||
@@ -28,7 +42,10 @@ export class DbConfigStore extends DisposableObject {
|
||||
private configErrors: DbConfigValidationError[];
|
||||
private configWatcher: chokidar.FSWatcher | undefined;
|
||||
|
||||
public constructor(private readonly app: App) {
|
||||
public constructor(
|
||||
private readonly app: App,
|
||||
private readonly shouldWatchConfig = true,
|
||||
) {
|
||||
super();
|
||||
|
||||
const storagePath = app.workspaceStoragePath || app.globalStoragePath;
|
||||
@@ -44,7 +61,9 @@ export class DbConfigStore extends DisposableObject {
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
await this.loadConfig();
|
||||
this.watchConfig();
|
||||
if (this.shouldWatchConfig) {
|
||||
this.watchConfig();
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(disposeHandler?: DisposeHandler): void {
|
||||
@@ -73,7 +92,7 @@ export class DbConfigStore extends DisposableObject {
|
||||
throw Error("Cannot select database item if config is not loaded");
|
||||
}
|
||||
|
||||
const config: DbConfig = {
|
||||
const config = {
|
||||
...this.config,
|
||||
selected: dbItem,
|
||||
};
|
||||
@@ -81,15 +100,110 @@ export class DbConfigStore extends DisposableObject {
|
||||
await this.writeConfig(config);
|
||||
}
|
||||
|
||||
public async updateExpandedState(expandedItems: ExpandedDbItem[]) {
|
||||
public async removeDbItem(dbItem: DbItem): Promise<void> {
|
||||
if (!this.config) {
|
||||
throw Error("Cannot update expansion state if config is not loaded");
|
||||
throw Error("Cannot remove item if config is not loaded");
|
||||
}
|
||||
|
||||
const config: DbConfig = {
|
||||
...this.config,
|
||||
expanded: expandedItems,
|
||||
};
|
||||
let config: DbConfig;
|
||||
|
||||
switch (dbItem.kind) {
|
||||
case DbItemKind.LocalList:
|
||||
config = removeLocalList(this.config, dbItem.listName);
|
||||
break;
|
||||
case DbItemKind.VariantAnalysisUserDefinedList:
|
||||
config = removeRemoteList(this.config, dbItem.listName);
|
||||
break;
|
||||
case DbItemKind.LocalDatabase:
|
||||
// When we start using local databases these need to be removed from disk as well.
|
||||
config = removeLocalDb(
|
||||
this.config,
|
||||
dbItem.databaseName,
|
||||
dbItem.parentListName,
|
||||
);
|
||||
break;
|
||||
case DbItemKind.RemoteRepo:
|
||||
config = removeRemoteRepo(
|
||||
this.config,
|
||||
dbItem.repoFullName,
|
||||
dbItem.parentListName,
|
||||
);
|
||||
break;
|
||||
case DbItemKind.RemoteOwner:
|
||||
config = removeRemoteOwner(this.config, dbItem.ownerName);
|
||||
break;
|
||||
default:
|
||||
throw Error(`Type '${dbItem.kind}' cannot be removed`);
|
||||
}
|
||||
|
||||
await this.writeConfig(config);
|
||||
}
|
||||
|
||||
public async addRemoteRepo(
|
||||
repoNwo: string,
|
||||
parentList?: string,
|
||||
): Promise<void> {
|
||||
if (!this.config) {
|
||||
throw Error("Cannot add remote repo if config is not loaded");
|
||||
}
|
||||
|
||||
if (repoNwo === "") {
|
||||
throw Error("Repository name cannot be empty");
|
||||
}
|
||||
|
||||
if (this.doesRemoteDbExist(repoNwo)) {
|
||||
throw Error(
|
||||
`A remote repository with the name '${repoNwo}' already exists`,
|
||||
);
|
||||
}
|
||||
|
||||
const config = cloneDbConfig(this.config);
|
||||
if (parentList) {
|
||||
const parent = config.databases.variantAnalysis.repositoryLists.find(
|
||||
(list) => list.name === parentList,
|
||||
);
|
||||
if (!parent) {
|
||||
throw Error(`Cannot find parent list '${parentList}'`);
|
||||
} else {
|
||||
parent.repositories.push(repoNwo);
|
||||
}
|
||||
} else {
|
||||
config.databases.variantAnalysis.repositories.push(repoNwo);
|
||||
}
|
||||
await this.writeConfig(config);
|
||||
}
|
||||
|
||||
public async addRemoteOwner(owner: string): Promise<void> {
|
||||
if (!this.config) {
|
||||
throw Error("Cannot add remote owner if config is not loaded");
|
||||
}
|
||||
|
||||
if (owner === "") {
|
||||
throw Error("Owner name cannot be empty");
|
||||
}
|
||||
|
||||
if (this.doesRemoteOwnerExist(owner)) {
|
||||
throw Error(`A remote owner with the name '${owner}' already exists`);
|
||||
}
|
||||
|
||||
const config = cloneDbConfig(this.config);
|
||||
config.databases.variantAnalysis.owners.push(owner);
|
||||
|
||||
await this.writeConfig(config);
|
||||
}
|
||||
|
||||
public async addLocalList(listName: string): Promise<void> {
|
||||
if (!this.config) {
|
||||
throw Error("Cannot add local list if config is not loaded");
|
||||
}
|
||||
|
||||
this.validateLocalListName(listName);
|
||||
|
||||
const config = cloneDbConfig(this.config);
|
||||
config.databases.local.lists.push({
|
||||
name: listName,
|
||||
databases: [],
|
||||
});
|
||||
|
||||
await this.writeConfig(config);
|
||||
}
|
||||
@@ -99,12 +213,10 @@ export class DbConfigStore extends DisposableObject {
|
||||
throw Error("Cannot add remote list if config is not loaded");
|
||||
}
|
||||
|
||||
if (this.doesRemoteListExist(listName)) {
|
||||
throw Error(`A remote list with the name '${listName}' already exists`);
|
||||
}
|
||||
this.validateRemoteListName(listName);
|
||||
|
||||
const config: DbConfig = cloneDbConfig(this.config);
|
||||
config.databases.remote.repositoryLists.push({
|
||||
const config = cloneDbConfig(this.config);
|
||||
config.databases.variantAnalysis.repositoryLists.push({
|
||||
name: listName,
|
||||
repositories: [],
|
||||
});
|
||||
@@ -112,16 +224,126 @@ export class DbConfigStore extends DisposableObject {
|
||||
await this.writeConfig(config);
|
||||
}
|
||||
|
||||
public async renameLocalList(
|
||||
currentDbItem: LocalListDbItem,
|
||||
newName: string,
|
||||
) {
|
||||
if (!this.config) {
|
||||
throw Error("Cannot rename local list if config is not loaded");
|
||||
}
|
||||
|
||||
this.validateLocalListName(newName);
|
||||
|
||||
const updatedConfig = renameLocalList(
|
||||
this.config,
|
||||
currentDbItem.listName,
|
||||
newName,
|
||||
);
|
||||
|
||||
await this.writeConfig(updatedConfig);
|
||||
}
|
||||
|
||||
public async renameRemoteList(
|
||||
currentDbItem: VariantAnalysisUserDefinedListDbItem,
|
||||
newName: string,
|
||||
) {
|
||||
if (!this.config) {
|
||||
throw Error("Cannot rename remote list if config is not loaded");
|
||||
}
|
||||
|
||||
this.validateRemoteListName(newName);
|
||||
|
||||
const updatedConfig = renameRemoteList(
|
||||
this.config,
|
||||
currentDbItem.listName,
|
||||
newName,
|
||||
);
|
||||
|
||||
await this.writeConfig(updatedConfig);
|
||||
}
|
||||
|
||||
public async renameLocalDb(
|
||||
currentDbItem: LocalDatabaseDbItem,
|
||||
newName: string,
|
||||
parentListName?: string,
|
||||
): Promise<void> {
|
||||
if (!this.config) {
|
||||
throw Error("Cannot rename local db if config is not loaded");
|
||||
}
|
||||
|
||||
this.validateLocalDbName(newName);
|
||||
|
||||
const updatedConfig = renameLocalDb(
|
||||
this.config,
|
||||
currentDbItem.databaseName,
|
||||
newName,
|
||||
parentListName,
|
||||
);
|
||||
|
||||
await this.writeConfig(updatedConfig);
|
||||
}
|
||||
|
||||
public doesRemoteListExist(listName: string): boolean {
|
||||
if (!this.config) {
|
||||
throw Error("Cannot check remote list existence if config is not loaded");
|
||||
}
|
||||
|
||||
return this.config.databases.remote.repositoryLists.some(
|
||||
return this.config.databases.variantAnalysis.repositoryLists.some(
|
||||
(l) => l.name === listName,
|
||||
);
|
||||
}
|
||||
|
||||
public doesLocalListExist(listName: string): boolean {
|
||||
if (!this.config) {
|
||||
throw Error("Cannot check local list existence if config is not loaded");
|
||||
}
|
||||
|
||||
return this.config.databases.local.lists.some((l) => l.name === listName);
|
||||
}
|
||||
|
||||
public doesLocalDbExist(dbName: string, listName?: string): boolean {
|
||||
if (!this.config) {
|
||||
throw Error(
|
||||
"Cannot check remote database existence if config is not loaded",
|
||||
);
|
||||
}
|
||||
|
||||
if (listName) {
|
||||
return this.config.databases.local.lists.some(
|
||||
(l) =>
|
||||
l.name === listName && l.databases.some((d) => d.name === dbName),
|
||||
);
|
||||
}
|
||||
|
||||
return this.config.databases.local.databases.some((d) => d.name === dbName);
|
||||
}
|
||||
|
||||
public doesRemoteDbExist(dbName: string, listName?: string): boolean {
|
||||
if (!this.config) {
|
||||
throw Error(
|
||||
"Cannot check remote database existence if config is not loaded",
|
||||
);
|
||||
}
|
||||
|
||||
if (listName) {
|
||||
return this.config.databases.variantAnalysis.repositoryLists.some(
|
||||
(l) => l.name === listName && l.repositories.includes(dbName),
|
||||
);
|
||||
}
|
||||
|
||||
return this.config.databases.variantAnalysis.repositories.includes(dbName);
|
||||
}
|
||||
|
||||
public doesRemoteOwnerExist(owner: string): boolean {
|
||||
if (!this.config) {
|
||||
throw Error(
|
||||
"Cannot check remote owner existence if config is not loaded",
|
||||
);
|
||||
}
|
||||
|
||||
return this.config.databases.variantAnalysis.owners.includes(owner);
|
||||
}
|
||||
|
||||
private async writeConfig(config: DbConfig): Promise<void> {
|
||||
await outputJSON(this.configPath, config, {
|
||||
spaces: 2,
|
||||
@@ -161,14 +383,14 @@ export class DbConfigStore extends DisposableObject {
|
||||
this.config = newConfig;
|
||||
await this.app.executeCommand(
|
||||
"setContext",
|
||||
"codeQLDatabasesExperimental.configError",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
this.config = undefined;
|
||||
await this.app.executeCommand(
|
||||
"setContext",
|
||||
"codeQLDatabasesExperimental.configError",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
true,
|
||||
);
|
||||
}
|
||||
@@ -195,14 +417,14 @@ export class DbConfigStore extends DisposableObject {
|
||||
this.config = newConfig;
|
||||
void this.app.executeCommand(
|
||||
"setContext",
|
||||
"codeQLDatabasesExperimental.configError",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
this.config = undefined;
|
||||
void this.app.executeCommand(
|
||||
"setContext",
|
||||
"codeQLDatabasesExperimental.configError",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
true,
|
||||
);
|
||||
}
|
||||
@@ -229,7 +451,7 @@ export class DbConfigStore extends DisposableObject {
|
||||
private createEmptyConfig(): DbConfig {
|
||||
return {
|
||||
databases: {
|
||||
remote: {
|
||||
variantAnalysis: {
|
||||
repositoryLists: [],
|
||||
owners: [],
|
||||
repositories: [],
|
||||
@@ -239,7 +461,36 @@ export class DbConfigStore extends DisposableObject {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
}
|
||||
|
||||
private validateLocalListName(listName: string): void {
|
||||
if (listName === "") {
|
||||
throw Error("List name cannot be empty");
|
||||
}
|
||||
|
||||
if (this.doesLocalListExist(listName)) {
|
||||
throw Error(`A local list with the name '${listName}' already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
private validateRemoteListName(listName: string): void {
|
||||
if (listName === "") {
|
||||
throw Error("List name cannot be empty");
|
||||
}
|
||||
|
||||
if (this.doesRemoteListExist(listName)) {
|
||||
throw Error(`A remote list with the name '${listName}' already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
private validateLocalDbName(dbName: string): void {
|
||||
if (dbName === "") {
|
||||
throw Error("Database name cannot be empty");
|
||||
}
|
||||
|
||||
if (this.doesLocalDbExist(dbName)) {
|
||||
throw Error(`A local database with the name '${dbName}' already exists`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export class DbConfigValidator {
|
||||
}
|
||||
|
||||
const duplicateRemoteDbLists = findDuplicateStrings(
|
||||
dbConfig.databases.remote.repositoryLists.map((n) => n.name),
|
||||
dbConfig.databases.variantAnalysis.repositoryLists.map((n) => n.name),
|
||||
);
|
||||
if (duplicateRemoteDbLists.length > 0) {
|
||||
errors.push(buildError(duplicateRemoteDbLists));
|
||||
@@ -83,7 +83,7 @@ export class DbConfigValidator {
|
||||
}
|
||||
|
||||
const duplicateRemoteDbs = findDuplicateStrings(
|
||||
dbConfig.databases.remote.repositories,
|
||||
dbConfig.databases.variantAnalysis.repositories,
|
||||
);
|
||||
if (duplicateRemoteDbs.length > 0) {
|
||||
errors.push(buildError(duplicateRemoteDbs));
|
||||
@@ -111,7 +111,7 @@ export class DbConfigValidator {
|
||||
}
|
||||
}
|
||||
|
||||
for (const list of dbConfig.databases.remote.repositoryLists) {
|
||||
for (const list of dbConfig.databases.variantAnalysis.repositoryLists) {
|
||||
const dups = findDuplicateStrings(list.repositories);
|
||||
if (dups.length > 0) {
|
||||
errors.push(buildError(list.name, dups));
|
||||
@@ -124,7 +124,9 @@ export class DbConfigValidator {
|
||||
private validateOwners(dbConfig: DbConfig): DbConfigValidationError[] {
|
||||
const errors: DbConfigValidationError[] = [];
|
||||
|
||||
const dups = findDuplicateStrings(dbConfig.databases.remote.owners);
|
||||
const dups = findDuplicateStrings(
|
||||
dbConfig.databases.variantAnalysis.owners,
|
||||
);
|
||||
if (dups.length > 0) {
|
||||
errors.push({
|
||||
kind: DbConfigValidationErrorKind.DuplicateNames,
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
export interface DbConfig {
|
||||
databases: DbConfigDatabases;
|
||||
expanded: ExpandedDbItem[];
|
||||
selected?: SelectedDbItem;
|
||||
}
|
||||
|
||||
export interface DbConfigDatabases {
|
||||
remote: RemoteDbConfig;
|
||||
variantAnalysis: RemoteDbConfig;
|
||||
local: LocalDbConfig;
|
||||
}
|
||||
|
||||
@@ -15,17 +14,17 @@ export type SelectedDbItem =
|
||||
| SelectedLocalUserDefinedList
|
||||
| SelectedLocalDatabase
|
||||
| SelectedRemoteSystemDefinedList
|
||||
| SelectedRemoteUserDefinedList
|
||||
| SelectedVariantAnalysisUserDefinedList
|
||||
| SelectedRemoteOwner
|
||||
| SelectedRemoteRepository;
|
||||
|
||||
export enum SelectedDbItemKind {
|
||||
LocalUserDefinedList = "localUserDefinedList",
|
||||
LocalDatabase = "localDatabase",
|
||||
RemoteSystemDefinedList = "remoteSystemDefinedList",
|
||||
RemoteUserDefinedList = "remoteUserDefinedList",
|
||||
RemoteOwner = "remoteOwner",
|
||||
RemoteRepository = "remoteRepository",
|
||||
VariantAnalysisSystemDefinedList = "variantAnalysisSystemDefinedList",
|
||||
VariantAnalysisUserDefinedList = "variantAnalysisUserDefinedList",
|
||||
VariantAnalysisOwner = "variantAnalysisOwner",
|
||||
VariantAnalysisRepository = "variantAnalysisRepository",
|
||||
}
|
||||
|
||||
export interface SelectedLocalUserDefinedList {
|
||||
@@ -40,22 +39,22 @@ export interface SelectedLocalDatabase {
|
||||
}
|
||||
|
||||
export interface SelectedRemoteSystemDefinedList {
|
||||
kind: SelectedDbItemKind.RemoteSystemDefinedList;
|
||||
kind: SelectedDbItemKind.VariantAnalysisSystemDefinedList;
|
||||
listName: string;
|
||||
}
|
||||
|
||||
export interface SelectedRemoteUserDefinedList {
|
||||
kind: SelectedDbItemKind.RemoteUserDefinedList;
|
||||
export interface SelectedVariantAnalysisUserDefinedList {
|
||||
kind: SelectedDbItemKind.VariantAnalysisUserDefinedList;
|
||||
listName: string;
|
||||
}
|
||||
|
||||
export interface SelectedRemoteOwner {
|
||||
kind: SelectedDbItemKind.RemoteOwner;
|
||||
kind: SelectedDbItemKind.VariantAnalysisOwner;
|
||||
ownerName: string;
|
||||
}
|
||||
|
||||
export interface SelectedRemoteRepository {
|
||||
kind: SelectedDbItemKind.RemoteRepository;
|
||||
kind: SelectedDbItemKind.VariantAnalysisRepository;
|
||||
repositoryName: string;
|
||||
listName?: string;
|
||||
}
|
||||
@@ -88,49 +87,18 @@ export interface LocalDatabase {
|
||||
storagePath: string;
|
||||
}
|
||||
|
||||
export type ExpandedDbItem =
|
||||
| RootLocalExpandedDbItem
|
||||
| LocalUserDefinedListExpandedDbItem
|
||||
| RootRemoteExpandedDbItem
|
||||
| RemoteUserDefinedListExpandedDbItem;
|
||||
|
||||
export enum ExpandedDbItemKind {
|
||||
RootLocal = "rootLocal",
|
||||
LocalUserDefinedList = "localUserDefinedList",
|
||||
RootRemote = "rootRemote",
|
||||
RemoteUserDefinedList = "remoteUserDefinedList",
|
||||
}
|
||||
|
||||
export interface RootLocalExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.RootLocal;
|
||||
}
|
||||
|
||||
export interface LocalUserDefinedListExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.LocalUserDefinedList;
|
||||
listName: string;
|
||||
}
|
||||
|
||||
export interface RootRemoteExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.RootRemote;
|
||||
}
|
||||
|
||||
export interface RemoteUserDefinedListExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.RemoteUserDefinedList;
|
||||
listName: string;
|
||||
}
|
||||
|
||||
export function cloneDbConfig(config: DbConfig): DbConfig {
|
||||
return {
|
||||
databases: {
|
||||
remote: {
|
||||
repositoryLists: config.databases.remote.repositoryLists.map(
|
||||
variantAnalysis: {
|
||||
repositoryLists: config.databases.variantAnalysis.repositoryLists.map(
|
||||
(list) => ({
|
||||
name: list.name,
|
||||
repositories: [...list.repositories],
|
||||
}),
|
||||
),
|
||||
owners: [...config.databases.remote.owners],
|
||||
repositories: [...config.databases.remote.repositories],
|
||||
owners: [...config.databases.variantAnalysis.owners],
|
||||
repositories: [...config.databases.variantAnalysis.repositories],
|
||||
},
|
||||
local: {
|
||||
lists: config.databases.local.lists.map((list) => ({
|
||||
@@ -140,13 +108,223 @@ export function cloneDbConfig(config: DbConfig): DbConfig {
|
||||
databases: config.databases.local.databases.map((db) => ({ ...db })),
|
||||
},
|
||||
},
|
||||
expanded: config.expanded.map(cloneDbConfigExpandedItem),
|
||||
selected: config.selected
|
||||
? cloneDbConfigSelectedItem(config.selected)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function renameLocalList(
|
||||
originalConfig: DbConfig,
|
||||
currentListName: string,
|
||||
newListName: string,
|
||||
): DbConfig {
|
||||
const config = cloneDbConfig(originalConfig);
|
||||
|
||||
const list = getLocalList(config, currentListName);
|
||||
list.name = newListName;
|
||||
|
||||
if (
|
||||
config.selected?.kind === SelectedDbItemKind.LocalUserDefinedList ||
|
||||
config.selected?.kind === SelectedDbItemKind.LocalDatabase
|
||||
) {
|
||||
if (config.selected.listName === currentListName) {
|
||||
config.selected.listName = newListName;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function renameRemoteList(
|
||||
originalConfig: DbConfig,
|
||||
currentListName: string,
|
||||
newListName: string,
|
||||
): DbConfig {
|
||||
const config = cloneDbConfig(originalConfig);
|
||||
|
||||
const list = getRemoteList(config, currentListName);
|
||||
list.name = newListName;
|
||||
|
||||
if (
|
||||
config.selected?.kind ===
|
||||
SelectedDbItemKind.VariantAnalysisUserDefinedList ||
|
||||
config.selected?.kind === SelectedDbItemKind.VariantAnalysisRepository
|
||||
) {
|
||||
if (config.selected.listName === currentListName) {
|
||||
config.selected.listName = newListName;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function renameLocalDb(
|
||||
originalConfig: DbConfig,
|
||||
currentDbName: string,
|
||||
newDbName: string,
|
||||
parentListName?: string,
|
||||
): DbConfig {
|
||||
const config = cloneDbConfig(originalConfig);
|
||||
|
||||
if (parentListName) {
|
||||
const list = getLocalList(config, parentListName);
|
||||
const dbIndex = list.databases.findIndex((db) => db.name === currentDbName);
|
||||
if (dbIndex === -1) {
|
||||
throw Error(
|
||||
`Cannot find database '${currentDbName}' in list '${parentListName}'`,
|
||||
);
|
||||
}
|
||||
list.databases[dbIndex].name = newDbName;
|
||||
} else {
|
||||
const dbIndex = config.databases.local.databases.findIndex(
|
||||
(db) => db.name === currentDbName,
|
||||
);
|
||||
if (dbIndex === -1) {
|
||||
throw Error(`Cannot find database '${currentDbName}' in local databases`);
|
||||
}
|
||||
config.databases.local.databases[dbIndex].name = newDbName;
|
||||
}
|
||||
|
||||
if (
|
||||
config.selected?.kind === SelectedDbItemKind.LocalDatabase &&
|
||||
config.selected.databaseName === currentDbName
|
||||
) {
|
||||
config.selected.databaseName = newDbName;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function removeLocalList(
|
||||
originalConfig: DbConfig,
|
||||
listName: string,
|
||||
): DbConfig {
|
||||
const config = cloneDbConfig(originalConfig);
|
||||
|
||||
config.databases.local.lists = config.databases.local.lists.filter(
|
||||
(list) => list.name !== listName,
|
||||
);
|
||||
|
||||
if (config.selected?.kind === SelectedDbItemKind.LocalUserDefinedList) {
|
||||
config.selected = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
config.selected?.kind === SelectedDbItemKind.LocalDatabase &&
|
||||
config.selected?.listName === listName
|
||||
) {
|
||||
config.selected = undefined;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function removeRemoteList(
|
||||
originalConfig: DbConfig,
|
||||
listName: string,
|
||||
): DbConfig {
|
||||
const config = cloneDbConfig(originalConfig);
|
||||
|
||||
config.databases.variantAnalysis.repositoryLists =
|
||||
config.databases.variantAnalysis.repositoryLists.filter(
|
||||
(list) => list.name !== listName,
|
||||
);
|
||||
|
||||
if (
|
||||
config.selected?.kind === SelectedDbItemKind.VariantAnalysisUserDefinedList
|
||||
) {
|
||||
config.selected = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
config.selected?.kind === SelectedDbItemKind.VariantAnalysisRepository &&
|
||||
config.selected?.listName === listName
|
||||
) {
|
||||
config.selected = undefined;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function removeLocalDb(
|
||||
originalConfig: DbConfig,
|
||||
databaseName: string,
|
||||
parentListName?: string,
|
||||
): DbConfig {
|
||||
const config = cloneDbConfig(originalConfig);
|
||||
|
||||
if (parentListName) {
|
||||
const parentList = getLocalList(config, parentListName);
|
||||
parentList.databases = parentList.databases.filter(
|
||||
(db) => db.name !== databaseName,
|
||||
);
|
||||
} else {
|
||||
config.databases.local.databases = config.databases.local.databases.filter(
|
||||
(db) => db.name !== databaseName,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
config.selected?.kind === SelectedDbItemKind.LocalDatabase &&
|
||||
config.selected?.databaseName === databaseName &&
|
||||
config.selected?.listName === parentListName
|
||||
) {
|
||||
config.selected = undefined;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function removeRemoteRepo(
|
||||
originalConfig: DbConfig,
|
||||
repoFullName: string,
|
||||
parentListName?: string,
|
||||
): DbConfig {
|
||||
const config = cloneDbConfig(originalConfig);
|
||||
|
||||
if (parentListName) {
|
||||
const parentList = getRemoteList(config, parentListName);
|
||||
parentList.repositories = parentList.repositories.filter(
|
||||
(r) => r !== repoFullName,
|
||||
);
|
||||
} else {
|
||||
config.databases.variantAnalysis.repositories =
|
||||
config.databases.variantAnalysis.repositories.filter(
|
||||
(r) => r !== repoFullName,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
config.selected?.kind === SelectedDbItemKind.VariantAnalysisRepository &&
|
||||
config.selected?.repositoryName === repoFullName &&
|
||||
config.selected?.listName === parentListName
|
||||
) {
|
||||
config.selected = undefined;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function removeRemoteOwner(
|
||||
originalConfig: DbConfig,
|
||||
ownerName: string,
|
||||
): DbConfig {
|
||||
const config = cloneDbConfig(originalConfig);
|
||||
|
||||
config.databases.variantAnalysis.owners =
|
||||
config.databases.variantAnalysis.owners.filter((o) => o !== ownerName);
|
||||
|
||||
if (
|
||||
config.selected?.kind === SelectedDbItemKind.VariantAnalysisOwner &&
|
||||
config.selected?.ownerName === ownerName
|
||||
) {
|
||||
config.selected = undefined;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function cloneDbConfigSelectedItem(selected: SelectedDbItem): SelectedDbItem {
|
||||
switch (selected.kind) {
|
||||
case SelectedDbItemKind.LocalUserDefinedList:
|
||||
@@ -160,40 +338,51 @@ function cloneDbConfigSelectedItem(selected: SelectedDbItem): SelectedDbItem {
|
||||
databaseName: selected.databaseName,
|
||||
listName: selected.listName,
|
||||
};
|
||||
case SelectedDbItemKind.RemoteSystemDefinedList:
|
||||
case SelectedDbItemKind.VariantAnalysisSystemDefinedList:
|
||||
return {
|
||||
kind: SelectedDbItemKind.RemoteSystemDefinedList,
|
||||
kind: SelectedDbItemKind.VariantAnalysisSystemDefinedList,
|
||||
listName: selected.listName,
|
||||
};
|
||||
case SelectedDbItemKind.RemoteUserDefinedList:
|
||||
case SelectedDbItemKind.VariantAnalysisUserDefinedList:
|
||||
return {
|
||||
kind: SelectedDbItemKind.RemoteUserDefinedList,
|
||||
kind: SelectedDbItemKind.VariantAnalysisUserDefinedList,
|
||||
listName: selected.listName,
|
||||
};
|
||||
case SelectedDbItemKind.RemoteOwner:
|
||||
case SelectedDbItemKind.VariantAnalysisOwner:
|
||||
return {
|
||||
kind: SelectedDbItemKind.RemoteOwner,
|
||||
kind: SelectedDbItemKind.VariantAnalysisOwner,
|
||||
ownerName: selected.ownerName,
|
||||
};
|
||||
case SelectedDbItemKind.RemoteRepository:
|
||||
case SelectedDbItemKind.VariantAnalysisRepository:
|
||||
return {
|
||||
kind: SelectedDbItemKind.RemoteRepository,
|
||||
kind: SelectedDbItemKind.VariantAnalysisRepository,
|
||||
repositoryName: selected.repositoryName,
|
||||
listName: selected.listName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function cloneDbConfigExpandedItem(item: ExpandedDbItem): ExpandedDbItem {
|
||||
switch (item.kind) {
|
||||
case ExpandedDbItemKind.RootLocal:
|
||||
case ExpandedDbItemKind.RootRemote:
|
||||
return { kind: item.kind };
|
||||
case ExpandedDbItemKind.LocalUserDefinedList:
|
||||
case ExpandedDbItemKind.RemoteUserDefinedList:
|
||||
return {
|
||||
kind: item.kind,
|
||||
listName: item.listName,
|
||||
};
|
||||
function getLocalList(config: DbConfig, listName: string): LocalList {
|
||||
const list = config.databases.local.lists.find((l) => l.name === listName);
|
||||
|
||||
if (!list) {
|
||||
throw Error(`Cannot find local list '${listName}'`);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function getRemoteList(
|
||||
config: DbConfig,
|
||||
listName: string,
|
||||
): RemoteRepositoryList {
|
||||
const list = config.databases.variantAnalysis.repositoryLists.find(
|
||||
(l) => l.name === listName,
|
||||
);
|
||||
|
||||
if (!list) {
|
||||
throw Error(`Cannot find remote list '${listName}'`);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
import { ExpandedDbItem, ExpandedDbItemKind } from "./config/db-config";
|
||||
import { DbItem, DbItemKind } from "./db-item";
|
||||
import { DbItem, DbItemKind, flattenDbItems } from "./db-item";
|
||||
|
||||
export function calculateNewExpandedState(
|
||||
export type ExpandedDbItem =
|
||||
| RootLocalExpandedDbItem
|
||||
| LocalUserDefinedListExpandedDbItem
|
||||
| RootRemoteExpandedDbItem
|
||||
| VariantAnalysisUserDefinedListExpandedDbItem;
|
||||
|
||||
export enum ExpandedDbItemKind {
|
||||
RootLocal = "rootLocal",
|
||||
LocalUserDefinedList = "localUserDefinedList",
|
||||
RootRemote = "rootRemote",
|
||||
RemoteUserDefinedList = "remoteUserDefinedList",
|
||||
}
|
||||
|
||||
export interface RootLocalExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.RootLocal;
|
||||
}
|
||||
|
||||
export interface LocalUserDefinedListExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.LocalUserDefinedList;
|
||||
listName: string;
|
||||
}
|
||||
|
||||
export interface RootRemoteExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.RootRemote;
|
||||
}
|
||||
|
||||
export interface VariantAnalysisUserDefinedListExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.RemoteUserDefinedList;
|
||||
listName: string;
|
||||
}
|
||||
|
||||
export function updateExpandedItem(
|
||||
currentExpandedItems: ExpandedDbItem[],
|
||||
dbItem: DbItem,
|
||||
itemExpanded: boolean,
|
||||
@@ -20,6 +50,34 @@ export function calculateNewExpandedState(
|
||||
}
|
||||
}
|
||||
|
||||
export function replaceExpandedItem(
|
||||
currentExpandedItems: ExpandedDbItem[],
|
||||
currentDbItem: DbItem,
|
||||
newDbItem: DbItem,
|
||||
): ExpandedDbItem[] {
|
||||
const newExpandedItems: ExpandedDbItem[] = [];
|
||||
|
||||
for (const item of currentExpandedItems) {
|
||||
if (isDbItemEqualToExpandedDbItem(currentDbItem, item)) {
|
||||
newExpandedItems.push(mapDbItemToExpandedDbItem(newDbItem));
|
||||
} else {
|
||||
newExpandedItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return newExpandedItems;
|
||||
}
|
||||
|
||||
export function cleanNonExistentExpandedItems(
|
||||
currentExpandedItems: ExpandedDbItem[],
|
||||
dbItems: DbItem[],
|
||||
): ExpandedDbItem[] {
|
||||
const flattenedDbItems = flattenDbItems(dbItems);
|
||||
return currentExpandedItems.filter((i) =>
|
||||
flattenedDbItems.some((dbItem) => isDbItemEqualToExpandedDbItem(dbItem, i)),
|
||||
);
|
||||
}
|
||||
|
||||
function mapDbItemToExpandedDbItem(dbItem: DbItem): ExpandedDbItem {
|
||||
switch (dbItem.kind) {
|
||||
case DbItemKind.RootLocal:
|
||||
@@ -31,7 +89,7 @@ function mapDbItemToExpandedDbItem(dbItem: DbItem): ExpandedDbItem {
|
||||
};
|
||||
case DbItemKind.RootRemote:
|
||||
return { kind: ExpandedDbItemKind.RootRemote };
|
||||
case DbItemKind.RemoteUserDefinedList:
|
||||
case DbItemKind.VariantAnalysisUserDefinedList:
|
||||
return {
|
||||
kind: ExpandedDbItemKind.RemoteUserDefinedList,
|
||||
listName: dbItem.listName,
|
||||
@@ -55,12 +113,15 @@ function isDbItemEqualToExpandedDbItem(
|
||||
);
|
||||
case DbItemKind.RootRemote:
|
||||
return expandedDbItem.kind === ExpandedDbItemKind.RootRemote;
|
||||
case DbItemKind.RemoteUserDefinedList:
|
||||
case DbItemKind.VariantAnalysisUserDefinedList:
|
||||
return (
|
||||
expandedDbItem.kind === ExpandedDbItemKind.RemoteUserDefinedList &&
|
||||
expandedDbItem.listName === dbItem.listName
|
||||
);
|
||||
default:
|
||||
throw Error(`Unknown db item kind ${dbItem.kind}`);
|
||||
case DbItemKind.LocalDatabase:
|
||||
case DbItemKind.RemoteSystemDefinedList:
|
||||
case DbItemKind.RemoteOwner:
|
||||
case DbItemKind.RemoteRepo:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
19
extensions/ql-vscode/src/databases/db-item-naming.ts
Normal file
19
extensions/ql-vscode/src/databases/db-item-naming.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { DbItem, DbItemKind } from "./db-item";
|
||||
|
||||
export function getDbItemName(dbItem: DbItem): string | undefined {
|
||||
switch (dbItem.kind) {
|
||||
case DbItemKind.RootLocal:
|
||||
case DbItemKind.RootRemote:
|
||||
return undefined;
|
||||
case DbItemKind.LocalList:
|
||||
case DbItemKind.VariantAnalysisUserDefinedList:
|
||||
case DbItemKind.RemoteSystemDefinedList:
|
||||
return dbItem.listName;
|
||||
case DbItemKind.RemoteOwner:
|
||||
return dbItem.ownerName;
|
||||
case DbItemKind.LocalDatabase:
|
||||
return dbItem.databaseName;
|
||||
case DbItemKind.RemoteRepo:
|
||||
return dbItem.repoFullName;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ function extractSelected(
|
||||
}
|
||||
}
|
||||
break;
|
||||
case DbItemKind.RemoteUserDefinedList:
|
||||
case DbItemKind.VariantAnalysisUserDefinedList:
|
||||
for (const repo of dbItem.repos) {
|
||||
if (repo.selected) {
|
||||
return repo;
|
||||
@@ -59,36 +59,36 @@ export function mapDbItemToSelectedDbItem(
|
||||
listName: dbItem.listName,
|
||||
};
|
||||
|
||||
case DbItemKind.RemoteUserDefinedList:
|
||||
case DbItemKind.VariantAnalysisUserDefinedList:
|
||||
return {
|
||||
kind: SelectedDbItemKind.RemoteUserDefinedList,
|
||||
kind: SelectedDbItemKind.VariantAnalysisUserDefinedList,
|
||||
listName: dbItem.listName,
|
||||
};
|
||||
|
||||
case DbItemKind.RemoteSystemDefinedList:
|
||||
return {
|
||||
kind: SelectedDbItemKind.RemoteSystemDefinedList,
|
||||
kind: SelectedDbItemKind.VariantAnalysisSystemDefinedList,
|
||||
listName: dbItem.listName,
|
||||
};
|
||||
|
||||
case DbItemKind.RemoteOwner:
|
||||
return {
|
||||
kind: SelectedDbItemKind.RemoteOwner,
|
||||
kind: SelectedDbItemKind.VariantAnalysisOwner,
|
||||
ownerName: dbItem.ownerName,
|
||||
};
|
||||
|
||||
case DbItemKind.LocalDatabase:
|
||||
return {
|
||||
kind: SelectedDbItemKind.LocalDatabase,
|
||||
listName: dbItem?.parentListName,
|
||||
databaseName: dbItem.databaseName,
|
||||
listName: dbItem?.parentListName,
|
||||
};
|
||||
|
||||
case DbItemKind.RemoteRepo:
|
||||
return {
|
||||
kind: SelectedDbItemKind.RemoteRepository,
|
||||
listName: dbItem?.parentListName,
|
||||
kind: SelectedDbItemKind.VariantAnalysisRepository,
|
||||
repositoryName: dbItem.repoFullName,
|
||||
listName: dbItem?.parentListName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,30 @@ export enum DbItemKind {
|
||||
LocalDatabase = "LocalDatabase",
|
||||
RootRemote = "RootRemote",
|
||||
RemoteSystemDefinedList = "RemoteSystemDefinedList",
|
||||
RemoteUserDefinedList = "RemoteUserDefinedList",
|
||||
VariantAnalysisUserDefinedList = "VariantAnalysisUserDefinedList",
|
||||
RemoteOwner = "RemoteOwner",
|
||||
RemoteRepo = "RemoteRepo",
|
||||
}
|
||||
|
||||
export const remoteDbKinds = [
|
||||
DbItemKind.RootRemote,
|
||||
DbItemKind.RemoteSystemDefinedList,
|
||||
DbItemKind.VariantAnalysisUserDefinedList,
|
||||
DbItemKind.RemoteOwner,
|
||||
DbItemKind.RemoteRepo,
|
||||
];
|
||||
|
||||
export const localDbKinds = [
|
||||
DbItemKind.RootLocal,
|
||||
DbItemKind.LocalList,
|
||||
DbItemKind.LocalDatabase,
|
||||
];
|
||||
|
||||
export enum DbListKind {
|
||||
Local = "Local",
|
||||
Remote = "Remote",
|
||||
}
|
||||
|
||||
export interface RootLocalDbItem {
|
||||
kind: DbItemKind.RootLocal;
|
||||
expanded: boolean;
|
||||
@@ -51,7 +70,7 @@ export type DbItem =
|
||||
|
||||
export type RemoteDbItem =
|
||||
| RemoteSystemDefinedListDbItem
|
||||
| RemoteUserDefinedListDbItem
|
||||
| VariantAnalysisUserDefinedListDbItem
|
||||
| RemoteOwnerDbItem
|
||||
| RemoteRepoDbItem;
|
||||
|
||||
@@ -63,8 +82,8 @@ export interface RemoteSystemDefinedListDbItem {
|
||||
listDescription: string;
|
||||
}
|
||||
|
||||
export interface RemoteUserDefinedListDbItem {
|
||||
kind: DbItemKind.RemoteUserDefinedList;
|
||||
export interface VariantAnalysisUserDefinedListDbItem {
|
||||
kind: DbItemKind.VariantAnalysisUserDefinedList;
|
||||
expanded: boolean;
|
||||
selected: boolean;
|
||||
listName: string;
|
||||
@@ -90,10 +109,10 @@ export function isRemoteSystemDefinedListDbItem(
|
||||
return dbItem.kind === DbItemKind.RemoteSystemDefinedList;
|
||||
}
|
||||
|
||||
export function isRemoteUserDefinedListDbItem(
|
||||
export function isVariantAnalysisUserDefinedListDbItem(
|
||||
dbItem: DbItem,
|
||||
): dbItem is RemoteUserDefinedListDbItem {
|
||||
return dbItem.kind === DbItemKind.RemoteUserDefinedList;
|
||||
): dbItem is VariantAnalysisUserDefinedListDbItem {
|
||||
return dbItem.kind === DbItemKind.VariantAnalysisUserDefinedList;
|
||||
}
|
||||
|
||||
export function isRemoteOwnerDbItem(
|
||||
@@ -126,7 +145,36 @@ const SelectableDbItemKinds = [
|
||||
DbItemKind.LocalList,
|
||||
DbItemKind.LocalDatabase,
|
||||
DbItemKind.RemoteSystemDefinedList,
|
||||
DbItemKind.RemoteUserDefinedList,
|
||||
DbItemKind.VariantAnalysisUserDefinedList,
|
||||
DbItemKind.RemoteOwner,
|
||||
DbItemKind.RemoteRepo,
|
||||
];
|
||||
|
||||
export function flattenDbItems(dbItems: DbItem[]): DbItem[] {
|
||||
const allItems: DbItem[] = [];
|
||||
|
||||
for (const dbItem of dbItems) {
|
||||
allItems.push(dbItem);
|
||||
switch (dbItem.kind) {
|
||||
case DbItemKind.RootLocal:
|
||||
allItems.push(...flattenDbItems(dbItem.children));
|
||||
break;
|
||||
case DbItemKind.LocalList:
|
||||
allItems.push(...flattenDbItems(dbItem.databases));
|
||||
break;
|
||||
case DbItemKind.RootRemote:
|
||||
allItems.push(...flattenDbItems(dbItem.children));
|
||||
break;
|
||||
case DbItemKind.VariantAnalysisUserDefinedList:
|
||||
allItems.push(...dbItem.repos);
|
||||
break;
|
||||
case DbItemKind.LocalDatabase:
|
||||
case DbItemKind.RemoteSystemDefinedList:
|
||||
case DbItemKind.RemoteOwner:
|
||||
case DbItemKind.RemoteRepo:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allItems;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,20 @@ import { App } from "../common/app";
|
||||
import { AppEvent, AppEventEmitter } from "../common/events";
|
||||
import { ValueResult } from "../common/value-result";
|
||||
import { DbConfigStore } from "./config/db-config-store";
|
||||
import { DbItem } from "./db-item";
|
||||
import { calculateNewExpandedState } from "./db-item-expansion";
|
||||
import {
|
||||
DbItem,
|
||||
DbItemKind,
|
||||
DbListKind,
|
||||
LocalDatabaseDbItem,
|
||||
LocalListDbItem,
|
||||
VariantAnalysisUserDefinedListDbItem,
|
||||
} from "./db-item";
|
||||
import {
|
||||
updateExpandedItem,
|
||||
replaceExpandedItem,
|
||||
ExpandedDbItem,
|
||||
cleanNonExistentExpandedItems,
|
||||
} from "./db-item-expansion";
|
||||
import {
|
||||
getSelectedDbItem,
|
||||
mapDbItemToSelectedDbItem,
|
||||
@@ -14,8 +26,12 @@ import { DbConfigValidationError } from "./db-validation-errors";
|
||||
export class DbManager {
|
||||
public readonly onDbItemsChanged: AppEvent<void>;
|
||||
private readonly onDbItemsChangesEventEmitter: AppEventEmitter<void>;
|
||||
private static readonly DB_EXPANDED_STATE_KEY = "db_expanded";
|
||||
|
||||
constructor(app: App, private readonly dbConfigStore: DbConfigStore) {
|
||||
constructor(
|
||||
private readonly app: App,
|
||||
private readonly dbConfigStore: DbConfigStore,
|
||||
) {
|
||||
this.onDbItemsChangesEventEmitter = app.createEventEmitter<void>();
|
||||
this.onDbItemsChanged = this.onDbItemsChangesEventEmitter.event;
|
||||
|
||||
@@ -40,9 +56,11 @@ export class DbManager {
|
||||
return ValueResult.fail(configResult.errors);
|
||||
}
|
||||
|
||||
const expandedItems = this.getExpandedItems();
|
||||
|
||||
return ValueResult.ok([
|
||||
createRemoteTree(configResult.value),
|
||||
createLocalTree(configResult.value),
|
||||
createRemoteTree(configResult.value, expandedItems),
|
||||
createLocalTree(configResult.value, expandedItems),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -57,33 +75,157 @@ export class DbManager {
|
||||
}
|
||||
}
|
||||
|
||||
public async updateDbItemExpandedState(
|
||||
public async removeDbItem(dbItem: DbItem): Promise<void> {
|
||||
await this.dbConfigStore.removeDbItem(dbItem);
|
||||
|
||||
await this.removeDbItemFromExpandedState(dbItem);
|
||||
}
|
||||
|
||||
public async removeDbItemFromExpandedState(dbItem: DbItem): Promise<void> {
|
||||
// When collapsing or expanding a list we clean up the expanded state and remove
|
||||
// all items that don't exist anymore.
|
||||
|
||||
await this.updateDbItemExpandedState(dbItem, false);
|
||||
}
|
||||
|
||||
public async addDbItemToExpandedState(dbItem: DbItem): Promise<void> {
|
||||
// When collapsing or expanding a list we clean up the expanded state and remove
|
||||
// all items that don't exist anymore.
|
||||
|
||||
await this.updateDbItemExpandedState(dbItem, true);
|
||||
}
|
||||
|
||||
public async addNewRemoteRepo(
|
||||
nwo: string,
|
||||
parentList?: string,
|
||||
): Promise<void> {
|
||||
await this.dbConfigStore.addRemoteRepo(nwo, parentList);
|
||||
}
|
||||
|
||||
public async addNewRemoteOwner(owner: string): Promise<void> {
|
||||
await this.dbConfigStore.addRemoteOwner(owner);
|
||||
}
|
||||
|
||||
public async addNewList(
|
||||
listKind: DbListKind,
|
||||
listName: string,
|
||||
): Promise<void> {
|
||||
switch (listKind) {
|
||||
case DbListKind.Local:
|
||||
await this.dbConfigStore.addLocalList(listName);
|
||||
break;
|
||||
case DbListKind.Remote:
|
||||
await this.dbConfigStore.addRemoteList(listName);
|
||||
break;
|
||||
default:
|
||||
throw Error(`Unknown list kind '${listKind}'`);
|
||||
}
|
||||
}
|
||||
|
||||
public async renameList(
|
||||
currentDbItem: LocalListDbItem | VariantAnalysisUserDefinedListDbItem,
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
if (currentDbItem.kind === DbItemKind.LocalList) {
|
||||
await this.dbConfigStore.renameLocalList(currentDbItem, newName);
|
||||
} else if (
|
||||
currentDbItem.kind === DbItemKind.VariantAnalysisUserDefinedList
|
||||
) {
|
||||
await this.dbConfigStore.renameRemoteList(currentDbItem, newName);
|
||||
}
|
||||
|
||||
const newDbItem = { ...currentDbItem, listName: newName };
|
||||
const newExpandedItems = replaceExpandedItem(
|
||||
this.getExpandedItems(),
|
||||
currentDbItem,
|
||||
newDbItem,
|
||||
);
|
||||
|
||||
await this.setExpandedItems(newExpandedItems);
|
||||
}
|
||||
|
||||
public async renameLocalDb(
|
||||
currentDbItem: LocalDatabaseDbItem,
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
await this.dbConfigStore.renameLocalDb(
|
||||
currentDbItem,
|
||||
newName,
|
||||
currentDbItem.parentListName,
|
||||
);
|
||||
}
|
||||
|
||||
public doesListExist(listKind: DbListKind, listName: string): boolean {
|
||||
switch (listKind) {
|
||||
case DbListKind.Local:
|
||||
return this.dbConfigStore.doesLocalListExist(listName);
|
||||
case DbListKind.Remote:
|
||||
return this.dbConfigStore.doesRemoteListExist(listName);
|
||||
default:
|
||||
throw Error(`Unknown list kind '${listKind}'`);
|
||||
}
|
||||
}
|
||||
|
||||
public doesRemoteOwnerExist(owner: string): boolean {
|
||||
return this.dbConfigStore.doesRemoteOwnerExist(owner);
|
||||
}
|
||||
|
||||
public doesRemoteRepoExist(nwo: string, listName?: string): boolean {
|
||||
return this.dbConfigStore.doesRemoteDbExist(nwo, listName);
|
||||
}
|
||||
|
||||
public doesLocalDbExist(dbName: string, listName?: string): boolean {
|
||||
return this.dbConfigStore.doesLocalDbExist(dbName, listName);
|
||||
}
|
||||
|
||||
private getExpandedItems(): ExpandedDbItem[] {
|
||||
const items = this.app.workspaceState.get<ExpandedDbItem[]>(
|
||||
DbManager.DB_EXPANDED_STATE_KEY,
|
||||
);
|
||||
|
||||
return items || [];
|
||||
}
|
||||
|
||||
private async setExpandedItems(items: ExpandedDbItem[]): Promise<void> {
|
||||
await this.app.workspaceState.update(
|
||||
DbManager.DB_EXPANDED_STATE_KEY,
|
||||
items,
|
||||
);
|
||||
}
|
||||
|
||||
private async updateExpandedItems(items: ExpandedDbItem[]): Promise<void> {
|
||||
let itemsToStore;
|
||||
|
||||
const dbItemsResult = this.getDbItems();
|
||||
|
||||
if (dbItemsResult.isFailure) {
|
||||
// Log an error but don't throw an exception since if the db items are failing
|
||||
// to be read, then there is a bigger problem than the expanded state.
|
||||
void this.app.logger.log(
|
||||
`Could not read db items when calculating expanded state: ${JSON.stringify(
|
||||
dbItemsResult.errors,
|
||||
)}`,
|
||||
);
|
||||
itemsToStore = items;
|
||||
} else {
|
||||
itemsToStore = cleanNonExistentExpandedItems(items, dbItemsResult.value);
|
||||
}
|
||||
|
||||
await this.setExpandedItems(itemsToStore);
|
||||
}
|
||||
|
||||
private async updateDbItemExpandedState(
|
||||
dbItem: DbItem,
|
||||
itemExpanded: boolean,
|
||||
): Promise<void> {
|
||||
const configResult = this.dbConfigStore.getConfig();
|
||||
if (configResult.isFailure) {
|
||||
throw Error("Cannot update expanded state if config is not loaded");
|
||||
}
|
||||
const currentExpandedItems = this.getExpandedItems();
|
||||
|
||||
const newExpandedItems = calculateNewExpandedState(
|
||||
configResult.value.expanded,
|
||||
const newExpandedItems = updateExpandedItem(
|
||||
currentExpandedItems,
|
||||
dbItem,
|
||||
itemExpanded,
|
||||
);
|
||||
|
||||
await this.dbConfigStore.updateExpandedState(newExpandedItems);
|
||||
}
|
||||
|
||||
public async addNewRemoteList(listName: string): Promise<void> {
|
||||
if (this.dbConfigStore.doesRemoteListExist(listName)) {
|
||||
throw Error(`A list with the name '${listName}' already exists`);
|
||||
}
|
||||
|
||||
await this.dbConfigStore.addRemoteList(listName);
|
||||
}
|
||||
|
||||
public doesRemoteListExist(listName: string): boolean {
|
||||
return this.dbConfigStore.doesRemoteListExist(listName);
|
||||
await this.updateExpandedItems(newExpandedItems);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DbConfigStore } from "./config/db-config-store";
|
||||
import { DbManager } from "./db-manager";
|
||||
import { DbPanel } from "./ui/db-panel";
|
||||
import { DbSelectionDecorationProvider } from "./ui/db-selection-decoration-provider";
|
||||
import { isCanary, isNewQueryRunExperienceEnabled } from "../config";
|
||||
import { isCanary, isVariantAnalysisReposPanelEnabled } from "../config";
|
||||
|
||||
export class DbModule extends DisposableObject {
|
||||
public readonly dbManager: DbManager;
|
||||
@@ -36,7 +36,7 @@ export class DbModule extends DisposableObject {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isCanary() && isNewQueryRunExperienceEnabled();
|
||||
return isCanary() && isVariantAnalysisReposPanelEnabled();
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
DbConfig,
|
||||
ExpandedDbItemKind,
|
||||
LocalDatabase,
|
||||
LocalList,
|
||||
RemoteRepositoryList,
|
||||
@@ -13,31 +12,36 @@ import {
|
||||
RemoteOwnerDbItem,
|
||||
RemoteRepoDbItem,
|
||||
RemoteSystemDefinedListDbItem,
|
||||
RemoteUserDefinedListDbItem,
|
||||
VariantAnalysisUserDefinedListDbItem,
|
||||
RootLocalDbItem,
|
||||
RootRemoteDbItem,
|
||||
} from "./db-item";
|
||||
import { ExpandedDbItem, ExpandedDbItemKind } from "./db-item-expansion";
|
||||
|
||||
export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
|
||||
export function createRemoteTree(
|
||||
dbConfig: DbConfig,
|
||||
expandedItems: ExpandedDbItem[],
|
||||
): RootRemoteDbItem {
|
||||
const systemDefinedLists = [
|
||||
createSystemDefinedList(10, dbConfig),
|
||||
createSystemDefinedList(100, dbConfig),
|
||||
createSystemDefinedList(1000, dbConfig),
|
||||
];
|
||||
|
||||
const userDefinedRepoLists = dbConfig.databases.remote.repositoryLists.map(
|
||||
(r) => createRemoteUserDefinedList(r, dbConfig),
|
||||
);
|
||||
const owners = dbConfig.databases.remote.owners.map((o) =>
|
||||
const userDefinedRepoLists =
|
||||
dbConfig.databases.variantAnalysis.repositoryLists.map((r) =>
|
||||
createVariantAnalysisUserDefinedList(r, dbConfig, expandedItems),
|
||||
);
|
||||
const owners = dbConfig.databases.variantAnalysis.owners.map((o) =>
|
||||
createOwnerItem(o, dbConfig),
|
||||
);
|
||||
const repos = dbConfig.databases.remote.repositories.map((r) =>
|
||||
const repos = dbConfig.databases.variantAnalysis.repositories.map((r) =>
|
||||
createRepoItem(r, dbConfig),
|
||||
);
|
||||
|
||||
const expanded =
|
||||
dbConfig.expanded &&
|
||||
dbConfig.expanded.some((e) => e.kind === ExpandedDbItemKind.RootRemote);
|
||||
const expanded = expandedItems.some(
|
||||
(e) => e.kind === ExpandedDbItemKind.RootRemote,
|
||||
);
|
||||
|
||||
return {
|
||||
kind: DbItemKind.RootRemote,
|
||||
@@ -51,17 +55,20 @@ export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
|
||||
};
|
||||
}
|
||||
|
||||
export function createLocalTree(dbConfig: DbConfig): RootLocalDbItem {
|
||||
export function createLocalTree(
|
||||
dbConfig: DbConfig,
|
||||
expandedItems: ExpandedDbItem[],
|
||||
): RootLocalDbItem {
|
||||
const localLists = dbConfig.databases.local.lists.map((l) =>
|
||||
createLocalList(l, dbConfig),
|
||||
createLocalList(l, dbConfig, expandedItems),
|
||||
);
|
||||
const localDbs = dbConfig.databases.local.databases.map((l) =>
|
||||
createLocalDb(l, dbConfig),
|
||||
);
|
||||
|
||||
const expanded =
|
||||
dbConfig.expanded &&
|
||||
dbConfig.expanded.some((e) => e.kind === ExpandedDbItemKind.RootLocal);
|
||||
const expanded = expandedItems.some(
|
||||
(e) => e.kind === ExpandedDbItemKind.RootLocal,
|
||||
);
|
||||
|
||||
return {
|
||||
kind: DbItemKind.RootLocal,
|
||||
@@ -78,7 +85,8 @@ function createSystemDefinedList(
|
||||
|
||||
const selected =
|
||||
dbConfig.selected &&
|
||||
dbConfig.selected.kind === SelectedDbItemKind.RemoteSystemDefinedList &&
|
||||
dbConfig.selected.kind ===
|
||||
SelectedDbItemKind.VariantAnalysisSystemDefinedList &&
|
||||
dbConfig.selected.listName === listName;
|
||||
|
||||
return {
|
||||
@@ -90,25 +98,25 @@ function createSystemDefinedList(
|
||||
};
|
||||
}
|
||||
|
||||
function createRemoteUserDefinedList(
|
||||
function createVariantAnalysisUserDefinedList(
|
||||
list: RemoteRepositoryList,
|
||||
dbConfig: DbConfig,
|
||||
): RemoteUserDefinedListDbItem {
|
||||
expandedItems: ExpandedDbItem[],
|
||||
): VariantAnalysisUserDefinedListDbItem {
|
||||
const selected =
|
||||
dbConfig.selected &&
|
||||
dbConfig.selected.kind === SelectedDbItemKind.RemoteUserDefinedList &&
|
||||
dbConfig.selected.kind ===
|
||||
SelectedDbItemKind.VariantAnalysisUserDefinedList &&
|
||||
dbConfig.selected.listName === list.name;
|
||||
|
||||
const expanded =
|
||||
dbConfig.expanded &&
|
||||
dbConfig.expanded.some(
|
||||
(e) =>
|
||||
e.kind === ExpandedDbItemKind.RemoteUserDefinedList &&
|
||||
e.listName === list.name,
|
||||
);
|
||||
const expanded = expandedItems.some(
|
||||
(e) =>
|
||||
e.kind === ExpandedDbItemKind.RemoteUserDefinedList &&
|
||||
e.listName === list.name,
|
||||
);
|
||||
|
||||
return {
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
kind: DbItemKind.VariantAnalysisUserDefinedList,
|
||||
listName: list.name,
|
||||
repos: list.repositories.map((r) => createRepoItem(r, dbConfig, list.name)),
|
||||
selected: !!selected,
|
||||
@@ -119,7 +127,7 @@ function createRemoteUserDefinedList(
|
||||
function createOwnerItem(owner: string, dbConfig: DbConfig): RemoteOwnerDbItem {
|
||||
const selected =
|
||||
dbConfig.selected &&
|
||||
dbConfig.selected.kind === SelectedDbItemKind.RemoteOwner &&
|
||||
dbConfig.selected.kind === SelectedDbItemKind.VariantAnalysisOwner &&
|
||||
dbConfig.selected.ownerName === owner;
|
||||
|
||||
return {
|
||||
@@ -136,7 +144,7 @@ function createRepoItem(
|
||||
): RemoteRepoDbItem {
|
||||
const selected =
|
||||
dbConfig.selected &&
|
||||
dbConfig.selected.kind === SelectedDbItemKind.RemoteRepository &&
|
||||
dbConfig.selected.kind === SelectedDbItemKind.VariantAnalysisRepository &&
|
||||
dbConfig.selected.repositoryName === repo &&
|
||||
dbConfig.selected.listName === listName;
|
||||
|
||||
@@ -148,19 +156,21 @@ function createRepoItem(
|
||||
};
|
||||
}
|
||||
|
||||
function createLocalList(list: LocalList, dbConfig: DbConfig): LocalListDbItem {
|
||||
function createLocalList(
|
||||
list: LocalList,
|
||||
dbConfig: DbConfig,
|
||||
expandedItems: ExpandedDbItem[],
|
||||
): LocalListDbItem {
|
||||
const selected =
|
||||
dbConfig.selected &&
|
||||
dbConfig.selected.kind === SelectedDbItemKind.LocalUserDefinedList &&
|
||||
dbConfig.selected.listName === list.name;
|
||||
|
||||
const expanded =
|
||||
dbConfig.expanded &&
|
||||
dbConfig.expanded.some(
|
||||
(e) =>
|
||||
e.kind === ExpandedDbItemKind.LocalUserDefinedList &&
|
||||
e.listName === list.name,
|
||||
);
|
||||
const expanded = expandedItems.some(
|
||||
(e) =>
|
||||
e.kind === ExpandedDbItemKind.LocalUserDefinedList &&
|
||||
e.listName === list.name,
|
||||
);
|
||||
|
||||
return {
|
||||
kind: DbItemKind.LocalList,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Uri } from "vscode";
|
||||
import { REPO_REGEX } from "../pure/helpers-pure";
|
||||
|
||||
/**
|
||||
* The URL pattern is https://github.com/{owner}/{name}/{subpages}.
|
||||
*
|
||||
* This function accepts any URL that matches the pattern above. It also accepts just the
|
||||
* name with owner (NWO): `<owner>/<repo>`.
|
||||
*
|
||||
* @param githubRepo The GitHub repository URL or NWO
|
||||
*
|
||||
* @return true if this looks like a valid GitHub repository URL or NWO
|
||||
*/
|
||||
export function looksLikeGithubRepo(
|
||||
githubRepo: string | undefined,
|
||||
): githubRepo is string {
|
||||
if (!githubRepo) {
|
||||
return false;
|
||||
}
|
||||
if (REPO_REGEX.test(githubRepo) || convertGitHubUrlToNwo(githubRepo)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a GitHub repository URL to the corresponding NWO.
|
||||
* @param githubUrl The GitHub repository URL
|
||||
* @return The corresponding NWO, or undefined if the URL is not valid
|
||||
*/
|
||||
export function convertGitHubUrlToNwo(githubUrl: string): string | undefined {
|
||||
try {
|
||||
const uri = Uri.parse(githubUrl, true);
|
||||
if (uri.scheme !== "https") {
|
||||
return;
|
||||
}
|
||||
if (uri.authority !== "github.com" && uri.authority !== "www.github.com") {
|
||||
return;
|
||||
}
|
||||
const paths = uri.path.split("/").filter((segment: string) => segment);
|
||||
const nwo = `${paths[0]}/${paths[1]}`;
|
||||
if (REPO_REGEX.test(nwo)) {
|
||||
return nwo;
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
// Ignore the error here, since we catch failures at a higher level.
|
||||
// In particular: returning undefined leads to an error in 'promptImportGithubDatabase'.
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export function mapDbItemToTreeViewItem(dbItem: DbItem): DbTreeViewItem {
|
||||
dbItem.listDescription,
|
||||
);
|
||||
|
||||
case DbItemKind.RemoteUserDefinedList:
|
||||
case DbItemKind.VariantAnalysisUserDefinedList:
|
||||
return createDbTreeViewItemUserDefinedList(
|
||||
dbItem,
|
||||
dbItem.listName,
|
||||
|
||||
@@ -1,55 +1,118 @@
|
||||
import { TreeViewExpansionEvent, window, workspace } from "vscode";
|
||||
import { commandRunner } from "../../commandRunner";
|
||||
import {
|
||||
commands,
|
||||
QuickPickItem,
|
||||
TreeView,
|
||||
TreeViewExpansionEvent,
|
||||
Uri,
|
||||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { commandRunner, UserCancellationException } from "../../commandRunner";
|
||||
import {
|
||||
getNwoFromGitHubUrl,
|
||||
isValidGitHubNwo,
|
||||
getOwnerFromGitHubUrl,
|
||||
isValidGitHubOwner,
|
||||
} from "../../common/github-url-identifier-helper";
|
||||
import { showAndLogErrorMessage } from "../../helpers";
|
||||
import { DisposableObject } from "../../pure/disposable-object";
|
||||
import {
|
||||
DbItem,
|
||||
DbItemKind,
|
||||
DbListKind,
|
||||
LocalDatabaseDbItem,
|
||||
LocalListDbItem,
|
||||
remoteDbKinds,
|
||||
VariantAnalysisUserDefinedListDbItem,
|
||||
} from "../db-item";
|
||||
import { getDbItemName } from "../db-item-naming";
|
||||
import { DbManager } from "../db-manager";
|
||||
import { DbTreeDataProvider } from "./db-tree-data-provider";
|
||||
import { DbTreeViewItem } from "./db-tree-view-item";
|
||||
import { getGitHubUrl } from "./db-tree-view-item-action";
|
||||
|
||||
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
|
||||
kind: string;
|
||||
}
|
||||
|
||||
export interface AddListQuickPickItem extends QuickPickItem {
|
||||
kind: DbListKind;
|
||||
}
|
||||
|
||||
export class DbPanel extends DisposableObject {
|
||||
private readonly dataProvider: DbTreeDataProvider;
|
||||
private readonly treeView: TreeView<DbTreeViewItem>;
|
||||
|
||||
public constructor(private readonly dbManager: DbManager) {
|
||||
super();
|
||||
|
||||
this.dataProvider = new DbTreeDataProvider(dbManager);
|
||||
|
||||
const treeView = window.createTreeView("codeQLDatabasesExperimental", {
|
||||
this.treeView = window.createTreeView("codeQLVariantAnalysisRepositories", {
|
||||
treeDataProvider: this.dataProvider,
|
||||
canSelectMany: false,
|
||||
});
|
||||
|
||||
this.push(
|
||||
treeView.onDidCollapseElement(async (e) => {
|
||||
this.treeView.onDidCollapseElement(async (e) => {
|
||||
await this.onDidCollapseElement(e);
|
||||
}),
|
||||
);
|
||||
this.push(
|
||||
treeView.onDidExpandElement(async (e) => {
|
||||
this.treeView.onDidExpandElement(async (e) => {
|
||||
await this.onDidExpandElement(e);
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(treeView);
|
||||
this.push(this.treeView);
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
this.push(
|
||||
commandRunner("codeQLDatabasesExperimental.openConfigFile", () =>
|
||||
commandRunner("codeQLVariantAnalysisRepositories.openConfigFile", () =>
|
||||
this.openConfigFile(),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLDatabasesExperimental.addNewList", () =>
|
||||
this.addNewRemoteList(),
|
||||
commandRunner("codeQLVariantAnalysisRepositories.addNewDatabase", () =>
|
||||
this.addNewRemoteDatabase(),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLVariantAnalysisRepositories.addNewList", () =>
|
||||
this.addNewList(),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabasesExperimental.setSelectedItem",
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItem",
|
||||
(treeViewItem: DbTreeViewItem) => this.setSelectedItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItemContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.setSelectedItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.openOnGitHub(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.renameItemContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.renameItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.removeItemContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.removeItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async openConfigFile(): Promise<void> {
|
||||
@@ -58,21 +121,160 @@ export class DbPanel extends DisposableObject {
|
||||
await window.showTextDocument(document);
|
||||
}
|
||||
|
||||
private async addNewRemoteList(): Promise<void> {
|
||||
private async addNewRemoteDatabase(): Promise<void> {
|
||||
const highlightedItem = await this.getHighlightedDbItem();
|
||||
|
||||
if (highlightedItem?.kind === DbItemKind.VariantAnalysisUserDefinedList) {
|
||||
await this.addNewRemoteRepo(highlightedItem.listName);
|
||||
} else if (
|
||||
highlightedItem?.kind === DbItemKind.RemoteRepo &&
|
||||
highlightedItem.parentListName
|
||||
) {
|
||||
await this.addNewRemoteRepo(highlightedItem.parentListName);
|
||||
} else {
|
||||
const quickPickItems = [
|
||||
{
|
||||
label: "$(repo) From a GitHub repository",
|
||||
detail: "Add a remote repository from GitHub",
|
||||
alwaysShow: true,
|
||||
kind: "repo",
|
||||
},
|
||||
{
|
||||
label: "$(organization) All repositories of a GitHub org or owner",
|
||||
detail:
|
||||
"Add a remote list of repositories from a GitHub organization/owner",
|
||||
alwaysShow: true,
|
||||
kind: "owner",
|
||||
},
|
||||
];
|
||||
const databaseKind =
|
||||
await window.showQuickPick<RemoteDatabaseQuickPickItem>(
|
||||
quickPickItems,
|
||||
{
|
||||
title: "Add a remote repository",
|
||||
placeHolder: "Select an option",
|
||||
ignoreFocusOut: true,
|
||||
},
|
||||
);
|
||||
if (!databaseKind) {
|
||||
// We don't need to display a warning pop-up in this case, since the user just escaped out of the operation.
|
||||
// We set 'true' to make this a silent exception.
|
||||
throw new UserCancellationException("No repository selected", true);
|
||||
}
|
||||
if (databaseKind.kind === "repo") {
|
||||
await this.addNewRemoteRepo();
|
||||
} else if (databaseKind.kind === "owner") {
|
||||
await this.addNewRemoteOwner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async addNewRemoteRepo(parentList?: string): Promise<void> {
|
||||
const repoName = await window.showInputBox({
|
||||
title: "Add a remote repository",
|
||||
prompt: "Insert a GitHub repository URL or name with owner",
|
||||
placeHolder: "github.com/<owner>/<repo> or <owner>/<repo>",
|
||||
});
|
||||
if (!repoName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nwo = getNwoFromGitHubUrl(repoName) || repoName;
|
||||
if (!isValidGitHubNwo(nwo)) {
|
||||
void showAndLogErrorMessage(`Invalid GitHub repository: ${repoName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dbManager.doesRemoteRepoExist(nwo, parentList)) {
|
||||
void showAndLogErrorMessage(`The repository '${nwo}' already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dbManager.addNewRemoteRepo(nwo, parentList);
|
||||
}
|
||||
|
||||
private async addNewRemoteOwner(): Promise<void> {
|
||||
const ownerName = await window.showInputBox({
|
||||
title: "Add all repositories of a GitHub org or owner",
|
||||
prompt: "Insert a GitHub organization or owner name",
|
||||
placeHolder: "github.com/<owner> or <owner>",
|
||||
});
|
||||
|
||||
if (!ownerName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = getOwnerFromGitHubUrl(ownerName) || ownerName;
|
||||
if (!isValidGitHubOwner(owner)) {
|
||||
void showAndLogErrorMessage(`Invalid user or organization: ${owner}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dbManager.doesRemoteOwnerExist(owner)) {
|
||||
void showAndLogErrorMessage(`The owner '${owner}' already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dbManager.addNewRemoteOwner(owner);
|
||||
}
|
||||
|
||||
private async addNewList(): Promise<void> {
|
||||
const listKind = await this.getAddNewListKind();
|
||||
|
||||
const listName = await window.showInputBox({
|
||||
prompt: "Enter a name for the new list",
|
||||
placeHolder: "example-list",
|
||||
});
|
||||
if (listName === undefined) {
|
||||
if (listName === undefined || listName === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dbManager.doesRemoteListExist(listName)) {
|
||||
void showAndLogErrorMessage(
|
||||
`A list with the name '${listName}' already exists`,
|
||||
);
|
||||
if (this.dbManager.doesListExist(listKind, listName)) {
|
||||
void showAndLogErrorMessage(`The list '${listName}' already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dbManager.addNewList(listKind, listName);
|
||||
}
|
||||
|
||||
private async getAddNewListKind(): Promise<DbListKind> {
|
||||
const highlightedItem = await this.getHighlightedDbItem();
|
||||
if (highlightedItem) {
|
||||
return remoteDbKinds.includes(highlightedItem.kind)
|
||||
? DbListKind.Remote
|
||||
: DbListKind.Local;
|
||||
} else {
|
||||
await this.dbManager.addNewRemoteList(listName);
|
||||
const quickPickItems = [
|
||||
{
|
||||
label: "$(cloud) Remote",
|
||||
detail: "Add a remote database from GitHub",
|
||||
alwaysShow: true,
|
||||
kind: DbListKind.Remote,
|
||||
},
|
||||
{
|
||||
label: "$(database) Local",
|
||||
detail: "Import a database from the cloud or a local file",
|
||||
alwaysShow: true,
|
||||
kind: DbListKind.Local,
|
||||
},
|
||||
];
|
||||
const selectedOption = await window.showQuickPick<AddListQuickPickItem>(
|
||||
quickPickItems,
|
||||
{
|
||||
title: "Add a new database",
|
||||
ignoreFocusOut: true,
|
||||
},
|
||||
);
|
||||
if (!selectedOption) {
|
||||
// We don't need to display a warning pop-up in this case, since the user just escaped out of the operation.
|
||||
// We set 'true' to make this a silent exception.
|
||||
throw new UserCancellationException(
|
||||
"No database list kind selected",
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
return selectedOption.kind;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +287,97 @@ export class DbPanel extends DisposableObject {
|
||||
await this.dbManager.setSelectedDbItem(treeViewItem.dbItem);
|
||||
}
|
||||
|
||||
private async renameItem(treeViewItem: DbTreeViewItem): Promise<void> {
|
||||
const dbItem = treeViewItem.dbItem;
|
||||
if (dbItem === undefined) {
|
||||
throw new Error(
|
||||
"Not a database item that can be renamed. Please select a valid item.",
|
||||
);
|
||||
}
|
||||
|
||||
const oldName = getDbItemName(dbItem);
|
||||
|
||||
const newName = await window.showInputBox({
|
||||
prompt: "Enter the new name",
|
||||
value: oldName,
|
||||
});
|
||||
|
||||
if (newName === undefined || newName === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (dbItem.kind) {
|
||||
case DbItemKind.LocalList:
|
||||
await this.renameLocalListItem(dbItem, newName);
|
||||
break;
|
||||
case DbItemKind.LocalDatabase:
|
||||
await this.renameLocalDatabaseItem(dbItem, newName);
|
||||
break;
|
||||
case DbItemKind.VariantAnalysisUserDefinedList:
|
||||
await this.renameVariantAnalysisUserDefinedListItem(dbItem, newName);
|
||||
break;
|
||||
default:
|
||||
throw Error(`Action not allowed for the '${dbItem.kind}' db item kind`);
|
||||
}
|
||||
}
|
||||
|
||||
private async renameLocalListItem(
|
||||
dbItem: LocalListDbItem,
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
if (dbItem.listName === newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dbManager.doesListExist(DbListKind.Local, newName)) {
|
||||
void showAndLogErrorMessage(`The list '${newName}' already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dbManager.renameList(dbItem, newName);
|
||||
}
|
||||
|
||||
private async renameLocalDatabaseItem(
|
||||
dbItem: LocalDatabaseDbItem,
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
if (dbItem.databaseName === newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dbManager.doesLocalDbExist(newName, dbItem.parentListName)) {
|
||||
void showAndLogErrorMessage(`The database '${newName}' already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dbManager.renameLocalDb(dbItem, newName);
|
||||
}
|
||||
|
||||
private async renameVariantAnalysisUserDefinedListItem(
|
||||
dbItem: VariantAnalysisUserDefinedListDbItem,
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
if (dbItem.listName === newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dbManager.doesListExist(DbListKind.Remote, newName)) {
|
||||
void showAndLogErrorMessage(`The list '${newName}' already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dbManager.renameList(dbItem, newName);
|
||||
}
|
||||
|
||||
private async removeItem(treeViewItem: DbTreeViewItem): Promise<void> {
|
||||
if (treeViewItem.dbItem === undefined) {
|
||||
throw new Error(
|
||||
"Not a removable database item. Please select a valid item.",
|
||||
);
|
||||
}
|
||||
await this.dbManager.removeDbItem(treeViewItem.dbItem);
|
||||
}
|
||||
|
||||
private async onDidCollapseElement(
|
||||
event: TreeViewExpansionEvent<DbTreeViewItem>,
|
||||
): Promise<void> {
|
||||
@@ -93,7 +386,7 @@ export class DbPanel extends DisposableObject {
|
||||
throw Error("Expected a database item.");
|
||||
}
|
||||
|
||||
await this.dbManager.updateDbItemExpandedState(event.element.dbItem, false);
|
||||
await this.dbManager.removeDbItemFromExpandedState(event.element.dbItem);
|
||||
}
|
||||
|
||||
private async onDidExpandElement(
|
||||
@@ -104,6 +397,32 @@ export class DbPanel extends DisposableObject {
|
||||
throw Error("Expected a database item.");
|
||||
}
|
||||
|
||||
await this.dbManager.updateDbItemExpandedState(event.element.dbItem, true);
|
||||
await this.dbManager.addDbItemToExpandedState(event.element.dbItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently highlighted database item in the tree view.
|
||||
* The VS Code API calls this the "selection", but we already have a notion of selection
|
||||
* (i.e. which item has a check mark next to it), so we call this "highlighted".
|
||||
*
|
||||
* @returns The highlighted database item, or `undefined` if no item is highlighted.
|
||||
*/
|
||||
private async getHighlightedDbItem(): Promise<DbItem | undefined> {
|
||||
// You can only select one item at a time, so selection[0] gives the selection
|
||||
return this.treeView.selection[0]?.dbItem;
|
||||
}
|
||||
|
||||
private async openOnGitHub(treeViewItem: DbTreeViewItem): Promise<void> {
|
||||
if (treeViewItem.dbItem === undefined) {
|
||||
throw new Error("Unable to open on GitHub. Please select a valid item.");
|
||||
}
|
||||
const githubUrl = getGitHubUrl(treeViewItem.dbItem);
|
||||
if (!githubUrl) {
|
||||
throw new Error(
|
||||
"Unable to open on GitHub. Please select a remote repository or owner.",
|
||||
);
|
||||
}
|
||||
|
||||
await commands.executeCommand("vscode.open", Uri.parse(githubUrl));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,16 @@ import {
|
||||
ProviderResult,
|
||||
Uri,
|
||||
} from "vscode";
|
||||
import { SELECTED_DB_ITEM_RESOURCE_URI } from "./db-tree-view-item";
|
||||
|
||||
export class DbSelectionDecorationProvider implements FileDecorationProvider {
|
||||
provideFileDecoration(
|
||||
uri: Uri,
|
||||
_token: CancellationToken,
|
||||
): ProviderResult<FileDecoration> {
|
||||
if (uri?.query === "selected=true") {
|
||||
if (uri.toString(true) === SELECTED_DB_ITEM_RESOURCE_URI) {
|
||||
return {
|
||||
badge: "●",
|
||||
badge: "✓",
|
||||
tooltip: "Currently selected",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { DbItem, DbItemKind, isSelectableDbItem } from "../db-item";
|
||||
|
||||
export type DbTreeViewItemAction =
|
||||
| "canBeSelected"
|
||||
| "canBeRemoved"
|
||||
| "canBeRenamed"
|
||||
| "canBeOpenedOnGitHub";
|
||||
|
||||
export function getDbItemActions(dbItem: DbItem): DbTreeViewItemAction[] {
|
||||
const actions: DbTreeViewItemAction[] = [];
|
||||
|
||||
if (canBeSelected(dbItem)) {
|
||||
actions.push("canBeSelected");
|
||||
}
|
||||
if (canBeRemoved(dbItem)) {
|
||||
actions.push("canBeRemoved");
|
||||
}
|
||||
if (canBeRenamed(dbItem)) {
|
||||
actions.push("canBeRenamed");
|
||||
}
|
||||
if (canBeOpenedOnGitHub(dbItem)) {
|
||||
actions.push("canBeOpenedOnGitHub");
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
const dbItemKindsThatCanBeRemoved = [
|
||||
DbItemKind.LocalList,
|
||||
DbItemKind.VariantAnalysisUserDefinedList,
|
||||
DbItemKind.LocalDatabase,
|
||||
DbItemKind.RemoteRepo,
|
||||
DbItemKind.RemoteOwner,
|
||||
];
|
||||
|
||||
const dbItemKindsThatCanBeRenamed = [
|
||||
DbItemKind.LocalList,
|
||||
DbItemKind.VariantAnalysisUserDefinedList,
|
||||
DbItemKind.LocalDatabase,
|
||||
];
|
||||
|
||||
const dbItemKindsThatCanBeOpenedOnGitHub = [
|
||||
DbItemKind.RemoteOwner,
|
||||
DbItemKind.RemoteRepo,
|
||||
];
|
||||
|
||||
function canBeSelected(dbItem: DbItem): boolean {
|
||||
return isSelectableDbItem(dbItem) && !dbItem.selected;
|
||||
}
|
||||
|
||||
function canBeRemoved(dbItem: DbItem): boolean {
|
||||
return dbItemKindsThatCanBeRemoved.includes(dbItem.kind);
|
||||
}
|
||||
|
||||
function canBeRenamed(dbItem: DbItem): boolean {
|
||||
return dbItemKindsThatCanBeRenamed.includes(dbItem.kind);
|
||||
}
|
||||
|
||||
function canBeOpenedOnGitHub(dbItem: DbItem): boolean {
|
||||
return dbItemKindsThatCanBeOpenedOnGitHub.includes(dbItem.kind);
|
||||
}
|
||||
|
||||
export function getGitHubUrl(dbItem: DbItem): string | undefined {
|
||||
switch (dbItem.kind) {
|
||||
case DbItemKind.RemoteOwner:
|
||||
return `https://github.com/${dbItem.ownerName}`;
|
||||
case DbItemKind.RemoteRepo:
|
||||
return `https://github.com/${dbItem.repoFullName}`;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,13 @@ import {
|
||||
RemoteOwnerDbItem,
|
||||
RemoteRepoDbItem,
|
||||
RemoteSystemDefinedListDbItem,
|
||||
RemoteUserDefinedListDbItem,
|
||||
VariantAnalysisUserDefinedListDbItem,
|
||||
RootLocalDbItem,
|
||||
RootRemoteDbItem,
|
||||
} from "../db-item";
|
||||
import { getDbItemActions } from "./db-tree-view-item-action";
|
||||
|
||||
export const SELECTED_DB_ITEM_RESOURCE_URI = "codeql://databases?selected=true";
|
||||
|
||||
/**
|
||||
* Represents an item in the database tree view. This item could be
|
||||
@@ -30,18 +33,21 @@ export class DbTreeViewItem extends vscode.TreeItem {
|
||||
) {
|
||||
super(label, collapsibleState);
|
||||
|
||||
if (dbItem && isSelectableDbItem(dbItem)) {
|
||||
if (dbItem.selected) {
|
||||
if (dbItem) {
|
||||
this.contextValue = getContextValue(dbItem);
|
||||
if (isSelectableDbItem(dbItem) && dbItem.selected) {
|
||||
// Define the resource id to drive the UI to render this item as selected.
|
||||
this.resourceUri = vscode.Uri.parse("codeql://databases?selected=true");
|
||||
} else {
|
||||
// Define a context value to drive the UI to show an action to select the item.
|
||||
this.contextValue = "selectableDbItem";
|
||||
this.resourceUri = vscode.Uri.parse(SELECTED_DB_ITEM_RESOURCE_URI);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getContextValue(dbItem: DbItem): string | undefined {
|
||||
const actions = getDbItemActions(dbItem);
|
||||
return actions.length > 0 ? actions.join(",") : undefined;
|
||||
}
|
||||
|
||||
export function createDbTreeViewItemError(
|
||||
label: string,
|
||||
tooltip: string,
|
||||
@@ -91,7 +97,7 @@ export function createDbTreeViewItemSystemDefinedList(
|
||||
}
|
||||
|
||||
export function createDbTreeViewItemUserDefinedList(
|
||||
dbItem: LocalListDbItem | RemoteUserDefinedListDbItem,
|
||||
dbItem: LocalListDbItem | VariantAnalysisUserDefinedListDbItem,
|
||||
listName: string,
|
||||
children: DbTreeViewItem[],
|
||||
): DbTreeViewItem {
|
||||
|
||||
@@ -591,7 +591,7 @@ async function activateWithInstalledDistribution(
|
||||
qs,
|
||||
getContextStoragePath(ctx),
|
||||
ctx.extensionPath,
|
||||
() => Credentials.initialize(ctx),
|
||||
() => Credentials.initialize(),
|
||||
);
|
||||
databaseUI.init();
|
||||
ctx.subscriptions.push(databaseUI);
|
||||
@@ -642,7 +642,7 @@ async function activateWithInstalledDistribution(
|
||||
cliServer,
|
||||
variantAnalysisStorageDir,
|
||||
variantAnalysisResultsManager,
|
||||
dbModule?.dbManager, // the dbModule is only needed when the newQueryRunExperience is enabled
|
||||
dbModule?.dbManager, // the dbModule is only needed when variantAnalysisReposPanel is enabled
|
||||
);
|
||||
ctx.subscriptions.push(variantAnalysisManager);
|
||||
ctx.subscriptions.push(variantAnalysisResultsManager);
|
||||
@@ -1164,6 +1164,15 @@ async function activateWithInstalledDistribution(
|
||||
}),
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner(
|
||||
"codeQL.openVariantAnalysisLogs",
|
||||
async (variantAnalysisId: number) => {
|
||||
await variantAnalysisManager.openVariantAnalysisLogs(variantAnalysisId);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner(
|
||||
"codeQL.copyVariantAnalysisRepoList",
|
||||
@@ -1236,7 +1245,7 @@ async function activateWithInstalledDistribution(
|
||||
commandRunner(
|
||||
"codeQL.exportRemoteQueryResults",
|
||||
async (queryId: string) => {
|
||||
await exportRemoteQueryResults(qhm, rqm, ctx, queryId);
|
||||
await exportRemoteQueryResults(qhm, rqm, queryId);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -1251,7 +1260,6 @@ async function activateWithInstalledDistribution(
|
||||
filterSort?: RepositoriesFilterSortStateWithIds,
|
||||
) => {
|
||||
await exportVariantAnalysisResults(
|
||||
ctx,
|
||||
variantAnalysisManager,
|
||||
variantAnalysisId,
|
||||
filterSort,
|
||||
@@ -1356,7 +1364,7 @@ async function activateWithInstalledDistribution(
|
||||
"codeQL.chooseDatabaseGithub",
|
||||
async (progress: ProgressCallback, token: CancellationToken) => {
|
||||
const credentials = isCanary()
|
||||
? await Credentials.initialize(ctx)
|
||||
? await Credentials.initialize()
|
||||
: undefined;
|
||||
await databaseUI.handleChooseDatabaseGithub(
|
||||
credentials,
|
||||
@@ -1411,7 +1419,7 @@ async function activateWithInstalledDistribution(
|
||||
* Credentials for authenticating to GitHub.
|
||||
* These are used when making API calls.
|
||||
*/
|
||||
const credentials = await Credentials.initialize(ctx);
|
||||
const credentials = await Credentials.initialize();
|
||||
const octokit = await credentials.getOctokit();
|
||||
const userInfo = await octokit.users.getAuthenticated();
|
||||
void showAndLogInformationMessage(
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
writeFile,
|
||||
opendir,
|
||||
} from "fs-extra";
|
||||
import * as glob from "glob-promise";
|
||||
import { promise as glob } from "glob-promise";
|
||||
import { load } from "js-yaml";
|
||||
import { join, basename } from "path";
|
||||
import { dirSync } from "tmp-promise";
|
||||
|
||||
@@ -59,18 +59,22 @@ export class SummaryLanguageSupport extends DisposableObject {
|
||||
super();
|
||||
|
||||
this.push(
|
||||
window.onDidChangeActiveTextEditor(this.handleDidChangeActiveTextEditor),
|
||||
);
|
||||
this.push(
|
||||
window.onDidChangeTextEditorSelection(
|
||||
this.handleDidChangeTextEditorSelection,
|
||||
window.onDidChangeActiveTextEditor(
|
||||
this.handleDidChangeActiveTextEditor.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
workspace.onDidCloseTextDocument(this.handleDidCloseTextDocument),
|
||||
window.onDidChangeTextEditorSelection(
|
||||
this.handleDidChangeTextEditorSelection.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
workspace.onDidCloseTextDocument(
|
||||
this.handleDidCloseTextDocument.bind(this),
|
||||
),
|
||||
);
|
||||
|
||||
this.push(commandRunner("codeQL.gotoQL", this.handleGotoQL));
|
||||
this.push(commandRunner("codeQL.gotoQL", this.handleGotoQL.bind(this)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { pathExists, stat, readdir } from "fs-extra";
|
||||
import { join } from "path";
|
||||
import { join, resolve } from "path";
|
||||
|
||||
/**
|
||||
* Recursively finds all .ql files in this set of Uris.
|
||||
@@ -50,3 +50,20 @@ export async function getDirectoryNamesInsidePath(
|
||||
|
||||
return dirNames;
|
||||
}
|
||||
|
||||
export function pathsEqual(
|
||||
path1: string,
|
||||
path2: string,
|
||||
platform: NodeJS.Platform,
|
||||
): boolean {
|
||||
// On Windows, "C:/", "C:\", and "c:/" are all equivalent. We need
|
||||
// to normalize the paths to ensure they all get resolved to the
|
||||
// same format. On Windows, we also need to do the comparison
|
||||
// case-insensitively.
|
||||
path1 = resolve(path1);
|
||||
path2 = resolve(path2);
|
||||
if (platform === "win32") {
|
||||
return path1.toLowerCase() === path2.toLowerCase();
|
||||
}
|
||||
return path1 === path2;
|
||||
}
|
||||
|
||||
@@ -397,7 +397,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
private readonly variantAnalysisManager: VariantAnalysisManager,
|
||||
private readonly evalLogViewer: EvalLogViewer,
|
||||
private readonly queryStorageDir: string,
|
||||
private readonly ctx: ExtensionContext,
|
||||
ctx: ExtensionContext,
|
||||
private readonly queryHistoryConfigListener: QueryHistoryConfig,
|
||||
private readonly labelProvider: HistoryItemLabelProvider,
|
||||
private readonly doCompareCallback: (
|
||||
@@ -633,7 +633,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
private getCredentials() {
|
||||
return Credentials.initialize(this.ctx);
|
||||
return Credentials.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { pathExists } from "fs-extra";
|
||||
import { EOL } from "os";
|
||||
import { extname } from "path";
|
||||
import { CancellationToken, ExtensionContext } from "vscode";
|
||||
import { CancellationToken } from "vscode";
|
||||
|
||||
import { Credentials } from "../authentication";
|
||||
import { Logger } from "../common";
|
||||
@@ -26,7 +26,6 @@ export class AnalysesResultsManager {
|
||||
private readonly analysesResults: Map<string, AnalysisResults[]>;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
readonly storagePath: string,
|
||||
private readonly logger: Logger,
|
||||
@@ -43,7 +42,7 @@ export class AnalysesResultsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
const credentials = await Credentials.initialize();
|
||||
|
||||
void this.logger.log(
|
||||
`Downloading and processing results for ${analysisSummary.nwo}`,
|
||||
@@ -77,7 +76,7 @@ export class AnalysesResultsManager {
|
||||
(x) => !this.isAnalysisInMemory(x),
|
||||
);
|
||||
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
const credentials = await Credentials.initialize();
|
||||
|
||||
void this.logger.log("Downloading and processing analyses results");
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ensureDir, writeFile } from "fs-extra";
|
||||
import {
|
||||
commands,
|
||||
CancellationToken,
|
||||
ExtensionContext,
|
||||
Uri,
|
||||
ViewColumn,
|
||||
window,
|
||||
@@ -74,7 +73,6 @@ export async function exportSelectedRemoteQueryResults(
|
||||
export async function exportRemoteQueryResults(
|
||||
queryHistoryManager: QueryHistoryManager,
|
||||
remoteQueriesManager: RemoteQueriesManager,
|
||||
ctx: ExtensionContext,
|
||||
queryId: string,
|
||||
): Promise<void> {
|
||||
const queryHistoryItem = queryHistoryManager.getRemoteQueryById(queryId);
|
||||
@@ -107,7 +105,6 @@ export async function exportRemoteQueryResults(
|
||||
const exportedResultsDirectory = join(exportDirectory, "exported-results");
|
||||
|
||||
await exportRemoteQueryAnalysisResults(
|
||||
ctx,
|
||||
exportedResultsDirectory,
|
||||
query,
|
||||
analysesResults,
|
||||
@@ -116,7 +113,6 @@ export async function exportRemoteQueryResults(
|
||||
}
|
||||
|
||||
export async function exportRemoteQueryAnalysisResults(
|
||||
ctx: ExtensionContext,
|
||||
exportedResultsPath: string,
|
||||
query: RemoteQuery,
|
||||
analysesResults: AnalysisResults[],
|
||||
@@ -126,7 +122,6 @@ export async function exportRemoteQueryAnalysisResults(
|
||||
const markdownFiles = generateMarkdown(query, analysesResults, exportFormat);
|
||||
|
||||
await exportResults(
|
||||
ctx,
|
||||
exportedResultsPath,
|
||||
description,
|
||||
markdownFiles,
|
||||
@@ -141,7 +136,6 @@ const MAX_VARIANT_ANALYSIS_EXPORT_PROGRESS_STEPS = 2;
|
||||
* The user is prompted to select the export format.
|
||||
*/
|
||||
export async function exportVariantAnalysisResults(
|
||||
ctx: ExtensionContext,
|
||||
variantAnalysisManager: VariantAnalysisManager,
|
||||
variantAnalysisId: number,
|
||||
filterSort: RepositoriesFilterSortStateWithIds | undefined,
|
||||
@@ -187,6 +181,17 @@ export async function exportVariantAnalysisResults(
|
||||
throw new UserCancellationException("Cancelled");
|
||||
}
|
||||
|
||||
const repositories = filterAndSortRepositoriesWithResults(
|
||||
variantAnalysis.scannedRepos,
|
||||
filterSort,
|
||||
)?.filter(
|
||||
(repo) =>
|
||||
repo.resultCount &&
|
||||
repoStates.find((r) => r.repositoryId === repo.repository.id)
|
||||
?.downloadStatus ===
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
);
|
||||
|
||||
async function* getAnalysesResults(): AsyncGenerator<
|
||||
[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]
|
||||
> {
|
||||
@@ -194,38 +199,11 @@ export async function exportVariantAnalysisResults(
|
||||
return;
|
||||
}
|
||||
|
||||
const repositories = filterAndSortRepositoriesWithResults(
|
||||
variantAnalysis.scannedRepos,
|
||||
filterSort,
|
||||
);
|
||||
if (!repositories) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const repo of repositories) {
|
||||
const repoState = repoStates.find(
|
||||
(r) => r.repositoryId === repo.repository.id,
|
||||
);
|
||||
|
||||
// Do not export if it has not yet completed or the download has not yet succeeded.
|
||||
if (
|
||||
repoState?.downloadStatus !==
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (repo.resultCount == 0) {
|
||||
yield [
|
||||
repo,
|
||||
{
|
||||
variantAnalysisId: variantAnalysis.id,
|
||||
repositoryId: repo.repository.id,
|
||||
},
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await variantAnalysisManager.loadResults(
|
||||
variantAnalysis.id,
|
||||
repo.repository.fullName,
|
||||
@@ -255,10 +233,10 @@ export async function exportVariantAnalysisResults(
|
||||
);
|
||||
|
||||
await exportVariantAnalysisAnalysisResults(
|
||||
ctx,
|
||||
exportedResultsDirectory,
|
||||
variantAnalysis,
|
||||
getAnalysesResults(),
|
||||
repositories?.length ?? 0,
|
||||
exportFormat,
|
||||
progress,
|
||||
token,
|
||||
@@ -266,12 +244,12 @@ export async function exportVariantAnalysisResults(
|
||||
}
|
||||
|
||||
export async function exportVariantAnalysisAnalysisResults(
|
||||
ctx: ExtensionContext,
|
||||
exportedResultsPath: string,
|
||||
variantAnalysis: VariantAnalysis,
|
||||
analysesResults: AsyncIterable<
|
||||
[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]
|
||||
>,
|
||||
expectedAnalysesResultsCount: number,
|
||||
exportFormat: "gist" | "local",
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
@@ -289,6 +267,7 @@ export async function exportVariantAnalysisAnalysisResults(
|
||||
const { markdownFiles, summaries } = await generateVariantAnalysisMarkdown(
|
||||
variantAnalysis,
|
||||
analysesResults,
|
||||
expectedAnalysesResultsCount,
|
||||
exportFormat,
|
||||
);
|
||||
const description = buildVariantAnalysisGistDescription(
|
||||
@@ -297,7 +276,6 @@ export async function exportVariantAnalysisAnalysisResults(
|
||||
);
|
||||
|
||||
await exportResults(
|
||||
ctx,
|
||||
exportedResultsPath,
|
||||
description,
|
||||
markdownFiles,
|
||||
@@ -341,7 +319,6 @@ async function determineExportFormat(): Promise<"gist" | "local" | undefined> {
|
||||
}
|
||||
|
||||
export async function exportResults(
|
||||
ctx: ExtensionContext,
|
||||
exportedResultsPath: string,
|
||||
description: string,
|
||||
markdownFiles: MarkdownFile[],
|
||||
@@ -354,7 +331,7 @@ export async function exportResults(
|
||||
}
|
||||
|
||||
if (exportFormat === "gist") {
|
||||
await exportToGist(ctx, description, markdownFiles, progress, token);
|
||||
await exportToGist(description, markdownFiles, progress, token);
|
||||
} else if (exportFormat === "local") {
|
||||
await exportToLocalMarkdown(
|
||||
exportedResultsPath,
|
||||
@@ -366,7 +343,6 @@ export async function exportResults(
|
||||
}
|
||||
|
||||
export async function exportToGist(
|
||||
ctx: ExtensionContext,
|
||||
description: string,
|
||||
markdownFiles: MarkdownFile[],
|
||||
progress?: ProgressCallback,
|
||||
@@ -378,7 +354,7 @@ export async function exportToGist(
|
||||
message: "Creating Gist",
|
||||
});
|
||||
|
||||
const credentials = await Credentials.initialize(ctx);
|
||||
const credentials = await Credentials.initialize();
|
||||
|
||||
if (token?.isCancellationRequested) {
|
||||
throw new UserCancellationException("Cancelled");
|
||||
|
||||
@@ -81,20 +81,19 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
private readonly view: RemoteQueriesView;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
ctx: ExtensionContext,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly storagePath: string,
|
||||
logger: Logger,
|
||||
) {
|
||||
super();
|
||||
this.analysesResultsManager = new AnalysesResultsManager(
|
||||
ctx,
|
||||
cliServer,
|
||||
storagePath,
|
||||
logger,
|
||||
);
|
||||
this.view = new RemoteQueriesView(ctx, logger, this.analysesResultsManager);
|
||||
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
|
||||
this.remoteQueriesMonitor = new RemoteQueriesMonitor(logger);
|
||||
|
||||
this.remoteQueryAddedEventEmitter = this.push(
|
||||
new EventEmitter<NewQueryEvent>(),
|
||||
@@ -160,7 +159,7 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
const credentials = await Credentials.initialize();
|
||||
|
||||
const {
|
||||
actionBranch,
|
||||
@@ -218,7 +217,7 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
remoteQuery: RemoteQuery,
|
||||
cancellationToken: CancellationToken,
|
||||
): Promise<void> {
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
const credentials = await Credentials.initialize();
|
||||
|
||||
const queryWorkflowResult = await this.remoteQueriesMonitor.monitorQuery(
|
||||
remoteQuery,
|
||||
|
||||
@@ -94,18 +94,25 @@ export async function generateVariantAnalysisMarkdown(
|
||||
results: AsyncIterable<
|
||||
[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]
|
||||
>,
|
||||
expectedResultsCount: number,
|
||||
linkType: MarkdownLinkType,
|
||||
): Promise<VariantAnalysisMarkdown> {
|
||||
const resultsFiles: MarkdownFile[] = [];
|
||||
const summaries: RepositorySummary[] = [];
|
||||
|
||||
for await (const [scannedRepo, result] of results) {
|
||||
if (!scannedRepo.resultCount || scannedRepo.resultCount === 0) {
|
||||
if (!scannedRepo.resultCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Append nwo and results count to the summary table
|
||||
const fullName = scannedRepo.repository.fullName;
|
||||
const fileName = createFileName(fullName);
|
||||
const fileName = createVariantAnalysisFileName(
|
||||
fullName,
|
||||
resultsFiles.length,
|
||||
expectedResultsCount,
|
||||
linkType,
|
||||
);
|
||||
summaries.push({
|
||||
fileName,
|
||||
repository: scannedRepo.repository,
|
||||
@@ -482,6 +489,32 @@ function createFileName(nwo: string) {
|
||||
return `${owner}-${repo}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the name of the markdown file for a given repository nwo.
|
||||
* This name doesn't include the file extension.
|
||||
*/
|
||||
function createVariantAnalysisFileName(
|
||||
fullName: string,
|
||||
index: number,
|
||||
expectedResultsCount: number,
|
||||
linkType: MarkdownLinkType,
|
||||
) {
|
||||
const baseName = createFileName(fullName);
|
||||
if (linkType === "gist") {
|
||||
const requiredNumberOfDecimals = Math.ceil(
|
||||
Math.log10(expectedResultsCount),
|
||||
);
|
||||
|
||||
const prefix = (index + 1)
|
||||
.toString()
|
||||
.padStart(requiredNumberOfDecimals, "0");
|
||||
|
||||
return `result-${prefix}-${baseName}`;
|
||||
}
|
||||
|
||||
return baseName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape characters that could be interpreted as HTML instead of raw code.
|
||||
*/
|
||||
|
||||
@@ -16,20 +16,13 @@ export class RemoteQueriesMonitor {
|
||||
private static readonly maxAttemptCount = 17280;
|
||||
private static readonly sleepTime = 5000;
|
||||
|
||||
constructor(
|
||||
private readonly extensionContext: vscode.ExtensionContext,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
constructor(private readonly logger: Logger) {}
|
||||
|
||||
public async monitorQuery(
|
||||
remoteQuery: RemoteQuery,
|
||||
cancellationToken: vscode.CancellationToken,
|
||||
): Promise<RemoteQueryWorkflowResult> {
|
||||
const credentials = await Credentials.initialize(this.extensionContext);
|
||||
|
||||
if (!credentials) {
|
||||
throw Error("Error authenticating with GitHub");
|
||||
}
|
||||
const credentials = await Credentials.initialize();
|
||||
|
||||
let attemptCount = 0;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { extLogger } from "../common";
|
||||
import {
|
||||
getRemoteRepositoryLists,
|
||||
getRemoteRepositoryListsPath,
|
||||
isNewQueryRunExperienceEnabled,
|
||||
isVariantAnalysisReposPanelEnabled,
|
||||
} from "../config";
|
||||
import { OWNER_REGEX, REPO_REGEX } from "../pure/helpers-pure";
|
||||
import { UserCancellationException } from "../commandRunner";
|
||||
@@ -36,7 +36,7 @@ interface RepoList {
|
||||
export async function getRepositorySelection(
|
||||
dbManager?: DbManager,
|
||||
): Promise<RepositorySelection> {
|
||||
if (isNewQueryRunExperienceEnabled()) {
|
||||
if (isVariantAnalysisReposPanelEnabled()) {
|
||||
const selectedDbItem = dbManager?.getSelectedDbItem();
|
||||
if (selectedDbItem) {
|
||||
switch (selectedDbItem.kind) {
|
||||
@@ -46,7 +46,7 @@ export async function getRepositorySelection(
|
||||
);
|
||||
case DbItemKind.RemoteSystemDefinedList:
|
||||
return { repositoryLists: [selectedDbItem.listName] };
|
||||
case DbItemKind.RemoteUserDefinedList:
|
||||
case DbItemKind.VariantAnalysisUserDefinedList:
|
||||
if (selectedDbItem.repos.length === 0) {
|
||||
throw new UserCancellationException(
|
||||
"The selected repository list is empty. Please add repositories to it before running a variant analysis.",
|
||||
|
||||
@@ -34,6 +34,7 @@ import { DbManager } from "../databases/db-manager";
|
||||
export interface QlPack {
|
||||
name: string;
|
||||
version: string;
|
||||
library?: boolean;
|
||||
dependencies: { [key: string]: string };
|
||||
defaultSuite?: Array<Record<string, unknown>>;
|
||||
defaultSuiteFile?: string;
|
||||
@@ -66,7 +67,7 @@ async function generateQueryPack(
|
||||
const targetQueryFileName = join(queryPackDir, packRelativePath);
|
||||
|
||||
let language: string | undefined;
|
||||
if (await pathExists(join(originalPackRoot, "qlpack.yml"))) {
|
||||
if (await getExistingPackFile(originalPackRoot)) {
|
||||
// don't include ql files. We only want the queryFile to be copied.
|
||||
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
|
||||
|
||||
@@ -162,7 +163,7 @@ async function generateQueryPack(
|
||||
async function findPackRoot(queryFile: string): Promise<string> {
|
||||
// recursively find the directory containing qlpack.yml
|
||||
let dir = dirname(queryFile);
|
||||
while (!(await pathExists(join(dir, "qlpack.yml")))) {
|
||||
while (!(await getExistingPackFile(dir))) {
|
||||
dir = dirname(dir);
|
||||
if (isFileSystemRoot(dir)) {
|
||||
// there is no qlpack.yml in this directory or any parent directory.
|
||||
@@ -174,6 +175,16 @@ async function findPackRoot(queryFile: string): Promise<string> {
|
||||
return dir;
|
||||
}
|
||||
|
||||
async function getExistingPackFile(dir: string) {
|
||||
if (await pathExists(join(dir, "qlpack.yml"))) {
|
||||
return join(dir, "qlpack.yml");
|
||||
}
|
||||
if (await pathExists(join(dir, "codeql-pack.yml"))) {
|
||||
return join(dir, "codeql-pack.yml");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isFileSystemRoot(dir: string): boolean {
|
||||
const pathObj = parse(dir);
|
||||
return pathObj.root === dir && pathObj.base === "";
|
||||
@@ -214,7 +225,7 @@ export async function prepareRemoteQueryRun(
|
||||
uri: Uri | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
dbManager?: DbManager, // the dbManager is only needed when the newQueryRunExperience is enabled
|
||||
dbManager?: DbManager, // the dbManager is only needed when variantAnalysisReposPanel is enabled
|
||||
): Promise<PreparedRemoteQuery> {
|
||||
if (!(await cliServer.cliConstraints.supportsRemoteQueries())) {
|
||||
throw new Error(
|
||||
@@ -314,7 +325,14 @@ async function fixPackFile(
|
||||
queryPackDir: string,
|
||||
packRelativePath: string,
|
||||
): Promise<void> {
|
||||
const packPath = join(queryPackDir, "qlpack.yml");
|
||||
const packPath = await getExistingPackFile(queryPackDir);
|
||||
|
||||
// This should not happen since we create the pack ourselves.
|
||||
if (!packPath) {
|
||||
throw new Error(
|
||||
`Could not find qlpack.yml or codeql-pack.yml file in '${queryPackDir}'`,
|
||||
);
|
||||
}
|
||||
const qlpack = load(await readFile(packPath, "utf8")) as QlPack;
|
||||
|
||||
// update pack name
|
||||
|
||||
@@ -19,6 +19,7 @@ import { DisposableObject } from "../pure/disposable-object";
|
||||
import { Credentials } from "../authentication";
|
||||
import { VariantAnalysisMonitor } from "./variant-analysis-monitor";
|
||||
import {
|
||||
getActionsWorkflowRunUrl,
|
||||
isVariantAnalysisComplete,
|
||||
parseVariantAnalysisQueryLanguage,
|
||||
VariantAnalysis,
|
||||
@@ -60,6 +61,7 @@ import {
|
||||
} from "../pure/variant-analysis-filter-sort";
|
||||
import { URLSearchParams } from "url";
|
||||
import { DbManager } from "../databases/db-manager";
|
||||
import { isVariantAnalysisReposPanelEnabled } from "../config";
|
||||
|
||||
export class VariantAnalysisManager
|
||||
extends DisposableObject
|
||||
@@ -101,12 +103,11 @@ export class VariantAnalysisManager
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly storagePath: string,
|
||||
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager,
|
||||
private readonly dbManager?: DbManager, // the dbManager is only needed when the newQueryRunExperience is enabled
|
||||
private readonly dbManager?: DbManager, // the dbManager is only needed when variantAnalysisReposPanel is enabled
|
||||
) {
|
||||
super();
|
||||
this.variantAnalysisMonitor = this.push(
|
||||
new VariantAnalysisMonitor(
|
||||
ctx,
|
||||
this.shouldCancelMonitorVariantAnalysis.bind(this),
|
||||
),
|
||||
);
|
||||
@@ -125,7 +126,7 @@ export class VariantAnalysisManager
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
const credentials = await Credentials.initialize();
|
||||
|
||||
const {
|
||||
actionBranch,
|
||||
@@ -479,10 +480,7 @@ export class VariantAnalysisManager
|
||||
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
if (!credentials) {
|
||||
throw Error("Error authenticating with GitHub");
|
||||
}
|
||||
const credentials = await Credentials.initialize();
|
||||
|
||||
if (cancellationToken && cancellationToken.isCancellationRequested) {
|
||||
repoState.downloadStatus =
|
||||
@@ -580,10 +578,7 @@ export class VariantAnalysisManager
|
||||
);
|
||||
}
|
||||
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
if (!credentials) {
|
||||
throw Error("Error authenticating with GitHub");
|
||||
}
|
||||
const credentials = await Credentials.initialize();
|
||||
|
||||
void showAndLogInformationMessage(
|
||||
"Cancelling variant analysis. This may take a while.",
|
||||
@@ -591,6 +586,20 @@ export class VariantAnalysisManager
|
||||
await cancelVariantAnalysis(credentials, variantAnalysis);
|
||||
}
|
||||
|
||||
public async openVariantAnalysisLogs(variantAnalysisId: number) {
|
||||
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
||||
if (!variantAnalysis) {
|
||||
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
||||
}
|
||||
|
||||
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(variantAnalysis);
|
||||
|
||||
await commands.executeCommand(
|
||||
"vscode.open",
|
||||
Uri.parse(actionsWorkflowRunUrl),
|
||||
);
|
||||
}
|
||||
|
||||
public async copyRepoListToClipboard(
|
||||
variantAnalysisId: number,
|
||||
filterSort: RepositoriesFilterSortStateWithIds = defaultFilterSortState,
|
||||
@@ -612,12 +621,25 @@ export class VariantAnalysisManager
|
||||
return;
|
||||
}
|
||||
|
||||
const text = [
|
||||
'"new-repo-list": [',
|
||||
...fullNames.slice(0, -1).map((repo) => ` "${repo}",`),
|
||||
` "${fullNames[fullNames.length - 1]}"`,
|
||||
"]",
|
||||
];
|
||||
let text: string[];
|
||||
if (isVariantAnalysisReposPanelEnabled()) {
|
||||
text = [
|
||||
"{",
|
||||
` "name": "new-repo-list",`,
|
||||
` "repositories": [`,
|
||||
...fullNames.slice(0, -1).map((repo) => ` "${repo}",`),
|
||||
` "${fullNames[fullNames.length - 1]}"`,
|
||||
` ]`,
|
||||
"}",
|
||||
];
|
||||
} else {
|
||||
text = [
|
||||
'"new-repo-list": [',
|
||||
...fullNames.slice(0, -1).map((repo) => ` "${repo}",`),
|
||||
` "${fullNames[fullNames.length - 1]}"`,
|
||||
"]",
|
||||
];
|
||||
}
|
||||
|
||||
await env.clipboard.writeText(text.join(EOL));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
CancellationToken,
|
||||
commands,
|
||||
EventEmitter,
|
||||
ExtensionContext,
|
||||
} from "vscode";
|
||||
import { CancellationToken, commands, EventEmitter } from "vscode";
|
||||
import { Credentials } from "../authentication";
|
||||
import { getVariantAnalysis } from "./gh-api/gh-api-client";
|
||||
|
||||
@@ -32,7 +27,6 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
readonly onVariantAnalysisChange = this._onVariantAnalysisChange.event;
|
||||
|
||||
constructor(
|
||||
private readonly extensionContext: ExtensionContext,
|
||||
private readonly shouldCancelMonitor: (
|
||||
variantAnalysisId: number,
|
||||
) => Promise<boolean>,
|
||||
@@ -44,10 +38,7 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
variantAnalysis: VariantAnalysis,
|
||||
cancellationToken: CancellationToken,
|
||||
): Promise<void> {
|
||||
const credentials = await Credentials.initialize(this.extensionContext);
|
||||
if (!credentials) {
|
||||
throw Error("Error authenticating with GitHub");
|
||||
}
|
||||
const credentials = await Credentials.initialize();
|
||||
|
||||
let attemptCount = 0;
|
||||
const scannedReposDownloaded: number[] = [];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { commands, ExtensionContext, Uri, ViewColumn } from "vscode";
|
||||
import { commands, ExtensionContext, ViewColumn } from "vscode";
|
||||
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
|
||||
import { extLogger } from "../common";
|
||||
import {
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from "../pure/interface-types";
|
||||
import { assertNever } from "../pure/helpers-pure";
|
||||
import {
|
||||
getActionsWorkflowRunUrl,
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
@@ -147,7 +146,10 @@ export class VariantAnalysisView
|
||||
);
|
||||
break;
|
||||
case "openLogs":
|
||||
await this.openLogs();
|
||||
await commands.executeCommand(
|
||||
"codeQL.openVariantAnalysisLogs",
|
||||
this.variantAnalysisId,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
@@ -183,23 +185,4 @@ export class VariantAnalysisView
|
||||
repoStates,
|
||||
});
|
||||
}
|
||||
|
||||
private async openLogs(): Promise<void> {
|
||||
const variantAnalysis = await this.manager.getVariantAnalysis(
|
||||
this.variantAnalysisId,
|
||||
);
|
||||
if (!variantAnalysis) {
|
||||
void showAndLogWarningMessage(
|
||||
"Could not open variant analysis logs. Variant analysis not found.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(variantAnalysis);
|
||||
|
||||
await commands.executeCommand(
|
||||
"vscode.open",
|
||||
Uri.parse(actionsWorkflowRunUrl),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"lib": ["ES2021", "dom"],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"rootDir": "..",
|
||||
"rootDir": "../../..",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
AnalysisAlert,
|
||||
AnalysisRawResults,
|
||||
} from "../../remote-queries/shared/analysis-result";
|
||||
import { createMockRepositoryWithMetadata } from "../../vscode-tests/factories/remote-queries/shared/repository";
|
||||
import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository";
|
||||
|
||||
import * as analysesResults from "../remote-queries/data/analysesResultsMessage.json";
|
||||
import * as rawResults from "../remote-queries/data/rawResults.json";
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
VariantAnalysisStatus,
|
||||
} from "../../remote-queries/shared/variant-analysis";
|
||||
import { createMockVariantAnalysis } from "../../vscode-tests/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "../../vscode-tests/factories/remote-queries/shared/repository";
|
||||
import { createMockVariantAnalysis } from "../../../test/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository";
|
||||
|
||||
export default {
|
||||
title: "Variant Analysis/Variant Analysis",
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
VariantAnalysisStatus,
|
||||
} from "../../remote-queries/shared/variant-analysis";
|
||||
import { AnalysisAlert } from "../../remote-queries/shared/analysis-result";
|
||||
import { createMockVariantAnalysis } from "../../vscode-tests/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "../../vscode-tests/factories/remote-queries/shared/repository";
|
||||
import { createMockScannedRepo } from "../../vscode-tests/factories/remote-queries/shared/scanned-repositories";
|
||||
import { createMockVariantAnalysis } from "../../../test/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository";
|
||||
import { createMockScannedRepo } from "../../../test/factories/remote-queries/shared/scanned-repositories";
|
||||
|
||||
import * as analysesResults from "../remote-queries/data/analysesResultsMessage.json";
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisStatus,
|
||||
} from "../../remote-queries/shared/variant-analysis";
|
||||
import { createMockVariantAnalysis } from "../../vscode-tests/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockScannedRepo } from "../../vscode-tests/factories/remote-queries/shared/scanned-repositories";
|
||||
import { createMockVariantAnalysis } from "../../../test/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockScannedRepo } from "../../../test/factories/remote-queries/shared/scanned-repositories";
|
||||
|
||||
export default {
|
||||
title: "Variant Analysis/Variant Analysis Header",
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisStatus,
|
||||
} from "../../remote-queries/shared/variant-analysis";
|
||||
import { createMockScannedRepo } from "../../vscode-tests/factories/remote-queries/shared/scanned-repositories";
|
||||
import { createMockVariantAnalysis } from "../../vscode-tests/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "../../vscode-tests/factories/remote-queries/shared/repository";
|
||||
import { createMockScannedRepo } from "../../../test/factories/remote-queries/shared/scanned-repositories";
|
||||
import { createMockVariantAnalysis } from "../../../test/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository";
|
||||
import {
|
||||
defaultFilterSortState,
|
||||
RepositoriesFilterSortState,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
|
||||
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
|
||||
import { VariantAnalysisSkippedRepositoriesTab } from "../../view/variant-analysis/VariantAnalysisSkippedRepositoriesTab";
|
||||
import { createMockRepositoryWithMetadata } from "../../vscode-tests/factories/remote-queries/shared/repository";
|
||||
import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository";
|
||||
|
||||
export default {
|
||||
title: "Variant Analysis/Variant Analysis Skipped Repositories Tab",
|
||||
|
||||
@@ -95,10 +95,8 @@ export class TelemetryListener extends ConfigListener {
|
||||
CANARY_FEATURES.getValue() &&
|
||||
!ENABLE_TELEMETRY.getValue()
|
||||
) {
|
||||
await Promise.all([
|
||||
this.setTelemetryRequested(false),
|
||||
this.requestTelemetryPermission(),
|
||||
]);
|
||||
await this.setTelemetryRequested(false);
|
||||
await this.requestTelemetryPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,12 +225,15 @@ export class TelemetryListener extends ConfigListener {
|
||||
/**
|
||||
* The global Telemetry instance
|
||||
*/
|
||||
export let telemetryListener: TelemetryListener;
|
||||
export let telemetryListener: TelemetryListener | undefined;
|
||||
|
||||
export async function initializeTelemetry(
|
||||
extension: Extension<any>,
|
||||
ctx: ExtensionContext,
|
||||
): Promise<void> {
|
||||
if (telemetryListener !== undefined) {
|
||||
throw new Error("Telemetry is already initialized");
|
||||
}
|
||||
telemetryListener = new TelemetryListener(
|
||||
extension.id,
|
||||
extension.packageJSON.version,
|
||||
|
||||
@@ -22,16 +22,21 @@ export const CodeFlowsDropdown = ({
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedOption = e.target;
|
||||
const selectedIndex = selectedOption.value as unknown as number;
|
||||
const selectedIndex = parseInt(selectedOption.value);
|
||||
setSelectedCodeFlow(codeFlows[selectedIndex]);
|
||||
},
|
||||
[setSelectedCodeFlow, codeFlows],
|
||||
);
|
||||
|
||||
return (
|
||||
<VSCodeDropdown onChange={handleChange}>
|
||||
<VSCodeDropdown
|
||||
onChange={
|
||||
handleChange as unknown as ((e: Event) => unknown) &
|
||||
React.FormEventHandler<HTMLElement>
|
||||
}
|
||||
>
|
||||
{codeFlows.map((codeFlow, index) => (
|
||||
<VSCodeOption key={index} value={index}>
|
||||
<VSCodeOption key={index} value={index.toString()}>
|
||||
{getCodeFlowName(codeFlow)}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { render as reactRender, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { CodePaths, CodePathsProps } from "../CodePaths";
|
||||
|
||||
import { createMockCodeFlows } from "../../../../vscode-tests/factories/remote-queries/shared/CodeFlow";
|
||||
import { createMockAnalysisMessage } from "../../../../vscode-tests/factories/remote-queries/shared/AnalysisMessage";
|
||||
import { createMockCodeFlows } from "../../../../../test/factories/remote-queries/shared/CodeFlow";
|
||||
import { createMockAnalysisMessage } from "../../../../../test/factories/remote-queries/shared/AnalysisMessage";
|
||||
|
||||
describe(CodePaths.name, () => {
|
||||
const render = (props?: CodePathsProps) =>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"lib": ["ES2021", "dom"],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"rootDir": "..",
|
||||
"rootDir": "../..",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "../../../remote-queries/shared/variant-analysis";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { RepoRow, RepoRowProps } from "../RepoRow";
|
||||
import { createMockRepositoryWithMetadata } from "../../../vscode-tests/factories/remote-queries/shared/repository";
|
||||
import { createMockRepositoryWithMetadata } from "../../../../test/factories/remote-queries/shared/repository";
|
||||
|
||||
describe(RepoRow.name, () => {
|
||||
const render = (props: Partial<RepoRowProps> = {}) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
VariantAnalysisStatus,
|
||||
} from "../../../remote-queries/shared/variant-analysis";
|
||||
import { VariantAnalysis, VariantAnalysisProps } from "../VariantAnalysis";
|
||||
import { createMockVariantAnalysis } from "../../../vscode-tests/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockVariantAnalysis } from "../../../../test/factories/remote-queries/shared/variant-analysis";
|
||||
|
||||
describe(VariantAnalysis.name, () => {
|
||||
const render = (props: Partial<VariantAnalysisProps> = {}) =>
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
VariantAnalysisAnalyzedRepos,
|
||||
VariantAnalysisAnalyzedReposProps,
|
||||
} from "../VariantAnalysisAnalyzedRepos";
|
||||
import { createMockVariantAnalysis } from "../../../vscode-tests/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "../../../vscode-tests/factories/remote-queries/shared/repository";
|
||||
import { createMockScannedRepo } from "../../../vscode-tests/factories/remote-queries/shared/scanned-repositories";
|
||||
import { createMockVariantAnalysis } from "../../../../test/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "../../../../test/factories/remote-queries/shared/repository";
|
||||
import { createMockScannedRepo } from "../../../../test/factories/remote-queries/shared/scanned-repositories";
|
||||
import {
|
||||
defaultFilterSortState,
|
||||
SortKey,
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
VariantAnalysisOutcomePanelProps,
|
||||
VariantAnalysisOutcomePanels,
|
||||
} from "../VariantAnalysisOutcomePanels";
|
||||
import { createMockVariantAnalysis } from "../../../vscode-tests/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "../../../vscode-tests/factories/remote-queries/shared/repository";
|
||||
import { createMockVariantAnalysis } from "../../../../test/factories/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "../../../../test/factories/remote-queries/shared/repository";
|
||||
import {
|
||||
createMockScannedRepo,
|
||||
createMockScannedRepos,
|
||||
} from "../../../vscode-tests/factories/remote-queries/shared/scanned-repositories";
|
||||
} from "../../../../test/factories/remote-queries/shared/scanned-repositories";
|
||||
import { defaultFilterSortState } from "../../../pure/variant-analysis-filter-sort";
|
||||
|
||||
describe(VariantAnalysisOutcomePanels.name, () => {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { commands, extensions, window } from "vscode";
|
||||
|
||||
import { CodeQLExtensionInterface } from "../../../extension";
|
||||
import { readJson } from "fs-extra";
|
||||
import * as path from "path";
|
||||
import { DbConfig } from "../../../databases/config/db-config";
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe("Db panel UI commands", () => {
|
||||
let extension: CodeQLExtensionInterface | Record<string, never>;
|
||||
let storagePath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
extension = await extensions
|
||||
.getExtension<CodeQLExtensionInterface | Record<string, never>>(
|
||||
"GitHub.vscode-codeql",
|
||||
)!
|
||||
.activate();
|
||||
|
||||
storagePath =
|
||||
extension.ctx.storageUri?.fsPath || extension.ctx.globalStorageUri.fsPath;
|
||||
});
|
||||
|
||||
it("should add new remote db list", async () => {
|
||||
// Add db list
|
||||
jest.spyOn(window, "showInputBox").mockResolvedValue("my-list-1");
|
||||
await commands.executeCommand("codeQLDatabasesExperimental.addNewList");
|
||||
|
||||
// Check db config
|
||||
const dbConfigFilePath = path.join(storagePath, "workspace-databases.json");
|
||||
const dbConfig: DbConfig = await readJson(dbConfigFilePath);
|
||||
expect(dbConfig.databases.remote.repositoryLists).toHaveLength(1);
|
||||
expect(dbConfig.databases.remote.repositoryLists[0].name).toBe("my-list-1");
|
||||
});
|
||||
});
|
||||
@@ -1,342 +0,0 @@
|
||||
import { CancellationTokenSource, commands, extensions } from "vscode";
|
||||
import { CodeQLExtensionInterface } from "../../../extension";
|
||||
import * as config from "../../../config";
|
||||
|
||||
import * as ghApiClient from "../../../remote-queries/gh-api/gh-api-client";
|
||||
import { VariantAnalysisMonitor } from "../../../remote-queries/variant-analysis-monitor";
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse,
|
||||
VariantAnalysisFailureReason,
|
||||
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository,
|
||||
} from "../../../remote-queries/gh-api/variant-analysis";
|
||||
import {
|
||||
createFailedMockApiResponse,
|
||||
createMockApiResponse,
|
||||
} from "../../factories/remote-queries/gh-api/variant-analysis-api-response";
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisStatus,
|
||||
} from "../../../remote-queries/shared/variant-analysis";
|
||||
import { createMockScannedRepos } from "../../factories/remote-queries/gh-api/scanned-repositories";
|
||||
import {
|
||||
processFailureReason,
|
||||
processScannedRepository,
|
||||
processUpdatedVariantAnalysis,
|
||||
} from "../../../remote-queries/variant-analysis-processor";
|
||||
import { Credentials } from "../../../authentication";
|
||||
import { createMockVariantAnalysis } from "../../factories/remote-queries/shared/variant-analysis";
|
||||
import { VariantAnalysisManager } from "../../../remote-queries/variant-analysis-manager";
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe("Variant Analysis Monitor", () => {
|
||||
let extension: CodeQLExtensionInterface | Record<string, never>;
|
||||
let mockGetVariantAnalysis: jest.SpiedFunction<
|
||||
typeof ghApiClient.getVariantAnalysis
|
||||
>;
|
||||
let cancellationTokenSource: CancellationTokenSource;
|
||||
let variantAnalysisMonitor: VariantAnalysisMonitor;
|
||||
let shouldCancelMonitor: jest.Mock<Promise<boolean>, [number]>;
|
||||
let variantAnalysis: VariantAnalysis;
|
||||
let variantAnalysisManager: VariantAnalysisManager;
|
||||
let mockGetDownloadResult: jest.SpiedFunction<
|
||||
typeof variantAnalysisManager.autoDownloadVariantAnalysisResult
|
||||
>;
|
||||
|
||||
const onVariantAnalysisChangeSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
jest
|
||||
.spyOn(config, "isVariantAnalysisLiveResultsEnabled")
|
||||
.mockReturnValue(false);
|
||||
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
variantAnalysis = createMockVariantAnalysis({});
|
||||
|
||||
shouldCancelMonitor = jest.fn();
|
||||
|
||||
extension = await extensions
|
||||
.getExtension<CodeQLExtensionInterface | Record<string, never>>(
|
||||
"GitHub.vscode-codeql",
|
||||
)!
|
||||
.activate();
|
||||
variantAnalysisMonitor = new VariantAnalysisMonitor(
|
||||
extension.ctx,
|
||||
shouldCancelMonitor,
|
||||
);
|
||||
variantAnalysisMonitor.onVariantAnalysisChange(onVariantAnalysisChangeSpy);
|
||||
|
||||
variantAnalysisManager = extension.variantAnalysisManager;
|
||||
mockGetDownloadResult = jest
|
||||
.spyOn(variantAnalysisManager, "autoDownloadVariantAnalysisResult")
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
mockGetVariantAnalysis = jest
|
||||
.spyOn(ghApiClient, "getVariantAnalysis")
|
||||
.mockRejectedValue(new Error("Not mocked"));
|
||||
|
||||
limitNumberOfAttemptsToMonitor();
|
||||
});
|
||||
|
||||
describe("when credentials are invalid", () => {
|
||||
beforeEach(async () => {
|
||||
jest
|
||||
.spyOn(Credentials, "initialize")
|
||||
.mockResolvedValue(undefined as unknown as Credentials);
|
||||
});
|
||||
|
||||
it("should return early if credentials are wrong", async () => {
|
||||
try {
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
} catch (error: any) {
|
||||
expect(error.message).toBe("Error authenticating with GitHub");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("when credentials are valid", () => {
|
||||
beforeEach(async () => {
|
||||
const mockCredentials = {
|
||||
getOctokit: () =>
|
||||
Promise.resolve({
|
||||
request: jest.fn(),
|
||||
}),
|
||||
} as unknown as Credentials;
|
||||
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials);
|
||||
});
|
||||
|
||||
it("should return early if variant analysis is cancelled", async () => {
|
||||
cancellationTokenSource.cancel();
|
||||
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(onVariantAnalysisChangeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return early if variant analysis should be cancelled", async () => {
|
||||
shouldCancelMonitor.mockResolvedValue(true);
|
||||
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(onVariantAnalysisChangeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when the variant analysis fails", () => {
|
||||
let mockFailedApiResponse: VariantAnalysisApiResponse;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockFailedApiResponse = createFailedMockApiResponse();
|
||||
mockGetVariantAnalysis.mockResolvedValue(mockFailedApiResponse);
|
||||
});
|
||||
|
||||
it("should mark as failed and stop monitoring", async () => {
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetVariantAnalysis).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(onVariantAnalysisChangeSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: VariantAnalysisStatus.Failed,
|
||||
failureReason: processFailureReason(
|
||||
mockFailedApiResponse.failure_reason as VariantAnalysisFailureReason,
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the variant analysis is in progress", () => {
|
||||
let mockApiResponse: VariantAnalysisApiResponse;
|
||||
let scannedRepos: ApiVariantAnalysisScannedRepository[];
|
||||
let succeededRepos: ApiVariantAnalysisScannedRepository[];
|
||||
|
||||
describe("when there are successfully scanned repos", () => {
|
||||
beforeEach(async () => {
|
||||
scannedRepos = createMockScannedRepos([
|
||||
"pending",
|
||||
"pending",
|
||||
"in_progress",
|
||||
"in_progress",
|
||||
"succeeded",
|
||||
"succeeded",
|
||||
"succeeded",
|
||||
]);
|
||||
mockApiResponse = createMockApiResponse("succeeded", scannedRepos);
|
||||
mockGetVariantAnalysis.mockResolvedValue(mockApiResponse);
|
||||
succeededRepos = scannedRepos.filter(
|
||||
(r) => r.analysis_status === "succeeded",
|
||||
);
|
||||
});
|
||||
|
||||
it("should trigger a download extension command for each repo", async () => {
|
||||
const succeededRepos = scannedRepos.filter(
|
||||
(r) => r.analysis_status === "succeeded",
|
||||
);
|
||||
const commandSpy = jest
|
||||
.spyOn(commands, "executeCommand")
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(commandSpy).toBeCalledTimes(succeededRepos.length);
|
||||
|
||||
succeededRepos.forEach((succeededRepo, index) => {
|
||||
expect(commandSpy).toHaveBeenNthCalledWith(
|
||||
index + 1,
|
||||
"codeQL.autoDownloadVariantAnalysisResult",
|
||||
processScannedRepository(succeededRepo),
|
||||
processUpdatedVariantAnalysis(variantAnalysis, mockApiResponse),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should download all available results", async () => {
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetDownloadResult).toBeCalledTimes(succeededRepos.length);
|
||||
|
||||
succeededRepos.forEach((succeededRepo, index) => {
|
||||
expect(mockGetDownloadResult).toHaveBeenNthCalledWith(
|
||||
index + 1,
|
||||
processScannedRepository(succeededRepo),
|
||||
processUpdatedVariantAnalysis(variantAnalysis, mockApiResponse),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there are only in progress repos", () => {
|
||||
let scannedRepos: ApiVariantAnalysisScannedRepository[];
|
||||
|
||||
beforeEach(async () => {
|
||||
scannedRepos = createMockScannedRepos(["pending", "in_progress"]);
|
||||
mockApiResponse = createMockApiResponse("in_progress", scannedRepos);
|
||||
mockGetVariantAnalysis.mockResolvedValue(mockApiResponse);
|
||||
});
|
||||
|
||||
it("should succeed and not download any repos via a command", async () => {
|
||||
const commandSpy = jest
|
||||
.spyOn(commands, "executeCommand")
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(commandSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not try to download any repos", async () => {
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetDownloadResult).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the responses change", () => {
|
||||
let scannedRepos: ApiVariantAnalysisScannedRepository[];
|
||||
|
||||
beforeEach(async () => {
|
||||
scannedRepos = createMockScannedRepos([
|
||||
"pending",
|
||||
"in_progress",
|
||||
"in_progress",
|
||||
"in_progress",
|
||||
"pending",
|
||||
"pending",
|
||||
]);
|
||||
mockApiResponse = createMockApiResponse("in_progress", scannedRepos);
|
||||
mockGetVariantAnalysis.mockResolvedValueOnce(mockApiResponse);
|
||||
|
||||
let nextApiResponse = {
|
||||
...mockApiResponse,
|
||||
scanned_repositories: [...scannedRepos.map((r) => ({ ...r }))],
|
||||
};
|
||||
nextApiResponse.scanned_repositories[0].analysis_status = "succeeded";
|
||||
nextApiResponse.scanned_repositories[1].analysis_status = "succeeded";
|
||||
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
|
||||
|
||||
nextApiResponse = {
|
||||
...mockApiResponse,
|
||||
scanned_repositories: [
|
||||
...nextApiResponse.scanned_repositories.map((r) => ({ ...r })),
|
||||
],
|
||||
};
|
||||
nextApiResponse.scanned_repositories[2].analysis_status = "succeeded";
|
||||
nextApiResponse.scanned_repositories[5].analysis_status = "succeeded";
|
||||
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
|
||||
|
||||
nextApiResponse = {
|
||||
...mockApiResponse,
|
||||
scanned_repositories: [
|
||||
...nextApiResponse.scanned_repositories.map((r) => ({ ...r })),
|
||||
],
|
||||
};
|
||||
nextApiResponse.scanned_repositories[3].analysis_status = "succeeded";
|
||||
nextApiResponse.scanned_repositories[4].analysis_status = "failed";
|
||||
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
|
||||
});
|
||||
|
||||
it("should trigger a download extension command for each repo", async () => {
|
||||
const commandSpy = jest
|
||||
.spyOn(commands, "executeCommand")
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetVariantAnalysis).toBeCalledTimes(4);
|
||||
expect(commandSpy).toBeCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there are no repos to scan", () => {
|
||||
beforeEach(async () => {
|
||||
scannedRepos = [];
|
||||
mockApiResponse = createMockApiResponse("succeeded", scannedRepos);
|
||||
mockGetVariantAnalysis.mockResolvedValue(mockApiResponse);
|
||||
});
|
||||
|
||||
it("should not try to download any repos", async () => {
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetDownloadResult).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function limitNumberOfAttemptsToMonitor() {
|
||||
VariantAnalysisMonitor.maxAttemptCount = 3;
|
||||
VariantAnalysisMonitor.sleepTime = 1;
|
||||
}
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Uri, WorkspaceFolder } from "vscode";
|
||||
import * as fs from "fs-extra";
|
||||
|
||||
import { QLTestDiscovery } from "../../qltest-discovery";
|
||||
|
||||
describe("qltest-discovery", () => {
|
||||
describe("discoverTests", () => {
|
||||
const baseUri = Uri.parse("file:/a/b");
|
||||
const baseDir = baseUri.fsPath;
|
||||
const cDir = Uri.parse("file:/a/b/c").fsPath;
|
||||
const dFile = Uri.parse("file:/a/b/c/d.ql").fsPath;
|
||||
const eFile = Uri.parse("file:/a/b/c/e.ql").fsPath;
|
||||
const hDir = Uri.parse("file:/a/b/c/f/g/h").fsPath;
|
||||
const iFile = Uri.parse("file:/a/b/c/f/g/h/i.ql").fsPath;
|
||||
let qlTestDiscover: QLTestDiscovery;
|
||||
|
||||
beforeEach(() => {
|
||||
qlTestDiscover = new QLTestDiscovery(
|
||||
{
|
||||
uri: baseUri,
|
||||
name: "My tests",
|
||||
} as unknown as WorkspaceFolder,
|
||||
{
|
||||
resolveTests() {
|
||||
return [
|
||||
Uri.parse("file:/a/b/c/d.ql").fsPath,
|
||||
Uri.parse("file:/a/b/c/e.ql").fsPath,
|
||||
Uri.parse("file:/a/b/c/f/g/h/i.ql").fsPath,
|
||||
];
|
||||
},
|
||||
} as any,
|
||||
);
|
||||
});
|
||||
|
||||
it("should run discovery", async () => {
|
||||
jest
|
||||
.spyOn(fs, "pathExists")
|
||||
.mockImplementation(() => Promise.resolve(true));
|
||||
|
||||
const result = await (qlTestDiscover as any).discover();
|
||||
expect(result.watchPath).toBe(baseDir);
|
||||
expect(result.testDirectory.path).toBe(baseDir);
|
||||
expect(result.testDirectory.name).toBe("My tests");
|
||||
|
||||
let children = result.testDirectory.children;
|
||||
expect(children[0].path).toBe(cDir);
|
||||
expect(children[0].name).toBe("c");
|
||||
expect(children.length).toBe(1);
|
||||
|
||||
children = children[0].children;
|
||||
expect(children[0].path).toBe(dFile);
|
||||
expect(children[0].name).toBe("d.ql");
|
||||
expect(children[1].path).toBe(eFile);
|
||||
expect(children[1].name).toBe("e.ql");
|
||||
|
||||
// A merged foler
|
||||
expect(children[2].path).toBe(hDir);
|
||||
expect(children[2].name).toBe("f / g / h");
|
||||
expect(children.length).toBe(3);
|
||||
|
||||
children = children[2].children;
|
||||
expect(children[0].path).toBe(iFile);
|
||||
expect(children[0].name).toBe("i.ql");
|
||||
});
|
||||
|
||||
it("should avoid discovery if a folder does not exist", async () => {
|
||||
jest
|
||||
.spyOn(fs, "pathExists")
|
||||
.mockImplementation(() => Promise.resolve(false));
|
||||
|
||||
const result = await (qlTestDiscover as any).discover();
|
||||
expect(result.watchPath).toBe(baseDir);
|
||||
expect(result.testDirectory.path).toBe(baseDir);
|
||||
expect(result.testDirectory.name).toBe("My tests");
|
||||
|
||||
expect(result.testDirectory.children.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,172 +0,0 @@
|
||||
import { resolve, join } from "path";
|
||||
import { platform } from "os";
|
||||
import { spawnSync } from "child_process";
|
||||
import {
|
||||
runTests,
|
||||
downloadAndUnzipVSCode,
|
||||
resolveCliArgsFromVSCodeExecutablePath,
|
||||
} from "@vscode/test-electron";
|
||||
import { assertNever } from "../pure/helpers-pure";
|
||||
import { dirSync } from "tmp-promise";
|
||||
|
||||
// For some reason, the following are not exported directly from `vscode-test`,
|
||||
// but we can be tricky and import directly from the out file.
|
||||
import { TestOptions } from "@vscode/test-electron/out/runTest";
|
||||
|
||||
// For CI purposes we want to leave this at 'stable' to catch any bugs
|
||||
// that might show up with new vscode versions released, even though
|
||||
// this makes testing not-quite-pure, but it can be changed for local
|
||||
// testing against old versions if necessary.
|
||||
const VSCODE_VERSION = "stable";
|
||||
|
||||
// List if test dirs
|
||||
// - no-workspace - Tests with no workspace selected upon launch.
|
||||
// - minimal-workspace - Tests with a simple workspace selected upon launch.
|
||||
// - cli-integration - Tests that require a cli to invoke actual commands
|
||||
enum TestDir {
|
||||
NoWorksspace = "no-workspace",
|
||||
MinimalWorksspace = "minimal-workspace",
|
||||
CliIntegration = "cli-integration",
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an integration test suite `suite`, retrying if it segfaults, at
|
||||
* most `tries` times.
|
||||
*/
|
||||
async function runTestsWithRetryOnSegfault(
|
||||
suite: TestOptions,
|
||||
tries: number,
|
||||
): Promise<number> {
|
||||
for (let t = 0; t < tries; t++) {
|
||||
try {
|
||||
// Download and unzip VS Code if necessary, and run the integration test suite.
|
||||
return await runTests(suite);
|
||||
} catch (err) {
|
||||
if (err === "SIGSEGV") {
|
||||
console.error("Test runner segfaulted.");
|
||||
if (t < tries - 1) console.error("Retrying...");
|
||||
} else if (platform() === "win32") {
|
||||
console.error(`Test runner caught exception (${err})`);
|
||||
if (t < tries - 1) console.error("Retrying...");
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.error(
|
||||
`Tried running suite ${tries} time(s), still failed, giving up.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tmpDir = dirSync({ unsafeCleanup: true });
|
||||
|
||||
/**
|
||||
* Integration test runner. Launches the VSCode Extension Development Host with this extension installed.
|
||||
* See https://github.com/microsoft/vscode-test/blob/master/sample/test/runTest.ts
|
||||
*/
|
||||
async function main() {
|
||||
let exitCode = 0;
|
||||
try {
|
||||
const extensionDevelopmentPath = resolve(__dirname, "../..");
|
||||
const vscodeExecutablePath = await downloadAndUnzipVSCode(VSCODE_VERSION);
|
||||
|
||||
// Which tests to run. Use a comma-separated list of directories.
|
||||
const testDirsString = process.argv[2];
|
||||
const dirs = testDirsString
|
||||
.split(",")
|
||||
.map((dir) => dir.trim().toLocaleLowerCase());
|
||||
const extensionTestsEnv: Record<string, string> = {};
|
||||
if (dirs.includes(TestDir.CliIntegration)) {
|
||||
console.log("Installing required extensions");
|
||||
const [cli, ...args] =
|
||||
resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);
|
||||
spawnSync(
|
||||
cli,
|
||||
[
|
||||
...args,
|
||||
"--install-extension",
|
||||
"hbenl.vscode-test-explorer",
|
||||
"--install-extension",
|
||||
"ms-vscode.test-adapter-converter",
|
||||
],
|
||||
{
|
||||
encoding: "utf-8",
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
extensionTestsEnv.INTEGRATION_TEST_MODE = "true";
|
||||
}
|
||||
|
||||
console.log(`Running integration tests in these directories: ${dirs}`);
|
||||
for (const dir of dirs) {
|
||||
const launchArgs = getLaunchArgs(dir as TestDir);
|
||||
console.log(`Next integration test dir: ${dir}`);
|
||||
console.log(`Launch args: ${launchArgs}`);
|
||||
exitCode = await runTestsWithRetryOnSegfault(
|
||||
{
|
||||
version: VSCODE_VERSION,
|
||||
vscodeExecutablePath,
|
||||
extensionDevelopmentPath,
|
||||
extensionTestsPath: resolve(__dirname, dir, "index"),
|
||||
extensionTestsEnv,
|
||||
launchArgs,
|
||||
},
|
||||
3,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Unexpected exception while running tests: ${err}`);
|
||||
if (err instanceof Error) {
|
||||
console.error(err.stack);
|
||||
}
|
||||
exitCode = 1;
|
||||
} finally {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
void main();
|
||||
|
||||
function getLaunchArgs(dir: TestDir) {
|
||||
switch (dir) {
|
||||
case TestDir.NoWorksspace:
|
||||
return [
|
||||
"--disable-extensions",
|
||||
"--disable-gpu",
|
||||
"--disable-workspace-trust",
|
||||
`--user-data-dir=${join(tmpDir.name, dir, "user-data")}`,
|
||||
];
|
||||
|
||||
case TestDir.MinimalWorksspace:
|
||||
return [
|
||||
"--disable-extensions",
|
||||
"--disable-gpu",
|
||||
"--disable-workspace-trust",
|
||||
`--user-data-dir=${join(tmpDir.name, dir, "user-data")}`,
|
||||
resolve(__dirname, "../../test/data"),
|
||||
];
|
||||
|
||||
case TestDir.CliIntegration:
|
||||
// CLI integration tests requires a multi-root workspace so that the data and the QL sources are accessible.
|
||||
return [
|
||||
"--disable-workspace-trust",
|
||||
"--disable-gpu",
|
||||
resolve(__dirname, "../../test/data"),
|
||||
|
||||
// explicitly disable extensions that are known to interfere with the CLI integration tests
|
||||
"--disable-extension",
|
||||
"eamodio.gitlens",
|
||||
"--disable-extension",
|
||||
"github.codespaces",
|
||||
"--disable-extension",
|
||||
"github.copilot",
|
||||
`--user-data-dir=${join(tmpDir.name, dir, "user-data")}`,
|
||||
].concat(
|
||||
process.env.TEST_CODEQL_PATH ? [process.env.TEST_CODEQL_PATH] : [],
|
||||
);
|
||||
|
||||
default:
|
||||
assertNever(dir);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { App, AppMode } from "../../src/common/app";
|
||||
import { AppEvent, AppEventEmitter } from "../../src/common/events";
|
||||
import { Memento } from "../../src/common/memento";
|
||||
import { Disposable } from "../../src/pure/disposable-object";
|
||||
import { createMockLogger } from "./loggerMock";
|
||||
import { createMockMemento } from "../mock-memento";
|
||||
|
||||
export function createMockApp({
|
||||
extensionPath = "/mock/extension/path",
|
||||
@@ -9,12 +11,14 @@ export function createMockApp({
|
||||
globalStoragePath = "/mock/global/storage/path",
|
||||
createEventEmitter = <T>() => new MockAppEventEmitter<T>(),
|
||||
executeCommand = jest.fn(() => Promise.resolve()),
|
||||
workspaceState = createMockMemento(),
|
||||
}: {
|
||||
extensionPath?: string;
|
||||
workspaceStoragePath?: string;
|
||||
globalStoragePath?: string;
|
||||
createEventEmitter?: <T>() => AppEventEmitter<T>;
|
||||
executeCommand?: () => Promise<void>;
|
||||
workspaceState?: Memento;
|
||||
}): App {
|
||||
return {
|
||||
mode: AppMode.Test,
|
||||
@@ -23,6 +27,7 @@ export function createMockApp({
|
||||
extensionPath,
|
||||
workspaceStoragePath,
|
||||
globalStoragePath,
|
||||
workspaceState,
|
||||
createEventEmitter,
|
||||
executeCommand,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import {
|
||||
DbConfig,
|
||||
ExpandedDbItem,
|
||||
LocalDatabase,
|
||||
LocalList,
|
||||
RemoteRepositoryList,
|
||||
SelectedDbItem,
|
||||
} from "../../databases/config/db-config";
|
||||
} from "../../src/databases/config/db-config";
|
||||
|
||||
export function createDbConfig({
|
||||
remoteLists = [],
|
||||
@@ -15,7 +14,6 @@ export function createDbConfig({
|
||||
localLists = [],
|
||||
localDbs = [],
|
||||
selected = undefined,
|
||||
expanded = [],
|
||||
}: {
|
||||
remoteLists?: RemoteRepositoryList[];
|
||||
remoteOwners?: string[];
|
||||
@@ -23,11 +21,10 @@ export function createDbConfig({
|
||||
localLists?: LocalList[];
|
||||
localDbs?: LocalDatabase[];
|
||||
selected?: SelectedDbItem;
|
||||
expanded?: ExpandedDbItem[];
|
||||
} = {}): DbConfig {
|
||||
return {
|
||||
databases: {
|
||||
remote: {
|
||||
variantAnalysis: {
|
||||
repositoryLists: remoteLists,
|
||||
owners: remoteOwners,
|
||||
repositories: remoteRepos,
|
||||
@@ -37,7 +34,6 @@ export function createDbConfig({
|
||||
databases: localDbs,
|
||||
},
|
||||
},
|
||||
expanded,
|
||||
selected,
|
||||
};
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
RemoteOwnerDbItem,
|
||||
RemoteRepoDbItem,
|
||||
RemoteSystemDefinedListDbItem,
|
||||
RemoteUserDefinedListDbItem,
|
||||
VariantAnalysisUserDefinedListDbItem,
|
||||
RootLocalDbItem,
|
||||
RootRemoteDbItem,
|
||||
} from "../../src/databases/db-item";
|
||||
@@ -79,7 +79,7 @@ export function createRemoteSystemDefinedListDbItem({
|
||||
};
|
||||
}
|
||||
|
||||
export function createRemoteUserDefinedListDbItem({
|
||||
export function createVariantAnalysisUserDefinedListDbItem({
|
||||
expanded = false,
|
||||
selected = false,
|
||||
listName = `list${faker.datatype.number()}`,
|
||||
@@ -93,9 +93,9 @@ export function createRemoteUserDefinedListDbItem({
|
||||
expanded?: boolean;
|
||||
selected?: boolean;
|
||||
repos?: RemoteRepoDbItem[];
|
||||
} = {}): RemoteUserDefinedListDbItem {
|
||||
} = {}): VariantAnalysisUserDefinedListDbItem {
|
||||
return {
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
kind: DbItemKind.VariantAnalysisUserDefinedList,
|
||||
expanded,
|
||||
selected,
|
||||
listName,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as vscode from "vscode";
|
||||
import { createMockMemento } from "../mock-memento";
|
||||
|
||||
/**
|
||||
* Creates a partially implemented mock of vscode.ExtensionContext.
|
||||
@@ -11,10 +12,12 @@ export function createMockExtensionContext({
|
||||
extensionPath?: string;
|
||||
workspaceStoragePath?: string;
|
||||
globalStoragePath?: string;
|
||||
workspaceState?: vscode.Memento;
|
||||
}): vscode.ExtensionContext {
|
||||
return {
|
||||
extensionPath,
|
||||
globalStorageUri: vscode.Uri.file(globalStoragePath),
|
||||
storageUri: vscode.Uri.file(workspaceStoragePath),
|
||||
workspaceState: createMockMemento(),
|
||||
} as any as vscode.ExtensionContext;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { InitialQueryInfo, LocalQueryInfo } from "../../../query-results";
|
||||
import { InitialQueryInfo, LocalQueryInfo } from "../../../src/query-results";
|
||||
import {
|
||||
QueryEvaluationInfo,
|
||||
QueryWithResults,
|
||||
} from "../../../run-queries-shared";
|
||||
} from "../../../src/run-queries-shared";
|
||||
import { CancellationTokenSource } from "vscode";
|
||||
import { QueryResultType } from "../../../pure/legacy-messages";
|
||||
import { QueryMetadata } from "../../../pure/interface-types";
|
||||
import { QueryResultType } from "../../../src/pure/legacy-messages";
|
||||
import { QueryMetadata } from "../../../src/pure/interface-types";
|
||||
|
||||
export function createMockLocalQueryInfo({
|
||||
startTime = new Date(),
|
||||
@@ -2,7 +2,7 @@ import { faker } from "@faker-js/faker";
|
||||
import {
|
||||
Repository,
|
||||
RepositoryWithMetadata,
|
||||
} from "../../../../remote-queries/gh-api/repository";
|
||||
} from "../../../../src/remote-queries/gh-api/repository";
|
||||
|
||||
export function createMockRepository(name = faker.random.word()): Repository {
|
||||
return {
|
||||
@@ -2,7 +2,7 @@ import { faker } from "@faker-js/faker";
|
||||
import {
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepository,
|
||||
} from "../../../../remote-queries/gh-api/variant-analysis";
|
||||
} from "../../../../src/remote-queries/gh-api/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "./repository";
|
||||
|
||||
export function createMockScannedRepo(
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
VariantAnalysisNotFoundRepositoryGroup,
|
||||
VariantAnalysisSkippedRepositories,
|
||||
VariantAnalysisSkippedRepositoryGroup,
|
||||
} from "../../../../remote-queries/gh-api/variant-analysis";
|
||||
} from "../../../../src/remote-queries/gh-api/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "./repository";
|
||||
|
||||
export function createMockSkippedRepos(): VariantAnalysisSkippedRepositories {
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisSkippedRepositories,
|
||||
VariantAnalysisStatus,
|
||||
} from "../../../../remote-queries/gh-api/variant-analysis";
|
||||
import { VariantAnalysisQueryLanguage } from "../../../../remote-queries/shared/variant-analysis";
|
||||
} from "../../../../src/remote-queries/gh-api/variant-analysis";
|
||||
import { VariantAnalysisQueryLanguage } from "../../../../src/remote-queries/shared/variant-analysis";
|
||||
import { createMockScannedRepos } from "./scanned-repositories";
|
||||
import { createMockSkippedRepos } from "./skipped-repositories";
|
||||
import { createMockRepository } from "./repository";
|
||||
@@ -1,6 +1,6 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { VariantAnalysisRepoTask } from "../../../../remote-queries/gh-api/variant-analysis";
|
||||
import { VariantAnalysisRepoStatus } from "../../../../remote-queries/shared/variant-analysis";
|
||||
import { VariantAnalysisRepoTask } from "../../../../src/remote-queries/gh-api/variant-analysis";
|
||||
import { VariantAnalysisRepoStatus } from "../../../../src/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepository } from "./repository";
|
||||
|
||||
export function createMockVariantAnalysisRepoTask(): VariantAnalysisRepoTask {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { RemoteQueryHistoryItem } from "../../../remote-queries/remote-query-history-item";
|
||||
import { QueryStatus } from "../../../query-status";
|
||||
import { RemoteQueryHistoryItem } from "../../../src/remote-queries/remote-query-history-item";
|
||||
import { QueryStatus } from "../../../src/query-status";
|
||||
|
||||
export function createMockRemoteQueryHistoryItem({
|
||||
date = new Date("2022-01-01T00:00:00.000Z"),
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnalysisMessage } from "../../../../remote-queries/shared/analysis-result";
|
||||
import { AnalysisMessage } from "../../../../src/remote-queries/shared/analysis-result";
|
||||
|
||||
export function createMockAnalysisMessage(): AnalysisMessage {
|
||||
return {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CodeFlow } from "../../../../remote-queries/shared/analysis-result";
|
||||
import { CodeFlow } from "../../../../src/remote-queries/shared/analysis-result";
|
||||
import { createMockAnalysisMessage } from "./AnalysisMessage";
|
||||
|
||||
export function createMockCodeFlows(): CodeFlow[] {
|
||||
@@ -2,7 +2,7 @@ import { faker } from "@faker-js/faker";
|
||||
import {
|
||||
Repository,
|
||||
RepositoryWithMetadata,
|
||||
} from "../../../../remote-queries/shared/repository";
|
||||
} from "../../../../src/remote-queries/shared/repository";
|
||||
|
||||
export function createMockRepository(): Repository {
|
||||
return {
|
||||
@@ -2,7 +2,7 @@ import { faker } from "@faker-js/faker";
|
||||
import {
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepository,
|
||||
} from "../../../../remote-queries/shared/variant-analysis";
|
||||
} from "../../../../src/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "./repository";
|
||||
|
||||
export function createMockScannedRepo(
|
||||
@@ -2,7 +2,7 @@ import { faker } from "@faker-js/faker";
|
||||
import {
|
||||
VariantAnalysisSkippedRepositories,
|
||||
VariantAnalysisSkippedRepositoryGroup,
|
||||
} from "../../../../remote-queries/shared/variant-analysis";
|
||||
} from "../../../../src/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "./repository";
|
||||
|
||||
export function createMockSkippedRepos(): VariantAnalysisSkippedRepositories {
|
||||
@@ -2,7 +2,7 @@ import { faker } from "@faker-js/faker";
|
||||
import {
|
||||
VariantAnalysisRepositoryTask,
|
||||
VariantAnalysisRepoStatus,
|
||||
} from "../../../../remote-queries/shared/variant-analysis";
|
||||
} from "../../../../src/remote-queries/shared/variant-analysis";
|
||||
import { createMockRepositoryWithMetadata } from "./repository";
|
||||
|
||||
export function createMockVariantAnalysisRepositoryTask(
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user