Commit 103c7d1c authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-06-19

# Conflicts:
#	app/models/project.rb

[ci skip]
parents 5151cb82 9092e96a
......@@ -27,25 +27,26 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Helping others](#helping-others)
- [I want to contribute!](#i-want-to-contribute)
- [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc)
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~P1, ~P2, ~P3 , ~P4)](#bug-priority-labels-p1-p2-p3-p4)
- [Severity labels (~S1, ~S2, ~S3 , ~S4)](#bug-severity-labels-s1-s2-s3-s4)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements)
- [Type labels](#type-labels)
- [Subject labels](#subject-labels)
- [Team labels](#team-labels)
- [Milestone labels](#milestone-labels)
- [Bug Priority labels](#bug-priority-labels)
- [Bug Severity labels](#bug-severity-labels)
- [Severity impact guidance](#severity-impact-guidance)
- [Label for community contributors](#label-for-community-contributors)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)
- [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
- [Technical and UX debt](#technical-and-ux-debt)
- [Stewardship](#stewardship)
- [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)
- [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
- [Technical and UX debt](#technical-and-ux-debt)
- [Stewardship](#stewardship)
- [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines)
- [Contribution acceptance criteria](#contribution-acceptance-criteria)
- [Merge request guidelines](#merge-request-guidelines)
- [Contribution acceptance criteria](#contribution-acceptance-criteria)
- [Definition of done](#definition-of-done)
- [Style guides](#style-guides)
- [Code of conduct](#code-of-conduct)
......@@ -146,7 +147,7 @@ labels, you can _always_ add the team and type, and often also the subject.
[milestones-page]: https://gitlab.com/gitlab-org/gitlab-ce/milestones
[labels-page]: https://gitlab.com/gitlab-org/gitlab-ce/labels
### Type labels (~"feature proposal", ~bug, ~customer, etc.)
### Type labels
Type labels are very important. They define what kind of issue this is. Every
issue should have one or more.
......@@ -162,28 +163,41 @@ already reserved for subject labels).
The descriptions on the [labels page][labels-page] explain what falls under each type label.
### Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)
### Subject labels
Subject labels are labels that define what area or feature of GitLab this issue
hits. They are not always necessary, but very convenient.
Examples of subject labels are ~wiki, ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry".
If you are an expert in a particular area, it makes it easier to find issues to
work on. You can also subscribe to those labels to receive an email each time an
issue is labeled with a subject label corresponding to your expertise.
Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry".
Subject labels are always all-lowercase.
### Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)
### Team labels
Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate
people.
The current team labels are ~Distribution, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products", ~"Configuration", and ~"UX".
The current team labels are:
- ~Configuration
- ~"CI/CD"
- ~Discussion
- ~Distribution
- ~Documentation
- ~Geo
- ~Gitaly
- ~Monitoring
- ~Platform
- ~Quality
- ~Release
- ~"Security Products"
- ~UX
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
......@@ -194,7 +208,7 @@ indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for
any issue.
### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
### Milestone labels
Milestone labels help us clearly communicate expectations of the work for the
release. There are three levels of Milestone labels:
......@@ -212,9 +226,9 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
### Bug Priority labels (~P1, ~P2, ~P3, ~P4)
### Bug Priority labels
Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
......@@ -225,7 +239,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
### Bug Severity labels (~S1, ~S2, ~S3, ~S4)
### Bug Severity labels
Severity labels help us clearly communicate the impact of a ~bug on users.
......@@ -241,11 +255,11 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Security Impact | Availability / Performance Impact |
|-------|---------------------------------------------------------------------|--------------------------------------------------------------|
| ~S1 | >50% users impacted (possible company extinction level event) | |
| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
| ~S3 | A few users or a single paid customer impacted | The issue is likely to occur in the near future |
| ~S4 | No paid users/customer impacted, or expected impact within 30 days | The issue _may_ occur but it's not likely |
### Label for community contributors (~"Accepting Merge Requests")
### Label for community contributors
Issues that are beneficial to our users, 'nice to haves', that we currently do
not have the capacity for or want to give the priority to, are labeled as
......@@ -301,14 +315,14 @@ For guidance on UX implementation at GitLab, please refer to our [Design System]
The UX team uses labels to manage their workflow.
The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/engineering/ux) of the handbook.
Once an issue has been worked on and is ready for development, a UXer removes the ~"UX" label and applies the ~"UX ready" label to that issue.
The UX team has a special type label called ~"design artifact". This label indicates that the final output
The UX team has a special type label called ~"design artifact". This label indicates that the final output
for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
needed until the solution has been decided.
~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone.
......
......@@ -173,7 +173,7 @@
</a>
<clipboard-button
:title="__('Copy commit SHA to clipboard')"
:text="mr.shortMergeCommitSha"
:text="mr.mergeCommitSha"
css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
/>
</p>
......
......@@ -26,6 +26,7 @@ export default class MergeRequestStore {
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
this.mergeCommitSha = data.merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
......
......@@ -173,7 +173,10 @@ table {
display: none;
}
.badge {
// Add to .label so that old system notes that are saved to the db
// will still receive the correct styling
.badge,
.label {
padding: 4px 5px;
font-size: 12px;
font-style: normal;
......
......@@ -23,7 +23,8 @@
position: relative;
line-height: 35px;
display: flex;
flex-grow: 1;
flex: 1 1;
min-width: 0;
@include media-breakpoint-up(sm) {
padding-left: 0;
......
......@@ -710,6 +710,8 @@
font-size: 14px;
line-height: 24px;
align-self: center;
overflow: hidden;
text-overflow: ellipsis;
}
.js-issuable-selector-wrap {
......
......@@ -127,13 +127,6 @@
color: $gl-danger;
}
.service-settings {
input[type="radio"],
input[type="checkbox"] {
margin-top: 10px;
}
}
.integration-settings-form {
.card.card-body,
.info-well {
......
module UploadsActions
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
included do
prepend_before_action :set_html_format, only: :show
end
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
......@@ -41,6 +47,13 @@ module UploadsActions
private
# Explicitly set the format.
# Otherwise rails 5 will set it from a file extension.
# See https://github.com/rails/rails/commit/84e8accd6fb83031e4c27e44925d7596655285f7#diff-2b8f2fbb113b55ca8e16001c393da8f1
def set_html_format
request.format = :html
end
def uploader_class
raise NotImplementedError
end
......
......@@ -31,7 +31,10 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
render
# https://gitlab.com/gitlab-org/gitlab-ce/issues/48097
Gitlab::GitalyClient.allow_n_plus_1_calls do
render
end
end
format.json do
branches = BranchesFinder.new(@repository, params).execute
......
......@@ -159,7 +159,7 @@ module IssuablesHelper
output = ""
output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline-block", tooltip: true)
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none")
end
......
......@@ -26,9 +26,12 @@ class Project < ActiveRecord::Base
include WithUploads
include BatchDestroyDependentAssociations
extend Gitlab::Cache::RequestCache
<<<<<<< HEAD
# EE specific modules
prepend EE::Project
=======
>>>>>>> upstream/master
extend Gitlab::ConfigHelper
......
......@@ -155,6 +155,7 @@ class ChatNotificationService < Service
end
def notify_for_ref?(data)
return true if data[:object_kind] == 'tag_push'
return true if data.dig(:object_attributes, :tag)
return true unless notify_only_default_branch?
......
......@@ -8,7 +8,7 @@
%colgroup
%col
%col
%col.d-none.d-sm-block
%col
%col{ width: "120" }
%thead
%tr
......
......@@ -8,28 +8,12 @@ class RepositoryForkWorker
target_project_id = args.shift
target_project = Project.find(target_project_id)
# By v10.8, we should've drained the queue of all jobs using the old arguments.
# We can remove the else clause if we're no longer logging the message in that clause.
# See https://gitlab.com/gitlab-org/gitaly/issues/1110
if args.empty?
source_project = target_project.forked_from_project
unless source_project
return target_project.mark_import_as_failed('Source project cannot be found.')
end
fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
else
Rails.logger.info("Project #{target_project.id} is being forked using old-style arguments.")
source_repository_storage_path, source_disk_path = *args
source_repository_storage_name = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
Gitlab.config.repositories.storages.find do |_, info|
info.legacy_disk_path == source_repository_storage_path
end&.first || raise("no shard found for path '#{source_repository_storage_path}'")
end
fork_repository(target_project, source_repository_storage_name, source_disk_path)
source_project = target_project.forked_from_project
unless source_project
return target_project.mark_import_as_failed('Source project cannot be found.')
end
fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
end
private
......
---
title: Fix chat service tag notifications not sending when only default branch enabled
merge_request: 19864
author:
type: fixed
---
title: Fix CSS for buttons not to be hidden on issues/MR title
merge_request: 19176
author: Takuya Noguchi
type: fixed
---
title: Uses long sha version of the merged commit in MR widget copy to clipboard button
merge_request: 19955
author:
type: other
title: Fixed pagination of groups API
merge_request: 19665
author: Marko, Peter
type: added
---
title: Added id sorting option to GET groups and subgroups API
merge_request: 19665
author: Marko, Peter
type: added
---
title: Rails5 fix format in uploads actions
merge_request: 19907
author: Jasper Maes
type: fixed
---
title: Eliminate N+1 queries in LFS file locks checks during a push
merge_request:
author:
type: performance
......@@ -107,7 +107,7 @@ Global Admins GitLab.org/GitLab INT/Global Groups/Global Admins
## GitLab LDAP configuration
The initial configuration of LDAP in GitLab requires changes to the `gitlab.rb` configuration file. Below is an example of a complete configuration using an Active Directory.
The initial configuration of LDAP in GitLab requires changes to the `gitlab.rb` configuration file (`/etc/gitlab/gitlab.rb`). Below is an example of a complete configuration using an Active Directory.
The two Active Directory specific values are `active_directory: true` and `uid: 'sAMAccountName'`. `sAMAccountName` is an attribute returned by Active Directory used for GitLab usernames. See the example output from `ldapsearch` for a full list of attributes a "person" object (user) has in **AD** - [`ldapsearch` example](#using-ldapsearch-unix)
......
......@@ -12,7 +12,7 @@ Parameters:
| `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin) |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
......@@ -96,7 +96,7 @@ Parameters:
| `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin) |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
......
......@@ -88,18 +88,18 @@ The example below simply moves all files from the root of the project to the
`public/` directory. The `.public` workaround is so `cp` doesn't also copy
`public/` to itself in an infinite loop:
```
```yaml
pages:
stage: deploy
script:
- mkdir .public
- cp -r * .public
- mv .public public
- mkdir .public
- cp -r * .public
- mv .public public
artifacts:
paths:
- public
- public
only:
- master
- master
```
Read more on [GitLab Pages user documentation](../../user/project/pages/index.md).
......@@ -131,15 +131,15 @@ if you set it per-job:
```yaml
before_script:
- global before script
- global before script
job:
before_script:
- execute this instead of global before script
- execute this instead of global before script
script:
- my command
- my command
after_script:
- execute this after my script
- execute this after my script
```
## `stages`
......@@ -409,18 +409,18 @@ fails, it will not stop the next stage from running, since it's marked with
job1:
stage: test
script:
- execute_script_that_will_fail
- execute_script_that_will_fail
allow_failure: true
job2:
stage: test
script:
- execute_script_that_will_succeed
- execute_script_that_will_succeed
job3:
stage: deploy
script:
- deploy_to_staging
- deploy_to_staging
```
## `when`
......@@ -442,38 +442,38 @@ For example:
```yaml
stages:
- build
- cleanup_build
- test
- deploy
- cleanup
- build
- cleanup_build
- test
- deploy
- cleanup
build_job:
stage: build
script:
- make build
- make build
cleanup_build_job:
stage: cleanup_build
script:
- cleanup build when failed
- cleanup build when failed
when: on_failure
test_job:
stage: test
script:
- make test
- make test
deploy_job:
stage: deploy
script:
- make deploy
- make deploy
when: manual
cleanup_job:
stage: cleanup
script:
- cleanup after jobs
- cleanup after jobs
when: always
```
......@@ -734,8 +734,8 @@ rspec:
script: test
cache:
paths:
- binaries/*.apk
- .config
- binaries/*.apk
- .config
```
Locally defined cache overrides globally defined options. The following `rspec`
......@@ -744,14 +744,14 @@ job will cache only `binaries/`:
```yaml
cache:
paths:
- my/files
- my/files
rspec:
script: test
cache:
key: rspec
paths:
- binaries/
- binaries/
```
Note that since cache is shared between jobs, if you're using different
......@@ -786,7 +786,7 @@ For example, to enable per-branch caching:
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- binaries/
- binaries/
```
If you use **Windows Batch** to run your shell scripts you need to replace
......@@ -796,7 +796,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
cache:
key: "%CI_COMMIT_REF_SLUG%"
paths:
- binaries/
- binaries/
```
### `cache:untracked`
......@@ -819,7 +819,7 @@ rspec:
cache:
untracked: true
paths:
- binaries/
- binaries/
```
### `cache:policy`
......@@ -897,8 +897,8 @@ Send all files in `binaries` and `.config`:
```yaml
artifacts:
paths:
- binaries/
- .config
- binaries/
- .config
```
To disable artifact passing, define the job with empty [dependencies](#dependencies):
......@@ -927,7 +927,7 @@ release-job:
- mvn package -U
artifacts:
paths:
- target/*.war
- target/*.war
only:
- tags
```
......@@ -949,7 +949,7 @@ job:
artifacts:
name: "$CI_JOB_NAME"
paths:
- binaries/
- binaries/
```
To create an archive with a name of the current branch or tag including only
......@@ -960,7 +960,7 @@ job:
artifacts:
name: "$CI_COMMIT_REF_NAME"
paths:
- binaries/
- binaries/
```
To create an archive with a name of the current job and the current branch or
......@@ -971,7 +971,7 @@ job:
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
paths:
- binaries/
- binaries/
```
To create an archive with a name of the current [stage](#stages) and branch name:
......@@ -981,7 +981,7 @@ job:
artifacts:
name: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
paths:
- binaries/
- binaries/
```
---
......@@ -994,7 +994,7 @@ job:
artifacts:
name: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
paths:
- binaries/
- binaries/
```
If you use **Windows PowerShell** to run your shell scripts you need to replace
......@@ -1005,7 +1005,7 @@ job:
artifacts:
name: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
paths:
- binaries/
- binaries/
```
### `artifacts:untracked`
......@@ -1030,7 +1030,7 @@ Send all Git untracked files and files in `binaries`:
artifacts:
untracked: true
paths:
- binaries/
- binaries/
```
### `artifacts:when`
......@@ -1120,26 +1120,26 @@ build:osx:
script: make build:osx
artifacts:
paths:
- binaries/
- binaries/
build:linux:
stage: build
script: make build:linux
artifacts:
paths:
- binaries/
- binaries/
test:osx:
stage: test
script: make test:osx
dependencies:
- build:osx
- build:osx
test:linux:
stage: test
script: make test:linux
dependencies:
- build:linux
- build:linux
deploy:
stage: deploy
......
......@@ -168,7 +168,7 @@ want their accounts to be upgraded to full internal accounts.
>**Note:**
The following information only applies for installations from source.
GitLab uses [Omniauth](http://www.omniauth.org/) for authentication and already ships
GitLab uses [Omniauth](https://github.com/omniauth/omniauth) for authentication and already ships
with a few providers pre-installed (e.g. LDAP, GitHub, Twitter). But sometimes that
is not enough and you need to integrate with other authentication solutions. For
these cases you can use the Omniauth provider.
......
......@@ -172,4 +172,12 @@ prevent any conflicting changes.
You can access your repos via [repository API](../../../api/repositories.md).
## Clone in Apple Xcode
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45820) in GitLab 11.0
Projects that contain a `.xcodeproj` or `.xcworkspace` directory can now be cloned
in Xcode using the new **Open in Xcode** button, located next to the Git URL
used for cloning your project. The button is only shown on macOS.
[jupyter]: https://jupyter.org
......@@ -41,7 +41,7 @@ module API
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
optional :search, type: String, desc: 'Search for a specific group'
optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
optional :order_by, type: String, values: %w[name path id], default: 'name', desc: 'Order by name, path or id'
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
use :pagination
end
......@@ -57,7 +57,9 @@ module API
groups = groups.preload(:ldap_group_links)
groups = groups.search(params[:search]) if params[:search].present?
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort])
order_options = { params[:order_by] => params[:sort] }
order_options["id"] ||= "asc"
groups = groups.reorder(order_options)
groups
end
......
......@@ -7,18 +7,10 @@ module Gitlab
# Created or deleted branch
return false if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
GitalyClient.migrate(:force_push) do |is_enabled|
if is_enabled
!project
.repository
.gitaly_commit_client
.ancestor?(oldrev, newrev)
else
Gitlab::Git::RevList.new(
project.repository.raw, oldrev: oldrev, newrev: newrev
).missed_ref.present?
end
end
!project
.repository
.gitaly_commit_client
.ancestor?(oldrev, newrev)
end
end
end
......
......@@ -472,13 +472,21 @@ module Gitlab
end
def count_commits(options)
count_commits_options = process_count_commits_options(options)
options = process_count_commits_options(options.dup)
gitaly_migrate(:count_commits, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
count_commits_by_gitaly(count_commits_options)
wrapped_gitaly_errors do
if options[:left_right]
from = options[:from]
to = options[:to]
right_count = gitaly_commit_client
.commit_count("#{from}..#{to}", options)
left_count = gitaly_commit_client
.commit_count("#{to}..#{from}", options)
[left_count, right_count]
else
count_commits_by_shelling_out(count_commits_options)
gitaly_commit_client.commit_count(options[:ref], options)
end
end
end
......@@ -676,15 +684,9 @@ module Gitlab
end
# Return total commits count accessible from passed ref
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/330
def commit_count(ref)
gitaly_migrate(:commit_count, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_commit_client.commit_count(ref)
else
rugged_commit_count(ref)
end
wrapped_gitaly_errors do
gitaly_commit_client.commit_count(ref)
end
end
......@@ -988,21 +990,7 @@ module Gitlab
def info_attributes
return @info_attributes if @info_attributes
content =
gitaly_migrate(:get_info_attributes, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.info_attributes
else
attributes_path = File.join(File.expand_path(path), 'info', 'attributes')
if File.exist?(attributes_path)
File.read(attributes_path)
else
""
end
end
end
content = gitaly_repository_client.info_attributes
@info_attributes = AttributesParser.new(content)
end
......@@ -1058,18 +1046,8 @@ module Gitlab
end
def license_short_name
gitaly_migrate(:license_short_name,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.license_short_name
else
begin
# The licensee gem creates a Rugged object from the path:
# https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
Licensee.license(path).try(:key)
rescue Rugged::Error
end
end
wrapped_gitaly_errors do
gitaly_repository_client.license_short_name
end
end
......@@ -1256,12 +1234,8 @@ module Gitlab
end
def rebase_in_progress?(rebase_id)
gitaly_migrate(:rebase_in_progress) do |is_enabled|
if is_enabled
gitaly_repository_client.rebase_in_progress?(rebase_id)
else
fresh_worktree?(worktree_path(REBASE_WORKTREE_PREFIX, rebase_id))
end
wrapped_gitaly_errors do
gitaly_repository_client.rebase_in_progress?(rebase_id)
end
end
......@@ -1277,12 +1251,8 @@ module Gitlab
end
def squash_in_progress?(squash_id)
gitaly_migrate(:squash_in_progress, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.squash_in_progress?(squash_id)
else
fresh_worktree?(worktree_path(SQUASH_WORKTREE_PREFIX, squash_id))
end
wrapped_gitaly_errors do
gitaly_repository_client.squash_in_progress?(squash_id)
end
end
......@@ -1528,10 +1498,6 @@ module Gitlab
run_git!(args, lazy_block: block)
end
def missed_ref(oldrev, newrev)
run_git!(['rev-list', '--max-count=1', oldrev, "^#{newrev}"])
end
def with_worktree(worktree_path, branch, sparse_checkout_files: nil, env:)
base_args = %w(worktree add --detach)
......@@ -1635,21 +1601,6 @@ module Gitlab
end
end
# This function is duplicated in Gitaly-Go, don't change it!
# https://gitlab.com/gitlab-org/gitaly/merge_requests/698
def fresh_worktree?(path)
File.exist?(path) && !clean_stuck_worktree(path)
end
# This function is duplicated in Gitaly-Go, don't change it!
# https://gitlab.com/gitlab-org/gitaly/merge_requests/698
def clean_stuck_worktree(path)
return false unless File.mtime(path) < 15.minutes.ago
FileUtils.rm_rf(path)
true
end
# Adding a worktree means checking out the repository. For large repos,
# this can be very expensive, so set up sparse checkout for the worktree
# to only check out the files we're interested in.
......@@ -1916,71 +1867,6 @@ module Gitlab
gitaly_repository_client.repository_size
end
def count_commits_by_gitaly(options)
if options[:left_right]
from = options[:from]
to = options[:to]
right_count = gitaly_commit_client
.commit_count("#{from}..#{to}", options)
left_count = gitaly_commit_client
.commit_count("#{to}..#{from}", options)
[left_count, right_count]
else
gitaly_commit_client.commit_count(options[:ref], options)
end
end
def count_commits_by_shelling_out(options)
cmd = count_commits_shelling_command(options)
raw_output, _status = run_git(cmd)
process_count_commits_raw_output(raw_output, options)
end
def count_commits_shelling_command(options)
cmd = %w[rev-list]
cmd << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << "--max-count=#{options[:max_count]}" if options[:max_count]
cmd << "--left-right" if options[:left_right]
cmd << '--count'
cmd << if options[:all]
'--all'
elsif options[:ref]
options[:ref]
else
raise ArgumentError, "Please specify a valid ref or set the 'all' attribute to true"
end
cmd += %W[-- #{options[:path]}] if options[:path].present?
cmd
end
def process_count_commits_raw_output(raw_output, options)
if options[:left_right]
result = raw_output.scan(/\d+/).map(&:to_i)
if result.sum != options[:max_count]
result
else # Reaching max count, right is not accurate
right_option =
process_count_commits_options(options
.except(:left_right, :from, :to)
.merge(ref: options[:to]))
right = count_commits_by_shelling_out(right_option)
[result.first, right] # left should be accurate in the first call
end
else
raw_output.to_i
end
end
def gitaly_ls_files(ref)
gitaly_commit_client.ls_files(ref)
end
......@@ -2415,16 +2301,6 @@ module Gitlab
nil
end
def rugged_commit_count(ref)
walker = Rugged::Walker.new(rugged)
walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE)
oid = rugged.rev_parse_oid(ref)
walker.push(oid)
walker.count
rescue Rugged::ReferenceError
0
end
def rev_list_param(spec)
spec == :all ? ['--all'] : spec
end
......
# Gitaly note: JV: will probably be migrated indirectly by migrating the call sites.
module Gitlab
module Git
class RevList
......@@ -45,13 +43,6 @@ module Gitlab
&lazy_block)
end
# This methods returns an array of missed references
#
# Should become obsolete after https://gitlab.com/gitlab-org/gitaly/issues/348.
def missed_ref
repository.missed_ref(oldrev, newrev).split("\n")
end
private
def execute(args)
......
# Gitaly note: JV: two sets of straightforward RPC's. 1 Hard RPC: fork_repository.
# SSH key operations are not part of Gitaly so will never be migrated.
# Gitaly note: SSH key operations are not part of Gitaly so will never be migrated.
require 'securerandom'
......@@ -153,8 +152,6 @@ module Gitlab
#
# Ex.
# mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new")
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873
def mv_repository(storage, path, new_path)
return false if path.empty? || new_path.empty?
......@@ -169,19 +166,11 @@ module Gitlab
#
# Ex.
# fork_repository("nfs-file06", "gitlab/gitlab-ci", "nfs-file07", "new-namespace/gitlab-ci")
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/817
def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path)
forked_from_relative_path = "#{forked_from_disk_path}.git"
fork_args = [forked_to_storage, "#{forked_to_disk_path}.git"]
gitaly_migrate(:fork_repository, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
GitalyGitlabProjects.new(forked_from_storage, forked_from_relative_path).fork_repository(*fork_args)
else
gitlab_projects(forked_from_storage, forked_from_relative_path).fork_repository(*fork_args)
end
end
GitalyGitlabProjects.new(forked_from_storage, forked_from_relative_path).fork_repository(*fork_args)
end
# Removes a repository from file system, using rm_diretory which is an alias
......@@ -193,8 +182,6 @@ module Gitlab
#
# Ex.
# remove_repository("/path/to/storage", "gitlab/gitlab-ci")
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873
def remove_repository(storage, name)
return false if name.empty?
......
......@@ -39,7 +39,8 @@ describe('MRWidgetMerged', () => {
readableClosedAt: '',
},
updatedAt: 'mergedUpdatedAt',
shortMergeCommitSha: 'asdf1234',
shortMergeCommitSha: '958c0475',
mergeCommitSha: '958c047516e182dfc52317f721f696e8a1ee85ed',
mergeCommitPath: 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d',
sourceBranch: 'bar',
targetBranch,
......@@ -153,7 +154,7 @@ describe('MRWidgetMerged', () => {
it('shows button to copy commit SHA to clipboard', () => {
expect(selectors.copyMergeShaButton).toExist();
expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(vm.mr.shortMergeCommitSha);
expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(vm.mr.mergeCommitSha);
});
it('shows merge commit SHA link', () => {
......
......@@ -99,7 +99,7 @@ describe Gitlab::Auth::UserAuthFinders do
context 'when the request format is empty' do
it 'the method call does not modify the original value' do
env['action_dispatch.request.formats'] = nil
env.delete('action_dispatch.request.formats')
find_user_from_feed_token
......
require 'spec_helper'
describe Gitlab::Checks::ForcePush do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
set(:project) { create(:project, :repository) }
context "exit code checking", :skip_gitaly_mock do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
allow(repository).to receive(:popen).and_return(['normal output', 0])
describe '.force_push?' do
it 'returns false if the repo is empty' do
allow(project).to receive(:empty_repo?).and_return(true)
expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
expect(described_class.force_push?(project, 'HEAD', 'HEAD~')).to be(false)
end
it "raises a GitError error if the `popen` call to git returns a non-zero exit code" do
allow(repository).to receive(:popen).and_return(['error', 1])
expect { described_class.force_push?(project, 'oldrev', 'newrev') }
.to raise_error(Gitlab::Git::Repository::GitError)
it 'checks if old rev is an anchestor' do
expect(described_class.force_push?(project, 'HEAD', 'HEAD~')).to be(true)
end
end
end
......@@ -1114,7 +1114,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#count_commits' do
shared_examples 'extended commit counting' do
describe 'extended commit counting' do
context 'with after timestamp' do
it 'returns the number of commits after timestamp' do
options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') }
......@@ -1199,14 +1199,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
end
context 'when Gitaly count_commits feature is enabled' do
it_behaves_like 'extended commit counting'
end
context 'when Gitaly count_commits feature is disabled', :disable_gitaly do
it_behaves_like 'extended commit counting'
end
end
describe '#autocrlf' do
......@@ -1688,70 +1680,52 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#languages' do
shared_examples 'languages' do
it 'returns exactly the expected results' do
languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6')
expected_languages = [
{ value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" },
{ value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" },
{ value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" },
{ value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" }
]
it 'returns exactly the expected results' do
languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6')
expected_languages = [
{ value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" },
{ value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" },
{ value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" },
{ value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" }
]
expect(languages.size).to eq(expected_languages.size)
expect(languages.size).to eq(expected_languages.size)
expected_languages.size.times do |i|
a = expected_languages[i]
b = languages[i]
expected_languages.size.times do |i|
a = expected_languages[i]
b = languages[i]
expect(a.keys.sort).to eq(b.keys.sort)
expect(a[:value]).to be_within(0.1).of(b[:value])
expect(a.keys.sort).to eq(b.keys.sort)
expect(a[:value]).to be_within(0.1).of(b[:value])
non_float_keys = a.keys - [:value]
expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys))
end
end
it "uses the repository's HEAD when no ref is passed" do
lang = repository.languages.first
expect(lang[:label]).to eq('Ruby')
non_float_keys = a.keys - [:value]
expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys))
end
end
it_behaves_like 'languages'
it "uses the repository's HEAD when no ref is passed" do
lang = repository.languages.first
context 'with rugged', :skip_gitaly_mock do
it_behaves_like 'languages'
expect(lang[:label]).to eq('Ruby')
end
end
describe '#license_short_name' do
shared_examples 'acquiring the Licensee license key' do
subject { repository.license_short_name }
context 'when no license file can be found' do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
subject { repository.license_short_name }
before do
project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
end
it { is_expected.to be_nil }
end
context 'when no license file can be found' do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
context 'when an mit license is found' do
it { is_expected.to eq('mit') }
before do
project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
end
end
context 'when gitaly is enabled' do
it_behaves_like 'acquiring the Licensee license key'
it { is_expected.to be_nil }
end
context 'when gitaly is disabled', :disable_gitaly do
it_behaves_like 'acquiring the Licensee license key'
context 'when an mit license is found' do
it { is_expected.to eq('mit') }
end
end
......
......@@ -93,14 +93,4 @@ describe Gitlab::Git::RevList do
expect { |b| rev_list.all_objects(&b) }.to yield_with_args(%w[sha1 sha2])
end
end
context "#missed_ref" do
let(:rev_list) { described_class.new(repository, oldrev: 'oldrev', newrev: 'newrev') }
it 'calls out to `popen`' do
stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', with_lazy_block: false, output: "sha1\nsha2")
expect(rev_list.missed_ref).to eq(%w[sha1 sha2])
end
end
end
......@@ -498,34 +498,18 @@ describe Gitlab::Shell do
)
end
context 'with gitaly' do
it 'returns true when the command succeeds' do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository)
.with(repository.raw_repository) { :gitaly_response_object }
is_expected.to be_truthy
end
it 'return false when the command fails' do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository)
.with(repository.raw_repository) { raise GRPC::BadStatus, 'bla' }
it 'returns true when the command succeeds' do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository)
.with(repository.raw_repository) { :gitaly_response_object }
is_expected.to be_falsy
end
is_expected.to be_truthy
end
context 'without gitaly', :disable_gitaly do
it 'returns true when the command succeeds' do
expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { true }
is_expected.to be_truthy
end
it 'return false when the command fails' do
expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { false }
it 'return false when the command fails' do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository)
.with(repository.raw_repository) { raise GRPC::BadStatus, 'bla' }
is_expected.to be_falsy
end
is_expected.to be_falsy
end
end
......@@ -665,7 +649,7 @@ describe Gitlab::Shell do
subject do
gitlab_shell.fetch_remote(repository.raw_repository, remote_name,
forced: true, no_tags: true, ssh_auth: ssh_auth)
forced: true, no_tags: true, ssh_auth: ssh_auth)
end
it 'passes the correct params to the gitaly service' do
......
......@@ -152,10 +152,15 @@ describe API::Groups do
context "when using sorting" do
let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") }
let(:group4) { create(:group, name: "same-name", path: "y#{group1.path}") }
let(:group5) { create(:group, name: "same-name") }
let(:response_groups) { json_response.map { |group| group['name'] } }
let(:response_groups_ids) { json_response.map { |group| group['id'] } }
before do
group3.add_owner(user1)
group4.add_owner(user1)
group5.add_owner(user1)
end
it "sorts by name ascending by default" do
......@@ -164,7 +169,7 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group3.name, group1.name])
expect(response_groups).to eq(Group.visible_to_user(user1).order(:name).pluck(:name))
end
it "sorts in descending order when passed" do
......@@ -173,16 +178,52 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group1.name, group3.name])
expect(response_groups).to eq(Group.visible_to_user(user1).order(name: :desc).pluck(:name))
end
it "sorts by the order_by param" do
it "sorts by path in order_by param" do
get api("/groups", user1), order_by: "path"
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group1.name, group3.name])
expect(response_groups).to eq(Group.visible_to_user(user1).order(:path).pluck(:name))
end
it "sorts by id in the order_by param" do
get api("/groups", user1), order_by: "id"
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq(Group.visible_to_user(user1).order(:id).pluck(:name))
end
it "sorts also by descending id with pagination fix" do
get api("/groups", user1), order_by: "id", sort: "desc"
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq(Group.visible_to_user(user1).order(id: :desc).pluck(:name))
end
it "sorts identical keys by id for good pagination" do
get api("/groups", user1), search: "same-name", order_by: "name"
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups_ids).to eq(Group.select { |group| group['name'] == 'same-name' }.map { |group| group['id'] }.sort)
end
it "sorts descending identical keys by id for good pagination" do
get api("/groups", user1), search: "same-name", order_by: "name", sort: "desc"
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups_ids).to eq(Group.select { |group| group['name'] == 'same-name' }.map { |group| group['id'] }.sort)
end
end
......
......@@ -13,7 +13,7 @@ module Spec
module Features
module NotesHelpers
def add_note(text)
Sidekiq::Testing.fake! do
perform_enqueued_jobs do
page.within(".js-main-target-form") do
fill_in("note[note]", with: text)
find(".js-comment-submit-button").click
......
......@@ -245,6 +245,70 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
end
describe 'Push events' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, creator: user) }
before do
allow(chat_service).to receive_messages(
project: project,
service_hook: true,
webhook: webhook_url
)
WebMock.stub_request(:post, webhook_url)
end
context 'only notify for the default branch' do
context 'when enabled' do
before do
chat_service.notify_only_default_branch = true
end
it 'does not notify push events if they are not for the default branch' do
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
chat_service.execute(push_sample_data)
expect(WebMock).not_to have_requested(:post, webhook_url)
end
it 'notifies about push events for the default branch' do
push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
chat_service.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
it 'still notifies about pushed tags' do
ref = "#{Gitlab::Git::TAG_REF_PREFIX}test"
push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
chat_service.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
context 'when disabled' do
before do
chat_service.notify_only_default_branch = false
end
it 'notifies about all push events' do
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
chat_service.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
end
end
describe "Note events" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, creator: user) }
......@@ -394,23 +458,6 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(result).to be_falsy
end
it 'does not notify push events if they are not for the default branch' do
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
chat_service.execute(push_sample_data)
expect(WebMock).not_to have_requested(:post, webhook_url)
end
it 'notifies about push events for the default branch' do
push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
chat_service.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
context 'when disabled' do
......
......@@ -92,16 +92,6 @@ describe RepositoryForkWorker do
end
it_behaves_like 'RepositoryForkWorker performing'
it 'logs a message about forking with old-style arguments' do
allow(subject).to receive(:gitlab_shell).and_return(shell)
expect(shell).to receive(:fork_repository) { true }
allow(Rails.logger).to receive(:info).with(anything) # To compensate for other logs
expect(Rails.logger).to receive(:info).with("Project #{fork_project.id} is being forked using old-style arguments.")
perform!
end
end
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