Use repositories instead of workspace folders
Use repositories instead of workspace folders
TL;DR: We use the wrong abstraction in the code. If we change the abstraction, we'll simplify the code significantly, make future work on the codebase much easier.
What is a workspace?
Workspace is the content of an open VS Code window. It can either contain one open folder or multiple folders if you use Multi-root Workspace.
single foldre in the workspace | mutli-root workspace |
---|---|
GitLab Workflow uses the workspace folder to run the git
command. The most common use case is this:
graph TD;
A[Obtains workspaceFolder from VS Code] --"/workspace/extension"--> B["Run git remote -v in that folder to get remote URL"]
B --"git@gitlab.com:gitlab-org/gitlab-vscode-extension.git"--> C["parse namespace and project from the remote"]
C --"gitlab-org/gitlab-vscode-extension"--> D["Run API queries for this project"]
workspaceFolders
The problem with using The problem with this approach is that the workspaceFolder
doesn't have 1:1 relationship with repository. You can open the VS Code in any folder (code .
in the command line). If we have the following folder structure:
.
└── workspace/
├── projectA/
│ ├── .git
│ ├── frontend/
│ │ └── src
│ └── backend
└── projectB/
├── .git
└── src/
This structure allows for the following three scenarios:
- Open
projectB
(cd workspace/projectB && code .
) -✅ This will work fine because theworkspaceFolder
is a git repository at the same time - Open
frontend
(cd workspace/projectA/frontend && code .
) -⚠ ️This now works, because there is still only one repository for theworkspaceFolder
. But it caused us trouble in the past: - Open
workspace
(cd workspace && code .
) -❌ The extension will rungit
command in the folder calledworskpace
and think that there are no repositories.workspace
folder contains two repositories.
Better approach: Using repositories
The key insight is that the workspaceFolder
<--> git repository 1:n relationship makes it very hard to keep the extension git information in sync with VS Code.
We've recently introduced the VS Code Git extension to our codebase (Support VSCode "git clone" command) and now we can use the way VS Code recognizes repositories.
Folder that contains multiple repositories
explorer | source control (git) tree view | GitLab Workflow tree view |
---|---|---|
Benefits of switching to repositories
- Benefits of changing the abstraction
- Clear abstraction. GitLab project = git repository. Easy to understand for new and existing contributors.
- Benefits of switching to the VS Code git extension
- Single source of truth for the git information, if user sees 4 repostiories in the SCM tree view, they'll see up to 4 in the GitLab Workflow TreeView
- Less dependency on
execa
module to run our customgit
commands and parse them. Running the git commands ourselves is tricky from a security perspective (e.g. Client side code execution)
Drawbacks of switching to repositories
There is no drawback (that I can think of) of changing the abstraction. However, there is a drawback of using the VS Code Git extension:
- Dependency on the VS Code Git extension means that if there is a bug or a missing feature, we are not fully in control, and we can only contribute the fix upstream and wait for a release.
- There is plenty of popular extensions (GitHub Pull Request, Git Lenses) depending on the same API, the "severe bug" scenario is not very likely.
- We can still use
execa
for any missing git commands/features, but I don't know of any.
Getting technical
(some experience with the extension code is recommended)
I have implemented a POC of this change in refactor-to-use-git-repositories branch. The potential for simplifying the code got me excited enough to write this issue.
Opportunity for simplifying the codebase
The way we now obtain information about the git repository or how we call API is quite complex. If we create a wrapper around the Repository
that we get from VS Code Git extension, we can reduce a significant amount of code and complexity from the extension1.
Good example
In this example, we already built the GitLabNewService to be decoupled from the repository logic.
export async function createGitLabNewService(workspaceFolder: string): Promise<GitLabNewService> {
return new GitLabNewService(await getInstanceUrl(workspaceFolder));
}
const gitService = createGitService(workspaceFolder);
const gitLabService = await createGitLabNewService(workspaceFolder);
const remote = await gitService.fetchGitRemote();
const snippets = await gitLabService.getSnippets(`${remote.namespace}/${remote.project}`);
Bad example
The old GitLabService is incredibly coupled with the workspace and git logic:
export async function getAllGitlabWorkspaces(): Promise<GitLabWorkspace[]> {
if (!vscode.workspace.workspaceFolders) {
return [];
}
const projectsWithUri = vscode.workspace.workspaceFolders.map(async workspaceFolder => {
const uri = workspaceFolder.uri.fsPath;
try {
const currentProject = await fetchCurrentProject(uri);
return {
label: currentProject?.name ?? basename(uri),
uri,
};
} catch (e) {
logError(e);
return { label: basename(uri), uri, error: true };
}
});
return Promise.all(projectsWithUri);
}
export async function fetchCurrentProject(workspaceFolder: string): Promise<GitLabProject | null> {
try {
const remote = await createGitService(workspaceFolder).fetchGitRemote();
return await fetchProjectData(remote, workspaceFolder);
} catch (e) {
throw new ApiError(e, 'get current project');
}
}
I imagine the new repository centric approach like this:
const activeEditor = vscode.workspace.activeEditor;
const gitlabRepository = repositoryManager.getRepositoryForFile(activeEditor.document.uri);
const snippets = await gitlabRepository.gitlabService.getSnippets();
const activeBranch = await gitlabRepository.git.getActiveBranch();
Related
-
I'm confident that even with covering the new logic with unit tests, we'll end up with less code in total.
↩