Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce
This commit is contained in:
commit
c2a7e7b8ac
279 changed files with 6164 additions and 1611 deletions
12
.pkgr.yml
12
.pkgr.yml
|
@ -3,6 +3,8 @@ group: git
|
|||
services:
|
||||
- postgres
|
||||
before_precompile: ./bin/pkgr_before_precompile.sh
|
||||
env:
|
||||
- SKIP_STORAGE_VALIDATION=true
|
||||
targets:
|
||||
debian-7: &wheezy
|
||||
build_dependencies:
|
||||
|
@ -25,6 +27,16 @@ targets:
|
|||
- libicu52
|
||||
- libpcre3
|
||||
- git
|
||||
ubuntu-16.04:
|
||||
build_dependencies:
|
||||
- libkrb5-dev
|
||||
- libicu-dev
|
||||
- cmake
|
||||
- pkg-config
|
||||
dependencies:
|
||||
- libicu55
|
||||
- libpcre3
|
||||
- git
|
||||
centos-6:
|
||||
build_dependencies:
|
||||
- krb5-devel
|
||||
|
|
41
CHANGELOG
41
CHANGELOG
|
@ -3,32 +3,46 @@ Please view this file on the master branch, on stable branches it's out of date.
|
|||
v 8.12.0 (unreleased)
|
||||
- Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
|
||||
- Only check :can_resolve permission if the note is resolvable
|
||||
- Bump fog-aws to v0.11.0 to support ap-south-1 region
|
||||
- Add ability to fork to a specific namespace using API. (ritave)
|
||||
- Allow to set request_access_enabled for groups and projects
|
||||
- Cleanup misalignments in Issue list view !6206
|
||||
- Only create a protected branch upon a push to a new branch if a rule for that branch doesn't exist
|
||||
- Prune events older than 12 months. (ritave)
|
||||
- Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
|
||||
- Fix issues/merge-request templates dropdown for forked projects
|
||||
- Filter tags by name !6121
|
||||
- Update gitlab shell secret file also when it is empty. !3774 (glensc)
|
||||
- Give project selection dropdowns responsive width, make non-wrapping.
|
||||
- Fix note form hint showing slash commands supported for commits.
|
||||
- Make push events have equal vertical spacing.
|
||||
- API: Ensure invitees are not returned in Members API.
|
||||
- Add two-factor recovery endpoint to internal API !5510
|
||||
- Pass the "Remember me" value to the U2F authentication form
|
||||
- Display stages in valid order in stages dropdown on build page
|
||||
- Only update projects.last_activity_at once per hour when creating a new event
|
||||
- Cycle analytics (first iteration) !5986
|
||||
- Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
|
||||
- Move pushes_since_gc from the database to Redis
|
||||
- Add font color contrast to external label in admin area (ClemMakesApps)
|
||||
- Change logo animation to CSS (ClemMakesApps)
|
||||
- Instructions for enabling Git packfile bitmaps !6104
|
||||
- Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint
|
||||
- Fix long comments in diffs messing with table width
|
||||
- Fix pagination on user snippets page
|
||||
- Run CI builds with the permissions of users !5735
|
||||
- Fix sorting of issues in API
|
||||
- Fix download artifacts button links !6407
|
||||
- Sort project variables by key. !6275 (Diego Souza)
|
||||
- Ensure specs on sorting of issues in API are deterministic on MySQL
|
||||
- Added ability to use predefined CI variables for environment name
|
||||
- Added ability to specify URL in environment configuration in gitlab-ci.yml
|
||||
- Escape search term before passing it to Regexp.new !6241 (winniehell)
|
||||
- Fix pinned sidebar behavior in smaller viewports !6169
|
||||
- Fix file permissions change when updating a file on the Gitlab UI !5979
|
||||
- Change merge_error column from string to text type
|
||||
- Reduce contributions calendar data payload (ClemMakesApps)
|
||||
- Replace contributions calendar timezone payload with dates (ClemMakesApps)
|
||||
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
|
||||
- Enable pipeline events by default !6278
|
||||
- Move parsing of sidekiq ps into helper !6245 (pascalbetz)
|
||||
|
@ -36,6 +50,7 @@ v 8.12.0 (unreleased)
|
|||
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
|
||||
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
|
||||
- Fix blame table layout width
|
||||
- Spec testing if issue authors can read issues on private projects
|
||||
- Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
|
||||
- Request only the LDAP attributes we need !6187
|
||||
- Center build stage columns in pipeline overview (ClemMakesApps)
|
||||
|
@ -43,11 +58,13 @@ v 8.12.0 (unreleased)
|
|||
- Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
|
||||
- Fix bug stopping issue description being scrollable after selecting issue template
|
||||
- Remove suggested colors hover underline (ClemMakesApps)
|
||||
- Fix jump to discussion button being displayed on commit notes
|
||||
- Shorten task status phrase (ClemMakesApps)
|
||||
- Fix project visibility level fields on settings
|
||||
- Add hover color to emoji icon (ClemMakesApps)
|
||||
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
|
||||
- Add textarea autoresize after comment (ClemMakesApps)
|
||||
- Do not write SSH public key 'comments' to authorized_keys !6381
|
||||
- Refresh todos count cache when an Issue/MR is deleted
|
||||
- Fix branches page dropdown sort alignment (ClemMakesApps)
|
||||
- Hides merge request button on branches page is user doesn't have permissions
|
||||
|
@ -57,8 +74,11 @@ v 8.12.0 (unreleased)
|
|||
- Test migration paths from 8.5 until current release !4874
|
||||
- Replace animateEmoji timeout with eventListener (ClemMakesApps)
|
||||
- Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
|
||||
- Require confirmation when not logged in for unsubscribe links !6223 (Maximiliano Perez Coto)
|
||||
- Add `wiki_page_events` to project hook APIs (Ben Boeckel)
|
||||
- Remove Gitorious import
|
||||
- Loads GFM autocomplete source only when required
|
||||
- Fix issue with slash commands not loading on new issue page
|
||||
- Fix inconsistent background color for filter input field (ClemMakesApps)
|
||||
- Remove prefixes from transition CSS property (ClemMakesApps)
|
||||
- Add Sentry logging to API calls
|
||||
|
@ -71,18 +91,21 @@ v 8.12.0 (unreleased)
|
|||
- Show queued time when showing a pipeline !6084
|
||||
- Remove unused mixins (ClemMakesApps)
|
||||
- Add search to all issue board lists
|
||||
- Scroll active tab into view on mobile
|
||||
- Fix groups sort dropdown alignment (ClemMakesApps)
|
||||
- Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
|
||||
- Use JavaScript tooltips for mentions !5301 (winniehell)
|
||||
- Add hover state to todos !5361 (winniehell)
|
||||
- Fix icon alignment of star and fork buttons !5451 (winniehell)
|
||||
- Fix alignment of icon buttons !5887 (winniehell)
|
||||
- Added Ubuntu 16.04 support for packager.io (JonTheNiceGuy)
|
||||
- Fix markdown help references (ClemMakesApps)
|
||||
- Add last commit time to repo view (ClemMakesApps)
|
||||
- Fix accessibility and visibility of project list dropdown button !6140
|
||||
- Fix missing flash messages on service edit page (airatshigapov)
|
||||
- Added project-specific enable/disable setting for LFS !5997
|
||||
- Added group-specific enable/disable setting for LFS !6164
|
||||
- Add optional 'author' param when making commits. !5822 (dandunckelman)
|
||||
- Don't expose a user's token in the `/api/v3/user` API (!6047)
|
||||
- Remove redundant js-timeago-pending from user activity log (ClemMakesApps)
|
||||
- Ability to manage project issues, snippets, wiki, merge requests and builds access level
|
||||
|
@ -100,6 +123,7 @@ v 8.12.0 (unreleased)
|
|||
- Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
|
||||
- Fix repo title alignment (ClemMakesApps)
|
||||
- Change update interval of contacted_at
|
||||
- Add LFS support to SSH !6043
|
||||
- Fix branch title trailing space on hover (ClemMakesApps)
|
||||
- Don't include 'Created By' tag line when importing from GitHub if there is a linked GitLab account (EspadaV8)
|
||||
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
|
||||
|
@ -134,16 +158,23 @@ v 8.12.0 (unreleased)
|
|||
- Refactor the triggers page and documentation !6217
|
||||
- Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Use default clone protocol on "check out, review, and merge locally" help page URL
|
||||
- Let the user choose a namespace and name on GitHub imports
|
||||
- API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska)
|
||||
- Allow bulk update merge requests from merge requests index page
|
||||
- Ensure validation messages are shown within the milestone form
|
||||
- Add notification_settings API calls !5632 (mahcsig)
|
||||
- Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Fix URLs with anchors in wiki !6300 (houqp)
|
||||
- Use a ConnectionPool for Rails.cache on Sidekiq servers
|
||||
- Deleting source project with existing fork link will close all related merge requests !6177 (Katarzyna Kobierska Ula Budziszeska)
|
||||
- Return 204 instead of 404 for /ci/api/v1/builds/register.json if no builds are scheduled for a runner !6225
|
||||
- Fix Gitlab::Popen.popen thread-safety issue
|
||||
- Add specs to removing project (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Clean environment variables when running git hooks
|
||||
- Add UX improvements for merge request version diffs
|
||||
- Fix Import/Export issues importing protected branches and some specific models
|
||||
- Fix non-master branch readme display in tree view
|
||||
- Add UX improvements for merge request version diffs
|
||||
|
||||
v 8.11.6
|
||||
- Fix unnecessary horizontal scroll area in pipeline visualizations. !6005
|
||||
|
@ -166,6 +197,7 @@ v 8.11.5
|
|||
- Scope webhooks/services that will run for confidential issues
|
||||
- Remove gitorious from import_sources
|
||||
- Fix confidential issues being exposed as public using gitlab.com export
|
||||
- Use oj gem for faster JSON processing
|
||||
|
||||
v 8.11.4
|
||||
- Fix resolving conflicts on forks. !6082
|
||||
|
@ -589,6 +621,7 @@ v 8.10.0
|
|||
- Export and import avatar as part of project import/export
|
||||
- Fix migration corrupting import data for old version upgrades
|
||||
- Show tooltip on GitLab export link in new project page
|
||||
- Fix import_data wrongly saved as a result of an invalid import_url !5206
|
||||
|
||||
v 8.9.9
|
||||
- Exclude some pending or inactivated rows in Member scopes
|
||||
|
@ -609,12 +642,6 @@ v 8.9.6
|
|||
- Keeps issue number when importing from Gitlab.com
|
||||
- Add Pending tab for Builds (Katarzyna Kobierska, Urszula Budziszewska)
|
||||
|
||||
v 8.9.7 (unreleased)
|
||||
- Fix import_data wrongly saved as a result of an invalid import_url
|
||||
|
||||
v 8.9.6
|
||||
- Fix importing of events under notes for GitLab projects
|
||||
|
||||
v 8.9.5
|
||||
- Add more debug info to import/export and memory killer. !5108
|
||||
- Fixed avatar alignment in new MR view. !5095
|
||||
|
@ -1880,7 +1907,7 @@ v 8.1.3
|
|||
- Use issue editor as cross reference comment author when issue is edited with a new mention
|
||||
- Add Facebook authentication
|
||||
|
||||
v 8.1.1
|
||||
v 8.1.2
|
||||
- Fix cloning Wiki repositories via HTTP (Stan Hu)
|
||||
- Add migration to remove satellites directory
|
||||
- Fix specific runners visibility
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.8.1
|
||||
0.8.2
|
||||
|
|
7
Gemfile
7
Gemfile
|
@ -135,8 +135,7 @@ gem 'after_commit_queue', '~> 1.3.0'
|
|||
gem 'acts-as-taggable-on', '~> 3.4'
|
||||
|
||||
# Background jobs
|
||||
gem 'sinatra', '~> 1.4.4', require: false
|
||||
gem 'sidekiq', '~> 4.0'
|
||||
gem 'sidekiq', '~> 4.2'
|
||||
gem 'sidekiq-cron', '~> 0.4.0'
|
||||
gem 'redis-namespace', '~> 1.5.2'
|
||||
|
||||
|
@ -206,6 +205,9 @@ gem 'mousetrap-rails', '~> 1.4.6'
|
|||
# Detect and convert string character encoding
|
||||
gem 'charlock_holmes', '~> 0.7.3'
|
||||
|
||||
# Faster JSON
|
||||
gem 'oj', '~> 2.17.4'
|
||||
|
||||
# Parse time & duration
|
||||
gem 'chronic', '~> 0.10.2'
|
||||
gem 'chronic_duration', '~> 0.10.6'
|
||||
|
@ -317,6 +319,7 @@ group :test do
|
|||
gem 'webmock', '~> 1.21.0'
|
||||
gem 'test_after_commit', '~> 0.4.2'
|
||||
gem 'sham_rack', '~> 1.3.6'
|
||||
gem 'timecop', '~> 0.8.0'
|
||||
end
|
||||
|
||||
group :production do
|
||||
|
|
24
Gemfile.lock
24
Gemfile.lock
|
@ -189,7 +189,7 @@ GEM
|
|||
erubis (2.7.0)
|
||||
escape_utils (1.1.1)
|
||||
eventmachine (1.0.8)
|
||||
excon (0.49.0)
|
||||
excon (0.52.0)
|
||||
execjs (2.6.0)
|
||||
expression_parser (0.9.0)
|
||||
factory_girl (4.5.0)
|
||||
|
@ -215,8 +215,8 @@ GEM
|
|||
flowdock (0.7.1)
|
||||
httparty (~> 0.7)
|
||||
multi_json
|
||||
fog-aws (0.9.2)
|
||||
fog-core (~> 1.27)
|
||||
fog-aws (0.11.0)
|
||||
fog-core (~> 1.38)
|
||||
fog-json (~> 1.0)
|
||||
fog-xml (~> 0.1)
|
||||
ipaddress (~> 0.8)
|
||||
|
@ -225,7 +225,7 @@ GEM
|
|||
fog-core (~> 1.27)
|
||||
fog-json (~> 1.0)
|
||||
fog-xml (~> 0.1)
|
||||
fog-core (1.40.0)
|
||||
fog-core (1.42.0)
|
||||
builder
|
||||
excon (~> 0.49)
|
||||
formatador (~> 0.2)
|
||||
|
@ -427,6 +427,7 @@ GEM
|
|||
rack (>= 1.2, < 3)
|
||||
octokit (4.3.0)
|
||||
sawyer (~> 0.7.0, >= 0.5.3)
|
||||
oj (2.17.4)
|
||||
omniauth (1.3.1)
|
||||
hashie (>= 1.2, < 4)
|
||||
rack (>= 1.0, < 3)
|
||||
|
@ -672,11 +673,11 @@ GEM
|
|||
rack
|
||||
shoulda-matchers (2.8.0)
|
||||
activesupport (>= 3.0.0)
|
||||
sidekiq (4.1.4)
|
||||
sidekiq (4.2.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (~> 1.5)
|
||||
redis (~> 3.2, >= 3.2.1)
|
||||
sinatra (>= 1.4.7)
|
||||
sidekiq-cron (0.4.0)
|
||||
redis-namespace (>= 1.5.2)
|
||||
rufus-scheduler (>= 2.0.24)
|
||||
|
@ -686,10 +687,6 @@ GEM
|
|||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.0)
|
||||
sinatra (1.4.7)
|
||||
rack (~> 1.5)
|
||||
rack-protection (~> 1.4)
|
||||
tilt (>= 1.3, < 3)
|
||||
slack-notifier (1.2.1)
|
||||
slop (3.6.0)
|
||||
spinach (0.8.10)
|
||||
|
@ -904,6 +901,7 @@ DEPENDENCIES
|
|||
nokogiri (~> 1.6.7, >= 1.6.7.2)
|
||||
oauth2 (~> 1.2.0)
|
||||
octokit (~> 4.3.0)
|
||||
oj (~> 2.17.4)
|
||||
omniauth (~> 1.3.1)
|
||||
omniauth-auth0 (~> 1.4.1)
|
||||
omniauth-azure-oauth2 (~> 0.0.6)
|
||||
|
@ -958,10 +956,9 @@ DEPENDENCIES
|
|||
settingslogic (~> 2.0.9)
|
||||
sham_rack (~> 1.3.6)
|
||||
shoulda-matchers (~> 2.8.0)
|
||||
sidekiq (~> 4.0)
|
||||
sidekiq (~> 4.2)
|
||||
sidekiq-cron (~> 0.4.0)
|
||||
simplecov (= 0.12.0)
|
||||
sinatra (~> 1.4.4)
|
||||
slack-notifier (~> 1.2.0)
|
||||
spinach-rails (~> 0.2.1)
|
||||
spinach-rerun-reporter (~> 0.0.2)
|
||||
|
@ -978,6 +975,7 @@ DEPENDENCIES
|
|||
teaspoon-jasmine (~> 2.2.0)
|
||||
test_after_commit (~> 0.4.2)
|
||||
thin (~> 1.7.0)
|
||||
timecop (~> 0.8.0)
|
||||
turbolinks (~> 2.5.0)
|
||||
u2f (~> 0.2.1)
|
||||
uglifier (~> 2.7.2)
|
||||
|
@ -993,4 +991,4 @@ DEPENDENCIES
|
|||
wikicloth (= 0.8.1)
|
||||
|
||||
BUNDLED WITH
|
||||
1.12.5
|
||||
1.13.0
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
[![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
|
||||
[![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
|
||||
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
|
||||
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
|
||||
|
||||
## Canonical source
|
||||
|
||||
The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible.
|
||||
The cannonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
|
||||
|
||||
## Open source software to collaborate on code
|
||||
|
||||
|
|
|
@ -27,10 +27,11 @@
|
|||
$(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
|
||||
$(window).off('resize.build').on('resize.build', this.hideSidebar);
|
||||
$(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
|
||||
$('#js-build-scroll > a').off('click').on('click', this.stepTrace);
|
||||
this.updateArtifactRemoveDate();
|
||||
if ($('#build-trace').length) {
|
||||
this.getInitialBuildTrace();
|
||||
this.initScrollButtonAffix();
|
||||
this.initScrollButtons();
|
||||
}
|
||||
if (this.build_status === "running" || this.build_status === "pending") {
|
||||
$('#autoscroll-button').on('click', function() {
|
||||
|
@ -106,7 +107,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
Build.prototype.initScrollButtonAffix = function() {
|
||||
Build.prototype.initScrollButtons = function() {
|
||||
var $body, $buildScroll, $buildTrace;
|
||||
$buildScroll = $('#js-build-scroll');
|
||||
$body = $('body');
|
||||
|
@ -165,6 +166,14 @@
|
|||
this.populateJobs(stage);
|
||||
};
|
||||
|
||||
Build.prototype.stepTrace = function(e) {
|
||||
e.preventDefault();
|
||||
$currentTarget = $(e.currentTarget);
|
||||
$.scrollTo($currentTarget.attr('href'), {
|
||||
offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
|
||||
});
|
||||
};
|
||||
|
||||
return Build;
|
||||
|
||||
})();
|
||||
|
|
92
app/assets/javascripts/cycle-analytics.js.es6
Normal file
92
app/assets/javascripts/cycle-analytics.js.es6
Normal file
|
@ -0,0 +1,92 @@
|
|||
((global) => {
|
||||
|
||||
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
|
||||
|
||||
gl.CycleAnalytics = class CycleAnalytics {
|
||||
constructor() {
|
||||
const that = this;
|
||||
|
||||
this.isHelpDismissed = $.cookie(COOKIE_NAME);
|
||||
this.vue = new Vue({
|
||||
el: '#cycle-analytics',
|
||||
name: 'CycleAnalytics',
|
||||
created: this.fetchData(),
|
||||
data: this.decorateData({ isLoading: true }),
|
||||
methods: {
|
||||
dismissLanding() {
|
||||
that.dismissLanding();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchData(options) {
|
||||
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.vue.$data = this.decorateData(data);
|
||||
this.initDropdown();
|
||||
})
|
||||
.error((data) => {
|
||||
this.handleError(data);
|
||||
})
|
||||
.always(() => {
|
||||
this.vue.isLoading = false;
|
||||
})
|
||||
}
|
||||
|
||||
decorateData(data) {
|
||||
data.summary = data.summary || [];
|
||||
data.stats = data.stats || [];
|
||||
data.isHelpDismissed = this.isHelpDismissed;
|
||||
data.isLoading = data.isLoading || false;
|
||||
|
||||
data.summary.forEach((item) => {
|
||||
item.value = item.value || '-';
|
||||
});
|
||||
|
||||
data.stats.forEach((item) => {
|
||||
item.value = item.value || '- - -';
|
||||
})
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
handleError(data) {
|
||||
this.vue.$data = {
|
||||
hasError: true,
|
||||
isHelpDismissed: this.isHelpDismissed
|
||||
};
|
||||
|
||||
new Flash('There was an error while fetching cycle analytics data.', 'alert');
|
||||
}
|
||||
|
||||
dismissLanding() {
|
||||
this.vue.isHelpDismissed = true;
|
||||
$.cookie(COOKIE_NAME, true);
|
||||
}
|
||||
|
||||
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.vue.isLoading = true;
|
||||
this.fetchData({ startDate: value });
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})(window.gl || (window.gl = {}));
|
|
@ -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':
|
||||
|
|
|
@ -607,13 +607,15 @@
|
|||
selectedObject = this.renderedData[selectedIndex];
|
||||
}
|
||||
}
|
||||
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 {
|
||||
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + escape(value) + "']");
|
||||
} else if(value) {
|
||||
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
|
||||
}
|
||||
if (el.hasClass(ACTIVE_CLASS)) {
|
||||
if (field.length && el.hasClass(ACTIVE_CLASS)) {
|
||||
el.removeClass(ACTIVE_CLASS);
|
||||
if (isInput) {
|
||||
field.val('');
|
||||
|
@ -623,7 +625,7 @@
|
|||
} else if (el.hasClass(INDETERMINATE_CLASS)) {
|
||||
el.addClass(ACTIVE_CLASS);
|
||||
el.removeClass(INDETERMINATE_CLASS);
|
||||
if (value == null) {
|
||||
if (field.length && value == null) {
|
||||
field.remove();
|
||||
}
|
||||
if (!field.length && fieldName) {
|
||||
|
@ -636,7 +638,7 @@
|
|||
this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
|
||||
}
|
||||
}
|
||||
if (value == null) {
|
||||
if (field.length && value == null) {
|
||||
field.remove();
|
||||
}
|
||||
// Toggle active class for the tick mark
|
||||
|
@ -644,7 +646,7 @@
|
|||
if (value != null) {
|
||||
if (!field.length && fieldName) {
|
||||
this.addInput(fieldName, value, selectedObject);
|
||||
} else {
|
||||
} else if (field.length) {
|
||||
field.val(value).trigger('change');
|
||||
}
|
||||
}
|
||||
|
@ -794,4 +796,4 @@
|
|||
});
|
||||
};
|
||||
|
||||
}).call(this);
|
||||
}).call(this);
|
|
@ -10,24 +10,24 @@
|
|||
ImporterStatus.prototype.initStatusPage = function() {
|
||||
$('.js-add-to-import').off('click').on('click', (function(_this) {
|
||||
return function(e) {
|
||||
var $btn, $namespace_input, $target_field, $tr, id, target_namespace;
|
||||
var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName;
|
||||
$btn = $(e.currentTarget);
|
||||
$tr = $btn.closest('tr');
|
||||
$target_field = $tr.find('.import-target');
|
||||
$namespace_input = $target_field.find('input');
|
||||
$namespace_input = $target_field.find('.js-select-namespace option:selected');
|
||||
id = $tr.attr('id').replace('repo_', '');
|
||||
target_namespace = null;
|
||||
|
||||
newName = null;
|
||||
if ($namespace_input.length > 0) {
|
||||
target_namespace = $namespace_input.prop('value');
|
||||
$target_field.empty().append(target_namespace + "/" + ($target_field.data('project_name')));
|
||||
target_namespace = $namespace_input[0].innerHTML;
|
||||
newName = $target_field.find('#path').prop('value');
|
||||
$target_field.empty().append(target_namespace + "/" + newName);
|
||||
}
|
||||
|
||||
$btn.disable().addClass('is-loading');
|
||||
|
||||
return $.post(_this.import_url, {
|
||||
repo_id: id,
|
||||
target_namespace: target_namespace
|
||||
target_namespace: target_namespace,
|
||||
new_name: newName
|
||||
}, {
|
||||
dataType: 'script'
|
||||
});
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
},
|
||||
initSearch: function() {
|
||||
this.timer = null;
|
||||
return $('#issue_search').off('keyup').on('keyup', function() {
|
||||
return $('#issuable_search').off('keyup').on('keyup', function() {
|
||||
clearTimeout(this.timer);
|
||||
return this.timer = setTimeout(function() {
|
||||
var $form, $input, $search;
|
||||
$search = $('#issue_search');
|
||||
$search = $('#issuable_search');
|
||||
$form = $('.js-filter-form');
|
||||
$input = $("input[name='" + ($search.attr('name')) + "']", $form);
|
||||
if ($input.length === 0) {
|
||||
|
|
|
@ -166,7 +166,7 @@
|
|||
instance.addInput(this.fieldName, label.id);
|
||||
}
|
||||
}
|
||||
if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + escape(this.id(label)) + "']").length) {
|
||||
if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) {
|
||||
selectedClass.push('is-active');
|
||||
}
|
||||
if ($dropdown.hasClass('js-multiselect') && removesAll) {
|
||||
|
|
|
@ -10,11 +10,13 @@
|
|||
};
|
||||
|
||||
$(function() {
|
||||
hideEndFade($('.scrolling-tabs'));
|
||||
var $scrollingTabs = $('.scrolling-tabs');
|
||||
|
||||
hideEndFade($scrollingTabs);
|
||||
$(window).off('resize.nav').on('resize.nav', function() {
|
||||
return hideEndFade($('.scrolling-tabs'));
|
||||
return hideEndFade($scrollingTabs);
|
||||
});
|
||||
return $('.scrolling-tabs').on('scroll', function(event) {
|
||||
$scrollingTabs.off('scroll').on('scroll', function(event) {
|
||||
var $this, currentPosition, maxPosition;
|
||||
$this = $(this);
|
||||
currentPosition = $this.scrollLeft();
|
||||
|
@ -22,6 +24,23 @@
|
|||
$this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
|
||||
return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
|
||||
});
|
||||
|
||||
$scrollingTabs.each(function () {
|
||||
var $this = $(this),
|
||||
scrollingTabWidth = $this.width(),
|
||||
$active = $this.find('.active'),
|
||||
activeWidth = $active.width();
|
||||
|
||||
if ($active.length) {
|
||||
var offset = $active.offset().left + activeWidth;
|
||||
|
||||
if (offset > scrollingTabWidth - 30) {
|
||||
var scrollLeft = scrollingTabWidth / 2;
|
||||
scrollLeft = (offset - scrollLeft) - (activeWidth / 2);
|
||||
$this.scrollLeft(scrollLeft);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
|
|
|
@ -232,10 +232,10 @@
|
|||
$('.hll').removeClass('hll');
|
||||
locationHash = window.location.hash;
|
||||
if (locationHash !== '') {
|
||||
hashClassString = "." + (locationHash.replace('#', ''));
|
||||
dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]';
|
||||
$diffLine = $(locationHash + ":not(.match)", $('#diffs'));
|
||||
if (!$diffLine.is('tr')) {
|
||||
$diffLine = $('#diffs').find("td" + locationHash + ", td" + hashClassString);
|
||||
$diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString);
|
||||
} else {
|
||||
$diffLine = $diffLine.find('td');
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
date.setDate(date.getDate() + i);
|
||||
|
||||
var day = date.getDay();
|
||||
var count = timestamps[date.getTime() * 0.001];
|
||||
var count = timestamps[dateFormat(date, 'yyyy-mm-dd')];
|
||||
|
||||
// Create a new group array if this is the first day of the week
|
||||
// or if is first object
|
||||
|
|
|
@ -164,7 +164,7 @@
|
|||
text-decoration: none;
|
||||
|
||||
&:after {
|
||||
content: url('icon_anchor.svg');
|
||||
content: image-url('icon_anchor.svg');
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
lex
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
|
@ -18,6 +19,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.is-ghost {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.dropdown-menu-issues-board-new {
|
||||
width: 320px;
|
||||
|
||||
|
@ -34,47 +39,13 @@
|
|||
> p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #9c9c9c;
|
||||
}
|
||||
}
|
||||
|
||||
.issue-boards-page {
|
||||
.content-wrapper {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sub-nav,
|
||||
.issues-filters {
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.page-with-sidebar {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.issue-boards-content {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
.content {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.boards-app-loading {
|
||||
|
@ -83,46 +54,38 @@
|
|||
}
|
||||
|
||||
.boards-list {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0;
|
||||
flex-basis: 0;
|
||||
min-height: calc(100vh - 152px);
|
||||
max-height: calc(100vh - 152px);
|
||||
height: calc(100vh - 152px);
|
||||
width: 100%;
|
||||
padding-top: 25px;
|
||||
padding-bottom: 25px;
|
||||
padding-right: ($gl-padding / 2);
|
||||
padding-left: ($gl-padding / 2);
|
||||
overflow-x: scroll;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
height: 475px; // Needed for PhantomJS
|
||||
height: calc(100vh - 220px);
|
||||
min-height: 475px;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
.board {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
min-width: calc(85vw - 15px);
|
||||
max-width: calc(85vw - 15px);
|
||||
margin-bottom: 25px;
|
||||
display: inline-block;
|
||||
width: calc(85vw - 15px);
|
||||
height: 100%;
|
||||
padding-right: ($gl-padding / 2);
|
||||
padding-left: ($gl-padding / 2);
|
||||
white-space: normal;
|
||||
vertical-align: top;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
min-width: 400px;
|
||||
max-width: 400px;
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.board-inner {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: $issue-boards-font-size;
|
||||
background: $background-color;
|
||||
border: 1px solid $border-color;
|
||||
|
@ -193,45 +156,31 @@
|
|||
}
|
||||
|
||||
.board-list {
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
height: 400px;
|
||||
height: calc(100% - 49px);
|
||||
margin-bottom: 0;
|
||||
padding: 5px;
|
||||
list-style: none;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.board-list-loading {
|
||||
margin-top: 10px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.is-ghost {
|
||||
opacity: 0.3;
|
||||
font-size: (26px / $issue-boards-font-size) * 1em;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 10px $gl-padding;
|
||||
background: #fff;
|
||||
border-radius: $border-radius-default;
|
||||
box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
|
||||
list-style: none;
|
||||
|
||||
&.user-can-drag {
|
||||
padding-left: $gl-padding;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
|
@ -256,14 +205,13 @@
|
|||
line-height: 25px;
|
||||
|
||||
.label {
|
||||
margin-right: 4px;
|
||||
margin-right: 5px;
|
||||
font-size: (14px / $issue-boards-font-size) * 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.card-number {
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.issue-boards-search {
|
||||
|
|
121
app/assets/stylesheets/pages/cycle_analytics.scss
Normal file
121
app/assets/stylesheets/pages/cycle_analytics.scss
Normal file
|
@ -0,0 +1,121 @@
|
|||
#cycle-analytics {
|
||||
margin: 24px auto 0;
|
||||
width: 800px;
|
||||
position: relative;
|
||||
|
||||
.panel {
|
||||
|
||||
.content-block {
|
||||
padding: 24px 0;
|
||||
border-bottom: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.column {
|
||||
text-align: center;
|
||||
|
||||
.header {
|
||||
font-size: 30px;
|
||||
line-height: 38px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: $layout-link-gray;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
top: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.bordered-box {
|
||||
border: 1px solid $border-color;
|
||||
@include border-radius($border-radius-default);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
&:text {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.value-col {
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
line-height: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.landing {
|
||||
margin-bottom: $gl-padding;
|
||||
overflow: hidden;
|
||||
|
||||
.dismiss-icon {
|
||||
position: absolute;
|
||||
right: $gl-padding;
|
||||
cursor: pointer;
|
||||
color: #b2b2b2;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin: 0 20px;
|
||||
float: left;
|
||||
width: 136px;
|
||||
height: 136px;
|
||||
}
|
||||
|
||||
.inner-content {
|
||||
width: 480px;
|
||||
float: left;
|
||||
|
||||
h4 {
|
||||
color: $gl-text-color;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #8c8c8c;
|
||||
margin-bottom: $gl-padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fa-spinner {
|
||||
font-size: 28px;
|
||||
position: relative;
|
||||
margin-left: -20px;
|
||||
left: 50%;
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
}
|
|
@ -68,6 +68,11 @@
|
|||
border-collapse: separate;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
table-layout: fixed;
|
||||
|
||||
.diff-line-num {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.line_holder td {
|
||||
line-height: $code_line_height;
|
||||
|
@ -98,10 +103,6 @@
|
|||
}
|
||||
|
||||
tr.line_holder.parallel {
|
||||
.old_line, .new_line {
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
td.line_content.parallel {
|
||||
width: 46%;
|
||||
}
|
||||
|
|
|
@ -373,11 +373,40 @@
|
|||
|
||||
.mr-version-controls {
|
||||
background: $background-color;
|
||||
padding: $gl-btn-padding;
|
||||
color: $gl-placeholder-color;
|
||||
border-bottom: 1px solid $border-color;
|
||||
color: $gl-text-color;
|
||||
|
||||
a.btn-link {
|
||||
color: $gl-dark-link-color;
|
||||
.mr-version-menus-container {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.comments-disabled-notif {
|
||||
padding: 10px 16px;
|
||||
.btn {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mr-version-dropdown,
|
||||
.mr-version-compare-dropdown {
|
||||
margin: 0 7px;
|
||||
}
|
||||
|
||||
.comments-disabled-notif {
|
||||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.dropdown-title {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
.fa-info-circle {
|
||||
color: $orange-normal;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -770,3 +770,13 @@ pre.light-well {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-path {
|
||||
.form-control {
|
||||
min-width: 100px;
|
||||
}
|
||||
.select2-choice {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
|
@ -13,10 +13,18 @@ module IssuableCollections
|
|||
issues_finder.execute
|
||||
end
|
||||
|
||||
def all_issues_collection
|
||||
IssuesFinder.new(current_user, filter_params_all).execute
|
||||
end
|
||||
|
||||
def merge_requests_collection
|
||||
merge_requests_finder.execute
|
||||
end
|
||||
|
||||
def all_merge_requests_collection
|
||||
MergeRequestsFinder.new(current_user, filter_params_all).execute
|
||||
end
|
||||
|
||||
def issues_finder
|
||||
@issues_finder ||= issuable_finder_for(IssuesFinder)
|
||||
end
|
||||
|
@ -54,6 +62,10 @@ module IssuableCollections
|
|||
@filter_params
|
||||
end
|
||||
|
||||
def filter_params_all
|
||||
@filter_params_all ||= filter_params.merge(state: 'all', sort: nil)
|
||||
end
|
||||
|
||||
def set_default_scope
|
||||
params[:scope] = 'all' if params[:scope].blank?
|
||||
end
|
||||
|
|
|
@ -10,6 +10,8 @@ module IssuesAction
|
|||
.preload(:author, :project)
|
||||
.page(params[:page])
|
||||
|
||||
@all_issues = all_issues_collection.non_archived
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.atom { render layout: false }
|
||||
|
|
|
@ -9,5 +9,7 @@ module MergeRequestsAction
|
|||
.non_archived
|
||||
.preload(:author, :target_project)
|
||||
.page(params[:page])
|
||||
|
||||
@all_merge_requests = all_merge_requests_collection.non_archived
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,11 +40,12 @@ class Import::GithubController < Import::BaseController
|
|||
def create
|
||||
@repo_id = params[:repo_id].to_i
|
||||
repo = client.repo(@repo_id)
|
||||
@project_name = repo.name
|
||||
@target_namespace = find_or_create_namespace(repo.owner.login, client.user.login)
|
||||
@project_name = params[:new_name].presence || repo.name
|
||||
namespace_path = params[:target_namespace].presence || current_user.namespace_path
|
||||
@target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
|
||||
|
||||
if current_user.can?(:create_projects, @target_namespace)
|
||||
@project = Gitlab::GithubImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
|
||||
@project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute
|
||||
else
|
||||
render 'unauthorized'
|
||||
end
|
||||
|
|
|
@ -11,7 +11,8 @@ class JwtController < ApplicationController
|
|||
service = SERVICES[params[:service]]
|
||||
return head :not_found unless service
|
||||
|
||||
result = service.new(@project, @user, auth_params).execute
|
||||
result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
|
||||
execute(authentication_abilities: @authentication_result.authentication_abilities || [])
|
||||
|
||||
render json: result, status: result[:http_status]
|
||||
end
|
||||
|
@ -19,31 +20,26 @@ class JwtController < ApplicationController
|
|||
private
|
||||
|
||||
def authenticate_project_or_user
|
||||
@authentication_result = Gitlab::Auth::Result.new
|
||||
|
||||
authenticate_with_http_basic do |login, password|
|
||||
# if it's possible we first try to authenticate project with login and password
|
||||
@project = authenticate_project(login, password)
|
||||
return if @project
|
||||
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
|
||||
|
||||
@user = authenticate_user(login, password)
|
||||
return if @user
|
||||
|
||||
render_403
|
||||
render_403 unless @authentication_result.success? &&
|
||||
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
|
||||
end
|
||||
rescue Gitlab::Auth::MissingPersonalTokenError
|
||||
render_missing_personal_token
|
||||
end
|
||||
|
||||
def render_missing_personal_token
|
||||
render plain: "HTTP Basic: Access denied\n" \
|
||||
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
|
||||
"You can generate one at #{profile_personal_access_tokens_url}",
|
||||
status: 401
|
||||
end
|
||||
|
||||
def auth_params
|
||||
params.permit(:service, :scope, :account, :client_id)
|
||||
end
|
||||
|
||||
def authenticate_project(login, password)
|
||||
if login == 'gitlab-ci-token'
|
||||
Project.with_builds_enabled.find_by(runners_token: password)
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_user(login, password)
|
||||
user = Gitlab::Auth.find_with_user_password(login, password)
|
||||
Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
|
||||
user
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,7 +35,11 @@ class Projects::BuildsController < Projects::ApplicationController
|
|||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render json: @build.to_json(methods: :trace_html)
|
||||
render json: {
|
||||
id: @build.id,
|
||||
status: @build.status,
|
||||
trace_html: @build.trace_html
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
67
app/controllers/projects/cycle_analytics_controller.rb
Normal file
67
app/controllers/projects/cycle_analytics_controller.rb
Normal file
|
@ -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
|
|
@ -4,7 +4,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController
|
|||
include ActionController::HttpAuthentication::Basic
|
||||
include KerberosSpnegoHelper
|
||||
|
||||
attr_reader :user
|
||||
attr_reader :authentication_result
|
||||
|
||||
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
|
||||
|
||||
alias_method :user, :actor
|
||||
|
||||
# Git clients will not know what authenticity token to send along
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
@ -15,32 +19,25 @@ class Projects::GitHttpClientController < Projects::ApplicationController
|
|||
private
|
||||
|
||||
def authenticate_user
|
||||
@authentication_result = Gitlab::Auth::Result.new
|
||||
|
||||
if project && project.public? && download_request?
|
||||
return # Allow access
|
||||
end
|
||||
|
||||
if allow_basic_auth? && basic_auth_provided?
|
||||
login, password = user_name_and_password(request)
|
||||
auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
|
||||
|
||||
if auth_result.type == :ci && download_request?
|
||||
@ci = true
|
||||
elsif auth_result.type == :oauth && !download_request?
|
||||
# Not allowed
|
||||
elsif auth_result.type == :missing_personal_token
|
||||
render_missing_personal_token
|
||||
return # Render above denied access, nothing left to do
|
||||
else
|
||||
@user = auth_result.user
|
||||
end
|
||||
|
||||
if ci? || user
|
||||
if handle_basic_authentication(login, password)
|
||||
return # Allow access
|
||||
end
|
||||
elsif allow_kerberos_spnego_auth? && spnego_provided?
|
||||
@user = find_kerberos_user
|
||||
kerberos_user = find_kerberos_user
|
||||
|
||||
if kerberos_user
|
||||
@authentication_result = Gitlab::Auth::Result.new(
|
||||
kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
|
||||
|
||||
if user
|
||||
send_final_spnego_response
|
||||
return # Allow access
|
||||
end
|
||||
|
@ -48,6 +45,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
|
|||
|
||||
send_challenges
|
||||
render plain: "HTTP Basic: Access denied\n", status: 401
|
||||
rescue Gitlab::Auth::MissingPersonalTokenError
|
||||
render_missing_personal_token
|
||||
end
|
||||
|
||||
def basic_auth_provided?
|
||||
|
@ -114,8 +113,41 @@ class Projects::GitHttpClientController < Projects::ApplicationController
|
|||
render plain: 'Not Found', status: :not_found
|
||||
end
|
||||
|
||||
def handle_basic_authentication(login, password)
|
||||
@authentication_result = Gitlab::Auth.find_for_git_client(
|
||||
login, password, project: project, ip: request.ip)
|
||||
|
||||
return false unless @authentication_result.success?
|
||||
|
||||
if download_request?
|
||||
authentication_has_download_access?
|
||||
else
|
||||
authentication_has_upload_access?
|
||||
end
|
||||
end
|
||||
|
||||
def ci?
|
||||
@ci.present?
|
||||
authentication_result.ci?(project)
|
||||
end
|
||||
|
||||
def lfs_deploy_token?
|
||||
authentication_result.lfs_deploy_token?(project)
|
||||
end
|
||||
|
||||
def authentication_has_download_access?
|
||||
has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code)
|
||||
end
|
||||
|
||||
def authentication_has_upload_access?
|
||||
has_authentication_ability?(:push_code)
|
||||
end
|
||||
|
||||
def has_authentication_ability?(capability)
|
||||
(authentication_abilities || []).include?(capability)
|
||||
end
|
||||
|
||||
def authentication_project
|
||||
authentication_result.project
|
||||
end
|
||||
|
||||
def verify_workhorse_api!
|
||||
|
|
|
@ -86,7 +86,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
|
|||
end
|
||||
|
||||
def access
|
||||
@access ||= Gitlab::GitAccess.new(user, project, 'http')
|
||||
@access ||= Gitlab::GitAccess.new(user, project, 'http', authentication_abilities: authentication_abilities)
|
||||
end
|
||||
|
||||
def access_check
|
||||
|
|
|
@ -23,20 +23,13 @@ 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])
|
||||
|
||||
@all_issues = all_issues_collection
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.atom { render layout: false }
|
||||
|
|
|
@ -31,22 +31,14 @@ 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)
|
||||
|
||||
@labels = @project.labels.where(title: params[:label_name])
|
||||
|
||||
@all_merge_requests = all_merge_requests_collection
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
|
@ -428,6 +420,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def validates_merge_request
|
||||
# If source project was removed and merge request for some reason
|
||||
# wasn't close (Ex. mr from fork to origin)
|
||||
return invalid_mr if !@merge_request.source_project && @merge_request.open?
|
||||
|
||||
# Show git not found page
|
||||
# if there is no saved commits between source & target branch
|
||||
if @merge_request.commits.blank?
|
||||
|
|
|
@ -3,12 +3,19 @@ class SentNotificationsController < ApplicationController
|
|||
|
||||
def unsubscribe
|
||||
@sent_notification = SentNotification.for(params[:id])
|
||||
return render_404 unless @sent_notification && @sent_notification.unsubscribable?
|
||||
|
||||
return render_404 unless @sent_notification && @sent_notification.unsubscribable?
|
||||
return unsubscribe_and_redirect if current_user || params[:force]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unsubscribe_and_redirect
|
||||
noteable = @sent_notification.noteable
|
||||
noteable.unsubscribe(@sent_notification.recipient)
|
||||
|
||||
flash[:notice] = "You have been unsubscribed from this thread."
|
||||
|
||||
if current_user
|
||||
case noteable
|
||||
when Issue
|
||||
|
|
|
@ -73,7 +73,7 @@ class UsersController < ApplicationController
|
|||
|
||||
def calendar
|
||||
calendar = contributions_calendar
|
||||
@timestamps = calendar.timestamps
|
||||
@activity_dates = calendar.activity_dates
|
||||
|
||||
render 'calendar', layout: false
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
||||
|
@ -280,23 +280,14 @@ module ApplicationHelper
|
|||
end
|
||||
end
|
||||
|
||||
def state_filters_text_for(entity, project)
|
||||
def state_filters_text_for(state, records)
|
||||
titles = {
|
||||
opened: "Open"
|
||||
}
|
||||
|
||||
entity_title = titles[entity] || entity.to_s.humanize
|
||||
|
||||
count =
|
||||
if project.nil?
|
||||
nil
|
||||
elsif current_controller?(:issues)
|
||||
project.issues.visible_to_user(current_user).send(entity).count
|
||||
elsif current_controller?(:merge_requests)
|
||||
project.merge_requests.send(entity).count
|
||||
end
|
||||
|
||||
html = content_tag :span, entity_title
|
||||
state_title = titles[state] || state.to_s.humanize
|
||||
count = records.public_send(state).size
|
||||
html = content_tag :span, state_title
|
||||
|
||||
if count.present?
|
||||
html += " "
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -25,13 +25,21 @@ module LfsHelper
|
|||
def lfs_download_access?
|
||||
return false unless project.lfs_enabled?
|
||||
|
||||
project.public? || ci? || (user && user.can?(:download_code, project))
|
||||
project.public? || ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
|
||||
end
|
||||
|
||||
def user_can_download_code?
|
||||
has_authentication_ability?(:download_code) && can?(user, :download_code, project)
|
||||
end
|
||||
|
||||
def build_can_download_code?
|
||||
has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
|
||||
end
|
||||
|
||||
def lfs_upload_access?
|
||||
return false unless project.lfs_enabled?
|
||||
|
||||
user && user.can?(:push_code, project)
|
||||
has_authentication_ability?(:push_code) && can?(user, :push_code, project)
|
||||
end
|
||||
|
||||
def render_lfs_forbidden
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
module NamespacesHelper
|
||||
def namespaces_options(selected = :current_user, display_path: false)
|
||||
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
|
||||
groups = current_user.owned_groups + current_user.masters_groups
|
||||
|
||||
groups << extra_group if extra_group && !Group.exists?(name: extra_group.name)
|
||||
|
||||
users = [current_user.namespace]
|
||||
|
||||
data_attr_group = { 'data-options-parent' => 'groups' }
|
||||
|
|
|
@ -10,6 +10,10 @@ module NotesHelper
|
|||
Ability.can_edit_note?(current_user, note)
|
||||
end
|
||||
|
||||
def note_supports_slash_commands?(note)
|
||||
Notes::SlashCommandsService.supported?(note, current_user)
|
||||
end
|
||||
|
||||
def noteable_json(noteable)
|
||||
{
|
||||
id: noteable.id,
|
||||
|
|
|
@ -108,6 +108,12 @@ class Notify < BaseMailer
|
|||
headers["X-GitLab-#{model.class.name}-ID"] = model.id
|
||||
headers['X-GitLab-Reply-Key'] = reply_key
|
||||
|
||||
if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
|
||||
headers['List-Unsubscribe'] = unsubscribe_sent_notification_url(@sent_notification, force: true)
|
||||
|
||||
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
|
||||
end
|
||||
|
||||
if Gitlab::IncomingEmail.enabled?
|
||||
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
|
||||
address.display_name = @project.name_with_namespace
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
module Ci
|
||||
class Build < CommitStatus
|
||||
include TokenAuthenticatable
|
||||
|
||||
belongs_to :runner, class_name: 'Ci::Runner'
|
||||
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
|
||||
belongs_to :erased_by, class_name: 'User'
|
||||
|
@ -23,7 +25,10 @@ module Ci
|
|||
|
||||
acts_as_taggable
|
||||
|
||||
add_authentication_token_field :token
|
||||
|
||||
before_save :update_artifacts_size, if: :artifacts_file_changed?
|
||||
before_save :ensure_token
|
||||
before_destroy { project }
|
||||
|
||||
after_create :execute_hooks
|
||||
|
@ -38,6 +43,7 @@ module Ci
|
|||
new_build.status = 'pending'
|
||||
new_build.runner_id = nil
|
||||
new_build.trigger_request_id = nil
|
||||
new_build.token = nil
|
||||
new_build.save
|
||||
end
|
||||
|
||||
|
@ -79,11 +85,14 @@ module Ci
|
|||
|
||||
after_transition any => [:success] do |build|
|
||||
if build.environment.present?
|
||||
service = CreateDeploymentService.new(build.project, build.user,
|
||||
environment: build.environment,
|
||||
sha: build.sha,
|
||||
ref: build.ref,
|
||||
tag: build.tag)
|
||||
service = CreateDeploymentService.new(
|
||||
build.project, build.user,
|
||||
environment: build.environment,
|
||||
sha: build.sha,
|
||||
ref: build.ref,
|
||||
tag: build.tag,
|
||||
options: build.options[:environment],
|
||||
variables: build.variables)
|
||||
service.execute(build)
|
||||
end
|
||||
end
|
||||
|
@ -173,7 +182,7 @@ module Ci
|
|||
end
|
||||
|
||||
def repo_url
|
||||
auth = "gitlab-ci-token:#{token}@"
|
||||
auth = "gitlab-ci-token:#{ensure_token!}@"
|
||||
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
|
||||
prefix + auth
|
||||
end
|
||||
|
@ -235,12 +244,7 @@ module Ci
|
|||
end
|
||||
|
||||
def trace
|
||||
trace = raw_trace
|
||||
if project && trace.present? && project.runners_token.present?
|
||||
trace.gsub(project.runners_token, 'xxxxxx')
|
||||
else
|
||||
trace
|
||||
end
|
||||
hide_secrets(raw_trace)
|
||||
end
|
||||
|
||||
def trace_length
|
||||
|
@ -253,6 +257,7 @@ module Ci
|
|||
|
||||
def trace=(trace)
|
||||
recreate_trace_dir
|
||||
trace = hide_secrets(trace)
|
||||
File.write(path_to_trace, trace)
|
||||
end
|
||||
|
||||
|
@ -266,6 +271,8 @@ module Ci
|
|||
def append_trace(trace_part, offset)
|
||||
recreate_trace_dir
|
||||
|
||||
trace_part = hide_secrets(trace_part)
|
||||
|
||||
File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
|
||||
File.open(path_to_trace, 'ab') do |f|
|
||||
f.write(trace_part)
|
||||
|
@ -341,12 +348,8 @@ module Ci
|
|||
)
|
||||
end
|
||||
|
||||
def token
|
||||
project.runners_token
|
||||
end
|
||||
|
||||
def valid_token?(token)
|
||||
project.valid_runners_token?(token)
|
||||
self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
|
||||
end
|
||||
|
||||
def has_tags?
|
||||
|
@ -488,5 +491,14 @@ module Ci
|
|||
|
||||
pipeline.config_processor.build_attributes(name)
|
||||
end
|
||||
|
||||
def hide_secrets(trace)
|
||||
return unless trace
|
||||
|
||||
trace = trace.dup
|
||||
Ci::MaskSecret.mask!(trace, project.runners_token) if project
|
||||
Ci::MaskSecret.mask!(trace, token)
|
||||
trace
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ module Ci
|
|||
class Pipeline < ActiveRecord::Base
|
||||
extend Ci::Model
|
||||
include HasStatus
|
||||
include Importable
|
||||
|
||||
self.table_name = 'ci_commits'
|
||||
|
||||
|
@ -12,12 +13,12 @@ module Ci
|
|||
has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
|
||||
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
|
||||
|
||||
validates_presence_of :sha
|
||||
validates_presence_of :ref
|
||||
validates_presence_of :status
|
||||
validate :valid_commit_sha
|
||||
validates_presence_of :sha, unless: :importing?
|
||||
validates_presence_of :ref, unless: :importing?
|
||||
validates_presence_of :status, unless: :importing?
|
||||
validate :valid_commit_sha, unless: :importing?
|
||||
|
||||
after_save :keep_around_commits
|
||||
after_save :keep_around_commits, unless: :importing?
|
||||
|
||||
delegate :stages, to: :statuses
|
||||
|
||||
|
@ -55,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
|
||||
|
@ -241,13 +252,16 @@ module Ci
|
|||
end
|
||||
|
||||
def build_updated
|
||||
case latest_builds_status
|
||||
when 'pending' then enqueue
|
||||
when 'running' then run
|
||||
when 'success' then succeed
|
||||
when 'failed' then drop
|
||||
when 'canceled' then cancel
|
||||
when 'skipped' then skip
|
||||
with_lock do
|
||||
reload
|
||||
case latest_builds_status
|
||||
when 'pending' then enqueue
|
||||
when 'running' then run
|
||||
when 'success' then succeed
|
||||
when 'failed' then drop
|
||||
when 'canceled' then cancel
|
||||
when 'skipped' then skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -276,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
|
||||
|
|
|
@ -69,15 +69,15 @@ class CommitStatus < ActiveRecord::Base
|
|||
commit_status.update_attributes finished_at: Time.now
|
||||
end
|
||||
|
||||
after_transition do |commit_status, transition|
|
||||
commit_status.pipeline.try(:build_updated) unless transition.loopback?
|
||||
end
|
||||
|
||||
after_transition any => [:success, :failed, :canceled] do |commit_status|
|
||||
commit_status.pipeline.try(:process!)
|
||||
true
|
||||
end
|
||||
|
||||
after_transition do |commit_status, transition|
|
||||
commit_status.pipeline.try(:build_updated) unless transition.loopback?
|
||||
end
|
||||
|
||||
after_transition [:created, :pending, :running] => :success do |commit_status|
|
||||
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
|
||||
end
|
||||
|
|
|
@ -20,7 +20,7 @@ module HasStatus
|
|||
skipped = scope.skipped.select('count(*)').to_sql
|
||||
|
||||
deduce_status = "(CASE
|
||||
WHEN (#{builds})=(#{created}) THEN NULL
|
||||
WHEN (#{builds})=(#{created}) THEN 'created'
|
||||
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
|
||||
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
|
||||
WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
|
||||
|
|
|
@ -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
|
||||
|
@ -286,4 +290,9 @@ module Issuable
|
|||
def can_move?(*)
|
||||
false
|
||||
end
|
||||
|
||||
def record_metrics
|
||||
metrics = self.metrics || create_metrics
|
||||
metrics.record!
|
||||
end
|
||||
end
|
||||
|
|
97
app/models/cycle_analytics.rb
Normal file
97
app/models/cycle_analytics.rb
Normal file
|
@ -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
|
24
app/models/cycle_analytics/summary.rb
Normal file
24
app/models/cycle_analytics/summary.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -4,6 +4,7 @@ class Environment < ActiveRecord::Base
|
|||
has_many :deployments
|
||||
|
||||
before_validation :nullify_external_url
|
||||
before_save :set_environment_type
|
||||
|
||||
validates :name,
|
||||
presence: true,
|
||||
|
@ -26,9 +27,24 @@ class Environment < ActiveRecord::Base
|
|||
self.external_url = nil if self.external_url.blank?
|
||||
end
|
||||
|
||||
def set_environment_type
|
||||
names = name.split('/')
|
||||
|
||||
self.environment_type =
|
||||
if names.many?
|
||||
names.first
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def includes_commit?(commit)
|
||||
return false unless last_deployment
|
||||
|
||||
last_deployment.includes_commit?(commit)
|
||||
end
|
||||
|
||||
def update_merge_request_metrics?
|
||||
self.name == "production"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,8 @@ class Event < ActiveRecord::Base
|
|||
LEFT = 9 # User left project
|
||||
DESTROYED = 10
|
||||
|
||||
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
|
||||
|
||||
delegate :name, :email, to: :author, prefix: true, allow_nil: true
|
||||
delegate :title, to: :issue, prefix: true, allow_nil: true
|
||||
delegate :title, to: :merge_request, prefix: true, allow_nil: true
|
||||
|
@ -324,8 +326,27 @@ class Event < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def reset_project_activity
|
||||
if project && Gitlab::ExclusiveLease.new("project:update_last_activity_at:#{project.id}", timeout: 60).try_obtain
|
||||
project.update_column(:last_activity_at, self.created_at)
|
||||
end
|
||||
return unless project
|
||||
|
||||
# Don't even bother obtaining a lock if the last update happened less than
|
||||
# 60 minutes ago.
|
||||
return if recent_update?
|
||||
|
||||
return unless try_obtain_lease
|
||||
|
||||
project.update_column(:last_activity_at, created_at)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def recent_update?
|
||||
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
|
||||
end
|
||||
|
||||
def try_obtain_lease
|
||||
Gitlab::ExclusiveLease.
|
||||
new("project:update_last_activity_at:#{project.id}",
|
||||
timeout: RESET_PROJECT_ACTIVITY_INTERVAL.to_i).
|
||||
try_obtain
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
21
app/models/issue/metrics.rb
Normal file
21
app/models/issue/metrics.rb
Normal file
|
@ -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
|
|
@ -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"))
|
||||
|
@ -652,7 +668,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def environments
|
||||
return unless diff_head_commit
|
||||
return [] unless diff_head_commit
|
||||
|
||||
target_project.environments.select do |environment|
|
||||
environment.includes_commit?(diff_head_commit)
|
||||
|
|
11
app/models/merge_request/metrics.rb
Normal file
11
app/models/merge_request/metrics.rb
Normal file
|
@ -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
|
7
app/models/merge_requests_closing_issues.rb
Normal file
7
app/models/merge_requests_closing_issues.rb
Normal file
|
@ -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
|
|
@ -1137,12 +1137,6 @@ class Project < ActiveRecord::Base
|
|||
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
|
||||
end
|
||||
|
||||
# TODO (ayufan): For now we use runners_token (backward compatibility)
|
||||
# In 8.4 every build will have its own individual token valid for time of build
|
||||
def valid_build_token?(token)
|
||||
self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
|
||||
end
|
||||
|
||||
def build_coverage_enabled?
|
||||
build_coverage_regex.present?
|
||||
end
|
||||
|
|
|
@ -756,62 +756,59 @@ class Repository
|
|||
@root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
|
||||
end
|
||||
|
||||
def commit_dir(user, path, message, branch)
|
||||
def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
|
||||
update_branch_with_hooks(user, branch) do |ref|
|
||||
committer = user_to_committer(user)
|
||||
options = {}
|
||||
options[:committer] = committer
|
||||
options[:author] = committer
|
||||
|
||||
options[:commit] = {
|
||||
message: message,
|
||||
branch: ref,
|
||||
update_ref: false,
|
||||
options = {
|
||||
commit: {
|
||||
branch: ref,
|
||||
message: message,
|
||||
update_ref: false
|
||||
}
|
||||
}
|
||||
|
||||
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
|
||||
|
||||
raw_repository.mkdir(path, options)
|
||||
end
|
||||
end
|
||||
|
||||
def commit_file(user, path, content, message, branch, update)
|
||||
def commit_file(user, path, content, message, branch, update, author_email: nil, author_name: nil)
|
||||
update_branch_with_hooks(user, branch) do |ref|
|
||||
committer = user_to_committer(user)
|
||||
options = {}
|
||||
options[:committer] = committer
|
||||
options[:author] = committer
|
||||
options[:commit] = {
|
||||
message: message,
|
||||
branch: ref,
|
||||
update_ref: false,
|
||||
options = {
|
||||
commit: {
|
||||
branch: ref,
|
||||
message: message,
|
||||
update_ref: false
|
||||
},
|
||||
file: {
|
||||
content: content,
|
||||
path: path,
|
||||
update: update
|
||||
}
|
||||
}
|
||||
|
||||
options[:file] = {
|
||||
content: content,
|
||||
path: path,
|
||||
update: update
|
||||
}
|
||||
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
|
||||
|
||||
Gitlab::Git::Blob.commit(raw_repository, options)
|
||||
end
|
||||
end
|
||||
|
||||
def update_file(user, path, content, branch:, previous_path:, message:)
|
||||
def update_file(user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil)
|
||||
update_branch_with_hooks(user, branch) do |ref|
|
||||
committer = user_to_committer(user)
|
||||
options = {}
|
||||
options[:committer] = committer
|
||||
options[:author] = committer
|
||||
options[:commit] = {
|
||||
message: message,
|
||||
branch: ref,
|
||||
update_ref: false
|
||||
options = {
|
||||
commit: {
|
||||
branch: ref,
|
||||
message: message,
|
||||
update_ref: false
|
||||
},
|
||||
file: {
|
||||
content: content,
|
||||
path: path,
|
||||
update: true
|
||||
}
|
||||
}
|
||||
|
||||
options[:file] = {
|
||||
content: content,
|
||||
path: path,
|
||||
update: true
|
||||
}
|
||||
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
|
||||
|
||||
if previous_path && previous_path != path
|
||||
options[:file][:previous_path] = previous_path
|
||||
|
@ -822,34 +819,39 @@ class Repository
|
|||
end
|
||||
end
|
||||
|
||||
def remove_file(user, path, message, branch)
|
||||
def remove_file(user, path, message, branch, author_email: nil, author_name: nil)
|
||||
update_branch_with_hooks(user, branch) do |ref|
|
||||
committer = user_to_committer(user)
|
||||
options = {}
|
||||
options[:committer] = committer
|
||||
options[:author] = committer
|
||||
options[:commit] = {
|
||||
message: message,
|
||||
branch: ref,
|
||||
update_ref: false,
|
||||
options = {
|
||||
commit: {
|
||||
branch: ref,
|
||||
message: message,
|
||||
update_ref: false
|
||||
},
|
||||
file: {
|
||||
path: path
|
||||
}
|
||||
}
|
||||
|
||||
options[:file] = {
|
||||
path: path
|
||||
}
|
||||
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
|
||||
|
||||
Gitlab::Git::Blob.remove(raw_repository, options)
|
||||
end
|
||||
end
|
||||
|
||||
def user_to_committer(user)
|
||||
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
|
||||
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
time: Time.now
|
||||
author: author,
|
||||
committer: committer
|
||||
}
|
||||
end
|
||||
|
||||
def user_to_committer(user)
|
||||
Gitlab::Git::committer_hash(email: user.email, name: user.name)
|
||||
end
|
||||
|
||||
def can_be_merged?(source_sha, target_branch)
|
||||
our_commit = rugged.branches[target_branch].target
|
||||
their_commit = rugged.lookup(source_sha)
|
||||
|
|
|
@ -46,6 +46,7 @@ class ProjectPolicy < BasePolicy
|
|||
can! :create_issue
|
||||
can! :create_note
|
||||
can! :upload_file
|
||||
can! :read_cycle_analytics
|
||||
end
|
||||
|
||||
def reporter_access!
|
||||
|
@ -64,6 +65,12 @@ class ProjectPolicy < BasePolicy
|
|||
can! :read_deployment
|
||||
end
|
||||
|
||||
# Permissions given when an user is team member of a project
|
||||
def team_member_reporter_access!
|
||||
can! :build_download_code
|
||||
can! :build_read_container_image
|
||||
end
|
||||
|
||||
def developer_access!
|
||||
can! :admin_merge_request
|
||||
can! :update_merge_request
|
||||
|
@ -109,6 +116,8 @@ class ProjectPolicy < BasePolicy
|
|||
can! :read_commit_status
|
||||
can! :read_pipeline
|
||||
can! :read_container_image
|
||||
can! :build_download_code
|
||||
can! :build_read_container_image
|
||||
end
|
||||
|
||||
def owner_access!
|
||||
|
@ -130,10 +139,11 @@ class ProjectPolicy < BasePolicy
|
|||
def team_access!(user)
|
||||
access = project.team.max_member_access(user.id)
|
||||
|
||||
guest_access! if access >= Gitlab::Access::GUEST
|
||||
reporter_access! if access >= Gitlab::Access::REPORTER
|
||||
developer_access! if access >= Gitlab::Access::DEVELOPER
|
||||
master_access! if access >= Gitlab::Access::MASTER
|
||||
guest_access! if access >= Gitlab::Access::GUEST
|
||||
reporter_access! if access >= Gitlab::Access::REPORTER
|
||||
team_member_reporter_access! if access >= Gitlab::Access::REPORTER
|
||||
developer_access! if access >= Gitlab::Access::DEVELOPER
|
||||
master_access! if access >= Gitlab::Access::MASTER
|
||||
end
|
||||
|
||||
def archived_access!
|
||||
|
@ -195,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
|
||||
|
|
|
@ -29,41 +29,11 @@ class AkismetService
|
|||
end
|
||||
|
||||
def submit_ham
|
||||
return false unless akismet_enabled?
|
||||
|
||||
params = {
|
||||
type: 'comment',
|
||||
text: text,
|
||||
author: owner.name,
|
||||
author_email: owner.email
|
||||
}
|
||||
|
||||
begin
|
||||
akismet_client.submit_ham(options[:ip_address], options[:user_agent], params)
|
||||
true
|
||||
rescue => e
|
||||
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
|
||||
false
|
||||
end
|
||||
submit(:ham)
|
||||
end
|
||||
|
||||
def submit_spam
|
||||
return false unless akismet_enabled?
|
||||
|
||||
params = {
|
||||
type: 'comment',
|
||||
text: text,
|
||||
author: owner.name,
|
||||
author_email: owner.email
|
||||
}
|
||||
|
||||
begin
|
||||
akismet_client.submit_spam(options[:ip_address], options[:user_agent], params)
|
||||
true
|
||||
rescue => e
|
||||
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
|
||||
false
|
||||
end
|
||||
submit(:spam)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -76,4 +46,23 @@ class AkismetService
|
|||
def akismet_enabled?
|
||||
current_application_settings.akismet_enabled
|
||||
end
|
||||
|
||||
def submit(type)
|
||||
return false unless akismet_enabled?
|
||||
|
||||
params = {
|
||||
type: 'comment',
|
||||
text: text,
|
||||
author: owner.name,
|
||||
author_email: owner.email
|
||||
}
|
||||
|
||||
begin
|
||||
akismet_client.public_send(type, options[:ip_address], options[:user_agent], params)
|
||||
true
|
||||
rescue => e
|
||||
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,9 @@ module Auth
|
|||
|
||||
AUDIENCE = 'container_registry'
|
||||
|
||||
def execute
|
||||
def execute(authentication_abilities:)
|
||||
@authentication_abilities = authentication_abilities
|
||||
|
||||
return error('not found', 404) unless registry.enabled
|
||||
|
||||
unless current_user || project
|
||||
|
@ -74,9 +76,9 @@ module Auth
|
|||
|
||||
case requested_action
|
||||
when 'pull'
|
||||
requested_project == project || can?(current_user, :read_container_image, requested_project)
|
||||
requested_project.public? || build_can_pull?(requested_project) || user_can_pull?(requested_project)
|
||||
when 'push'
|
||||
requested_project == project || can?(current_user, :create_container_image, requested_project)
|
||||
build_can_push?(requested_project) || user_can_push?(requested_project)
|
||||
else
|
||||
false
|
||||
end
|
||||
|
@ -85,5 +87,29 @@ module Auth
|
|||
def registry
|
||||
Gitlab.config.registry
|
||||
end
|
||||
|
||||
def build_can_pull?(requested_project)
|
||||
# Build can:
|
||||
# 1. pull from its own project (for ex. a build)
|
||||
# 2. read images from dependent projects if creator of build is a team member
|
||||
@authentication_abilities.include?(:build_read_container_image) &&
|
||||
(requested_project == project || can?(current_user, :build_read_container_image, requested_project))
|
||||
end
|
||||
|
||||
def user_can_pull?(requested_project)
|
||||
@authentication_abilities.include?(:read_container_image) &&
|
||||
can?(current_user, :read_container_image, requested_project)
|
||||
end
|
||||
|
||||
def build_can_push?(requested_project)
|
||||
# Build can push only to the project from which it originates
|
||||
@authentication_abilities.include?(:build_create_container_image) &&
|
||||
requested_project == project
|
||||
end
|
||||
|
||||
def user_can_push?(requested_project)
|
||||
@authentication_abilities.include?(:create_container_image) &&
|
||||
can?(current_user, :create_container_image, requested_project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,11 +2,9 @@ require_relative 'base_service'
|
|||
|
||||
class CreateDeploymentService < BaseService
|
||||
def execute(deployable = nil)
|
||||
environment = project.environments.find_or_create_by(
|
||||
name: params[:environment]
|
||||
)
|
||||
environment = find_or_create_environment
|
||||
|
||||
project.deployments.create(
|
||||
deployment = project.deployments.create(
|
||||
environment: environment,
|
||||
ref: params[:ref],
|
||||
tag: params[:tag],
|
||||
|
@ -14,5 +12,43 @@ class CreateDeploymentService < BaseService
|
|||
user: current_user,
|
||||
deployable: deployable
|
||||
)
|
||||
|
||||
deployment.update_merge_request_metrics!
|
||||
|
||||
deployment
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_or_create_environment
|
||||
project.environments.find_or_create_by(name: expanded_name) do |environment|
|
||||
environment.external_url = expanded_url
|
||||
end
|
||||
end
|
||||
|
||||
def expanded_name
|
||||
ExpandVariables.expand(name, variables)
|
||||
end
|
||||
|
||||
def expanded_url
|
||||
return unless url
|
||||
|
||||
@expanded_url ||= ExpandVariables.expand(url, variables)
|
||||
end
|
||||
|
||||
def name
|
||||
params[:environment]
|
||||
end
|
||||
|
||||
def url
|
||||
options[:url]
|
||||
end
|
||||
|
||||
def options
|
||||
params[:options] || {}
|
||||
end
|
||||
|
||||
def variables
|
||||
params[:variables] || []
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,8 @@ module Files
|
|||
params[:file_content]
|
||||
end
|
||||
@last_commit_sha = params[:last_commit_sha]
|
||||
@author_email = params[:author_email]
|
||||
@author_name = params[:author_name]
|
||||
|
||||
# Validate parameters
|
||||
validate
|
||||
|
|
|
@ -3,7 +3,7 @@ require_relative "base_service"
|
|||
module Files
|
||||
class CreateDirService < Files::BaseService
|
||||
def commit
|
||||
repository.commit_dir(current_user, @file_path, @commit_message, @target_branch)
|
||||
repository.commit_dir(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
|
||||
end
|
||||
|
||||
def validate
|
||||
|
|
|
@ -3,7 +3,7 @@ require_relative "base_service"
|
|||
module Files
|
||||
class CreateService < Files::BaseService
|
||||
def commit
|
||||
repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false)
|
||||
repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false, author_email: @author_email, author_name: @author_name)
|
||||
end
|
||||
|
||||
def validate
|
||||
|
|
|
@ -3,7 +3,7 @@ require_relative "base_service"
|
|||
module Files
|
||||
class DeleteService < Files::BaseService
|
||||
def commit
|
||||
repository.remove_file(current_user, @file_path, @commit_message, @target_branch)
|
||||
repository.remove_file(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,9 @@ module Files
|
|||
repository.update_file(current_user, @file_path, @file_content,
|
||||
branch: @target_branch,
|
||||
previous_path: @previous_path,
|
||||
message: @commit_message)
|
||||
message: @commit_message,
|
||||
author_email: @author_email,
|
||||
author_name: @author_name)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -87,7 +87,7 @@ class GitPushService < BaseService
|
|||
project.change_head(branch_name)
|
||||
|
||||
# Set protection on the default branch if configured
|
||||
if current_application_settings.default_branch_protection != PROTECTION_NONE
|
||||
if current_application_settings.default_branch_protection != PROTECTION_NONE && !@project.protected_branch?(@project.default_branch)
|
||||
|
||||
params = {
|
||||
name: @project.default_branch,
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,7 @@ module Milestones
|
|||
def execute
|
||||
milestone = project.milestones.new(params)
|
||||
|
||||
if milestone.save!
|
||||
if milestone.save
|
||||
event_service.open_milestone(milestone, current_user)
|
||||
end
|
||||
|
||||
|
|
|
@ -5,9 +5,18 @@ module Notes
|
|||
'MergeRequest' => MergeRequests::UpdateService
|
||||
}
|
||||
|
||||
def supported?(note)
|
||||
def self.noteable_update_service(note)
|
||||
UPDATE_SERVICES[note.noteable_type]
|
||||
end
|
||||
|
||||
def self.supported?(note, current_user)
|
||||
noteable_update_service(note) &&
|
||||
can?(current_user, :"update_#{note.noteable_type.underscore}", note.noteable)
|
||||
current_user &&
|
||||
current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable)
|
||||
end
|
||||
|
||||
def supported?(note)
|
||||
self.class.supported?(note, current_user)
|
||||
end
|
||||
|
||||
def extract_commands(note)
|
||||
|
@ -21,13 +30,7 @@ module Notes
|
|||
return if command_params.empty?
|
||||
return unless supported?(note)
|
||||
|
||||
noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def noteable_update_service(note)
|
||||
UPDATE_SERVICES[note.noteable_type]
|
||||
self.class.noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
- 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)}'" }
|
||||
|
@ -5,5 +6,6 @@
|
|||
%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" } }
|
||||
data: { container: "body" },
|
||||
disabled: diff_notes_disabled }
|
||||
= custom_icon("next_discussion")
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
.btn-group{ role: "group" }
|
||||
= link_to_reply_discussion(discussion, line_type)
|
||||
= render "discussions/resolve_all", discussion: discussion
|
||||
= render "discussions/jump_to_next", discussion: discussion
|
||||
- if discussion.for_merge_request?
|
||||
= render "discussions/jump_to_next", discussion: discussion
|
||||
- else
|
||||
= link_to_reply_discussion(discussion)
|
||||
|
|
|
@ -45,7 +45,17 @@
|
|||
%td
|
||||
= github_project_link(repo.full_name)
|
||||
%td.import-target
|
||||
= import_project_target(repo.owner.login, repo.name)
|
||||
%fieldset.row
|
||||
.input-group
|
||||
.project-path.input-group-btn
|
||||
- if current_user.can_select_namespace?
|
||||
- selected = params[:namespace_id] || :current_user
|
||||
- opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
|
||||
= select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
|
||||
- else
|
||||
= text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
|
||||
%span.input-group-addon /
|
||||
= text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
|
||||
%td.import-actions.job-status
|
||||
= button_tag class: "btn btn-import js-add-to-import" do
|
||||
Import
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
- if @labels_url
|
||||
adjust your #{link_to 'label subscriptions', @labels_url}.
|
||||
- else
|
||||
- if @sent_notification && @sent_notification.unsubscribable?
|
||||
= link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
|
||||
- if @sent_notification_url
|
||||
= link_to "unsubscribe", @sent_notification_url
|
||||
from this thread or
|
||||
adjust your notification settings.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -112,14 +112,14 @@
|
|||
%span.label.label-primary
|
||||
= tag
|
||||
|
||||
- if builds.size > 1
|
||||
- if @build.pipeline.stages.many?
|
||||
.dropdown.build-dropdown
|
||||
.title Stage
|
||||
%button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
|
||||
%span.stage-selection More
|
||||
= icon('caret-down')
|
||||
%ul.dropdown-menu
|
||||
- builds.map(&:stage).uniq.each do |stage|
|
||||
- @build.pipeline.stages.each do |stage|
|
||||
%li
|
||||
%a.stage-item= stage
|
||||
|
||||
|
|
|
@ -37,6 +37,6 @@
|
|||
%li.dropdown-header Previous Artifacts
|
||||
- artifacts.each do |job|
|
||||
%li
|
||||
= link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do
|
||||
= link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download '#{job.name}'
|
||||
|
|
57
app/views/projects/cycle_analytics/show.html.haml
Normal file
57
app/views/projects/cycle_analytics/show.html.haml
Normal file
|
@ -0,0 +1,57 @@
|
|||
- @no_container = true
|
||||
- page_title "Cycle Analytics"
|
||||
= render "projects/pipelines/head"
|
||||
|
||||
#cycle-analytics{"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()")
|
||||
= custom_icon('icon_cycle_analytics_splash')
|
||||
.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-xs-3.column{"v-for" => "item in summary"}
|
||||
%h3.header {{item.value}}
|
||||
%p.text {{item.title}}
|
||||
|
||||
.col-xs-3.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 stats"}
|
||||
.container-fluid
|
||||
.row
|
||||
.col-xs-10.title-col
|
||||
%p.title
|
||||
{{item.title}}
|
||||
%p.text
|
||||
{{item.description}}
|
||||
.col-xs-2.value-col
|
||||
%span
|
||||
{{item.value}}
|
|
@ -5,7 +5,7 @@
|
|||
- unless diff_file.submodule?
|
||||
.file-actions.hidden-xs
|
||||
- if blob_text_viewable?(blob)
|
||||
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do
|
||||
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this files", disabled: @diff_notes_disabled do
|
||||
= icon('comment')
|
||||
\
|
||||
|
||||
|
|
|
@ -1,42 +1,23 @@
|
|||
- if @merge_request_diffs.size > 1
|
||||
.mr-version-controls
|
||||
Changes between
|
||||
%span.dropdown.inline.mr-version-dropdown
|
||||
%a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
|
||||
%strong
|
||||
- if @merge_request_diff.latest?
|
||||
latest version
|
||||
- else
|
||||
version #{version_index(@merge_request_diff)}
|
||||
%span.caret
|
||||
%ul.dropdown-menu.dropdown-menu-selectable
|
||||
- @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
|
||||
%span.dropdown.inline.mr-version-compare-dropdown
|
||||
%a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
|
||||
%strong
|
||||
- if @start_sha
|
||||
version #{version_index(@start_version)}
|
||||
%div.mr-version-menus-container.content-block
|
||||
Changes between
|
||||
%span.dropdown.inline.mr-version-dropdown
|
||||
%a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} }
|
||||
%span
|
||||
- if @merge_request_diff.latest?
|
||||
latest version
|
||||
- else
|
||||
#{@merge_request.target_branch}
|
||||
version #{version_index(@merge_request_diff)}
|
||||
%span.caret
|
||||
%ul.dropdown-menu.dropdown-menu-selectable
|
||||
- @comparable_diffs.each do |merge_request_diff|
|
||||
.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, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
|
||||
= 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
|
||||
|
@ -44,17 +25,46 @@
|
|||
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)
|
||||
%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)}
|
||||
|
||||
- if @merge_request_diff.base_commit_sha
|
||||
and
|
||||
%span.dropdown.inline.mr-version-compare-dropdown
|
||||
%a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
|
||||
%span
|
||||
- if @start_sha
|
||||
version #{version_index(@start_version)}
|
||||
- else
|
||||
#{@merge_request.target_branch}
|
||||
%span.caret
|
||||
%ul.dropdown-menu.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)}
|
||||
|
||||
- unless @merge_request_diff.latest? && !@start_sha
|
||||
.prepend-top-10
|
||||
.comments-disabled-notif.content-block
|
||||
= icon('info-circle')
|
||||
- if @start_sha
|
||||
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'
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
- supports_slash_commands = note_supports_slash_commands?(@note)
|
||||
|
||||
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
|
||||
= hidden_field_tag :view, diff_view
|
||||
= hidden_field_tag :line_type
|
||||
|
@ -14,8 +16,8 @@
|
|||
attr: :note,
|
||||
classes: 'note-textarea js-note-text',
|
||||
placeholder: "Write a comment or drag your files here...",
|
||||
supports_slash_commands: true
|
||||
= render 'projects/notes/hints', supports_slash_commands: true
|
||||
supports_slash_commands: supports_slash_commands
|
||||
= render 'projects/notes/hints', supports_slash_commands: supports_slash_commands
|
||||
.error-alert
|
||||
|
||||
.note-form-actions.clearfix
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%article.file-holder.readme-holder
|
||||
.file-title
|
||||
= blob_icon readme.mode, readme.name
|
||||
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, @path, readme.name)) do
|
||||
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, @path, readme.name)) do
|
||||
%strong
|
||||
= readme.name
|
||||
.file-content.wiki
|
||||
|
|
|
@ -10,12 +10,16 @@
|
|||
in group #{link_to @group.name, @group}
|
||||
|
||||
.results.prepend-top-10
|
||||
.search-results
|
||||
- if @scope == 'projects'
|
||||
.term
|
||||
= render 'shared/projects/list', projects: @search_objects
|
||||
- else
|
||||
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects
|
||||
- if @scope == 'commits'
|
||||
%ul.list-unstyled
|
||||
= render partial: "search/results/commit", collection: @search_objects
|
||||
- else
|
||||
.search-results
|
||||
- if @scope == 'projects'
|
||||
.term
|
||||
= render 'shared/projects/list', projects: @search_objects
|
||||
- else
|
||||
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects
|
||||
|
||||
- if @scope != 'projects'
|
||||
= paginate(@search_objects, theme: 'gitlab')
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
.search-result-row
|
||||
= render 'projects/commits/commit', project: @project, commit: commit
|
||||
= render 'projects/commits/commit', project: @project, commit: commit
|
||||
|
|
19
app/views/sent_notifications/unsubscribe.html.haml
Normal file
19
app/views/sent_notifications/unsubscribe.html.haml
Normal file
|
@ -0,0 +1,19 @@
|
|||
- noteable = @sent_notification.noteable
|
||||
- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false)
|
||||
- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
|
||||
|
||||
- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace
|
||||
|
||||
|
||||
%h3.page-title
|
||||
Unsubscribe from #{noteable_type} #{noteable_text}
|
||||
|
||||
%p
|
||||
= succeed '?' do
|
||||
Are you sure you want to unsubscribe from #{noteable_type}
|
||||
= link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
|
||||
|
||||
%p
|
||||
= link_to 'Unsubscribe', unsubscribe_sent_notification_path(@sent_notification, force: true),
|
||||
class: 'btn btn-primary append-right-10'
|
||||
= link_to 'Cancel', new_user_session_path, class: 'btn append-right-10'
|
1
app/views/shared/icons/_icon_cycle_analytics_splash.svg
Normal file
1
app/views/shared/icons/_icon_cycle_analytics_splash.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.3 KiB |
|
@ -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
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
- if params[:label_name].present?
|
||||
- if params[:label_name].respond_to?('any?')
|
||||
- params[:label_name].each do |label|
|
||||
= hidden_field_tag "label_name[]", u(label), id: nil
|
||||
= hidden_field_tag "label_name[]", label, id: nil
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
|
||||
%span.dropdown-toggle-text
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
%ul.nav-links.issues-state-filters
|
||||
- if defined?(type) && type == :merge_requests
|
||||
- page_context_word = 'merge requests'
|
||||
- records = @all_merge_requests
|
||||
- else
|
||||
- page_context_word = 'issues'
|
||||
- records = @all_issues
|
||||
%li{class: ("active" if params[:state] == 'opened')}
|
||||
= link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
|
||||
#{state_filters_text_for(:opened, @project)}
|
||||
#{state_filters_text_for(:opened, records)}
|
||||
|
||||
- if defined?(type) && type == :merge_requests
|
||||
%li{class: ("active" if params[:state] == 'merged')}
|
||||
= link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
|
||||
#{state_filters_text_for(:merged, @project)}
|
||||
#{state_filters_text_for(:merged, records)}
|
||||
|
||||
%li{class: ("active" if params[:state] == 'closed')}
|
||||
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
|
||||
#{state_filters_text_for(:closed, @project)}
|
||||
#{state_filters_text_for(:closed, records)}
|
||||
- else
|
||||
%li{class: ("active" if params[:state] == 'closed')}
|
||||
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
|
||||
#{state_filters_text_for(:closed, @project)}
|
||||
#{state_filters_text_for(:closed, records)}
|
||||
|
||||
%li{class: ("active" if params[:state] == 'all')}
|
||||
= link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
|
||||
#{state_filters_text_for(:all, @project)}
|
||||
#{state_filters_text_for(:all, records)}
|
||||
|
|
|
@ -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 }
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue