@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
771 lines (718 loc) • 31.2 kB
text/typescript
/*
* Copyright © 2020 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// tslint:disable:max-file-line-count
import { HandleCommand } from "@atomist/automation-client/lib/HandleCommand";
import { HandleEvent } from "@atomist/automation-client/lib/HandleEvent";
import { ConfigurationAware, HandlerContext } from "@atomist/automation-client/lib/HandlerContext";
import { Success } from "@atomist/automation-client/lib/HandlerResult";
import {
declareMappedParameter,
declareParameter,
declareSecret,
declareValue,
} from "@atomist/automation-client/lib/internal/metadata/decoratorSupport";
import { OnCommand } from "@atomist/automation-client/lib/onCommand";
import { eventHandlerFrom } from "@atomist/automation-client/lib/onEvent";
import { CommandDetails } from "@atomist/automation-client/lib/operations/CommandDetails";
import { GitHubRepoRef } from "@atomist/automation-client/lib/operations/common/GitHubRepoRef";
import { ProjectOperationCredentials } from "@atomist/automation-client/lib/operations/common/ProjectOperationCredentials";
import { andFilter } from "@atomist/automation-client/lib/operations/common/repoFilter";
import { RepoFinder } from "@atomist/automation-client/lib/operations/common/repoFinder";
import { RemoteRepoRef } from "@atomist/automation-client/lib/operations/common/RepoId";
import { RepoLoader } from "@atomist/automation-client/lib/operations/common/repoLoader";
import { relevantRepos } from "@atomist/automation-client/lib/operations/common/repoUtils";
import { editOne } from "@atomist/automation-client/lib/operations/edit/editAll";
import { PullRequest } from "@atomist/automation-client/lib/operations/edit/editModes";
import {
EditResult,
failedEdit,
ProjectEditor,
successfulEdit,
} from "@atomist/automation-client/lib/operations/edit/projectEditor";
import { chainEditors } from "@atomist/automation-client/lib/operations/edit/projectEditorOps";
import { GitHubRepoCreationParameters } from "@atomist/automation-client/lib/operations/generate/GitHubRepoCreationParameters";
import { GitProject } from "@atomist/automation-client/lib/project/git/GitProject";
import { isProject, Project } from "@atomist/automation-client/lib/project/Project";
import { NoParameters } from "@atomist/automation-client/lib/SmartParameters";
import { Maker, toFactory } from "@atomist/automation-client/lib/util/constructionUtils";
import { logger } from "@atomist/automation-client/lib/util/logger";
import { codeBlock, italic } from "@atomist/slack-messages";
import * as _ from "lodash";
import { GitHubRepoTargets } from "../../api/command/target/GitHubRepoTargets";
import { isTransformModeSuggestion } from "../../api/command/target/TransformModeSuggestion";
import { NoParameterPrompt } from "../../api/context/parameterPrompt";
import { NoPreferenceStore } from "../../api/context/preferenceStore";
import { SdmContext } from "../../api/context/SdmContext";
import { createSkillContext } from "../../api/context/skillContext";
import { CommandListenerInvocation } from "../../api/listener/CommandListener";
import { isValidationError, RepoTargets } from "../../api/machine/RepoTargets";
import { SoftwareDeliveryMachineOptions } from "../../api/machine/SoftwareDeliveryMachineOptions";
import { ProjectPredicate } from "../../api/mapping/PushTest";
import { CodeInspectionRegistration, CodeInspectionResult } from "../../api/registration/CodeInspectionRegistration";
import { CodeTransform, CodeTransformOrTransforms } from "../../api/registration/CodeTransform";
import { CodeTransformRegistration } from "../../api/registration/CodeTransformRegistration";
import { CommandHandlerRegistration } from "../../api/registration/CommandHandlerRegistration";
import { CommandRegistration } from "../../api/registration/CommandRegistration";
import { EventHandlerRegistration } from "../../api/registration/EventHandlerRegistration";
import { GeneratorRegistration } from "../../api/registration/GeneratorRegistration";
import { ParametersBuilder } from "../../api/registration/ParametersBuilder";
import {
DeclarationType,
MappedParameterOrSecretDeclaration,
NamedParameter,
ParametersDefinition,
ParametersListing,
ValueDeclaration,
} from "../../api/registration/ParametersDefinition";
import { createCommand } from "../command/createCommand";
import { generatorCommand, isSeedDrivenGeneratorParameters } from "../command/generator/generatorCommand";
import { chattyDryRunAwareEditor } from "../command/transform/chattyDryRunAwareEditor";
import { LoggingProgressLog } from "../log/LoggingProgressLog";
import { formatDate } from "../misc/dateFormat";
import { createJob } from "../misc/job/createJob";
import { slackErrorMessage } from "../misc/slack/messages";
import { projectLoaderRepoLoader } from "./projectLoaderRepoLoader";
import { isRepoTargetingParameters, RepoTargetingParameters } from "./RepoTargetingParameters";
import { MachineOrMachineOptions, toMachineOptions } from "./toMachineOptions";
export const GeneratorTag = "generator";
export const InspectionTag = "inspection";
export const TransformTag = "transform";
export function codeTransformRegistrationToCommand(
sdm: MachineOrMachineOptions,
ctr: CodeTransformRegistration<any>,
): Maker<HandleCommand> {
tagWith(ctr, TransformTag);
const mo = toMachineOptions(sdm);
addDryRunParameters(ctr);
addJobParameters(ctr);
addParametersDefinedInBuilder(ctr);
ctr.paramsMaker = toRepoTargetingParametersMaker(
ctr.paramsMaker || NoParameters,
ctr.targets || mo.targets || GitHubRepoTargets,
);
const description = ctr.description || ctr.name;
const asCommand: CommandHandlerRegistration = {
description,
...(ctr as CommandRegistration<any>),
listener: async ci => {
ci.credentials = await resolveCredentialsPromise(ci.credentials);
const targets = (ci.parameters as RepoTargetingParameters).targets;
const vr = targets.bindAndValidate();
if (isValidationError(vr)) {
return ci.addressChannels(
slackErrorMessage(
`Code Transform`,
`Invalid parameters to code transform ${italic(ci.commandName)}:
${vr.message}`,
ci.context,
),
);
}
const repoFinder: RepoFinder = !!(ci.parameters as RepoTargetingParameters).targets.repoRef
? () => Promise.resolve([(ci.parameters as RepoTargetingParameters).targets.repoRef])
: ctr.repoFinder || toMachineOptions(sdm).repoFinder;
const repoLoader: RepoLoader = !!ctr.repoLoader
? ctr.repoLoader(ci.parameters)
: projectLoaderRepoLoader(mo.projectLoader, ci.credentials, false, ci.context);
const concurrency = {
maxConcurrent: 2, // TODO make maxConcurrent globally configurable
requiresJob: false,
...(ctr.concurrency || {}),
};
try {
const ids = await relevantRepos(ci.context, repoFinder, andFilter(targets.test, ctr.repoFilter));
const requiresJob = _.get(ci.parameters, "job.required", concurrency.requiresJob);
if (ids.length > 1 || !!requiresJob) {
const params: any = {
...ci.parameters,
};
params.targets.repos = undefined;
params.targets.repo = undefined;
delete params["job.name"];
delete params["job.description"];
delete params["job.required"];
await createJob(
{
name: _.get(ci.parameters, "job.name") || `CodeTransform/${ci.commandName}`,
command: ci.commandName,
parameters: ids.map(id => ({
...params,
"targets": {
owner: id.owner,
repo: id.repo,
branch: id.branch,
sha: id.sha,
},
"job.required": false,
})),
description:
_.get(ci.parameters, "job.description") ||
`Running code transform ${italic(ci.commandName)} on ${ids.length} ${
ids.length === 1 ? "repository" : "repositories"
}`,
concurrentTasks: concurrency.maxConcurrent,
},
ci.context,
);
} else {
const editMode = toEditModeOrFactory(ctr, ci);
const result = await editOne<any>(
ci.context,
ci.credentials,
chattyDryRunAwareEditor(
ctr,
toScalarProjectEditor(ctr.transform, toMachineOptions(sdm), ctr.projectTest),
),
editMode,
ids[0],
ci.parameters,
repoLoader,
);
if (!!ctr.onTransformResults) {
await ctr.onTransformResults([result], {
...ci,
progressLog: new LoggingProgressLog(ctr.name, "debug"),
});
} else if (!!result && !!result.error) {
const error = result.error;
return ci.addressChannels(
slackErrorMessage(
`Code Transform`,
`Code transform ${italic(ci.commandName)} failed${
!!error.message ? ":\n\n" + codeBlock(error.message) : "."
}`,
ci.context,
),
);
} else {
logger.debug("No react function to react to result of code transformation '%s'", ctr.name);
}
}
} catch (e) {
return ci.addressChannels(
slackErrorMessage(
`Code Transform`,
`Code transform ${italic(ci.commandName)} failed${
!!e.message ? ":\n\n" + codeBlock(e.message) : "."
}`,
ci.context,
),
);
}
},
};
return commandHandlerRegistrationToCommand(sdm, asCommand);
}
export function codeInspectionRegistrationToCommand<R>(
sdm: MachineOrMachineOptions,
cir: CodeInspectionRegistration<R, any>,
): Maker<HandleCommand> {
tagWith(cir, InspectionTag);
const mo = toMachineOptions(sdm);
addJobParameters(cir);
addParametersDefinedInBuilder(cir);
cir.paramsMaker = toRepoTargetingParametersMaker(
cir.paramsMaker || NoParameters,
cir.targets || mo.targets || GitHubRepoTargets,
);
const description = cir.description || cir.name;
const asCommand: CommandHandlerRegistration = {
description,
...(cir as CommandRegistration<any>),
listener: async ci => {
ci.credentials = await resolveCredentialsPromise(ci.credentials);
const targets = (ci.parameters as RepoTargetingParameters).targets;
const vr = targets.bindAndValidate();
if (isValidationError(vr)) {
return ci.addressChannels(
slackErrorMessage(
`Code Inspection`,
`Invalid parameters to code inspection ${italic(ci.commandName)}:
${codeBlock(vr.message)}`,
ci.context,
),
);
}
const action: (p: Project, params: any) => Promise<CodeInspectionResult<R>> = async p => {
if (!!cir.projectTest && !(await cir.projectTest(p))) {
return { repoId: p.id, result: undefined };
}
return {
repoId: p.id,
result: await cir.inspection(p, { ...ci, progressLog: new LoggingProgressLog(cir.name, "debug") }),
};
};
const repoFinder: RepoFinder = !!(ci.parameters as RepoTargetingParameters).targets.repoRef
? () => Promise.resolve([(ci.parameters as RepoTargetingParameters).targets.repoRef])
: cir.repoFinder || toMachineOptions(sdm).repoFinder;
const repoLoader: RepoLoader = !!cir.repoLoader
? cir.repoLoader(ci.parameters)
: projectLoaderRepoLoader(
mo.projectLoader,
(ci.parameters as RepoTargetingParameters).targets.credentials,
true,
ci.context,
);
const concurrency = {
maxConcurrent: 2, // TODO make maxConcurrent globally configurable
requiresJob: false,
...(cir.concurrency || {}),
};
try {
const ids = await relevantRepos(ci.context, repoFinder, andFilter(targets.test, cir.repoFilter));
const requiresJob = _.get(ci.parameters, "job.required", concurrency.requiresJob);
if (ids.length > 1 || !!requiresJob) {
const params: any = {
...ci.parameters,
};
params.targets.repos = undefined;
params.targets.repo = undefined;
delete params["job.name"];
delete params["job.description"];
delete params["job.required"];
await createJob(
{
name: `CodeInspection/${ci.commandName}`,
command: ci.commandName,
parameters: ids.map(id => ({
...params,
"targets": {
owner: id.owner,
repo: id.repo,
branch: id.branch,
sha: id.sha,
},
"job.required": false,
})),
description: `Running code inspection ${italic(ci.commandName)} on ${
ids.length
} repositories`,
},
ci.context,
);
} else {
const project = await repoLoader(ids[0]);
const result = await action(project, ci.parameters);
if (!!cir.onInspectionResults) {
await cir.onInspectionResults([result], ci);
} else {
logger.debug("No react function to react to results of code inspection '%s'", cir.name);
}
}
} catch (e) {
return ci.addressChannels(
slackErrorMessage(
`Code Inspection`,
`Code Inspection ${italic(ci.commandName)} failed:
${codeBlock(e.message)}`,
ci.context,
),
);
}
},
};
return commandHandlerRegistrationToCommand(sdm, asCommand);
}
/**
* Tag the command details with the given tag if it isn't already
* @param {Partial<CommandDetails>} e
* @param {string} tag
*/
function tagWith(e: Partial<CommandDetails>, tag: string): void {
if (!e.tags) {
e.tags = [];
}
if (typeof e.tags === "string") {
e.tags = [e.tags];
}
if (!e.tags.includes(tag)) {
e.tags.push(tag);
}
}
export function generatorRegistrationToCommand<P = any>(
sdm: MachineOrMachineOptions,
e: GeneratorRegistration<P>,
): Maker<HandleCommand<P>> {
tagWith(e, GeneratorTag);
if (!e.paramsMaker) {
e.paramsMaker = (NoParameters as any) as Maker<P>;
}
if (e.startingPoint && isProject(e.startingPoint) && !e.startingPoint.id) {
// TODO should probably be handled in automation-client
e.startingPoint.id = new GitHubRepoRef("ignore", "this");
}
addParametersDefinedInBuilder(e);
return () =>
generatorCommand(
sdm,
() => toScalarProjectEditor(e.transform, toMachineOptions(sdm)),
e.name,
e.paramsMaker,
e.fallbackTarget || GitHubRepoCreationParameters,
e.startingPoint,
e as any, // required because we redefine the afterAction
e,
);
}
export function commandHandlerRegistrationToCommand<P = NoParameters>(
sdm: MachineOrMachineOptions,
c: CommandHandlerRegistration<P>,
): Maker<HandleCommand<P>> {
return () => createCommand(sdm, toOnCommand(c), c.name, c.paramsMaker, c);
}
export function eventHandlerRegistrationToEvent(
sdm: MachineOrMachineOptions,
e: EventHandlerRegistration<any, any>,
): Maker<HandleEvent> {
addParametersDefinedInBuilder(e);
return () =>
eventHandlerFrom(e.listener, e.paramsMaker || NoParameters, e.subscription, e.name, e.description, e.tags);
}
export class CommandListenerExecutionInterruptError extends Error {
constructor(public readonly message: string) {
super(message);
}
}
function toOnCommand<PARAMS>(c: CommandHandlerRegistration<any>): (sdm: MachineOrMachineOptions) => OnCommand<PARAMS> {
addParametersDefinedInBuilder(c);
return sdm => async (context, parameters) => {
const cli = toCommandListenerInvocation(c, context, parameters, toMachineOptions(sdm));
cli.credentials = await resolveCredentialsPromise(cli.credentials);
try {
const result = await c.listener(cli);
return !!result ? result : Success;
} catch (err) {
if (err instanceof CommandListenerExecutionInterruptError) {
return {
code: -1,
};
} else {
logger.error("Error executing command '%s': %s", cli.commandName, err.message);
logger.error(err.stack);
return {
code: 1,
message: err.message,
};
}
}
};
}
export function toCommandListenerInvocation<P>(
c: CommandRegistration<P>,
context: HandlerContext,
parameters: P,
sdm: SoftwareDeliveryMachineOptions,
): CommandListenerInvocation {
// It may already be there
let credentials = !!context ? ((context as any) as SdmContext).credentials : undefined;
let ids: RemoteRepoRef[];
if (isSeedDrivenGeneratorParameters(parameters)) {
credentials = parameters.target.credentials;
ids = [parameters.target.repoRef];
} else if (isRepoTargetingParameters(parameters)) {
credentials = parameters.targets.credentials;
ids = !!parameters.targets.repoRef ? [parameters.targets.repoRef] : undefined;
}
if (!!sdm.credentialsResolver) {
try {
credentials = sdm.credentialsResolver.commandHandlerCredentials(context, ids ? ids[0] : undefined);
} catch (e) {
logger.debug(`Failed to obtain credentials from credentialsResolver: ${e.message}`);
}
}
let matches: RegExpExecArray;
if (c.intent instanceof RegExp) {
matches = c.intent.exec((context as any).trigger.raw_message);
}
const addressChannels = (msg, opts) => context.messageClient.respond(msg, opts);
const promptFor = sdm.parameterPromptFactory ? sdm.parameterPromptFactory(context) : NoParameterPrompt as any;
const preferences = sdm.preferenceStoreFactory ? sdm.preferenceStoreFactory(context) : NoPreferenceStore;
const configuration = (((context || {}) as any) as ConfigurationAware).configuration;
return {
commandName: c.name,
context,
parameters,
addressChannels,
configuration,
promptFor,
preferences,
credentials,
ids,
matches,
skill: createSkillContext(context),
};
}
export const DryRunParameter: NamedParameter = {
name: "dry-run",
description: "Run Code Transform in dry run mode so that changes aren't committed to the repository",
required: false,
defaultValue: false,
type: "boolean",
};
export const MsgIdParameter: NamedParameter = {
name: "msgId",
description: "Id of the code transform message",
required: false,
type: "string",
displayable: false,
};
/**
* Add the dryRun parameter into the list of parameters
*/
function addDryRunParameters<PARAMS>(c: CommandRegistration<PARAMS>): void {
const params = toParametersListing(c.parameters || ({} as any));
params.parameters.push(DryRunParameter, MsgIdParameter);
c.parameters = params;
}
export const JobNameParameter: NamedParameter = {
name: "job.name",
description: "Name of the job to create",
required: false,
type: "string",
displayable: false,
};
export const JobDescriptionParameter: NamedParameter = {
name: "job.description",
description: "Description of the job to create",
required: false,
type: "string",
displayable: false,
};
export const JobRequiredParameter: NamedParameter = {
name: "job.required",
description: "Is job required",
required: false,
type: "boolean",
};
/**
* Add the job parameter into the list of parameters
*/
function addJobParameters<PARAMS>(c: CommandRegistration<PARAMS>): void {
const params = toParametersListing(c.parameters || ({} as any));
params.parameters.push(JobNameParameter, JobDescriptionParameter, JobRequiredParameter);
c.parameters = params;
}
/**
* Add to the existing ParametersMaker any parameters defined in the builder itself
* @param {CommandHandlerRegistration<PARAMS>} c
*/
function addParametersDefinedInBuilder<PARAMS>(c: CommandRegistration<PARAMS>): void {
const oldMaker = c.paramsMaker || NoParameters;
if (!!c.parameters) {
c.paramsMaker = () => {
const paramsInstance: any = toFactory(oldMaker)();
const paramListing = toParametersListing(c.parameters as any);
paramListing.parameters.forEach(p => {
paramsInstance[p.name] = p.defaultValue;
declareParameter(paramsInstance, p.name, p);
});
paramListing.mappedParameters.forEach(mp =>
declareMappedParameter(paramsInstance, mp.name, mp.uri, mp.required),
);
paramListing.secrets.forEach(s => declareSecret(paramsInstance, s.name, s.uri));
paramListing.values.forEach(v =>
declareValue(paramsInstance, v.name, { path: v.path, required: v.required, type: v.type }),
);
return paramsInstance;
};
}
}
function isMappedParameterOrSecretDeclaration(x: any): x is MappedParameterOrSecretDeclaration {
const maybe = x as MappedParameterOrSecretDeclaration;
return !!maybe && !!maybe.declarationType;
}
function isValueDeclaration(x: any): x is ValueDeclaration {
const maybe = x as ValueDeclaration;
return !!maybe && maybe.path !== undefined && maybe.path !== null;
}
function isParametersListing(p: ParametersDefinition<any>): p is ParametersListing {
const maybe = p as ParametersListing;
return maybe.parameters !== undefined && maybe.mappedParameters !== undefined;
}
export function toParametersListing(p: ParametersDefinition<any>): ParametersListing {
if (isParametersListing(p)) {
return p;
}
const builder = new ParametersBuilder();
for (const name of Object.getOwnPropertyNames(p)) {
const value = p[name];
if (isMappedParameterOrSecretDeclaration(value)) {
switch (value.declarationType) {
case DeclarationType.Mapped:
builder.addMappedParameters({ name, uri: value.uri, required: value.required });
break;
case DeclarationType.Secret:
builder.addSecrets({ name, uri: value.uri });
break;
}
} else if (isValueDeclaration(value)) {
builder.addValues({ name, path: value.path, required: value.required, type: value.type });
} else {
builder.addParameters({ name, ...value });
}
}
return builder;
}
/**
* Convert to legacy automation-client "editor" signature
* @param {CodeTransformOrTransforms<PARAMS>} ctot
* @param {ProjectPredicate} projectPredicate
* @return {ProjectEditor<PARAMS>}
*/
export function toScalarProjectEditor<PARAMS>(
ctot: CodeTransformOrTransforms<PARAMS>,
sdm: SoftwareDeliveryMachineOptions,
projectPredicate?: ProjectPredicate,
): ProjectEditor<PARAMS> {
const unguarded = Array.isArray(ctot)
? chainEditors(...ctot.map(c => toProjectEditor(c, sdm)))
: toProjectEditor(ctot, sdm);
if (!!projectPredicate) {
// Filter out this project if it doesn't match the predicate
return async (p, context, params) => {
return (await projectPredicate(p))
? unguarded(p, context, params)
: Promise.resolve({ success: true, edited: false, target: p });
};
}
return unguarded;
}
// Convert to an old style, automation-client, ProjectEditor to allow
// underlying code to work for now
function toProjectEditor<P>(ct: CodeTransform<P>, sdm: SoftwareDeliveryMachineOptions): ProjectEditor<P> {
return async (p, ctx, params) => {
const ci = toCommandListenerInvocation(p, ctx, params, toMachineOptions(sdm));
ci.credentials = await resolveCredentialsPromise(ci.credentials);
// Mix in handler context for old style callers
const n = await ct(
p,
{
...ctx,
...ci,
} as any,
params,
);
if (n === undefined) {
// The transform returned void
return { target: p, edited: await isDirty(p), success: true };
}
const r: Project | EditResult = n as any;
try {
return isProject(r) ? successfulEdit(r, await isDirty(r)) : r;
} catch (e) {
return failedEdit(p, e);
}
};
}
async function isDirty(p: Project): Promise<boolean> {
if (isGitProject(p)) {
try {
const status = await p.gitStatus();
return !status.isClean;
} catch {
// Ignore
}
}
return undefined;
}
function isGitProject(p: Project): p is GitProject {
const maybe = p as GitProject;
return !!maybe.gitStatus;
}
/**
* Return a parameters maker that is targeting aware
* @param {Maker<PARAMS>} paramsMaker
* @param targets targets parameters to set if necessary
* @return {Maker<EditorOrReviewerParameters & PARAMS>}
*/
export function toRepoTargetingParametersMaker<PARAMS>(
paramsMaker: Maker<PARAMS>,
targets: Maker<RepoTargets>,
): Maker<RepoTargetingParameters & PARAMS> {
const sampleParams = toFactory(paramsMaker)();
return isRepoTargetingParameters(sampleParams)
? (paramsMaker as Maker<RepoTargetingParameters & PARAMS>)
: () => {
const rawParms: PARAMS = toFactory(paramsMaker)();
const allParms = rawParms as RepoTargetingParameters & PARAMS;
const targetsInstance: RepoTargets = toFactory(targets)();
allParms.targets = targetsInstance;
return allParms;
};
}
function toEditModeOrFactory<P>(ctr: CodeTransformRegistration<P>, ci: CommandListenerInvocation<P>): any {
const description = ctr.description || ctr.name;
if (!!ctr.transformPresentation) {
return (p: Project) =>
ctr.transformPresentation(
{
...ci,
progressLog: new LoggingProgressLog(ctr.name, "debug"),
},
p,
);
}
// Get EditMode from parameters if possible
if (isTransformModeSuggestion(ci.parameters)) {
const tms = ci.parameters;
return new PullRequest(tms.desiredBranchName, tms.desiredPullRequestTitle || description);
}
// Default it if not supplied
return new PullRequest(`transform-${gitBranchCompatible(ctr.name)}-${formatDate()}`, description);
}
/**
* Takes a potential git branch name and returns a legalised iteration of it
*
* @param name the git branch name to sanitise.
*/
export function gitBranchCompatible(name: string): string {
// handles spaces and .. ~ : ^ ? * [ @{
let branchName = name.replace(/\s+|(\.\.)+|~+|:+|\^+|\?+|\*+|\[+|(\@\{)+/g, "_");
// handles double slashes
branchName = branchName.replace(/(\/\/)+|(\\)+/g, "/");
// handles back slashes
branchName = branchName.replace(/\\+/g, "/");
if (branchName.startsWith(".") || branchName.startsWith("/")) {
branchName = branchName.substring(1);
}
if (branchName.endsWith(".") || branchName.endsWith("/")) {
branchName = branchName.slice(0, -1);
}
const lock = ".lock";
if (branchName.endsWith(lock)) {
branchName = branchName.slice(0, -lock.length);
}
if (branchName === "@") {
branchName = "at";
}
// handles ascii control characters
branchName = branchName.replace(/[\x00-\x1F\x7F]+/g, "");
return branchName;
}
export async function resolveCredentialsPromise(
creds: Promise<ProjectOperationCredentials> | ProjectOperationCredentials,
): Promise<ProjectOperationCredentials> {
if (creds instanceof Promise) {
try {
return await creds;
} catch (e) {
logger.debug(e.message);
}
} else if (!!creds) {
return creds;
}
return undefined;
}