Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-16 21:09:52 +00:00
parent bf1600d157
commit aa33f5d5bb
23 changed files with 668 additions and 128 deletions

View File

@ -10,13 +10,13 @@
/doc/ @gl-docsteam
/doc/administration/monitoring/ @aqualls
/doc/development/ @marcia @mjang1
/doc/development/documentation/ @mikelewis
/doc/development/documentation/ @cnorris
/doc/ci @marcel.amirault @sselhorn
/doc/operations @aqualls @eread
/doc/user/clusters @aqualls
/doc/user/infrastructure @aqualls
/doc/user/project/clusters @aqualls
/doc/.vale/ @marcel.amirault @eread @aqualls @mikelewis
/doc/.vale/ @marcel.amirault @eread @aqualls @cnorris
[Docs Create]
/doc/user/project/merge_requests/allow_collaboration.md @marcia

View File

@ -2,6 +2,7 @@
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import PublishToolbar from './publish_toolbar.vue';
import EditHeader from './edit_header.vue';
import EditDrawer from './edit_drawer.vue';
import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
@ -15,6 +16,7 @@ export default {
RichContentEditor,
PublishToolbar,
EditHeader,
EditDrawer,
UnsavedChangesConfirmDialog,
},
props: {
@ -48,6 +50,8 @@ export default {
parsedSource: parseSourceFile(this.preProcess(true, this.content)),
editorMode: EDITOR_TYPES.wysiwyg,
isModified: false,
hasMatter: false,
isDrawerOpen: false,
};
},
imageRepository: imageRepository(),
@ -55,10 +59,19 @@ export default {
editableContent() {
return this.parsedSource.content(this.isWysiwygMode);
},
editableMatter() {
return this.isDrawerOpen ? this.parsedSource.matter() : {};
},
hasSettings() {
return this.hasMatter && this.isWysiwygMode;
},
isWysiwygMode() {
return this.editorMode === EDITOR_TYPES.wysiwyg;
},
},
created() {
this.refreshEditHelpers();
},
methods: {
preProcess(isWrap, value) {
const formattedContent = formatter(value);
@ -67,9 +80,21 @@ export default {
: templater.unwrap(formattedContent);
return templatedContent;
},
refreshEditHelpers() {
this.isModified = this.parsedSource.isModified();
this.hasMatter = this.parsedSource.hasMatter();
},
onDrawerOpen() {
this.isDrawerOpen = true;
this.refreshEditHelpers();
},
onDrawerClose() {
this.isDrawerOpen = false;
this.refreshEditHelpers();
},
onInputChange(newVal) {
this.parsedSource.syncContent(newVal, this.isWysiwygMode);
this.isModified = this.parsedSource.isModified();
this.refreshEditHelpers();
},
onModeChange(mode) {
this.editorMode = mode;
@ -77,6 +102,9 @@ export default {
const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent);
this.$refs.editor.resetInitialValue(preProcessedContent);
},
onUpdateSettings(settings) {
this.parsedSource.syncMatter(settings);
},
onUploadImage({ file, imageUrl }) {
this.$options.imageRepository.add(file, imageUrl);
},
@ -93,6 +121,13 @@ export default {
<template>
<div class="d-flex flex-grow-1 flex-column h-100">
<edit-header class="py-2" :title="title" />
<edit-drawer
v-if="hasMatter"
:is-open="isDrawerOpen"
:settings="editableMatter"
@close="onDrawerClose"
@updateSettings="onUpdateSettings"
/>
<rich-content-editor
ref="editor"
:content="editableContent"
@ -106,9 +141,11 @@ export default {
<unsaved-changes-confirm-dialog :modified="isModified" />
<publish-toolbar
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
:has-settings="hasSettings"
:return-url="returnUrl"
:saveable="isModified"
:saving-changes="savingChanges"
@editSettings="onDrawerOpen"
@submit="onSubmit"
/>
</div>

View File

@ -0,0 +1,32 @@
<script>
import { GlDrawer } from '@gitlab/ui';
import FrontMatterControls from './front_matter_controls.vue';
export default {
components: {
GlDrawer,
FrontMatterControls,
},
props: {
isOpen: {
type: Boolean,
required: true,
},
settings: {
type: Object,
required: true,
},
},
};
</script>
<template>
<gl-drawer class="pt-6" :open="isOpen" @close="$emit('close')">
<template #header>{{ __('Page settings') }}</template>
<template>
<front-matter-controls
:settings="settings"
@updateSettings="$emit('updateSettings', $event)"
/>
</template>
</gl-drawer>
</template>

View File

@ -6,6 +6,11 @@ export default {
GlButton,
},
props: {
hasSettings: {
type: Boolean,
required: false,
default: false,
},
returnUrl: {
type: String,
required: false,
@ -31,12 +36,21 @@ export default {
s__('StaticSiteEditor|Return to site')
}}</gl-button>
<gl-button
v-if="hasSettings"
ref="settings"
:disabled="savingChanges"
@click="$emit('editSettings')"
>
{{ __('Settings') }}
</gl-button>
<gl-button
ref="submit"
variant="success"
:disabled="!saveable"
:loading="savingChanges"
@click="$emit('submit')"
>
<span>{{ __('Submit Changes') }}</span>
{{ __('Submit changes') }}
</gl-button>
</div>
</div>

View File

@ -17,33 +17,24 @@ const parseSourceFile = raw => {
const content = (isBody = false) => (isBody ? editable.content.trim() : trimmedEditable()); // gray-matter internally adds an eof newline so we trim to bypass, open issue: https://github.com/jonschlinkert/gray-matter/issues/96
const matter = () => editable.matter;
const matter = () => editable.data;
const syncMatter = newMatter => {
const targetMatter = newMatter.replace(/---/gm, ''); // TODO dynamic delimiter removal vs. hard code
const currentMatter = matter();
const currentContent = content();
const newSource = currentContent.replace(currentMatter, targetMatter);
syncContent(newSource);
editable.matter = newMatter;
};
const matterObject = () => editable.data;
const syncMatterObject = obj => {
editable.data = obj;
const syncMatter = settings => {
const source = grayMatter.stringify(editable.content, settings);
syncContent(source);
};
const isModified = () => trimmedEditable() !== raw;
const hasMatter = () => editable.matter.length > 0;
return {
matter,
syncMatter,
matterObject,
syncMatterObject,
content,
syncContent,
isModified,
hasMatter,
};
};

View File

@ -41,6 +41,10 @@ class Project < ApplicationRecord
STATISTICS_ATTRIBUTE = 'repositories_count'
UNKNOWN_IMPORT_URL = 'http://unknown.git'
# Hashed Storage versions handle rolling out new storage to project and dependents models:
# nil: legacy
# 1: repository
# 2: attachments
LATEST_STORAGE_VERSION = 2
HASHED_STORAGE_FEATURES = {
repository: 1,

View File

@ -0,0 +1,5 @@
---
title: Add a front matter editing UI in WYSIWYG mode of the Static Site Editor
merge_request: 41920
author:
type: added

View File

@ -403,6 +403,7 @@ sanitization
sbt
scatterplot
scatterplots
Schemastore
Sendmail
Sentry
serializer

View File

@ -127,5 +127,5 @@ unexpected timing. For example, when a source or target branch is advanced.
In this case, the pipeline fails because of `fatal: reference is not a tree:` error,
which indicates that the checkout-SHA is not found in the merge ref.
This behavior was improved at GitLab 12.4 by introducing [Persistent pipeline refs](../../pipelines/index.md#troubleshooting-fatal-reference-is-not-a-tree).
This behavior was improved at GitLab 12.4 by introducing [Persistent pipeline refs](../../troubleshooting.md#fatal-reference-is-not-a-tree-error).
You should be able to create pipelines at any timings without concerning the error.

View File

@ -535,32 +535,3 @@ GitLab provides API endpoints to:
- Trigger pipeline runs. For more information, see:
- [Triggering pipelines through the API](../triggers/README.md).
- [Pipeline triggers API](../../api/pipeline_triggers.md).
## Troubleshooting `fatal: reference is not a tree:`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17043) in GitLab 12.4.
Previously, you'd have encountered unexpected pipeline failures when you force-pushed
a branch to its remote repository. To illustrate the problem, suppose you've had the current workflow:
1. A user creates a feature branch named `example` and pushes it to a remote repository.
1. A new pipeline starts running on the `example` branch.
1. A user rebases the `example` branch on the latest `master` branch and force-pushes it to its remote repository.
1. A new pipeline starts running on the `example` branch again, however,
the previous pipeline (2) fails because of `fatal: reference is not a tree:` error.
This is because the previous pipeline cannot find a checkout-SHA (which associated with the pipeline record)
from the `example` branch that the commit history has already been overwritten by the force-push.
Similarly, [Pipelines for merged results](../merge_request_pipelines/pipelines_for_merged_results/index.md)
might have failed intermittently due to [the same reason](../merge_request_pipelines/pipelines_for_merged_results/index.md#intermittently-pipelines-fail-by-fatal-reference-is-not-a-tree-error).
As of GitLab 12.4, we've improved this behavior by persisting pipeline refs exclusively.
To illustrate its life cycle:
1. A pipeline is created on a feature branch named `example`.
1. A persistent pipeline ref is created at `refs/pipelines/<pipeline-id>`,
which retains the checkout-SHA of the associated pipeline record.
This persistent ref stays intact during the pipeline execution,
even if the commit history of the `example` branch has been overwritten by force-push.
1. The runner fetches the persistent pipeline ref and gets source code from the checkout-SHA.
1. When the pipeline finished, its persistent ref is cleaned up in a background process.

View File

@ -7,6 +7,220 @@ type: reference
# Troubleshooting CI/CD
GitLab provides several tools to help make troubleshooting your pipelines easier.
This guide also lists common issues and possible solutions.
## Verify syntax
An early source of problems can be incorrect syntax. The pipeline shows a `yaml invalid`
badge and does not start running if any syntax or formatting problems are found.
### Edit `gitlab-ci.yml` with the Web IDE
The [GitLab Web IDE](../user/project/web_ide/index.md) offers advanced authoring tools,
including syntax highlighting for the `.gitlab-ci.yml`, and is the recommended editing
experience (rather than the single file editor). It offers code completion suggestions
that ensure you are only using accepted keywords.
If you prefer to use another editor, you can use a schema like [the Schemastore `gitlab-ci` schema](https://json.schemastore.org/gitlab-ci)
with your editor of choice.
### Verify syntax with CI Lint tool
The [CI Lint tool](lint.md) is a simple way to ensure the syntax of a CI/CD configuration
file is correct. Paste in full `gitlab-ci.yml` files or individual jobs configuration,
to verify the basic syntax.
When a `.gitlab-ci.yml` file is present in a project, you can also use the CI Lint
tool to [simulate the creation of a full pipeline](lint.md#pipeline-simulation).
It does deeper verification of the configuration syntax.
## Verify variables
A key part of troubleshooting CI/CD is to verify which variables are present in a
pipeline, and what their values are. A lot of pipeline configuration is dependent
on variables, and verifying them is one of the fastest ways to find the source of
a problem.
[Export the full list of variables](variables/README.md#list-all-environment-variables)
available in each problematic job. Check if the variables you expect are present,
and check if their values are what you expect.
## GitLab CI/CD documentation
The [complete `gitlab-ci.yml` reference](yaml/README.md) contains a full list of
every keyword you may need to use to configure your pipelines.
You can also look at a large number of pipeline configuration [examples](examples/README.md)
and [templates](examples/README.md#cicd-templates).
### Documentation for pipeline types
Some pipeline types have their own detailed usage guides that you should read
if you are using that type:
- [Multi-project pipelines](multi_project_pipelines.md): Have your pipeline trigger
a pipeline in a different project.
- [Parent/child pipelines](parent_child_pipelines.md): Have your main pipeline trigger
and run separate pipelines in the same project. You can also
[dynamically generate the child pipeline's configuration](parent_child_pipelines.md#dynamic-child-pipelines)
at runtime.
- [Pipelines for Merge Requests](merge_request_pipelines/index.md): Run a pipeline
in the context of a merge request.
- [Pipelines for Merge Results](merge_request_pipelines/pipelines_for_merged_results/index.md):
Pipelines for merge requests that run on the combined source and target branch
- [Merge Trains](merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md):
Multiple pipelines for merged results that queue and run automatically before
changes are merged.
### Troubleshooting Guides for CI/CD features
There are troubleshooting guides available for some CI/CD features and related topics:
- [Container Registry](../user/packages/container_registry/index.md#troubleshooting-the-gitlab-container-registry)
- [GitLab Runner](https://docs.gitlab.com/runner/faq/)
- [Merge Trains](merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md#troubleshooting)
- [Docker Build](docker/using_docker_build.md#troubleshooting)
- [Environments](environments/deployment_safety.md#ensure-only-one-deployment-job-runs-at-a-time)
## Common CI/CD issues
A lot of common pipeline issues can be fixed by analyzing the behavior of the `rules`
or `only/except` configuration. You shouldn't use these two configurations in the same
pipeline, as they behave differently. It's hard to predict how a pipeline runs with
this mixed behavior.
If your `rules` or `only/except` configuration makes use of [predefined variables](variables/predefined_variables.md)
like `CI_PIPELINE_SOURCE`, `CI_MERGE_REQUEST_ID`, you should [verify them](#verify-variables)
as the first troubleshooting step.
### Jobs or pipelines don't run when expected
The `rules` or `only/except` keywords are what determine whether or not a job is
added to a pipeline. If a pipeline runs, but a job is not added to the pipeline,
it's usually due to `rules` or `only/except` configuration issues.
If a pipeline does not seem to run at all, with no error message, it may also be
due to `rules` or `only/except` configuration, or the `workflow: rules` keyword.
If you are converting from `only/except` to the `rules` keyword, you should check
the [`rules` configuration details](yaml/README.md#rules) carefully. The behavior
of `only/except` and `rules` is different and can cause unexpected behavior when migrating
between the two.
The [common `if` clauses for `rules`](yaml/README.md#common-if-clauses-for-rules)
can be very helpful for examples of how to write rules that behave the way you expect.
#### Two pipelines run at the same time
Two pipelines can run when pushing a commit to a branch that has an open merge request
associated with it. Usually one pipeline is a merge request pipeline, and the other
is a branch pipeline.
This is usually caused by the `rules` configuration, and there are several ways to
[prevent duplicate pipelines](yaml/README.md#prevent-duplicate-pipelines).
#### A job is not in the pipeline
GitLab determines if a job is added to a pipeline based on the [`only/except`](yaml/README.md#onlyexcept-basic)
or [`rules`](yaml/README.md#rules) defined for the job. If it didn't run, it's probably
not evaluating as you expect.
#### No pipeline or the wrong type of pipeline runs
Before a pipeline can run, GitLab evaluates all the jobs in the configuration and tries
to add them to all available pipeline types. A pipeline does not run if no jobs are added
to it at the end of the evaluation.
If a pipeline did not run, it's likely that all the jobs had `rules` or `only/except` that
blocked them from being added to the pipeline.
If the wrong pipeline type ran, then the `rules` or `only/except` configuration should
be checked to make sure the jobs are added to the correct pipeline type. For
example, if a merge request pipeline did not run, the jobs may have been added to
a branch pipeline instead.
It's also possible that your [`workflow: rules`](yaml/README.md#workflowrules) configuration
blocked the pipeline, or allowed the wrong pipeline type.
### A job runs unexpectedly
A common reason a job is added to a pipeline unexpectedly is because the `changes`
keyword always evaluates to true in certain cases. For example, `changes` is always
true in certain pipeline types, including scheduled pipelines and pipelines for tags.
The `changes` keyword is used in combination with [`only/except`](yaml/README.md#onlychangesexceptchanges)
or [`rules`](yaml/README.md#ruleschanges)). It's recommended to use `changes` with
`rules` or `only/except` configuration that ensures the job is only added to branch
pipelines or merge request pipelines.
### "fatal: reference is not a tree" error
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17043) in GitLab 12.4.
Previously, you'd have encountered unexpected pipeline failures when you force-pushed
a branch to its remote repository. To illustrate the problem, suppose you've had the current workflow:
1. A user creates a feature branch named `example` and pushes it to a remote repository.
1. A new pipeline starts running on the `example` branch.
1. A user rebases the `example` branch on the latest `master` branch and force-pushes it to its remote repository.
1. A new pipeline starts running on the `example` branch again, however,
the previous pipeline (2) fails because of `fatal: reference is not a tree:` error.
This is because the previous pipeline cannot find a checkout-SHA (which is associated with the pipeline record)
from the `example` branch that the commit history has already been overwritten by the force-push.
Similarly, [Pipelines for merged results](merge_request_pipelines/pipelines_for_merged_results/index.md)
might have failed intermittently due to [the same reason](merge_request_pipelines/pipelines_for_merged_results/index.md#intermittently-pipelines-fail-by-fatal-reference-is-not-a-tree-error).
As of GitLab 12.4, we've improved this behavior by persisting pipeline refs exclusively.
To illustrate its life cycle:
1. A pipeline is created on a feature branch named `example`.
1. A persistent pipeline ref is created at `refs/pipelines/<pipeline-id>`,
which retains the checkout-SHA of the associated pipeline record.
This persistent ref stays intact during the pipeline execution,
even if the commit history of the `example` branch has been overwritten by force-push.
1. The runner fetches the persistent pipeline ref and gets source code from the checkout-SHA.
1. When the pipeline finishes, its persistent ref is cleaned up in a background process.
### Merge request pipeline messages
The merge request pipeline widget shows information about the pipeline status in
a merge request. It's displayed above the [ability to merge status widget](#merge-request-status-messages).
#### "Checking pipeline status" message
This message is shown when the merge request has no pipeline associated with the
latest commit yet. This might be because:
- GitLab hasn't finished creating the pipeline yet.
- You are using an external CI service and GitLab hasn't heard back from the service yet.
- You are not using CI/CD pipelines in your project.
- The latest pipeline was deleted (this is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/214323)).
After the pipeline is created, the message updates with the pipeline status.
### Merge request status messages
The merge request status widget shows the **Merge** button and whether or not a merge
request is ready to merge. If the merge request can't be merged, the reason for this
is displayed.
If the pipeline is still running, the **Merge** button is replaced with the
**Merge when pipeline succeeds** button.
If [**Merge Trains**](merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md)
are enabled, the button is either **Add to merge train** or **Add to merge train when pipeline succeeds**. **(PREMIUM)**
#### "A CI/CD pipeline must run and be successful before merge" message
This message is shown if the [Pipelines must succeed](../user/project/merge_requests/merge_when_pipeline_succeeds.md#only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds)
setting is enabled in the project and a pipeline has not yet run successfully.
This also applies if the pipeline has not been created yet, or if you are waiting
for an external CI service. If you don't use pipelines for your project, then you
should disable **Pipelines must succeed** so you can accept merge requests.
## Pipeline warnings
Pipeline configuration warnings are shown when you:
@ -14,41 +228,19 @@ Pipeline configuration warnings are shown when you:
- [Validate configuration with the CI Lint tool](yaml/README.md#validate-the-gitlab-ciyml).
- [Manually run a pipeline](pipelines/index.md#run-a-pipeline-manually).
### "Job may allow multiple pipelines to run for a single action"
### "Job may allow multiple pipelines to run for a single action" warning
When you use [`rules`](yaml/README.md#rules) with a `when:` clause without
an `if:` clause, multiple pipelines may run. Usually
this occurs when you push a commit to a branch that has an open merge request associated with it.
When you use [`rules`](yaml/README.md#rules) with a `when:` clause without an `if:`
clause, multiple pipelines may run. Usually this occurs when you push a commit to
a branch that has an open merge request associated with it.
To [prevent duplicate pipelines](yaml/README.md#prevent-duplicate-pipelines), use
[`workflow: rules`](yaml/README.md#workflowrules) or rewrite your rules
to control which pipelines can run.
[`workflow: rules`](yaml/README.md#workflowrules) or rewrite your rules to control
which pipelines can run.
## Merge request pipeline widget
## How to get help
The merge request pipeline widget shows information about the pipeline status in a Merge Request. It's displayed above the [merge request ability to merge widget](#merge-request-ability-to-merge-widget).
If you are unable to resolve pipeline issues, you can get help from:
There are several messages that can be displayed depending on the status of the pipeline.
### "Checking pipeline status"
This message is shown when the merge request has no pipeline associated with the latest commit yet. This might be because:
- GitLab hasn't finished creating the pipeline yet.
- You are using an external CI service and GitLab hasn't heard back from the service yet.
- You are not using CI/CD pipelines in your project.
- The latest pipeline was deleted (this is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/214323)).
After the pipeline is created, the message will update with the pipeline status.
## Merge request ability to merge widget
The merge request status widget shows the **Merge** button and whether or not a merge request is ready to merge. If the merge request can't be merged, the reason for this is displayed.
If the pipeline is still running, the **Merge** button is replaced with the **Merge when pipeline succeeds** button.
If [**Merge Trains**](merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md) are enabled, the button is either **Add to merge train** or **Add to merge train when pipeline succeeds**. **(PREMIUM)**
### "A CI/CD pipeline must run and be successful before merge"
This message is shown if the [Pipelines must succeed](../user/project/merge_requests/merge_when_pipeline_succeeds.md#only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds) setting is enabled in the project and a pipeline has not yet run successfully. This also applies if the pipeline has not been created yet, or if you are waiting for an external CI service. If you don't use pipelines for your project, then you should disable **Pipelines must succeed** so you can accept merge requests.
- The [GitLab community forum](https://forum.gitlab.com/)
- GitLab [Support](https://about.gitlab.com/support/)

View File

@ -121,3 +121,71 @@ may be affected by factors such as (but not limited to):
In other words, this data is only reliable for a frequently used database with
plenty of data and with as many GitLab features enabled (and being used) as
possible.
## Requirements for naming indexes
Indexes with complex definitions need to be explicitly named rather than
relying on the implicit naming behavior of migration methods. In short,
that means you **must** provide an explicit name argument for an index
created with one or more of the following options:
- `where`
- `using`
- `order`
- `length`
- `type`
- `opclass`
### Considerations for index names
Index names don't have any significance in the database, so they should
attempt to communicate intent to others. The most important rule to
remember is that generic names are more likely to conflict or be duplicated,
and should not be used. Some other points to consider:
- For general indexes, use a template, like: `index_{table}_{column}_{options}`.
- For indexes added to solve a very specific problem, it may make sense
for the name to reflect their use.
- Identifiers in PostgreSQL have a maximum length of 63 bytes.
- Check `db/structure.sql` for conflicts and ideas.
### Why explicit names are required
As Rails is database agnostic, it generates an index name only
from the required options of all indexes: table name and column name(s).
For example, imagine the following two indexes are created in a migration:
```ruby
def up
add_index :my_table, :my_column
add_index :my_table, :my_column, where: 'my_column IS NOT NULL'
end
```
Creation of the second index would fail, because Rails would generate
the same name for both indexes.
This is further complicated by the behavior of the `index_exists?` method.
It considers only the table name, column name(s) and uniqueness specification
of the index when making a comparison. Consider:
```ruby
def up
unless index_exists?(:my_table, :my_column, where: 'my_column IS NOT NULL')
add_index :my_table, :my_column, where: 'my_column IS NOT NULL'
end
end
```
The call to `index_exists?` will return true if **any** index exists on
`:my_table` and `:my_column`, and index creation will be bypassed.
The `add_concurrent_index` helper is a requirement for creating indexes
on populated tables. Since it cannot be used inside a transactional
migration, it has a built-in check that detects if the index already
exists. In the event a match is found, index creation is skipped.
Without an explicit name argument, Rails can return a false positive
for `index_exists?`, causing a required index to not be created
properly. By always requiring a name for certain types of indexes, the
chance of error is greatly reduced.

View File

@ -465,13 +465,17 @@ class MyMigration < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
remove_concurrent_index :table_name, :column_name
remove_concurrent_index :table_name, :column_name, name: :index_name
end
end
```
Note that it is not necessary to check if the index exists prior to
removing it.
removing it, however it is required to specify the name of the
index that is being removed. This can be done either by passing the name
as an option to the appropriate form of `remove_index` or `remove_concurrent_index`,
or more simply by using the `remove_concurrent_index_by_name` method. Explicitly
specifying the name is important to ensure the correct index is removed.
For a small table (such as an empty one or one with less than `1,000` records),
it is recommended to use `remove_index` in a single-transaction migration,
@ -512,11 +516,16 @@ class MyMigration < ActiveRecord::Migration[6.0]
end
def down
remove_concurrent_index :table, :column
remove_concurrent_index :table, :column, name: index_name
end
end
```
You must explicitly name indexes that are created with more complex
definitions beyond table name, column name(s) and uniqueness constraint.
Consult the [Adding Database Indexes](adding_database_indexes.md#requirements-for-naming-indexes)
guide for more details.
If you need to add a unique index, please keep in mind there is the possibility
of existing duplicates being present in the database. This means that should
always _first_ add a migration that removes any duplicates, before adding the
@ -526,6 +535,42 @@ For a small table (such as an empty one or one with less than `1,000` records),
it is recommended to use `add_index` in a single-transaction migration, combining it with other
operations that don't require `disable_ddl_transaction!`.
## Testing for existence of indexes
If a migration requires conditional logic based on the absence or
presence of an index, you must test for existence of that index using
its name. This helps avoids problems with how Rails compares index definitions,
which can lead to unexpected results. For more details, review the
[Adding Database Indexes](adding_database_indexes.md#why-explicit-names-are-required)
guide.
The easiest way to test for existence of an index by name is to use the
`index_name_exists?` method, but the `index_exists?` method can also
be used with a name option. For example:
```ruby
class MyMigration < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_name'
def up
# an index must be conditionally created due to schema inconsistency
unless index_exists?(:table_name, :column_name, name: INDEX_NAME)
add_index :table_name, :column_name, name: INDEX_NAME
end
end
def down
# no op
end
end
```
Keep in mind that concurrent index helpers like `add_concurrent_index`,
`remove_concurrent_index`, and `remove_concurrent_index_by_name` already
perform existence checks internally.
## Adding foreign-key constraints
When adding a foreign-key constraint to either an existing or a new column also

View File

@ -12,18 +12,6 @@ your self-managed GitLab instance.
## Overview
NOTE: **Note:**
These instructions work for users on GitLab.com, but if you are an
administrator of a self-managed GitLab instance or if you are importing from GitHub Enterprise,
you must enable [GitHub integration](../../../integration/github.md). GitHub integration is the only method for
importing from GitHub Enterprise. If you are using GitLab.com, you can alternatively import
GitHub repositories using a [personal access token](#using-a-github-token),
but this method is not recommended because it cannot associate all user activity
(such as issues and pull requests) with matching GitLab users.
If you are an administrator of a self-managed GitLab instance, you can also use the
[GitHub Rake task](../../../administration/raketasks/github_import.md) to import projects from
GitHub without the constraints of a Sidekiq worker.
The following aspects of a project are imported:
- Repository description (GitLab.com & 7.7+)
@ -43,6 +31,30 @@ each imported repository maintains visibility level unless that [visibility
level is restricted](../../../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects),
in which case it defaults to the default project visibility.
The namespace is a user or group in GitLab, such as `gitlab.com/janedoe` or `gitlab.com/customer-success`. You can do some bulk actions to move projects to different namespaces in the rails console.
This process does not migrate or import any types of groups or organizations from GitHub to GitLab.
### If you're using GitLab.com
If you're using GitLab.com, you can alternatively import
GitHub repositories using a [personal access token](#using-a-github-token),
but we don't recommend this method because it can't associate all user activity
(such as issues and pull requests) with matching GitLab users.
### If you're importing from GitLab Enterprise
If you're importing from GitHub Enterprise, you must enable [GitHub integration][gh-import].
### If you're using a self-managed GitLab instance
If you're an administrator of a self-managed GitLab instance, you must enable
[GitHub integration][gh-import].
If you're an administrator of a self-managed GitLab instance, you can also use the
[GitHub Rake task](../../../administration/raketasks/github_import.md) to import projects from
GitHub without the constraints of a Sidekiq worker.
## How it works
When issues and pull requests are being imported, the importer attempts to find their GitHub authors and

View File

@ -35,7 +35,7 @@ other internal references (refs) that are automatically created by GitLab. These
- `refs/merge-requests/*` for merge requests.
- `refs/pipelines/*` for
[pipelines](../../../ci/pipelines/index.md#troubleshooting-fatal-reference-is-not-a-tree).
[pipelines](../../../ci/troubleshooting.md#fatal-reference-is-not-a-tree-error).
- `refs/environments/*` for environments.
Git doesn't usually download these refs to make cloning and fetch faster, but we can use the `--mirror` option to

View File

@ -18102,6 +18102,9 @@ msgstr ""
msgid "Page not found"
msgstr ""
msgid "Page settings"
msgstr ""
msgid "Page was successfully deleted"
msgstr ""
@ -24336,15 +24339,15 @@ msgstr ""
msgid "Submit %{humanized_resource_name}"
msgstr ""
msgid "Submit Changes"
msgstr ""
msgid "Submit a review"
msgstr ""
msgid "Submit as spam"
msgstr ""
msgid "Submit changes"
msgstr ""
msgid "Submit feedback"
msgstr ""

View File

@ -43,7 +43,7 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.164.0",
"@gitlab/ui": "21.3.0",
"@gitlab/ui": "21.3.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.22.3",

View File

@ -8,6 +8,7 @@ module QA
include QA::Page::Settings::Common
include Component::Select2
include SubMenus::Project
include Component::Breadcrumbs
view 'app/views/projects/edit.html.haml' do
element :advanced_settings

View File

@ -6,11 +6,13 @@ import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/consta
import EditArea from '~/static_site_editor/components/edit_area.vue';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import EditHeader from '~/static_site_editor/components/edit_header.vue';
import EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved_changes_confirm_dialog.vue';
import {
sourceContentTitle as title,
sourceContentYAML as content,
sourceContentHeaderObjYAML as headerSettings,
sourceContentBody as body,
returnUrl,
} from '../mock_data';
@ -36,6 +38,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
};
const findEditHeader = () => wrapper.find(EditHeader);
const findEditDrawer = () => wrapper.find(EditDrawer);
const findRichContentEditor = () => wrapper.find(RichContentEditor);
const findPublishToolbar = () => wrapper.find(PublishToolbar);
const findUnsavedChangesConfirmDialog = () => wrapper.find(UnsavedChangesConfirmDialog);
@ -46,6 +49,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders edit header', () => {
@ -53,6 +57,10 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
expect(findEditHeader().props('title')).toBe(title);
});
it('renders edit drawer', () => {
expect(findEditDrawer().exists()).toBe(true);
});
it('renders rich content editor with a format pass', () => {
expect(findRichContentEditor().exists()).toBe(true);
expect(findRichContentEditor().props('content')).toBe(formattedBody);
@ -148,11 +156,88 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
});
});
describe('when content has front matter', () => {
it('renders a closed edit drawer', () => {
expect(findEditDrawer().exists()).toBe(true);
expect(findEditDrawer().props('isOpen')).toBe(false);
});
it('opens the edit drawer', () => {
findPublishToolbar().vm.$emit('editSettings');
return wrapper.vm.$nextTick().then(() => {
expect(findEditDrawer().props('isOpen')).toBe(true);
});
});
it('closes the edit drawer', () => {
findEditDrawer().vm.$emit('close');
return wrapper.vm.$nextTick().then(() => {
expect(findEditDrawer().props('isOpen')).toBe(false);
});
});
it('forwards the matter settings when the drawer is open', () => {
findPublishToolbar().vm.$emit('editSettings');
jest.spyOn(wrapper.vm.parsedSource, 'matter').mockReturnValueOnce(headerSettings);
return wrapper.vm.$nextTick().then(() => {
expect(findEditDrawer().props('settings')).toEqual(headerSettings);
});
});
it('enables toolbar submit button', () => {
expect(findPublishToolbar().props('hasSettings')).toBe(true);
});
it('syncs matter changes regardless of edit mode', () => {
const newSettings = { title: 'test' };
const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'syncMatter');
findEditDrawer().vm.$emit('updateSettings', newSettings);
expect(spySyncParsedSource).toHaveBeenCalledWith(newSettings);
});
it('syncs matter changes to content in markdown mode', () => {
wrapper.setData({ editorMode: EDITOR_TYPES.markdown });
const newSettings = { title: 'test' };
findEditDrawer().vm.$emit('updateSettings', newSettings);
return wrapper.vm.$nextTick().then(() => {
expect(findRichContentEditor().props('content')).toContain('title: test');
});
});
});
describe('when content lacks front matter', () => {
beforeEach(() => {
buildWrapper({ content: body });
});
afterEach(() => {
wrapper.destroy();
});
it('does not render edit drawer', () => {
expect(findEditDrawer().exists()).toBe(false);
});
it('does not enable toolbar submit button', () => {
expect(findPublishToolbar().props('hasSettings')).toBe(false);
});
});
describe('when content is submitted', () => {
it('should format the content', () => {
findPublishToolbar().vm.$emit('submit', content);
expect(wrapper.emitted('submit')[0][0].content).toBe(`${content} format-pass format-pass`);
expect(wrapper.emitted('submit').length).toBe(1);
});
});
});

View File

@ -0,0 +1,68 @@
import { shallowMount } from '@vue/test-utils';
import { GlDrawer } from '@gitlab/ui';
import EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
import FrontMatterControls from '~/static_site_editor/components/front_matter_controls.vue';
describe('~/static_site_editor/components/edit_drawer.vue', () => {
let wrapper;
const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(EditDrawer, {
propsData: {
isOpen: false,
settings: { title: 'Some title' },
...propsData,
},
});
};
const findFrontMatterControls = () => wrapper.find(FrontMatterControls);
const findGlDrawer = () => wrapper.find(GlDrawer);
beforeEach(() => {
buildWrapper();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders the GlDrawer', () => {
expect(findGlDrawer().exists()).toBe(true);
});
it('renders the FrontMatterControls', () => {
expect(findFrontMatterControls().exists()).toBe(true);
});
it('forwards the settings to FrontMatterControls', () => {
expect(findFrontMatterControls().props('settings')).toBe(wrapper.props('settings'));
});
it('is closed by default', () => {
expect(findGlDrawer().props('open')).toBe(false);
});
it('can open', () => {
buildWrapper({ isOpen: true });
expect(findGlDrawer().props('open')).toBe(true);
});
it.each`
event | payload | finderFn
${'close'} | ${undefined} | ${findGlDrawer}
${'updateSettings'} | ${{ some: 'data' }} | ${findFrontMatterControls}
`(
'forwards the emitted $event event from the $finderFn with $payload',
({ event, payload, finderFn }) => {
finderFn().vm.$emit(event, payload);
expect(wrapper.emitted(event)[0][0]).toBe(payload);
expect(wrapper.emitted(event).length).toBe(1);
},
);
});

View File

@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
@ -11,6 +10,7 @@ describe('Static Site Editor Toolbar', () => {
const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(PublishToolbar, {
propsData: {
hasSettings: false,
saveable: false,
...propsData,
},
@ -18,7 +18,8 @@ describe('Static Site Editor Toolbar', () => {
};
const findReturnUrlLink = () => wrapper.find({ ref: 'returnUrlLink' });
const findSaveChangesButton = () => wrapper.find(GlButton);
const findSaveChangesButton = () => wrapper.find({ ref: 'submit' });
const findEditSettingsButton = () => wrapper.find({ ref: 'settings' });
beforeEach(() => {
buildWrapper();
@ -28,6 +29,10 @@ describe('Static Site Editor Toolbar', () => {
wrapper.destroy();
});
it('does not render Settings button', () => {
expect(findEditSettingsButton().exists()).toBe(false);
});
it('renders Submit Changes button', () => {
expect(findSaveChangesButton().exists()).toBe(true);
});
@ -51,6 +56,14 @@ describe('Static Site Editor Toolbar', () => {
expect(findReturnUrlLink().attributes('href')).toBe(returnUrl);
});
describe('when providing settings CTA', () => {
it('enables Submit Changes button', () => {
buildWrapper({ hasSettings: true });
expect(findEditSettingsButton().exists()).toBe(true);
});
});
describe('when saveable', () => {
it('enables Submit Changes button', () => {
buildWrapper({ saveable: true });

View File

@ -16,16 +16,12 @@ describe('static_site_editor/services/parse_source_file', () => {
describe('unmodified front matter', () => {
it.each`
parsedSource | targetFrontMatter
${parseSourceFile(content)} | ${yamlFrontMatter}
${parseSourceFile(contentComplex)} | ${yamlFrontMatter}
`(
'returns $targetFrontMatter when frontMatter queried',
({ parsedSource, targetFrontMatter }) => {
expect(targetFrontMatter).toContain(parsedSource.matter());
expect(parsedSource.matterObject()).toEqual(yamlFrontMatterObj);
},
);
parsedSource
${parseSourceFile(content)}
${parseSourceFile(contentComplex)}
`('returns $targetFrontMatter when frontMatter queried', ({ parsedSource }) => {
expect(parsedSource.matter()).toEqual(yamlFrontMatterObj);
});
});
describe('unmodified content', () => {
@ -69,12 +65,11 @@ describe('static_site_editor/services/parse_source_file', () => {
`(
'returns the correct front matter and modified content',
({ parsedSource, targetContent }) => {
expect(yamlFrontMatter).toContain(parsedSource.matter());
expect(parsedSource.matter()).toMatchObject(yamlFrontMatterObj);
parsedSource.syncMatter(newYamlFrontMatter);
parsedSource.syncMatter(newYamlFrontMatterObj);
expect(parsedSource.matter()).toBe(newYamlFrontMatter);
expect(parsedSource.matterObject()).toEqual(newYamlFrontMatterObj);
expect(parsedSource.matter()).toMatchObject(newYamlFrontMatterObj);
expect(parsedSource.content()).toBe(targetContent);
},
);
@ -85,16 +80,19 @@ describe('static_site_editor/services/parse_source_file', () => {
const newComplexBody = `${complexBody} ${edit}`;
it.each`
parsedSource | isModified | targetRaw | targetBody
${parseSourceFile(content)} | ${false} | ${content} | ${body}
${parseSourceFile(content)} | ${true} | ${newContent} | ${newBody}
${parseSourceFile(contentComplex)} | ${false} | ${contentComplex} | ${complexBody}
${parseSourceFile(contentComplex)} | ${true} | ${newContentComplex} | ${newComplexBody}
parsedSource | hasMatter | isModified | targetRaw | targetBody
${parseSourceFile(content)} | ${true} | ${false} | ${content} | ${body}
${parseSourceFile(content)} | ${true} | ${true} | ${newContent} | ${newBody}
${parseSourceFile(contentComplex)} | ${true} | ${false} | ${contentComplex} | ${complexBody}
${parseSourceFile(contentComplex)} | ${true} | ${true} | ${newContentComplex} | ${newComplexBody}
${parseSourceFile(body)} | ${false} | ${false} | ${body} | ${body}
${parseSourceFile(body)} | ${false} | ${true} | ${newBody} | ${newBody}
`(
'returns $isModified after a $targetRaw sync',
({ parsedSource, isModified, targetRaw, targetBody }) => {
({ parsedSource, hasMatter, isModified, targetRaw, targetBody }) => {
parsedSource.syncContent(targetRaw);
expect(parsedSource.hasMatter()).toBe(hasMatter);
expect(parsedSource.isModified()).toBe(isModified);
expect(parsedSource.content()).toBe(targetRaw);
expect(parsedSource.content(true)).toBe(targetBody);

View File

@ -848,10 +848,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.164.0.tgz#6cefad871c45f945ef92b99015d0f510b1d2de4a"
integrity sha512-a9e/cYUc1QQk7azjH4x/m6/p3icavwGEi5F9ipNlDqiJtUor5tqojxvMxPOhuVbN/mTwnC6lGsSZg4tqTsdJAQ==
"@gitlab/ui@21.3.0":
version "21.3.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.3.0.tgz#1cdcfed11e9aa730af1b1a9102390cab81c36928"
integrity sha512-6k02S4y1wNnxmPpHOe5WA0FIeAfzs2M06Xz1CCYQ+7M7P0PBaG0FFW+uvWyVnnp5CUaL2frMBn9TXD9pXdOeKg==
"@gitlab/ui@21.3.1":
version "21.3.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.3.1.tgz#027b767804540539da73d4874370895d7398adea"
integrity sha512-ynyg8i8W8Ud+GoySr4hAjJoW55kWMwSEFLX5MEX8CbdqGurkTLqHYLLpXPBSSnVEcw4stR+bFbKSc35rmBkWPA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"