Commit 186ab901 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into dz-sec-report-base-head

* master: (54 commits)
  Add Go Back link to WebIDE
  Resolve conflicts
  Docs: refactor doc general guidelines and style guidelines
  Docs: refactor doc general guidelines and style guidelines
  fixed specs & eslint
  Add version available info to integrity check rake task docs
  Extract constant for LfsPointerFile::VERSION_LINE
  dont log any errors with event listeners in tests
  Resolve CE to EE merge conflicts on documentation
  added default editor options
  Bump fog-google to 1.3.3 to fix LFS upload
  removed shadow on modified edior changed selection background color
  improve styling of diff viewer
  fixed karma spec
  fixed scss-lint added pointer cursor to diff overview
  change editor when opening file from sidebar
  fixed SCSS-lint
  added border to overview hide viewport display
  change background of diff overview fix content not resetting when discarded
  remove hacky margin added to hide diff overview added comment re. try/catch in dispose
  ...
parents e7721aa9 e58fce3f
......@@ -77,19 +77,32 @@ stages:
# (as many users are still using 9.2).
- postgres:9.2
- redis:alpine
- docker.elastic.co/elasticsearch/elasticsearch:5.5.2
.use-pg-9-6-no-elasticsearch: &use-pg-9-6-no-elasticsearch
.use-mysql: &use-mysql
services:
- mysql:latest
- redis:alpine
# BEGIN EE-only service helpers
.use-pg-9-6: &use-pg-9-6
services:
- postgres:9.6
- redis:alpine
.use-mysql: &use-mysql
.use-pg-with-elasticsearch: &use-pg-with-elasticsearch
services:
- postgres:9.2
- redis:alpine
- docker.elastic.co/elasticsearch/elasticsearch:5.5.2
.use-mysql-with-elasticsearch: &use-mysql-with-elasticsearch
services:
- mysql:latest
- redis:alpine
- docker.elastic.co/elasticsearch/elasticsearch:5.5.2
# END EE-only service helpers
# Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes.
......@@ -179,15 +192,15 @@ stages:
.rspec-ee-pg: &rspec-ee-pg
<<: *rspec-metadata-ee
<<: *use-pg
<<: *use-pg-with-elasticsearch
.rspec-ee-mysql: &rspec-ee-mysql
<<: *rspec-metadata-ee
<<: *use-mysql
<<: *use-mysql-with-elasticsearch
.rspec-geo-pg-9-6: &rspec-metadata-pg-geo
<<: *rspec-metadata
<<: *use-pg-9-6-no-elasticsearch
<<: *use-pg-9-6
stage: test
script:
- export NO_KNAPSACK=1
......
......@@ -109,7 +109,7 @@ gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
gem 'fog-aws', '~> 2.0'
gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 1.3.2'
gem 'fog-google', '~> 1.3.3'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
......@@ -371,7 +371,7 @@ group :development, :test do
gem 'benchmark-ips', '~> 2.3.0', require: false
gem 'license_finder', '~> 3.1', require: false
gem 'knapsack', '~> 1.11.0'
gem 'knapsack', '~> 1.16'
gem 'activerecord_sane_schema_dumper', '0.2'
......@@ -435,9 +435,9 @@ gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 1.0.0', require: false
# Feature toggles
gem 'flipper', '~> 0.11.0'
gem 'flipper-active_record', '~> 0.11.0'
gem 'flipper-active_support_cache_store', '~> 0.11.0'
gem 'flipper', '~> 0.13.0'
gem 'flipper-active_record', '~> 0.13.0'
gem 'flipper-active_support_cache_store', '~> 0.13.0'
# Structured logging
gem 'lograge', '~> 0.5'
......
......@@ -242,13 +242,13 @@ GEM
path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
flipper (0.11.0)
flipper-active_record (0.11.0)
flipper (0.13.0)
flipper-active_record (0.13.0)
activerecord (>= 3.2, < 6)
flipper (~> 0.11.0)
flipper-active_support_cache_store (0.11.0)
flipper (~> 0.13.0)
flipper-active_support_cache_store (0.13.0)
activesupport (>= 3.2, < 6)
flipper (~> 0.11.0)
flipper (~> 0.13.0)
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
......@@ -266,7 +266,7 @@ GEM
builder
excon (~> 0.58)
formatador (~> 0.2)
fog-google (1.3.2)
fog-google (1.3.3)
fog-core
fog-json
fog-xml
......@@ -479,9 +479,8 @@ GEM
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
kgio (2.10.0)
knapsack (1.11.0)
knapsack (1.16.0)
rake
timecop (>= 0.1.0)
kubeclient (2.2.0)
http (= 0.9.8)
recursive-open-struct (= 1.0.0)
......@@ -1075,13 +1074,13 @@ DEPENDENCIES
fast_blank
ffaker (~> 2.4)
flay (~> 2.10.0)
flipper (~> 0.11.0)
flipper-active_record (~> 0.11.0)
flipper-active_support_cache_store (~> 0.11.0)
flipper (~> 0.13.0)
flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0)
fog-aliyun (~> 0.2.0)
fog-aws (~> 2.0)
fog-core (~> 1.44)
fog-google (~> 1.3.2)
fog-google (~> 1.3.3)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1)
......@@ -1126,7 +1125,7 @@ DEPENDENCIES
json-schema (~> 2.8.0)
jwt (~> 1.5.6)
kaminari (~> 1.0)
knapsack (~> 1.11.0)
knapsack (~> 1.16)
kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 3.1)
......
......@@ -73,6 +73,7 @@ export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('peek');
const paddingTop = 16;
this.diffsLoaded = false;
......@@ -86,6 +87,10 @@ export default class MergeRequestTabs {
this.showTab = this.showTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (peek) {
this.stickyTop += peek.offsetHeight;
}
if (mergeRequestTabs) {
this.stickyTop += mergeRequestTabs.offsetHeight;
}
......
......@@ -132,13 +132,35 @@
.multi-file-tabs {
display: flex;
overflow-x: auto;
background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark;
> li {
> ul {
display: flex;
overflow-x: auto;
}
li {
position: relative;
}
.dropdown {
display: flex;
margin-left: auto;
margin-bottom: 1px;
padding: 0 $grid-size;
border-left: 1px solid $white-dark;
background-color: $white-light;
&.shadow {
box-shadow: 0 0 10px $dropdown-shadow-color;
}
.btn {
margin-top: auto;
margin-bottom: auto;
}
}
}
.multi-file-tab {
......@@ -207,6 +229,70 @@
.vertical-center {
min-height: auto;
}
.monaco-editor .lines-content .cigr {
display: none;
}
.monaco-diff-editor.vs {
.editor.modified {
box-shadow: none;
}
.diagonal-fill {
display: none !important;
}
.diffOverview {
background-color: $white-light;
border-left: 1px solid $white-dark;
cursor: ns-resize;
}
.diffViewport {
display: none;
}
.char-insert {
background-color: $line-added-dark;
}
.char-delete {
background-color: $line-removed-dark;
}
.line-numbers {
color: $black-transparent;
}
.view-overlays {
.line-insert {
background-color: $line-added;
}
.line-delete {
background-color: $line-removed;
}
}
.margin {
background-color: $gray-light;
border-right: 1px solid $white-normal;
.line-insert {
border-right: 1px solid $line-added-dark;
}
.line-delete {
border-right: 1px solid $line-removed-dark;
}
}
.margin-view-overlays .insert-sign,
.margin-view-overlays .delete-sign {
opacity: .4;
}
}
}
.multi-file-editor-holder {
......@@ -596,11 +682,6 @@
padding-bottom: 0;
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $context-header-height});
min-height: calc(100vh - #{$header-height + $context-header-height});
}
&.flash-shown {
.content-wrapper {
margin-top: 0;
......@@ -609,10 +690,11 @@
.ide-view {
height: calc(100vh - #{$header-height + $flash-height});
}
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height});
min-height: calc(100vh - #{$header-height + $flash-height + $context-header-height});
.projects-sidebar {
.multi-file-commit-panel-inner-scroll {
flex: 1;
}
}
}
......@@ -632,11 +714,6 @@
height: calc(100vh - #{$header-height + $performance-bar-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
min-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
}
&.flash-shown {
.content-wrapper {
margin-top: 0;
......@@ -645,11 +722,6 @@
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
min-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
}
}
}
......@@ -684,3 +756,29 @@
.ide-commit-new-branch {
margin-left: 25px;
}
.ide-external-links {
p {
margin: 0;
}
}
.ide-sidebar-link {
padding: $gl-padding-8 $gl-padding;
background: $indigo-700;
color: $white-light;
text-decoration: none;
display: flex;
align-items: center;
&:focus,
&:hover {
color: $white-light;
text-decoration: underline;
background: $indigo-500;
}
&:active {
background: $indigo-800;
}
}
......@@ -348,15 +348,15 @@ class ApplicationSetting < ActiveRecord::Base
end
def home_page_url_column_exists?
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
::Gitlab::Database.cached_column_exists?(:application_settings, :home_page_url)
end
def help_page_support_url_column_exists?
ActiveRecord::Base.connection.column_exists?(:application_settings, :help_page_support_url)
::Gitlab::Database.cached_column_exists?(:application_settings, :help_page_support_url)
end
def sidekiq_throttling_column_exists?
ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled)
::Gitlab::Database.cached_column_exists?(:application_settings, :sidekiq_throttling_enabled)
end
def domain_whitelist_raw
......
......@@ -161,11 +161,6 @@ class JiraService < IssueTrackerService
add_comment(data, jira_issue)
end
# reason why service cannot be tested
def disabled_title
"Please fill in Password and Username."
end
def test(_)
result = test_settings
success = result.present?
......
......@@ -39,10 +39,6 @@ class PipelinesEmailService < Service
project.pipelines.any?
end
def disabled_title
'Please setup a pipeline on your repository.'
end
def test_data(project, user)
data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last)
data[:user] = user.hook_attrs
......
......@@ -163,11 +163,6 @@ class Service < ActiveRecord::Base
true
end
# reason why service cannot be tested
def disabled_title
"Please setup a project repository."
end
# Provide convenient accessor methods
# for each serialized property.
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
......
module Files
class CreateService < Files::BaseService
def create_commit!
handler = Lfs::FileModificationHandler.new(project, @branch_name)
transformer = Lfs::FileTransformer.new(project, @branch_name)
handler.new_file(@file_path, @file_content) do |content_or_lfs_pointer|
create_transformed_commit(content_or_lfs_pointer)
end
result = transformer.new_file(@file_path, @file_content)
create_transformed_commit(result.content)
end
private
......
......@@ -3,11 +3,33 @@ module Files
UPDATE_FILE_ACTIONS = %w(update move delete).freeze
def create_commit!
transformer = Lfs::FileTransformer.new(project, @branch_name)
actions = actions_after_lfs_transformation(transformer, params[:actions])
commit_actions!(actions)
end
private
def actions_after_lfs_transformation(transformer, actions)
actions.map do |action|
if action[:action] == 'create'
result = transformer.new_file(action[:file_path], action[:content], encoding: action[:encoding])
action[:content] = result.content
action[:encoding] = result.encoding
end
action
end
end
def commit_actions!(actions)
repository.multi_action(
current_user,
message: @commit_message,
branch_name: @branch_name,
actions: params[:actions],
actions: actions,
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
......@@ -17,8 +39,6 @@ module Files
raise_error(e)
end
private
def validate!
super
......
module Lfs
class FileModificationHandler
# Usage: Calling `new_file` check to see if a file should be in LFS and
# return a transformed result with `content` and `encoding` to commit.
#
# For LFS an LfsObject linked to the project is stored and an LFS
# pointer returned. If the file isn't in LFS the untransformed content
# is returned to save in the commit.
#
# transformer = Lfs::FileTransformer.new(project, @branch_name)
# content_or_lfs_pointer = transformer.new_file(file_path, content).content
# create_transformed_commit(content_or_lfs_pointer)
#
class FileTransformer
attr_reader :project, :branch_name
delegate :repository, to: :project
......@@ -9,24 +20,37 @@ module Lfs
@branch_name = branch_name
end
def new_file(file_path, file_content)
def new_file(file_path, file_content, encoding: nil)
if project.lfs_enabled? && lfs_file?(file_path)
file_content = Base64.decode64(file_content) if encoding == 'base64'
lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content)
lfs_object = create_lfs_object!(lfs_pointer_file, file_content)
content = lfs_pointer_file.pointer
success = yield(content)
link_lfs_object!(lfs_object)
link_lfs_object!(lfs_object) if success
Result.new(content: lfs_pointer_file.pointer, encoding: 'text')
else
yield(file_content)
Result.new(content: file_content, encoding: encoding)
end
end
class Result
attr_reader :content, :encoding
def initialize(content:, encoding:)
@content = content
@encoding = encoding
end
end
private
def lfs_file?(file_path)
repository.attributes_at(branch_name, file_path)['filter'] == 'lfs'
cached_attributes.attributes(file_path)['filter'] == 'lfs'
end
def cached_attributes
@cached_attributes ||= Gitlab::Git::AttributesAtRefParser.new(repository, branch_name)
end
def create_lfs_object!(lfs_pointer_file, file_content)
......
......@@ -15,11 +15,6 @@
.footer-block.row-content-block
= service_save_button(@service)
&nbsp;
- if @service.valid? && @service.activated?
- unless @service.can_test?
- disabled_class = 'disabled'
- disabled_title = @service.disabled_title
= link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel'
- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
......
---
title: Update knapsack to 1.16.0
merge_request: 17735
author: Takuya Noguchi
type: other
---
title: Create commit API and Web IDE obey LFS filters
merge_request: 16718
author:
type: fixed
---
title: Cache column_exists? for application settings
merge_request:
author:
type: performance
---
title: Cache table_exists?('application_settings') to reduce repeated schema reloads
merge_request:
author:
type: performance
......@@ -29,7 +29,7 @@ in your testing/production environment.
GitLab stores a number of secret values in the `/etc/gitlab/gitlab-secrets.json`
file which *must* match between the primary and secondary nodes. Until there is
a means of automatically replicating these between nodes (see issue [gitlab-org/gitlab-ee#3789]),
a means of automatically replicating these between nodes (see issue [gitlab-org/gitlab-ee#3789]),
they must be manually replicated to the secondary.
1. SSH into the **primary** node, and execute the command below:
......@@ -127,7 +127,11 @@ keys must be manually replicated to the secondary node.
1. Restart sshd:
```bash
service ssh restart
# Debian or Ubuntu installations
sudo service ssh reload
# CentOS installations
sudo service sshd reload
```
### Step 3. Add the secondary GitLab node
......@@ -145,13 +149,13 @@ keys must be manually replicated to the secondary node.
```
gitlab-ctl restart
```
Check if there are any common issue with your Geo setup by running:
```
gitlab-rake gitlab:geo:check
```
1. SSH into your GitLab **primary** server and login as root to verify the
secondary is reachable or there are any common issue with your Geo setup:
......@@ -164,13 +168,13 @@ replicating missing data from the primary in a process known as **backfill**.
Meanwhile, the primary node will start to notify the secondary of any changes, so
that the secondary can act on those notifications immediately.
Make sure the secondary instance is running and accessible.
Make sure the secondary instance is running and accessible.
You can login to the secondary node with the same credentials as used in the primary.
### Step 4. (Optional) Enabling hashed storage (from GitLab 10.0)
CAUTION: **Warning**:
Hashed storage is in **Beta**. It is not considered production-ready. See
Hashed storage is in **Beta**. It is not considered production-ready. See
[Hashed Storage] for more detail, and for the latest updates, check
infrastructure issue [gitlab-com/infrastructure#2821].
......
......@@ -84,9 +84,9 @@ checks using those checksums can be run. These checks also detect missing files.
Currently, integrity checks are supported for the following types of file:
* CI artifacts
* LFS objects
* User uploads
* CI artifacts (Available from version 10.7.0)
* LFS objects (Available from version 10.6.0)
* User uploads (Available from version 10.6.0)
**Omnibus Installation**
......
......@@ -16,18 +16,26 @@ codequality:
- docker:dind
script:
- docker pull codeclimate/codeclimate
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 init
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json || true
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run
--env SOURCE_CODE="$PWD" \
--volume "$PWD":/code \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts:
paths: [codeclimate.json]
```
This will create a `codequality` job in your CI pipeline and will allow you to
download and analyze the report artifact in JSON format.
The above example will create a `codequality` job in your CI/CD pipeline which
will scan your source code for code quality issues. The report will be saved
as an artifact that you can later download and analyze.
For [GitLab Starter][ee] users, this information can be automatically
extracted and shown right in the merge request widget. [Learn more on code quality
diffs in merge requests](../../user/project/merge_requests/code_quality_diff.md).
TIP: **Tip:**
Starting with [GitLab Starter][ee] 9.3, this information will
be automatically extracted and shown right in the merge request widget. To do
so, the CI/CD job must be named `codequality` and the artifact path must be
`codeclimate.json`.
[Learn more on code quality diffs in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html).
[cli]: https://github.com/codeclimate/codeclimate
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
......
# Documentation styleguide
# Documentation style guidelines
This styleguide recommends best practices to improve documentation and to keep
it organized and easy to find.
The documentation style guide defines the markup structure used in
GitLab documentation. Check the
[documentation guidelines](writing_documentation.md) for general development instructions.
See also [writing documentation](writing_documentation.md).
## Location and naming of documents
>**Note:**
These guidelines derive from the discussion taken place in issue [#3349][ce-3349].
The documentation hierarchy can be vastly improved by providing a better layout
and organization of directories.
Having a structured document layout, we will be able to have meaningful URLs
like `docs.gitlab.com/user/project/merge_requests.html`. With this pattern,
you can immediately tell that you are navigating a user related documentation
and is about the project and its merge requests.
Do not create summaries of similar types of content (e.g. an index of all articles, videos, etc.),
rather organise content by its subject (e.g. everything related to CI goes together)
and cross-link between any related content.
The table below shows what kind of documentation goes where.
| Directory | What belongs here |
| --------- | -------------- |
| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
| `doc/api/` | API related documentation. |
| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. |
| `doc/legal/` | Legal documents about contributing to GitLab. |
| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
---
**General rules:**
1. The correct naming and location of a new document, is a combination
of the relative URL of the document in question and the GitLab Map design
that is used for UX purposes ([source][graffle], [image][gitlab-map]).
1. When creating a new document and it has more than one word in its name,
make sure to use underscores instead of spaces or dashes (`-`). For example,
a proper naming would be `import_projects_from_github.md`. The same rule
applies to images.
1. There are four main directories, `user`, `administration`, `api` and `development`.
1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`,
`profile/`, `dashboard/` and `admin_area/`.
1. `doc/user/project/` should contain all project related documentation.
1. `doc/user/group/` should contain all group related documentation.
1. `doc/user/profile/` should contain all profile related documentation.
Every page you would navigate under `/profile` should have its own document,
i.e. `account.md`, `applications.md`, `emails.md`, etc.
1. `doc/user/dashboard/` should contain all dashboard related documentation.
1. `doc/user/admin_area/` should contain all admin related documentation
describing what can be achieved by accessing GitLab's admin interface
(_not to be confused with `doc/administration` where server access is
required_).
1. Every category under `/admin/application_settings` should have its
own document located at `doc/user/admin_area/settings/`. For example,
the **Visibility and Access Controls** category should have a document
located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
1. The `doc/topics/` directory holds topic-related technical content. Create
`doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
General user- and admin- related documentation, should be placed accordingly.
---
If you are unsure where a document should live, you can ping `@axil` or `@marcia` in your
merge request.
Check the GitLab hanbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines).
## Text
- Split up long lines, this makes it much easier to review and edit. Only
- Split up long lines (wrap text), this makes it much easier to review and edit. Only
double line breaks are shown as a full line break in [GitLab markdown][gfm].
80-100 characters is a good line length
- Make sure that the documentation is added in the correct directory and that
- Make sure that the documentation is added in the correct
[directory](writing_documentation.md#documentation-directory-structure) and that
there's a link to it somewhere useful
- Do not duplicate information
- Be brief and clear
- Unless there's a logical reason not to, add documents in alphabetical order
- Write in US English
- Use [single spaces][] instead of double spaces
- Jump a line between different markups (e.g., after every paragraph, hearder, list, etc)
- Capitalize "G" and "L" in GitLab
- Capitalize feature, products, and methods names. E.g.: GitLab Runner, Geo,
Issue Boards, Git, Prometheus, Continuous Integration.
## Formatting
......@@ -103,6 +42,8 @@ merge request.
links shift too, which eventually leads to dead links. If you think it is
compelling to add numbers in headings, make sure to at least discuss it with
someone in the Merge Request
- [Avoid using symbols and special chars](https://gitlab.com/gitlab-com/gitlab-docs/issues/84)
in headers. Whenever possible, they should be plain and short text.
- Avoid adding things that show ephemeral statuses. For example, if a feature is
considered beta or experimental, put this info in a note, not in the heading.
- When introducing a new document, be careful for the headings to be
......@@ -121,71 +62,18 @@ merge request.
you can use `[Text][identifier]` and at the bottom of the section or the
document add: `[identifier]: https://example.com`, in which case, we do
encourage you to also add an alternative text: `[identifier]: https://example.com "Alternative text"` that appears when hovering your mouse on a link.
### Linking to inline docs
Sometimes it's needed to link to the built-in documentation that GitLab provides
under `/help`. This is normally done in files inside the `app/views/` directory
with the help of the `help_page_path` helper method.
In its simplest form, the HAML code to generate a link to the `/help` page is:
```haml
= link_to 'Help page', help_page_path('user/permissions')
```
The `help_page_path` contains the path to the document you want to link to with
the following conventions:
- it is relative to the `doc/` directory in the GitLab repository
- the `.md` extension must be omitted
- it must not end with a slash (`/`)
Below are some special cases where should be used depending on the context.
You can combine one or more of the following:
1. **Linking to an anchor link.** Use `anchor` as part of the `help_page_path`
method:
```haml
= link_to 'Help page', help_page_path('user/permissions', anchor: 'anchor-link')
```
1. **Opening links in a new tab.** This should be the default behavior:
```haml
= link_to 'Help page', help_page_path('user/permissions'), target: '_blank'
```
1. **Linking to a circle icon.** Usually used in settings where a long
description cannot be used, like near checkboxes. You can basically use
any font awesome icon, but prefer the `question-circle`:
```haml
= link_to icon('question-circle'), help_page_path('user/permissions')
```
1. **Using a button link.** Useful in places where text would be out of context
with the rest of the page layout:
```haml
= link_to 'Help page', help_page_path('user/permissions'), class: 'btn btn-info'
```
1. **Using links inline of some text.**
```haml
Description to #{link_to 'Help page', help_page_path('user/permissions')}.
```
1. **Adding a period at the end of the sentence.** Useful when you don't want
the period to be part of the link:
```haml
= succeed '.' do
Learn more in the
= link_to 'Help page', help_page_path('user/permissions')
```
- To link to internal documentation, use relative links, not full URLs. Use `../` to
navigate tp high-level directories, and always add the file name `file.md` at the
end of the link with the `.md` extension, not `.html`.
Example: instead of `[text](../../merge_requests/)`, use
`[text](../../merge_requests/index.md)` or, `[text](../../ci/README.md)`, or,
for anchor links, `[text](../../ci/README.md#examples)`.
Using the markdown extension is necessary for the [`/help`](writing_documentation.md#gitlab-help)
section of GitLab.
- To link from CE to EE-only documentation, use the EE-only doc full URL.
- Use [meaningful anchor texts](https://www.futurehosting.com/blog/links-should-have-meaningful-anchor-text-heres-why/).
E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
write `Read more about [GitLab Issue Boards](LINK)`.
## Images
......@@ -222,7 +110,7 @@ Inside the document:
- Notes should be quoted with the word `Note:` being bold. Use this form:
```
```md
>**Note:**
This is something to note.
```
......@@ -234,25 +122,25 @@ Inside the document:
If the note spans across multiple lines it's OK to split the line.
## New features
## Specific sections and terms
New features must be shipped with its accompanying documentation and the doc
reviewed by a technical writer.
To mention and/or reference specific terms in GitLab, please follow the styles
below.
### Mentioning GitLab versions and tiers
### GitLab versions and tiers
- Every piece of documentation that comes with a new feature should declare the
GitLab version that feature got introduced. Right below the heading add a
note:
```
```md
> Introduced in GitLab 8.3.
```
- If possible every feature should have a link to the MR, issue, or epic that introduced it.
The above note would be then transformed to:
```
```md
> [Introduced][ce-1242] in GitLab 8.3.
```
......@@ -263,121 +151,41 @@ reviewed by a technical writer.
the [paid tier](https://about.gitlab.com/handbook/marketing/product-marketing/#tiers)
the feature is available in:
```
```md
> [Introduced][ee-1234] in [GitLab Starter](https://about.gitlab.com/products/) 8.3.
```
Otherwise, leave this mention out.
## References
- **GitLab Restart:**
There are many cases that a restart/reconfigure of GitLab is required. To
avoid duplication, link to the special document that can be found in
[`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will
read like:
```
Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
for the changes to take effect.
```
If the document you are editing resides in a place other than the GitLab CE/EE
`doc/` directory, instead of the relative link, use the full path:
`http://docs.gitlab.com/ce/administration/restart_gitlab.html`.
Replace `reconfigure` with `restart` where appropriate.
## Installation guide
- **Ruby:**
In [step 2 of the installation guide](../install/installation.md#2-ruby),
we install Ruby from source. Whenever there is a new version that needs to
be updated, remember to change it throughout the codeblock and also replace
the sha256sum (it can be found in the [downloads page][ruby-dl] of the Ruby
website).
### GitLab Restart
[ruby-dl]: https://www.ruby-lang.org/en/downloads/ "Ruby download website"
## Changing document location
Changing a document's location is not to be taken lightly. Remember that the
documentation is available to all installations under `help/` and not only to
GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the
Documentation team beforehand.
If you indeed need to change a document's location, do NOT remove the old
document, but rather replace all of its contents with a new line:
```
This document was moved to [another location](path/to/new_doc.md).
```
There are many cases that a restart/reconfigure of GitLab is required. To
avoid duplication, link to the special document that can be found in
[`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will
read like:
where `path/to/new_doc.md` is the relative path to the root directory `doc/`.
```
Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
for the changes to take effect.
```
---
If the document you are editing resides in a place other than the GitLab CE/EE
`doc/` directory, instead of the relative link, use the full path:
`http://docs.gitlab.com/ce/administration/restart_gitlab.html`.
Replace `reconfigure` with `restart` where appropriate.
For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
`doc/administration/lfs.md`, then the steps would be:
### Installation guide
1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md`
1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
**Ruby:**
In [step 2 of the installation guide](../install/installation.md#2-ruby),
we install Ruby from source. Whenever there is a new version that needs to
be updated, remember to change it throughout the codeblock and also replace
the sha256sum (it can be found in the [downloads page][ruby-dl] of the Ruby
website).
```
This document was moved to [another location](../../administration/lfs.md).
```
1. Find and replace any occurrences of the old location with the new one.
A quick way to find them is to use `git grep`. First go to the root directory
where you cloned the `gitlab-ce` repository and then do:
```
git grep -n "workflow/lfs/lfs_administration"
git grep -n "lfs/lfs_administration"
```
NOTE: **Note:**
If the document being moved has any Disqus comments on it, there are extra steps
to follow documented just [below](#redirections-for-pages-with-disqus-comments).
Things to note:
- Since we also use inline documentation, except for the documentation itself,
the document might also be referenced in the views of GitLab (`app/`) which will
render when visiting `/help`, and sometimes in the testing suite (`spec/`).
- The above `git grep` command will search recursively in the directory you run
it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration`
and will print the file and the line where this file is mentioned.
You may ask why the two greps. Since we use relative paths to link to
documentation, sometimes it might be useful to search a path deeper.
- The `*.md` extension is not used when a document is linked to GitLab's
built-in help page, that's why we omit it in `git grep`.
- Use the checklist on the documentation MR description template.
### Redirections for pages with Disqus comments
If the documentation page being relocated already has any Disqus comments,
we need to preserve the Disqus thread.
Disqus uses an identifier per page, and for docs.gitlab.com, the page identifier
is configured to be the page URL. Therefore, when we change the document location,
we need to preserve the old URL as the same Disqus identifier.
To do that, add to the frontmatter the variable `redirect_from`,
using the old URL as value. For example, let's say I moved the document
available under `https://docs.gitlab.com/my-old-location/README.html` to a new location,
`https://docs.gitlab.com/my-new-location/index.html`.
Into the **new document** frontmatter add the following:
```yaml
---
redirect_from: 'https://docs.gitlab.com/my-old-location/README.html'
---
```
Note: it is necessary to include the file name in the `redirect_from` URL,
even if it's `index.html` or `README.html`.
[ruby-dl]: https://www.ruby-lang.org/en/downloads/ "Ruby download website"
## Configuration documentation for source and Omnibus installations
### Configuration documentation for source and Omnibus installations
GitLab currently officially supports two installation methods: installations
from source and Omnibus packages installations.
......@@ -394,7 +202,7 @@ When there is a list of steps to perform, usually that entails editing the
configuration file and reconfiguring/restarting GitLab. In such case, follow
the style below as a guide:
````
```md
**For Omnibus installations**
1. Edit `/etc/gitlab/gitlab.rb`:
......@@ -421,7 +229,7 @@ the style below as a guide:
[reconfigure]: path/to/administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: path/to/administration/restart_gitlab.md#installations-from-source
````
```
In this case:
......@@ -433,7 +241,7 @@ In this case:
- different highlighting languages are used for each config in the code block
- the [references](#references) guide is used for reconfigure/restart
## Fake tokens
### Fake tokens
There may be times where a token is needed to demonstrate an API call using
cURL or a secret variable used in CI. It is strongly advised not to use real
......@@ -456,7 +264,7 @@ You can use the following fake tokens as examples.
| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
| Request profile token | `7VgpS4Ax5utVD2esNstz` |
## API
### API
Here is a list of must-have items. Use them in the exact order that appears
on this document. Further explanation is given below.
......@@ -472,10 +280,10 @@ on this document. Further explanation is given below.
- Every method must have a cURL example.
- Every method must have a response body (in JSON format).
### Method description
#### Method description
Use the following table headers to describe the methods. Attributes should
always be in code blocks using backticks (`).
always be in code blocks using backticks (``` ` ```).
```
| Attribute | Type | Required | Description |
......@@ -488,7 +296,7 @@ Rendered example:
| --------- | ---- | -------- | ----------- |
| `user` | string | yes | The GitLab username |
### cURL commands
#### cURL commands
- Use `https://gitlab.example.com/api/v4/` as an endpoint.
- Wherever needed use this personal access token: `9koXpg98eAheJpvBs5tK`.
......@@ -505,11 +313,11 @@ Rendered example:
| `-X PUT` | Use this method when updating existing objects |
| `-X DELETE` | Use this method when removing existing objects |
### cURL Examples
#### cURL Examples
Below is a set of [cURL][] examples that you can use in the API documentation.
#### Simple cURL command
##### Simple cURL command
Get the details of a group:
......@@ -517,7 +325,7 @@ Get the details of a group:
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/gitlab-org
```
#### cURL example with parameters passed in the URL
##### cURL example with parameters passed in the URL
Create a new project under the authenticated user's namespace:
......@@ -525,7 +333,7 @@ Create a new project under the authenticated user's namespace:
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects?name=foo"
```
#### Post data using cURL's --data
##### Post data using cURL's --data
Instead of using `-X POST` and appending the parameters to the URI, you can use
cURL's `--data` option. The example below will create a new project `foo` under
......@@ -535,7 +343,7 @@ the authenticated user's namespace.
curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
```
#### Post data using JSON content
##### Post data using JSON content
> **Note:** In this example we create a new group. Watch carefully the single
and double quotes.
......@@ -544,7 +352,7 @@ and double quotes.
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v4/groups
```
#### Post data using form-data
##### Post data using form-data
Instead of using JSON or urlencode you can use multipart/form-data which
properly handles data encoding:
......@@ -556,7 +364,7 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title
The above example is run by and administrator and will add an SSH public key
titled ssh-key to user's account which has an id of 25.
#### Escape special characters
##### Escape special characters
Spaces or slashes (`/`) may sometimes result to errors, thus it is recommended
to escape them when possible. In the example below we create a new issue which
......@@ -569,7 +377,7 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitl
Use `%2F` for slashes (`/`).
#### Pass arrays to API calls
##### Pass arrays to API calls
The GitLab API sometimes accepts arrays of strings or integers. For example, to
restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and
......@@ -584,6 +392,3 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain
[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation"
[ce-1242]: https://gitlab.com/gitlab-org/gitlab-ce/issues/1242
[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
[ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure"
[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
# Writing documentation
# GitLab Documentation guidelines
- **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
- **[Technical articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/community-writers/).
- **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
## Documentation style guidelines
All the docs follow the same [styleguide](doc_styleguide.md).
## Contributing to docs
Whenever a feature is changed, updated, introduced, or deprecated, the merge
......@@ -29,23 +25,16 @@ Whenever you submit a merge request for the documentation, use the documentation
Please check the [documentation workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/) before getting started.
### Documentation directory structure
## Documentation structure
The documentation is structured based on the GitLab UI structure itself,
separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user),
[`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development).
- Overview and use cases: what it is, why it is necessary, why one would use it
- Requirements: what do we need to get started
- Tutorial: how to set it up, how to use it
To learn where to place a new document, check the [documentation style guide](doc_styleguide.md#location-and-naming-of-documents).
Always link a new document from its topic-related index, otherwise, it will
not be included it in the documentation site search.
In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation,
all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent.
The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have
been deprecated and the majority their docs have been moved to their correct location
in small iterations. Don't create new docs in these folders.
To move a document from its location to another directory, read the section
[changing document location](doc_styleguide.md#changing-document-location) of the doc style guide.
_Note: to be extended._
### Feature overview and use cases
......@@ -75,16 +64,169 @@ overview there.
> **Overview** and **use cases** are required to **every** Enterprise Edition feature,
and for every **major** feature present in Community Edition.
### Markdown
## Markdown and styles
Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
### Previewing locally
All the docs follow the [documentation style guidelines](doc_styleguide.md).
To preview your changes to documentation locally, please follow
this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
## Documentation directory structure
The documentation is structured based on the GitLab UI structure itself,
separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user),
[`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development).
In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation,
all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent.
The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have
been deprecated and the majority their docs have been moved to their correct location
in small iterations. Please don't create new docs in these folders.
### Testing
### Location and naming documents
The documentation hierarchy can be vastly improved by providing a better layout
and organization of directories.
Having a structured document layout, we will be able to have meaningful URLs
like `docs.gitlab.com/user/project/merge_requests/index.html`. With this pattern,
you can immediately tell that you are navigating a user related documentation
and is about the project and its merge requests.
Do not create summaries of similar types of content (e.g. an index of all articles, videos, etc.),
rather organize content by its subject (e.g. everything related to CI goes together)
and cross-link between any related content.
The table below shows what kind of documentation goes where.
| Directory | What belongs here |
| --------- | -------------- |
| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
| `doc/api/` | API related documentation. |
| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. |
| `doc/legal/` | Legal documents about contributing to GitLab. |
| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
---
**General rules:**
1. The correct naming and location of a new document, is a combination
of the relative URL of the document in question and the GitLab Map design
that is used for UX purposes ([source][graffle], [image][gitlab-map]).
1. When creating a new document and it has more than one word in its name,
make sure to use underscores instead of spaces or dashes (`-`). For example,
a proper naming would be `import_projects_from_github.md`. The same rule
applies to images.
1. Start a new directory with an `index.md` file.
1. There are four main directories, `user`, `administration`, `api` and `development`.
1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`,
`profile/`, `dashboard/` and `admin_area/`.
1. `doc/user/project/` should contain all project related documentation.
1. `doc/user/group/` should contain all group related documentation.
1. `doc/user/profile/` should contain all profile related documentation.
Every page you would navigate under `/profile` should have its own document,
i.e. `account.md`, `applications.md`, `emails.md`, etc.
1. `doc/user/dashboard/` should contain all dashboard related documentation.
1. `doc/user/admin_area/` should contain all admin related documentation
describing what can be achieved by accessing GitLab's admin interface
(_not to be confused with `doc/administration` where server access is
required_).
1. Every category under `/admin/application_settings` should have its
own document located at `doc/user/admin_area/settings/`. For example,
the **Visibility and Access Controls** category should have a document
located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
1. The `doc/topics/` directory holds topic-related technical content. Create
`doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
General user- and admin- related documentation, should be placed accordingly.
If you are unsure where a document should live, you can ping `@axil` or `@marcia` in your
merge request.
### Changing document location
Changing a document's location is not to be taken lightly. Remember that the
documentation is available to all installations under `help/` and not only to
GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the
Documentation team beforehand.
If you indeed need to change a document's location, do NOT remove the old
document, but rather replace all of its contents with a new line:
```
This document was moved to [another location](path/to/new_doc.md).
```
where `path/to/new_doc.md` is the relative path to the root directory `doc/`.
---
For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
`doc/administration/lfs.md`, then the steps would be:
1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md`
1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
```
This document was moved to [another location](../../administration/lfs.md).
```
1. Find and replace any occurrences of the old location with the new one.
A quick way to find them is to use `git grep`. First go to the root directory
where you cloned the `gitlab-ce` repository and then do:
```
git grep -n "workflow/lfs/lfs_administration"
git grep -n "lfs/lfs_administration"
```
NOTE: **Note:**
If the document being moved has any Disqus comments on it, there are extra steps
to follow documented just [below](#redirections-for-pages-with-disqus-comments).
Things to note:
- Since we also use inline documentation, except for the documentation itself,
the document might also be referenced in the views of GitLab (`app/`) which will
render when visiting `/help`, and sometimes in the testing suite (`spec/`).
- The above `git grep` command will search recursively in the directory you run
it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration`
and will print the file and the line where this file is mentioned.
You may ask why the two greps. Since we use relative paths to link to
documentation, sometimes it might be useful to search a path deeper.
- The `*.md` extension is not used when a document is linked to GitLab's
built-in help page, that's why we omit it in `git grep`.
- Use the checklist on the documentation MR description template.
### Redirections for pages with Disqus comments
If the documentation page being relocated already has any Disqus comments,
we need to preserve the Disqus thread.
Disqus uses an identifier per page, and for docs.gitlab.com, the page identifier
is configured to be the page URL. Therefore, when we change the document location,
we need to preserve the old URL as the same Disqus identifier.
To do that, add to the frontmatter the variable `redirect_from`,
using the old URL as value. For example, let's say I moved the document
available under `https://docs.gitlab.com/my-old-location/README.html` to a new location,
`https://docs.gitlab.com/my-new-location/index.html`.
Into the **new document** frontmatter add the following:
```yaml
---
redirect_from: 'https://docs.gitlab.com/my-old-location/README.html'
---
```
Note: it is necessary to include the file name in the `redirect_from` URL,
even if it's `index.html` or `README.html`.
## Testing
We treat documentation as code, thus have implemented some testing.
Currently, the following tests are in place:
......@@ -103,7 +245,7 @@ Currently, the following tests are in place:
Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is
essential to avoid them.
### Branch naming
## Branch naming
If your contribution contains **only** documentation changes, you can speed up
the CI process by following some branch naming conventions. You have three
......@@ -118,7 +260,7 @@ choices:
If your branch name matches any of the above, it will run only the docs
tests. If it doesn't, the whole test suite will run (including docs).
### Merge requests for GitLab documentation
## Merge requests for GitLab documentation
Before getting started, make sure you read the introductory section
"[contributing to docs](#contributing-to-docs)" above and the
......@@ -137,7 +279,7 @@ frozen or released, use the label `Pick into X.Y` to get it merged into
the correct release. Avoid picking into a past release as much as you can, as
it increases the work of the release managers.
#### Cherry-picking from CE to EE
### Cherry-picking from CE to EE
As we have the `master` branch of CE merged into EE once a day, it's common to
run into merge conflicts. To avoid them, we [test for merge conflicts against EE](#testing)
......@@ -161,7 +303,13 @@ a couple more commits to the EE branch, but ask the reviewer to review the EE me
additionally to the CE MR. If there are many EE-only changes though, start a new MR
to EE only.
### Previewing the changes live
## Previewing the changes live
To preview your changes to documentation locally, please follow
this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
To preview your changes to documentation locally, please follow
this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
If you want to preview the doc changes of your merge request live, you can use
the manual `review-docs-deploy` job in your merge request. You will need at
......@@ -221,7 +369,7 @@ working on. If you don't, the remote docs branch won't be removed either,
and the server where the Review Apps are hosted will eventually be out of
disk space.
#### Technical aspects
### Technical aspects
If you want to know the hot details, here's what's really happening:
......@@ -256,7 +404,75 @@ The following GitLab features are used among others:
- [Artifacts](../ci/yaml/README.md#artifacts)
- [Specific Runner](../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
## General documentation vs technical articles
## GitLab `/help`
Every GitLab instance includes the documentation, which is available from `/help`
(`http://my-instance.com/help`), e.g., <https://gitlab.com/help>.
When you're building a new feature, you may need to link the documentation
from GitLab, the application. This is normally done in files inside the
`app/views/` directory with the help of the `help_page_path` helper method.
In its simplest form, the HAML code to generate a link to the `/help` page is:
```haml
= link_to 'Help page', help_page_path('user/permissions')
```
The `help_page_path` contains the path to the document you want to link to with
the following conventions:
- it is relative to the `doc/` directory in the GitLab repository
- the `.md` extension must be omitted
- it must not end with a slash (`/`)
Below are some special cases where should be used depending on the context.
You can combine one or more of the following:
1. **Linking to an anchor link.** Use `anchor` as part of the `help_page_path`
method:
```haml
= link_to 'Help page', help_page_path('user/permissions', anchor: 'anchor-link')
```
1. **Opening links in a new tab.** This should be the default behavior:
```haml
= link_to 'Help page', help_page_path('user/permissions'), target: '_blank'
```
1. **Linking to a circle icon.** Usually used in settings where a long
description cannot be used, like near checkboxes. You can basically use
any font awesome icon, but prefer the `question-circle`:
```haml
= link_to icon('question-circle'), help_page_path('user/permissions')
```
1. **Using a button link.** Useful in places where text would be out of context
with the rest of the page layout:
```haml
= link_to 'Help page', help_page_path('user/permissions'), class: 'btn btn-info'
```
1. **Using links inline of some text.**
```haml
Description to #{link_to 'Help page', help_page_path('user/permissions')}.
```
1. **Adding a period at the end of the sentence.** Useful when you don't want
the period to be part of the link:
```haml
= succeed '.' do
Learn more in the
= link_to 'Help page', help_page_path('user/permissions')
```
## General Documentation vs Technical Articles
### General documentation
......@@ -270,7 +486,7 @@ They are topic-related documentation, written with an user-friendly approach and
A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab.
They should be placed in a new directory named `/article-title/index.md` under a topic-related folder, and their images should be placed in `/article-title/img/`. For example, a new article on GitLab Pages should be placed in `doc/user/project/pages/article-title/` and a new article on GitLab CI/CD should be placed in `doc/ci/article-title/`.
They should be placed in a new directory named `/article-title/index.md` under a topic-related folder, and their images should be placed in `/article-title/img/`. For example, a new article on GitLab Pages should be placed in `doc/user/project/pages/article-title/` and a new article on GitLab CI/CD should be placed in `doc/ci/examples/article-title/`.
#### Types of technical articles
......@@ -324,3 +540,5 @@ date: 2017-02-01
Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team.
[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
......@@ -70,7 +70,7 @@ To downgrade an Omnibus installation, it is sufficient to install the Community
Edition package on top of the currently installed one. You can do this manually,
by directly [downloading the package](https://packages.gitlab.com/gitlab/gitlab-ce)
you need, or by adding our CE package repository and following the
[CE installation instructions](https://about.gitlab.com/downloads/?version=ce).
[CE installation instructions](https://about.gitlab.com/installation/?version=ce).
**Source Installation**
......
......@@ -24,8 +24,11 @@
methods: {
...mapActions([
'discardFileChanges',
'updateViewer',
]),
openFileInEditor(file) {
this.updateViewer('diff');
router.push(`/project${file.url}`);
},
},
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
required: true,
},
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
},
};
</script>
<template>
<div
class="dropdown"
:class="{
shadow: showShadow,
}"
>
<button
type="button"
class="btn btn-primary btn-sm"
:class="{
'btn-inverted': hasChanges,
}"
data-toggle="dropdown"
>
<template v-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
{{ __('Reviewing') }}
</template>
<icon
name="angle-down"
:size="12"
css-classes="caret-down"
/>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
<li>
<a
href="#"
@click.prevent="changeMode('editor')"
:class="{
'is-active': viewer === 'editor',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('View and edit lines') }}
</span>
</a>
</li>
<li>
<a
href="#"
@click.prevent="changeMode('diff')"
:class="{
'is-active': viewer === 'diff',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the last commit') }}
</span>
</a>
</li>
</ul>
</div>
</div>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
projectUrl: {
type: String,
required: true,
},
},
computed: {
goBackUrl() {
return document.referrer || this.projectUrl;
},
},
};
</script>
<template>
<nav
class="ide-external-links"
v-once
>
<p>
<a
:href="goBackUrl"
class="ide-sidebar-link"
>
<icon
:size="16"
class="append-right-8"
name="go-back"
/>
<span class="ide-external-links-text">
{{ s__('Go back') }}
</span>
</a>
</p>
</nav>
</template>
<script>
import projectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import branchesTree from './ide_project_branches_tree.vue';
import externalLinks from './ide_external_links.vue';
export default {
components: {
branchesTree,
externalLinks,
projectAvatarImage,
},
props: {
......@@ -37,6 +39,9 @@ export default {
</div>
</a>
</div>
<external-links
:project-url="project.web_url"
/>
<div class="multi-file-commit-panel-inner-scroll">
<branches-tree
v-for="branch in project.branches"
......
......@@ -15,6 +15,8 @@ export default {
'leftPanelCollapsed',
'rightPanelCollapsed',
'panelResizing',
'viewer',
'delayViewerUpdated',
]),
shouldHideEditor() {
return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
......@@ -37,6 +39,9 @@ export default {
this.editor.updateDimensions();
}
},
viewer() {
this.createEditorInstance();
},
},
beforeDestroy() {
this.editor.dispose();
......@@ -59,6 +64,8 @@ export default {
'setFileLanguage',
'setEditorPosition',
'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
]),
initMonaco() {
if (this.shouldHideEditor) return;
......@@ -67,16 +74,34 @@ export default {
this.getRawFileData(this.activeFile)
.then(() => {
this.editor.createInstance(this.$refs.editor);
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
return viewerPromise;
})
.then(() => {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.then(() => this.setupEditor())
.catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
},
createEditorInstance() {
this.editor.dispose();
this.$nextTick(() => {
if (this.viewer === 'editor') {
this.editor.createInstance(this.$refs.editor);
} else {
this.editor.createDiffInstance(this.$refs.editor);
}
this.setupEditor();
});
},
setupEditor() {
if (!this.activeFile) return;
if (!this.activeFile || !this.editor.instance) return;
this.model = this.editor.createModel(this.activeFile);
......
<script>
import { mapState } from 'vuex';
import { mapActions, mapState } from 'vuex';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
......@@ -70,6 +70,9 @@
}
},
methods: {
...mapActions([
'updateDelayViewerUpdated',
]),
clickFile(row) {
// Manual Action if a tree is selected/opened
if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
......@@ -78,7 +81,13 @@
tree: this.file,
});
}
this.$router.push(`/project${row.url}`);
const delayPromise = this.file.changed ?
Promise.resolve() : this.updateDelayViewerUpdated(true);
return delayPromise.then(() => {
this.$router.push(`/project${row.url}`);
});
},
},
};
......
<script>
import { mapState } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
export default {
components: {
'repo-tab': RepoTab,
RepoTab,
EditorMode,
},
data() {
return {
showShadow: false,
};
},
computed: {
...mapGetters([
'hasChanges',
]),
...mapState([
'openFiles',
'viewer',
]),
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions([
'updateViewer',
]),
},
};
</script>
<template>
<ul
class="multi-file-tabs list-unstyled append-bottom-0"
>
<repo-tab
v-for="tab in openFiles"
:key="tab.key"
:tab="tab"
<div class="multi-file-tabs">
<ul
class="list-unstyled append-bottom-0"
ref="tabsScroller"
>
<repo-tab
v-for="tab in openFiles"
:key="tab.key"
:tab="tab"
/>
</ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
@click="updateViewer"
/>
</ul>
</div>
</template>
......@@ -26,6 +26,9 @@ export default class Model {
this.events = new Map();
this.updateContent = this.updateContent.bind(this);
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
......@@ -75,6 +78,7 @@ export default class Model {
this.disposable.dispose();
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
}
import eventHub from 'ee/ide/eventhub';
import Disposable from './disposable';
import Model from './model';
......@@ -25,9 +26,17 @@ export default class ModelManager {
this.models.set(model.path, model);
this.disposable.add(model);
eventHub.$on(`editor.update.model.dispose.${file.path}`, this.removeCachedModel.bind(this, file));
return model;
}
removeCachedModel(file) {
this.models.delete(file.path);
eventHub.$off(`editor.update.model.dispose.${file.path}`, this.removeCachedModel);
}
dispose() {
// dispose of all the models
this.disposable.dispose();
......
......@@ -27,6 +27,8 @@ export default class DecorationsController {
}
decorate(model) {
if (!this.editor.instance) return;
const decorations = this.getAllDecorationsForModel(model);
const oldDecorations = this.editorDecorations.get(model.url) || [];
......
......@@ -3,9 +3,16 @@ import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions from './editor_options';
import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
import gitlabTheme from 'ee/ide/lib/themes/gl_theme'; // eslint-disable-line import/first
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
while (el.firstChild) {
el.removeChild(el.firstChild);
}
};
export default class Editor {
static create(monaco) {
......@@ -34,19 +41,31 @@ export default class Editor {
createInstance(domElement) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.create(domElement, {
...defaultEditorOptions,
})),
(this.dirtyDiffController = new DirtyDiffController(
this.modelManager,
this.decorationsController,
)),
);
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
createDiffInstance(domElement) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
this.instance = this.monaco.editor.create(domElement, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
}),
this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController,
),
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
readOnly: true,
})),
);
window.addEventListener('resize', this.debouncedUpdate, false);
......@@ -58,25 +77,39 @@ export default class Editor {
}
attachModel(model) {
if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
this.instance.setModel({
original: model.getOriginalModel(),
modified: model.getModel(),
});
return;
}
this.instance.setModel(model.getModel());
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model;
this.instance.updateOptions(editorOptions.reduce((acc, obj) => {
Object.keys(obj).forEach((key) => {
Object.assign(acc, {
[key]: obj[key](model),
this.instance.updateOptions(
editorOptions.reduce((acc, obj) => {
Object.keys(obj).forEach(key => {
Object.assign(acc, {
[key]: obj[key](model),
});
});
});
return acc;
}, {}));
return acc;
}, {}),
);
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
setupMonacoTheme() {
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.defineTheme(
gitlabTheme.themeName,
gitlabTheme.monacoTheme,
);
this.monaco.editor.setTheme('gitlab');
}
......@@ -88,12 +121,21 @@ export default class Editor {
}
dispose() {
this.disposable.dispose();
window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance
if (this.instance) {
// catch any potential errors with disposing the error
// this is mainly for tests caused by elements not existing
try {
this.disposable.dispose();
this.instance = null;
} catch (e) {
this.instance = null;
if (process.env.NODE_ENV !== 'test') {
// eslint-disable-next-line no-console
console.error(e);
}
}
}
......@@ -113,6 +155,8 @@ export default class Editor {
}
onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
this.disposable.add(
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
);
......
export default [{
readOnly: model => !!model.file.file_lock,
}];
export const defaultEditorOptions = {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
};
export default [
{
readOnly: model => !!model.file.file_lock,
},
];
......@@ -6,6 +6,9 @@ export default {
rules: [],
colors: {
'editorLineNumber.foreground': '#CCCCCC',
'diffEditor.insertedTextBackground': '#ddfbe6',
'diffEditor.removedTextBackground': '#f9d7dc',
'editor.selectionBackground': '#aad6f8',
},
},
};
......@@ -84,6 +84,14 @@ export const scrollToTab = () => {
});
};
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
......
import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import eventHub from 'ee/ide/eventhub';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
......@@ -27,6 +28,8 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
}
dispatch('getLastCommitData');
eventHub.$emit(`editor.update.model.dispose.${file.path}`);
};
export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
......@@ -150,4 +153,6 @@ export const discardFileChanges = ({ commit }, file) => {
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, file);
}
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
};
......@@ -15,3 +15,5 @@ export const canEditFile = (state) => {
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const hasChanges = state => !!state.changedFiles.length;
......@@ -46,3 +46,6 @@ export const SET_EDIT_MODE = 'SET_EDIT_MODE';
export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
......@@ -57,6 +57,16 @@ export default {
lastCommitMsg,
});
},
[types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, {
viewer,
});
},
[types.UPDATE_DELAY_VIEWER_CHANGE](state, delayViewerUpdated) {
Object.assign(state, {
delayViewerUpdated,
});
},
...projectMutations,
...fileMutations,
...treeMutations,
......
......@@ -20,4 +20,6 @@ export default () => ({
leftPanelCollapsed: false,
rightPanelCollapsed: false,
panelResizing: false,
viewer: 'editor',
delayViewerUpdated: false,
});
---
title: Add a Go back button to WebIDE to allow returning to where it was launched
from
merge_request:
author:
type: added
require 'spec_helper'
describe 'Project elastic search', :js, :elastic do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
project.add_master(user)
sign_in(user)
end
describe 'searching' do
it 'finds issues' do
create(:issue, project: project, title: 'Test searching for an issue')
expect_search_result(scope: 'Issues', term: 'Test', result: 'Test searching for an issue')
end
it 'finds merge requests' do
create(:merge_request, source_project: project, target_project: project, title: 'Test searching for an MR')
expect_search_result(scope: 'Merge requests', term: 'Test', result: 'Test searching for an MR')
end
it 'finds milestones' do
create(:milestone, project: project, title: 'Test searching for a milestone')
expect_search_result(scope: 'Milestones', term: 'Test', result: 'Test searching for a milestone')
end
it 'finds wiki pages' do
project.wiki.create_page('test.md', 'Test searching for a wiki page')
expect_search_result(scope: 'Wiki', term: 'Test', result: 'Test searching for a wiki page')
end
it 'finds notes' do
create(:note, project: project, note: 'Test searching for a note')
search(scope: 'Comments', term: 'Test')
expect(page).to have_content(/showing (\d+) - (\d+) of (\d+) notes/i)
expect(page).to have_content('Test searching for a note')
end
it 'finds commits' do
project.repository.index_commits
search(scope: 'Commits', term: 'initial')
expect(page).to have_content(/showing (\d+) - (\d+) of (\d+) commits/i)
expect(page).to have_content('Initial commit')
end
it 'finds blobs' do
project.repository.index_blobs
search(scope: 'Code', term: 'def')
expect(page).to have_content(/showing (\d+) - (\d+) of (\d+) blobs/i)
expect(page).to have_content('def username_regex')
end
end
def search(scope:, term:)
visit project_path(project)
fill_in('search', with: term)
find('#search').native.send_keys(:enter)
page.within '.search-filter' do
click_link scope
end
end
def expect_search_result(scope:, term:, result:)
search(scope: scope, term: term)
expect(page).to have_content(/showing (\d+) - (\d+) of (\d+) #{Regexp.escape(scope)}/i)
expect(page).to have_content(result)
end
end
require 'spec_helper'
describe 'Snippet elastic search', :js, :elastic do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
project.add_master(user)
sign_in(user)
end
describe 'searching' do
it 'finds a personal snippet' do
create(:personal_snippet, author: user, content: 'Test searching for personal snippets')
visit explore_snippets_path
fill_in 'search', with: 'Test'
click_button 'Go'
expect(page).to have_content('Test searching for personal snippets')
end
it 'finds a project snippet' do
create(:project_snippet, project: project, content: 'Test searching for personal snippets')
visit explore_snippets_path
fill_in 'search', with: 'Test'
click_button 'Go'
expect(page).to have_content('Test searching for personal snippets')
end
end
end
require 'spec_helper'
describe SearchHelper do
describe '#parse_search_result_from_elastic' do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
it "returns parsed result" do
project = create :project, :repository
project.repository.index_blobs
Gitlab::Elastic::Helper.refresh_index
result = project.repository.search(
'def popen',
type: :blob,
options: { highlight: true }
)[:blobs][:results][0]
parsed_result = helper.parse_search_result(result)
expect(parsed_result.ref). to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0')
expect(parsed_result.filename).to eq('files/ruby/popen.rb')
expect(parsed_result.startline).to eq(2)
expect(parsed_result.data).to include("Popen")
end
end
end
require 'spec_helper'
describe Issue, elastic: true do
describe Issue, :elastic do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
let(:project) { create :project }
......
require 'spec_helper'
describe MergeRequest, elastic: true do
describe MergeRequest, :elastic do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
it "searches merge requests" do
......
require 'spec_helper'
describe Milestone, elastic: true do
describe Milestone, :elastic do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
it "searches milestones" do
......
require 'spec_helper'
describe Note, elastic: true do
describe Note, :elastic do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
it "searches notes" do
......
require 'spec_helper'
describe Project, elastic: true do
describe Project, :elastic do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
it "finds projects" do
......
require 'spec_helper'
describe ProjectWiki, elastic: true do
describe ProjectWiki, :elastic do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
it "searches wiki page" do
......
require 'spec_helper'
describe Repository, elastic: true do
describe Repository, :elastic do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
def index!(project)
......
require 'spec_helper'
describe Snippet, elastic: true do
describe Snippet, :elastic do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
context 'searching snippets by code' do
......
......@@ -1312,4 +1312,48 @@ describe Project do
expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
end
end
describe 'project import state transitions' do
context 'state transition: [:started] => [:finished]' do
context 'elasticsearch indexing disabled' do
before do
stub_ee_application_setting(elasticsearch_indexing: false)
end
it 'does not index the repository' do
project = create(:project, :import_started, import_type: :github)
expect(ElasticCommitIndexerWorker).not_to receive(:perform_async)
project.import_finish
end
end
context 'elasticsearch indexing enabled' do
let(:project) { create(:project, :import_started, import_type: :github) }
before do
stub_ee_application_setting(elasticsearch_indexing: true)
end
context 'no index status' do
it 'schedules a full index of the repository' do
expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, nil)
project.import_finish
end
end
context 'with index status' do
let!(:index_status) { project.create_index_status!(indexed_at: Time.now, last_commit: 'foo') }
it 'schedules a progressive index of the repository' do
expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, index_status.last_commit)
project.import_finish
end
end
end
end
end
end
......@@ -26,4 +26,40 @@ describe Repository do
repository.after_sync
end
end
describe "Elastic search", :elastic do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
describe "class method find_commits_by_message_with_elastic" do
it "returns commits" do
project = create :project, :repository
project1 = create :project, :repository
project.repository.index_commits
project1.repository.index_commits
Gitlab::Elastic::Helper.refresh_index
expect(described_class.find_commits_by_message_with_elastic('initial').first).to be_a(Commit)
expect(described_class.find_commits_by_message_with_elastic('initial').count).to eq(2)
expect(described_class.find_commits_by_message_with_elastic('initial').total_count).to eq(2)
end
end
describe "find_commits_by_message_with_elastic" do
it "returns commits" do
project = create :project, :repository
project.repository.index_commits
Gitlab::Elastic::Helper.refresh_index
expect(project.repository.find_commits_by_message_with_elastic('initial').first).to be_a(Commit)
expect(project.repository.find_commits_by_message_with_elastic('initial').count).to eq(1)
expect(project.repository.find_commits_by_message_with_elastic('initial').total_count).to eq(1)
end
end
end
end
require 'spec_helper'
describe API::Search do
set(:user) { create(:user) }
set(:group) { create(:group) }
let(:project) { create(:project, :public, name: 'awesome project', group: group) }
let(:repo_project) { create(:project, :public, :repository, group: group) }
shared_examples 'response is correct' do |schema:, size: 1|
it { expect(response).to have_gitlab_http_status(200) }
it { expect(response).to match_response_schema(schema) }
it { expect(response).to include_limited_pagination_headers }
it { expect(json_response.size).to eq(size) }
end
shared_examples 'elasticsearch disabled' do
it 'returns 400 error for wiki_blobs scope' do
get api(endpoint, user), scope: 'wiki_blobs', search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 error for blobs scope' do
get api(endpoint, user), scope: 'blobs', search: 'monitors'
expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 error for commits scope' do
get api(endpoint, user), scope: 'commits', search: 'folder'
expect(response).to have_gitlab_http_status(400)
end
end
shared_examples 'elasticsearch enabled' do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
context 'for wiki_blobs scope' do
before do
wiki = create(:project_wiki, project: project)
create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: "Awesome page" })
project.wiki.index_blobs
Gitlab::Elastic::Helper.refresh_index
get api(endpoint, user), scope: 'wiki_blobs', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs'
end
context 'for commits scope' do
before do
repo_project.repository.index_commits
Gitlab::Elastic::Helper.refresh_index
get api(endpoint, user), scope: 'commits', search: 'folder'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details', size: 2
end
context 'for blobs scope' do
before do
repo_project.repository.index_blobs
Gitlab::Elastic::Helper.refresh_index
get api(endpoint, user), scope: 'blobs', search: 'monitors'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs'
end
end
describe 'GET /search' do
context 'with correct params' do
context 'when elasticsearch is disabled' do
it_behaves_like 'elasticsearch disabled' do
let(:endpoint) { '/search' }
end
end
context 'when elasticsearch is enabled' do
it_behaves_like 'elasticsearch enabled' do
let(:endpoint) { '/search' }
end
end
end
end
describe "GET /groups/:id/-/search" do
context 'with correct params' do
context 'when elasticsearch is disabled' do
it_behaves_like 'elasticsearch disabled' do
let(:endpoint) { "/groups/#{group.id}/-/search" }
end
end
context 'when elasticsearch is enabled' do
it_behaves_like 'elasticsearch enabled' do
let(:endpoint) { "/groups/#{group.id}/-/search" }
end
end
end
end
end
require 'spec_helper'
describe Search::GroupService do
shared_examples_for 'group search' do
context 'finding projects by name' do
let(:user) { create(:user) }
let(:term) { "Project Name" }
let(:nested_group) { create(:group, :nested) }
# These projects shouldn't be found
let!(:outside_project) { create(:project, :public, name: "Outside #{term}") }
let!(:private_project) { create(:project, :private, namespace: nested_group, name: "Private #{term}" )}
let!(:other_project) { create(:project, :public, namespace: nested_group, name: term.reverse) }
# These projects should be found
let!(:project1) { create(:project, :internal, namespace: nested_group, name: "Inner #{term} 1") }
let!(:project2) { create(:project, :internal, namespace: nested_group, name: "Inner #{term} 2") }
let!(:project3) { create(:project, :internal, namespace: nested_group.parent, name: "Outer #{term}") }
let(:results) { described_class.new(user, search_group, search: term).execute }
subject { results.objects('projects') }
context 'in parent group' do
let(:search_group) { nested_group.parent }
it { is_expected.to match_array([project1, project2, project3]) }
end
context 'in subgroup' do
let(:search_group) { nested_group }
it { is_expected.to match_array([project1, project2]) }
end
end
end
describe 'elasticsearch' do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
# Ensure these are present when the index is refreshed
_ = [
outside_project, private_project, other_project,
project1, project2, project3
]
Gitlab::Elastic::Helper.refresh_index
end
after do
Gitlab::Elastic::Helper.delete_index
end
include_examples 'group search'
end
end
RSpec.configure do |config|
config.before(:each, :elastic) do
Gitlab::Elastic::Helper.create_empty_index
end
config.after(:each, :elastic) do
Gitlab::Elastic::Helper.delete_index
end
end
require 'spec_helper'
describe ElasticIndexerWorker, elastic: true do
describe ElasticIndexerWorker, :elastic do
subject { described_class.new }
before do
......@@ -8,12 +8,6 @@ describe ElasticIndexerWorker, elastic: true do
Elasticsearch::Model.client =
Gitlab::Elastic::Client.build(Gitlab::CurrentSettings.elasticsearch_config)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
end
it 'returns true if ES disabled' do
......
Feature: Global Search
Background:
Given I sign in as a user
And I own project "Shop"
And Elasticsearch is enabled
Scenario: I search through the all projects
Given project has all data available for the search
And I visit dashboard page
Then I search "initial"
And I find an Issue
And I find a Merge Request
And I find a Milestone
\ No newline at end of file
Feature: Project Search
Background:
Given I sign in as a user
And I own project "Shop"
And Elasticsearch is enabled
Scenario: I search through the all project items
Given project has all data available for the search
And I visit my project's home page
Then I search "initial"
And I find an Issue
And I find a Merge Request
And I find a Milestone
And I find a Comment
And I find a Commit
And I find a Wiki Page
Then I visit my project's home page
Then I search "def"
And I find a Code
Feature: Snippets Search
Background:
Given I sign in as a user
And Elasticsearch is enabled
Scenario: I search through the snippets
Given there is a snippet "index" with "php rocks" string
And there is a snippet "php" with "benefits" string
And I visit snippets page
Then I search "php"
And I find "index" snippet
Then I select search by titles and filenames
And I find "php" snippet
\ No newline at end of file
require_dependency Rails.root.join('spec', 'support', 'stub_configuration')
class Spinach::Features::GlobalSearch < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
include SharedElastic
include StubConfiguration
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
step 'project has all data available for the search' do
@project = create :project
@project.add_master(current_user)
@issue = create :issue, title: 'bla-bla initial', project: @project
@merge_request = create :merge_request, title: 'bla-bla initial', source_project: @project
@milestone = create :milestone, title: 'bla-bla initial', project: @project
end
end
class Spinach::Features::ProjectSearch < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
include SharedElastic
include StubConfiguration
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
step 'project has all data available for the search' do
@project = create :project, :repository
@project.add_master(current_user)
@issue = create :issue, title: 'bla-bla initial', project: @project
@merge_request = create :merge_request, title: 'bla-bla initial', source_project: @project
@milestone = create :milestone, title: 'bla-bla initial', project: @project
@note = create :note, note: 'bla-bla initial', project: @project, noteable: @issue
@project.repository.index_blobs
@project.repository.index_commits
@project.wiki.create_page("index_page", "Bla bla initial")
end
step 'I search "def"' do
fill_in "search", with: "def"
click_button "Go"
end
step 'I find a Comment' do
select_filter("Comments")
expect(page.find('.search-result-row')).to have_content(@note.note)
end
step 'I find a Wiki Page' do
select_filter("Wiki")
expect(page.find('.blob-result')).to have_content('Bla bla init')
end
step 'I find a Commit' do
select_filter("Commits")
expect(page.find('.commit-content .item-title')).to have_content("Initial commit")
end
step 'I find a Code' do
expect(page.first('.blob-result')).to have_content("def")
end
end
class Spinach::Features::SnippetsSearch < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
include SharedElastic
include StubConfiguration
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
step 'there is a snippet "index" with "php rocks" string' do
create :personal_snippet, :public, content: "php rocks", title: "index"
Gitlab::Elastic::Helper.refresh_index
end
step 'there is a snippet "php" with "benefits" string' do
create :personal_snippet, :public, content: "benefits", title: "php"
Gitlab::Elastic::Helper.refresh_index
end
step 'I search "php"' do
fill_in "search", with: "php"
click_button "Go"
end
step 'I find "index" snippet' do
expect(page.find('.file-holder')).to have_content("php rocks")
end
step 'I select search by titles and filenames' do
select_filter("Titles and Filenames")
end
step 'I find "php" snippet' do
expect(page.find('.search-result-row')).to have_content("php")
end
end
module SharedElastic
include Spinach::DSL
step 'I search "initial"' do
fill_in "search", with: "initial"
click_button "Go"
end
step 'I find an Issue' do
select_filter("Issues")
expect(page.find('.search-result-row')).to have_content(@issue.title)
end
step 'I find a Merge Request' do
select_filter("Merge requests")
expect(page.find('.search-result-row')).to have_content(@merge_request.title)
end
step 'I find a Milestone' do
select_filter("Milestones")
expect(page.find('.search-result-row')).to have_content(@milestone.title)
end
step 'Elasticsearch is enabled' do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
def select_filter(name)
find(:xpath, "//ul[contains(@class, 'search-filter')]//a[contains(.,'#{name}')]").click
end
end
......@@ -70,7 +70,7 @@ module Gitlab
active_db_connection = ActiveRecord::Base.connection.active? rescue false
active_db_connection &&
ActiveRecord::Base.connection.table_exists?('application_settings')
Gitlab::Database.cached_table_exists?('application_settings')
rescue ActiveRecord::NoDatabaseError
false
end
......
......@@ -203,6 +203,11 @@ module Gitlab
connection.schema_cache.columns_hash(table_name).has_key?(column_name.to_s)
end
def self.cached_table_exists?(table_name)
# Rails 5 uses data_source_exists? instead of table_exists?
connection.schema_cache.table_exists?(table_name)
end
private_class_method :connection
def self.database_version
......
module Gitlab
module Git
class LfsPointerFile
VERSION = "https://git-lfs.github.com/spec/v1".freeze
VERSION_LINE = "version #{VERSION}".freeze
def initialize(data)
@data = data
end
def pointer
@pointer ||= <<~FILE
version https://git-lfs.github.com/spec/v1
#{VERSION_LINE}
oid sha256:#{sha256}
size #{size}
FILE
......@@ -20,6 +23,10 @@ module Gitlab
def sha256
@sha256 ||= Digest::SHA256.hexdigest(@data)
end
def inspect
"#<#{self.class}:#{object_id} @size=#{size}, @sha256=#{sha256.inspect}>"
end
end
end
end
......@@ -1006,8 +1006,9 @@ module Gitlab
# This only checks the root .gitattributes file,
# it does not traverse subfolders to find additional .gitattributes files
#
# This method is around 30 times slower than `attributes`,
# which uses `$GIT_DIR/info/attributes`
# This method is around 30 times slower than `attributes`, which uses
# `$GIT_DIR/info/attributes`. Consider caching AttributesAtRefParser
# and reusing that for multiple calls instead of this method.
def attributes_at(ref, file_path)
parser = AttributesAtRefParser.new(self, ref)
parser.attributes(file_path)
......
......@@ -32,7 +32,7 @@ branch_to_fetch = 'master'
if ls_remote_output.include?(minimal_ce_branch_name)
remote_to_fetch = ce_repo_url
branch_to_fetch = ls_remote_output.split("refs/heads/").last.strip
branch_to_fetch = ls_remote_output.scan(%r{(?<=refs/heads/).+}).sort_by(&:size).first
puts
puts "💪 We found the branch '#{branch_to_fetch}' in the #{ce_repo_url} repository. We will fetch it."
else
......
......@@ -6,39 +6,6 @@ describe SearchHelper do
str
end
describe '#parse_search_result_from_elastic' do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
it "returns parsed result" do
project = create :project, :repository
project.repository.index_blobs
Gitlab::Elastic::Helper.refresh_index
result = project.repository.search(
'def popen',
type: :blob,
options: { highlight: true }
)[:blobs][:results][0]
parsed_result = helper.parse_search_result(result)
expect(parsed_result.ref). to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0')
expect(parsed_result.filename).to eq('files/ruby/popen.rb')
expect(parsed_result.startline).to eq(2)
expect(parsed_result.data).to include("Popen")
end
end
describe 'search_autocomplete_source' do
context "with no current user" do
before do
......
......@@ -36,6 +36,7 @@ describe('Multi-file editor commit sidebar list item', () => {
it('opens a closed file in the editor when clicking the file path', () => {
spyOn(vm, 'openFileInEditor').and.callThrough();
spyOn(vm, 'updateViewer');
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
......@@ -44,6 +45,16 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(router.push).toHaveBeenCalled();
});
it('calls updateViewer with diff when clicking file', () => {
spyOn(vm, 'openFileInEditor').and.callThrough();
spyOn(vm, 'updateViewer');
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
expect(vm.updateViewer).toHaveBeenCalledWith('diff');
});
describe('computed', () => {
describe('iconName', () => {
it('returns modified when not a tempFile', () => {
......
import Vue from 'vue';
import ideExternalLinks from 'ee/ide/components/ide_external_links.vue';
import createComponent from 'spec/helpers/vue_mount_component_helper';
describe('ide external links component', () => {
let vm;
let fakeReferrer;
let Component;
const fakeProjectUrl = '/project/';
beforeEach(() => {
Component = Vue.extend(ideExternalLinks);
});
afterEach(() => {
vm.$destroy();
});
describe('goBackUrl', () => {
it('renders the Go Back link with the referrer when present', () => {
fakeReferrer = '/example/README.md';
spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
vm = createComponent(Component, {
projectUrl: fakeProjectUrl,
}).$mount();
expect(vm.goBackUrl).toEqual(fakeReferrer);
});
it('renders the Go Back link with the project url when referrer is not present', () => {
fakeReferrer = '';
spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
vm = createComponent(Component, {
projectUrl: fakeProjectUrl,
}).$mount();
expect(vm.goBackUrl).toEqual(fakeProjectUrl);
});
});
});
......@@ -61,6 +61,34 @@ describe('RepoEditor', () => {
});
});
describe('createEditorInstance', () => {
it('calls createInstance when viewer is editor', (done) => {
spyOn(vm.editor, 'createInstance');
vm.createEditorInstance();
vm.$nextTick(() => {
expect(vm.editor.createInstance).toHaveBeenCalled();
done();
});
});
it('calls createDiffInstance when viewer is diff', (done) => {
vm.$store.state.viewer = 'diff';
spyOn(vm.editor, 'createDiffInstance');
vm.createEditorInstance();
vm.$nextTick(() => {
expect(vm.editor.createDiffInstance).toHaveBeenCalled();
done();
});
});
});
describe('setupEditor', () => {
it('creates new model', () => {
spyOn(vm.editor, 'createModel').and.callThrough();
......
......@@ -7,15 +7,17 @@ describe('RepoTabs', () => {
const openedFiles = [file('open1'), file('open2')];
let vm;
function createComponent() {
function createComponent(el = null) {
const RepoTabs = Vue.extend(repoTabs);
return new RepoTabs({
store,
}).$mount();
}).$mount(el);
}
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
......@@ -34,4 +36,44 @@ describe('RepoTabs', () => {
done();
});
});
describe('updated', () => {
it('sets showShadow as true when scroll width is larger than width', (done) => {
const el = document.createElement('div');
el.innerHTML = '<div id="test-app"></div>';
document.body.appendChild(el);
const style = document.createElement('style');
style.innerText = `
.multi-file-tabs {
width: 100px;
}
.multi-file-tabs .list-unstyled {
display: flex;
overflow-x: auto;
}
`;
document.head.appendChild(style);
vm = createComponent('#test-app');
openedFiles[0].active = true;
vm.$nextTick()
.then(() => {
expect(vm.showShadow).toBeFalsy();
vm.$store.state.openFiles = openedFiles;
})
.then(vm.$nextTick)
.then(() => {
expect(vm.showShadow).toBeTruthy();
style.remove();
el.remove();
})
.then(done)
.catch(done.fail);
});
});
});
/* global monaco */
import eventHub from 'ee/ide/eventhub';
import monacoLoader from 'ee/ide/monaco_loader';
import ModelManager from 'ee/ide/lib/common/model_manager';
import { file } from '../../helpers';
......@@ -47,6 +48,15 @@ describe('Multi-file editor library model manager', () => {
expect(instance.models.get).toHaveBeenCalled();
});
it('adds eventHub listener', () => {
const f = file();
spyOn(eventHub, '$on').and.callThrough();
instance.addModel(f);
expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${f.path}`, jasmine.anything());
});
});
describe('hasCachedModel', () => {
......@@ -69,6 +79,30 @@ describe('Multi-file editor library model manager', () => {
});
});
describe('removeCachedModel', () => {
let f;
beforeEach(() => {
f = file();
instance.addModel(f);
});
it('clears cached model', () => {
instance.removeCachedModel(f);
expect(instance.models.size).toBe(0);
});
it('removes eventHub listener', () => {
spyOn(eventHub, '$off').and.callThrough();
instance.removeCachedModel(f);
expect(eventHub.$off).toHaveBeenCalledWith(`editor.update.model.dispose.${f.path}`, jasmine.anything());
});
});
describe('dispose', () => {
it('clears cached models', () => {
instance.addModel(file());
......
/* global monaco */
import eventHub from 'ee/ide/eventhub';
import monacoLoader from 'ee/ide/monaco_loader';
import Model from 'ee/ide/lib/common/model';
import { file } from '../../helpers';
......@@ -7,6 +8,8 @@ describe('Multi-file editor library model', () => {
let model;
beforeEach((done) => {
spyOn(eventHub, '$on').and.callThrough();
monacoLoader(['vs/editor/editor.main'], () => {
model = new Model(monaco, file('path'));
......@@ -23,6 +26,10 @@ describe('Multi-file editor library model', () => {
expect(model.model).not.toBeNull();
});
it('adds eventHub listener', () => {
expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything());
});
describe('path', () => {
it('returns file path', () => {
expect(model.path).toBe('path');
......@@ -88,5 +95,13 @@ describe('Multi-file editor library model', () => {
expect(model.events.size).toBe(0);
});
it('removes eventHub listener', () => {
spyOn(eventHub, '$off').and.callThrough();
model.dispose();
expect(eventHub.$off).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything());
});
});
});
......@@ -5,8 +5,16 @@ import { file } from '../helpers';
describe('Multi-file editor library', () => {
let instance;
let el;
let holder;
beforeEach(done => {
el = document.createElement('div');
holder = document.createElement('div');
el.appendChild(holder);
document.body.appendChild(el);
beforeEach((done) => {
monacoLoader(['vs/editor/editor.main'], () => {
instance = editor.create(monaco);
......@@ -16,6 +24,8 @@ describe('Multi-file editor library', () => {
afterEach(() => {
instance.dispose();
el.remove();
});
it('creates instance of editor', () => {
......@@ -27,33 +37,48 @@ describe('Multi-file editor library', () => {
});
describe('createInstance', () => {
let el;
beforeEach(() => {
el = document.createElement('div');
});
it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'create').and.callThrough();
instance.createInstance(el);
instance.createInstance(holder);
expect(instance.monaco.editor.create).toHaveBeenCalled();
});
it('creates dirty diff controller', () => {
instance.createInstance(el);
instance.createInstance(holder);
expect(instance.dirtyDiffController).not.toBeNull();
});
it('creates model manager', () => {
instance.createInstance(el);
instance.createInstance(holder);
expect(instance.modelManager).not.toBeNull();
});
});
describe('createDiffInstance', () => {
it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough();
instance.createDiffInstance(holder);
expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith(
holder,
{
model: null,
contextmenu: true,
minimap: {
enabled: false,
},
readOnly: true,
scrollBeyondLastLine: false,
},
);
});
});
describe('createModel', () => {
it('calls model manager addModel', () => {
spyOn(instance.modelManager, 'addModel');
......@@ -87,12 +112,28 @@ describe('Multi-file editor library', () => {
expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
});
it('sets original & modified when diff editor', () => {
spyOn(instance.instance, 'getEditorType').and.returnValue(
'vs.editor.IDiffEditor',
);
spyOn(instance.instance, 'setModel');
instance.attachModel(model);
expect(instance.instance.setModel).toHaveBeenCalledWith({
original: model.getOriginalModel(),
modified: model.getModel(),
});
});
it('attaches the model to the dirty diff controller', () => {
spyOn(instance.dirtyDiffController, 'attachModel');
instance.attachModel(model);
expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(
model,
);
});
it('re-decorates with the dirty diff controller', () => {
......@@ -100,7 +141,9 @@ describe('Multi-file editor library', () => {
instance.attachModel(model);
expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(
model,
);
});
});
......
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import store from 'ee/ide/stores';
import service from 'ee/ide/services';
import router from 'ee/ide/ide_router';
import eventHub from 'ee/ide/eventhub';
import { file, resetStore } from '../../helpers';
describe('Multi-file store file actions', () => {
......@@ -457,6 +458,8 @@ describe('Multi-file store file actions', () => {
let tmpFile;
beforeEach(() => {
spyOn(eventHub, '$on');
tmpFile = file();
tmpFile.content = 'testing';
......
......@@ -210,4 +210,15 @@ describe('Multi-file store actions', () => {
.catch(done.fail);
});
});
describe('updateViewer', () => {
it('updates viewer state', (done) => {
store.dispatch('updateViewer', 'diff')
.then(() => {
expect(store.state.viewer).toBe('diff');
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -401,6 +401,8 @@ describe('IDE commit module actions', () => {
});
it('redirects to new merge request page', (done) => {
spyOn(eventHub, '$on');
store.state.commit.commitAction = '3';
store.dispatch('commit/commitChanges')
......
......@@ -106,4 +106,12 @@ describe('Multi-file store mutations', () => {
expect(localState.rightPanelCollapsed).toBeFalsy();
});
});
describe('UPDATE_VIEWER', () => {
it('sets viewer state', () => {
mutations.UPDATE_VIEWER(localState, 'diff');
expect(localState.viewer).toBe('diff');
});
});
});
......@@ -298,6 +298,18 @@ describe Gitlab::Database do
end
end
describe '.cached_table_exists?' do
it 'only retrieves data once per table' do
expect(ActiveRecord::Base.connection).to receive(:table_exists?).with(:projects).once.and_call_original
expect(ActiveRecord::Base.connection).to receive(:table_exists?).with(:bogus_table_name).once.and_call_original
2.times do
expect(described_class.cached_table_exists?(:projects)).to be_truthy
expect(described_class.cached_table_exists?(:bogus_table_name)).to be_falsey
end
end
end
describe '#true_value' do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
......
......@@ -1846,46 +1846,6 @@ describe Project do
expect(housekeeping_service).not_to have_received(:execute)
end
context 'elasticsearch indexing disabled' do
before do
stub_ee_application_setting(elasticsearch_indexing: false)
end
it 'does not index the repository' do
project = create(:project, :import_started, import_type: :github)
expect(ElasticCommitIndexerWorker).not_to receive(:perform_async)
project.import_finish
end
end
context 'elasticsearch indexing enabled' do
let(:project) { create(:project, :import_started, import_type: :github) }
before do
stub_ee_application_setting(elasticsearch_indexing: true)
end
context 'no index status' do
it 'schedules a full index of the repository' do
expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, nil)
project.import_finish
end
end
context 'with index status' do
let!(:index_status) { project.create_index_status!(indexed_at: Time.now, last_commit: 'foo') }
it 'schedules a progressive index of the repository' do
expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, index_status.last_commit)
project.import_finish
end
end
end
end
end
......
......@@ -1813,48 +1813,6 @@ describe Repository do
end
end
describe "Elastic search", :elastic do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
describe "class method find_commits_by_message_with_elastic" do
it "returns commits" do
project = create :project, :repository
project1 = create :project, :repository
project.repository.index_commits
project1.repository.index_commits
Gitlab::Elastic::Helper.refresh_index
expect(described_class.find_commits_by_message_with_elastic('initial').first).to be_a(Commit)
expect(described_class.find_commits_by_message_with_elastic('initial').count).to eq(2)
expect(described_class.find_commits_by_message_with_elastic('initial').total_count).to eq(2)
end
end
describe "find_commits_by_message_with_elastic" do
it "returns commits" do
project = create :project, :repository
project.repository.index_commits
Gitlab::Elastic::Helper.refresh_index
expect(project.repository.find_commits_by_message_with_elastic('initial').first).to be_a(Commit)
expect(project.repository.find_commits_by_message_with_elastic('initial').count).to eq(1)
expect(project.repository.find_commits_by_message_with_elastic('initial').total_count).to eq(1)
end
end
end
describe '#after_create' do
it 'flushes the exists cache' do
expect(repository).to receive(:expire_exists_cache)
......
......@@ -3,8 +3,8 @@ require 'spec_helper'
describe API::Search do
set(:user) { create(:user) }
set(:group) { create(:group) }
let(:project) { create(:project, :public, name: 'awesome project', group: group) }
let(:repo_project) { create(:project, :public, :repository, group: group) }
set(:project) { create(:project, :public, name: 'awesome project', group: group) }
set(:repo_project) { create(:project, :public, :repository, group: group) }
shared_examples 'response is correct' do |schema:, size: 1|
it { expect(response).to have_gitlab_http_status(200) }
......@@ -13,74 +13,6 @@ describe API::Search do
it { expect(json_response.size).to eq(size) }
end
shared_examples 'elasticsearch disabled' do
it 'returns 400 error for wiki_blobs scope' do
get api(endpoint, user), scope: 'wiki_blobs', search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 error for blobs scope' do
get api(endpoint, user), scope: 'blobs', search: 'monitors'
expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 error for commits scope' do
get api(endpoint, user), scope: 'commits', search: 'folder'
expect(response).to have_gitlab_http_status(400)
end
end
shared_examples 'elasticsearch enabled' do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
context 'for wiki_blobs scope' do
before do
wiki = create(:project_wiki, project: project)
create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: "Awesome page" })
project.wiki.index_blobs
Gitlab::Elastic::Helper.refresh_index
get api(endpoint, user), scope: 'wiki_blobs', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs'
end
context 'for commits scope' do
before do
repo_project.repository.index_commits
Gitlab::Elastic::Helper.refresh_index
get api(endpoint, user), scope: 'commits', search: 'folder'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details', size: 2
end
context 'for blobs scope' do
before do
repo_project.repository.index_blobs
Gitlab::Elastic::Helper.refresh_index
get api(endpoint, user), scope: 'blobs', search: 'monitors'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs'
end
end
describe 'GET /search' do
context 'when user is not authenticated' do
it 'returns 401 error' do
......@@ -109,8 +41,6 @@ describe API::Search do
context 'with correct params' do
context 'for projects scope' do
before do
project
get api('/search', user), scope: 'projects', search: 'awesome'
end
......@@ -166,18 +96,6 @@ describe API::Search do
it_behaves_like 'response is correct', schema: 'public_api/v4/snippets'
end
context 'when elasticsearch is disabled' do
it_behaves_like 'elasticsearch disabled' do
let(:endpoint) { '/search' }
end
end
context 'when elasticsearch is enabled' do
it_behaves_like 'elasticsearch enabled' do
let(:endpoint) { '/search' }
end
end
end
end
......@@ -227,8 +145,6 @@ describe API::Search do
context 'with correct params' do
context 'for projects scope' do
before do
project
get api("/groups/#{group.id}/-/search", user), scope: 'projects', search: 'awesome'
end
......@@ -276,18 +192,6 @@ describe API::Search do
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
context 'when elasticsearch is disabled' do
it_behaves_like 'elasticsearch disabled' do
let(:endpoint) { "/groups/#{group.id}/-/search" }
end
end
context 'when elasticsearch is enabled' do
it_behaves_like 'elasticsearch enabled' do
let(:endpoint) { "/groups/#{group.id}/-/search" }
end
end
end
end
......
......@@ -43,7 +43,7 @@ describe Files::CreateService do
blob = repository.blob_at('lfs', file_path)
expect(blob.data).not_to start_with('version https://git-lfs.github.com/spec/v1')
expect(blob.data).not_to start_with(Gitlab::Git::LfsPointerFile::VERSION_LINE)
expect(blob.data).to eq(file_content)
end
end
......@@ -58,7 +58,7 @@ describe Files::CreateService do
blob = repository.blob_at('lfs', file_path)
expect(blob.data).to start_with('version https://git-lfs.github.com/spec/v1')
expect(blob.data).to start_with(Gitlab::Git::LfsPointerFile::VERSION_LINE)
end
it "creates an LfsObject with the file's content" do
......
......@@ -4,28 +4,30 @@ describe Files::MultiService do
subject { described_class.new(project, user, commit_params) }
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user) }
let(:branch_name) { project.default_branch }
let(:original_file_path) { 'files/ruby/popen.rb' }
let(:new_file_path) { 'files/ruby/popen.rb' }
let(:file_content) { 'New content' }
let(:action) { 'update' }
let!(:original_commit_id) do
Gitlab::Git::Commit.last_for_path(project.repository, branch_name, original_file_path).sha
end
let(:actions) do
[
{
action: action,
file_path: new_file_path,
previous_path: original_file_path,
content: 'New content',
last_commit_id: original_commit_id
}
]
let(:default_action) do
{
action: action,
file_path: new_file_path,
previous_path: original_file_path,
content: file_content,
last_commit_id: original_commit_id
}
end
let(:actions) { [default_action] }
let(:commit_params) do
{
commit_message: "Update File",
......@@ -110,6 +112,56 @@ describe Files::MultiService do
end
end
context 'when creating a file matching an LFS filter' do
let(:action) { 'create' }
let(:branch_name) { 'lfs' }
let(:new_file_path) { 'test_file.lfs' }
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
end
it 'creates an LFS pointer' do
subject.execute
blob = repository.blob_at('lfs', new_file_path)
expect(blob.data).to start_with(Gitlab::Git::LfsPointerFile::VERSION_LINE)
end
it "creates an LfsObject with the file's content" do
subject.execute
expect(LfsObject.last.file.read).to eq file_content
end
context 'with base64 encoded content' do
let(:raw_file_content) { 'Raw content' }
let(:file_content) { Base64.encode64(raw_file_content) }
let(:actions) { [default_action.merge(encoding: 'base64')] }
it 'creates an LFS pointer' do
subject.execute
blob = repository.blob_at('lfs', new_file_path)
expect(blob.data).to start_with(Gitlab::Git::LfsPointerFile::VERSION_LINE)
end
it "creates an LfsObject with the file's content" do
subject.execute
expect(LfsObject.last.file.read).to eq raw_file_content
end
end
it 'links the LfsObject to the project' do
expect do
subject.execute
end.to change { project.lfs_objects.count }.by(1)
end
end
context 'when file status validation is skipped' do
let(:action) { 'create' }
let(:new_file_path) { 'files/ruby/new_file.rb' }
......
require "spec_helper"
describe Lfs::FileTransformer do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:file_content) { 'Test file content' }
let(:branch_name) { 'lfs' }
let(:file_path) { 'test_file.lfs' }
subject { described_class.new(project, branch_name) }
describe '#new_file' do
context 'with lfs disabled' do
it 'skips gitattributes check' do
expect(repository.raw).not_to receive(:blob_at)
subject.new_file(file_path, file_content)
end
it 'returns untransformed content' do
result = subject.new_file(file_path, file_content)
expect(result.content).to eq(file_content)
end
it 'returns untransformed encoding' do
result = subject.new_file(file_path, file_content, encoding: 'base64')
expect(result.encoding).to eq('base64')
end
end
context 'with lfs enabled' do
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
end
it 'reuses cached gitattributes' do
second_file = 'another_file.lfs'
expect(repository.raw).to receive(:blob_at).with(branch_name, '.gitattributes').once
subject.new_file(file_path, file_content)
subject.new_file(second_file, file_content)
end
it "creates an LfsObject with the file's content" do
subject.new_file(file_path, file_content)
expect(LfsObject.last.file.read).to eq file_content
end
it 'returns an LFS pointer' do
result = subject.new_file(file_path, file_content)
expect(result.content).to start_with(Gitlab::Git::LfsPointerFile::VERSION_LINE)
end
it 'returns LFS pointer encoding as text' do
result = subject.new_file(file_path, file_content, encoding: 'base64')
expect(result.encoding).to eq('text')
end
context "when doesn't use LFS" do
let(:file_path) { 'other.filetype' }
it "doesn't create LFS pointers" do
new_content = subject.new_file(file_path, file_content).content
expect(new_content).not_to start_with(Gitlab::Git::LfsPointerFile::VERSION_LINE)
expect(new_content).to eq(file_content)
end
end
it 'links LfsObjects to project' do
expect do
subject.new_file(file_path, file_content)
end.to change { project.lfs_objects.count }.by(1)
end
context 'when LfsObject already exists' do
let(:lfs_pointer) { Gitlab::Git::LfsPointerFile.new(file_content) }
before do
create(:lfs_object, oid: lfs_pointer.sha256, size: lfs_pointer.size)
end
it 'links LfsObjects to project' do
expect do
subject.new_file(file_path, file_content)
end.to change { project.lfs_objects.count }.by(1)
end
end
end
end
end
......@@ -37,25 +37,4 @@ describe Search::GroupService do
describe 'basic search' do
include_examples 'group search'
end
describe 'elasticsearch' do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
# Ensure these are present when the index is refreshed
_ = [
outside_project, private_project, other_project,
project1, project2, project3
]
Gitlab::Elastic::Helper.refresh_index
end
after do
Gitlab::Elastic::Helper.delete_index
end
include_examples 'group search'
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment