summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js8
-rw-r--r--app/assets/javascripts/cycle-analytics.js.es693
-rw-r--r--app/assets/javascripts/dispatcher.js8
-rw-r--r--app/assets/javascripts/gl_dropdown.js25
-rw-r--r--app/assets/javascripts/issuable.js.es641
-rw-r--r--app/assets/javascripts/project_find_file.js24
-rw-r--r--app/assets/javascripts/protected_branch_edit.js.es61
-rw-r--r--app/assets/stylesheets/framework/blocks.scss11
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/nav.scss3
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/pages/builds.scss4
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss144
-rw-r--r--app/assets/stylesheets/pages/groups.scss13
-rw-r--r--app/assets/stylesheets/pages/milestone.scss27
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/assets/stylesheets/pages/profile.scss41
-rw-r--r--app/assets/stylesheets/pages/snippets.scss7
-rw-r--r--app/assets/stylesheets/pages/tree.scss9
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb8
-rw-r--r--app/controllers/jwt_controller.rb6
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb67
-rw-r--r--app/controllers/projects/git_http_client_controller.rb6
-rw-r--r--app/controllers/projects/issues_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests_controller.rb10
-rw-r--r--app/controllers/projects/snippets_controller.rb5
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/controllers/snippets_controller.rb3
-rw-r--r--app/finders/issuable_finder.rb9
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb10
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/award_emoji_helper.rb9
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/milestones_helper.rb24
-rw-r--r--app/models/ci/build.rb9
-rw-r--r--app/models/ci/pipeline.rb20
-rw-r--r--app/models/concerns/awardable.rb6
-rw-r--r--app/models/concerns/issuable.rb13
-rw-r--r--app/models/cycle_analytics.rb97
-rw-r--r--app/models/cycle_analytics/summary.rb24
-rw-r--r--app/models/deployment.rb34
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/global_milestone.rb13
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/issue/metrics.rb21
-rw-r--r--app/models/merge_request.rb48
-rw-r--r--app/models/merge_request/metrics.rb11
-rw-r--r--app/models/merge_request_diff.rb14
-rw-r--r--app/models/merge_requests_closing_issues.rb7
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project.rb16
-rw-r--r--app/models/project_feature.rb6
-rw-r--r--app/models/project_team.rb72
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/create_deployment_service.rb6
-rw-r--r--app/services/git_push_service.rb8
-rw-r--r--app/services/issuable_base_service.rb5
-rw-r--r--app/services/merge_requests/create_service.rb1
-rw-r--r--app/services/merge_requests/refresh_service.rb9
-rw-r--r--app/services/merge_requests/update_service.rb4
-rw-r--r--app/views/admin/application_settings/_form.html.haml54
-rw-r--r--app/views/award_emoji/_awards_block.html.haml2
-rw-r--r--app/views/discussions/_jump_to_next.html.haml4
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/layouts/project.html.haml3
-rw-r--r--app/views/projects/_zen.html.haml3
-rw-r--r--app/views/projects/builds/_table.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml59
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml74
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml25
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/projects/pipelines/_head.html.haml6
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml2
-rw-r--r--app/views/projects/snippets/_actions.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml23
-rw-r--r--app/views/shared/_milestones_filter.html.haml15
-rw-r--r--app/views/shared/icons/_icon_cycle_analytics_splash.svg1
-rw-r--r--app/views/shared/icons/_icon_fork.svg4
-rw-r--r--app/views/shared/issuable/_filter.html.haml8
-rw-r--r--app/views/shared/issuable/_search_form.html.haml4
-rw-r--r--app/views/shared/milestones/_milestone.html.haml2
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/users/show.html.haml94
89 files changed, 1199 insertions, 330 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 6df2ecf57a2..1cd2302111e 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -16,9 +16,6 @@
.replace(':id', group_id);
return $.ajax({
url: url,
- data: {
- private_token: gon.api_token
- },
dataType: "json"
}).done(function(group) {
return callback(group);
@@ -31,7 +28,6 @@
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
per_page: 20
},
@@ -46,7 +42,6 @@
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
per_page: 20
},
@@ -61,7 +56,6 @@
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
order_by: order,
per_page: 20
@@ -74,7 +68,6 @@
newLabel: function(project_id, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id);
- data.private_token = gon.api_token;
return $.ajax({
url: url,
type: "POST",
@@ -93,7 +86,6 @@
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
per_page: 20
},
diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle-analytics.js.es6
new file mode 100644
index 00000000000..cd9886ba58d
--- /dev/null
+++ b/app/assets/javascripts/cycle-analytics.js.es6
@@ -0,0 +1,93 @@
+((global) => {
+
+ const COOKIE_NAME = 'cycle_analytics_help_dismissed';
+ const store = gl.cycleAnalyticsStore = {
+ isLoading: true,
+ hasError: false,
+ isHelpDismissed: $.cookie(COOKIE_NAME),
+ analytics: {}
+ };
+
+ gl.CycleAnalytics = class CycleAnalytics {
+ constructor() {
+ const that = this;
+
+ this.vue = new Vue({
+ el: '#cycle-analytics',
+ name: 'CycleAnalytics',
+ created: this.fetchData(),
+ data: store,
+ methods: {
+ dismissLanding() {
+ that.dismissLanding();
+ }
+ }
+ });
+ }
+
+ fetchData(options) {
+ store.isLoading = true;
+ options = options || { startDate: 30 };
+
+ $.ajax({
+ url: $('#cycle-analytics').data('request-path'),
+ method: 'GET',
+ dataType: 'json',
+ contentType: 'application/json',
+ data: { start_date: options.startDate }
+ }).done((data) => {
+ this.decorateData(data);
+ this.initDropdown();
+ })
+ .error((data) => {
+ this.handleError(data);
+ })
+ .always(() => {
+ store.isLoading = false;
+ })
+ }
+
+ decorateData(data) {
+ data.summary = data.summary || [];
+ data.stats = data.stats || [];
+
+ data.summary.forEach((item) => {
+ item.value = item.value || '-';
+ });
+
+ data.stats.forEach((item) => {
+ item.value = item.value || '- - -';
+ });
+
+ store.analytics = data;
+ }
+
+ handleError(data) {
+ store.hasError = true;
+ new Flash('There was an error while fetching cycle analytics data.', 'alert');
+ }
+
+ dismissLanding() {
+ store.isHelpDismissed = true;
+ $.cookie(COOKIE_NAME, true, {
+ path: gon.relative_url_root || '/'
+ });
+ }
+
+ initDropdown() {
+ const $dropdown = $('.js-ca-dropdown');
+ const $label = $dropdown.find('.dropdown-label');
+
+ $dropdown.find('li a').off('click').on('click', (e) => {
+ e.preventDefault();
+ const $target = $(e.currentTarget);
+ const value = $target.data('value');
+
+ $label.text($target.text().trim());
+ this.fetchData({ startDate: value });
+ })
+ }
+
+ }
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 99b16f7d59b..ddf11ecf34c 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -94,6 +94,11 @@
break;
case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver()
+ break;
+ case 'projects:merge_requests:index':
+ shortcut_handler = new ShortcutsNavigation();
+ Issuable.init();
+ break;
case 'dashboard:activity':
new Activities();
break;
@@ -185,6 +190,9 @@
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
+ case 'projects:cycle_analytics:show':
+ new gl.CycleAnalytics();
+ break;
}
switch (path.first()) {
case 'admin':
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index c05cda25bbd..1b6db641200 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -608,27 +608,28 @@
}
}
field = [];
- fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName;
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
field = $(this.el);
} else if(value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
- if (field.length && el.hasClass(ACTIVE_CLASS)) {
+ if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS);
- if (isInput) {
- field.val('');
- } else {
- field.remove();
+ if (field && field.length) {
+ if (isInput) {
+ field.val('');
+ } else {
+ field.remove();
+ }
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
- if (field.length && value == null) {
+ if (field && field.length && value == null) {
field.remove();
}
- if (!field.length && fieldName) {
+ if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
}
} else {
@@ -638,15 +639,15 @@
this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
}
}
- if (field.length && value == null) {
+ if (field && field.length && value == null) {
field.remove();
}
// Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
if (value != null) {
- if (!field.length && fieldName) {
+ if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
- } else if (field.length) {
+ } else if (field && field.length) {
field.val(value).trigger('change');
}
}
@@ -796,4 +797,4 @@
});
};
-}).call(this); \ No newline at end of file
+}).call(this);
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 53faaa38a0c..73e2664e9c0 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -15,25 +15,32 @@
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
},
initSearch: function() {
- this.timer = null;
- return $('#issue_search').off('keyup').on('keyup', function() {
- clearTimeout(this.timer);
- return this.timer = setTimeout(function() {
- var $form, $input, $search;
- $search = $('#issue_search');
- $form = $('.js-filter-form');
- $input = $("input[name='" + ($search.attr('name')) + "']", $form);
- if ($input.length === 0) {
- $form.append("<input type='hidden' name='" + ($search.attr('name')) + "' value='" + (_.escape($search.val())) + "'/>");
- } else {
- $input.val($search.val());
- }
- if ($search.val() !== '') {
- return Issuable.filterResults($form);
- }
- }, 500);
+ // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
+ const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false);
+
+ $('#issuable_search').off('keyup').on('keyup', debouncedExecSearch);
+
+ // ensures existing filters are preserved when manually submitted
+ $('#issue_search_form').on('submit', (e) => {
+ e.preventDefault();
+ debouncedExecSearch(e);
});
},
+ executeSearch: function(e) {
+ const $search = $('#issuable_search');
+ const $searchName = $search.attr('name');
+ const $searchValue = $search.val();
+ const $filtersForm = $('.js-filter-form');
+ const $input = $(`input[name='${$searchName}']`, $filtersForm);
+
+ if (!$input.length) {
+ $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
+ } else {
+ $input.val($searchValue);
+ }
+
+ Issuable.filterResults($filtersForm);
+ },
initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
var $button;
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 5bf900f3e1d..8e38ccf7e44 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -7,7 +7,6 @@
function ProjectFindFile(element1, options) {
this.element = element1;
this.options = options;
- this.goToBlob = bind(this.goToBlob, this);
this.goToTree = bind(this.goToTree, this);
this.selectRowDown = bind(this.selectRowDown, this);
this.selectRowUp = bind(this.selectRowUp, this);
@@ -36,16 +35,6 @@
}
};
})(this));
- return this.element.find(".tree-content-holder .tree-table").on("click", function(event) {
- var path;
- if (event.target.nodeName !== "A") {
- path = this.element.find(".tree-item-file-name a", this).attr("href");
- if (path) {
- return location.href = path;
- }
- }
- });
- // init event
};
ProjectFindFile.prototype.findFile = function() {
@@ -121,11 +110,12 @@
// make tbody row html
ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) {
var $tr;
- $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>");
+ $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>");
if (matches) {
$tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl));
} else {
- $tr.find("a").attr("href", blobItemUrl).text(filePath);
+ $tr.find("a").attr("href", blobItemUrl);
+ $tr.find(".str-truncated").text(filePath);
}
return $tr;
};
@@ -164,14 +154,6 @@
return location.href = this.options.treeUrl;
};
- ProjectFindFile.prototype.goToBlob = function() {
- var path;
- path = this.element.find(".tree-item.selected .tree-item-file-name a").attr("href");
- if (path) {
- return location.href = path;
- }
- };
-
return ProjectFindFile;
})();
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6
index 40bc4adb71b..15a6dca2875 100644
--- a/app/assets/javascripts/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branch_edit.js.es6
@@ -40,7 +40,6 @@
dataType: 'json',
data: {
_method: 'PATCH',
- id: this.$wrap.data('banchId'),
protected_branch: {
merge_access_levels_attributes: [{
id: this.$allowedToMergeDropdown.data('access-level-id'),
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index f5223207f3a..2432ddb72f4 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -129,8 +129,6 @@
position: relative;
.avatar-holder {
- margin-bottom: 16px;
-
.avatar, .identicon {
margin: 0 auto;
float: none;
@@ -143,13 +141,7 @@
.cover-title {
color: $gl-header-color;
- margin: 0;
- font-size: 24px;
- font-weight: normal;
- margin-bottom: 10px;
- color: #4c4e54;
font-size: 23px;
- line-height: 1.1;
h1 {
color: $gl-gray-dark;
@@ -213,6 +205,9 @@
}
}
}
+ &.user-cover-block {
+ padding: 24px 0 0;
+ }
.group-info {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 0c21d0240b3..7ae309ba103 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -3,7 +3,6 @@
margin: 0;
margin-bottom: $gl-padding;
font-size: 14px;
- z-index: 100;
.flash-notice {
@extend .alert;
@@ -41,4 +40,3 @@
}
}
}
-
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 553768b2e68..ea43f4afc37 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -99,8 +99,7 @@
.top-area {
@include clearfix;
-
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid $btn-gray-hover;
.nav-text {
padding-top: 16px;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 2582cde5a71..9f2d53d5206 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -204,7 +204,7 @@ body {
}
h1, h2, h3, h4, h5, h6 {
- color: $gl-header-color;
+ color: $gl-title-color;
font-weight: 600;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 9f563a4de35..14ec310de2d 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -102,7 +102,7 @@ $gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-gray-light: $gl-placeholder-color;
-$gl-header-color: $gl-title-color;
+$gl-header-color: #4c4e54;
/*
* Lists
@@ -270,6 +270,12 @@ $calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: $gray-light;
/*
+ * Cycle Analytics
+ */
+$cycle-analytics-box-padding: 30px;
+$cycle-analytics-box-text-color: #8c8c8c;
+
+/*
* Personal Access Tokens
*/
$personal-access-tokens-disabled-label-color: #bbb;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index c879074c7fe..a5a260d4c8f 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -109,6 +109,10 @@
width: 100%;
}
+ .block-first {
+ padding: 5px 16px 11px;
+ }
+
.js-build-variable {
color: $code-color;
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
new file mode 100644
index 00000000000..778471a34d7
--- /dev/null
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -0,0 +1,144 @@
+#cycle-analytics {
+ margin: 24px auto 0;
+ max-width: 800px;
+ position: relative;
+
+ .panel {
+
+ .content-block {
+ padding: 24px 0;
+ border-bottom: none;
+ position: relative;
+
+ @media (max-width: $screen-sm-min) {
+ padding: 6px 0 24px;
+ }
+ }
+
+ .column {
+ text-align: center;
+
+ @media (max-width: $screen-sm-min) {
+ padding: 15px 0;
+ }
+
+ .header {
+ font-size: 30px;
+ line-height: 38px;
+ font-weight: normal;
+ margin: 0;
+ }
+
+ .text {
+ color: $layout-link-gray;
+ margin: 0;
+ }
+
+ &:last-child {
+ text-align: right;
+
+ @media (max-width: $screen-sm-min) {
+ text-align: center;
+ }
+ }
+ }
+
+ .dropdown {
+ top: 13px;
+ }
+ }
+
+ .bordered-box {
+ border: 1px solid $border-color;
+ @include border-radius($border-radius-default);
+
+ }
+
+ .content-list {
+ li {
+ padding: 18px $gl-padding $gl-padding;
+
+ .container-fluid {
+ padding: 0;
+ }
+ }
+
+ .title-col {
+ p {
+ margin: 0;
+
+ &.title {
+ line-height: 19px;
+ font-size: 15px;
+ font-weight: 600;
+ color: $gl-title-color;
+ }
+
+ &.text {
+ color: $layout-link-gray;
+
+ &.value-col {
+ color: $gl-title-color;
+ }
+ }
+ }
+ }
+
+ .value-col {
+ text-align: right;
+
+ span {
+ position: relative;
+ vertical-align: middle;
+ top: 3px;
+ }
+ }
+ }
+
+ .landing {
+ margin-bottom: $gl-padding;
+ overflow: hidden;
+
+ .dismiss-icon {
+ position: absolute;
+ right: $cycle-analytics-box-padding;
+ cursor: pointer;
+ color: #b2b2b2;
+ }
+
+ .svg-container {
+ text-align: center;
+
+ svg {
+ width: 136px;
+ height: 136px;
+ }
+ }
+
+ .inner-content {
+ @media (max-width: $screen-sm-min) {
+ padding: 0 28px;
+ text-align: center;
+ }
+
+ h4 {
+ color: $gl-text-color;
+ font-size: 17px;
+ }
+
+ p {
+ color: $cycle-analytics-box-text-color;
+ margin-bottom: $gl-padding;
+ }
+ }
+ }
+
+ .fa-spinner {
+ font-size: 28px;
+ position: relative;
+ margin-left: -20px;
+ left: 50%;
+ margin-top: 36px;
+ }
+
+}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index b657ca47d38..732dc645c66 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -55,3 +55,16 @@
}
}
}
+
+.groups-header {
+
+ @media (min-width: $screen-sm-min) {
+ .nav-links {
+ width: 35%;
+ }
+
+ .nav-controls {
+ width: 65%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index b94f524b513..6b865730487 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -2,13 +2,17 @@
max-width: 90%;
}
-li.milestone {
- h4 {
- font-weight: bold;
- }
+.milestones {
+ .milestone {
+ padding: 10px 16px;
+
+ h4 {
+ font-weight: bold;
+ }
- .progress {
- height: 6px;
+ .progress {
+ height: 6px;
+ }
}
}
@@ -64,3 +68,14 @@ li.milestone {
border-bottom: 1px solid $border-color;
padding: 20px 0;
}
+
+@media (max-width: $screen-sm-min) {
+ .milestone-actions {
+ @include clearfix();
+ padding-top: $gl-vert-padding;
+
+ .btn:first-child {
+ margin-left: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 1b4d12d3053..b035bfc9f3c 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -177,6 +177,10 @@
border-bottom: 2px solid $border-color;
}
}
+
+ a {
+ display: block;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 6f58203f49c..0fcdaf94a21 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -93,8 +93,9 @@
.profile-user-bio {
// Limits the width of the user bio for readability.
- max-width: 750px;
- margin: auto;
+ max-width: 600px;
+ margin: 15px auto 0;
+ padding: 0 16px;
}
.user-avatar-button {
@@ -212,6 +213,28 @@
}
.user-profile {
+ .cover-controls a {
+ margin-left: 5px;
+ }
+ .profile-header {
+ margin: 0 auto;
+ .avatar-holder {
+ width: 90px;
+ display: inline-block;
+ }
+ .user-info {
+ display: inline-block;
+ text-align: left;
+ vertical-align: middle;
+ margin-left: 15px;
+ .handle {
+ color: $gl-gray-light;
+ }
+ .member-date {
+ margin-bottom: 4px;
+ }
+ }
+ }
@media (max-width: $screen-xs-max) {
.cover-block {
padding-top: 20px;
@@ -219,16 +242,26 @@
.cover-controls {
position: static;
+ padding: 0 16px;
margin-bottom: 20px;
+ display: -webkit-flex;
+ display: flex;
.btn {
- display: inline-block;
- width: 46%;
+ -webkit-flex-grow: 1;
+ flex-grow: 1;
+ &:first-child {
+ margin-left: 0;
+ }
}
}
}
}
+.user-profile-nav {
+ margin-top: 15px;
+}
+
table.u2f-registrations {
th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent;
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 5270aea4e79..4d5df566d9b 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -12,11 +12,18 @@
.snippet-file-content {
border-radius: 3px;
+ margin-bottom: $gl-padding;
+
.btn-clipboard {
@extend .btn;
}
}
+.project-snippets .awards {
+ border-bottom: 1px solid $table-border-color;
+ padding-bottom: $gl-padding;
+}
+
.snippet-title {
font-size: 24px;
font-weight: 600;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 1778c069706..7b6577c513e 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -55,6 +55,15 @@
}
.tree-item {
+ .link-container {
+ padding: 0;
+
+ a {
+ padding: 10px $gl-padding;
+ display: block;
+ }
+ }
+
.tree-item-file-name {
max-width: 320px;
vertical-align: middle;
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 172d5344b7a..3717c49f272 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -10,7 +10,9 @@ module ToggleAwardEmoji
if awardable.user_can_award?(current_user, name)
awardable.toggle_award_emoji(name, current_user)
- TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
+
+ todoable = to_todoable(awardable)
+ TodoService.new.new_award_emoji(todoable, current_user) if todoable
render json: { ok: true }
else
@@ -24,8 +26,10 @@ module ToggleAwardEmoji
case awardable
when Note
awardable.noteable
- else
+ when MergeRequest, Issue
awardable
+ when Snippet
+ nil
end
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 06d96774754..34d5d99558e 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -11,10 +11,8 @@ class JwtController < ApplicationController
service = SERVICES[params[:service]]
return head :not_found unless service
- @authentication_result ||= Gitlab::Auth::Result.new
-
result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
- execute(authentication_abilities: @authentication_result.authentication_abilities)
+ execute(authentication_abilities: @authentication_result.authentication_abilities || [])
render json: result, status: result[:http_status]
end
@@ -22,6 +20,8 @@ class JwtController < ApplicationController
private
def authenticate_project_or_user
+ @authentication_result = Gitlab::Auth::Result.new
+
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
new file mode 100644
index 00000000000..16a7b1fc6e2
--- /dev/null
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -0,0 +1,67 @@
+class Projects::CycleAnalyticsController < Projects::ApplicationController
+ include ActionView::Helpers::DateHelper
+ include ActionView::Helpers::TextHelper
+
+ before_action :authorize_read_cycle_analytics!
+
+ def show
+ @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date)
+
+ respond_to do |format|
+ format.html
+ format.json { render json: cycle_analytics_json }
+ end
+ end
+
+ private
+
+ def parse_start_date
+ case cycle_analytics_params[:start_date]
+ when '30' then 30.days.ago
+ when '90' then 90.days.ago
+ else 90.days.ago
+ end
+ end
+
+ def cycle_analytics_params
+ return {} unless params[:cycle_analytics].present?
+
+ { start_date: params[:cycle_analytics][:start_date] }
+ end
+
+ def cycle_analytics_json
+ cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"],
+ [:plan, "Plan", "Time before an issue starts implementation"],
+ [:code, "Code", "Time until first merge request"],
+ [:test, "Test", "Total test time for all commits/merges"],
+ [:review, "Review", "Time between merge request creation and merge/close"],
+ [:staging, "Staging", "From merge request merge until deploy to production"],
+ [:production, "Production", "From issue creation until deploy to production"]]
+
+ stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|
+ value = @cycle_analytics.send(stage_method).presence
+
+ stats << {
+ title: stage_text,
+ description: stage_description,
+ value: value && !value.zero? ? distance_of_time_in_words(value) : nil
+ }
+ stats
+ end
+
+ issues = @cycle_analytics.summary.new_issues
+ commits = @cycle_analytics.summary.commits
+ deploys = @cycle_analytics.summary.deploys
+
+ summary = [
+ { title: "New Issue".pluralize(issues), value: issues },
+ { title: "Commit".pluralize(commits), value: commits },
+ { title: "Deploy".pluralize(deploys), value: deploys }
+ ]
+
+ {
+ summary: summary,
+ stats: stats
+ }
+ end
+end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index cbfd3cab3dd..383e184d796 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -32,11 +32,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController
return # Allow access
end
elsif allow_kerberos_spnego_auth? && spnego_provided?
- user = find_kerberos_user
+ kerberos_user = find_kerberos_user
- if user
+ if kerberos_user
@authentication_result = Gitlab::Auth::Result.new(
- user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
+ kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
send_final_spnego_response
return # Allow access
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index de02e28e384..3eb13a121bf 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -23,18 +23,9 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html
def index
- terms = params['issue_search']
@issues = issues_collection
-
- if terms.present?
- if terms =~ /\A#(\d+)\z/
- @issues = @issues.where(iid: $1)
- else
- @issues = @issues.full_search(terms)
- end
- end
-
@issues = @issues.page(params[:page])
+
@labels = @project.labels.where(title: params[:label_name])
respond_to do |format|
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 0288ee87717..935417d4ae8 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -31,17 +31,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
def index
- terms = params['issue_search']
@merge_requests = merge_requests_collection
-
- if terms.present?
- if terms =~ /\A[#!](\d+)\z/
- @merge_requests = @merge_requests.where(iid: $1)
- else
- @merge_requests = @merge_requests.full_search(terms)
- end
- end
-
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project)
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 17ceefec3b8..e290a0eadda 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,6 +1,8 @@
class Projects::SnippetsController < Projects::ApplicationController
+ include ToggleAwardEmoji
+
before_action :module_enabled
- before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji]
# Allow read any snippet
before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
@@ -80,6 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController
def snippet
@snippet ||= @project.snippets.find(params[:id])
end
+ alias_method :awardable, :snippet
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 61517d21f9f..d01e0dedf52 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -6,8 +6,6 @@ class SearchController < ApplicationController
layout 'search'
def show
- return if params[:search].nil? || params[:search].blank?
-
if params[:project_id].present?
@project = Project.find_by(id: params[:project_id])
@project = nil unless can?(current_user, :download_code, @project)
@@ -18,6 +16,8 @@ class SearchController < ApplicationController
@group = nil unless can?(current_user, :read_group, @group)
end
+ return if params[:search].nil? || params[:search].blank?
+
@search_term = params[:search]
@scope = params[:scope]
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 2a17c1f34db..d198782138a 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,4 +1,6 @@
class SnippetsController < ApplicationController
+ include ToggleAwardEmoji
+
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
@@ -85,6 +87,7 @@ class SnippetsController < ApplicationController
PersonalSnippet.find(params[:id])
end
end
+ alias_method :awardable, :snippet
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 60996b181f2..8f9ef8f725c 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -216,7 +216,14 @@ class IssuableFinder
end
def by_search(items)
- items = items.search(search) if search
+ if search
+ items =
+ if search =~ iid_pattern
+ items.where(iid: $~[:iid])
+ else
+ items.full_search(search)
+ end
+ end
items
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index c2befa5a5b3..be00a219205 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -25,4 +25,8 @@ class IssuesFinder < IssuableFinder
def init_collection
Issue.visible_to_user(current_user)
end
+
+ def iid_pattern
+ @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
+ end
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index b258216d0d4..3b254e7d9d5 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -19,4 +19,14 @@ class MergeRequestsFinder < IssuableFinder
def klass
MergeRequest
end
+
+ private
+
+ def iid_pattern
+ @iid_pattern ||= %r{\A[
+ #{Regexp.escape(MergeRequest.reference_prefix)}
+ #{Regexp.escape(Issue.reference_prefix)}
+ ](?<iid>\d+)\z
+ }x
+ end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 5f3765cad0d..1df430e6279 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -249,7 +249,7 @@ module ApplicationHelper
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
- issue_search: params[:issue_search],
+ search: params[:search],
label_name: params[:label_name]
}
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
new file mode 100644
index 00000000000..aa134cea31c
--- /dev/null
+++ b/app/helpers/award_emoji_helper.rb
@@ -0,0 +1,9 @@
+module AwardEmojiHelper
+ def toggle_award_url(awardable)
+ if @project
+ url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
+ else
+ url_for([:toggle_award_emoji, awardable])
+ end
+ end
+end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index a322a90cc4e..670a7ca36f4 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -46,6 +46,10 @@ module GitlabRoutingHelper
namespace_project_environments_path(project.namespace, project, *args)
end
+ def project_cycle_analytics_path(project, *args)
+ namespace_project_cycle_analytics_path(project.namespace, project, *args)
+ end
+
def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args)
end
@@ -66,6 +70,10 @@ module GitlabRoutingHelper
namespace_project_runner_path(@project.namespace, @project, runner, *args)
end
+ def environment_path(environment, *args)
+ namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
+ end
+
def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end
@@ -98,6 +106,14 @@ module GitlabRoutingHelper
end
end
+ def toggle_award_emoji_personal_snippet_path(*args)
+ toggle_award_emoji_snippet_path(*args)
+ end
+
+ def toggle_award_emoji_namespace_project_project_snippet_path(*args)
+ toggle_award_emoji_namespace_project_snippet_path(*args)
+ end
+
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index b3e6e468ecd..a11c313a6b8 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -35,6 +35,30 @@ module MilestonesHelper
milestone.issues.with_label(label.title).send(state).size
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)
options = {
class: 'progress-bar progress-bar-success',
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index dd984aef318..522e2264bb8 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -91,7 +91,7 @@ module Ci
sha: build.sha,
ref: build.ref,
tag: build.tag,
- options: build.options[:environment],
+ options: build.options.to_h[:environment],
variables: build.variables)
service.execute(build)
end
@@ -493,8 +493,11 @@ module Ci
end
def hide_secrets(trace)
- trace = Ci::MaskSecret.mask(trace, project.runners_token) if project
- trace = Ci::MaskSecret.mask(trace, token)
+ return unless trace
+
+ trace = trace.dup
+ Ci::MaskSecret.mask!(trace, project.runners_token) if project
+ Ci::MaskSecret.mask!(trace, token)
trace
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 895eac1a258..663c5b1e231 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -56,6 +56,16 @@ module Ci
pipeline.finished_at = Time.now
end
+ after_transition [:created, :pending] => :running do |pipeline|
+ MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
+ update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
+ end
+
+ after_transition any => [:success] do |pipeline|
+ MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
+ update_all(latest_build_finished_at: pipeline.finished_at)
+ end
+
before_transition do |pipeline|
pipeline.update_duration
end
@@ -280,6 +290,16 @@ module Ci
project.execute_services(data, :pipeline_hooks)
end
+ # Merge requests for which the current pipeline is running against
+ # the merge request's latest commit.
+ def merge_requests
+ @merge_requests ||=
+ begin
+ project.merge_requests.where(source_branch: self.ref).
+ select { |merge_request| merge_request.pipeline.try(:id) == self.id }
+ end
+ end
+
private
def pipeline_data
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index d8d4575bb4d..073ac4c1b65 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -71,6 +71,12 @@ module Awardable
end
end
+ def user_authored?(current_user)
+ author = self.respond_to?(:author) ? self.author : self.user
+
+ author == current_user
+ end
+
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 22231b2e0f0..ff465d2c745 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -28,10 +28,13 @@ module Issuable
loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? }
end
end
+
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy
+ has_one :metrics
+
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
@@ -81,6 +84,7 @@ module Issuable
acts_as_paranoid
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
+ after_save :record_metrics
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignee
@@ -196,10 +200,6 @@ module Issuable
end
end
- def user_authored?(user)
- user == author
- end
-
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
@@ -286,4 +286,9 @@ module Issuable
def can_move?(*)
false
end
+
+ def record_metrics
+ metrics = self.metrics || create_metrics
+ metrics.record!
+ end
end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
new file mode 100644
index 00000000000..be295487fd2
--- /dev/null
+++ b/app/models/cycle_analytics.rb
@@ -0,0 +1,97 @@
+class CycleAnalytics
+ include Gitlab::Database::Median
+ include Gitlab::Database::DateTime
+
+ def initialize(project, from:)
+ @project = project
+ @from = from
+ end
+
+ def summary
+ @summary ||= Summary.new(@project, from: @from)
+ end
+
+ def issue
+ calculate_metric(:issue,
+ Issue.arel_table[:created_at],
+ [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
+ Issue::Metrics.arel_table[:first_added_to_board_at]])
+ end
+
+ def plan
+ calculate_metric(:plan,
+ [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
+ Issue::Metrics.arel_table[:first_added_to_board_at]],
+ Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
+ end
+
+ def code
+ calculate_metric(:code,
+ Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
+ MergeRequest.arel_table[:created_at])
+ end
+
+ def test
+ calculate_metric(:test,
+ MergeRequest::Metrics.arel_table[:latest_build_started_at],
+ MergeRequest::Metrics.arel_table[:latest_build_finished_at])
+ end
+
+ def review
+ calculate_metric(:review,
+ MergeRequest.arel_table[:created_at],
+ MergeRequest::Metrics.arel_table[:merged_at])
+ end
+
+ def staging
+ calculate_metric(:staging,
+ MergeRequest::Metrics.arel_table[:merged_at],
+ MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ end
+
+ def production
+ calculate_metric(:production,
+ Issue.arel_table[:created_at],
+ MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ end
+
+ private
+
+ def calculate_metric(name, start_time_attrs, end_time_attrs)
+ cte_table = Arel::Table.new("cte_table_for_#{name}")
+
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+ interval_query = Arel::Nodes::As.new(
+ cte_table,
+ subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s))
+
+ median_datetime(cte_table, interval_query, name)
+ end
+
+ # Join table with a row for every <issue,merge_request> pair (where the merge request
+ # closes the given issue) with issue and merge request metrics included. The metrics
+ # are loaded with an inner join, so issues / merge requests without metrics are
+ # automatically excluded.
+ def base_query
+ arel_table = MergeRequestsClosingIssues.arel_table
+
+ # Load issues
+ query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])).
+ join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])).
+ where(Issue.arel_table[:project_id].eq(@project.id)).
+ where(Issue.arel_table[:deleted_at].eq(nil)).
+ where(Issue.arel_table[:created_at].gteq(@from))
+
+ # Load merge_requests
+ query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin).
+ on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])).
+ join(MergeRequest::Metrics.arel_table).
+ on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
+
+ # Limit to merge requests that have been deployed to production after `@from`
+ query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
+ end
+end
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
new file mode 100644
index 00000000000..53b2cacb131
--- /dev/null
+++ b/app/models/cycle_analytics/summary.rb
@@ -0,0 +1,24 @@
+class CycleAnalytics
+ class Summary
+ def initialize(project, from:)
+ @project = project
+ @from = from
+ end
+
+ def new_issues
+ @project.issues.created_after(@from).count
+ end
+
+ def commits
+ repository = @project.repository.raw_repository
+
+ if @project.default_branch
+ repository.log(ref: @project.default_branch, after: @from).count
+ end
+ end
+
+ def deploys
+ @project.deployments.where("created_at > ?", @from).count
+ end
+ end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 1e338889714..07d7e19e70d 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -42,4 +42,38 @@ class Deployment < ActiveRecord::Base
project.repository.is_ancestor?(commit.id, sha)
end
+
+ def update_merge_request_metrics!
+ return unless environment.update_merge_request_metrics?
+
+ merge_requests = project.merge_requests.
+ joins(:metrics).
+ where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }).
+ where("merge_request_metrics.merged_at <= ?", self.created_at)
+
+ if previous_deployment
+ merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at)
+ end
+
+ # Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table
+ # that we're updating.
+ merge_request_ids =
+ if Gitlab::Database.postgresql?
+ merge_requests.select(:id)
+ elsif Gitlab::Database.mysql?
+ merge_requests.map(&:id)
+ end
+
+ MergeRequest::Metrics.
+ where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil).
+ update_all(first_deployed_to_production_at: self.created_at)
+ end
+
+ def previous_deployment
+ @previous_deployment ||=
+ project.deployments.joins(:environment).
+ where(environments: { name: self.environment.name }, ref: self.ref).
+ where.not(id: self.id).
+ take
+ end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 33c9abf382a..49e0a20640c 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -43,4 +43,8 @@ class Environment < ActiveRecord::Base
last_deployment.includes_commit?(commit)
end
+
+ def update_merge_request_metrics?
+ self.name == "production"
+ end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index da7c265a371..bda2b5c5d5d 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -8,7 +8,8 @@ class GlobalMilestone
milestones = milestones.group_by(&:title)
milestones.map do |title, milestones|
- new(title, milestones)
+ milestones_relation = Milestone.where(id: milestones.map(&:id))
+ new(title, milestones_relation)
end
end
@@ -31,7 +32,7 @@ class GlobalMilestone
end
def projects
- @projects ||= Project.for_milestones(milestones.map(&:id))
+ @projects ||= Project.for_milestones(milestones.select(:id))
end
def state
@@ -53,19 +54,19 @@ class GlobalMilestone
end
def issues
- @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
+ @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels)
end
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
def participants
- @participants ||= milestones.map(&:participants).flatten.compact.uniq
+ @participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq
end
def labels
- @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
+ @labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten)
.sort_by!(&:title)
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 788611305fe..abd58e0454a 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -23,6 +23,8 @@ class Issue < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy
+ has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
+
validates :project, presence: true
scope :cared, ->(user) { where(assignee_id: user) }
@@ -36,6 +38,8 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+ scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
+
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb
new file mode 100644
index 00000000000..012d545c440
--- /dev/null
+++ b/app/models/issue/metrics.rb
@@ -0,0 +1,21 @@
+class Issue::Metrics < ActiveRecord::Base
+ belongs_to :issue
+
+ def record!
+ if issue.milestone_id.present? && self.first_associated_with_milestone_at.blank?
+ self.first_associated_with_milestone_at = Time.now
+ end
+
+ if issue_assigned_to_list_label? && self.first_added_to_board_at.blank?
+ self.first_added_to_board_at = Time.now
+ end
+
+ self.save
+ end
+
+ private
+
+ def issue_assigned_to_list_label?
+ issue.labels.any? { |label| label.lists.present? }
+ end
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 75f48fd4ba5..2dcf7f89bfc 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -16,6 +16,8 @@ class MergeRequest < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy
+ has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
+
serialize :merge_params, Hash
after_create :ensure_merge_request_diff, unless: :importing?
@@ -501,6 +503,19 @@ class MergeRequest < ActiveRecord::Base
target_project
end
+ # If the merge request closes any issues, save this information in the
+ # `MergeRequestsClosingIssues` model. This is a performance optimization.
+ # Calculating this information for a number of merge requests requires
+ # running `ReferenceExtractor` on each of them separately.
+ def cache_merge_request_closes_issues!(current_user = self.author)
+ transaction do
+ self.merge_requests_closing_issues.delete_all
+ closes_issues(current_user).each do |issue|
+ self.merge_requests_closing_issues.create!(issue: issue)
+ end
+ end
+ end
+
def closes_issue?(issue)
closes_issues.include?(issue)
end
@@ -508,7 +523,8 @@ class MergeRequest < ActiveRecord::Base
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
- messages = commits.map(&:safe_message) << description
+ messages = [description]
+ messages.concat(commits.map(&:safe_message)) if merge_request_diff
Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(messages.join("\n"))
@@ -654,9 +670,12 @@ class MergeRequest < ActiveRecord::Base
def environments
return [] unless diff_head_commit
- target_project.environments.select do |environment|
- environment.includes_commit?(diff_head_commit)
- end
+ environments = source_project.environments_for(
+ source_branch, diff_head_commit)
+ environments += target_project.environments_for(
+ target_branch, diff_head_commit, with_tags: true)
+
+ environments.uniq
end
def state_human_name
@@ -745,10 +764,23 @@ class MergeRequest < ActiveRecord::Base
end
def all_pipelines
- @all_pipelines ||=
- if diff_head_sha && source_project
- source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch)
- end
+ return unless source_project
+
+ @all_pipelines ||= begin
+ sha = if persisted?
+ all_commits_sha
+ else
+ diff_head_sha
+ end
+
+ source_project.pipelines.order(id: :desc).
+ where(sha: sha, ref: source_branch)
+ end
+ end
+
+ # Note that this could also return SHA from now dangling commits
+ def all_commits_sha
+ merge_request_diffs.flat_map(&:commits_sha).uniq
end
def merge_commit
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
new file mode 100644
index 00000000000..99c49a020c9
--- /dev/null
+++ b/app/models/merge_request/metrics.rb
@@ -0,0 +1,11 @@
+class MergeRequest::Metrics < ActiveRecord::Base
+ belongs_to :merge_request
+
+ def record!
+ if merge_request.merged? && self.merged_at.blank?
+ self.merged_at = Time.now
+ end
+
+ self.save
+ end
+end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 18c583add88..36b8b70870b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -30,6 +30,10 @@ class MergeRequestDiff < ActiveRecord::Base
select(column_names - ['st_diffs'])
end
+ def st_commits
+ super || []
+ end
+
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
@@ -83,7 +87,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commits
- @commits ||= load_commits(st_commits || [])
+ @commits ||= load_commits(st_commits)
end
def reload_commits
@@ -117,6 +121,14 @@ class MergeRequestDiff < ActiveRecord::Base
project.commit(head_commit_sha)
end
+ def commits_sha
+ if @commits
+ commits.map(&:sha)
+ else
+ st_commits.map { |commit| commit[:id] }
+ end
+ end
+
def diff_refs
return unless start_commit_sha || base_commit_sha
diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb
new file mode 100644
index 00000000000..ab597c37947
--- /dev/null
+++ b/app/models/merge_requests_closing_issues.rb
@@ -0,0 +1,7 @@
+class MergeRequestsClosingIssues < ActiveRecord::Base
+ belongs_to :merge_request
+ belongs_to :issue
+
+ validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
+ validates :issue_id, presence: true
+end
diff --git a/app/models/note.rb b/app/models/note.rb
index b94e3cff2ce..f2656df028b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -223,10 +223,6 @@ class Note < ActiveRecord::Base
end
end
- def user_authored?(user)
- user == author
- end
-
def award_emoji?
can_be_award_emoji? && contains_emoji_only?
end
diff --git a/app/models/project.rb b/app/models/project.rb
index d7f20070be0..7265cb55594 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1293,6 +1293,22 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
+ def environments_for(ref, commit, with_tags: false)
+ environment_ids = deployments.group(:environment_id).
+ select(:environment_id)
+
+ environment_ids =
+ if with_tags
+ environment_ids.where('ref=? OR tag IS TRUE', ref)
+ else
+ environment_ids.where(ref: ref)
+ end
+
+ environments.where(id: environment_ids).select do |environment|
+ environment.includes_commit?(commit)
+ end
+ end
+
private
def pushes_since_gc_redis_key
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 9c602c582bd..8c9534c3565 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -22,6 +22,12 @@ class ProjectFeature < ActiveRecord::Base
belongs_to :project
+ default_value_for :builds_access_level, value: ENABLED, allows_nil: false
+ default_value_for :issues_access_level, value: ENABLED, allows_nil: false
+ default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false
+ default_value_for :snippets_access_level, value: ENABLED, allows_nil: false
+ default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
+
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index ab6ea2aae36..d9ce5088903 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -163,7 +163,7 @@ class ProjectTeam
# Each group produces a list of maximum access level per user. We take the
# max of the values produced by each group.
- if project.invited_groups.any? && project.allowed_to_share_with_group?
+ if project_shared_with_group?
project.project_group_links.each do |group_link|
invited_access = max_invited_level_for_users(group_link, user_ids)
merge_max!(access, invited_access)
@@ -200,43 +200,17 @@ class ProjectTeam
def fetch_members(level = nil)
project_members = project.members
group_members = group ? group.members : []
- invited_members = []
-
- if project.invited_groups.any? && project.allowed_to_share_with_group?
- project.project_group_links.includes(group: [:group_members]).each do |group_link|
- invited_group = group_link.group
- im = invited_group.members
-
- if level
- int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
-
- # Skip group members if we ask for masters
- # but max group access is developers
- next if int_level > group_link.group_access
-
- # If we ask for developers and max
- # group access is developers we need to provide
- # both group master, developers as devs
- if int_level == group_link.group_access
- im.where("access_level >= ?)", group_link.group_access)
- else
- im.send(level)
- end
- end
-
- invited_members << im
- end
-
- invited_members = invited_members.flatten.compact
- end
if level
- project_members = project_members.send(level)
- group_members = group_members.send(level) if group
+ project_members = project_members.public_send(level)
+ group_members = group_members.public_send(level) if group
end
user_ids = project_members.pluck(:user_id)
+
+ invited_members = fetch_invited_members(level)
user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
+
user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
@@ -249,4 +223,38 @@ class ProjectTeam
def merge_max!(first_hash, second_hash)
first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new }
end
+
+ def project_shared_with_group?
+ project.invited_groups.any? && project.allowed_to_share_with_group?
+ end
+
+ def fetch_invited_members(level = nil)
+ invited_members = []
+
+ return invited_members unless project_shared_with_group?
+
+ project.project_group_links.includes(group: [:group_members]).each do |link|
+ invited_group_members = link.group.members
+
+ if level
+ numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
+
+ # If we're asked for a level that's higher than the group's access,
+ # there's nothing left to do
+ next if numeric_level > link.group_access
+
+ # Make sure we include everyone _above_ the requested level as well
+ invited_group_members =
+ if numeric_level == link.group_access
+ invited_group_members.where("access_level >= ?", link.group_access)
+ else
+ invited_group_members.public_send(level)
+ end
+ end
+
+ invited_members << invited_group_members
+ end
+
+ invited_members.flatten.compact
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 772c62a4124..51557228ab9 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -840,7 +840,7 @@ class Repository
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
- author = name && email ? Gitlab::Git::committer_hash(email: email, name: name) : committer
+ author = Gitlab::Git::committer_hash(email: email, name: name) || committer
{
author: author,
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 5ec933601ac..8a1730f3f36 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -4,6 +4,7 @@ class Snippet < ActiveRecord::Base
include Participable
include Referable
include Sortable
+ include Awardable
default_value_for :visibility_level, Snippet::PRIVATE
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 00c4c7b1440..be25c750d67 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -46,6 +46,7 @@ class ProjectPolicy < BasePolicy
can! :create_issue
can! :create_note
can! :upload_file
+ can! :read_cycle_analytics
end
def reporter_access!
@@ -204,6 +205,7 @@ class ProjectPolicy < BasePolicy
can! :read_commit_status
can! :read_container_image
can! :download_code
+ can! :read_cycle_analytics
# NOTE: may be overridden by IssuePolicy
can! :read_issue
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 98da6563947..38ac6631228 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -5,7 +5,7 @@ module Auth
AUDIENCE = 'container_registry'
def execute(authentication_abilities:)
- @authentication_abilities = authentication_abilities || []
+ @authentication_abilities = authentication_abilities
return error('not found', 404) unless registry.enabled
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index e6667132e27..799ad3e1bd0 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -4,7 +4,7 @@ class CreateDeploymentService < BaseService
def execute(deployable = nil)
environment = find_or_create_environment
- project.deployments.create(
+ deployment = project.deployments.create(
environment: environment,
ref: params[:ref],
tag: params[:tag],
@@ -12,6 +12,10 @@ class CreateDeploymentService < BaseService
user: current_user,
deployable: deployable
)
+
+ deployment.update_merge_request_metrics!
+
+ deployment
end
private
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 948041063c0..c499427605a 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -134,6 +134,7 @@ class GitPushService < BaseService
end
commit.create_cross_references!(authors[commit], closed_issues)
+ update_issue_metrics(commit, authors)
end
end
@@ -186,4 +187,11 @@ class GitPushService < BaseService
def branch_name
@branch_name ||= Gitlab::Git.ref_name(params[:ref])
end
+
+ def update_issue_metrics(commit, authors)
+ mentioned_issues = commit.all_references(authors[commit]).issues
+
+ Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil).
+ update_all(first_mentioned_in_commit_at: commit.committed_date)
+ end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 4c8d93999a7..fbce46769f7 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -157,6 +157,10 @@ class IssuableBaseService < BaseService
# To be overridden by subclasses
end
+ def after_update(issuable)
+ # To be overridden by subclasses
+ end
+
def update_issuable(issuable, attributes)
issuable.with_transaction_returning_status do
issuable.update(attributes.merge(updated_by: current_user))
@@ -182,6 +186,7 @@ class IssuableBaseService < BaseService
end
handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
+ after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 73247e62421..b0ae2dfe4ce 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -20,6 +20,7 @@ module MergeRequests
event_service.open_mr(issuable, current_user)
notification_service.new_merge_request(issuable, current_user)
todo_service.new_merge_request(issuable, current_user)
+ issuable.cache_merge_request_closes_issues!(current_user)
end
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 5cedd6f11d9..22596b4014a 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -13,6 +13,7 @@ module MergeRequests
reload_merge_requests
reset_merge_when_build_succeeds
mark_pending_todos_done
+ cache_merge_requests_closing_issues
# Leave a system note if a branch was deleted/added
if branch_added? || branch_removed?
@@ -141,6 +142,14 @@ module MergeRequests
end
end
+ # If the merge requests closes any issues, save this information in the
+ # `MergeRequestsClosingIssues` model (as a performance optimization).
+ def cache_merge_requests_closing_issues
+ @project.merge_requests.where(source_branch: @branch_name).each do |merge_request|
+ merge_request.cache_merge_request_closes_issues!(@current_user)
+ end
+ end
+
def filter_merge_requests(merge_requests)
merge_requests.uniq.select(&:source_project)
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 398ec47f0ea..f14f9e4b327 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -77,5 +77,9 @@ module MergeRequests
def close_service
MergeRequests::CloseService
end
+
+ def after_update(issuable)
+ issuable.cache_merge_request_closes_issues!(current_user)
+ end
end
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index d929364fc96..0d79ca7dc52 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -49,28 +49,6 @@
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.help-block#clone-protocol-help
Allow only the selected protocols to be used for Git access.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :version_check_enabled do
- = f.check_box :version_check_enabled
- Version check enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :email_author_in_body do
- = f.check_box :email_author_in_body
- Include author name in notification email body
- .help-block
- 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.
- .form-group
- = f.label :admin_notification_email, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :admin_notification_email, class: 'form-control'
- .help-block
- Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
%fieldset
%legend Account and Limit Settings
@@ -341,6 +319,15 @@
%a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com
%fieldset
+ %legend Abuse reports
+ .form-group
+ = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :admin_notification_email, class: 'form-control'
+ .help-block
+ Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
+
+ %fieldset
%legend Error Reporting and Logging
%p
These settings require a restart to take effect.
@@ -407,6 +394,29 @@
= succeed "." do
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+ %fieldset
+ %legend Usage statistics
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :version_check_enabled do
+ = f.check_box :version_check_enabled
+ Version check enabled
+ .help-block
+ Let GitLab inform you when an update is available.
+
+ %fieldset
+ %legend Email
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :email_author_in_body do
+ = f.check_box :email_author_in_body
+ Include author name in notification email body
+ .help-block
+ 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.
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 02efcecc889..fbe3ab912b6 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,5 +1,5 @@
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
-.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } }
+.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false)
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
index da970792b4d..7ed09dd1a98 100644
--- a/app/views/discussions/_jump_to_next.html.haml
+++ b/app/views/discussions/_jump_to_next.html.haml
@@ -1,4 +1,3 @@
-- diff_notes_disabled = (@merge_request_diff.latest? && !!@start_sha) if @merge_request_diff
- discussion = local_assigns.fetch(:discussion, nil)
- if current_user
%jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
@@ -6,6 +5,5 @@
%button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
title: "Jump to next unresolved discussion",
"aria-label" => "Jump to next unresolved discussion",
- data: { container: "body" },
- disabled: diff_notes_disabled }
+ data: { container: "body" }}
= custom_icon("next_discussion")
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 53ed4fa991d..31db6ee0cad 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -23,7 +23,7 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-%div{ class: container_class }
+%div.groups-header{ class: container_class }
.top-area
%ul.nav-links
%li.active
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 8e4937b7aa0..e44a2bfed9d 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -47,7 +47,7 @@
Repository
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :environments]) do
+ = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 9fe94291db7..277eb71ea73 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -14,9 +14,6 @@
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.preview_markdown_path = "#{preview_markdown_path}";
-- content_for :scripts_body do
- = render "layouts/init_auto_complete" if current_user
-
- content_for :header_content do
.js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 3978fa60d66..cb97181b9e1 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -7,3 +7,6 @@
= text_area_tag attr, nil, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress')
+
+- content_for :scripts_body do
+ = render "layouts/init_auto_complete" if current_user && (@target_project || @project)
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
index 61eff73da26..c2bcfb773a6 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -9,7 +9,7 @@
%thead
%tr
%th Status
- %th Commit
+ %th Build
- if admin
%th Project
%th Runner
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
new file mode 100644
index 00000000000..7f346df8797
--- /dev/null
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -0,0 +1,59 @@
+- @no_container = true
+- page_title "Cycle Analytics"
+= render "projects/pipelines/head"
+
+#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}}
+
+ .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
+ = icon('times', class: 'dismiss-icon', "@click": "dismissLanding()")
+ .row
+ .col-sm-3.col-xs-12.svg-container
+ = custom_icon('icon_cycle_analytics_splash')
+ .col-sm-8.col-xs-12.inner-content
+ %h4
+ 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'
+
+ = icon("spinner spin", "v-show" => "isLoading")
+
+ .wrapper{"v-show" => "!isLoading && !hasError"}
+ .panel.panel-default
+ .panel-heading
+ Pipeline Health
+
+ .content-block
+ .container-fluid
+ .row
+ .col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"}
+ %h3.header {{item.value}}
+ %p.text {{item.title}}
+
+ .col-sm-3.col-xs-12.column
+ .dropdown.inline.js-ca-dropdown
+ %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
+ %span.dropdown-label Last 30 days
+ %i.fa.fa-chevron-down
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ %a{'href' => "#", 'data-value' => '30'}
+ Last 30 days
+ %li
+ %a{'href' => "#", 'data-value' => '90'}
+ Last 90 days
+
+ .bordered-box
+ %ul.content-list
+ %li{"v-for" => "item in analytics.stats"}
+ .container-fluid
+ .row
+ .col-xs-8.title-col
+ %p.title
+ {{item.title}}
+ %p.text
+ {{item.description}}
+ .col-xs-4.value-col
+ %span
+ {{item.value}}
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 8f7b5d1543e..49819519759 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -10,23 +10,25 @@
- else
version #{version_index(@merge_request_diff)}
%span.caret
- %ul.dropdown-menu.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Version:
- %button.dropdown-title-button.dropdown-menu-close
- %i.fa.fa-times.dropdown-menu-close-icon
- - @merge_request_diffs.each do |merge_request_diff|
- %li
- = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
- %strong
- - if merge_request_diff.latest?
- latest version
- - else
- version #{version_index(merge_request_diff)}
- .monospace #{short_sha(merge_request_diff.head_commit_sha)}
- %small
- #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
- = time_ago_with_tooltip(merge_request_diff.created_at)
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times', class: 'dropdown-menu-close-icon')
+ .dropdown-content
+ %ul
+ - @merge_request_diffs.each do |merge_request_diff|
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ %small
+ #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
+ = time_ago_with_tooltip(merge_request_diff.created_at)
- if @merge_request_diff.base_commit_sha
and
@@ -38,27 +40,29 @@
- else
#{@merge_request.target_branch}
%span.caret
- %ul.dropdown-menu.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Compared with:
- %button.dropdown-title-button.dropdown-menu-close
- %i.fa.fa-times.dropdown-menu-close-icon
- - @comparable_diffs.each do |merge_request_diff|
- %li
- = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
- %strong
- - if merge_request_diff.latest?
- latest version
- - else
- version #{version_index(merge_request_diff)}
- .monospace #{short_sha(merge_request_diff.head_commit_sha)}
- %small
- = time_ago_with_tooltip(merge_request_diff.created_at)
- %li
- = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
- %strong
- #{@merge_request.target_branch} (base)
- .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times', class: 'dropdown-menu-close-icon')
+ .dropdown-content
+ %ul
+ - @comparable_diffs.each do |merge_request_diff|
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ %small
+ = time_ago_with_tooltip(merge_request_diff.created_at)
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
+ %strong
+ #{@merge_request.target_branch} (base)
+ .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
- unless @merge_request_diff.latest? && !@start_sha
.comments-disabled-notif.content-block
@@ -67,4 +71,4 @@
Comments are disabled because you're comparing two versions of this merge request.
- else
Comments are disabled because you're viewing an old version of this merge request.
- = link_to 'Show latest version', merge_request_version_path(@project, @merge_request, @merge_request_diff), class: 'btn btn-sm'
+ = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm'
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 494695a03a5..44e645a7e81 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -43,15 +43,16 @@
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
-- @merge_request.environments.each do |environment|
- .mr-widget-heading
- .ci_widget.ci-success
- = ci_icon_for_status("success")
- %span.hidden-sm
- Deployed to
- = succeed '.' do
- = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment'
- - external_url = environment.external_url
- - if external_url
- = link_to external_url, target: '_blank' do
- = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
+- @merge_request.environments.sort_by(&:name).each do |environment|
+ - if can?(current_user, :read_environment, environment)
+ .mr-widget-heading
+ .ci_widget.ci-success
+ = ci_icon_for_status("success")
+ %span.hidden-sm
+ Deployed to
+ = succeed '.' do
+ = link_to environment.name, environment_path(environment), class: 'environment'
+ - external_url = environment.external_url
+ - if external_url
+ = link_to external_url, target: '_blank' do
+ = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 74538a9723e..8352eba7446 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -14,9 +14,9 @@
.disabled-comment.text-center
.disabled-comment-text.inline
Please
- = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
+ = link_to "sign up", new_session_path(:user, redirect_to_referer: 'yes')
or
- = link_to "login", new_session_path(:user, redirect_to_referer: 'yes')
+ = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
to post a comment
:javascript
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index f611ddc8f5f..5f571499e80 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -19,3 +19,9 @@
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span
Environments
+
+ - if can?(current_user, :read_cycle_analytics, @project)
+ = nav_link(controller: %w(cycle_analytics)) do
+ = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do
+ %span
+ Cycle Analytics
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index 0628134b1bb..0193800dedf 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -1,4 +1,4 @@
-%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch), branch_id: protected_branch.id } }
+%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
%td
= protected_branch.name
- if @project.root_ref?(protected_branch.name)
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index a5a5619fa12..4aa4ab46a2f 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -3,7 +3,7 @@
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do
New Snippet
- if can?(current_user, :update_project_snippet, @snippet)
- = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
- if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index b70fda88a79..9503dbded13 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -2,13 +2,16 @@
= render 'shared/snippets/header'
-%article.file-holder.snippet-file-content
- .file-title
- = blob_icon 0, @snippet.file_name
- = @snippet.file_name
- .file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
- = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
- = render 'shared/snippets/blob'
-
-%div#notes= render "projects/notes/notes_with_form"
+.project-snippets
+ %article.file-holder.snippet-file-content
+ .file-title
+ = blob_icon 0, @snippet.file_name
+ = @snippet.file_name
+ .file-actions
+ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+ = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
+ = render 'shared/snippets/blob'
+
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+
+ %div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index cf16c203f9c..73d288e2236 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,10 +1,19 @@
+- if @project
+ - counts = milestone_counts(@project.milestones)
+
%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
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
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
All
+ - if @project
+ %span.badge #{counts[:all]}
diff --git a/app/views/shared/icons/_icon_cycle_analytics_splash.svg b/app/views/shared/icons/_icon_cycle_analytics_splash.svg
new file mode 100644
index 00000000000..eb5a962d651
--- /dev/null
+++ b/app/views/shared/icons/_icon_cycle_analytics_splash.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99 102" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m35.12 56.988c4.083-4.385 5.968-12.155 5.968-24.04 0-20.2-15.874-32.16-15.874-32.16-1.114-.954-2.929-.979-4.04 0 0 0-15.874 11.957-15.874 32.16 0 11.882 1.884 19.652 5.968 24.04h23.848"/><mask id="1" width="35.783" height="56.924" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(0-4)"><g transform="translate(32.15 3.976)"><g fill="#6b4fbb"><path d="m11.928 56.988l1.325-1.325v3.313c0 .737.59 1.325 1.325 1.325h17.229c.736 0 1.325-.59 1.325-1.325v-3.313l1.325 1.325h-22.53m22.53-1.325v3.313c0 1.464-1.18 2.651-2.651 2.651h-17.229c-1.464 0-2.651-1.178-2.651-2.651v-3.313h22.53m-5.964 7.361h.663c0 3.294-2.67 5.964-5.964 5.964-3.294 0-5.964-2.67-5.964-5.964h.663.663c0 2.562 2.077 4.639 4.639 4.639 2.562 0 4.639-2.077 4.639-4.639h.663"/><path d="m5.816 42.535c-.346-2.839-.515-6.03-.515-9.584 0-20.2 15.874-32.16 15.874-32.16 1.106-.979 2.921-.954 4.04 0 0 0 15.874 11.957 15.874 32.16 0 11.882-1.884 19.652-5.968 24.04h-23.848c-2.861-3.073-4.643-7.807-5.453-14.453-.06-.493-.115-.997-.164-1.511l-4.04 2.884c-.891.637-1.614 2.041-1.614 3.137v14.581c0 1.465.971 1.958 2.165 1.106l8.691-6.208c-.282-.332-.553-.681-.813-1.048l-8.648 6.177c-.147.105-.069.152-.069-.027v-14.581c0-.668.516-1.671 1.059-2.059l3.432-2.451m38.4 20.2c1.193.852 2.165.359 2.165-1.106v-14.581c0-1.096-.723-2.5-1.614-3.137l-4.04-2.884c-.049.514-.104 1.018-.164 1.511l3.432 2.451c.543.388 1.059 1.391 1.059 2.059v14.581c0 .179.078.132-.069.027l-8.648-6.177c-.26.367-.531.716-.813 1.048l8.691 6.208"/></g><use fill="#fff" stroke="#6b4fbb" stroke-width="2.651" mask="url(#1)" xlink:href="#0"/><g fill="#b5a7dd"><path d="m30.482 28.494c0-4.03-3.263-7.289-7.289-7.289-4.03 0-7.289 3.263-7.289 7.289 0 4.03 3.263 7.289 7.289 7.289 4.03 0 7.289-3.263 7.289-7.289m-15.904 0c0-4.758 3.857-8.614 8.614-8.614 4.758 0 8.614 3.857 8.614 8.614 0 4.758-3.857 8.614-8.614 8.614-4.758 0-8.614-3.857-8.614-8.614"/><path d="m27.17 28.494c0-2.196-1.78-3.976-3.976-3.976-2.196 0-3.976 1.78-3.976 3.976 0 2.196 1.78 3.976 3.976 3.976 2.196 0 3.976-1.78 3.976-3.976m-9.277 0c0-2.928 2.373-5.301 5.301-5.301 2.928 0 5.301 2.373 5.301 5.301 0 2.928-2.373 5.301-5.301 5.301-2.928 0-5.301-2.373-5.301-5.301"/></g><path fill="#6b4fbb" d="m34.458 87.47c0 1.098.89 1.988 1.988 1.988 1.098 0 1.988-.89 1.988-1.988 0-.366.297-.663.663-.663.366 0 .663.297.663.663 0 1.83-1.483 3.313-3.313 3.313-1.826 0-3.307-1.478-3.313-3.302 0-.002 0-.003 0-.005v-2.663c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.657m-21.2-6.615c0-.002 0-.003 0-.005v-2.663c0-.358-.297-.657-.663-.657-.369 0-.663.294-.663.657v2.657c0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 1.83 1.483 3.313 3.313 3.313 1.826 0 3.307-1.477 3.313-3.302m5.301 7.285c0-.001 0-.002 0-.003v-16.576c0-.362-.297-.658-.663-.658-.369 0-.663.295-.663.658v16.571c0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.745 2.225 4.97 4.97 4.97 2.742 0 4.966-2.221 4.97-4.963m10.602 8.607v-18.555c0-.365-.297-.661-.663-.661-.369 0-.663.296-.663.661v18.557c0 0 0 0 0 .001.001 2.744 2.226 4.968 4.97 4.968 2.745 0 4.97-2.225 4.97-4.97 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m3.976-25.19c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m5.301 0c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-5.301 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-13.253c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663"/></g><path fill="#e2ddf2" d="m97.75 76.54c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m-60.964-57.651c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645"/><path fill="#b5a7dd" d="m98.41 34.458c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988m-86.14 20.542c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988"/></g></svg>
diff --git a/app/views/shared/icons/_icon_fork.svg b/app/views/shared/icons/_icon_fork.svg
index fc970e4ce50..ce22b6cdaea 100644
--- a/app/views/shared/icons/_icon_fork.svg
+++ b/app/views/shared/icons/_icon_fork.svg
@@ -1,3 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40">
- <path fill="#7E7E7E" fill-rule="evenodd" d="M22,29.5351288 L22,22.7193602 C26.1888699,21.5098039 29.3985457,16.802989 29.3985457,16.802989 C29.740988,16.3567547 30,15.5559546 30,15.0081969 L30,10.4648712 C31.1956027,9.77325238 32,8.48056471 32,7 C32,4.790861 30.209139,3 28,3 C25.790861,3 24,4.790861 24,7 C24,8.48056471 24.8043973,9.77325238 26,10.4648712 L26,14.7083871 C26,14.8784435 25.9055559,15.0987329 25.7890533,15.2104147 C25.7890533,15.2104147 24.5373893,16.4126202 23.9488702,16.9515733 C22.5015398,18.2770075 21.1191354,19 20.090554,19 C19.0477772,19 17.6172728,18.2608988 16.1128852,16.9142923 C15.5030182,16.3683886 14.3672121,15.3403307 14.3672121,15.3403307 C14.1659605,15.1583364 14.0000086,14.7846305 14.0000192,14.5088473 C14.0000192,14.5088473 14.0000932,12.7539451 14.0001308,10.4647956 C15.1956614,9.77315812 16,8.48051074 16,7 C16,4.790861 14.209139,3 12,3 C9.790861,3 8,4.790861 8,7 C8,8.48056471 8.80439726,9.77325238 10,10.4648712 L10,15.0081969 C10,15.5446944 10.2736352,16.3534183 10.6111812,16.7893819 C10.6111812,16.7893819 13.8599776,21.3779363 18,22.6668724 L18,29.5351288 C16.8043973,30.2267476 16,31.5194353 16,33 C16,35.209139 17.790861,37 20,37 C22.209139,37 24,35.209139 24,33 C24,31.5194353 23.1956027,30.2267476 22,29.5351288 Z M14,7 C14,5.8954305 13.1045695,5 12,5 C10.8954305,5 10,5.8954305 10,7 C10,8.1045695 10.8954305,9 12,9 C13.1045695,9 14,8.1045695 14,7 Z M30,7 C30,5.8954305 29.1045695,5 28,5 C26.8954305,5 26,5.8954305 26,7 C26,8.1045695 26.8954305,9 28,9 C29.1045695,9 30,8.1045695 30,7 Z M22,33 C22,31.8954305 21.1045695,31 20,31 C18.8954305,31 18,31.8954305 18,33 C18,34.1045695 18.8954305,35 20,35 C21.1045695,35 22,34.1045695 22,33 Z"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40"><path fill="#7E7E7E" fill-rule="evenodd" d="M22 29.535V22.72c4.19-1.21 7.4-5.917 7.4-5.917.34-.446.6-1.247.6-1.795v-4.543C31.196 9.773 32 8.48 32 7c0-2.21-1.79-4-4-4s-4 1.79-4 4c0 1.48.804 2.773 2 3.465v4.243c0 .17-.094.39-.21.502 0 0-1.253 1.203-1.84 1.742C22.5 18.277 21.12 19 20.09 19c-1.042 0-2.473-.74-3.977-2.086-.61-.546-1.746-1.574-1.746-1.574-.2-.182-.367-.555-.367-.83v-4.045C15.196 9.773 16 8.48 16 7c0-2.21-1.79-4-4-4S8 4.79 8 7c0 1.48.804 2.773 2 3.465v4.543c0 .537.274 1.345.61 1.78 0 0 3.25 4.59 7.39 5.88v6.867c-1.196.692-2 1.984-2 3.465 0 2.21 1.79 4 4 4s4-1.79 4-4c0-1.48-.804-2.773-2-3.465zM14 7c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2zm16 0c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2zm-8 26c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2z"/></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 93c4d5c3d30..cf26197f7d7 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -2,9 +2,9 @@
.issues-filters
.issues-details-filters.row-content-block.second-block
- = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do
- - if params[:issue_search].present?
- = hidden_field_tag :issue_search, params[:issue_search]
+ = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
+ - if params[:search].present?
+ = hidden_field_tag :search, params[:search]
- if @bulk_edit
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
@@ -29,7 +29,7 @@
= render "shared/issuable/label_dropdown"
.filter-item.inline.reset-filters
- %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search])} Reset filters
+ %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters
.pull-right
- if boards_page
diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml
index 186963b32b8..2c89217cadd 100644
--- a/app/views/shared/issuable/_search_form.html.haml
+++ b/app/views/shared/issuable/_search_form.html.haml
@@ -1,2 +1,2 @@
-= form_tag(path, method: :get, id: "issue_search_form", class: 'issue-search-form') do
- = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by name ...', class: 'form-control issue_search search-text-input input-short', spellcheck: false }
+= form_tag(path, method: :get, id: "issuable_search_form", class: 'issuable-search-form') do
+ = search_field_tag :search, params[:search], { id: 'issuable_search', placeholder: 'Filter by name ...', class: 'form-control issuable_search search-text-input input-short', spellcheck: false }
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index acc3ccf4dcf..3dccfb147bf 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -33,7 +33,7 @@
- if @project
.row
.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?
= link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do
Edit
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index fa403da8f79..cd89155c616 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -10,3 +10,5 @@
= clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
= link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
= render 'shared/snippets/blob'
+
+= render 'award_emoji/awards_block', awardable: @snippet, inline: true \ No newline at end of file
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 9a052abe40a..2a57ac90bab 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -10,72 +10,72 @@
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
.user-profile
- .cover-block
+ .cover-block.user-cover-block
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-gray' do
= icon('pencil')
- elsif current_user
- %span.report-abuse
- - if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
- = icon('exclamation-circle')
- - else
- = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
- = icon('exclamation-circle')
+ - if @user.abuse_report
+ %button.btn.btn-danger{ title: 'Already reported for abuse',
+ data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }}
+ = icon('exclamation-circle')
+ - else
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
+ title: 'Report abuse', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('exclamation-circle')
- if current_user
- &nbsp;
= link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
= icon('rss')
- if current_user.admin?
- &nbsp;
= link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users')
- .avatar-holder
- = link_to avatar_icon(@user, 400), target: '_blank' do
- = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
- .cover-title
- = @user.name
-
- .cover-desc
- %span.middle-dot-divider
- @#{@user.username}
- %span.middle-dot-divider
- Member since #{@user.created_at.to_s(:medium)}
+ .profile-header
+ .avatar-holder
+ = link_to avatar_icon(@user, 400), target: '_blank' do
+ = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
+
+ .user-info
+ .cover-title
+ = @user.name
+ %span.handle
+ @#{@user.username}
+
+ .cover-desc.member-date
+ %span.middle-dot-divider
+ Member since #{@user.created_at.to_s(:medium)}
+
+ .cover-desc
+ - unless @user.public_email.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.public_email, "mailto:#{@user.public_email}"
+ - unless @user.skype.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "skype:#{@user.skype}", title: "Skype" do
+ = icon('skype')
+ - unless @user.linkedin.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
+ = icon('linkedin-square')
+ - unless @user.twitter.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
+ = icon('twitter-square')
+ - unless @user.website_url.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.short_website_url, @user.full_website_url
+ - unless @user.location.blank?
+ .profile-link-holder.middle-dot-divider
+ = icon('map-marker')
+ = @user.location
- if @user.bio.present?
.cover-desc
%p.profile-user-bio
= @user.bio
- .cover-desc
- - unless @user.public_email.blank?
- .profile-link-holder.middle-dot-divider
- = link_to @user.public_email, "mailto:#{@user.public_email}"
- - unless @user.skype.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "skype:#{@user.skype}", title: "Skype" do
- = icon('skype')
- - unless @user.linkedin.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
- = icon('linkedin-square')
- - unless @user.twitter.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
- = icon('twitter-square')
- - unless @user.website_url.blank?
- .profile-link-holder.middle-dot-divider
- = link_to @user.short_website_url, @user.full_website_url
- - unless @user.location.blank?
- .profile-link-holder.middle-dot-divider
- = icon('map-marker')
- = @user.location
-
%ul.nav-links.center.user-profile-nav
%li.js-activity-tab
= link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do