Draft: feat(branch-tree-view): Display tag pipelines in branch tree
-
Please check this box if this contribution uses AI-generated content (including content generated by GitLab Duo features) as outlined in the GitLab DCO & CLA
Description
This POC MR outlines kitchen-sink changes to introduce displaying tag pipelines along side branch resources. The general approach taken was to add an auxiliary TagState
to BranchState
and ensure the logic to process the top level TagState
was flexible enough to also handle the auxiliary TagState
on a BranchState
.
The Tree data provider has refactors that follow suit with this pattern by encapsulating tag display logic into a TagTree
class that can either be displayed as a top level node or under an auxiliary node. BranchTree
similarly contains the logic for displaying the branch state and an optional TagTree
property for the auxiliary tag state.
Moving forward, we could potentially part these changes out into at least two MRs:
-
CurrentBranchRefresher
handles populatingtagState
on theBranchState
interface -
CurrentBranchDataProvider
handles the display of an auxiliarytagState
onBranchState
The refactors made some of these changes easier to execute on, but if the refactors went too far I can also pullback changes in these MRs to be more lean!
More detailed technical notes
- Changes to
CurrentBranchRefresher
:
The CurrentBranchRefresher
class has been modified, with some of its logic moved to a new HeadStateFetcher
class to improve modularity and separation of concerns. One of the main targets of these refactors was ensuring that the auxiliary tagState
on the BranchState
interface can be processed in the same way as the top level TagState
interface.
// Feature branch
export class CurrentBranchRefresher {
// ...
async refresh(userInitiated = false) {
const projectInRepository = getActiveProject();
const headStateFetcher = new HeadStateFetcher(projectInRepository, userInitiated);
this.#latestState = await headStateFetcher.fetch();
this.#stateChangedEmitter.fire(this.#latestState);
this.#lastRefresh = dayjs();
}
// ...
}
- Introduction of
HeadStateFetcher
:
The new HeadStateFetcher
class is responsible for fetching the state of the HEAD ref and populating the tree view with the appropriate resources. It uses a two-step process: first creating a stub state, then filling it out with fetched API resources. This class largely just moves logic from the CurrentBranchRefresher
with some light refactors. The extra modularity here allows TagState
to be processed with the same logic whether it's a parent state or an auxiliary state on BranchState
.
// Feature branch
export class HeadStateFetcher {
// ...
async fetch(): Promise<TreeState> {
try {
const projectInRepository = this.#projectInRepository;
if (!projectInRepository) return INVALID_STATE;
const headStateStub = await this.#getStubForHead();
if (headStateStub.type === 'invalid') return headStateStub;
return await this.#buildStateFromStub(headStateStub);
} catch (e) {
log.error(e);
return { type: 'invalid', error: e };
}
}
// ...
}
- Changes to
getTrackingBranchName
:
The getTrackingBranchName
function has been modified to use getBranch
instead of directly accessing state.HEAD.name
. This change aims to ensure that only the actual branch name is returned, not a tag name when in detached HEAD state.
// Feature branch
export const getTrackingBranchName = async (
rawRepository: Repository,
): Promise<string | undefined> => {
const branch = await rawRepository.getBranch('HEAD');
const branchName = branch?.name;
// ...
}
// Main branch
export const getTrackingBranchName = async (
rawRepository: Repository,
): Promise<string | undefined> => {
const branchName = rawRepository.state.HEAD?.name;
// ...
}
- Changes to Tree Data Providers:
The CurrentBranchDataProvider
has been renamed to CurrentHeadDataProvider
to reflect that it handles displaying both Branch and Tag states. It now uses separate BranchTree
and TagTree
classes to handle the respective states.
// Feature branch
export class CurrentHeadDataProvider implements vscode.TreeDataProvider<ItemModel | vscode.TreeItem>, vscode.Disposable {
// ...
async getChildren(element?: ItemModel | vscode.TreeItem): Promise<(ItemModel | vscode.TreeItem)[]> {
if (element instanceof ItemModel || element instanceof TagNode) return element.getChildren();
if (!this.#root) return [];
return this.#root.getChildren();
}
// ...
refresh(state: TreeState) {
this.#root?.dispose();
if (state.type === 'branch') {
this.#root = new BranchTree(state);
} else if (state.type === 'tag') {
this.#root = new TagTree(state);
} else {
this.#root = undefined;
}
this.#eventEmitter.fire();
}
// ...
}
- Introduction of
BranchTree
andTagTree
:
These new classes encapsulate the logic for displaying branch and tag resources. The goal was to provide a more object-oriented approach to handling different states and reduce the amount of logic and conditionals in the tree data provider. Similar to the refactors splitting CurrentBranchRefresher
and HeadStateFetcher
, a tighter separation of concerns allows the auxiliary TagState
to be processed with the same logic that is used for the parent TagState
.
// Feature branch
export class BranchTree implements vscode.Disposable {
// ...
async getChildren(): Promise<(ItemModel | vscode.TreeItem)[]> {
// Logic for displaying branch items
}
// ...
}
export class TagTree implements vscode.Disposable {
// ...
async getChildren(): Promise<(ItemModel | vscode.TreeItem)[]> {
// Logic for displaying tag items
}
// ...
}
- Dynamic Panel Label:
The changes introduce the ability to change the label of the panel based on the current state (branch or tag).
// Feature branch
export class CurrentHeadDataProvider {
// ...
getViewTitle(): string {
if (!this.#root) return 'For current branch';
return this.#root.getViewTitle();
}
// ...
}
Related Issues
Relates to #1246
What CHANGELOG entry will this MR create?
This MR is not intended to be merged, and is for discussion only. If we move forward, we can track some more iterative MRs back to this parent MR and close it once this effort is complete.