Commit cd0e25e3 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'ce/master' into rc/ce-to-ee-friday

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents 1d510fd8 82f6c0f5
......@@ -20,7 +20,7 @@ gem 'rugged', '~> 0.24.0'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.2'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2'
......
......@@ -352,7 +352,7 @@ GEM
temple (~> 0.7.6)
thor
tilt
hashie (3.5.1)
hashie (3.5.5)
health_check (2.2.1)
rails (>= 4.0)
hipchat (1.5.2)
......@@ -465,7 +465,7 @@ GEM
octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4)
omniauth (1.3.2)
omniauth (1.4.2)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1)
......@@ -951,7 +951,7 @@ DEPENDENCIES
oauth2 (~> 1.2.0)
octokit (~> 4.6.2)
oj (~> 2.17.4)
omniauth (~> 1.3.2)
omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.0)
omniauth-azure-oauth2 (~> 0.0.6)
......
......@@ -7,9 +7,7 @@
/* global Aside */
window.$ = window.jQuery = require('jquery');
require('jquery-ui/ui/autocomplete');
require('jquery-ui/ui/draggable');
require('jquery-ui/ui/effect-highlight');
require('jquery-ui/ui/sortable');
require('jquery-ujs');
require('vendor/jquery.endless-scroll');
......@@ -107,6 +105,7 @@ require('./droplab/droplab_filter');
require('./abuse_reports');
require('./activities');
require('./admin');
require('./ajax_loading_spinner');
require('./api');
require('./aside');
require('./autosave');
......
......@@ -123,14 +123,18 @@ class List {
if (listFrom) {
this.issuesSize += 1;
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
.then(() => {
listFrom.getIssues(false);
});
this.updateIssueLabel(issue, listFrom);
}
}
}
updateIssueLabel(issue, listFrom) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
.then(() => {
listFrom.getIssues(false);
});
}
findIssue (id) {
return this.issues.filter(issue => issue.id === id)[0];
}
......
......@@ -97,9 +97,12 @@
const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label);
// Add to new lists issues if it doesn't already exist
if (!issueTo) {
// Add to new lists issues if it doesn't already exist
listTo.addIssue(issue, listFrom, newIndex);
} else {
listTo.updateIssueLabel(issue, listFrom);
issueTo.removeLabel(listFrom.label);
}
if (listTo.type === 'done') {
......
......@@ -38,6 +38,7 @@
/* global AdminEmailSelect */
const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout');
(function() {
var Dispatcher;
......@@ -287,6 +288,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'ci:lints:show':
new gl.CILintEditor();
break;
case 'users:show':
new UserCallout();
break;
}
switch (path.first()) {
case 'sessions':
......@@ -326,6 +330,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'dashboard':
case 'root':
shortcut_handler = new ShortcutsDashboardNavigation();
new UserCallout();
break;
case 'profiles':
new NotificationsForm();
......
......@@ -78,7 +78,6 @@
} else {
$(element).find('.assignee-icon').empty();
}
return $(element).effect('highlight');
};
function Milestone() {
......
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
......@@ -20,15 +20,35 @@
};
NewBranchForm.prototype.init = function() {
if (this.name.val().length > 0) {
if (this.name.length && this.name.val().length > 0) {
return this.name.trigger('blur');
}
};
NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) {
return this.ref.autocomplete({
source: availableRefs,
minLength: 1
var $branchSelect = $('.js-branch-select');
$branchSelect.glDropdown({
data: availableRefs,
filterable: true,
filterByText: true,
remote: false,
fieldName: $branchSelect.data('field-name'),
selectable: true,
isSelectable: function(branch, $el) {
return !$el.hasClass('is-active');
},
text: function(branch) {
return branch;
},
id: function(branch) {
return branch;
},
toggleLabel: function(branch) {
if (branch) {
return branch;
}
}
});
};
......
// require everything else in this directory
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/));
require('./gl_crop');
require('./profile');
......@@ -93,6 +93,9 @@
$.scrollTo(0);
new Flash('Failed to update branch!');
}
}).always(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
});
}
......
// require everything else in this directory
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/));
require('./protected_branch_access_dropdown');
require('./protected_branch_create');
require('./protected_branch_dropdown');
require('./protected_branch_edit');
require('./protected_branch_edit_list');
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */
// require everything else in this directory
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/));
(function() {
$(function() {
var editor = ace.edit("editor");
......
/* global Cookies */
const userCalloutElementName = '.user-callout';
const closeButton = '.close-user-callout';
const userCalloutBtn = '.user-callout-btn';
const userCalloutSvgAttrName = 'callout-svg';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
const USER_CALLOUT_TEMPLATE = `
<div class="bordered-box landing content-block">
<button class="btn btn-default close close-user-callout" type="button">
<i class="fa fa-times dismiss-icon"></i>
</button>
<div class="row">
<div class="col-sm-3 col-xs-12 svg-container">
</div>
<div class="col-sm-8 col-xs-12 inner-content">
<h4>
Customize your experience
</h4>
<p>
Change syntax themes, default project pages, and more in preferences.
</p>
<a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
</div>
</div>
</div>`;
class UserCallout {
constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
this.userCalloutBody = $(userCalloutElementName);
this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
$(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
this.init();
}
init() {
const $template = $(USER_CALLOUT_TEMPLATE);
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
$template.find('.svg-container').append(this.userCalloutSvg);
this.userCalloutBody.append($template);
$template.find(closeButton).on('click', e => this.dismissCallout(e));
$template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
}
}
dismissCallout(e) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
const $currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('close-user-callout')) {
this.userCalloutBody.empty();
}
}
}
module.exports = UserCallout;
// require everything else in this directory
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/));
require('./calendar');
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */
/* eslint-disable no-param-reassign, no-alert */
((gl) => {
gl.VuePipelineActions = Vue.extend({
......@@ -16,6 +16,20 @@
download(name) {
return `Download ${name} artifacts`;
},
/**
* Shows a dialog when the user clicks in the cancel button.
* We need to prevent the default behavior and stop propagation because the
* link relies on UJS.
*
* @param {Event} event
*/
confirmAction(event) {
if (!confirm('Are you sure you want to cancel this pipeline?')) {
event.preventDefault();
event.stopPropagation();
}
},
},
template: `
<td class="pipeline-actions hidden-xs">
......@@ -87,6 +101,7 @@
</a>
<a
v-if='pipeline.flags.cancelable'
@click="confirmAction"
class="btn btn-remove has-tooltip"
title="Cancel"
rel="nofollow"
......
......@@ -2,7 +2,6 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope.
*= require jquery-ui/autocomplete
*= require jquery.atwho
*= require select2
*= require_self
......
......@@ -2,17 +2,6 @@
font-family: $regular_font;
font-size: $font-size-base;
&.ui-autocomplete {
border-color: $jq-ui-border;
padding: 0;
margin-top: 2px;
z-index: 1001;
.ui-menu-item a {
padding: 4px 10px;
}
}
.ui-state-default {
border: 1px solid $white-light;
background: $white-light;
......
......@@ -277,3 +277,41 @@ table.u2f-registrations {
padding-left: 18px;
}
}
.user-callout {
margin: 24px auto 0;
.bordered-box {
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.landing {
margin-bottom: $gl-padding;
.close {
margin-right: 20px;
}
.dismiss-icon {
float: right;
cursor: pointer;
color: $cycle-analytics-dismiss-icon-color;
}
.svg-container {
text-align: center;
svg {
width: 136px;
height: 136px;
}
}
}
@media(max-width: $screen-xs-max) {
.inner-content {
padding-left: 30px;
}
}
}
......@@ -36,7 +36,7 @@ class Event < ActiveRecord::Base
scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do
where(project_id: projects).recent
where(project_id: projects.pluck(:id)).recent
end
scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
......
......@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService
'This service sends notifications about projects events to Mattermost channels.<br />
To set up this service:
<ol>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li>
<li>Paste the webhook <strong>URL</strong> into the field bellow. </li>
<li>Select events below to enable notifications. The channel and username are optional. </li>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation.</li>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event.</li>
<li>Paste the webhook <strong>URL</strong> into the field below.</li>
<li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li>
</ol>'
end
......@@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel_placeholder
"town-square"
"Channel handle (e.g. town-square)"
end
end
......@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService
def help
'This service sends notifications about projects events to Slack channels.<br />
To setup this service:
To set up this service:
<ol>
<li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li>
<li>Paste the <strong>Webhook URL</strong> into the field below. </li>
<li>Select events below to enable notifications. The channel and username are optional. </li>
<li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications. The <strong>Channel name</strong> and <strong>Username</strong> fields are optional.</li>
</ol>'
end
......@@ -27,14 +27,14 @@ class SlackService < ChatNotificationService
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel_placeholder
"#general"
"Channel name (e.g. general)"
end
end
......@@ -116,9 +116,7 @@ class Repository
offset: offset,
after: after,
before: before,
# --follow doesn't play well with --skip. See:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
follow: false,
follow: path.present?,
skip_merges: skip_merges
}
......
......@@ -5,6 +5,8 @@
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
- if @projects.any? || params[:filter_projects]
= render 'dashboard/projects_head'
......
......@@ -101,5 +101,3 @@
$("#created-personal-access-token").click(function() {
this.select();
});
$("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
......@@ -12,12 +12,16 @@
.form-group
= label_tag :branch_name, nil, class: 'control-label'
.col-sm-10
= text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name'
= text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name'
.help-block.text-danger.js-branch-name-error
.form-group
= label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10
= text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control'
= hidden_field_tag :ref, params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide',
filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } })
.help-block Existing branch name, tag, or commit SHA
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
......
......@@ -9,7 +9,11 @@
.form-group
= f.label :ref, 'Create for', class: 'control-label'
.col-sm-10
= f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide',
filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
.help-block Existing branch name, tag
.form-actions
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112 90" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><rect width="112" height="90" fill="#fff" rx="6"/><path fill="#eee" fill-rule="nonzero" d="m4 6.01v77.98c0 1.11.899 2.01 2 2.01h100c1.105 0 2-.898 2-2.01v-77.98c0-1.11-.899-2.01-2-2.01h-100c-1.105 0-2 .898-2 2.01m-4 0c0-3.319 2.686-6.01 6-6.01h100c3.315 0 6 2.694 6 6.01v77.98c0 3.319-2.686 6.01-6 6.01h-100c-3.315 0-6-2.694-6-6.01v-77.98"/><g transform="translate(26 35)"><rect width="4" height="39" x="5" fill="#eee" rx="2" id="0"/><rect width="4" height="21" x="5" y="18" fill="#fef0ea" rx="2"/><circle cx="7" cy="13" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 20c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(49 35)"><use xlink:href="#0"/><rect width="4" height="21" x="5" y="18" fill="#b5a7dd" rx="2"/><circle cx="7" cy="25" r="5" fill="#fff"/><path fill="#6b4fbb" fill-rule="nonzero" d="m7 32c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(72 33)"><rect width="4" height="39" x="5" y="2" fill="#eee" rx="2"/><rect width="4" height="34" x="5" y="7" fill="#fef0ea" rx="2"/><circle cx="7" cy="7" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 14c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g fill="#6b4fbb"><circle cx="13.5" cy="11.5" r="2.5"/><circle cx="23.5" cy="11.5" r="2.5" opacity=".5"/><circle cx="33.5" cy="11.5" r="2.5" opacity=".5"/></g><path fill="#eee" d="m0 19h111v4h-111z"/></g></svg>
......@@ -98,6 +98,7 @@
Snippets
%div{ class: container_class }
.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
......
---
title: Make Git history follow renames again by performing the --skip in Ruby
merge_request:
author:
---
title: 'Add performance query regression fix for !9088 affecting #27267'
merge_request:
author:
---
title: Add Runner's registration/deletion v4 API
merge_request: 9246
author:
---
title: Removes label when moving issue to another list that it is currently in
merge_request:
author:
---
title: Removed jQuery UI highlight & autocomplete
merge_request:
author:
---
title: Bump Hashie to 3.5.5 and omniauth to 1.4.2 to eliminate warning noise
merge_request:
author:
---
title: 'API: Return 400 for all validation erros in the mebers API'
merge_request: 9523
author: Robert Schilling
---
title: update Vue to v2.1.10
merge_request: 9386
author:
---
title: Added user callouts to the projects dashboard and user profile
merge_request:
author:
......@@ -88,7 +88,7 @@ var config = {
'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap',
'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
'vue$': IS_PRODUCTION ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js',
'vue$': 'vue/dist/vue.common.js',
}
}
}
......
......@@ -42,5 +42,6 @@ changes are in V4:
- Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736)
- Remove the ProjectGitHook API. Use the ProjectPushRule API instead [!1301](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1301)
- Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384)
- Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523)
- Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505)
- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
\ No newline at end of file
- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
......@@ -1018,7 +1018,7 @@ A simple example:
```yaml
job1:
coverage: /Code coverage: \d+\.\d+/
coverage: '/Code coverage: \d+\.\d+/'
```
## Git Strategy
......
This diff is collapsed.
......@@ -24,23 +24,24 @@ There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
- Confidential issue
- Merge request
- Note
- Tag push
- Build
- Pipeline
- Wiki page
Bellow each of these event checkboxes, you will have an input field to insert
which Mattermost channel you want to send that event message, with `#town-square`
being the default. The hash sign is optional.
Below each of these event checkboxes, you have an input field to enter
which Mattermost channel you want to send that event message. Enter your preferred channel handle (the hash sign `#` is optional).
At the end, fill in your Mattermost details:
| Field | Description |
| ----- | ----------- |
| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... |
| **Webhook** | The incoming webhook URL which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo… |
| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
![Mattermost configuration](img/mattermost_configuration.png)
......@@ -21,23 +21,25 @@ There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
- Confidential issue
- Merge request
- Note
- Tag push
- Build
- Pipeline
- Wiki page
Bellow each of these event checkboxes, you will have an input field to insert
which Slack channel you want to send that event message, with `#general`
being the default. Enter your preferred channel **without** the hash sign (`#`).
Below each of these event checkboxes, you have an input field to enter
which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`).
At the end, fill in your Slack details:
| Field | Description |
| ----- | ----------- |
| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. |
| **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. |
| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
After you are all done, click **Save changes** for the changes to take effect.
......
......@@ -63,6 +63,12 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server
```
>**Note**: Make sure that `.gitattributes` is tracked by git. Otherwise Git
LFS will not be working properly for people cloning the project.
```bash
git add .gitattributes
```
Cloning the repository works the same as before. Git automatically detects the
LFS-tracked files and clones them via HTTP. If you performed the git clone
command with a SSH URL, you have to enter your GitLab credentials for HTTP
......
......@@ -13,6 +13,7 @@ Feature: Project Commits Branches
Given I visit project protected branches page
Then I should see "Shop" protected branches list
@javascript
Scenario: I create a branch
Given I visit project branches page
And I click new branch link
......@@ -33,12 +34,7 @@ Feature: Project Commits Branches
And I submit new branch form with invalid name
Then I should see new an error that branch is invalid
Scenario: I create a branch with invalid reference
Given I visit project branches page
And I click new branch link
And I submit new branch form with invalid reference
Then I should see new an error that ref is invalid
@javascript
Scenario: I create a branch that already exists
Given I visit project branches page
And I click new branch link
......
......@@ -34,25 +34,19 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step 'I submit new branch form' do
fill_in 'branch_name', with: 'deploy_keys'
fill_in 'ref', with: 'master'
select_branch('master')
click_button 'Create branch'
end
step 'I submit new branch form with invalid name' do
fill_in 'branch_name', with: '1.0 stable'
fill_in 'ref', with: 'master'
click_button 'Create branch'
end
step 'I submit new branch form with invalid reference' do
fill_in 'branch_name', with: 'foo'
fill_in 'ref', with: 'foo'
select_branch('master')
click_button 'Create branch'
end
step 'I submit new branch form with branch that already exists' do
fill_in 'branch_name', with: 'master'
fill_in 'ref', with: 'master'
select_branch('master')
click_button 'Create branch'
end
......@@ -65,10 +59,6 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
expect(page).to have_content "can't contain spaces"
end
step 'I should see new an error that ref is invalid' do
expect(page).to have_content 'Invalid reference name'
end
step 'I should see new an error that branch already exists' do
expect(page).to have_content 'Branch already exists'
end
......@@ -88,4 +78,12 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step "I should not see branch 'improve/awesome'" do
expect(page.all(visible: true)).not_to have_content 'improve/awesome'
end
def select_branch(branch_name)
click_button 'master'
page.within '#new-branch-form .dropdown-menu' do
click_link branch_name
end
end
end
......@@ -97,6 +97,7 @@ module API
mount ::API::Projects
mount ::API::ProjectSnippets
mount ::API::Repositories
mount ::API::Runner
mount ::API::Runners
mount ::API::Services
mount ::API::Session
......
......@@ -683,6 +683,10 @@ module API
end
end
class RunnerRegistrationDetails < Grape::Entity
expose :id, :token
end
class BuildArtifactFile < Grape::Entity
expose :filename, :size
end
......
module API
module Helpers
module Runner
def runner_registration_token_valid?
ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token],
current_application_settings.runners_registration_token)
end
def get_runner_version_from_params
return unless params['info'].present?
attributes_for_keys(%w(name version revision platform architecture), params['info'])
end
def authenticate_runner!
forbidden! unless current_runner
end
def current_runner
@runner ||= ::Ci::Runner.find_by_token(params[:token].to_s)
end
end
end
end
......@@ -61,7 +61,6 @@ module API
## EE specific
member = source.members.find_by(user_id: params[:user_id])
conflict!('Member already exists') if member
member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
......@@ -69,9 +68,6 @@ module API
if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
......@@ -93,9 +89,6 @@ module API
if member.update_attributes(declared_params(include_missing: false))
present member.user, with: Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
......
module API
class Runner < Grape::API
helpers ::API::Helpers::Runner
resource :runners do
desc 'Registers a new Runner' do
success Entities::RunnerRegistrationDetails
http_codes [[201, 'Runner was created'], [403, 'Forbidden']]
end
params do
requires :token, type: String, desc: 'Registration token'
optional :description, type: String, desc: %q(Runner's description)
optional :info, type: Hash, desc: %q(Runner's metadata)
optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
end
post '/' do
attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list]
runner =
if runner_registration_token_valid?
# Create shared runner. Requires admin access
Ci::Runner.create(attributes.merge(is_shared: true))
elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for project.
project.runners.create(attributes)
end
return forbidden! unless runner
if runner.id
runner.update(get_runner_version_from_params)
present runner, with: Entities::RunnerRegistrationDetails
else
not_found!
end
end
desc 'Deletes a registered Runner' do
http_codes [[200, 'Runner was deleted'], [403, 'Forbidden']]
end
params do
requires :token, type: String, desc: %q(Runner's authentication token)
end
delete '/' do
authenticate_runner!
Ci::Runner.find_by_token(params[:token]).destroy
end
end
end
end
......@@ -324,24 +324,30 @@ module Gitlab
end
def log_by_shell(sha, options)
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log)
cmd += %W(-n #{options[:limit].to_i})
cmd += %w(--format=%H)
cmd += %W(--skip=#{options[:offset].to_i})
cmd += %w(--follow) if options[:follow]
cmd += %w(--no-merges) if options[:skip_merges]
cmd += %W(--after=#{options[:after].iso8601}) if options[:after]
cmd += %W(--before=#{options[:before].iso8601}) if options[:before]
cmd += [sha]
cmd += %W(-- #{options[:path]}) if options[:path].present?
raw_output = IO.popen(cmd) {|io| io.read }
log = raw_output.lines.map do |c|
Rugged::Commit.new(rugged, c.strip)
end
log.is_a?(Array) ? log : []
limit = options[:limit].to_i
offset = options[:offset].to_i
use_follow_flag = options[:follow] && options[:path].present?
# We will perform the offset in Ruby because --follow doesn't play well with --skip.
# See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
offset_in_ruby = use_follow_flag && options[:offset].present?
limit += offset if offset_in_ruby
cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log]
cmd << "--max-count=#{limit}"
cmd << '--format=%H'
cmd << "--skip=#{offset}" unless offset_in_ruby
cmd << '--follow' if use_follow_flag
cmd << '--no-merges' if options[:skip_merges]
cmd << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << sha
cmd += %W[-- #{options[:path]}] if options[:path].present?
raw_output = IO.popen(cmd) { |io| io.read }
lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
end
def sha_from_ref(ref)
......
......@@ -371,8 +371,14 @@ describe 'Pipelines', :feature, :js do
visit new_namespace_project_pipeline_path(project.namespace, project)
end
context 'for valid commit' do
before { fill_in('pipeline[ref]', with: 'master') }
context 'for valid commit', js: true do
before do
click_button project.default_branch
page.within '.dropdown-menu' do
click_link 'master'
end
end
context 'with gitlab-ci.yml' do
before { stub_ci_pipeline_to_return_yaml_file }
......@@ -389,15 +395,6 @@ describe 'Pipelines', :feature, :js do
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
end
end
context 'for invalid commit' do
before do
fill_in('pipeline[ref]', with: 'invalid-reference')
click_on 'Create pipeline'
end
it { expect(page).to have_content('Reference not found') }
end
end
describe 'Create pipelines' do
......@@ -409,18 +406,22 @@ describe 'Pipelines', :feature, :js do
describe 'new pipeline page' do
it 'has field to add a new pipeline' do
expect(page).to have_field('pipeline[ref]')
expect(page).to have_selector('.js-branch-select')
expect(find('.js-branch-select')).to have_content project.default_branch
expect(page).to have_content('Create for')
end
end
describe 'find pipelines' do
it 'shows filtered pipelines', js: true do
fill_in('pipeline[ref]', with: 'fix')
find('input#ref').native.send_keys(:keydown)
click_button project.default_branch
within('.ui-autocomplete') do
expect(page).to have_selector('li', text: 'fix')
page.within '.dropdown-menu' do
find('.dropdown-input-field').native.send_keys('fix')
page.within '.dropdown-content' do
expect(page).to have_content('fix')
end
end
end
end
......
require 'spec_helper'
describe 'User Callouts', js: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') }
before do
login_as(user)
project.team << [user, :master]
end
it 'takes you to the profile preferences when the link is clicked' do
visit dashboard_projects_path
click_link 'Check it out'
expect(current_path).to eq profile_preferences_path
end
describe 'user callout should appear in two routes' do
it 'shows up on the user profile' do
visit user_path(user)
expect(find('.user-callout')).to have_content 'Customize your experience'
end
it 'shows up on the dashboard projects' do
visit dashboard_projects_path
expect(find('.user-callout')).to have_content 'Customize your experience'
end
end
it 'hides the user callout when click on the dismiss icon' do
visit user_path(user)
within('.user-callout') do
find('.close-user-callout').click
end
expect(page).not_to have_selector('#user-callout')
end
end
......@@ -3,7 +3,9 @@
/* global boardsMockInterceptor */
/* global BoardService */
/* global List */
/* global ListIssue */
/* global listObj */
/* global listObjDuplicate */
require('~/lib/utils/url_utility');
require('~/boards/models/issue');
......@@ -84,4 +86,23 @@ describe('List model', () => {
done();
}, 0);
});
it('sends service request to update issue label', () => {
const listDup = new List(listObjDuplicate);
const issue = new ListIssue({
title: 'Testing',
iid: 1,
confidential: false,
labels: [list.label, listDup.label]
});
list.issues.push(issue);
listDup.issues.push(issue);
spyOn(gl.boardService, 'moveIssue').and.callThrough();
listDup.updateIssueLabel(list, issue);
expect(gl.boardService.moveIssue).toHaveBeenCalledWith(issue.id, list.id, listDup.id);
});
});
.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
const UserCallout = require('~/user_callout');
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
const Cookie = window.Cookies;
describe('UserCallout', () => {
const fixtureName = 'static/user_callout.html.raw';
preloadFixtures(fixtureName);
beforeEach(function () {
loadFixtures(fixtureName);
this.userCallout = new UserCallout();
this.closeButton = $('.close-user-callout');
this.userCalloutBtn = $('.user-callout-btn');
this.userCalloutContainer = $('.user-callout');
Cookie.set(USER_CALLOUT_COOKIE, 'false');
});
afterEach(function () {
Cookie.set(USER_CALLOUT_COOKIE, 'false');
});
it('shows when cookie is set to false', function () {
expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined();
expect(this.userCalloutContainer.is(':visible')).toBe(true);
});
it('hides when user clicks on the dismiss-icon', function () {
this.closeButton.click();
expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true');
});
it('hides when user clicks on the "check it out" button', function () {
this.userCalloutBtn.click();
expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true');
});
});
......@@ -529,7 +529,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
commit_with_new_name = nil
rename_commit = nil
before(:all) do
before(:context) do
# Add new commits so that there's a renamed file in the commit history
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
......@@ -538,49 +538,119 @@ describe Gitlab::Git::Repository, seed_helper: true do
commit_with_new_name = new_commit_edit_new_file(repo)
end
after(:context) do
# Erase our commits so other tests get the original repo
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
end
context "where 'follow' == true" do
options = { ref: "master", follow: true }
let(:options) { { ref: "master", follow: true } }
context "and 'path' is a directory" do
let(:log_commits) do
repository.log(options.merge(path: "encoding"))
end
it "does not follow renames" do
log_commits = repository.log(options.merge(path: "encoding"))
it "should not follow renames" do
expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_old_name)
aggregate_failures do
expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_old_name)
end
end
end
context "and 'path' is a file that matches the new filename" do
let(:log_commits) do
repository.log(options.merge(path: "encoding/CHANGELOG"))
context 'without offset' do
it "follows renames" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG"))
aggregate_failures do
expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
end
it "should follow renames" do
expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
context 'with offset=1' do
it "follows renames and skip the latest commit" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1))
aggregate_failures do
expect(log_commits).not_to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
end
context 'with offset=1', 'and limit=1' do
it "follows renames, skip the latest commit and return only one commit" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1))
expect(log_commits).to contain_exactly(rename_commit)
end
end
context 'with offset=1', 'and limit=2' do
it "follows renames, skip the latest commit and return only two commits" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2))
aggregate_failures do
expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name)
end
end
end
context 'with offset=2' do
it "follows renames and skip the latest commit" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2))
aggregate_failures do
expect(log_commits).not_to include(commit_with_new_name)
expect(log_commits).not_to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
end
context 'with offset=2', 'and limit=1' do
it "follows renames, skip the two latest commit and return only one commit" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1))
expect(log_commits).to contain_exactly(commit_with_old_name)
end
end
context 'with offset=2', 'and limit=2' do
it "follows renames, skip the two latest commit and return only one commit" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2))
aggregate_failures do
expect(log_commits).not_to include(commit_with_new_name)
expect(log_commits).not_to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
end
end
context "and 'path' is a file that matches the old filename" do
let(:log_commits) do
repository.log(options.merge(path: "CHANGELOG"))
end
it "does not follow renames" do
log_commits = repository.log(options.merge(path: "CHANGELOG"))
it "should not follow renames" do
expect(log_commits).to include(commit_with_old_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_new_name)
aggregate_failures do
expect(log_commits).not_to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
end
context "unknown ref" do
let(:log_commits) { repository.log(options.merge(ref: 'unknown')) }
it "returns an empty array" do
log_commits = repository.log(options.merge(ref: 'unknown'))
it "should return empty" do
expect(log_commits).to eq([])
end
end
......@@ -699,12 +769,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
end
after(:all) do
# Erase our commits so other tests get the original repo
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
end
end
describe "#commits_between" do
......
......@@ -173,11 +173,11 @@ describe API::Members, api: true do
expect(response).to have_http_status(400)
end
it 'returns 422 when access_level is not valid' do
it 'returns 400 when access_level is not valid' do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id, access_level: 1234
expect(response).to have_http_status(422)
expect(response).to have_http_status(400)
end
end
end
......@@ -247,11 +247,11 @@ describe API::Members, api: true do
expect(response).to have_http_status(400)
end
it 'returns 422 when access level is not valid' do
it 'returns 400 when access level is not valid' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
access_level: 1234
expect(response).to have_http_status(422)
expect(response).to have_http_status(400)
end
end
end
......@@ -363,7 +363,7 @@ describe API::Members, api: true do
post api("/projects/#{project.id}/members", master),
user_id: stranger.id, access_level: Member::OWNER
expect(response).to have_http_status(422)
expect(response).to have_http_status(400)
end.to change { project.members.count }.by(0)
end
end
......
require 'spec_helper'
describe API::Runner do
include ApiHelpers
include StubGitlabCalls
let(:registration_token) { 'abcdefg123456' }
before do
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
end
describe '/api/v4/runners' do
describe 'POST /api/v4/runners' do
context 'when no token is provided' do
it 'returns 400 error' do
post api('/runners')
expect(response).to have_http_status 400
end
end
context 'when invalid token is provided' do
it 'returns 403 error' do
post api('/runners'), token: 'invalid'
expect(response).to have_http_status 403
end
end
context 'when valid token is provided' do
it 'creates runner with default values' do
post api('/runners'), token: registration_token
runner = Ci::Runner.first
expect(response).to have_http_status 201
expect(json_response['id']).to eq(runner.id)
expect(json_response['token']).to eq(runner.token)
expect(runner.run_untagged).to be true
end
context 'when project token is used' do
let(:project) { create(:empty_project) }
it 'creates runner' do
post api('/runners'), token: project.runners_token
expect(response).to have_http_status 201
expect(project.runners.size).to eq(1)
end
end
end
context 'when runner description is provided' do
it 'creates runner' do
post api('/runners'), token: registration_token,
description: 'server.hostname'
expect(response).to have_http_status 201
expect(Ci::Runner.first.description).to eq('server.hostname')
end
end
context 'when runner tags are provided' do
it 'creates runner' do
post api('/runners'), token: registration_token,
tag_list: 'tag1, tag2'
expect(response).to have_http_status 201
expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
end
end
context 'when option for running untagged jobs is provided' do
context 'when tags are provided' do
it 'creates runner' do
post api('/runners'), token: registration_token,
run_untagged: false,
tag_list: ['tag']
expect(response).to have_http_status 201
expect(Ci::Runner.first.run_untagged).to be false
expect(Ci::Runner.first.tag_list.sort).to eq(['tag'])
end
end
context 'when tags are not provided' do
it 'returns 404 error' do
post api('/runners'), token: registration_token,
run_untagged: false
expect(response).to have_http_status 404
end
end
end
context 'when option for locking Runner is provided' do
it 'creates runner' do
post api('/runners'), token: registration_token,
locked: true
expect(response).to have_http_status 201
expect(Ci::Runner.first.locked).to be true
end
end
%w(name version revision platform architecture).each do |param|
context "when info parameter '#{param}' info is present" do
let(:value) { "#{param}_value" }
it %q(updates provided Runner's parameter) do
post api('/runners'), token: registration_token,
info: { param => value }
expect(response).to have_http_status 201
expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
end
end
end
end
describe 'DELETE /api/v4/runners' do
context 'when no token is provided' do
it 'returns 400 error' do
delete api('/runners')
expect(response).to have_http_status 400
end
end
context 'when invalid token is provided' do
it 'returns 403 error' do
delete api('/runners'), token: 'invalid'
expect(response).to have_http_status 403
end
end
context 'when valid token is provided' do
let(:runner) { create(:ci_runner) }
it 'deletes Runner' do
delete api('/runners'), token: runner.token
expect(response).to have_http_status 200
expect(Ci::Runner.count).to eq(0)
end
end
end
end
end
......@@ -4410,9 +4410,9 @@ vue-resource@^0.9.3:
version "0.9.3"
resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d"
vue@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.0.3.tgz#3f7698f83d6ad1f0e35955447901672876c63fde"
vue@^2.1.10:
version "2.1.10"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b"
watchpack@^1.2.0:
version "1.2.1"
......
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