Commit c2a7e7b8 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 87991030 6df3dd9d
...@@ -3,6 +3,8 @@ group: git ...@@ -3,6 +3,8 @@ group: git
services: services:
- postgres - postgres
before_precompile: ./bin/pkgr_before_precompile.sh before_precompile: ./bin/pkgr_before_precompile.sh
env:
- SKIP_STORAGE_VALIDATION=true
targets: targets:
debian-7: &wheezy debian-7: &wheezy
build_dependencies: build_dependencies:
...@@ -25,6 +27,16 @@ targets: ...@@ -25,6 +27,16 @@ targets:
- libicu52 - libicu52
- libpcre3 - libpcre3
- git - git
ubuntu-16.04:
build_dependencies:
- libkrb5-dev
- libicu-dev
- cmake
- pkg-config
dependencies:
- libicu55
- libpcre3
- git
centos-6: centos-6:
build_dependencies: build_dependencies:
- krb5-devel - krb5-devel
......
...@@ -3,32 +3,46 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -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) v 8.12.0 (unreleased)
- Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251 - Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
- Only check :can_resolve permission if the note is resolvable - Only check :can_resolve permission if the note is resolvable
- Bump fog-aws to v0.11.0 to support ap-south-1 region
- Add ability to fork to a specific namespace using API. (ritave) - 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 - 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) - Prune events older than 12 months. (ritave)
- Prepend blank line to `Closes` message on merge request linked to issue (lukehowell) - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
- Fix issues/merge-request templates dropdown for forked projects - Fix issues/merge-request templates dropdown for forked projects
- Filter tags by name !6121 - Filter tags by name !6121
- Update gitlab shell secret file also when it is empty. !3774 (glensc) - Update gitlab shell secret file also when it is empty. !3774 (glensc)
- Give project selection dropdowns responsive width, make non-wrapping. - 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. - 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 - Add two-factor recovery endpoint to internal API !5510
- Pass the "Remember me" value to the U2F authentication form - 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) - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
- Move pushes_since_gc from the database to Redis - Move pushes_since_gc from the database to Redis
- Add font color contrast to external label in admin area (ClemMakesApps) - Add font color contrast to external label in admin area (ClemMakesApps)
- Change logo animation to CSS (ClemMakesApps) - Change logo animation to CSS (ClemMakesApps)
- Instructions for enabling Git packfile bitmaps !6104 - Instructions for enabling Git packfile bitmaps !6104
- Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint - 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 - Fix pagination on user snippets page
- Run CI builds with the permissions of users !5735
- Fix sorting of issues in API - Fix sorting of issues in API
- Fix download artifacts button links !6407
- Sort project variables by key. !6275 (Diego Souza) - Sort project variables by key. !6275 (Diego Souza)
- Ensure specs on sorting of issues in API are deterministic on MySQL - 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) - Escape search term before passing it to Regexp.new !6241 (winniehell)
- Fix pinned sidebar behavior in smaller viewports !6169 - Fix pinned sidebar behavior in smaller viewports !6169
- Fix file permissions change when updating a file on the Gitlab UI !5979 - Fix file permissions change when updating a file on the Gitlab UI !5979
- Change merge_error column from string to text type - Change merge_error column from string to text type
- Reduce contributions calendar data payload (ClemMakesApps) - 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) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278 - Enable pipeline events by default !6278
- Move parsing of sidekiq ps into helper !6245 (pascalbetz) - Move parsing of sidekiq ps into helper !6245 (pascalbetz)
...@@ -36,6 +50,7 @@ v 8.12.0 (unreleased) ...@@ -36,6 +50,7 @@ v 8.12.0 (unreleased)
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel) - 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) - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
- Fix blame table layout width - 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) - Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
- Request only the LDAP attributes we need !6187 - Request only the LDAP attributes we need !6187
- Center build stage columns in pipeline overview (ClemMakesApps) - Center build stage columns in pipeline overview (ClemMakesApps)
...@@ -43,11 +58,13 @@ v 8.12.0 (unreleased) ...@@ -43,11 +58,13 @@ v 8.12.0 (unreleased)
- Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps) - Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
- Fix bug stopping issue description being scrollable after selecting issue template - Fix bug stopping issue description being scrollable after selecting issue template
- Remove suggested colors hover underline (ClemMakesApps) - Remove suggested colors hover underline (ClemMakesApps)
- Fix jump to discussion button being displayed on commit notes
- Shorten task status phrase (ClemMakesApps) - Shorten task status phrase (ClemMakesApps)
- Fix project visibility level fields on settings - Fix project visibility level fields on settings
- Add hover color to emoji icon (ClemMakesApps) - Add hover color to emoji icon (ClemMakesApps)
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
- Add textarea autoresize after comment (ClemMakesApps) - 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 - Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps) - Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions - Hides merge request button on branches page is user doesn't have permissions
...@@ -57,8 +74,11 @@ v 8.12.0 (unreleased) ...@@ -57,8 +74,11 @@ v 8.12.0 (unreleased)
- Test migration paths from 8.5 until current release !4874 - Test migration paths from 8.5 until current release !4874
- Replace animateEmoji timeout with eventListener (ClemMakesApps) - Replace animateEmoji timeout with eventListener (ClemMakesApps)
- Optimistic locking for Issues and Merge Requests (title and description overriding prevention) - Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
- Require confirmation when not logged in for unsubscribe links !6223 (Maximiliano Perez Coto)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel) - Add `wiki_page_events` to project hook APIs (Ben Boeckel)
- Remove Gitorious import - 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) - Fix inconsistent background color for filter input field (ClemMakesApps)
- Remove prefixes from transition CSS property (ClemMakesApps) - Remove prefixes from transition CSS property (ClemMakesApps)
- Add Sentry logging to API calls - Add Sentry logging to API calls
...@@ -71,18 +91,21 @@ v 8.12.0 (unreleased) ...@@ -71,18 +91,21 @@ v 8.12.0 (unreleased)
- Show queued time when showing a pipeline !6084 - Show queued time when showing a pipeline !6084
- Remove unused mixins (ClemMakesApps) - Remove unused mixins (ClemMakesApps)
- Add search to all issue board lists - Add search to all issue board lists
- Scroll active tab into view on mobile
- Fix groups sort dropdown alignment (ClemMakesApps) - Fix groups sort dropdown alignment (ClemMakesApps)
- Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps) - Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
- Use JavaScript tooltips for mentions !5301 (winniehell) - Use JavaScript tooltips for mentions !5301 (winniehell)
- Add hover state to todos !5361 (winniehell) - Add hover state to todos !5361 (winniehell)
- Fix icon alignment of star and fork buttons !5451 (winniehell) - Fix icon alignment of star and fork buttons !5451 (winniehell)
- Fix alignment of icon buttons !5887 (winniehell) - Fix alignment of icon buttons !5887 (winniehell)
- Added Ubuntu 16.04 support for packager.io (JonTheNiceGuy)
- Fix markdown help references (ClemMakesApps) - Fix markdown help references (ClemMakesApps)
- Add last commit time to repo view (ClemMakesApps) - Add last commit time to repo view (ClemMakesApps)
- Fix accessibility and visibility of project list dropdown button !6140 - Fix accessibility and visibility of project list dropdown button !6140
- Fix missing flash messages on service edit page (airatshigapov) - Fix missing flash messages on service edit page (airatshigapov)
- Added project-specific enable/disable setting for LFS !5997 - Added project-specific enable/disable setting for LFS !5997
- Added group-specific enable/disable setting for LFS !6164 - 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) - Don't expose a user's token in the `/api/v3/user` API (!6047)
- Remove redundant js-timeago-pending from user activity log (ClemMakesApps) - Remove redundant js-timeago-pending from user activity log (ClemMakesApps)
- Ability to manage project issues, snippets, wiki, merge requests and builds access level - Ability to manage project issues, snippets, wiki, merge requests and builds access level
...@@ -100,6 +123,7 @@ v 8.12.0 (unreleased) ...@@ -100,6 +123,7 @@ v 8.12.0 (unreleased)
- Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell) - Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
- Fix repo title alignment (ClemMakesApps) - Fix repo title alignment (ClemMakesApps)
- Change update interval of contacted_at - Change update interval of contacted_at
- Add LFS support to SSH !6043
- Fix branch title trailing space on hover (ClemMakesApps) - 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) - 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) - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
...@@ -134,16 +158,23 @@ v 8.12.0 (unreleased) ...@@ -134,16 +158,23 @@ v 8.12.0 (unreleased)
- Refactor the triggers page and documentation !6217 - Refactor the triggers page and documentation !6217
- Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska) - 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 - 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) - API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska)
- Allow bulk update merge requests from merge requests index page - 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) - Add notification_settings API calls !5632 (mahcsig)
- Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska) - Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska)
- Fix URLs with anchors in wiki !6300 (houqp) - 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) - 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 - 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 - Fix Gitlab::Popen.popen thread-safety issue
- Add specs to removing project (Katarzyna Kobierska Ula Budziszewska) - Add specs to removing project (Katarzyna Kobierska Ula Budziszewska)
- Clean environment variables when running git hooks - 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 v 8.11.6
- Fix unnecessary horizontal scroll area in pipeline visualizations. !6005 - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005
...@@ -166,6 +197,7 @@ v 8.11.5 ...@@ -166,6 +197,7 @@ v 8.11.5
- Scope webhooks/services that will run for confidential issues - Scope webhooks/services that will run for confidential issues
- Remove gitorious from import_sources - Remove gitorious from import_sources
- Fix confidential issues being exposed as public using gitlab.com export - Fix confidential issues being exposed as public using gitlab.com export
- Use oj gem for faster JSON processing
v 8.11.4 v 8.11.4
- Fix resolving conflicts on forks. !6082 - Fix resolving conflicts on forks. !6082
...@@ -589,6 +621,7 @@ v 8.10.0 ...@@ -589,6 +621,7 @@ v 8.10.0
- Export and import avatar as part of project import/export - Export and import avatar as part of project import/export
- Fix migration corrupting import data for old version upgrades - Fix migration corrupting import data for old version upgrades
- Show tooltip on GitLab export link in new project page - 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 v 8.9.9
- Exclude some pending or inactivated rows in Member scopes - Exclude some pending or inactivated rows in Member scopes
...@@ -609,12 +642,6 @@ v 8.9.6 ...@@ -609,12 +642,6 @@ v 8.9.6
- Keeps issue number when importing from Gitlab.com - Keeps issue number when importing from Gitlab.com
- Add Pending tab for Builds (Katarzyna Kobierska, Urszula Budziszewska) - 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 v 8.9.5
- Add more debug info to import/export and memory killer. !5108 - Add more debug info to import/export and memory killer. !5108
- Fixed avatar alignment in new MR view. !5095 - Fixed avatar alignment in new MR view. !5095
...@@ -1880,7 +1907,7 @@ v 8.1.3 ...@@ -1880,7 +1907,7 @@ v 8.1.3
- Use issue editor as cross reference comment author when issue is edited with a new mention - Use issue editor as cross reference comment author when issue is edited with a new mention
- Add Facebook authentication - Add Facebook authentication
v 8.1.1 v 8.1.2
- Fix cloning Wiki repositories via HTTP (Stan Hu) - Fix cloning Wiki repositories via HTTP (Stan Hu)
- Add migration to remove satellites directory - Add migration to remove satellites directory
- Fix specific runners visibility - Fix specific runners visibility
......
...@@ -135,8 +135,7 @@ gem 'after_commit_queue', '~> 1.3.0' ...@@ -135,8 +135,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 3.4' gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs # Background jobs
gem 'sinatra', '~> 1.4.4', require: false gem 'sidekiq', '~> 4.2'
gem 'sidekiq', '~> 4.0'
gem 'sidekiq-cron', '~> 0.4.0' gem 'sidekiq-cron', '~> 0.4.0'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.5.2'
...@@ -206,6 +205,9 @@ gem 'mousetrap-rails', '~> 1.4.6' ...@@ -206,6 +205,9 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding # Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3' gem 'charlock_holmes', '~> 0.7.3'
# Faster JSON
gem 'oj', '~> 2.17.4'
# Parse time & duration # Parse time & duration
gem 'chronic', '~> 0.10.2' gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6' gem 'chronic_duration', '~> 0.10.6'
...@@ -317,6 +319,7 @@ group :test do ...@@ -317,6 +319,7 @@ group :test do
gem 'webmock', '~> 1.21.0' gem 'webmock', '~> 1.21.0'
gem 'test_after_commit', '~> 0.4.2' gem 'test_after_commit', '~> 0.4.2'
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
end end
group :production do group :production do
......
...@@ -189,7 +189,7 @@ GEM ...@@ -189,7 +189,7 @@ GEM
erubis (2.7.0) erubis (2.7.0)
escape_utils (1.1.1) escape_utils (1.1.1)
eventmachine (1.0.8) eventmachine (1.0.8)
excon (0.49.0) excon (0.52.0)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
factory_girl (4.5.0) factory_girl (4.5.0)
...@@ -215,8 +215,8 @@ GEM ...@@ -215,8 +215,8 @@ GEM
flowdock (0.7.1) flowdock (0.7.1)
httparty (~> 0.7) httparty (~> 0.7)
multi_json multi_json
fog-aws (0.9.2) fog-aws (0.11.0)
fog-core (~> 1.27) fog-core (~> 1.38)
fog-json (~> 1.0) fog-json (~> 1.0)
fog-xml (~> 0.1) fog-xml (~> 0.1)
ipaddress (~> 0.8) ipaddress (~> 0.8)
...@@ -225,7 +225,7 @@ GEM ...@@ -225,7 +225,7 @@ GEM
fog-core (~> 1.27) fog-core (~> 1.27)
fog-json (~> 1.0) fog-json (~> 1.0)
fog-xml (~> 0.1) fog-xml (~> 0.1)
fog-core (1.40.0) fog-core (1.42.0)
builder builder
excon (~> 0.49) excon (~> 0.49)
formatador (~> 0.2) formatador (~> 0.2)
...@@ -427,6 +427,7 @@ GEM ...@@ -427,6 +427,7 @@ GEM
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
octokit (4.3.0) octokit (4.3.0)
sawyer (~> 0.7.0, >= 0.5.3) sawyer (~> 0.7.0, >= 0.5.3)
oj (2.17.4)
omniauth (1.3.1) omniauth (1.3.1)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
...@@ -672,11 +673,11 @@ GEM ...@@ -672,11 +673,11 @@ GEM
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
sidekiq (4.1.4) sidekiq (4.2.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (~> 1.5)
redis (~> 3.2, >= 3.2.1) redis (~> 3.2, >= 3.2.1)
sinatra (>= 1.4.7)
sidekiq-cron (0.4.0) sidekiq-cron (0.4.0)
redis-namespace (>= 1.5.2) redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24) rufus-scheduler (>= 2.0.24)
...@@ -686,10 +687,6 @@ GEM ...@@ -686,10 +687,6 @@ GEM
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
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) slack-notifier (1.2.1)
slop (3.6.0) slop (3.6.0)
spinach (0.8.10) spinach (0.8.10)
...@@ -904,6 +901,7 @@ DEPENDENCIES ...@@ -904,6 +901,7 @@ DEPENDENCIES
nokogiri (~> 1.6.7, >= 1.6.7.2) nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.2.0) oauth2 (~> 1.2.0)
octokit (~> 4.3.0) octokit (~> 4.3.0)
oj (~> 2.17.4)
omniauth (~> 1.3.1) omniauth (~> 1.3.1)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-azure-oauth2 (~> 0.0.6) omniauth-azure-oauth2 (~> 0.0.6)
...@@ -958,10 +956,9 @@ DEPENDENCIES ...@@ -958,10 +956,9 @@ DEPENDENCIES
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.0) sidekiq (~> 4.2)
sidekiq-cron (~> 0.4.0) sidekiq-cron (~> 0.4.0)
simplecov (= 0.12.0) simplecov (= 0.12.0)
sinatra (~> 1.4.4)
slack-notifier (~> 1.2.0) slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1) spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2) spinach-rerun-reporter (~> 0.0.2)
...@@ -978,6 +975,7 @@ DEPENDENCIES ...@@ -978,6 +975,7 @@ DEPENDENCIES
teaspoon-jasmine (~> 2.2.0) teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2) test_after_commit (~> 0.4.2)
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.8.0)
turbolinks (~> 2.5.0) turbolinks (~> 2.5.0)
u2f (~> 0.2.1) u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
...@@ -993,4 +991,4 @@ DEPENDENCIES ...@@ -993,4 +991,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.12.5 1.13.0
...@@ -3,10 +3,11 @@ ...@@ -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) [![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) [![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) [![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 ## 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 ## Open source software to collaborate on code
......
...@@ -27,10 +27,11 @@ ...@@ -27,10 +27,11 @@
$(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); $(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
$(window).off('resize.build').on('resize.build', this.hideSidebar); $(window).off('resize.build').on('resize.build', this.hideSidebar);
$(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); $(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
$('#js-build-scroll > a').off('click').on('click', this.stepTrace);
this.updateArtifactRemoveDate(); this.updateArtifactRemoveDate();
if ($('#build-trace').length) { if ($('#build-trace').length) {
this.getInitialBuildTrace(); this.getInitialBuildTrace();
this.initScrollButtonAffix(); this.initScrollButtons();
} }
if (this.build_status === "running" || this.build_status === "pending") { if (this.build_status === "running" || this.build_status === "pending") {
$('#autoscroll-button').on('click', function() { $('#autoscroll-button').on('click', function() {
...@@ -106,7 +107,7 @@ ...@@ -106,7 +107,7 @@
} }
}; };
Build.prototype.initScrollButtonAffix = function() { Build.prototype.initScrollButtons = function() {
var $body, $buildScroll, $buildTrace; var $body, $buildScroll, $buildTrace;
$buildScroll = $('#js-build-scroll'); $buildScroll = $('#js-build-scroll');
$body = $('body'); $body = $('body');
...@@ -165,6 +166,14 @@ ...@@ -165,6 +166,14 @@
this.populateJobs(stage); 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; return Build;
})(); })();
......
((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 @@ ...@@ -94,6 +94,11 @@
break; break;
case "projects:merge_requests:conflicts": case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver() window.mcui = new MergeConflictResolver()
break;
case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation();
Issuable.init();
break;
case 'dashboard:activity': case 'dashboard:activity':
new Activities(); new Activities();
break; break;
...@@ -185,6 +190,9 @@ ...@@ -185,6 +190,9 @@
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
break; break;
case 'projects:cycle_analytics:show':
new gl.CycleAnalytics();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'admin': case 'admin':
......
...@@ -607,13 +607,15 @@ ...@@ -607,13 +607,15 @@
selectedObject = this.renderedData[selectedIndex]; 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; value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) { if (isInput) {
field = $(this.el); field = $(this.el);
} else { } else if(value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + escape(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); el.removeClass(ACTIVE_CLASS);
if (isInput) { if (isInput) {
field.val(''); field.val('');
...@@ -623,7 +625,7 @@ ...@@ -623,7 +625,7 @@
} else if (el.hasClass(INDETERMINATE_CLASS)) { } else if (el.hasClass(INDETERMINATE_CLASS)) {
el.addClass(ACTIVE_CLASS); el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS); el.removeClass(INDETERMINATE_CLASS);
if (value == null) { if (field.length && value == null) {
field.remove(); field.remove();
} }
if (!field.length && fieldName) { if (!field.length && fieldName) {
...@@ -636,7 +638,7 @@ ...@@ -636,7 +638,7 @@
this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
} }
} }
if (value == null) { if (field.length && value == null) {
field.remove(); field.remove();
} }
// Toggle active class for the tick mark // Toggle active class for the tick mark
...@@ -644,7 +646,7 @@ ...@@ -644,7 +646,7 @@
if (value != null) { if (value != null) {
if (!field.length && fieldName) { if (!field.length && fieldName) {
this.addInput(fieldName, value, selectedObject); this.addInput(fieldName, value, selectedObject);
} else { } else if (field.length) {
field.val(value).trigger('change'); field.val(value).trigger('change');
} }
} }
...@@ -794,4 +796,4 @@ ...@@ -794,4 +796,4 @@
}); });
}; };
}).call(this); }).call(this);
\ No newline at end of file
...@@ -10,24 +10,24 @@ ...@@ -10,24 +10,24 @@
ImporterStatus.prototype.initStatusPage = function() { ImporterStatus.prototype.initStatusPage = function() {
$('.js-add-to-import').off('click').on('click', (function(_this) { $('.js-add-to-import').off('click').on('click', (function(_this) {
return function(e) { 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); $btn = $(e.currentTarget);
$tr = $btn.closest('tr'); $tr = $btn.closest('tr');
$target_field = $tr.find('.import-target'); $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_', ''); id = $tr.attr('id').replace('repo_', '');
target_namespace = null; target_namespace = null;
newName = null;
if ($namespace_input.length > 0) { if ($namespace_input.length > 0) {
target_namespace = $namespace_input.prop('value'); target_namespace = $namespace_input[0].innerHTML;
$target_field.empty().append(target_namespace + "/" + ($target_field.data('project_name'))); newName = $target_field.find('#path').prop('value');
$target_field.empty().append(target_namespace + "/" + newName);
} }
$btn.disable().addClass('is-loading'); $btn.disable().addClass('is-loading');
return $.post(_this.import_url, { return $.post(_this.import_url, {
repo_id: id, repo_id: id,
target_namespace: target_namespace target_namespace: target_namespace,
new_name: newName
}, { }, {
dataType: 'script' dataType: 'script'
}); });
......
...@@ -16,11 +16,11 @@ ...@@ -16,11 +16,11 @@
}, },
initSearch: function() { initSearch: function() {
this.timer = null; this.timer = null;
return $('#issue_search').off('keyup').on('keyup', function() { return $('#issuable_search').off('keyup').on('keyup', function() {
clearTimeout(this.timer); clearTimeout(this.timer);
return this.timer = setTimeout(function() { return this.timer = setTimeout(function() {
var $form, $input, $search; var $form, $input, $search;
$search = $('#issue_search'); $search = $('#issuable_search');
$form = $('.js-filter-form'); $form = $('.js-filter-form');
$input = $("input[name='" + ($search.attr('name')) + "']", $form); $input = $("input[name='" + ($search.attr('name')) + "']", $form);
if ($input.length === 0) { if ($input.length === 0) {
......
...@@ -166,7 +166,7 @@ ...@@ -166,7 +166,7 @@
instance.addInput(this.fieldName, label.id); 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'); selectedClass.push('is-active');
} }
if ($dropdown.hasClass('js-multiselect') && removesAll) { if ($dropdown.hasClass('js-multiselect') && removesAll) {
......
...@@ -10,11 +10,13 @@ ...@@ -10,11 +10,13 @@
}; };
$(function() { $(function() {
hideEndFade($('.scrolling-tabs')); var $scrollingTabs = $('.scrolling-tabs');
hideEndFade($scrollingTabs);
$(window).off('resize.nav').on('resize.nav', function() { $(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; var $this, currentPosition, maxPosition;
$this = $(this); $this = $(this);
currentPosition = $this.scrollLeft(); currentPosition = $this.scrollLeft();
...@@ -22,6 +24,23 @@ ...@@ -22,6 +24,23 @@
$this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); 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); }).call(this);
...@@ -232,10 +232,10 @@ ...@@ -232,10 +232,10 @@
$('.hll').removeClass('hll'); $('.hll').removeClass('hll');
locationHash = window.location.hash; locationHash = window.location.hash;
if (locationHash !== '') { if (locationHash !== '') {
hashClassString = "." + (locationHash.replace('#', '')); dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]';
$diffLine = $(locationHash + ":not(.match)", $('#diffs')); $diffLine = $(locationHash + ":not(.match)", $('#diffs'));
if (!$diffLine.is('tr')) { if (!$diffLine.is('tr')) {
$diffLine = $('#diffs').find("td" + locationHash + ", td" + hashClassString); $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString);
} else { } else {
$diffLine = $diffLine.find('td'); $diffLine = $diffLine.find('td');
} }
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
date.setDate(date.getDate() + i); date.setDate(date.getDate() + i);
var day = date.getDay(); 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 // Create a new group array if this is the first day of the week
// or if is first object // or if is first object
......
...@@ -164,7 +164,7 @@ ...@@ -164,7 +164,7 @@
text-decoration: none; text-decoration: none;
&:after { &:after {
content: url('icon_anchor.svg'); content: image-url('icon_anchor.svg');
visibility: hidden; visibility: hidden;
} }
} }
......
lex
[v-cloak] { [v-cloak] {
display: none; display: none;
} }
...@@ -18,6 +19,10 @@ ...@@ -18,6 +19,10 @@
} }
} }
.is-ghost {
opacity: 0.3;
}
.dropdown-menu-issues-board-new { .dropdown-menu-issues-board-new {
width: 320px; width: 320px;
...@@ -34,47 +39,13 @@ ...@@ -34,47 +39,13 @@
> p { > p {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
color: #9c9c9c;
} }
} }
.issue-boards-page { .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 { .page-with-sidebar {
display: -webkit-flex;
display: flex;
min-height: 100vh;
max-height: 100vh;
padding-bottom: 0; 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 { .boards-app-loading {
...@@ -83,46 +54,38 @@ ...@@ -83,46 +54,38 @@
} }
.boards-list { .boards-list {
display: -webkit-flex; height: calc(100vh - 152px);
display: flex; width: 100%;
-webkit-flex: 1;
flex: 1;
-webkit-flex-basis: 0;
flex-basis: 0;
min-height: calc(100vh - 152px);
max-height: calc(100vh - 152px);
padding-top: 25px; padding-top: 25px;
padding-bottom: 25px;
padding-right: ($gl-padding / 2); padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2); padding-left: ($gl-padding / 2);
overflow-x: scroll; overflow-x: scroll;
white-space: nowrap;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px);
min-height: 475px; min-height: 475px;
max-height: none;
} }
} }
.board { .board {
display: -webkit-flex; display: inline-block;
display: flex; width: calc(85vw - 15px);
min-width: calc(85vw - 15px); height: 100%;
max-width: calc(85vw - 15px);
margin-bottom: 25px;
padding-right: ($gl-padding / 2); padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2); padding-left: ($gl-padding / 2);
white-space: normal;
vertical-align: top;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
min-width: 400px; width: 400px;
max-width: 400px;
} }
} }
.board-inner { .board-inner {
display: -webkit-flex; height: 100%;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
width: 100%;
font-size: $issue-boards-font-size; font-size: $issue-boards-font-size;
background: $background-color; background: $background-color;
border: 1px solid $border-color; border: 1px solid $border-color;
...@@ -193,45 +156,31 @@ ...@@ -193,45 +156,31 @@
} }
.board-list { .board-list {
-webkit-flex: 1; height: calc(100% - 49px);
flex: 1;
height: 400px;
margin-bottom: 0; margin-bottom: 0;
padding: 5px; padding: 5px;
list-style: none;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
} }
.board-list-loading { .board-list-loading {
margin-top: 10px; margin-top: 10px;
font-size: 26px; font-size: (26px / $issue-boards-font-size) * 1em;
}
.is-ghost {
opacity: 0.3;
} }
.card { .card {
position: relative; position: relative;
width: 100%;
padding: 10px $gl-padding; padding: 10px $gl-padding;
background: #fff; background: #fff;
border-radius: $border-radius-default; border-radius: $border-radius-default;
box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5); box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
list-style: none; list-style: none;
&.user-can-drag {
padding-left: $gl-padding;
}
&:not(:last-child) { &:not(:last-child) {
margin-bottom: 5px; margin-bottom: 5px;
} }
a {
cursor: pointer;
}
.label { .label {
border: 0; border: 0;
outline: 0; outline: 0;
...@@ -256,14 +205,13 @@ ...@@ -256,14 +205,13 @@
line-height: 25px; line-height: 25px;
.label { .label {
margin-right: 4px; margin-right: 5px;
font-size: (14px / $issue-boards-font-size) * 1em; font-size: (14px / $issue-boards-font-size) * 1em;
} }
} }
.card-number { .card-number {
margin-right: 8px; margin-right: 5px;
font-weight: 500;
} }
.issue-boards-search { .issue-boards-search {
......
#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 @@ ...@@ -68,6 +68,11 @@
border-collapse: separate; border-collapse: separate;
margin: 0; margin: 0;
padding: 0; padding: 0;
table-layout: fixed;
.diff-line-num {
width: 50px;
}
.line_holder td { .line_holder td {
line-height: $code_line_height; line-height: $code_line_height;
...@@ -98,10 +103,6 @@ ...@@ -98,10 +103,6 @@
} }
tr.line_holder.parallel { tr.line_holder.parallel {
.old_line, .new_line {
min-width: 50px;
}
td.line_content.parallel { td.line_content.parallel {
width: 46%; width: 46%;
} }
......
...@@ -373,11 +373,40 @@ ...@@ -373,11 +373,40 @@
.mr-version-controls { .mr-version-controls {
background: $background-color; background: $background-color;
padding: $gl-btn-padding; border-bottom: 1px solid $border-color;
color: $gl-placeholder-color; color: $gl-text-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;
}
a.btn-link { .fa-info-circle {
color: $gl-dark-link-color; color: $orange-normal;
padding-right: 5px;
} }
} }
......
...@@ -770,3 +770,13 @@ pre.light-well { ...@@ -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;
}
}
\ No newline at end of file
...@@ -13,10 +13,18 @@ module IssuableCollections ...@@ -13,10 +13,18 @@ module IssuableCollections
issues_finder.execute issues_finder.execute
end end
def all_issues_collection
IssuesFinder.new(current_user, filter_params_all).execute
end
def merge_requests_collection def merge_requests_collection
merge_requests_finder.execute merge_requests_finder.execute
end end
def all_merge_requests_collection
MergeRequestsFinder.new(current_user, filter_params_all).execute
end
def issues_finder def issues_finder
@issues_finder ||= issuable_finder_for(IssuesFinder) @issues_finder ||= issuable_finder_for(IssuesFinder)
end end
...@@ -54,6 +62,10 @@ module IssuableCollections ...@@ -54,6 +62,10 @@ module IssuableCollections
@filter_params @filter_params
end end
def filter_params_all
@filter_params_all ||= filter_params.merge(state: 'all', sort: nil)
end
def set_default_scope def set_default_scope
params[:scope] = 'all' if params[:scope].blank? params[:scope] = 'all' if params[:scope].blank?
end end
......
...@@ -10,6 +10,8 @@ module IssuesAction ...@@ -10,6 +10,8 @@ module IssuesAction
.preload(:author, :project) .preload(:author, :project)
.page(params[:page]) .page(params[:page])
@all_issues = all_issues_collection.non_archived
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: false }
......
...@@ -9,5 +9,7 @@ module MergeRequestsAction ...@@ -9,5 +9,7 @@ module MergeRequestsAction
.non_archived .non_archived
.preload(:author, :target_project) .preload(:author, :target_project)
.page(params[:page]) .page(params[:page])
@all_merge_requests = all_merge_requests_collection.non_archived
end end
end end
...@@ -40,11 +40,12 @@ class Import::GithubController < Import::BaseController ...@@ -40,11 +40,12 @@ class Import::GithubController < Import::BaseController
def create def create
@repo_id = params[:repo_id].to_i @repo_id = params[:repo_id].to_i
repo = client.repo(@repo_id) repo = client.repo(@repo_id)
@project_name = repo.name @project_name = params[:new_name].presence || repo.name
@target_namespace = find_or_create_namespace(repo.owner.login, client.user.login) 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) 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 else
render 'unauthorized' render 'unauthorized'
end end
......
...@@ -11,7 +11,8 @@ class JwtController < ApplicationController ...@@ -11,7 +11,8 @@ class JwtController < ApplicationController
service = SERVICES[params[:service]] service = SERVICES[params[:service]]
return head :not_found unless 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] render json: result, status: result[:http_status]
end end
...@@ -19,31 +20,26 @@ class JwtController < ApplicationController ...@@ -19,31 +20,26 @@ class JwtController < ApplicationController
private private
def authenticate_project_or_user def authenticate_project_or_user
authenticate_with_http_basic do |login, password| @authentication_result = Gitlab::Auth::Result.new
# if it's possible we first try to authenticate project with login and password
@project = authenticate_project(login, password)
return if @project
@user = authenticate_user(login, password) authenticate_with_http_basic do |login, password|
return if @user @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
render_403 render_403 unless @authentication_result.success? &&
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end end
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
end end
def auth_params def render_missing_personal_token
params.permit(:service, :scope, :account, :client_id) 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 end
def authenticate_project(login, password) def auth_params
if login == 'gitlab-ci-token' params.permit(:service, :scope, :account, :client_id)
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
end end
...@@ -35,7 +35,11 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -35,7 +35,11 @@ class Projects::BuildsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do 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 end
end end
......
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 ...@@ -4,7 +4,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController
include ActionController::HttpAuthentication::Basic include ActionController::HttpAuthentication::Basic
include KerberosSpnegoHelper 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 # Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
...@@ -15,32 +19,25 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -15,32 +19,25 @@ class Projects::GitHttpClientController < Projects::ApplicationController
private private
def authenticate_user def authenticate_user
@authentication_result = Gitlab::Auth::Result.new
if project && project.public? && download_request? if project && project.public? && download_request?
return # Allow access return # Allow access
end end
if allow_basic_auth? && basic_auth_provided? if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request) 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 return # Allow access
end end
elsif allow_kerberos_spnego_auth? && spnego_provided? 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 send_final_spnego_response
return # Allow access return # Allow access
end end
...@@ -48,6 +45,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -48,6 +45,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401 render plain: "HTTP Basic: Access denied\n", status: 401
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
end end
def basic_auth_provided? def basic_auth_provided?
...@@ -114,8 +113,41 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -114,8 +113,41 @@ class Projects::GitHttpClientController < Projects::ApplicationController
render plain: 'Not Found', status: :not_found render plain: 'Not Found', status: :not_found
end 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? 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 end
def verify_workhorse_api! def verify_workhorse_api!
......
...@@ -86,7 +86,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -86,7 +86,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end end
def access def access
@access ||= Gitlab::GitAccess.new(user, project, 'http') @access ||= Gitlab::GitAccess.new(user, project, 'http', authentication_abilities: authentication_abilities)
end end
def access_check def access_check
......
...@@ -23,20 +23,13 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -23,20 +23,13 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html respond_to :html
def index def index
terms = params['issue_search']
@issues = issues_collection @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]) @issues = @issues.page(params[:page])
@labels = @project.labels.where(title: params[:label_name]) @labels = @project.labels.where(title: params[:label_name])
@all_issues = all_issues_collection
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: false }
......
...@@ -31,22 +31,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -31,22 +31,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts] before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
def index def index
terms = params['issue_search']
@merge_requests = merge_requests_collection @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.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project) @merge_requests = @merge_requests.preload(:target_project)
@labels = @project.labels.where(title: params[:label_name]) @labels = @project.labels.where(title: params[:label_name])
@all_merge_requests = all_merge_requests_collection
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
...@@ -428,6 +420,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -428,6 +420,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def validates_merge_request 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 # Show git not found page
# if there is no saved commits between source & target branch # if there is no saved commits between source & target branch
if @merge_request.commits.blank? if @merge_request.commits.blank?
......
...@@ -3,12 +3,19 @@ class SentNotificationsController < ApplicationController ...@@ -3,12 +3,19 @@ class SentNotificationsController < ApplicationController
def unsubscribe def unsubscribe
@sent_notification = SentNotification.for(params[:id]) @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 = @sent_notification.noteable
noteable.unsubscribe(@sent_notification.recipient) noteable.unsubscribe(@sent_notification.recipient)
flash[:notice] = "You have been unsubscribed from this thread." flash[:notice] = "You have been unsubscribed from this thread."
if current_user if current_user
case noteable case noteable
when Issue when Issue
......
...@@ -73,7 +73,7 @@ class UsersController < ApplicationController ...@@ -73,7 +73,7 @@ class UsersController < ApplicationController
def calendar def calendar
calendar = contributions_calendar calendar = contributions_calendar
@timestamps = calendar.timestamps @activity_dates = calendar.activity_dates
render 'calendar', layout: false render 'calendar', layout: false
end end
......
...@@ -216,7 +216,14 @@ class IssuableFinder ...@@ -216,7 +216,14 @@ class IssuableFinder
end end
def by_search(items) 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 items
end end
......
...@@ -25,4 +25,8 @@ class IssuesFinder < IssuableFinder ...@@ -25,4 +25,8 @@ class IssuesFinder < IssuableFinder
def init_collection def init_collection
Issue.visible_to_user(current_user) Issue.visible_to_user(current_user)
end end
def iid_pattern
@iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
end
end end
...@@ -19,4 +19,14 @@ class MergeRequestsFinder < IssuableFinder ...@@ -19,4 +19,14 @@ class MergeRequestsFinder < IssuableFinder
def klass def klass
MergeRequest MergeRequest
end 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 end
...@@ -249,7 +249,7 @@ module ApplicationHelper ...@@ -249,7 +249,7 @@ module ApplicationHelper
milestone_title: params[:milestone_title], milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id], assignee_id: params[:assignee_id],
author_id: params[:author_id], author_id: params[:author_id],
issue_search: params[:issue_search], search: params[:search],
label_name: params[:label_name] label_name: params[:label_name]
} }
...@@ -280,23 +280,14 @@ module ApplicationHelper ...@@ -280,23 +280,14 @@ module ApplicationHelper
end end
end end
def state_filters_text_for(entity, project) def state_filters_text_for(state, records)
titles = { titles = {
opened: "Open" opened: "Open"
} }
entity_title = titles[entity] || entity.to_s.humanize state_title = titles[state] || state.to_s.humanize
count = records.public_send(state).size
count = html = content_tag :span, state_title
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
if count.present? if count.present?
html += " " html += " "
......
...@@ -46,6 +46,10 @@ module GitlabRoutingHelper ...@@ -46,6 +46,10 @@ module GitlabRoutingHelper
namespace_project_environments_path(project.namespace, project, *args) namespace_project_environments_path(project.namespace, project, *args)
end end
def project_cycle_analytics_path(project, *args)
namespace_project_cycle_analytics_path(project.namespace, project, *args)
end
def project_builds_path(project, *args) def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args) namespace_project_builds_path(project.namespace, project, *args)
end end
......
...@@ -25,13 +25,21 @@ module LfsHelper ...@@ -25,13 +25,21 @@ module LfsHelper
def lfs_download_access? def lfs_download_access?
return false unless project.lfs_enabled? 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 end
def lfs_upload_access? def lfs_upload_access?
return false unless project.lfs_enabled? return false unless project.lfs_enabled?
user && user.can?(:push_code, project) has_authentication_ability?(:push_code) && can?(user, :push_code, project)
end end
def render_lfs_forbidden def render_lfs_forbidden
......
module NamespacesHelper 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 = current_user.owned_groups + current_user.masters_groups
groups << extra_group if extra_group && !Group.exists?(name: extra_group.name)
users = [current_user.namespace] users = [current_user.namespace]
data_attr_group = { 'data-options-parent' => 'groups' } data_attr_group = { 'data-options-parent' => 'groups' }
......
...@@ -10,6 +10,10 @@ module NotesHelper ...@@ -10,6 +10,10 @@ module NotesHelper
Ability.can_edit_note?(current_user, note) Ability.can_edit_note?(current_user, note)
end end
def note_supports_slash_commands?(note)
Notes::SlashCommandsService.supported?(note, current_user)
end
def noteable_json(noteable) def noteable_json(noteable)
{ {
id: noteable.id, id: noteable.id,
......
...@@ -108,6 +108,12 @@ class Notify < BaseMailer ...@@ -108,6 +108,12 @@ class Notify < BaseMailer
headers["X-GitLab-#{model.class.name}-ID"] = model.id headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key 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? if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace address.display_name = @project.name_with_namespace
......
module Ci module Ci
class Build < CommitStatus class Build < CommitStatus
include TokenAuthenticatable
belongs_to :runner, class_name: 'Ci::Runner' belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
belongs_to :erased_by, class_name: 'User' belongs_to :erased_by, class_name: 'User'
...@@ -23,7 +25,10 @@ module Ci ...@@ -23,7 +25,10 @@ module Ci
acts_as_taggable acts_as_taggable
add_authentication_token_field :token
before_save :update_artifacts_size, if: :artifacts_file_changed? before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
before_destroy { project } before_destroy { project }
after_create :execute_hooks after_create :execute_hooks
...@@ -38,6 +43,7 @@ module Ci ...@@ -38,6 +43,7 @@ module Ci
new_build.status = 'pending' new_build.status = 'pending'
new_build.runner_id = nil new_build.runner_id = nil
new_build.trigger_request_id = nil new_build.trigger_request_id = nil
new_build.token = nil
new_build.save new_build.save
end end
...@@ -79,11 +85,14 @@ module Ci ...@@ -79,11 +85,14 @@ module Ci
after_transition any => [:success] do |build| after_transition any => [:success] do |build|
if build.environment.present? if build.environment.present?
service = CreateDeploymentService.new(build.project, build.user, service = CreateDeploymentService.new(
environment: build.environment, build.project, build.user,
sha: build.sha, environment: build.environment,
ref: build.ref, sha: build.sha,
tag: build.tag) ref: build.ref,
tag: build.tag,
options: build.options[:environment],
variables: build.variables)
service.execute(build) service.execute(build)
end end
end end
...@@ -173,7 +182,7 @@ module Ci ...@@ -173,7 +182,7 @@ module Ci
end end
def repo_url def repo_url
auth = "gitlab-ci-token:#{token}@" auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix| project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
prefix + auth prefix + auth
end end
...@@ -235,12 +244,7 @@ module Ci ...@@ -235,12 +244,7 @@ module Ci
end end
def trace def trace
trace = raw_trace hide_secrets(raw_trace)
if project && trace.present? && project.runners_token.present?
trace.gsub(project.runners_token, 'xxxxxx')
else
trace
end
end end
def trace_length def trace_length
...@@ -253,6 +257,7 @@ module Ci ...@@ -253,6 +257,7 @@ module Ci
def trace=(trace) def trace=(trace)
recreate_trace_dir recreate_trace_dir
trace = hide_secrets(trace)
File.write(path_to_trace, trace) File.write(path_to_trace, trace)
end end
...@@ -266,6 +271,8 @@ module Ci ...@@ -266,6 +271,8 @@ module Ci
def append_trace(trace_part, offset) def append_trace(trace_part, offset)
recreate_trace_dir recreate_trace_dir
trace_part = hide_secrets(trace_part)
File.truncate(path_to_trace, offset) if File.exist?(path_to_trace) File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
File.open(path_to_trace, 'ab') do |f| File.open(path_to_trace, 'ab') do |f|
f.write(trace_part) f.write(trace_part)
...@@ -341,12 +348,8 @@ module Ci ...@@ -341,12 +348,8 @@ module Ci
) )
end end
def token
project.runners_token
end
def valid_token?(token) def valid_token?(token)
project.valid_runners_token?(token) self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end end
def has_tags? def has_tags?
...@@ -488,5 +491,14 @@ module Ci ...@@ -488,5 +491,14 @@ module Ci
pipeline.config_processor.build_attributes(name) pipeline.config_processor.build_attributes(name)
end 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
end end
...@@ -2,6 +2,7 @@ module Ci ...@@ -2,6 +2,7 @@ module Ci
class Pipeline < ActiveRecord::Base class Pipeline < ActiveRecord::Base
extend Ci::Model extend Ci::Model
include HasStatus include HasStatus
include Importable
self.table_name = 'ci_commits' self.table_name = 'ci_commits'
...@@ -12,12 +13,12 @@ module Ci ...@@ -12,12 +13,12 @@ module Ci
has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id 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 has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
validates_presence_of :sha validates_presence_of :sha, unless: :importing?
validates_presence_of :ref validates_presence_of :ref, unless: :importing?
validates_presence_of :status validates_presence_of :status, unless: :importing?
validate :valid_commit_sha validate :valid_commit_sha, unless: :importing?
after_save :keep_around_commits after_save :keep_around_commits, unless: :importing?
delegate :stages, to: :statuses delegate :stages, to: :statuses
...@@ -55,6 +56,16 @@ module Ci ...@@ -55,6 +56,16 @@ module Ci
pipeline.finished_at = Time.now pipeline.finished_at = Time.now
end 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| before_transition do |pipeline|
pipeline.update_duration pipeline.update_duration
end end
...@@ -241,13 +252,16 @@ module Ci ...@@ -241,13 +252,16 @@ module Ci
end end
def build_updated def build_updated
case latest_builds_status with_lock do
when 'pending' then enqueue reload
when 'running' then run case latest_builds_status
when 'success' then succeed when 'pending' then enqueue
when 'failed' then drop when 'running' then run
when 'canceled' then cancel when 'success' then succeed
when 'skipped' then skip when 'failed' then drop
when 'canceled' then cancel
when 'skipped' then skip
end
end end
end end
...@@ -276,6 +290,16 @@ module Ci ...@@ -276,6 +290,16 @@ module Ci
project.execute_services(data, :pipeline_hooks) project.execute_services(data, :pipeline_hooks)
end 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 private
def pipeline_data def pipeline_data
......
...@@ -69,15 +69,15 @@ class CommitStatus < ActiveRecord::Base ...@@ -69,15 +69,15 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now commit_status.update_attributes finished_at: Time.now
end 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| after_transition any => [:success, :failed, :canceled] do |commit_status|
commit_status.pipeline.try(:process!) commit_status.pipeline.try(:process!)
true true
end 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| after_transition [:created, :pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
end end
......
...@@ -20,7 +20,7 @@ module HasStatus ...@@ -20,7 +20,7 @@ module HasStatus
skipped = scope.skipped.select('count(*)').to_sql skipped = scope.skipped.select('count(*)').to_sql
deduce_status = "(CASE deduce_status = "(CASE
WHEN (#{builds})=(#{created}) THEN NULL WHEN (#{builds})=(#{created}) THEN 'created'
WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending' WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
......
...@@ -28,10 +28,13 @@ module Issuable ...@@ -28,10 +28,13 @@ module Issuable
loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? } loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? }
end end
end end
has_many :label_links, as: :target, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy has_many :todos, as: :target, dependent: :destroy
has_one :metrics
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 } validates :title, presence: true, length: { within: 0..255 }
...@@ -81,6 +84,7 @@ module Issuable ...@@ -81,6 +84,7 @@ module Issuable
acts_as_paranoid acts_as_paranoid
after_save :update_assignee_cache_counts, if: :assignee_id_changed? after_save :update_assignee_cache_counts, if: :assignee_id_changed?
after_save :record_metrics
def update_assignee_cache_counts def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignee # make sure we flush the cache for both the old *and* new assignee
...@@ -286,4 +290,9 @@ module Issuable ...@@ -286,4 +290,9 @@ module Issuable
def can_move?(*) def can_move?(*)
false false
end end
def record_metrics
metrics = self.metrics || create_metrics
metrics.record!
end
end end
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
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 ...@@ -42,4 +42,38 @@ class Deployment < ActiveRecord::Base
project.repository.is_ancestor?(commit.id, sha) project.repository.is_ancestor?(commit.id, sha)
end 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 end
...@@ -4,6 +4,7 @@ class Environment < ActiveRecord::Base ...@@ -4,6 +4,7 @@ class Environment < ActiveRecord::Base
has_many :deployments has_many :deployments
before_validation :nullify_external_url before_validation :nullify_external_url
before_save :set_environment_type
validates :name, validates :name,
presence: true, presence: true,
...@@ -26,9 +27,24 @@ class Environment < ActiveRecord::Base ...@@ -26,9 +27,24 @@ class Environment < ActiveRecord::Base
self.external_url = nil if self.external_url.blank? self.external_url = nil if self.external_url.blank?
end end
def set_environment_type
names = name.split('/')
self.environment_type =
if names.many?
names.first
else
nil
end
end
def includes_commit?(commit) def includes_commit?(commit)
return false unless last_deployment return false unless last_deployment
last_deployment.includes_commit?(commit) last_deployment.includes_commit?(commit)
end end
def update_merge_request_metrics?
self.name == "production"
end
end end
...@@ -13,6 +13,8 @@ class Event < ActiveRecord::Base ...@@ -13,6 +13,8 @@ class Event < ActiveRecord::Base
LEFT = 9 # User left project LEFT = 9 # User left project
DESTROYED = 10 DESTROYED = 10
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true
...@@ -324,8 +326,27 @@ class Event < ActiveRecord::Base ...@@ -324,8 +326,27 @@ class Event < ActiveRecord::Base
end end
def reset_project_activity def reset_project_activity
if project && Gitlab::ExclusiveLease.new("project:update_last_activity_at:#{project.id}", timeout: 60).try_obtain return unless project
project.update_column(:last_activity_at, self.created_at)
end # 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
end end
...@@ -23,6 +23,8 @@ class Issue < ActiveRecord::Base ...@@ -23,6 +23,8 @@ class Issue < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy has_many :events, as: :target, dependent: :destroy
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
validates :project, presence: true validates :project, presence: true
scope :cared, ->(user) { where(assignee_id: user) } scope :cared, ->(user) { where(assignee_id: user) }
...@@ -36,6 +38,8 @@ class Issue < ActiveRecord::Base ...@@ -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_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 :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 :title, spam_title: true
attr_spammable :description, spam_description: true attr_spammable :description, spam_description: true
......
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 ...@@ -16,6 +16,8 @@ class MergeRequest < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy has_many :events, as: :target, dependent: :destroy
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
serialize :merge_params, Hash serialize :merge_params, Hash
after_create :ensure_merge_request_diff, unless: :importing? after_create :ensure_merge_request_diff, unless: :importing?
...@@ -501,6 +503,19 @@ class MergeRequest < ActiveRecord::Base ...@@ -501,6 +503,19 @@ class MergeRequest < ActiveRecord::Base
target_project target_project
end 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) def closes_issue?(issue)
closes_issues.include?(issue) closes_issues.include?(issue)
end end
...@@ -508,7 +523,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -508,7 +523,8 @@ class MergeRequest < ActiveRecord::Base
# Return the set of issues that will be closed if this merge request is accepted. # Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author) def closes_issues(current_user = self.author)
if target_branch == project.default_branch 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). Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(messages.join("\n")) closed_by_message(messages.join("\n"))
...@@ -652,7 +668,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -652,7 +668,7 @@ class MergeRequest < ActiveRecord::Base
end end
def environments def environments
return unless diff_head_commit return [] unless diff_head_commit
target_project.environments.select do |environment| target_project.environments.select do |environment|
environment.includes_commit?(diff_head_commit) environment.includes_commit?(diff_head_commit)
......
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
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 ...@@ -1137,12 +1137,6 @@ class Project < ActiveRecord::Base
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end 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? def build_coverage_enabled?
build_coverage_regex.present? build_coverage_regex.present?
end end
......
...@@ -756,62 +756,59 @@ class Repository ...@@ -756,62 +756,59 @@ class Repository
@root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref } @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
end 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| update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user) options = {
options = {} commit: {
options[:committer] = committer branch: ref,
options[:author] = committer message: message,
update_ref: false
options[:commit] = { }
message: message,
branch: ref,
update_ref: false,
} }
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
raw_repository.mkdir(path, options) raw_repository.mkdir(path, options)
end end
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| update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user) options = {
options = {} commit: {
options[:committer] = committer branch: ref,
options[:author] = committer message: message,
options[:commit] = { update_ref: false
message: message, },
branch: ref, file: {
update_ref: false, content: content,
path: path,
update: update
}
} }
options[:file] = { options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
content: content,
path: path,
update: update
}
Gitlab::Git::Blob.commit(raw_repository, options) Gitlab::Git::Blob.commit(raw_repository, options)
end end
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| update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user) options = {
options = {} commit: {
options[:committer] = committer branch: ref,
options[:author] = committer message: message,
options[:commit] = { update_ref: false
message: message, },
branch: ref, file: {
update_ref: false content: content,
path: path,
update: true
}
} }
options[:file] = { options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
content: content,
path: path,
update: true
}
if previous_path && previous_path != path if previous_path && previous_path != path
options[:file][:previous_path] = previous_path options[:file][:previous_path] = previous_path
...@@ -822,34 +819,39 @@ class Repository ...@@ -822,34 +819,39 @@ class Repository
end end
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| update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user) options = {
options = {} commit: {
options[:committer] = committer branch: ref,
options[:author] = committer message: message,
options[:commit] = { update_ref: false
message: message, },
branch: ref, file: {
update_ref: false, path: path
}
} }
options[:file] = { options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
path: path
}
Gitlab::Git::Blob.remove(raw_repository, options) Gitlab::Git::Blob.remove(raw_repository, options)
end end
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, author: author,
name: user.name, committer: committer
time: Time.now
} }
end 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) def can_be_merged?(source_sha, target_branch)
our_commit = rugged.branches[target_branch].target our_commit = rugged.branches[target_branch].target
their_commit = rugged.lookup(source_sha) their_commit = rugged.lookup(source_sha)
......
...@@ -46,6 +46,7 @@ class ProjectPolicy < BasePolicy ...@@ -46,6 +46,7 @@ class ProjectPolicy < BasePolicy
can! :create_issue can! :create_issue
can! :create_note can! :create_note
can! :upload_file can! :upload_file
can! :read_cycle_analytics
end end
def reporter_access! def reporter_access!
...@@ -64,6 +65,12 @@ class ProjectPolicy < BasePolicy ...@@ -64,6 +65,12 @@ class ProjectPolicy < BasePolicy
can! :read_deployment can! :read_deployment
end 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! def developer_access!
can! :admin_merge_request can! :admin_merge_request
can! :update_merge_request can! :update_merge_request
...@@ -109,6 +116,8 @@ class ProjectPolicy < BasePolicy ...@@ -109,6 +116,8 @@ class ProjectPolicy < BasePolicy
can! :read_commit_status can! :read_commit_status
can! :read_pipeline can! :read_pipeline
can! :read_container_image can! :read_container_image
can! :build_download_code
can! :build_read_container_image
end end
def owner_access! def owner_access!
...@@ -130,10 +139,11 @@ class ProjectPolicy < BasePolicy ...@@ -130,10 +139,11 @@ class ProjectPolicy < BasePolicy
def team_access!(user) def team_access!(user)
access = project.team.max_member_access(user.id) access = project.team.max_member_access(user.id)
guest_access! if access >= Gitlab::Access::GUEST guest_access! if access >= Gitlab::Access::GUEST
reporter_access! if access >= Gitlab::Access::REPORTER reporter_access! if access >= Gitlab::Access::REPORTER
developer_access! if access >= Gitlab::Access::DEVELOPER team_member_reporter_access! if access >= Gitlab::Access::REPORTER
master_access! if access >= Gitlab::Access::MASTER developer_access! if access >= Gitlab::Access::DEVELOPER
master_access! if access >= Gitlab::Access::MASTER
end end
def archived_access! def archived_access!
...@@ -195,6 +205,7 @@ class ProjectPolicy < BasePolicy ...@@ -195,6 +205,7 @@ class ProjectPolicy < BasePolicy
can! :read_commit_status can! :read_commit_status
can! :read_container_image can! :read_container_image
can! :download_code can! :download_code
can! :read_cycle_analytics
# NOTE: may be overridden by IssuePolicy # NOTE: may be overridden by IssuePolicy
can! :read_issue can! :read_issue
......
...@@ -29,25 +29,25 @@ class AkismetService ...@@ -29,25 +29,25 @@ class AkismetService
end end
def submit_ham def submit_ham
return false unless akismet_enabled? submit(:ham)
end
params = { def submit_spam
type: 'comment', submit(:spam)
text: text, end
author: owner.name,
author_email: owner.email
}
begin private
akismet_client.submit_ham(options[:ip_address], options[:user_agent], params)
true def akismet_client
rescue => e @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") Gitlab.config.gitlab.url)
false
end
end end
def submit_spam def akismet_enabled?
current_application_settings.akismet_enabled
end
def submit(type)
return false unless akismet_enabled? return false unless akismet_enabled?
params = { params = {
...@@ -58,22 +58,11 @@ class AkismetService ...@@ -58,22 +58,11 @@ class AkismetService
} }
begin begin
akismet_client.submit_spam(options[:ip_address], options[:user_agent], params) akismet_client.public_send(type, options[:ip_address], options[:user_agent], params)
true true
rescue => e rescue => e
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
false false
end end
end end
private
def akismet_client
@akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
Gitlab.config.gitlab.url)
end
def akismet_enabled?
current_application_settings.akismet_enabled
end
end end
...@@ -4,7 +4,9 @@ module Auth ...@@ -4,7 +4,9 @@ module Auth
AUDIENCE = 'container_registry' AUDIENCE = 'container_registry'
def execute def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities
return error('not found', 404) unless registry.enabled return error('not found', 404) unless registry.enabled
unless current_user || project unless current_user || project
...@@ -74,9 +76,9 @@ module Auth ...@@ -74,9 +76,9 @@ module Auth
case requested_action case requested_action
when 'pull' 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' when 'push'
requested_project == project || can?(current_user, :create_container_image, requested_project) build_can_push?(requested_project) || user_can_push?(requested_project)
else else
false false
end end
...@@ -85,5 +87,29 @@ module Auth ...@@ -85,5 +87,29 @@ module Auth
def registry def registry
Gitlab.config.registry Gitlab.config.registry
end 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
end end
...@@ -2,11 +2,9 @@ require_relative 'base_service' ...@@ -2,11 +2,9 @@ require_relative 'base_service'
class CreateDeploymentService < BaseService class CreateDeploymentService < BaseService
def execute(deployable = nil) def execute(deployable = nil)
environment = project.environments.find_or_create_by( environment = find_or_create_environment
name: params[:environment]
)
project.deployments.create( deployment = project.deployments.create(
environment: environment, environment: environment,
ref: params[:ref], ref: params[:ref],
tag: params[:tag], tag: params[:tag],
...@@ -14,5 +12,43 @@ class CreateDeploymentService < BaseService ...@@ -14,5 +12,43 @@ class CreateDeploymentService < BaseService
user: current_user, user: current_user,
deployable: deployable 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
end end
...@@ -16,6 +16,8 @@ module Files ...@@ -16,6 +16,8 @@ module Files
params[:file_content] params[:file_content]
end end
@last_commit_sha = params[:last_commit_sha] @last_commit_sha = params[:last_commit_sha]
@author_email = params[:author_email]
@author_name = params[:author_name]
# Validate parameters # Validate parameters
validate validate
......
...@@ -3,7 +3,7 @@ require_relative "base_service" ...@@ -3,7 +3,7 @@ require_relative "base_service"
module Files module Files
class CreateDirService < Files::BaseService class CreateDirService < Files::BaseService
def commit 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 end
def validate def validate
......
...@@ -3,7 +3,7 @@ require_relative "base_service" ...@@ -3,7 +3,7 @@ require_relative "base_service"
module Files module Files
class CreateService < Files::BaseService class CreateService < Files::BaseService
def commit 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 end
def validate def validate
......
...@@ -3,7 +3,7 @@ require_relative "base_service" ...@@ -3,7 +3,7 @@ require_relative "base_service"
module Files module Files
class DeleteService < Files::BaseService class DeleteService < Files::BaseService
def commit 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 end
end end
...@@ -8,7 +8,9 @@ module Files ...@@ -8,7 +8,9 @@ module Files
repository.update_file(current_user, @file_path, @file_content, repository.update_file(current_user, @file_path, @file_content,
branch: @target_branch, branch: @target_branch,
previous_path: @previous_path, previous_path: @previous_path,
message: @commit_message) message: @commit_message,
author_email: @author_email,
author_name: @author_name)
end end
private private
......
...@@ -87,7 +87,7 @@ class GitPushService < BaseService ...@@ -87,7 +87,7 @@ class GitPushService < BaseService
project.change_head(branch_name) project.change_head(branch_name)
# Set protection on the default branch if configured # 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 = { params = {
name: @project.default_branch, name: @project.default_branch,
...@@ -134,6 +134,7 @@ class GitPushService < BaseService ...@@ -134,6 +134,7 @@ class GitPushService < BaseService
end end
commit.create_cross_references!(authors[commit], closed_issues) commit.create_cross_references!(authors[commit], closed_issues)
update_issue_metrics(commit, authors)
end end
end end
...@@ -186,4 +187,11 @@ class GitPushService < BaseService ...@@ -186,4 +187,11 @@ class GitPushService < BaseService
def branch_name def branch_name
@branch_name ||= Gitlab::Git.ref_name(params[:ref]) @branch_name ||= Gitlab::Git.ref_name(params[:ref])
end 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 end
...@@ -157,6 +157,10 @@ class IssuableBaseService < BaseService ...@@ -157,6 +157,10 @@ class IssuableBaseService < BaseService
# To be overridden by subclasses # To be overridden by subclasses
end end
def after_update(issuable)
# To be overridden by subclasses
end
def update_issuable(issuable, attributes) def update_issuable(issuable, attributes)
issuable.with_transaction_returning_status do issuable.with_transaction_returning_status do
issuable.update(attributes.merge(updated_by: current_user)) issuable.update(attributes.merge(updated_by: current_user))
...@@ -182,6 +186,7 @@ class IssuableBaseService < BaseService ...@@ -182,6 +186,7 @@ class IssuableBaseService < BaseService
end end
handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users) handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
after_update(issuable)
issuable.create_new_cross_references!(current_user) issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update') execute_hooks(issuable, 'update')
end end
......
...@@ -20,6 +20,7 @@ module MergeRequests ...@@ -20,6 +20,7 @@ module MergeRequests
event_service.open_mr(issuable, current_user) event_service.open_mr(issuable, current_user)
notification_service.new_merge_request(issuable, current_user) notification_service.new_merge_request(issuable, current_user)
todo_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 end
end end
...@@ -13,6 +13,7 @@ module MergeRequests ...@@ -13,6 +13,7 @@ module MergeRequests
reload_merge_requests reload_merge_requests
reset_merge_when_build_succeeds reset_merge_when_build_succeeds
mark_pending_todos_done mark_pending_todos_done
cache_merge_requests_closing_issues
# Leave a system note if a branch was deleted/added # Leave a system note if a branch was deleted/added
if branch_added? || branch_removed? if branch_added? || branch_removed?
...@@ -141,6 +142,14 @@ module MergeRequests ...@@ -141,6 +142,14 @@ module MergeRequests
end end
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) def filter_merge_requests(merge_requests)
merge_requests.uniq.select(&:source_project) merge_requests.uniq.select(&:source_project)
end end
......
...@@ -77,5 +77,9 @@ module MergeRequests ...@@ -77,5 +77,9 @@ module MergeRequests
def close_service def close_service
MergeRequests::CloseService MergeRequests::CloseService
end end
def after_update(issuable)
issuable.cache_merge_request_closes_issues!(current_user)
end
end end
end end
...@@ -3,7 +3,7 @@ module Milestones ...@@ -3,7 +3,7 @@ module Milestones
def execute def execute
milestone = project.milestones.new(params) milestone = project.milestones.new(params)
if milestone.save! if milestone.save
event_service.open_milestone(milestone, current_user) event_service.open_milestone(milestone, current_user)
end end
......
...@@ -5,9 +5,18 @@ module Notes ...@@ -5,9 +5,18 @@ module Notes
'MergeRequest' => MergeRequests::UpdateService '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) && 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 end
def extract_commands(note) def extract_commands(note)
...@@ -21,13 +30,7 @@ module Notes ...@@ -21,13 +30,7 @@ module Notes
return if command_params.empty? return if command_params.empty?
return unless supported?(note) return unless supported?(note)
noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable) self.class.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]
end end
end end
end end
- diff_notes_disabled = (@merge_request_diff.latest? && !!@start_sha) if @merge_request_diff
- discussion = local_assigns.fetch(:discussion, nil) - discussion = local_assigns.fetch(:discussion, nil)
- if current_user - if current_user
%jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" } %jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
...@@ -5,5 +6,6 @@ ...@@ -5,5 +6,6 @@
%button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion", %button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
title: "Jump to next unresolved discussion", title: "Jump to next unresolved discussion",
"aria-label" => "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") = custom_icon("next_discussion")
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
.btn-group{ role: "group" } .btn-group{ role: "group" }
= link_to_reply_discussion(discussion, line_type) = link_to_reply_discussion(discussion, line_type)
= render "discussions/resolve_all", discussion: discussion = 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 - else
= link_to_reply_discussion(discussion) = link_to_reply_discussion(discussion)
...@@ -45,7 +45,17 @@ ...@@ -45,7 +45,17 @@
%td %td
= github_project_link(repo.full_name) = github_project_link(repo.full_name)
%td.import-target %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 %td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do = button_tag class: "btn btn-import js-add-to-import" do
Import Import
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
Repository Repository
- if project_nav_tab? :pipelines - 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 = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span %span
Pipelines Pipelines
......
...@@ -25,8 +25,8 @@ ...@@ -25,8 +25,8 @@
- if @labels_url - if @labels_url
adjust your #{link_to 'label subscriptions', @labels_url}. adjust your #{link_to 'label subscriptions', @labels_url}.
- else - else
- if @sent_notification && @sent_notification.unsubscribable? - if @sent_notification_url
= link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification) = link_to "unsubscribe", @sent_notification_url
from this thread or from this thread or
adjust your notification settings. adjust your notification settings.
......
...@@ -14,9 +14,6 @@ ...@@ -14,9 +14,6 @@
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.preview_markdown_path = "#{preview_markdown_path}"; 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 - content_for :header_content do
.js-dropdown-menu-projects .js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects .dropdown-menu.dropdown-select.dropdown-menu-projects
......
...@@ -7,3 +7,6 @@ ...@@ -7,3 +7,6 @@
= text_area_tag attr, nil, class: classes, placeholder: placeholder = text_area_tag attr, nil, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress') = icon('compress')
- content_for :scripts_body do
= render "layouts/init_auto_complete" if current_user && (@target_project || @project)
...@@ -112,14 +112,14 @@ ...@@ -112,14 +112,14 @@
%span.label.label-primary %span.label.label-primary
= tag = tag
- if builds.size > 1 - if @build.pipeline.stages.many?
.dropdown.build-dropdown .dropdown.build-dropdown
.title Stage .title Stage
%button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.stage-selection More %span.stage-selection More
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu %ul.dropdown-menu
- builds.map(&:stage).uniq.each do |stage| - @build.pipeline.stages.each do |stage|
%li %li
%a.stage-item= stage %a.stage-item= stage
......
...@@ -37,6 +37,6 @@ ...@@ -37,6 +37,6 @@
%li.dropdown-header Previous Artifacts %li.dropdown-header Previous Artifacts
- artifacts.each do |job| - artifacts.each do |job|
%li %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 %i.fa.fa-download
%span Download '#{job.name}' %span Download '#{job.name}'
- @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 @@ ...@@ -5,7 +5,7 @@
- unless diff_file.submodule? - unless diff_file.submodule?
.file-actions.hidden-xs .file-actions.hidden-xs
- if blob_text_viewable?(blob) - 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') = icon('comment')
\ \
......
- if @merge_request_diffs.size > 1 - if @merge_request_diffs.size > 1
.mr-version-controls .mr-version-controls
Changes between %div.mr-version-menus-container.content-block
%span.dropdown.inline.mr-version-dropdown Changes between
%a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} } %span.dropdown.inline.mr-version-dropdown
%strong %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} }
- if @merge_request_diff.latest? %span
latest version - if @merge_request_diff.latest?
- else latest version
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)}
- else - else
#{@merge_request.target_branch} version #{version_index(@merge_request_diff)}
%span.caret %span.caret
%ul.dropdown-menu.dropdown-menu-selectable %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 %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 %strong
- if merge_request_diff.latest? - if merge_request_diff.latest?
latest version latest version
...@@ -44,17 +25,46 @@ ...@@ -44,17 +25,46 @@
version #{version_index(merge_request_diff)} version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)} .monospace #{short_sha(merge_request_diff.head_commit_sha)}
%small %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) = 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 - if @merge_request_diff.base_commit_sha
%strong and
#{@merge_request.target_branch} (base) %span.dropdown.inline.mr-version-compare-dropdown
.monospace #{short_sha(@merge_request_diff.base_commit_sha)} %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 - unless @merge_request_diff.latest? && !@start_sha
.prepend-top-10 .comments-disabled-notif.content-block
= icon('info-circle') = icon('info-circle')
- if @start_sha - if @start_sha
Comments are disabled because you're comparing two versions of this merge request. Comments are disabled because you're comparing two versions of this merge request.
- else - else
Comments are disabled because you're viewing an old version of this merge request. 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'
- 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| = 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 :view, diff_view
= hidden_field_tag :line_type = hidden_field_tag :line_type
...@@ -14,8 +16,8 @@ ...@@ -14,8 +16,8 @@
attr: :note, attr: :note,
classes: 'note-textarea js-note-text', classes: 'note-textarea js-note-text',
placeholder: "Write a comment or drag your files here...", placeholder: "Write a comment or drag your files here...",
supports_slash_commands: true supports_slash_commands: supports_slash_commands
= render 'projects/notes/hints', supports_slash_commands: true = render 'projects/notes/hints', supports_slash_commands: supports_slash_commands
.error-alert .error-alert
.note-form-actions.clearfix .note-form-actions.clearfix
......
...@@ -19,3 +19,9 @@ ...@@ -19,3 +19,9 @@
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span %span
Environments 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
%article.file-holder.readme-holder %article.file-holder.readme-holder
.file-title .file-title
= blob_icon readme.mode, readme.name = 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 %strong
= readme.name = readme.name
.file-content.wiki .file-content.wiki
......
...@@ -10,12 +10,16 @@ ...@@ -10,12 +10,16 @@
in group #{link_to @group.name, @group} in group #{link_to @group.name, @group}
.results.prepend-top-10 .results.prepend-top-10
.search-results - if @scope == 'commits'
- if @scope == 'projects' %ul.list-unstyled
.term = render partial: "search/results/commit", collection: @search_objects
= render 'shared/projects/list', projects: @search_objects - else
- else .search-results
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects - if @scope == 'projects'
.term
= render 'shared/projects/list', projects: @search_objects
- else
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects
- if @scope != 'projects' - if @scope != 'projects'
= paginate(@search_objects, theme: 'gitlab') = paginate(@search_objects, theme: 'gitlab')
.search-result-row = render 'projects/commits/commit', project: @project, commit: commit
= render 'projects/commits/commit', project: @project, commit: commit
- 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'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99 102" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m35.12 56.988c4.083-4.385 5.968-12.155 5.968-24.04 0-20.2-15.874-32.16-15.874-32.16-1.114-.954-2.929-.979-4.04 0 0 0-15.874 11.957-15.874 32.16 0 11.882 1.884 19.652 5.968 24.04h23.848"/><mask id="1" width="35.783" height="56.924" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(0-4)"><g transform="translate(32.15 3.976)"><g fill="#6b4fbb"><path d="m11.928 56.988l1.325-1.325v3.313c0 .737.59 1.325 1.325 1.325h17.229c.736 0 1.325-.59 1.325-1.325v-3.313l1.325 1.325h-22.53m22.53-1.325v3.313c0 1.464-1.18 2.651-2.651 2.651h-17.229c-1.464 0-2.651-1.178-2.651-2.651v-3.313h22.53m-5.964 7.361h.663c0 3.294-2.67 5.964-5.964 5.964-3.294 0-5.964-2.67-5.964-5.964h.663.663c0 2.562 2.077 4.639 4.639 4.639 2.562 0 4.639-2.077 4.639-4.639h.663"/><path d="m5.816 42.535c-.346-2.839-.515-6.03-.515-9.584 0-20.2 15.874-32.16 15.874-32.16 1.106-.979 2.921-.954 4.04 0 0 0 15.874 11.957 15.874 32.16 0 11.882-1.884 19.652-5.968 24.04h-23.848c-2.861-3.073-4.643-7.807-5.453-14.453-.06-.493-.115-.997-.164-1.511l-4.04 2.884c-.891.637-1.614 2.041-1.614 3.137v14.581c0 1.465.971 1.958 2.165 1.106l8.691-6.208c-.282-.332-.553-.681-.813-1.048l-8.648 6.177c-.147.105-.069.152-.069-.027v-14.581c0-.668.516-1.671 1.059-2.059l3.432-2.451m38.4 20.2c1.193.852 2.165.359 2.165-1.106v-14.581c0-1.096-.723-2.5-1.614-3.137l-4.04-2.884c-.049.514-.104 1.018-.164 1.511l3.432 2.451c.543.388 1.059 1.391 1.059 2.059v14.581c0 .179.078.132-.069.027l-8.648-6.177c-.26.367-.531.716-.813 1.048l8.691 6.208"/></g><use fill="#fff" stroke="#6b4fbb" stroke-width="2.651" mask="url(#1)" xlink:href="#0"/><g fill="#b5a7dd"><path d="m30.482 28.494c0-4.03-3.263-7.289-7.289-7.289-4.03 0-7.289 3.263-7.289 7.289 0 4.03 3.263 7.289 7.289 7.289 4.03 0 7.289-3.263 7.289-7.289m-15.904 0c0-4.758 3.857-8.614 8.614-8.614 4.758 0 8.614 3.857 8.614 8.614 0 4.758-3.857 8.614-8.614 8.614-4.758 0-8.614-3.857-8.614-8.614"/><path d="m27.17 28.494c0-2.196-1.78-3.976-3.976-3.976-2.196 0-3.976 1.78-3.976 3.976 0 2.196 1.78 3.976 3.976 3.976 2.196 0 3.976-1.78 3.976-3.976m-9.277 0c0-2.928 2.373-5.301 5.301-5.301 2.928 0 5.301 2.373 5.301 5.301 0 2.928-2.373 5.301-5.301 5.301-2.928 0-5.301-2.373-5.301-5.301"/></g><path fill="#6b4fbb" d="m34.458 87.47c0 1.098.89 1.988 1.988 1.988 1.098 0 1.988-.89 1.988-1.988 0-.366.297-.663.663-.663.366 0 .663.297.663.663 0 1.83-1.483 3.313-3.313 3.313-1.826 0-3.307-1.478-3.313-3.302 0-.002 0-.003 0-.005v-2.663c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.657m-21.2-6.615c0-.002 0-.003 0-.005v-2.663c0-.358-.297-.657-.663-.657-.369 0-.663.294-.663.657v2.657c0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 1.83 1.483 3.313 3.313 3.313 1.826 0 3.307-1.477 3.313-3.302m5.301 7.285c0-.001 0-.002 0-.003v-16.576c0-.362-.297-.658-.663-.658-.369 0-.663.295-.663.658v16.571c0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.745 2.225 4.97 4.97 4.97 2.742 0 4.966-2.221 4.97-4.963m10.602 8.607v-18.555c0-.365-.297-.661-.663-.661-.369 0-.663.296-.663.661v18.557c0 0 0 0 0 .001.001 2.744 2.226 4.968 4.97 4.968 2.745 0 4.97-2.225 4.97-4.97 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m3.976-25.19c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m5.301 0c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-5.301 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-13.253c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663"/></g><path fill="#e2ddf2" d="m97.75 76.54c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m-60.964-57.651c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645"/><path fill="#b5a7dd" d="m98.41 34.458c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988m-86.14 20.542c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988"/></g></svg>
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
.issues-filters .issues-filters
.issues-details-filters.row-content-block.second-block .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 = 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[:issue_search].present? - if params[:search].present?
= hidden_field_tag :issue_search, params[:issue_search] = hidden_field_tag :search, params[:search]
- if @bulk_edit - if @bulk_edit
.check-all-holder .check-all-holder
= check_box_tag "check_all_issues", nil, false, = check_box_tag "check_all_issues", nil, false,
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
= render "shared/issuable/label_dropdown" = render "shared/issuable/label_dropdown"
.filter-item.inline.reset-filters .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 .pull-right
- if boards_page - if boards_page
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
- if params[:label_name].present? - if params[:label_name].present?
- if params[:label_name].respond_to?('any?') - if params[:label_name].respond_to?('any?')
- params[:label_name].each do |label| - params[:label_name].each do |label|
= hidden_field_tag "label_name[]", u(label), id: nil = hidden_field_tag "label_name[]", label, id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
%span.dropdown-toggle-text %span.dropdown-toggle-text
......
%ul.nav-links.issues-state-filters %ul.nav-links.issues-state-filters
- if defined?(type) && type == :merge_requests - if defined?(type) && type == :merge_requests
- page_context_word = 'merge requests' - page_context_word = 'merge requests'
- records = @all_merge_requests
- else - else
- page_context_word = 'issues' - page_context_word = 'issues'
- records = @all_issues
%li{class: ("active" if params[:state] == 'opened')} %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 = 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 - if defined?(type) && type == :merge_requests
%li{class: ("active" if params[:state] == 'merged')} %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 = 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')} %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 = 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 - else
%li{class: ("active" if params[:state] == 'closed')} %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 = 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')} %li{class: ("active" if params[:state] == 'all')}
= link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do = 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)}
= form_tag(path, method: :get, id: "issue_search_form", class: 'issue-search-form') do = form_tag(path, method: :get, id: "issuable_search_form", class: 'issuable-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 } = 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 }
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment