Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/lib/utils/ref_validator.js')
-rw-r--r--app/assets/javascripts/lib/utils/ref_validator.js145
1 files changed, 145 insertions, 0 deletions
diff --git a/app/assets/javascripts/lib/utils/ref_validator.js b/app/assets/javascripts/lib/utils/ref_validator.js
new file mode 100644
index 00000000000..d679a3b4198
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/ref_validator.js
@@ -0,0 +1,145 @@
+import { __, sprintf } from '~/locale';
+
+// this service validates tagName agains git ref format.
+// the git spec can be found here: https://git-scm.com/docs/git-check-ref-format#_description
+
+// the ruby counterpart of the validator is here:
+// lib/gitlab/git_ref_validator.rb
+
+const EXPANDED_PREFIXES = ['refs/heads/', 'refs/remotes/', 'refs/tags'];
+const DISALLOWED_PREFIXES = ['-', '/'];
+const DISALLOWED_POSTFIXES = ['/'];
+const DISALLOWED_NAMES = ['HEAD', '@'];
+const DISALLOWED_SUBSTRINGS = [' ', '\\', '~', ':', '..', '^', '?', '*', '[', '@{'];
+const DISALLOWED_SEQUENCE_POSTFIXES = ['.lock', '.'];
+const DISALLOWED_SEQUENCE_PREFIXES = ['.'];
+
+// eslint-disable-next-line no-control-regex
+const CONTROL_CHARACTERS_REGEX = /[\x00-\x19\x7f]/;
+
+const toReadableString = (array) => array.map((item) => `"${item}"`).join(', ');
+
+const DisallowedPrefixesValidationMessage = sprintf(
+ __('Tag name should not start with %{prefixes}'),
+ {
+ prefixes: toReadableString([...EXPANDED_PREFIXES, ...DISALLOWED_PREFIXES]),
+ },
+ false,
+);
+
+const DisallowedPostfixesValidationMessage = sprintf(
+ __('Tag name should not end with %{postfixes}'),
+ { postfixes: toReadableString(DISALLOWED_POSTFIXES) },
+ false,
+);
+
+const DisallowedNameValidationMessage = sprintf(
+ __('Tag name cannot be one of the following: %{names}'),
+ { names: toReadableString(DISALLOWED_NAMES) },
+ false,
+);
+
+const EmptyNameValidationMessage = __('Tag name should not be empty');
+
+const DisallowedSubstringsValidationMessage = sprintf(
+ __('Tag name should not contain any of the following: %{substrings}'),
+ { substrings: toReadableString(DISALLOWED_SUBSTRINGS) },
+ false,
+);
+
+const DisallowedSequenceEmptyValidationMessage = __(
+ `No slash-separated tag name component can be empty`,
+);
+
+const DisallowedSequencePrefixesValidationMessage = sprintf(
+ __('No slash-separated component can begin with %{sequencePrefixes}'),
+ { sequencePrefixes: toReadableString(DISALLOWED_SEQUENCE_PREFIXES) },
+ false,
+);
+
+const DisallowedSequencePostfixesValidationMessage = sprintf(
+ __('No slash-separated component can end with %{sequencePostfixes}'),
+ { sequencePostfixes: toReadableString(DISALLOWED_SEQUENCE_POSTFIXES) },
+ false,
+);
+
+const ControlCharactersValidationMessage = __('Tag name should not contain any control characters');
+
+export const validationMessages = {
+ EmptyNameValidationMessage,
+ DisallowedPrefixesValidationMessage,
+ DisallowedPostfixesValidationMessage,
+ DisallowedNameValidationMessage,
+ DisallowedSubstringsValidationMessage,
+ DisallowedSequenceEmptyValidationMessage,
+ DisallowedSequencePrefixesValidationMessage,
+ DisallowedSequencePostfixesValidationMessage,
+ ControlCharactersValidationMessage,
+};
+
+export class ValidationResult {
+ isValid = true;
+ validationErrors = [];
+
+ addValidationError = (errorMessage) => {
+ this.isValid = false;
+ this.validationErrors.push(errorMessage);
+ };
+}
+
+export const validateTag = (refName) => {
+ if (typeof refName !== 'string') {
+ throw new Error('refName argument must be a string');
+ }
+
+ const validationResult = new ValidationResult();
+
+ if (!refName || refName.trim() === '') {
+ validationResult.addValidationError(EmptyNameValidationMessage);
+ return validationResult;
+ }
+
+ if (CONTROL_CHARACTERS_REGEX.test(refName)) {
+ validationResult.addValidationError(ControlCharactersValidationMessage);
+ }
+
+ if (DISALLOWED_NAMES.some((name) => name === refName)) {
+ validationResult.addValidationError(DisallowedNameValidationMessage);
+ }
+
+ if ([...EXPANDED_PREFIXES, ...DISALLOWED_PREFIXES].some((prefix) => refName.startsWith(prefix))) {
+ validationResult.addValidationError(DisallowedPrefixesValidationMessage);
+ }
+
+ if (DISALLOWED_POSTFIXES.some((postfix) => refName.endsWith(postfix))) {
+ validationResult.addValidationError(DisallowedPostfixesValidationMessage);
+ }
+
+ if (DISALLOWED_SUBSTRINGS.some((substring) => refName.includes(substring))) {
+ validationResult.addValidationError(DisallowedSubstringsValidationMessage);
+ }
+
+ const refNameParts = refName.split('/');
+
+ if (refNameParts.some((part) => part === '')) {
+ validationResult.addValidationError(DisallowedSequenceEmptyValidationMessage);
+ }
+
+ if (
+ refNameParts.some((part) =>
+ DISALLOWED_SEQUENCE_PREFIXES.some((prefix) => part.startsWith(prefix)),
+ )
+ ) {
+ validationResult.addValidationError(DisallowedSequencePrefixesValidationMessage);
+ }
+
+ if (
+ refNameParts.some((part) =>
+ DISALLOWED_SEQUENCE_POSTFIXES.some((postfix) => part.endsWith(postfix)),
+ )
+ ) {
+ validationResult.addValidationError(DisallowedSequencePostfixesValidationMessage);
+ }
+
+ return validationResult;
+};