From 52c0cf593ee46f88061dca54778c88eada852e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Minelli?= <git@minelli.me> Date: Fri, 15 Mar 2024 16:19:44 +0100 Subject: [PATCH] Move to Gitbeaker --- .../subcommands/AssignmentCreateCommand.ts | 10 +- .../auth/subcommands/AuthTestCommand.ts | 4 +- .../subcommands/ExerciseCreateCommand.ts | 11 ++- NodeApp/src/helpers/AccessesHelper.ts | 6 +- NodeApp/src/helpers/AutoCompletionHelper.ts | 1 + NodeApp/src/helpers/GlobalHelper.ts | 13 +++ NodeApp/src/managers/DojoBackendManager.ts | 7 +- NodeApp/src/managers/GitlabManager.ts | 97 ++++++++++++------- NodeApp/src/managers/HttpManager.ts | 50 ++-------- NodeApp/src/managers/SessionManager.ts | 12 ++- NodeApp/src/shared | 2 +- NodeApp/src/sharedByClients | 2 +- 12 files changed, 114 insertions(+), 101 deletions(-) diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts index fed344e..d79934e 100644 --- a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts +++ b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts @@ -2,10 +2,10 @@ import CommanderCommand from '../../CommanderCommand'; import ora from 'ora'; import AccessesHelper from '../../../helpers/AccessesHelper'; import Assignment from '../../../sharedByClients/models/Assignment'; -import GitlabUser from '../../../shared/types/Gitlab/GitlabUser'; -import GitlabManager from '../../../managers/GitlabManager'; import DojoBackendManager from '../../../managers/DojoBackendManager'; import Toolbox from '../../../shared/helpers/Toolbox'; +import * as Gitlab from '@gitbeaker/rest'; +import GlobalHelper from '../../../helpers/GlobalHelper'; import TextStyle from '../../../types/TextStyle'; @@ -24,7 +24,7 @@ class AssignmentCreateCommand extends CommanderCommand { } protected async commandAction(options: { name: string, template?: string, members_id?: Array<number>, members_username?: Array<string>, clone?: string | boolean }): Promise<void> { - let members!: Array<GitlabUser> | false; + let members!: Array<Gitlab.UserSchema> | false; let templateIdOrNamespace: string | null = null; let assignment!: Assignment; @@ -36,7 +36,7 @@ class AssignmentCreateCommand extends CommanderCommand { return; } - members = await GitlabManager.fetchMembers(options); + members = await GlobalHelper.gitlabManager.fetchMembers(options); if ( !members ) { return; } @@ -89,7 +89,7 @@ class AssignmentCreateCommand extends CommanderCommand { if ( options.clone ) { console.log(TextStyle.BLOCK('Please wait while we are cloning the repository...')); - await GitlabManager.cloneRepository(options.clone, assignment.gitlabCreationInfo.ssh_url_to_repo, undefined, true, 0); + await GlobalHelper.gitlabManager.cloneRepository(options.clone, assignment.gitlabCreationInfo.ssh_url_to_repo, undefined, true, 0); } } } diff --git a/NodeApp/src/commander/auth/subcommands/AuthTestCommand.ts b/NodeApp/src/commander/auth/subcommands/AuthTestCommand.ts index d1f57e6..163678b 100644 --- a/NodeApp/src/commander/auth/subcommands/AuthTestCommand.ts +++ b/NodeApp/src/commander/auth/subcommands/AuthTestCommand.ts @@ -1,6 +1,6 @@ import CommanderCommand from '../../CommanderCommand'; import SessionManager from '../../../managers/SessionManager'; -import GitlabManager from '../../../managers/GitlabManager'; +import GlobalHelper from '../../../helpers/GlobalHelper'; class AuthTestCommand extends CommanderCommand { @@ -14,7 +14,7 @@ class AuthTestCommand extends CommanderCommand { protected async commandAction(): Promise<void> { await SessionManager.testSession(); - await GitlabManager.testToken(); + await GlobalHelper.gitlabManager.testToken(); } } diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts index 2fc6bf7..63567f1 100644 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts @@ -1,11 +1,12 @@ import CommanderCommand from '../../CommanderCommand'; -import GitlabManager from '../../../managers/GitlabManager'; -import GitlabUser from '../../../shared/types/Gitlab/GitlabUser'; +import chalk from 'chalk'; import ora from 'ora'; import DojoBackendManager from '../../../managers/DojoBackendManager'; import AccessesHelper from '../../../helpers/AccessesHelper'; import Assignment from '../../../sharedByClients/models/Assignment'; import Exercise from '../../../sharedByClients/models/Exercise'; +import * as Gitlab from '@gitbeaker/rest'; +import GlobalHelper from '../../../helpers/GlobalHelper'; import TextStyle from '../../../types/TextStyle'; @@ -23,7 +24,7 @@ class ExerciseCreateCommand extends CommanderCommand { } protected async commandAction(options: { assignment: string, members_id?: Array<number>, members_username?: Array<string>, clone?: string | boolean }): Promise<void> { - let members!: Array<GitlabUser> | false; + let members!: Array<Gitlab.UserSchema> | false; let assignment!: Assignment | undefined; let exercise!: Exercise; @@ -35,7 +36,7 @@ class ExerciseCreateCommand extends CommanderCommand { return; } - members = await GitlabManager.fetchMembers(options); + members = await GlobalHelper.gitlabManager.fetchMembers(options); if ( !members ) { return; } @@ -92,7 +93,7 @@ class ExerciseCreateCommand extends CommanderCommand { if ( options.clone ) { console.log(TextStyle.BLOCK('Please wait while we are cloning the repository...')); - await GitlabManager.cloneRepository(options.clone, exercise.gitlabCreationInfo.ssh_url_to_repo, `DojoExercise - ${ exercise.assignmentName }`, true, 0); + await GlobalHelper.gitlabManager.cloneRepository(options.clone, exercise.gitlabCreationInfo.ssh_url_to_repo, `DojoExercise - ${ exercise.assignmentName }`, true, 0); } } } diff --git a/NodeApp/src/helpers/AccessesHelper.ts b/NodeApp/src/helpers/AccessesHelper.ts index c9ff5e1..9b868cf 100644 --- a/NodeApp/src/helpers/AccessesHelper.ts +++ b/NodeApp/src/helpers/AccessesHelper.ts @@ -1,5 +1,5 @@ import SessionManager from '../managers/SessionManager'; -import GitlabManager from '../managers/GitlabManager'; +import GlobalHelper from './GlobalHelper'; class AccessesHelper { @@ -11,7 +11,7 @@ class AccessesHelper { } if ( testGitlab ) { - return (await GitlabManager.testToken(true)).every(result => result); + return (await GlobalHelper.gitlabManager.testToken(true)).every(result => result); } else { return true; } @@ -25,7 +25,7 @@ class AccessesHelper { } if ( testGitlab ) { - return (await GitlabManager.testToken(true)).every(result => result); + return (await GlobalHelper.gitlabManager.testToken(true)).every(result => result); } else { return true; } diff --git a/NodeApp/src/helpers/AutoCompletionHelper.ts b/NodeApp/src/helpers/AutoCompletionHelper.ts index 04464f6..f2d3cbd 100644 --- a/NodeApp/src/helpers/AutoCompletionHelper.ts +++ b/NodeApp/src/helpers/AutoCompletionHelper.ts @@ -67,6 +67,7 @@ complete -f -c dojo function isHidden(cmd: Command): boolean { return (cmd as Command & { _hidden: boolean })._hidden; + const isDone: boolean = false; } diff --git a/NodeApp/src/helpers/GlobalHelper.ts b/NodeApp/src/helpers/GlobalHelper.ts index c7dccb1..ca90dc7 100644 --- a/NodeApp/src/helpers/GlobalHelper.ts +++ b/NodeApp/src/helpers/GlobalHelper.ts @@ -1,5 +1,8 @@ import { Command, Option } from 'commander'; import Config from '../config/Config'; +import SharedGitlabManager from '../shared/managers/SharedGitlabManager'; +import SessionManager from '../managers/SessionManager'; +import GitlabManager from '../managers/GitlabManager'; class GlobalHelper { @@ -12,6 +15,16 @@ class GlobalHelper { return command; } + + + private readonly refreshTokenFunction = async () => { + await SessionManager.refreshTokens(); + + return SessionManager.gitlabCredentials.accessToken ?? ''; + }; + + readonly gitlabManager = new GitlabManager('', this.refreshTokenFunction.bind(this)); + readonly sharedGitlabManager = new SharedGitlabManager('', this.refreshTokenFunction.bind(this)); } diff --git a/NodeApp/src/managers/DojoBackendManager.ts b/NodeApp/src/managers/DojoBackendManager.ts index 9bf65df..f536d18 100644 --- a/NodeApp/src/managers/DojoBackendManager.ts +++ b/NodeApp/src/managers/DojoBackendManager.ts @@ -1,7 +1,7 @@ import axios, { AxiosError } from 'axios'; import ora from 'ora'; import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute'; -import GitlabUser from '../shared/types/Gitlab/GitlabUser'; +import { StatusCodes } from 'http-status-codes'; import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; import Assignment from '../sharedByClients/models/Assignment'; import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse'; @@ -9,6 +9,7 @@ import Exercise from '../sharedByClients/models/Exercise'; import GitlabToken from '../shared/types/Gitlab/GitlabToken'; import User from '../sharedByClients/models/User'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; +import * as Gitlab from '@gitbeaker/rest'; import DojoBackendHelper from '../sharedByClients/helpers/Dojo/DojoBackendHelper'; @@ -106,7 +107,7 @@ class DojoBackendManager { } } - public async createAssignment(name: string, members: Array<GitlabUser>, templateIdOrNamespace: string | null, verbose: boolean = true): Promise<Assignment> { + public async createAssignment(name: string, members: Array<Gitlab.UserSchema>, templateIdOrNamespace: string | null, verbose: boolean = true): Promise<Assignment> { const spinner: ora.Ora = ora('Creating assignment...'); if ( verbose ) { @@ -131,7 +132,7 @@ class DojoBackendManager { } } - public async createExercise(assignmentName: string, members: Array<GitlabUser>, verbose: boolean = true): Promise<Exercise> { + public async createExercise(assignmentName: string, members: Array<Gitlab.UserSchema>, verbose: boolean = true): Promise<Exercise> { const spinner: ora.Ora = ora('Creating exercise...'); if ( verbose ) { diff --git a/NodeApp/src/managers/GitlabManager.ts b/NodeApp/src/managers/GitlabManager.ts index f8ee3f1..dda8d39 100644 --- a/NodeApp/src/managers/GitlabManager.ts +++ b/NodeApp/src/managers/GitlabManager.ts @@ -1,16 +1,47 @@ -import axios from 'axios'; -import ora from 'ora'; -import GitlabUser from '../shared/types/Gitlab/GitlabUser'; -import GitlabRoute from '../shared/types/Gitlab/GitlabRoute'; -import SharedConfig from '../shared/config/SharedConfig'; -import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; -import fs from 'fs-extra'; -import { spawn } from 'child_process'; +import ora from 'ora'; +import SharedConfig from '../shared/config/SharedConfig'; +import fs from 'fs-extra'; +import { spawn } from 'child_process'; +import { Gitlab, UserSchema } from '@gitbeaker/rest'; +import * as GitlabCore from '@gitbeaker/core'; +import { EditNotificationSettingsOptions } from '@gitbeaker/core'; +import { GitbeakerRequestError } from '@gitbeaker/requester-utils'; +import { StatusCodes } from 'http-status-codes'; +import { NotificationSettingSchema } from '@gitbeaker/core/dist'; +import GlobalHelper from '../helpers/GlobalHelper'; + + +type getGitlabUser = (param: number | string) => Promise<UserSchema | undefined> class GitlabManager { - private getApiUrl(route: GitlabRoute): string { - return `${ SharedConfig.gitlab.apiURL }${ route }`; + private api!: GitlabCore.Gitlab<false>; + private readonly refreshTokenFunction?: () => Promise<string>; + + setToken(token: string) { + this.api = new Gitlab(Object.assign({ + host : SharedConfig.gitlab.URL, + token: token + })); + } + + constructor(token: string, refreshTokenFunction?: () => Promise<string>) { + this.refreshTokenFunction = refreshTokenFunction; + this.setToken(token); + } + + private async executeGitlabRequest<T>(request: () => Promise<T>, refreshTokenIfNeeded: boolean = true): Promise<T> { + try { + return await request(); + } catch ( error ) { + if ( this.refreshTokenFunction && refreshTokenIfNeeded && error instanceof GitbeakerRequestError && error.cause?.response.status === StatusCodes.UNAUTHORIZED ) { + this.setToken(await this.refreshTokenFunction()); + + return this.executeGitlabRequest(request, false); + } else { + throw error; + } + } } public async testToken(verbose: boolean = true): Promise<[ boolean, boolean ]> { @@ -83,15 +114,15 @@ class GitlabManager { return result; } - public getNotificationSettings() { - return axios.get(this.getApiUrl(GitlabRoute.NOTIFICATION_SETTINGS)); + public getNotificationSettings(): Promise<NotificationSettingSchema> { + return this.executeGitlabRequest(() => this.api.NotificationSettings.show()); } - public setNotificationSettings(newSettings: Record<string, string>) { - return axios.put(this.getApiUrl(GitlabRoute.NOTIFICATION_SETTINGS), { params: new URLSearchParams(newSettings) }); + public setNotificationSettings(newSettings: EditNotificationSettingsOptions) { + return this.executeGitlabRequest(() => this.api.NotificationSettings.edit(newSettings)); } - private async getGitlabUsers(paramsToSearch: Array<string | number>, paramName: string, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> { + private async getGitlabUsers(paramsToSearch: Array<string | number>, searchFunction: getGitlabUser, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<UserSchema | undefined>> { try { return await Promise.all(paramsToSearch.map(async param => { const spinner: ora.Ora = ora({ @@ -101,18 +132,14 @@ class GitlabManager { if ( verbose ) { spinner.start(); } - const params: { [key: string]: unknown } = {}; - params[paramName] = param; - const user = await axios.get<Array<GitlabUser>>(this.getApiUrl(GitlabRoute.USERS_GET), { params: params }); - if ( user.data[0] ) { - const gitlabUser = user.data[0]; + const user = await searchFunction(param); + if ( user ) { if ( verbose ) { - spinner.succeed(`${ gitlabUser.username } (${ gitlabUser.id })`); + spinner.succeed(`${ user.username } (${ user.id })`); } - - return gitlabUser; + return user; } else { if ( verbose ) { spinner.fail(`${ param }`); @@ -126,30 +153,26 @@ class GitlabManager { } } - public async getUsersById(ids: Array<number>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> { - return this.getGitlabUsers(ids, 'id', verbose, verboseIndent); - } - - public async getUsersByUsername(usernames: Array<string>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> { - return this.getGitlabUsers(usernames, 'search', verbose, verboseIndent); + public async getUsersById(ids: Array<number>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<UserSchema | undefined>> { + return await this.getGitlabUsers(ids, GlobalHelper.sharedGitlabManager.getUserById.bind(GlobalHelper.sharedGitlabManager) as getGitlabUser, verbose, verboseIndent); } - public async getRepository(repoId: number): Promise<GitlabRepository> { - return axios.get(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', repoId.toString())); + public async getUsersByUsername(usernames: Array<string>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<UserSchema | undefined>> { + return await this.getGitlabUsers(usernames, GlobalHelper.sharedGitlabManager.getUserByUsername.bind(GlobalHelper.sharedGitlabManager) as getGitlabUser, verbose, verboseIndent); } - public async fetchMembers(options: { members_id?: Array<number>, members_username?: Array<string> }): Promise<Array<GitlabUser> | false> { + public async fetchMembers(options: { members_id?: Array<number>, members_username?: Array<string> }): Promise<Array<UserSchema> | false> { if ( options.members_id || options.members_username ) { ora('Checking Gitlab members:').start().info(); } - let members: Array<GitlabUser> = []; + let members: Array<UserSchema> = []; async function getMembers<T>(context: unknown, functionName: string, paramsToSearch: Array<T>): Promise<boolean> { - const gitlabUsers = await ((context as { [functionName: string]: (arg: Array<T>, verbose: boolean, verboseIndent: number) => Promise<Array<GitlabUser | undefined>> })[functionName])(paramsToSearch, true, 8); + const result = await ((context as { [functionName: string]: (arg: Array<T>, verbose: boolean, verboseIndent: number) => Promise<Array<UserSchema | undefined>> })[functionName])(paramsToSearch, true, 8); - if ( gitlabUsers.every(user => user) ) { - members = members.concat(gitlabUsers as Array<GitlabUser>); + if ( result.every(user => user) ) { + members = members.concat(result as Array<UserSchema>); return true; } else { return false; @@ -225,4 +248,4 @@ class GitlabManager { } -export default new GitlabManager(); +export default GitlabManager; diff --git a/NodeApp/src/managers/HttpManager.ts b/NodeApp/src/managers/HttpManager.ts index a28fc21..1dfae23 100644 --- a/NodeApp/src/managers/HttpManager.ts +++ b/NodeApp/src/managers/HttpManager.ts @@ -1,14 +1,15 @@ import axios, { AxiosError, AxiosRequestConfig, AxiosRequestHeaders } from 'axios'; -import SessionManager from './SessionManager'; -import FormData from 'form-data'; -import { StatusCodes } from 'http-status-codes'; -import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; -import { version } from '../config/Version'; -import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse'; -import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; -import boxen from 'boxen'; +import SessionManager from './SessionManager'; +import FormData from 'form-data'; +import { StatusCodes } from 'http-status-codes'; +import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; +import { version } from '../config/Version'; +import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse'; +import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; +import boxen from 'boxen'; +import Config from '../config/Config'; +import { stateConfigFile } from '../config/ConfigFiles'; import SharedConfig from '../shared/config/SharedConfig'; -import { stateConfigFile } from '../config/ConfigFiles'; import TextStyle from '../types/TextStyle'; @@ -22,7 +23,6 @@ class HttpManager { private registerRequestInterceptor() { axios.interceptors.request.use(config => { - if ( config.data instanceof FormData ) { config.headers = { ...config.headers, ...config.data.getHeaders() } as AxiosRequestHeaders; } @@ -42,10 +42,6 @@ class HttpManager { config.headers['client-version'] = version; } - if ( SessionManager.gitlabCredentials.accessToken && config.url && config.url.indexOf(SharedConfig.gitlab.apiURL) !== -1 ) { - config.headers.Authorization = `Bearer ${ SessionManager.gitlabCredentials.accessToken }`; - } - return config; }); } @@ -98,29 +94,6 @@ class HttpManager { } } - private async gitlabAuthorizationError(error: AxiosError, isFromGitlab: boolean): Promise<Promise<unknown> | undefined> { - const originalConfig = Object.assign({}, error.config, { _retry: false }); - - // Try to refresh the Gitlab tokens if the request have failed with a 401 error - if ( error.response && error.response.status === StatusCodes.UNAUTHORIZED && isFromGitlab && !originalConfig?._retry ) { - originalConfig._retry = true; - - try { - await SessionManager.refreshTokens(); - - return axios(error.config as AxiosRequestConfig); - } catch ( subError ) { - if ( subError instanceof AxiosError && subError.response && subError.response.data ) { - return Promise.reject(subError.response.data); - } - - return Promise.reject(subError); - } - } - - return undefined; - } - private registerResponseInterceptor() { axios.interceptors.response.use(response => { if ( response.data && response.data.sessionToken ) { @@ -140,11 +113,8 @@ class HttpManager { return response; }, async error => { if ( error.response ) { - const isFromApi = error.response.config.url && error.response.config.url.indexOf(ClientsSharedConfig.apiURL) !== -1; - const isFromGitlab = error.response.config.url && error.response.config.url.indexOf(SharedConfig.gitlab.URL) !== -1; - await this.gitlabAuthorizationError(error, isFromGitlab); this.apiMethodNotAllowed(error, isFromApi); this.apiAuthorizationError(error, isFromApi); } else { diff --git a/NodeApp/src/managers/SessionManager.ts b/NodeApp/src/managers/SessionManager.ts index c25748c..aa48e10 100644 --- a/NodeApp/src/managers/SessionManager.ts +++ b/NodeApp/src/managers/SessionManager.ts @@ -15,11 +15,10 @@ import EventEmitter from 'events'; import SharedConfig from '../shared/config/SharedConfig'; import chalk from 'chalk'; import inquirer from 'inquirer'; -import SharedGitlabManager from '../shared/managers/SharedGitlabManager'; -import GitlabManager from './GitlabManager'; import GitlabToken from '../shared/types/Gitlab/GitlabToken'; import open from 'open'; import { sessionConfigFile } from '../config/ConfigFiles'; +import GlobalHelper from '../helpers/GlobalHelper'; import TextStyle from '../types/TextStyle'; import DojoBackendHelper from '../sharedByClients/helpers/Dojo/DojoBackendHelper'; @@ -116,6 +115,11 @@ class SessionManager { set gitlabCredentials(credentials: DojoGitlabCredentials) { sessionConfigFile.setParam(LocalConfigKeys.GITLAB, credentials); + + if ( credentials.accessToken ) { + GlobalHelper.gitlabManager.setToken(credentials.accessToken); + GlobalHelper.sharedGitlabManager.setToken(credentials.accessToken); + } } private async getGitlabCodeFromHeadlessEnvironment(): Promise<string> { @@ -205,7 +209,7 @@ class SessionManager { }).start(); let gitlabTokens: GitlabToken; try { - gitlabTokens = await SharedGitlabManager.getTokens(gitlabCode); + gitlabTokens = await GlobalHelper.sharedGitlabManager.getTokens(gitlabCode); this.gitlabCredentials = { refreshToken: gitlabTokens.refresh_token, accessToken : gitlabTokens.access_token @@ -216,7 +220,7 @@ class SessionManager { throw error; } - const isGitlabTokensValid = (await GitlabManager.testToken()).every(value => value); + const isGitlabTokensValid = (await GlobalHelper.gitlabManager.testToken()).every((value) => value); if ( !isGitlabTokensValid ) { throw new Error('Gitlab tokens are invalid'); } diff --git a/NodeApp/src/shared b/NodeApp/src/shared index 75fedb2..92e13a3 160000 --- a/NodeApp/src/shared +++ b/NodeApp/src/shared @@ -1 +1 @@ -Subproject commit 75fedb26c47bb6f707725307a79a45a13e62496d +Subproject commit 92e13a3dc0ca751737d782430f5e902f1ec20c14 diff --git a/NodeApp/src/sharedByClients b/NodeApp/src/sharedByClients index c4efbcf..488f4ee 160000 --- a/NodeApp/src/sharedByClients +++ b/NodeApp/src/sharedByClients @@ -1 +1 @@ -Subproject commit c4efbcfb2a50e7108e101fb673e84f87fad8e246 +Subproject commit 488f4ee9aab9fb87d198af93fdb860cc626963d8 -- GitLab