Skip to content

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
e3c8ae52e5a24d82bfd88b406d150871 2e96f2863e554495b225c1770e775aa4

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"]

The problem with using workspaceFolders

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:

  1. Open projectB (cd workspace/projectB && code .) - This will work fine because the workspaceFolder is a git repository at the same time
  2. Open frontend (cd workspace/projectA/frontend && code .) - ️This now works, because there is still only one repository for the workspaceFolder. But it caused us trouble in the past:
  3. Open workspace (cd workspace && code .) - The extension will run git command in the folder called worskpace 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
cbdebfffbbb54267b27496f93ffd4718 74b38c20d8b544f99efe3a0b4ed18367 0197b75bb9b6465bab9c1459eb5fcd84

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 custom git 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.

Getting the GitLabNewService

export async function createGitLabNewService(workspaceFolder: string): Promise<GitLabNewService> {
  return new GitLabNewService(await getInstanceUrl(workspaceFolder));
}

Using the new service:

 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

  1. I'm confident that even with covering the new logic with unit tests, we'll end up with less code in total.

Edited by Tomas Vik