Commit 7b3e7150 authored by Sean Packham's avatar Sean Packham

Merge branch 'master' into add-university-content

parents 1feffcee 23ed83a1
...@@ -12,7 +12,7 @@ variables: ...@@ -12,7 +12,7 @@ variables:
RSPEC_RETRY_RETRY_COUNT: "3" RSPEC_RETRY_RETRY_COUNT: "3"
RAILS_ENV: "test" RAILS_ENV: "test"
SIMPLECOV: "true" SIMPLECOV: "true"
USE_DB: "true" SETUP_DB: "true"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20" GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1" PHANTOMJS_VERSION: "2.1.1"
...@@ -23,7 +23,7 @@ before_script: ...@@ -23,7 +23,7 @@ before_script:
- bundle --version - bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack - retry gem install knapsack
- '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
stages: stages:
- prepare - prepare
...@@ -35,7 +35,7 @@ stages: ...@@ -35,7 +35,7 @@ stages:
.knapsack-state: &knapsack-state .knapsack-state: &knapsack-state
services: [] services: []
variables: variables:
USE_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
cache: cache:
key: "knapsack" key: "knapsack"
...@@ -196,7 +196,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21 ...@@ -196,7 +196,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
.ruby-static-analysis: &ruby-static-analysis .ruby-static-analysis: &ruby-static-analysis
variables: variables:
SIMPLECOV: "false" SIMPLECOV: "false"
USE_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
.exec: &exec .exec: &exec
...@@ -224,6 +224,23 @@ rake db:migrate:reset: ...@@ -224,6 +224,23 @@ rake db:migrate:reset:
script: script:
- rake db:migrate:reset - rake db:migrate:reset
rake db:seed_fu:
stage: test
<<: *use-db
variables:
SIZE: "1"
SETUP_DB: "false"
RAILS_ENV: "development"
script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
- bundle exec rake db:setup db:seed_fu
artifacts:
when: on_failure
expire_in: 1d
paths:
- log/development.log
teaspoon: teaspoon:
stage: test stage: test
<<: *use-db <<: *use-db
...@@ -272,7 +289,7 @@ coverage: ...@@ -272,7 +289,7 @@ coverage:
stage: post-test stage: post-test
services: [] services: []
variables: variables:
USE_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
script: script:
- bundle exec scripts/merge-simplecov - bundle exec scripts/merge-simplecov
...@@ -288,7 +305,7 @@ coverage: ...@@ -288,7 +305,7 @@ coverage:
notify:slack: notify:slack:
stage: post-test stage: post-test
variables: variables:
USE_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
script: script:
- ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>" - ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.12.0 (unreleased) v 8.13.0 (unreleased)
- Speed-up group milestones show page
v 8.12.2 (unreleased)
v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
- Fix issue with search filter labels not displaying
v 8.12.0
- Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251 - Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
- Only check :can_resolve permission if the note is resolvable - Only check :can_resolve permission if the note is resolvable
- Bump fog-aws to v0.11.0 to support ap-south-1 region - Bump fog-aws to v0.11.0 to support ap-south-1 region
...@@ -81,6 +90,7 @@ v 8.12.0 (unreleased) ...@@ -81,6 +90,7 @@ v 8.12.0 (unreleased)
- Fix markdown anchor icon interaction (ClemMakesApps) - Fix markdown anchor icon interaction (ClemMakesApps)
- Test migration paths from 8.5 until current release !4874 - Test migration paths from 8.5 until current release !4874
- Replace animateEmoji timeout with eventListener (ClemMakesApps) - Replace animateEmoji timeout with eventListener (ClemMakesApps)
- Show badges in Milestone tabs. !5946 (Dan Rowden)
- Optimistic locking for Issues and Merge Requests (title and description overriding prevention) - Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
- Require confirmation when not logged in for unsubscribe links !6223 (Maximiliano Perez Coto) - Require confirmation when not logged in for unsubscribe links !6223 (Maximiliano Perez Coto)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel) - Add `wiki_page_events` to project hook APIs (Ben Boeckel)
......
...@@ -322,10 +322,6 @@ group :test do ...@@ -322,10 +322,6 @@ group :test do
gem 'timecop', '~> 0.8.0' gem 'timecop', '~> 0.8.0'
end end
group :production do
gem 'gitlab_meta', '7.0'
end
gem 'newrelic_rpm', '~> 3.16' gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.3.0' gem 'octokit', '~> 4.3.0'
......
...@@ -284,7 +284,6 @@ GEM ...@@ -284,7 +284,6 @@ GEM
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
rugged (~> 0.24.0) rugged (~> 0.24.0)
gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1) gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9) net-ldap (~> 0.9)
omniauth (~> 1.0) omniauth (~> 1.0)
...@@ -864,7 +863,6 @@ DEPENDENCIES ...@@ -864,7 +863,6 @@ DEPENDENCIES
github-markup (~> 1.4) github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_git (~> 10.6.6) gitlab_git (~> 10.6.6)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2) gollum-rugged_adapter (~> 0.4.2)
...@@ -991,4 +989,4 @@ DEPENDENCIES ...@@ -991,4 +989,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.13.0 1.13.1
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
## Canonical source ## Canonical source
The cannonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
## Open source software to collaborate on code ## Open source software to collaborate on code
......
((global) => { ((global) => {
const COOKIE_NAME = 'cycle_analytics_help_dismissed'; const COOKIE_NAME = 'cycle_analytics_help_dismissed';
const store = gl.cycleAnalyticsStore = {
isLoading: true,
hasError: false,
isHelpDismissed: $.cookie(COOKIE_NAME),
analytics: {}
};
gl.CycleAnalytics = class CycleAnalytics { gl.CycleAnalytics = class CycleAnalytics {
constructor() { constructor() {
const that = this; const that = this;
this.isHelpDismissed = $.cookie(COOKIE_NAME);
this.vue = new Vue({ this.vue = new Vue({
el: '#cycle-analytics', el: '#cycle-analytics',
name: 'CycleAnalytics', name: 'CycleAnalytics',
created: this.fetchData(), created: this.fetchData(),
data: this.decorateData({ isLoading: true }), data: store,
methods: { methods: {
dismissLanding() { dismissLanding() {
that.dismissLanding(); that.dismissLanding();
...@@ -21,6 +26,7 @@ ...@@ -21,6 +26,7 @@
} }
fetchData(options) { fetchData(options) {
store.isLoading = true;
options = options || { startDate: 30 }; options = options || { startDate: 30 };
$.ajax({ $.ajax({
...@@ -30,22 +36,20 @@ ...@@ -30,22 +36,20 @@
contentType: 'application/json', contentType: 'application/json',
data: { start_date: options.startDate } data: { start_date: options.startDate }
}).done((data) => { }).done((data) => {
this.vue.$data = this.decorateData(data); this.decorateData(data);
this.initDropdown(); this.initDropdown();
}) })
.error((data) => { .error((data) => {
this.handleError(data); this.handleError(data);
}) })
.always(() => { .always(() => {
this.vue.isLoading = false; store.isLoading = false;
}) })
} }
decorateData(data) { decorateData(data) {
data.summary = data.summary || []; data.summary = data.summary || [];
data.stats = data.stats || []; data.stats = data.stats || [];
data.isHelpDismissed = this.isHelpDismissed;
data.isLoading = data.isLoading || false;
data.summary.forEach((item) => { data.summary.forEach((item) => {
item.value = item.value || '-'; item.value = item.value || '-';
...@@ -53,23 +57,21 @@ ...@@ -53,23 +57,21 @@
data.stats.forEach((item) => { data.stats.forEach((item) => {
item.value = item.value || '- - -'; item.value = item.value || '- - -';
}) });
return data; store.analytics = data;
} }
handleError(data) { handleError(data) {
this.vue.$data = { store.hasError = true;
hasError: true,
isHelpDismissed: this.isHelpDismissed
};
new Flash('There was an error while fetching cycle analytics data.', 'alert'); new Flash('There was an error while fetching cycle analytics data.', 'alert');
} }
dismissLanding() { dismissLanding() {
this.vue.isHelpDismissed = true; store.isHelpDismissed = true;
$.cookie(COOKIE_NAME, true); $.cookie(COOKIE_NAME, true, {
path: gon.relative_url_root || '/'
});
} }
initDropdown() { initDropdown() {
...@@ -82,7 +84,6 @@ ...@@ -82,7 +84,6 @@
const value = $target.data('value'); const value = $target.data('value');
$label.text($target.text().trim()); $label.text($target.text().trim());
this.vue.isLoading = true;
this.fetchData({ startDate: value }); this.fetchData({ startDate: value });
}) })
} }
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
margin: 0; margin: 0;
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
font-size: 14px; font-size: 14px;
z-index: 100;
.flash-notice { .flash-notice {
@extend .alert; @extend .alert;
...@@ -41,4 +40,3 @@ ...@@ -41,4 +40,3 @@
} }
} }
} }
...@@ -269,6 +269,12 @@ $calendar-hover-bg: #ecf3fe; ...@@ -269,6 +269,12 @@ $calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1); $calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: $gray-light; $calendar-unselectable-bg: $gray-light;
/*
* Cycle Analytics
*/
$cycle-analytics-box-padding: 30px;
$cycle-analytics-box-text-color: #8c8c8c;
/* /*
* Personal Access Tokens * Personal Access Tokens
*/ */
......
#cycle-analytics { #cycle-analytics {
margin: 24px auto 0; margin: 24px auto 0;
width: 800px; max-width: 800px;
position: relative; position: relative;
.panel { .panel {
...@@ -9,10 +9,18 @@ ...@@ -9,10 +9,18 @@
padding: 24px 0; padding: 24px 0;
border-bottom: none; border-bottom: none;
position: relative; position: relative;
@media (max-width: $screen-sm-min) {
padding: 6px 0 24px;
}
} }
.column { .column {
text-align: center; text-align: center;
@media (max-width: $screen-sm-min) {
padding: 15px 0;
}
.header { .header {
font-size: 30px; font-size: 30px;
...@@ -28,11 +36,14 @@ ...@@ -28,11 +36,14 @@
&:last-child { &:last-child {
text-align: right; text-align: right;
@media (max-width: $screen-sm-min) {
text-align: center;
}
} }
} }
.dropdown { .dropdown {
position: relative;
top: 13px; top: 13px;
} }
} }
...@@ -40,7 +51,7 @@ ...@@ -40,7 +51,7 @@
.bordered-box { .bordered-box {
border: 1px solid $border-color; border: 1px solid $border-color;
@include border-radius($border-radius-default); @include border-radius($border-radius-default);
position: relative;
} }
.content-list { .content-list {
...@@ -60,9 +71,15 @@ ...@@ -60,9 +71,15 @@
line-height: 19px; line-height: 19px;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: $gl-title-color;
} }
&:text {
color: #8c8c8c; &.text {
color: $layout-link-gray;
&.value-col {
color: $gl-title-color;
}
} }
} }
} }
...@@ -71,7 +88,9 @@ ...@@ -71,7 +88,9 @@
text-align: right; text-align: right;
span { span {
line-height: 42px; position: relative;
vertical-align: middle;
top: 3px;
} }
} }
} }
...@@ -82,21 +101,25 @@ ...@@ -82,21 +101,25 @@
.dismiss-icon { .dismiss-icon {
position: absolute; position: absolute;
right: $gl-padding; right: $cycle-analytics-box-padding;
cursor: pointer; cursor: pointer;
color: #b2b2b2; color: #b2b2b2;
} }
svg { .svg-container {
margin: 0 20px; text-align: center;
float: left;
width: 136px; svg {
height: 136px; width: 136px;
height: 136px;
}
} }
.inner-content { .inner-content {
width: 480px; @media (max-width: $screen-sm-min) {
float: left; padding: 0 28px;
text-align: center;
}
h4 { h4 {
color: $gl-text-color; color: $gl-text-color;
...@@ -104,7 +127,7 @@ ...@@ -104,7 +127,7 @@
} }
p { p {
color: #8c8c8c; color: $cycle-analytics-box-text-color;
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
} }
} }
......
...@@ -2,13 +2,17 @@ ...@@ -2,13 +2,17 @@
max-width: 90%; max-width: 90%;
} }
li.milestone { .milestones {
h4 { .milestone {
font-weight: bold; padding: 10px 16px;
}
h4 {
font-weight: bold;
}
.progress { .progress {
height: 6px; height: 6px;
}
} }
} }
...@@ -64,3 +68,14 @@ li.milestone { ...@@ -64,3 +68,14 @@ li.milestone {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
padding: 20px 0; padding: 20px 0;
} }
@media (max-width: $screen-sm-min) {
.milestone-actions {
@include clearfix();
padding-top: $gl-vert-padding;
.btn:first-child {
margin-left: 0;
}
}
}
...@@ -177,6 +177,10 @@ ...@@ -177,6 +177,10 @@
border-bottom: 2px solid $border-color; border-bottom: 2px solid $border-color;
} }
} }
a {
display: block;
}
} }
} }
......
...@@ -35,6 +35,30 @@ module MilestonesHelper ...@@ -35,6 +35,30 @@ module MilestonesHelper
milestone.issues.with_label(label.title).send(state).size milestone.issues.with_label(label.title).send(state).size
end end
# Returns count of milestones for different states
# Uses explicit hash keys as the 'opened' state URL params differs from the db value
# and we need to add the total
def milestone_counts(milestones)
counts = milestones.reorder(nil).group(:state).count
{
opened: counts['active'] || 0,
closed: counts['closed'] || 0,
all: counts.values.sum || 0
}
end
# Show 'active' class if provided GET param matches check
# `or_blank` allows the function to return 'active' when given an empty param
# Could be refactored to be simpler but that may make it harder to read
def milestone_class_for_state(param, check, match_blank_param = false)
if match_blank_param
'active' if param.blank? || param == check
else
'active' if param == check
end
end
def milestone_progress_bar(milestone) def milestone_progress_bar(milestone)
options = { options = {
class: 'progress-bar progress-bar-success', class: 'progress-bar progress-bar-success',
......
...@@ -91,7 +91,7 @@ module Ci ...@@ -91,7 +91,7 @@ module Ci
sha: build.sha, sha: build.sha,
ref: build.ref, ref: build.ref,
tag: build.tag, tag: build.tag,
options: build.options[:environment], options: build.options.to_h[:environment],
variables: build.variables) variables: build.variables)
service.execute(build) service.execute(build)
end end
......
...@@ -8,7 +8,8 @@ class GlobalMilestone ...@@ -8,7 +8,8 @@ class GlobalMilestone
milestones = milestones.group_by(&:title) milestones = milestones.group_by(&:title)
milestones.map do |title, milestones| milestones.map do |title, milestones|
new(title, milestones) milestones_relation = Milestone.where(id: milestones.map(&:id))
new(title, milestones_relation)
end end
end end
...@@ -31,7 +32,7 @@ class GlobalMilestone ...@@ -31,7 +32,7 @@ class GlobalMilestone
end end
def projects def projects
@projects ||= Project.for_milestones(milestones.map(&:id)) @projects ||= Project.for_milestones(milestones.select(:id))
end end
def state def state
...@@ -53,19 +54,19 @@ class GlobalMilestone ...@@ -53,19 +54,19 @@ class GlobalMilestone
end end
def issues def issues
@issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project) @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels)
end end
def merge_requests def merge_requests
@merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project) @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels)
end end
def participants def participants
@participants ||= milestones.map(&:participants).flatten.compact.uniq @participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq
end end
def labels def labels
@labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten) @labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten)
.sort_by!(&:title) .sort_by!(&:title)
end end
......
...@@ -2,18 +2,20 @@ ...@@ -2,18 +2,20 @@
- page_title "Cycle Analytics" - page_title "Cycle Analytics"
= render "projects/pipelines/head" = render "projects/pipelines/head"
#cycle-analytics{"v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}} #cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}}
.bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"} .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
= icon('times', class: 'dismiss-icon', "@click": "dismissLanding()") = icon('times', class: 'dismiss-icon', "@click": "dismissLanding()")
= custom_icon('icon_cycle_analytics_splash') .row
.inner-content .col-sm-3.col-xs-12.svg-container
%h4 = custom_icon('icon_cycle_analytics_splash')
Introducing Cycle Analytics .col-sm-8.col-xs-12.inner-content
%p %h4
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. Introducing Cycle Analytics
%p
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
= link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
= icon("spinner spin", "v-show" => "isLoading") = icon("spinner spin", "v-show" => "isLoading")
...@@ -25,11 +27,11 @@ ...@@ -25,11 +27,11 @@
.content-block .content-block
.container-fluid .container-fluid
.row .row
.col-xs-3.column{"v-for" => "item in summary"} .col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"}
%h3.header {{item.value}} %h3.header {{item.value}}
%p.text {{item.title}} %p.text {{item.title}}
.col-xs-3.column .col-sm-3.col-xs-12.column
.dropdown.inline.js-ca-dropdown .dropdown.inline.js-ca-dropdown
%button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"} %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
%span.dropdown-label Last 30 days %span.dropdown-label Last 30 days
...@@ -44,14 +46,14 @@ ...@@ -44,14 +46,14 @@
.bordered-box .bordered-box
%ul.content-list %ul.content-list
%li{"v-for" => "item in stats"} %li{"v-for" => "item in analytics.stats"}
.container-fluid .container-fluid
.row .row
.col-xs-10.title-col .col-xs-8.title-col
%p.title %p.title
{{item.title}} {{item.title}}
%p.text %p.text
{{item.description}} {{item.description}}
.col-xs-2.value-col .col-xs-4.value-col
%span %span
{{item.value}} {{item.value}}
- if @project
- counts = milestone_counts(@project.milestones)
%ul.nav-links %ul.nav-links
%li{class: ("active" if params[:state].blank? || params[:state] == 'opened')} %li{class: milestone_class_for_state(params[:state], 'opened', true)}
= link_to milestones_filter_path(state: 'opened') do = link_to milestones_filter_path(state: 'opened') do
Open Open
%li{class: ("active" if params[:state] == 'closed')} - if @project
%span.badge #{counts[:opened]}
%li{class: milestone_class_for_state(params[:state], 'closed')}
= link_to milestones_filter_path(state: 'closed') do = link_to milestones_filter_path(state: 'closed') do
Closed Closed
%li{class: ("active" if params[:state] == 'all')} - if @project
%span.badge #{counts[:closed]}
%li{class: milestone_class_for_state(params[:state], 'all')}
= link_to milestones_filter_path(state: 'all') do = link_to milestones_filter_path(state: 'all') do
All All
- if @project
%span.badge #{counts[:all]}
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
- if @project - if @project
.row .row
.col-sm-6= render('shared/milestone_expired', milestone: milestone) .col-sm-6= render('shared/milestone_expired', milestone: milestone)
.col-sm-6 .col-sm-6.milestone-actions
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
= link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do
Edit Edit
......
...@@ -3,11 +3,11 @@ require 'sidekiq/testing' ...@@ -3,11 +3,11 @@ require 'sidekiq/testing'
Sidekiq::Testing.inline! do Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do Gitlab::Seeder.quiet do
project_urls = [ project_urls = [
'https://github.com/documentcloud/underscore.git', 'https://gitlab.com/gitlab-org/gitlab-test.git',
'https://gitlab.com/gitlab-org/gitlab-ce.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git',
'https://gitlab.com/gitlab-org/gitlab-ci.git', 'https://gitlab.com/gitlab-org/gitlab-ci.git',
'https://gitlab.com/gitlab-org/gitlab-shell.git', 'https://gitlab.com/gitlab-org/gitlab-shell.git',
'https://gitlab.com/gitlab-org/gitlab-test.git', 'https://github.com/documentcloud/underscore.git',
'https://github.com/twitter/flight.git', 'https://github.com/twitter/flight.git',
'https://github.com/twitter/typeahead.js.git', 'https://github.com/twitter/typeahead.js.git',
'https://github.com/h5bp/html5-boilerplate.git', 'https://github.com/h5bp/html5-boilerplate.git',
...@@ -38,12 +38,7 @@ Sidekiq::Testing.inline! do ...@@ -38,12 +38,7 @@ Sidekiq::Testing.inline! do
] ]
# You can specify how many projects you need during seed execution # You can specify how many projects you need during seed execution
size = if ENV['SIZE'].present? size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8
ENV['SIZE'].to_i
else
8
end
project_urls.first(size).each_with_index do |url, i| project_urls.first(size).each_with_index do |url, i|
group_path, project_path = url.split('/')[-2..-1] group_path, project_path = url.split('/')[-2..-1]
......
class AddIndexToLabelsTitle < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
add_concurrent_index :labels, :title
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160915042921) do ActiveRecord::Schema.define(version: 20160920160832) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -521,6 +521,7 @@ ActiveRecord::Schema.define(version: 20160915042921) do ...@@ -521,6 +521,7 @@ ActiveRecord::Schema.define(version: 20160915042921) do
add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
create_table "lfs_objects", force: :cascade do |t| create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false t.string "oid", null: false
......
...@@ -10,6 +10,7 @@ following locations: ...@@ -10,6 +10,7 @@ following locations:
- [Award Emoji](award_emoji.md) - [Award Emoji](award_emoji.md)
- [Branches](branches.md) - [Branches](branches.md)
- [Broadcast Messages](broadcast_messages.md)
- [Builds](builds.md) - [Builds](builds.md)
- [Build Triggers](build_triggers.md) - [Build Triggers](build_triggers.md)
- [Build Variables](build_variables.md) - [Build Variables](build_variables.md)
......
...@@ -13,7 +13,7 @@ Apart from those, here is an collection of tutorials and guides on setting up yo ...@@ -13,7 +13,7 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [Test a Scala application](test-scala-application.md) - [Test a Scala application](test-scala-application.md)
- [Using `dpl` as deployment tool](deployment/README.md) - [Using `dpl` as deployment tool](deployment/README.md)
- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) - [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples) - [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) - [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml [gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
...@@ -44,7 +44,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`. ...@@ -44,7 +44,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | | **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run | | **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | | **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returnes the address of the registry tied to the specific project | | **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used | | **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab | | **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags | | **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
......
...@@ -7,7 +7,7 @@ module Banzai ...@@ -7,7 +7,7 @@ module Banzai
UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
def whitelist def whitelist
whitelist = super.dup whitelist = super
customize_whitelist(whitelist) customize_whitelist(whitelist)
...@@ -42,58 +42,58 @@ module Banzai ...@@ -42,58 +42,58 @@ module Banzai
# Allow any protocol in `a` elements... # Allow any protocol in `a` elements...
whitelist[:protocols].delete('a') whitelist[:protocols].delete('a')
whitelist[:transformers] = whitelist[:transformers].dup
# ...but then remove links with unsafe protocols # ...but then remove links with unsafe protocols
whitelist[:transformers].push(remove_unsafe_links) whitelist[:transformers].push(self.class.remove_unsafe_links)
# Remove `rel` attribute from `a` elements # Remove `rel` attribute from `a` elements
whitelist[:transformers].push(remove_rel) whitelist[:transformers].push(self.class.remove_rel)
# Remove `class` attribute from non-highlight spans # Remove `class` attribute from non-highlight spans
whitelist[:transformers].push(clean_spans) whitelist[:transformers].push(self.class.clean_spans)
whitelist whitelist
end end
def remove_unsafe_links class << self
lambda do |env| def remove_unsafe_links
node = env[:node] lambda do |env|
node = env[:node]
return unless node.name == 'a' return unless node.name == 'a'
return unless node.has_attribute?('href') return unless node.has_attribute?('href')
begin begin
uri = Addressable::URI.parse(node['href']) uri = Addressable::URI.parse(node['href'])
uri.scheme = uri.scheme.strip.downcase if uri.scheme uri.scheme = uri.scheme.strip.downcase if uri.scheme
node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme) node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
node.remove_attribute('href') node.remove_attribute('href')
end
end end
end end
end
def remove_rel def remove_rel
lambda do |env| lambda do |env|
if env[:node_name] == 'a' if env[:node_name] == 'a'
env[:node].remove_attribute('rel') env[:node].remove_attribute('rel')
end
end end
end end
end
def clean_spans def clean_spans
lambda do |env| lambda do |env|
node = env[:node] node = env[:node]
return unless node.name == 'span' return unless node.name == 'span'
return unless node.has_attribute?('class') return unless node.has_attribute?('class')
unless has_ancestor?(node, 'pre') unless node.ancestors.any? { |n| n.name.casecmp('pre').zero? }
node.remove_attribute('class') node.remove_attribute('class')
end end
{ node_whitelist: [node] } { node_whitelist: [node] }
end
end end
end end
end end
......
...@@ -59,10 +59,8 @@ module Gitlab ...@@ -59,10 +59,8 @@ module Gitlab
# When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
active_db_connection = ActiveRecord::Base.connection.active? rescue false active_db_connection = ActiveRecord::Base.connection.active? rescue false
ENV['USE_DB'] != 'false' &&
active_db_connection && active_db_connection &&
ActiveRecord::Base.connection.table_exists?('application_settings') ActiveRecord::Base.connection.table_exists?('application_settings')
rescue ActiveRecord::NoDatabaseError rescue ActiveRecord::NoDatabaseError
false false
end end
......
...@@ -9,19 +9,22 @@ module Gitlab ...@@ -9,19 +9,22 @@ module Gitlab
SIDEKIQ_NAMESPACE = 'resque:gitlab' SIDEKIQ_NAMESPACE = 'resque:gitlab'
MAILROOM_NAMESPACE = 'mail_room:gitlab' MAILROOM_NAMESPACE = 'mail_room:gitlab'
DEFAULT_REDIS_URL = 'redis://localhost:6379' DEFAULT_REDIS_URL = 'redis://localhost:6379'
CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
# To be thread-safe we must be careful when writing the class instance # To be thread-safe we must be careful when writing the class instance
# variables @url and @pool. Because @pool depends on @url we need two # variables @_raw_config and @pool. Because @pool depends on @_raw_config we need two
# mutexes to prevent deadlock. # mutexes to prevent deadlock.
PARAMS_MUTEX = Mutex.new RAW_CONFIG_MUTEX = Mutex.new
POOL_MUTEX = Mutex.new POOL_MUTEX = Mutex.new
private_constant :PARAMS_MUTEX, :POOL_MUTEX private_constant :RAW_CONFIG_MUTEX, :POOL_MUTEX
class << self class << self
# Do NOT cache in an instance variable. Result may be mutated by caller.
def params def params
@params || PARAMS_MUTEX.synchronize { @params = new.params } new.params
end end
# Do NOT cache in an instance variable. Result may be mutated by caller.
# @deprecated Use .params instead to get sentinel support # @deprecated Use .params instead to get sentinel support
def url def url
new.url new.url
...@@ -36,8 +39,17 @@ module Gitlab ...@@ -36,8 +39,17 @@ module Gitlab
@pool.with { |redis| yield redis } @pool.with { |redis| yield redis }
end end
def reset_params! def _raw_config
@params = nil return @_raw_config if defined?(@_raw_config)
RAW_CONFIG_MUTEX.synchronize do
begin
@_raw_config = File.read(CONFIG_FILE).freeze
rescue Errno::ENOENT
@_raw_config = false
end
end
@_raw_config
end end
end end
...@@ -83,12 +95,7 @@ module Gitlab ...@@ -83,12 +95,7 @@ module Gitlab
end end
def fetch_config def fetch_config
file = config_file self.class._raw_config ? YAML.load(self.class._raw_config)[@rails_env] : false
File.exist?(file) ? YAML.load_file(file)[@rails_env] : false
end
def config_file
File.expand_path('../../../config/resque.yml', __FILE__)
end end
end end
end end
...@@ -60,7 +60,7 @@ module Gitlab ...@@ -60,7 +60,7 @@ module Gitlab
def send_git_diff(repository, diff_refs) def send_git_diff(repository, diff_refs)
params = { params = {
'RepoPath' => repository.path_to_repo, 'RepoPath' => repository.path_to_repo,
'ShaFrom' => diff_refs.start_sha, 'ShaFrom' => diff_refs.base_sha,
'ShaTo' => diff_refs.head_sha 'ShaTo' => diff_refs.head_sha
} }
...@@ -73,7 +73,7 @@ module Gitlab ...@@ -73,7 +73,7 @@ module Gitlab
def send_git_patch(repository, diff_refs) def send_git_patch(repository, diff_refs)
params = { params = {
'RepoPath' => repository.path_to_repo, 'RepoPath' => repository.path_to_repo,
'ShaFrom' => diff_refs.start_sha, 'ShaFrom' => diff_refs.base_sha,
'ShaTo' => diff_refs.head_sha 'ShaTo' => diff_refs.head_sha
} }
...@@ -107,15 +107,15 @@ module Gitlab ...@@ -107,15 +107,15 @@ module Gitlab
bytes bytes
end end
end end
def write_secret def write_secret
bytes = SecureRandom.random_bytes(SECRET_LENGTH) bytes = SecureRandom.random_bytes(SECRET_LENGTH)
File.open(secret_path, 'w:BINARY', 0600) do |f| File.open(secret_path, 'w:BINARY', 0600) do |f|
f.chmod(0600) f.chmod(0600)
f.write(Base64.strict_encode64(bytes)) f.write(Base64.strict_encode64(bytes))
end end
end end
def verify_api_request!(request_headers) def verify_api_request!(request_headers)
JWT.decode( JWT.decode(
request_headers[INTERNAL_API_REQUEST_HEADER], request_headers[INTERNAL_API_REQUEST_HEADER],
...@@ -128,7 +128,7 @@ module Gitlab ...@@ -128,7 +128,7 @@ module Gitlab
def secret_path def secret_path
Rails.root.join('.gitlab_workhorse_secret') Rails.root.join('.gitlab_workhorse_secret')
end end
protected protected
def encode(hash) def encode(hash)
......
...@@ -3,10 +3,15 @@ FactoryGirl.define do ...@@ -3,10 +3,15 @@ FactoryGirl.define do
title title
project project
trait :active do
state "active"
end
trait :closed do trait :closed do
state :closed state "closed"
end end
factory :active_milestone, traits: [:active]
factory :closed_milestone, traits: [:closed] factory :closed_milestone, traits: [:closed]
end end
end end
require 'spec_helper'
describe MilestonesHelper do
describe '#milestone_counts' do
let(:project) { FactoryGirl.create(:project) }
let(:counts) { helper.milestone_counts(project.milestones) }
context 'when there are milestones' do
let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) }
let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) }
let!(:milestone_3) { FactoryGirl.create(:closed_milestone, project: project) }
it 'returns the correct counts' do
expect(counts).to eq(opened: 2, closed: 1, all: 3)
end
end
context 'when there are only milestones of one type' do
let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) }
let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) }
it 'returns the correct counts' do
expect(counts).to eq(opened: 2, closed: 0, all: 2)
end
end
context 'when there are no milestones' do
it 'returns the correct counts' do
expect(counts).to eq(opened: 0, closed: 0, all: 0)
end
end
end
end
...@@ -3,19 +3,27 @@ require 'spec_helper' ...@@ -3,19 +3,27 @@ require 'spec_helper'
describe Gitlab::Redis do describe Gitlab::Redis do
let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s } let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s }
before(:each) { described_class.reset_params! } before(:each) { clear_raw_config }
after(:each) { described_class.reset_params! } after(:each) { clear_raw_config }
describe '.params' do describe '.params' do
subject { described_class.params } subject { described_class.params }
it 'withstands mutation' do
params1 = described_class.params
params2 = described_class.params
params1[:foo] = :bar
expect(params2).not_to have_key(:foo)
end
context 'when url contains unix socket reference' do context 'when url contains unix socket reference' do
let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s } let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s }
let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s } let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s }
context 'with old format' do context 'with old format' do
it 'returns path key instead' do it 'returns path key instead' do
expect_any_instance_of(described_class).to receive(:config_file) { config_old } stub_const("#{described_class}::CONFIG_FILE", config_old)
is_expected.to include(path: '/path/to/old/redis.sock') is_expected.to include(path: '/path/to/old/redis.sock')
is_expected.not_to have_key(:url) is_expected.not_to have_key(:url)
...@@ -24,7 +32,7 @@ describe Gitlab::Redis do ...@@ -24,7 +32,7 @@ describe Gitlab::Redis do
context 'with new format' do context 'with new format' do
it 'returns path key instead' do it 'returns path key instead' do
expect_any_instance_of(described_class).to receive(:config_file) { config_new } stub_const("#{described_class}::CONFIG_FILE", config_new)
is_expected.to include(path: '/path/to/redis.sock') is_expected.to include(path: '/path/to/redis.sock')
is_expected.not_to have_key(:url) is_expected.not_to have_key(:url)
...@@ -38,7 +46,7 @@ describe Gitlab::Redis do ...@@ -38,7 +46,7 @@ describe Gitlab::Redis do
context 'with old format' do context 'with old format' do
it 'returns hash with host, port, db, and password' do it 'returns hash with host, port, db, and password' do
expect_any_instance_of(described_class).to receive(:config_file) { config_old } stub_const("#{described_class}::CONFIG_FILE", config_old)
is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99) is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99)
is_expected.not_to have_key(:url) is_expected.not_to have_key(:url)
...@@ -47,7 +55,7 @@ describe Gitlab::Redis do ...@@ -47,7 +55,7 @@ describe Gitlab::Redis do
context 'with new format' do context 'with new format' do
it 'returns hash with host, port, db, and password' do it 'returns hash with host, port, db, and password' do
expect_any_instance_of(described_class).to receive(:config_file) { config_new } stub_const("#{described_class}::CONFIG_FILE", config_new)
is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99) is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99)
is_expected.not_to have_key(:url) is_expected.not_to have_key(:url)
...@@ -56,6 +64,30 @@ describe Gitlab::Redis do ...@@ -56,6 +64,30 @@ describe Gitlab::Redis do
end end
end end
describe '.url' do
it 'withstands mutation' do
url1 = described_class.url
url2 = described_class.url
url1 << 'foobar'
expect(url2).not_to end_with('foobar')
end
end
describe '._raw_config' do
subject { described_class._raw_config }
it 'should be frozen' do
expect(subject).to be_frozen
end
it 'returns false when the file does not exist' do
stub_const("#{described_class}::CONFIG_FILE", '/var/empty/doesnotexist')
expect(subject).to eq(false)
end
end
describe '#raw_config_hash' do describe '#raw_config_hash' do
it 'returns default redis url when no config file is present' do it 'returns default redis url when no config file is present' do
expect(subject).to receive(:fetch_config) { false } expect(subject).to receive(:fetch_config) { false }
...@@ -71,9 +103,15 @@ describe Gitlab::Redis do ...@@ -71,9 +103,15 @@ describe Gitlab::Redis do
describe '#fetch_config' do describe '#fetch_config' do
it 'returns false when no config file is present' do it 'returns false when no config file is present' do
allow(File).to receive(:exist?).with(redis_config) { false } allow(described_class).to receive(:_raw_config) { false }
expect(subject.send(:fetch_config)).to be_falsey expect(subject.send(:fetch_config)).to be_falsey
end end
end end
def clear_raw_config
described_class.remove_instance_variable(:@_raw_config)
rescue NameError
# raised if @_raw_config was not set; ignore
end
end end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Workhorse, lib: true do describe Gitlab::Workhorse, lib: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:subject) { Gitlab::Workhorse } let(:repository) { project.repository }
def decode_workhorse_header(array)
key, value = array
command, encoded_params = value.split(":")
params = JSON.parse(Base64.urlsafe_decode64(encoded_params))
[key, command, params]
end
describe ".send_git_archive" do describe ".send_git_archive" do
context "when the repository doesn't have an archive file path" do context "when the repository doesn't have an archive file path" do
...@@ -11,11 +19,37 @@ describe Gitlab::Workhorse, lib: true do ...@@ -11,11 +19,37 @@ describe Gitlab::Workhorse, lib: true do
end end
it "raises an error" do it "raises an error" do
expect { subject.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError) expect { described_class.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
end end
end end
end end
describe '.send_git_patch' do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
subject { described_class.send_git_patch(repository, diff_refs) }
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
expect(key).to eq("Gitlab-Workhorse-Send-Data")
expect(command).to eq("git-format-patch")
expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
end
end
describe '.send_git_diff' do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
subject { described_class.send_git_patch(repository, diff_refs) }
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
expect(key).to eq("Gitlab-Workhorse-Send-Data")
expect(command).to eq("git-format-patch")
expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
end
end
describe ".secret" do describe ".secret" do
subject { described_class.secret } subject { described_class.secret }
......
...@@ -50,8 +50,9 @@ describe GlobalMilestone, models: true do ...@@ -50,8 +50,9 @@ describe GlobalMilestone, models: true do
milestone1_project2, milestone1_project2,
milestone1_project3, milestone1_project3,
] ]
milestones_relation = Milestone.where(id: milestones.map(&:id))
@global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones) @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones_relation)
end end
it 'has exactly one group milestone' do it 'has exactly one group milestone' do
...@@ -67,7 +68,7 @@ describe GlobalMilestone, models: true do ...@@ -67,7 +68,7 @@ describe GlobalMilestone, models: true do
let(:milestone) { create(:milestone, title: "git / test", project: project1) } let(:milestone) { create(:milestone, title: "git / test", project: project1) }
it 'strips out slashes and spaces' do it 'strips out slashes and spaces' do
global_milestone = GlobalMilestone.new(milestone.title, [milestone]) global_milestone = GlobalMilestone.new(milestone.title, Milestone.where(id: milestone.id))
expect(global_milestone.safe_title).to eq('git-test') expect(global_milestone.safe_title).to eq('git-test')
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