Project-centric data model - change extension model to reflect Git and GitLab models
Problem to solve
There are many edge cases when the extension's internal state can't reflect how entities (git remotes and GitLab projects) relate to each other. This is caused by the incremental nature of the current model design.
- We started with workspace centric approach, where the whole extension operated on an opened folder. This implementation couldn't support multiple repositories.
- We refactored the extension to use repositories instead of workspaces (#345 (closed)). This was a large improvement, but the model still struggles to reflect some scenarios. It especially struggles with multiple remotes and repositories that don't contain GitLab projects.
The way how repository-centred approach doesn't exactly reflect the Git repository + GitLab project entities is a cause of small bugs and misunderstandings. Currently, we mix repository and project-related methods.
Proposal
In hindsight, the solution is clear: Use the GitLabProject-centric approach. As the extension starts or configuration changes, we'll loop over all remote URLs in all repositories and find out which ones correspond to GitLab projects. Then, if there's more than one GitLab project per repository, we'll let the user choose which one they want to use.
Benefits
- Simpler logic, many places in the codebase are now concerned about how to obtain GitLabProject from a repository. There is often an implicit expectation that the project is present.
- Simpler mental model - a few entities will reflect the whole application state, clear separation between the GitLab project model and the local repository model
- Easy support for multiple accounts, which will become more important once we implement OAuth login
Further details
Here are a few issues that we implemented to work around the repository-centric model:
- Helping users set the correct remote name
- Unable to Validate CI config for mirrored repository
- Extension ignores expired token and fails in the wrong place
Proposed model
I'll put this in a diagram a bit later:
interface Repository {
remotes: Remote[]
// all local repository git methods will be here
}
interface Remote {
name: string;
urlEntries: UrlEntry[];
}
interface UrlEntry {
type: 'fetch' | 'push' | 'both';
url: string;
}
/* This pointer allows us to work with remote URLs (because they represent GitLab Project),
* but also keep track of what local repository contains this remote URL.
*/
interface RemoteUrlPointer {
repository: Repository;
remote: Remote;
urlEntry: UrlEntry;
}
interface Account {
instanceUrl: string;
token: string;
}
/* Represents a GitLab project that we can infer from remote URL and instance URL */
interface ParsedProject {
remoteUrl: string;
instanceUrl: string;
projectName: string;
namespace: string;
}
/* In a case when there is more than one GitLab project in a repository,
* the user will create this setting to indicate which project should be used.
* This will be a persistent setting stored in the global storage.
*
* There can be more than one GitLab project for a repository if there are multiple
* remotes like:
*
* - git@gitlab.com:gitlab-org/gitlab-vscode-extension.git
* - git@gitlab.com:gitlab-org/security/gitlab-vscode-extension.git
*
* Or when there are multiple accounts on the same GitLab instance.
*/
interface SelectedProject {
pointer: RemoteUrlPointer;
account: Account;
projectName: string;
namespace: string;
}
/* This represents a GitLab project that the rest of the extension will work with
* this entity only exists at runtime, and the `GitLabProject` is always populated by querying a GitLab API
*/
interface InitializedProject {
project: GitLabProject;
pointer: RemoteUrlPointer;
account: Account;
// all GitLab project methods are going to be either here or accessed through here
}