Commit cde1181d authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-09-03

# Conflicts:
#	doc/api/settings.md
#	lib/gitlab/email/handler.rb
#	locale/gitlab.pot

[ci skip]
parents 9cda7958 48541cac
......@@ -23,6 +23,11 @@ export default {
required: true,
},
},
data() {
return {
wasValidated: false,
};
},
computed: {
...mapState([
'badgeInAddForm',
......@@ -39,16 +44,6 @@ export default {
return this.badgeInAddForm;
},
canSubmit() {
return (
this.badge !== null &&
this.badge.imageUrl &&
this.badge.imageUrl.trim() !== '' &&
this.badge.linkUrl &&
this.badge.linkUrl.trim() !== '' &&
!this.isSaving
);
},
helpText() {
const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
.map(placeholder => `<code>%{${placeholder}}</code>`)
......@@ -93,11 +88,18 @@ export default {
});
},
},
submitButtonLabel() {
if (this.isEditing) {
return s__('Badges|Save changes');
}
return s__('Badges|Add badge');
badgeImageUrlExample() {
const exampleUrl =
'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/badge.svg';
return sprintf(s__('Badges|e.g. %{exampleUrl}'), {
exampleUrl,
});
},
badgeLinkUrlExample() {
const exampleUrl = 'https://example.gitlab.com/%{project_path}';
return sprintf(s__('Badges|e.g. %{exampleUrl}'), {
exampleUrl,
});
},
},
methods: {
......@@ -109,7 +111,9 @@ export default {
this.stopEditing();
},
onSubmit() {
if (!this.canSubmit) {
const form = this.$el;
if (!form.checkValidity()) {
this.wasValidated = true;
return Promise.resolve();
}
......@@ -117,6 +121,7 @@ export default {
return this.saveBadge()
.then(() => {
createFlash(s__('Badges|The badge was saved.'), 'notice');
this.wasValidated = false;
})
.catch(error => {
createFlash(
......@@ -129,6 +134,7 @@ export default {
return this.addBadge()
.then(() => {
createFlash(s__('Badges|A new badge was added.'), 'notice');
this.wasValidated = false;
})
.catch(error => {
createFlash(
......@@ -138,47 +144,58 @@ export default {
});
},
},
badgeImageUrlPlaceholder:
'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg',
badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}',
};
</script>
<template>
<form
class="prepend-top-default append-bottom-default"
:class="{ 'was-validated': wasValidated }"
class="prepend-top-default append-bottom-default needs-validation"
novalidate
@submit.prevent.stop="onSubmit"
>
<div class="form-group">
<label for="badge-link-url">{{ s__('Badges|Link') }}</label>
<label
for="badge-link-url"
class="label-bold"
>{{ s__('Badges|Link') }}</label>
<p v-html="helpText"></p>
<input
id="badge-link-url"
v-model="linkUrl"
:placeholder="$options.badgeLinkUrlPlaceholder"
type="text"
type="URL"
class="form-control"
required
@input="debouncedPreview"
/>
<span
class="form-text text-muted"
v-html="helpText"
></span>
<div class="invalid-feedback">
{{ s__('Badges|Please fill in a valid URL') }}
</div>
<span class="form-text text-muted">
{{ badgeLinkUrlExample }}
</span>
</div>
<div class="form-group">
<label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
<label
for="badge-image-url"
class="label-bold"
>{{ s__('Badges|Badge image URL') }}</label>
<p v-html="helpText"></p>
<input
id="badge-image-url"
v-model="imageUrl"
:placeholder="$options.badgeImageUrlPlaceholder"
type="text"
type="URL"
class="form-control"
required
@input="debouncedPreview"
/>
<span
class="form-text text-muted"
v-html="helpText"
></span>
<div class="invalid-feedback">
{{ s__('Badges|Please fill in a valid URL') }}
</div>
<span class="form-text text-muted">
{{ badgeImageUrlExample }}
</span>
</div>
<div class="form-group">
......@@ -200,20 +217,32 @@ export default {
>{{ s__('Badges|No image to preview') }}</p>
</div>
<div class="row-content-block">
<div
v-if="isEditing"
class="row-content-block"
>
<loading-button
:disabled="!canSubmit"
:loading="isSaving"
:label="submitButtonLabel"
:label="s__('Badges|Save changes')"
type="submit"
container-class="btn btn-success"
/>
<button
v-if="isEditing"
class="btn btn-cancel"
type="button"
@click="onCancel"
>{{ __('Cancel') }}</button>
</div>
<div
v-else
class="form-group"
>
<loading-button
:loading="isSaving"
:label="s__('Badges|Add badge')"
type="submit"
container-class="btn btn-success"
/>
</div>
</form>
</template>
......@@ -28,7 +28,7 @@ export default {
{{ s__('Badges|Your badges') }}
<span
v-show="!isLoading"
class="badge"
class="badge badge-pill"
>{{ badges.length }}</span>
</div>
<loading-icon
......
......@@ -43,13 +43,13 @@ export default {
<badge
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
class="table-section section-30"
class="table-section section-40"
/>
<span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-10">
<span class="badge">{{ badgeKindText }}</span>
<span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-15">
<span class="badge badge-pill">{{ badgeKindText }}</span>
</div>
<div class="table-section section-10 table-button-footer">
<div class="table-section section-15 table-button-footer">
<div
v-if="canEditBadge"
class="table-action-buttons">
......
......@@ -2,14 +2,13 @@ import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
import initSettingsPanels from '~/settings_panels';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import { GROUP_BADGE } from '~/badges/constants';
document.addEventListener('DOMContentLoaded', () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal();
});
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
mountBadgeSettings(GROUP_BADGE);
});
import { PROJECT_BADGE } from '~/badges/constants';
import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import initProjectLoadingSpinner from '../shared/save_project_loader';
import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
......@@ -13,4 +15,5 @@ document.addEventListener('DOMContentLoaded', () => {
projectAvatar();
initProjectPermissionsSettings();
initConfirmDangerModal();
mountBadgeSettings(PROJECT_BADGE);
});
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { PROJECT_BADGE } from '~/badges/constants';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
mountBadgeSettings(PROJECT_BADGE);
});
module Groups
module Settings
class BadgesController < Groups::ApplicationController
include API::Helpers::RelatedResourcesHelpers
before_action :authorize_admin_group!
def index
@badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id))
end
end
end
end
class GroupsController < Groups::ApplicationController
include API::Helpers::RelatedResourcesHelpers
include IssuesAction
include MergeRequestsAction
include ParamsBackwardCompatibility
......@@ -78,6 +79,7 @@ class GroupsController < Groups::ApplicationController
end
def edit
@badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id))
end
def projects
......
module Projects
module Settings
class BadgesController < Projects::ApplicationController
include API::Helpers::RelatedResourcesHelpers
before_action :authorize_admin_project!
def index
@badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id))
end
end
end
end
class ProjectsController < Projects::ApplicationController
include API::Helpers::RelatedResourcesHelpers
include IssuableCollections
include ExtractsPath
include PreviewMarkdown
......@@ -34,6 +35,7 @@ class ProjectsController < Projects::ApplicationController
end
def edit
@badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id))
render 'edit'
end
......
......@@ -25,6 +25,18 @@
.settings-content
= render 'groups/settings/permissions'
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('GroupSettings|Badges')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= s_('GroupSettings|Customize your group badges.')
= link_to s_('GroupSettings|Learn more about badges.'), help_page_path('user/project/badges')
.settings-content
= render 'shared/badges/badge_settings'
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
......
......@@ -130,12 +130,6 @@
%span
= _('General')
= nav_link(controller: :badges) do
= link_to group_settings_badges_path(@group), title: _('Project Badges') do
%span
= _('Project Badges')
= nav_link(path: 'groups#projects') do
= link_to projects_group_path(@group), title: _('Projects') do
%span
......
......@@ -325,11 +325,6 @@
= link_to project_project_members_path(@project), title: _('Members') do
%span
= _('Members')
- if can_edit
= nav_link(controller: :badges) do
= link_to project_settings_badges_path(@project), title: _('Badges') do
%span
= _('Badges')
- if can_edit
= nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
= link_to project_settings_integrations_path(@project), title: _('Integrations') do
......
......@@ -102,6 +102,18 @@
= render_if_exists 'projects/service_desk_settings'
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('ProjectSettings|Badges')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= s_('ProjectSettings|Customize your project badges.')
= link_to s_('ProjectSettings|Learn more about badges.'), help_page_path('user/project/badges')
.settings-content
= render 'shared/badges/badge_settings'
= render 'export', project: @project
%section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) }
......
---
title: Add JSON logging for Bitbucket Server importer
merge_request: 21378
author:
type: other
---
title: Move badge settings to general settings
merge_request: 21333
author:
type: changed
......@@ -26,7 +26,6 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do
namespace :settings do
resource :ci_cd, only: [:show], controller: 'ci_cd'
resources :badges, only: [:index]
end
resource :variables, only: [:show, :update]
......
......@@ -526,7 +526,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :repository, only: [:show], controller: :repository do
post :create_deploy_token, path: 'deploy_token/create'
end
resources :badges, only: [:index]
end
# Since both wiki and repository routing contains wildcard characters
......
......@@ -219,6 +219,15 @@ installations from source.
It logs information whenever a [repository check is run][repocheck] on a project.
## `importer.log`
Introduced in GitLab 11.3. This file lives in `/var/log/gitlab/gitlab-rails/importer.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/importer.log` for
installations from source.
Currently it logs the progress of project imports from the Bitbucket Server
importer. Future importers may use this file.
## Reconfigure Logs
Reconfigure log files live in `/var/log/gitlab/reconfigure` for Omnibus GitLab
......
......@@ -140,12 +140,18 @@ are listed in the descriptions of the relevant settings.
| `after_sign_up_text` | string | no | Text shown to the user after signing up |
| `akismet_api_key` | string | required by: `akismet_enabled` | API key for akismet spam protection. |
| `akismet_enabled` | boolean | no | (**If enabled, requires:** `akismet_api_key`) Enable or disable akismet spam protection. |
<<<<<<< HEAD
| `allow_group_owners_to_manage_ldap` | boolean | no | **(Premium)** Set to `true` to allow group owners to manage LDAP |
=======
>>>>>>> upstream/master
| `allow_local_requests_from_hooks_and_services` | boolean | no | Allow requests to the local network from hooks and services. |
| `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. |
| `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. |
<<<<<<< HEAD
| `check_namespace_plan` | boolean | no | **(Premium)** Enabling this will make only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. |
=======
>>>>>>> upstream/master
| `circuitbreaker_access_retries` | integer | no | The number of attempts GitLab will make to access a storage. |
| `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. |
| `circuitbreaker_failure_count_threshold` | integer | no | The number of failures after which GitLab will completely prevent access to the storage. |
......@@ -167,6 +173,7 @@ are listed in the descriptions of the relevant settings.
| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
| `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. |
| `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. |
<<<<<<< HEAD
| `elasticsearch_aws` | boolean | no | **(Premium)** Enable the use of AWS hosted Elasticsearch |
| `elasticsearch_aws_access_key` | string | no | **(Premium)** AWS IAM access key |
| `elasticsearch_aws_region` | string | no | **(Premium)** The AWS region the elasticsearch domain is configured |
......@@ -188,6 +195,11 @@ are listed in the descriptions of the relevant settings.
| `external_authorization_service_url` | string | required by: `external_authorization_service_enabled` | **(Premium)** URL to which authorization requests will be directed |
| `file_template_project_id` | integer | no | **(Premium)** The ID of a project to load custom file templates from |
| `geo_status_timeout` | integer | no | **(Premium)** The amount of seconds after which a request to get a secondary node status will time out. |
=======
| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
| `enforce_terms` | boolean | no | (**If enabled, requires:** `terms`) Enforce application ToS to all users. |
>>>>>>> upstream/master
| `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. |
| `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. |
| `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. |
......@@ -196,7 +208,10 @@ are listed in the descriptions of the relevant settings.
| `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help. |
| `help_page_support_url` | string | no | Alternate support URL for help page. |
| `help_page_text` | string | no | Custom text displayed on the help page. |
<<<<<<< HEAD
| `help_text` | string | no | **(Premium)** GitLab server administrator information |
=======
>>>>>>> upstream/master
| `hide_third_party_offers` | boolean | no | Do not display offers from third parties within GitLab. |
| `home_page_url` | string | no | Redirect to this URL when not logged in. |
| `housekeeping_bitmaps_enabled` | boolean | required by: `housekeeping_enabled` | Enable Git pack file bitmap creation. |
......@@ -221,9 +236,12 @@ are listed in the descriptions of the relevant settings.
| `metrics_sample_interval` | integer | required by: `metrics_enabled` | The sampling interval in seconds. |
| `metrics_timeout` | integer | required by: `metrics_enabled` | The amount of seconds after which InfluxDB will time out. |
| `mirror_available` | boolean | no | Allow mirrors to be setup for projects. If disabled, only admins will be able to setup mirrors in projects. |
<<<<<<< HEAD
| `mirror_capacity_threshold` | integer | no | **(Premium)** Minimum capacity to be available before scheduling more mirrors preemptively |
| `mirror_max_capacity` | integer | no | **(Premium)** Maximum number of mirrors that can be synchronizing at the same time. |
| `mirror_max_delay` | integer | no | **(Premium)** Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. |
=======
>>>>>>> upstream/master
| `pages_domain_verification_enabled` | boolean | no | Require users to prove ownership of custom domains. Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled. |
| `password_authentication_enabled_for_git` | boolean | no | Enable authentication for Git over HTTP(S) via a GitLab account password. Default is `true`. |
| `password_authentication_enabled_for_web` | boolean | no | Enable authentication for the web interface via a GitLab account password. Default is `true`. |
......@@ -235,12 +253,18 @@ are listed in the descriptions of the relevant settings.
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. |
| `project_export_enabled` | boolean | no | Enable project export. |
| `prometheus_metrics_enabled` | boolean | no | Enable prometheus metrics. |
<<<<<<< HEAD
| `pseudonymizer_enabled` | boolean | no | **(Premium)** When enabled, GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory.
=======
>>>>>>> upstream/master
| `recaptcha_enabled` | boolean | no | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable recaptcha. |
| `recaptcha_private_key` | string | required by: `recaptcha_enabled` | Private key for recaptcha. |
| `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for recaptcha. |
| `repository_checks_enabled` | boolean | no | GitLab will periodically run `git fsck` in all project and wiki repositories to look for silent disk corruption issues. |
<<<<<<< HEAD
| `repository_size_limit` | integer | no | **(Premium)** Size limit per repository (MB) |
=======
>>>>>>> upstream/master
| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. |
| `require_two_factor_authentication` | boolean | no | (**If enabled, requires:** `two_factor_grace_period`) Require all users to set up Two-factor authentication. |
| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is `null` which means there is no restriction. |
......@@ -249,8 +273,12 @@ are listed in the descriptions of the relevant settings.
| `sentry_dsn` | string | required by: `sentry_enabled` | Sentry Data Source Name. |
| `sentry_enabled` | boolean | no | (**If enabled, requires:** `sentry_dsn`) Sentry is an error reporting and logging tool which is currently not shipped with GitLab, available at https://getsentry.com. |
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
<<<<<<< HEAD
| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text` and `shared_runners_minutes`) Enable shared runners for new projects. |
| `shared_runners_minutes` | integer | required by: `shared_runners_enabled` | **(Premium)** Set the maximum number of pipeline minutes that a group can use on shared Runners per month. |
=======
| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text`) Enable shared runners for new projects. |
>>>>>>> upstream/master
| `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. |
| `sidekiq_throttling_enabled` | boolean | no | (**If enabled, requires:** `sidekiq_throttling_factor` and `sidekiq_throttling_queues`) Enable Sidekiq Job Throttling. |
| `sidekiq_throttling_factor` | decimal | required by: `sidekiq_throttling_enabled` | The factor by which the queues should be throttled. A value between `0.0` and `1.0`, exclusive. |
......@@ -258,10 +286,13 @@ are listed in the descriptions of the relevant settings.
| `sign_in_text` | string | no | Text on the login page. |
| `signin_enabled` | string | no | (Deprecated: Use `password_authentication_enabled_for_web` instead) Flag indicating if password authentication is enabled for the web interface. |
| `signup_enabled` | boolean | no | Enable registration. Default is `true`. |
<<<<<<< HEAD
| `slack_app_enabled` | boolean | no | **(Premium)** (**If enabled, requires:** `slack_app_id`, `slack_app_secret` and `slack_app_secret`) Enable Slack app. |
| `slack_app_id` | string | required by: slack_app_enabled` | **(Premium)** The app id of the Slack-app. |
| `slack_app_secret` | string | required by: slack_app_enabled` | **(Premium)** The app secret of the Slack-app. |
| `slack_app_verification_token` | string | required by: slack_app_enabled` | **(Premium)** The verification token of the Slack-app. |
=======
>>>>>>> upstream/master
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to `0` for unlimited time. |
| `terms` | text | required by: `enforce_terms` | (**Required by:** `enforce_terms`) Markdown content for the ToS. |
| `throttle_authenticated_api_enabled` | boolean | no | (**If enabled, requires:** `throttle_authenticated_api_period_in_seconds` and `throttle_authenticated_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (e.g. from crawlers or abusive bots). |
......@@ -280,5 +311,9 @@ are listed in the descriptions of the relevant settings.
| `usage_ping_enabled` | boolean | no | Every week GitLab will report license usage back to GitLab, Inc. |
| `user_default_external` | boolean | no | Newly registered users will be external by default. |
| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider. |
<<<<<<< HEAD
| `user_show_add_ssh_key_message` | boolean | no | When set to `false`, disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. |
=======
| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. |
>>>>>>> upstream/master
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
......@@ -17,7 +17,7 @@ If you find that you have to add the same badges to several projects, you may wa
To add a new badge to a project:
1. Navigate to your project's **Settings > Badges**.
1. Navigate to your project's **Settings > General > Badges**.
1. Under "Link", enter the URL that the badges should point to and under
"Badge image URL" the URL of the image that should be displayed.
1. Submit the badge by clicking the **Add badge** button.
......@@ -39,7 +39,7 @@ project, consider adding them on the [project level](#project-badges) or use
To add a new badge to a group:
1. Navigate to your group's **Settings > Project Badges**.
1. Navigate to your group's **Settings > General > Badges**.
1. Under "Link", enter the URL that the badges should point to and under
"Badge image URL" the URL of the image that should be displayed.
1. Submit the badge by clicking the **Add badge** button.
......
......@@ -7,6 +7,7 @@ module Gitlab
attr_reader :recover_missing_commits
attr_reader :project, :project_key, :repository_slug, :client, :errors, :users
attr_accessor :logger
REMOTE_NAME = 'bitbucket_server'.freeze
BATCH_SIZE = 100
......@@ -36,6 +37,7 @@ module Gitlab
@errors = []
@users = {}
@temp_branches = []
@logger = Gitlab::Import::Logger.build
end
def execute
......@@ -44,6 +46,8 @@ module Gitlab
delete_temp_branches
handle_errors
log_info(stage: "complete")
true
end
......@@ -118,15 +122,21 @@ module Gitlab
client.create_branch(project_key, repository_slug, branch_name, sha)
branches_created << temp_branch
rescue BitbucketServer::Connection::ConnectionError => e
Rails.logger.warn("BitbucketServerImporter: Unable to recreate branch for SHA #{sha}: #{e}")
log_warn(message: "Unable to recreate branch", sha: sha, error: e.message)
end
end
end
def import_repository
log_info(stage: 'import_repository', message: 'starting import')
project.ensure_repository
project.repository.fetch_as_mirror(project.import_url, refmap: self.class.refmap, remote_name: REMOTE_NAME)
log_info(stage: 'import_repository', message: 'finished import')
rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
log_error(stage: 'import_repository', message: 'failed import', error: e.message)
# Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true
......@@ -157,7 +167,10 @@ module Gitlab
begin
import_bitbucket_pull_request(pull_request)
rescue StandardError => e
errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, trace: e.backtrace.join("\n"), raw_response: pull_request.raw }
backtrace = Gitlab::Profiler.clean_backtrace(e.backtrace)
log_error(stage: 'import_pull_requests', iid: pull_request.iid, error: e.message, backtrace: backtrace)
errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, backtrace: backtrace.join("\n"), raw_response: pull_request.raw }
end
end
end
......@@ -169,12 +182,15 @@ module Gitlab
client.delete_branch(project_key, repository_slug, branch.name, branch.sha)
project.repository.delete_branch(branch.name)
rescue BitbucketServer::Connection::ConnectionError => e
log_error(stage: 'delete_temp_branches', branch: branch.name, error: e.message)
@errors << { type: :delete_temp_branches, branch_name: branch.name, errors: e.message }
end
end
end
def import_bitbucket_pull_request(pull_request)
log_info(stage: 'import_bitbucket_pull_requests', message: 'starting', iid: pull_request.iid)
description = ''
description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author_email)
description += pull_request.description if pull_request.description
......@@ -201,9 +217,13 @@ module Gitlab
merge_request = creator.execute(attributes)
import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
log_info(stage: 'import_bitbucket_pull_requests', message: 'finished', iid: pull_request.iid)
end
def import_pull_request_comments(pull_request, merge_request)
log_info(stage: 'import_pull_request_comments', message: 'starting', iid: merge_request.iid)
comments, other_activities = client.activities(project_key, repository_slug, pull_request.iid).partition(&:comment?)
merge_event = other_activities.find(&:merge_event?)
......@@ -213,9 +233,16 @@ module Gitlab
import_inline_comments(inline_comments.map(&:comment), merge_request)
import_standalone_pr_comments(pr_comments.map(&:comment), merge_request)
log_info(stage: 'import_pull_request_comments', message: 'finished', iid: merge_request.iid,
merge_event_found: merge_event.present?,
inline_comments_count: inline_comments.count,
standalone_pr_comments: pr_comments.count)
end
def import_merge_event(merge_request, merge_event)
log_info(stage: 'import_merge_event', message: 'starting', iid: merge_request.iid)
committer = merge_event.committer_email
user_id = gitlab_user_id(committer)
......@@ -223,9 +250,13 @@ module Gitlab
merge_request.update({ merge_commit_sha: merge_event.merge_commit })
metric = MergeRequest::Metrics.find_or_initialize_by(merge_request: merge_request)
metric.update(merged_by_id: user_id, merged_at: timestamp)
log_info(stage: 'import_merge_event', message: 'finished', iid: merge_request.iid)
end
def import_inline_comments(inline_comments, merge_request)
log_info(stage: 'import_inline_comments', message: 'starting', iid: merge_request.iid)
inline_comments.each do |comment|
position = build_position(merge_request, comment)
parent = create_diff_note(merge_request, comment, position)
......@@ -238,6 +269,8 @@ module Gitlab
create_diff_note(merge_request, reply, position, discussion_id)
end
end
log_info(stage: 'import_inline_comments', message: 'finished', iid: merge_request.iid)
end
def create_diff_note(merge_request, comment, position, discussion_id = nil)
......@@ -252,11 +285,14 @@ module Gitlab
return note
end
log_info(stage: 'create_diff_note', message: 'creating fallback DiffNote', iid: merge_request.iid)
# Bitbucket Server supports the ability to comment on any line, not just the
# line in the diff. If we can't add the note as a DiffNote, fallback to creating
# a regular note.
create_fallback_diff_note(merge_request, comment, position)
rescue StandardError => e
log_error(stage: 'create_diff_note', comment_id: comment.id, error: e.message)
errors << { type: :pull_request, id: comment.id, errors: e.message }
nil
end
......@@ -294,7 +330,8 @@ module Gitlab
merge_request.notes.create!(pull_request_comment_attributes(replies))
end
rescue StandardError => e
errors << { type: :pull_request, iid: comment.id, errors: e.message }
log_error(stage: 'import_standalone_pr_comments', merge_request_id: merge_request.id, comment_id: comment.id, error: e.message)
errors << { type: :pull_request, comment_id: comment.id, errors: e.message }
end
end
end
......@@ -324,6 +361,26 @@ module Gitlab
updated_at: comment.updated_at
}
end
def log_info(details)
logger.info(log_base_data.merge(details))
end
def log_error(details)
logger.error(log_base_data.merge(details))
end
def log_warn(details)
logger.warn(log_base_data.merge(details))
end
def log_base_data
{
class: self.class.name,
project_id: project.id,
project_path: project.full_path
}
end
end
end
end
......@@ -3,8 +3,11 @@
module Gitlab
module Email
module Handler
<<<<<<< HEAD
prepend ::EE::Gitlab::Email::Handler
=======
>>>>>>> upstream/master
def self.handlers
@handlers ||= load_handlers
end
......
module Gitlab
module Import
class Logger < ::Gitlab::JsonLogger
def self.file_name_noext
'importer'
end
end
end
end
......@@ -957,6 +957,9 @@ msgstr ""
msgid "Badges|No image to preview"
msgstr ""
msgid "Badges|Please fill in a valid URL"
msgstr ""
msgid "Badges|Project Badge"
msgstr ""
......@@ -990,6 +993,9 @@ msgstr ""
msgid "Badges|Your badges"
msgstr ""
msgid "Badges|e.g. %{exampleUrl}"
msgstr ""
msgid "Begin with the selected commit"
msgstr ""
......@@ -3685,6 +3691,7 @@ msgstr ""
msgid "Group: %{group_name}"
msgstr ""
<<<<<<< HEAD
msgid "GroupRoadmap|From %{dateWord}"
msgstr ""
......@@ -3719,6 +3726,15 @@ msgid "GroupRoadmap|To widen your search, change or remove filters. In the weeks
msgstr ""
msgid "GroupRoadmap|Until %{dateWord}"
=======
msgid "GroupSettings|Badges"
msgstr ""
msgid "GroupSettings|Customize your group badges."
msgstr ""
msgid "GroupSettings|Learn more about badges."
>>>>>>> upstream/master
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
......@@ -5743,7 +5759,20 @@ msgstr ""
msgid "PrometheusAlerts|Add alert"
msgstr ""
<<<<<<< HEAD
msgid "PrometheusAlerts|Alert set"
=======
msgid "ProjectSettings|Badges"
msgstr ""
msgid "ProjectSettings|Customize your project badges."
msgstr ""
msgid "ProjectSettings|Learn more about badges."
msgstr ""
msgid "Projects"
>>>>>>> upstream/master
msgstr ""
msgid "PrometheusAlerts|Edit alert"
......
......@@ -57,6 +57,16 @@ describe GroupsController do
end
end
describe 'GET edit' do
it 'sets the badge API endpoint' do
sign_in(owner)
get :edit, id: group.to_param
expect(assigns(:badge_api_endpoint)).not_to be_nil
end
end
describe 'GET #new' do
context 'when creating subgroups', :nested_groups do
[true, false].each do |can_create_group_status|
......
......@@ -307,6 +307,19 @@ describe ProjectsController do
end
end
describe 'GET edit' do
it 'sets the badge API endpoint' do
sign_in(user)
project.add_maintainer(user)
get :edit,
namespace_id: project.namespace.path,
id: project.path
expect(assigns(:badge_api_endpoint)).not_to be_nil
end
end
describe "#update" do
render_views
......
......@@ -14,7 +14,7 @@ describe 'Group Badges' do
group.add_owner(user)
sign_in(user)
visit(group_settings_badges_path(group))
visit(edit_group_path(group))
end
it 'shows a list of badges', :js do
......
......@@ -15,7 +15,7 @@ describe 'Project Badges' do
group.add_maintainer(user)
sign_in(user)
visit(project_settings_badges_path(project))
visit(edit_project_path(project))
end
it 'shows a list of badges', :js do
......
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import store from '~/badges/store';
import createEmptyBadge from '~/badges/empty_badge';
import BadgeForm from '~/badges/components/badge_form.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { createDummyBadge } from '../dummy_badge';
import { DUMMY_IMAGE_URL, TEST_HOST } from '../../test_constants';
// avoid preview background process
BadgeForm.methods.debouncedPreview = () => {};
describe('BadgeForm component', () => {
const Component = Vue.extend(BadgeForm);
let axiosMock;
let vm;
beforeEach(() => {
setFixtures(`
<div id="dummy-element"></div>
`);
axiosMock = new MockAdapter(axios);
});
afterEach(() => {
vm.$destroy();
axiosMock.restore();
});
describe('methods', () => {
......@@ -38,93 +48,86 @@ describe('BadgeForm component', () => {
expect(vm.stopEditing).toHaveBeenCalled();
});
});
});
const sharedSubmitTests = submitAction => {
const imageUrlSelector = '#badge-image-url';
const findImageUrlElement = () => vm.$el.querySelector(imageUrlSelector);
const linkUrlSelector = '#badge-link-url';
const findLinkUrlElement = () => vm.$el.querySelector(linkUrlSelector);
const setValue = (inputElementSelector, url) => {
const inputElement = vm.$el.querySelector(inputElementSelector);
inputElement.value = url;
inputElement.dispatchEvent(new Event('input'));
};
const submitForm = () => {
const submitButton = vm.$el.querySelector('button[type="submit"]');
submitButton.click();
};
const expectInvalidInput = inputElementSelector => {
const inputElement = vm.$el.querySelector(inputElementSelector);
expect(inputElement).toBeMatchedBy(':invalid');
const feedbackElement = vm.$el.querySelector(`${inputElementSelector} + .invalid-feedback`);
expect(feedbackElement).toBeVisible();
};
describe('onSubmit', () => {
describe('if isEditing is true', () => {
beforeEach(() => {
spyOn(vm, 'saveBadge').and.returnValue(Promise.resolve());
spyOn(vm, submitAction).and.returnValue(Promise.resolve());
store.replaceState({
...store.state,
badgeInAddForm: createEmptyBadge(),
badgeInEditForm: createEmptyBadge(),
isSaving: false,
badgeInEditForm: createDummyBadge(),
});
vm.isEditing = true;
});
it('returns immediately if imageUrl is empty', () => {
store.state.badgeInEditForm.imageUrl = '';
vm.onSubmit();
expect(vm.saveBadge).not.toHaveBeenCalled();
});
it('returns immediately if linkUrl is empty', () => {
store.state.badgeInEditForm.linkUrl = '';
vm.onSubmit();
expect(vm.saveBadge).not.toHaveBeenCalled();
});
it('returns immediately if isSaving is true', () => {
store.state.isSaving = true;
vm.onSubmit();
expect(vm.saveBadge).not.toHaveBeenCalled();
setValue(linkUrlSelector, `${TEST_HOST}/link/url`);
setValue(imageUrlSelector, `${window.location.origin}${DUMMY_IMAGE_URL}`);
});
it('calls saveBadge', () => {
vm.onSubmit();
it('returns immediately if imageUrl is empty', () => {
setValue(imageUrlSelector, '');
expect(vm.saveBadge).toHaveBeenCalled();
});
});
submitForm();
describe('if isEditing is false', () => {
beforeEach(() => {
spyOn(vm, 'addBadge').and.returnValue(Promise.resolve());
store.replaceState({
...store.state,
isSaving: false,
badgeInAddForm: createDummyBadge(),
});
vm.isEditing = false;
expectInvalidInput(imageUrlSelector);
expect(vm[submitAction]).not.toHaveBeenCalled();
});
it('returns immediately if imageUrl is empty', () => {
store.state.badgeInAddForm.imageUrl = '';
it('returns immediately if imageUrl is malformed', () => {
setValue(imageUrlSelector, 'not-a-url');
vm.onSubmit();
submitForm();
expect(vm.addBadge).not.toHaveBeenCalled();
expectInvalidInput(imageUrlSelector);
expect(vm[submitAction]).not.toHaveBeenCalled();
});
it('returns immediately if linkUrl is empty', () => {
store.state.badgeInAddForm.linkUrl = '';
setValue(linkUrlSelector, '');
vm.onSubmit();
submitForm();
expect(vm.addBadge).not.toHaveBeenCalled();
expectInvalidInput(linkUrlSelector);
expect(vm[submitAction]).not.toHaveBeenCalled();
});
it('returns immediately if isSaving is true', () => {
store.state.isSaving = true;
it('returns immediately if linkUrl is malformed', () => {
setValue(linkUrlSelector, 'not-a-url');
vm.onSubmit();
submitForm();
expect(vm.addBadge).not.toHaveBeenCalled();
expectInvalidInput(linkUrlSelector);
expect(vm[submitAction]).not.toHaveBeenCalled();
});
it('calls addBadge', () => {
vm.onSubmit();
it(`calls ${submitAction}`, () => {
submitForm();
expect(vm.addBadge).toHaveBeenCalled();
});
});
});
expect(findImageUrlElement()).toBeMatchedBy(':valid');
expect(findLinkUrlElement()).toBeMatchedBy(':valid');
expect(vm[submitAction]).toHaveBeenCalled();
});
};
describe('if isEditing is false', () => {
beforeEach(() => {
......@@ -138,12 +141,15 @@ describe('BadgeForm component', () => {
});
it('renders one button', () => {
const buttons = vm.$el.querySelectorAll('.row-content-block button');
expect(vm.$el.querySelector('.row-content-block')).toBeNull();
const buttons = vm.$el.querySelectorAll('.form-group:last-of-type button');
expect(buttons.length).toBe(1);
const buttonAddElement = buttons[0];
expect(buttonAddElement).toBeVisible();
expect(buttonAddElement).toHaveText('Add badge');
});
sharedSubmitTests('addBadge');
});
describe('if isEditing is true', () => {
......@@ -167,5 +173,7 @@ describe('BadgeForm component', () => {
expect(buttonCancelElement).toBeVisible();
expect(buttonCancelElement).toHaveText('Cancel');
});
sharedSubmitTests('saveBadge');
});
});
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