Commit dc1d269f authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into pipeline-emails

* upstream/master: (237 commits)
  Grapify boards API
  Add test, fix merge error
  Use local assigns to get the dropdown title
  Updated issuable dropdown titles
  Added safety check for formatted values
  Minor style improvement
  Fixed conflict and corrected teaspoon test
  Rename method in test
  Moved ci_status environments logic to new action ci_envrionments_status and set up frontend polling
  Refactor ci_status on MergeRequestController
  Fix indenting error in HAML
  Show what time ago a MR was deployed
  Fixed missing links
  Fixed missing links
  Refactor merge requests revisions
  Add link to update docs for source installations
  Grapify todos API
  Link to review apps example from docs
  fix grafana_configuration.md move link
  Do not run before_script, artifacts, cache in trigger_docs job
  ...
parents b5f9d4c4 ca3bef55
......@@ -210,6 +210,13 @@ rake brakeman: *exec
rake flay: *exec
license_finder: *exec
rake downtime_check: *exec
rake ce_to_ee_merge_check:
<<: *exec
only:
- branches
except:
- tags
allow_failure: yes
rake db:migrate:reset:
stage: test
......@@ -255,6 +262,12 @@ lint-doc:
script:
- scripts/lint-doc.sh
bundler:check:
stage: test
<<: *ruby-static-analysis
script:
- bundle check
bundler:audit:
stage: test
<<: *ruby-static-analysis
......@@ -293,6 +306,17 @@ coverage:
- coverage/index.html
- coverage/assets/
# Trigger docs build
trigger_docs:
stage: post-test
before_script: []
cache: {}
artifacts: {}
script:
- "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master https://gitlab.com/api/v3/projects/38069/trigger/builds"
only:
- master
# Notify slack in the end
notify:slack:
......@@ -325,3 +349,16 @@ pages:
- public
only:
- master
# Insurance in case a gem needed by one of our releases gets yanked from
# rubygems.org in the future.
cache gems:
only:
- tags
variables:
SETUP_DB: "false"
script:
- bundle package --all --all-platforms
artifacts:
paths:
- vendor/cache
Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased)
- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
- Respond with 404 Not Found for non-existent tags (Linus Thiel)
- Truncate long labels with ellipsis in labels page
- Adding members no longer silently fails when there is extra whitespace
- Update runner version only when updating contacted_at
- Add link from system note to compare with previous version
- Improve issue load time performance by avoiding ORDER BY in find_by call
- Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Use gitlab-shell v3.6.6
- Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine)
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- Updating verbiage on git basics to be more intuitive
- Clarify documentation for Runners API (Gennady Trafimenkov)
- Change user & group landing page routing from /u/:username to /:username
- Prevent running GfmAutocomplete setup for each diff note !6569
- Added documentation for .gitattributes files
- AbstractReferenceFilter caches project_refs on RequestStore when active
- Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Fix Error 500 when viewing old merge requests with bad diff data
- Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar)
- Speed-up group milestones show page
- Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
- Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Add tag shortcut from the Commit page. !6543
- Keep refs for each deployment
- Allow browsing branches that end with '.atom'
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps)
- Add more tests for calendar contribution (ClemMakesApps)
- Update Gitlab Shell to fix some problems with moving projects between storages
- Cache rendered markdown in the database, rather than Redis
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Do not alter 'force_remove_source_branch' options on MergeRequest unless specified
- Simplify Mentionable concern instance methods
- API: Ability to retrieve version information (Robert Schilling)
- Fix permission for setting an issue's due date
- API: Multi-file commit !6096 (mahcsig)
- Unicode emoji are now converted to images
- Revert "Label list shows all issues (opened or closed) with that label"
- Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments
- Added copy file path button to merge request diff files
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API
- Add new issue button to each list on Issues Board
- Added soft wrap button to repository file/blob editor
- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
- Show the time ago a merge request was deployed to an environment
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps)
- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- Remove redundant mixins (ClemMakesApps)
- Added 'Download' button to the Snippets page (Justin DiPierro)
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix that manual jobs would no longer block jobs in the next stage. !6604
- Add configurable email subject suffix (Fu Xu)
- Use defined colour for a language when available !6748 (nilsding)
- Added tooltip to fork count on project show page. (Justin DiPierro)
- Use a ConnectionPool for Rails.cache on Sidekiq servers
- Replace `alias_method_chain` with `Module#prepend`
......@@ -57,28 +81,54 @@ v 8.13.0 (unreleased)
- Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts?
- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
- Add disabled delete button to protected branches (ClemMakesApps)
- Add broadcast messages and alerts below sub-nav
- Better empty state for Groups view
- API: New /users/:id/events endpoint
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile
- Ignore deployment for statistics in Cycle Analytics, except in staging and production stages
- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
- Fix deploy status responsiveness error !6633
- Make searching for commits case insensitive
- Fix resolved discussion display in side-by-side diff view !6575
- Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Reorder issue and merge request titles to show IDs first. !6503 (Greg Laubenstein)
- Reduce queries needed to find users using their SSH keys when pushing commits
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- Fix broken repository 500 errors in project list
- Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone)
- Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Retouch environments list and deployments list
- Add multiple command support for all label related slash commands !6780 (barthc)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Allow empty merge requests !6384 (Artem Sidorenko)
- Grouped pipeline dropdown is a scrollable container
v 8.12.5 (unreleased)
- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
- Fixes padding in all clipboard icons that have .btn class
- Fix a typo in doc/api/labels.md
- API: all unknown routing will be handled with 404 Not Found
- Make guests unable to view MRs on private projects
v 8.12.7
- Use gitlab-markup gem instead of github-markup to fix `.rst` file rendering. !6659
v 8.12.6
- Update mailroom to 0.8.1 in Gemfile.lock !6814
v 8.12.5
- Switch from request to env in ::API::Helpers. !6615
- Update the mail_room gem to 0.8.1 to fix a race condition with the mailbox watching thread. !6714
- Improve issue load time performance by avoiding ORDER BY in find_by call. !6724
- Add a new gitlab:users:clear_all_authentication_tokens task. !6745
- Don't send Private-Token (API authentication) headers to Sentry
- Share projects via the API only with groups the authenticated user can access
v 8.12.4
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
......@@ -93,7 +143,7 @@ v 8.12.4
- Fix failed project deletion when feature visibility set to private. !6688
- Prevent claiming associated model IDs via import.
- Set GitLab project exported file permissions to owner only
- Change user & group landing page routing from /u/:username to /:username
- Improve the way merge request versions are compared with each other
v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves
......@@ -119,6 +169,7 @@ v 8.12.1
- Fix issue with search filter labels not displaying
v 8.12.0
- Removes inconsistency regarding tagging immediatelly as merged once you create a new branch. !6408
- Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
- Only check :can_resolve permission if the note is resolvable
- Bump fog-aws to v0.11.0 to support ap-south-1 region
......@@ -307,6 +358,10 @@ v 8.12.0
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
v 8.11.9
- Don't send Private-Token (API authentication) headers to Sentry
- Share projects via the API only with groups the authenticated user can access
v 8.11.8
- Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests
......@@ -532,6 +587,10 @@ v 8.11.0
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
v 8.10.12
- Don't send Private-Token (API authentication) headers to Sentry
- Share projects via the API only with groups the authenticated user can access
v 8.10.11
- Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests
......
......@@ -51,7 +51,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem 'gitlab_git', '~> 10.6.7'
gem 'gitlab_git', '~> 10.6.8'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
......@@ -101,7 +101,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie'
gem 'github-markup', '~> 1.4'
gem 'gitlab-markup', '~> 1.5.0'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~>3.6'
......@@ -225,7 +225,7 @@ gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
gem 'jquery-ui-rails', '~> 5.0.0'
gem 'request_store', '~> 1.3.0'
gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
......@@ -262,6 +262,8 @@ group :development do
# thin instead webrick
gem 'thin', '~> 1.7.0'
gem 'activerecord_sane_schema_dumper', '0.2'
end
group :development, :test do
......@@ -324,7 +326,7 @@ gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.3.0'
gem 'mail_room', '~> 0.8'
gem 'mail_room', '~> 0.8.1'
gem 'email_reply_parser', '~> 0.5.8'
......@@ -341,7 +343,7 @@ gem 'oauth2', '~> 1.2.0'
gem 'paranoia', '~> 2.0'
# Health check
gem 'health_check', '~> 2.1.0'
gem 'health_check', '~> 2.2.0'
# System information
gem 'vmstat', '~> 2.2'
......
......@@ -38,6 +38,8 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 1.5.2, < 3)
railties (>= 4.0, < 5.1)
activerecord_sane_schema_dumper (0.2)
rails (>= 4, < 5)
activesupport (4.2.7.1)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
......@@ -280,7 +282,8 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab_git (10.6.7)
gitlab-markup (1.5.0)
gitlab_git (10.6.8)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -334,7 +337,7 @@ GEM
thor
tilt
hashie (3.4.4)
health_check (2.1.0)
health_check (2.2.1)
rails (>= 4.0)
hipchat (1.5.2)
httparty
......@@ -399,7 +402,7 @@ GEM
systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
mail_room (0.8.0)
mail_room (0.8.1)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
......@@ -805,6 +808,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0)
activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0)
......@@ -861,9 +865,9 @@ DEPENDENCIES
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
github-linguist (~> 4.7.0)
github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_git (~> 10.6.7)
gitlab-markup (~> 1.5.0)
gitlab_git (~> 10.6.8)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
......@@ -872,7 +876,7 @@ DEPENDENCIES
grape-entity (~> 0.4.2)
haml_lint (~> 0.18.2)
hamlit (~> 2.6.1)
health_check (~> 2.1.0)
health_check (~> 2.2.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
......@@ -889,7 +893,7 @@ DEPENDENCIES
license_finder (~> 2.1.0)
licensee (~> 8.0.0)
loofah (~> 2.0.3)
mail_room (~> 0.8)
mail_room (~> 0.8.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
......@@ -934,7 +938,7 @@ DEPENDENCIES
redis (~> 3.2)
redis-namespace (~> 1.5.2)
redis-rails (~> 4.0.0)
request_store (~> 1.3.0)
request_store (~> 1.3)
rerun (~> 0.11.0)
responders (~> 2.0)
rouge (~> 2.0)
......
# GitLab
[![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)
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](http://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
[![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)
......
......@@ -6,11 +6,10 @@
groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true",
labelsPath: "/:namespace_path/:project_path/labels",
licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
licensePath: "/api/:version/templates/licenses/:key",
gitignorePath: "/api/:version/templates/gitignores/:key",
gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
group: function(group_id, callback) {
var url = Api.buildUrl(Api.groupPath)
.replace(':id', group_id);
......
......@@ -28,12 +28,13 @@ $(() => {
state: Store.state,
loading: true,
endpoint: $boardApp.dataset.endpoint,
boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase
},
init: Store.create.bind(Store),
created () {
gl.boardService = new BoardService(this.endpoint);
gl.boardService = new BoardService(this.endpoint, this.boardId);
},
ready () {
Store.disabled = this.disabled;
......
class BoardService {
constructor (root) {
constructor (root, boardId) {
Vue.http.options.root = root;
this.lists = Vue.resource(`${root}/lists{/id}`, {}, {
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
url: `${root}/lists/generate.json`
url: `${root}/${boardId}/lists/generate.json`
}
});
this.issue = Vue.resource(`${root}/issues{/id}`, {});
this.issues = Vue.resource(`${root}/lists{/id}/issues`, {});
this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {});
Vue.http.interceptors.push((request, next) => {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
......
......@@ -21,6 +21,7 @@
shortcut_handler = null;
switch (page) {
case 'projects:boards:show':
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:merge_requests:index':
......@@ -126,6 +127,9 @@
new TreeView();
}
break;
case 'projects:pipelines:show':
new gl.Pipelines();
break;
case 'groups:activity':
new Activities();
break;
......
......@@ -52,37 +52,27 @@
}
}
},
setup: function(input) {
setup: _.debounce(function(input) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
// destroy previous instances
this.destroyAtWho();
// set up instances
this.setupAtWho();
if (this.dataSource) {
if (!this.dataLoading && !this.cachedData) {
if (this.dataSource && !this.dataLoading && !this.cachedData) {
this.dataLoading = true;
setTimeout((function(_this) {
return function() {
var fetch;
fetch = _this.fetchData(_this.dataSource);
return fetch.done(function(data) {
_this.dataLoading = false;
return _this.loadData(data);
return this.fetchData(this.dataSource)
.done((data) => {
this.dataLoading = false;
this.loadData(data);
});
};
// We should wait until initializations are done
// and only trigger the last .setup since
// The previous .dataSource belongs to the previous issuable
// and the last one will have the **proper** .dataSource property
// TODO: Make this a singleton and turn off events when moving to another page
})(this), 1000);
}
if (this.cachedData != null) {
return this.loadData(this.cachedData);
}
}
},
}, 1000),
setupAtWho: function() {
// Emoji
this.input.atwho({
......
......@@ -738,6 +738,7 @@
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
e.preventDefault();
_this.selectRowAtIndex();
}
};
......
......@@ -292,7 +292,7 @@
return;
}
if (page === 'projects:boards:show') {
if ($('html').hasClass('issue-boards-page')) {
return;
}
if ($dropdown.hasClass('js-multiselect')) {
......@@ -334,7 +334,7 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
if (page === 'projects:boards:show') {
if ($('html').hasClass('issue-boards-page')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
}
......
(function() {
((global) => {
var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
this.MergeRequestWidget = (function() {
const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
<div class="ci_widget ci-success">
<%= ci_success_icon %>
<span>
Deployed to
<a href="<%- url %>" target="_blank" class="environment">
<%- name %>
</a>
<span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
<%- deployed_at %>
</span>
<a class="js-environment-link" href="<%- external_url %>" target="_blank">
<i class="fa fa-external-link"></i>
View on <%- external_url_formatted %>
</a>
</span>
</div>
</div>`;
global.MergeRequestWidget = (function() {
function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior
//
......@@ -10,17 +29,23 @@
// ci_status_url - String, URL to use to check CI status
//
this.opts = opts;
this.$widgetBody = $('.mr-widget-body');
$('#modal_merge_info').modal({
show: false
});
this.firstCICheck = true;
this.readyForCICheck = false;
this.readyForCIEnvironmentCheck = false;
this.cancel = false;
clearInterval(this.fetchBuildStatusInterval);
clearInterval(this.fetchBuildEnvironmentStatusInterval);
this.clearEventListeners();
this.addEventListeners();
this.getCIStatus(false);
this.getCIEnvironmentsStatus();
this.retrieveSuccessIcon();
this.pollCIStatus();
this.pollCIEnvironmentsStatus();
notifyPermissions();
}
......@@ -41,6 +66,7 @@
page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) {
clearInterval(_this.fetchBuildStatusInterval);
clearInterval(_this.fetchBuildEnvironmentStatusInterval);
_this.cancelPolling();
return _this.clearEventListeners();
}
......@@ -48,6 +74,12 @@
})(this));
};
MergeRequestWidget.prototype.retrieveSuccessIcon = function() {
const $ciSuccessIcon = $('.js-success-icon');
this.$ciSuccessIcon = $ciSuccessIcon.html();
$ciSuccessIcon.remove();
}
MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
if (deleteSourceBranch == null) {
deleteSourceBranch = false;
......@@ -62,7 +94,7 @@
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
} else {
callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch);
......@@ -118,6 +150,7 @@
if (data.status === '') {
return;
}
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) {
_this.opts.ci_status = data.status;
_this.showCIStatus(data.status);
......@@ -150,6 +183,41 @@
})(this));
};
MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
if (!this.readyForCIEnvironmentCheck) return;
this.getCIEnvironmentsStatus();
this.readyForCIEnvironmentCheck = false;
}, 300000);
};
MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
$.getJSON(this.opts.ci_environments_status_url, (environments) => {
if (this.cancel) return;
this.readyForCIEnvironmentCheck = true;
if (environments && environments.length) this.renderEnvironments(environments);
});
};
MergeRequestWidget.prototype.renderEnvironments = function(environments) {
for (let i = 0; i < environments.length; i++) {
const environment = environments[i];
if ($(`.mr-state-widget #${ environment.id }`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = $.timeago(environment.deployed_at) + '.';
} else {
$('.js-environment-timeago', $template).remove();
environment.name += '.';
}
environment.ci_success_icon = this.$ciSuccessIcon;
const templateString = _.unescape($template[0].outerHTML);
const template = _.template(templateString)(environment)
this.$widgetBody.before(template);
}
};
MergeRequestWidget.prototype.showCIStatus = function(state) {
var allowed_states;
if (state == null) {
......@@ -190,4 +258,4 @@
})();
}).call(this);
})(window.gl || (window.gl = {}));
......@@ -110,7 +110,7 @@
e.preventDefault();
return;
}
if (page === 'projects:boards:show') {
if ($('html').hasClass('issue-boards-page')) {
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault();
......
(function() {
function toggleGraph() {
((global) => {
class Pipelines {
constructor() {
$(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph);
this.addMarginToBuildColumns();
}
toggleGraph() {
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
const $btnText = $(this).find('.toggle-btn-text');
const $icon = $(this).find('.fa');
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
const expandIcon = 'fa-caret-down';
const hideIcon = 'fa-caret-up';
if(graphCollapsed) {
$btnText.text('Expand');
$icon.removeClass(hideIcon).addClass(expandIcon);
} else {
$btnText.text('Hide');
$icon.removeClass(expandIcon).addClass(hideIcon);
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
}
addMarginToBuildColumns() {
const $secondChildBuildNode = $('.build:nth-child(2)');
if ($secondChildBuildNode.length) {
const $firstChildBuildNode = $secondChildBuildNode.prev('.build');
const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column');
const $previousColumn = $multiBuildColumn.prev('.stage-column');
$multiBuildColumn.addClass('left-margin');
$firstChildBuildNode.addClass('left-connector');
$previousColumn.each(function() {
$this = $(this);
if ($('.build', $this).length === 1) $this.addClass('no-margin');
});
}
$('.pipeline-graph').removeClass('hidden');
}
}
global.Pipelines = Pipelines;
$(document).on('click', '.toggle-pipeline-btn', toggleGraph);
})();
})(window.gl || (window.gl = {}));
......@@ -4,7 +4,9 @@
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this);
this.$selects = $('.features select');
this.$selects = $('.features select').filter(function () {
return $(this).data('field');
});
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
......
......@@ -160,7 +160,7 @@
selectedId = user.id;
return;
}
if (page === 'projects:boards:show') {
if ($('html').hasClass('issue-boards-page')) {
selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl();
......@@ -261,10 +261,11 @@
}
}
if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
var trimmed = query.term.trim();
emailUser = {
name: "Invite \"" + query.term + "\"",
username: query.term,
id: query.term
username: trimmed,
id: trimmed
};
data.results.unshift(emailUser);
}
......
......@@ -4,7 +4,7 @@
width: 40px;
height: 40px;
padding: 0;
@include border-radius($avatar_radius);
border-radius: $avatar_radius;
border: 1px solid rgba(0, 0, 0, .1);
&.avatar-inline {
......@@ -17,7 +17,7 @@
}
&.avatar-tile {
@include border-radius(0);
border-radius: 0;
border: none;
}
......
......@@ -133,7 +133,7 @@
}
.identicon {
@include border-radius(50%);
border-radius: 50%;
}
}
......
@mixin btn-default {
@include border-radius(3px);
border-radius: 3px;
font-size: $gl-font-size;
font-weight: 500;
padding: $gl-vert-padding $gl-btn-padding;
......@@ -8,7 +8,7 @@
&:active {
outline: none;
background-color: $btn-active-gray;
@include box-shadow($gl-btn-active-background);
box-shadow: $gl-btn-active-background;
}
}
......@@ -43,7 +43,7 @@
&:active,
&.active {
@include box-shadow ($gl-btn-active-background);
box-shadow: $gl-btn-active-background;
background-color: $dark;
border-color: $border-dark;
......@@ -279,7 +279,7 @@
}
.active {
@include box-shadow($gl-btn-active-background);
box-shadow: $gl-btn-active-background;
border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important;
......
......@@ -73,7 +73,7 @@ label {
}
.form-control {
@include box-shadow(none);
box-shadow: none;
border-radius: 3px;
padding: $gl-vert-padding $gl-input-padding;
}
......
......@@ -16,7 +16,7 @@
margin-top: 5px;
}
@include border-radius(3px);
border-radius: 3px;
display: block;
float: left;
margin-right: 10px;
......
@mixin unique-keyframes {
$animation-name: unique-id();
@include webkit-prefix(animation-name, $animation-name);
@-webkit-keyframes #{$animation-name} {
@content;
}
@keyframes #{$animation-name} {
@content;
}
}
@mixin tanuki-logo-colors($path-color) {
fill: $path-color;
transition: all 0.8s;
......@@ -20,28 +8,6 @@
}
}
@mixin tanuki-second-highlight-animations($tanuki-color) {
@include unique-keyframes {
10%, 80% {
fill: #{$tanuki-color}
}
20%, 90% {
fill: lighten($tanuki-color, 25%);
}
}
}
@mixin tanuki-forth-highlight-animations($tanuki-color) {
@include unique-keyframes {
30%, 60% {
fill: #{$tanuki-color};
}
40%, 70% {
fill: lighten($tanuki-color, 25%);
}
}
}
.tanuki-logo {
.tanuki-left-ear,
......@@ -67,7 +33,7 @@
}
.tanuki-left-cheek {
@include unique-keyframes {
@include include-keyframes(animate-tanuki-left-cheek) {
0%, 10%, 100% {
fill: lighten($tanuki-yellow, 25%);
}
......@@ -78,15 +44,29 @@
}
.tanuki-left-eye {
@include tanuki-second-highlight-animations($tanuki-orange);
@include include-keyframes(animate-tanuki-left-eye) {
10%, 80% {
fill: $tanuki-orange;
}
20%, 90% {
fill: lighten($tanuki-orange, 25%);
}
}
}
.tanuki-left-ear {
@include tanuki-second-highlight-animations($tanuki-red);
@include include-keyframes(animate-tanuki-left-ear) {
10%, 80% {
fill: $tanuki-red;
}
20%, 90% {
fill: lighten($tanuki-red, 25%);
}
}
}
.tanuki-nose {
@include unique-keyframes {
@include include-keyframes(animate-tanuki-nose) {
20%, 70% {
fill: $tanuki-red;
}
......@@ -97,15 +77,29 @@
}
.tanuki-right-eye {
@include tanuki-forth-highlight-animations($tanuki-orange);
@include include-keyframes(animate-tanuki-right-eye) {
30%, 60% {
fill: $tanuki-orange;
}
40%, 70% {
fill: lighten($tanuki-orange, 25%);
}
}
}
.tanuki-right-ear {
@include tanuki-forth-highlight-animations($tanuki-red);
@include include-keyframes(animate-tanuki-right-ear) {
30%, 60% {
fill: $tanuki-red;
}
40%, 70% {
fill: lighten($tanuki-red, 25%);
}
}
}
.tanuki-right-cheek {
@include unique-keyframes {
@include include-keyframes(animate-tanuki-right-cheek) {
40% {
fill: $tanuki-yellow;
}
......
......@@ -86,7 +86,7 @@
}
.markdown-area {
@include border-radius(0);
border-radius: 0;
background: #fff;
border: 1px solid #ddd;
min-height: 140px;
......
/**
* Generic mixins
*/
@mixin box-shadow($shadow) {
box-shadow: $shadow;
}
@mixin border-radius($radius) {
border-radius: $radius;
}
/**
* Prefilled mixins
* Mixins with fixed values
......@@ -95,3 +84,10 @@
@content;
}
}
@mixin include-keyframes($animation-name) {
@include webkit-prefix(animation-name, $animation-name);
@include keyframes($animation-name) {
@content;
}
}
......@@ -133,5 +133,5 @@
font-size: 20px;
color: #777;
z-index: 100;
@include box-shadow(0 1px 2px #ddd);
box-shadow: 0 1px 2px #ddd;
}
......@@ -46,8 +46,8 @@
}
.select2-drop {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
@include border-radius ($border-radius-default);
box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0;
border-radius: $border-radius-default;
border: none;
min-width: 175px;
}
......@@ -72,7 +72,7 @@
.select2-container-active {
.select2-choice, .select2-choices {
@include box-shadow(none);
box-shadow: none;
}
}
......@@ -82,13 +82,13 @@
outline: 0;
background-image: none;
background-color: $white-dark;
@include box-shadow($gl-btn-active-gradient);
box-shadow: $gl-btn-active-gradient;
}
}
.select2-container-multi {
.select2-choices {
@include border-radius($border-radius-default);
border-radius: $border-radius-default;
border-color: $input-border;
background: none;
......@@ -123,7 +123,7 @@
&.select2-container-active .select2-choices,
&.select2-dropdown-open .select2-choices {
border-color: $border-white-normal;
@include box-shadow($gl-btn-active-gradient);
box-shadow: $gl-btn-active-gradient;
}
}
......@@ -157,7 +157,7 @@
background-repeat: no-repeat;
background-position: right 0 bottom 6px;
border: 1px solid $input-border;
@include border-radius($border-radius-default);
border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
......
......@@ -4,7 +4,7 @@
&.page-sidebar-pinned {
.sidebar-wrapper {
@include box-shadow(none);
box-shadow: none;
}
}
......@@ -17,7 +17,7 @@
width: 0;
overflow: hidden;
transition: width $sidebar-transition-duration;
@include box-shadow(2px 0 16px 0 $black-transparent);
box-shadow: 2px 0 16px 0 $black-transparent;
}
}
......@@ -100,7 +100,7 @@
.count {
float: right;
padding: 0 8px;
@include border-radius(6px);
border-radius: 6px;
}
}
......
......@@ -116,7 +116,7 @@
font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
@include border-radius(2px);
border-radius: 2px;
}
p > code {
......
......@@ -17,8 +17,10 @@ $white-normal: #ededed;
$white-dark: #ececec;
$gray-light: #fafafa;
$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
$gray-dark: #ededed;
$gray-darker: #eee;
$gray-darkest: #c9c9c9;
$green-light: #38ae67;
......@@ -33,6 +35,8 @@ $blue-medium-light: #3498cb;
$blue-medium: #2f8ebf;
$blue-medium-dark: #2d86b4;
$blue-light-transparent: rgba(44, 159, 216, 0.05);
$orange-light: #fc8a51;
$orange-normal: #e75e40;
$orange-dark: #ce5237;
......@@ -91,6 +95,7 @@ $table-text-gray: #8f8f8f;
$gl-font-size: 15px;
$gl-title-color: #333;
$gl-text-color: #5c5c5c;
$gl-text-color-light: #8c8c8c;
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
......
......@@ -50,7 +50,7 @@
.bordered-box {
border: 1px solid $border-color;
@include border-radius($border-radius-default);
border-radius: $border-radius-default;
}
......
.file-editor {
#editor {
border: none;
@include border-radius(0);
border-radius: 0;
height: 500px;
margin: 0;
padding: 0;
......
......@@ -91,7 +91,7 @@
float: right;
border: 1px solid #eee;
padding: 5px;
@include border-radius(5px);
border-radius: 5px;
background: $gray-light;
margin-left: 10px;
top: -6px;
......
.suggest-colors {
margin-top: 5px;
a {
@include border-radius(4px);
border-radius: 4px;
width: 30px;
height: 30px;
display: inline-block;
......@@ -17,7 +17,7 @@
overflow: hidden;
a {
@include border-radius(0);
border-radius: 0;
width: (100% / 7);
margin-right: 0;
margin-bottom: -5px;
......
......@@ -73,12 +73,12 @@
height: auto;
&.top {
@include border-radius(5px 5px 0 0);
border-radius: 5px 5px 0 0;
margin-bottom: 0;
}
&.bottom {
@include border-radius(0 0 5px 5px);
border-radius: 0 0 5px 5px;
border-top: 0;
margin-bottom: 20px;
}
......@@ -86,7 +86,7 @@
&.middle {
border-top: 0;
margin-bottom: 0;
@include border-radius(0);
border-radius: 0;
}
&:active, &:focus {
......
......@@ -6,7 +6,7 @@
background: $background-color;
color: $gl-gray;
border: 1px solid $border-color;
@include border-radius(2px);
border-radius: 2px;
form {
margin-bottom: 0;
......@@ -121,6 +121,10 @@
color: #5c5d5e;
}
.js-deployment-link {
display: inline-block;
}
.mr-widget-body {
h4 {
font-weight: 600;
......@@ -204,6 +208,18 @@
word-break: break-all;
}
.commits-empty {
text-align: center;
h4 {
padding-top: 20px;
padding-bottom: 10px;
}
svg {
width: 230px;
}
}
.mr-list {
.merge-request {
padding: 10px 15px;
......@@ -389,8 +405,12 @@
padding: 16px;
}
.content-block {
border-top: 1px solid $border-color;
padding: $gl-padding-top $gl-padding;
}
.comments-disabled-notif {
padding: 10px 16px;
.btn {
margin-left: 5px;
}
......@@ -401,10 +421,6 @@
margin: 0 7px;
}
.comments-disabled-notif {
border-top: 1px solid $border-color;
}
.dropdown-title {
color: $gl-text-color;
}
......
......@@ -334,7 +334,7 @@ ul.notes {
.add-diff-note {
margin-top: -4px;
@include border-radius(40px);
border-radius: 40px;
background: #fff;
padding: 4px;
font-size: 16px;
......
......@@ -303,16 +303,41 @@
.stage-column {
display: inline-block;
vertical-align: top;
margin-right: 65px;
&:not(:last-child) {
margin-right: 44px;
}
&.left-margin {
&:not(:first-child) {
margin-left: 44px;
.left-connector {
&::before {
content: '';
position: absolute;
top: 48%;
left: -48px;
border-top: 2px solid $border-color;
width: 48px;
height: 1px;
}
}
}
}
&.no-margin {
margin: 0;
}
li {
list-style: none;
}
.stage-name {
margin-bottom: 15px;
margin: 0 0 15px 10px;
font-weight: bold;
width: 150px;
width: 176px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
......@@ -321,17 +346,23 @@
.build {
border: 1px solid $border-color;
position: relative;
padding: 6px 10px;
padding: 7px 10px 8px;
border-radius: 30px;
width: 150px;
width: 186px;
margin-bottom: 10px;
&:hover {
background-color: $gray-lighter;
.dropdown-menu-toggle {
background-color: transparent;
}
}
&.playable {
background-color: $gray-light;
svg {
height: 12px;
width: 12px;
height: 13px;
width: 20px;
position: relative;
top: 1px;
......@@ -342,10 +373,20 @@
}
.build-content {
width: 130px;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
width: 164px;
.ci-status-icon {
svg {
height: 20px;
width: 20px;
}
}
.ci-status-text {
width: 110px;
width: 135px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
......@@ -356,44 +397,56 @@
}
a {
color: $layout-link-gray;
color: $gl-text-color-light;
text-decoration: none;
&:hover {
.ci-status-text {
text-decoration: underline;
}
}
}
.dropdown-menu-toggle {
border: none;
width: auto;
padding: 0;
color: $layout-link-gray;
color: $gl-text-color-light;
flex-grow: 1;
.ci-status-text {
width: 80px;
max-width: 112px;
width: auto;
}
}
.grouped-pipeline-dropdown {
padding: 8px 0;
width: 200px;
width: 186px;
left: auto;
right: -214px;
right: -197px;
top: -9px;
ul {
max-height: 245px;
overflow-y: scroll;
overflow: auto;
}
a {
color: $gl-text-color;
padding: 7px 8px 8px;
&:hover {
background-color: $blue-light-transparent;
border-radius: 3px;
a:hover {
.ci-status-text {
text-decoration: none;
}
}
}
svg {
width: 14px;
height: 14px;
}
.ci-status-text {
width: 145px;
width: 112px;
}
.arrow {
......@@ -426,9 +479,10 @@
}
.badge {
background-color: $gray-dark;
color: $layout-link-gray;
background-color: $gray-darker;
color: $gl-text-color-light;
font-weight: normal;
margin-left: $btn-xs-side-margin;
}
}
......@@ -442,10 +496,10 @@
&::after {
content: '';
position: absolute;
top: 50%;
right: -69px;
top: 48%;
right: -48px;
border-top: 2px solid $border-color;
width: 69px;
width: 48px;
height: 1px;
}
}
......@@ -454,25 +508,25 @@
&:not(:first-child) {
&::after, &::before {
content: '';
top: -47px;
top: -49px;
position: absolute;
border-bottom: 2px solid $border-color;
width: 20px;
height: 65px;
width: 25px;
height: 69px;
}
// Right connecting curves
&::after {
right: -20px;
right: -25px;
border-right: 2px solid $border-color;
border-radius: 0 0 15px;
border-radius: 0 0 20px;
}
// Left connecting curves
&::before {
left: -20px;
left: -25px;
border-left: 2px solid $border-color;
border-radius: 0 0 0 15px;
border-radius: 0 0 0 20px;
}
}
......@@ -480,7 +534,7 @@
&:nth-child(2) {
&::after, &::before {
height: 29px;
top: -10px;
top: -9px;
}
.curve {
display: block;
......@@ -538,20 +592,20 @@
width: 21px;
height: 25px;
position: absolute;
top: -29px;
top: -32px;
border-top: 2px solid $border-color;
}
&::after {
left: -39px;
left: -44px;
border-right: 2px solid $border-color;
border-radius: 0 15px;
border-radius: 0 20px;
}
&::before {
right: -39px;
right: -44px;
border-left: 2px solid $border-color;
border-radius: 15px 0 0;
border-radius: 20px 0 0;
}
}
}
......
......@@ -4,7 +4,7 @@
text-align: center;
.preview {
@include border-radius(4px);
border-radius: 4px;
height: 80px;
margin-bottom: 10px;
......@@ -47,7 +47,7 @@
width: 160px;
img {
@include border-radius(4px);
border-radius: 4px;
max-width: 100%;
}
......
......@@ -354,7 +354,7 @@ a.deploy-project-label {
justify-content: flex-start;
.fork-thumbnail {
@include border-radius($border-radius-base);
border-radius: $border-radius-base;
background-color: $white-light;
border: 1px solid $border-white-light;
height: 202px;
......@@ -371,7 +371,7 @@ a.deploy-project-label {
background-color: $gray-light;
border: 1px solid $gray-dark;
margin: 0 auto;
@include border-radius(50%);
border-radius: 50%;
i {
font-size: 100px;
color: $gray-dark;
......@@ -390,7 +390,7 @@ a.deploy-project-label {
}
img {
@include border-radius(50%);
border-radius: 50%;
max-width: 100px;
}
}
......@@ -496,7 +496,7 @@ pre.light-well {
}
.light-well {
@include border-radius (2px);
border-radius: 2px;
color: #5b6169;
font-size: 13px;
......
......@@ -4,7 +4,7 @@
margin-right: 10px;
border: 1px solid #eee;
white-space: nowrap;
@include border-radius(4px);
border-radius: 4px;
&:hover {
text-decoration: none;
......
module Ci
class ApplicationController < ::ApplicationController
def self.railtie_helpers_paths
"app/helpers/ci"
end
end
end
module Ci
class LintsController < ApplicationController
class LintsController < ::ApplicationController
before_action :authenticate_user!
def show
......
module Ci
class ProjectsController < Ci::ApplicationController
class ProjectsController < ::ApplicationController
before_action :project
before_action :no_cache, only: [:badge]
before_action :authorize_read_project!, except: [:badge, :index]
......
......@@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
@projects = TrendingProjectsFinder.new.execute
@projects = filter_projects(@projects)
@projects = filter_projects(Project.trending)
@projects = @projects.page(params[:page])
respond_to do |format|
......
class NamespacesController < ApplicationController
skip_before_action :authenticate_user!
def show
namespace = Namespace.find_by(path: params[:id])
if namespace
if namespace.is_a?(Group)
group = namespace
else
user = namespace.owner
end
end
if user
redirect_to user_path(user)
elsif group && can?(current_user, :read_group, group)
redirect_to group_path(group)
elsif current_user.nil?
authenticate_user!
else
render_404
end
end
end
class Projects::BoardListsController < Projects::ApplicationController
respond_to :json
before_action :authorize_admin_list!
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
def create
list = Boards::Lists::CreateService.new(project, current_user, list_params).execute
if list.valid?
render json: list.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
else
render json: list.errors, status: :unprocessable_entity
end
end
def update
service = Boards::Lists::MoveService.new(project, current_user, move_params)
if service.execute
head :ok
else
head :unprocessable_entity
end
end
def destroy
service = Boards::Lists::DestroyService.new(project, current_user, params)
if service.execute
head :ok
else
head :unprocessable_entity
end
end
def generate
service = Boards::Lists::GenerateService.new(project, current_user)
if service.execute
render json: project.board.lists.label.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
else
head :unprocessable_entity
end
end
private
def authorize_admin_list!
return render_403 unless can?(current_user, :admin_list, project)
end
def list_params
params.require(:list).permit(:label_id)
end
def move_params
params.require(:list).permit(:position).merge(id: params[:id])
end
def record_not_found(exception)
render json: { error: exception.message }, status: :not_found
end
end
......@@ -16,9 +16,8 @@ module Projects
end
def create
list = project.board.lists.find(params[:list_id])
service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
issue = service.execute(list)
issue = service.execute
if issue.valid?
render json: serialize_as_json(issue)
......@@ -60,15 +59,15 @@ module Projects
end
def filter_params
params.merge(id: params[:list_id])
params.merge(board_id: params[:board_id], id: params[:list_id])
end
def move_params
params.permit(:id, :from_list_id, :to_list_id)
params.permit(:board_id, :id, :from_list_id, :to_list_id)
end
def issue_params
params.require(:issue).permit(:title).merge(request: request)
params.require(:issue).permit(:title).merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
end
def serialize_as_json(resource)
......
......@@ -5,11 +5,11 @@ module Projects
before_action :authorize_read_list!, only: [:index]
def index
render json: serialize_as_json(project.board.lists)
render json: serialize_as_json(board.lists)
end
def create
list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute
list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute(board)
if list.valid?
render json: serialize_as_json(list)
......@@ -19,7 +19,7 @@ module Projects
end
def update
list = project.board.lists.movable.find(params[:id])
list = board.lists.movable.find(params[:id])
service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
if service.execute(list)
......@@ -30,8 +30,8 @@ module Projects
end
def destroy
list = project.board.lists.destroyable.find(params[:id])
service = ::Boards::Lists::DestroyService.new(project, current_user, params)
list = board.lists.destroyable.find(params[:id])
service = ::Boards::Lists::DestroyService.new(project, current_user)
if service.execute(list)
head :ok
......@@ -43,8 +43,8 @@ module Projects
def generate
service = ::Boards::Lists::GenerateService.new(project, current_user)
if service.execute
render json: serialize_as_json(project.board.lists.movable)
if service.execute(board)
render json: serialize_as_json(board.lists.movable)
else
head :unprocessable_entity
end
......@@ -60,6 +60,10 @@ module Projects
return render_403 unless can?(current_user, :read_list, project)
end
def board
@board ||= project.boards.find(params[:board_id])
end
def list_params
params.require(:list).permit(:label_id)
end
......
class Projects::BoardsController < Projects::ApplicationController
include IssuableCollections
respond_to :html
before_action :authorize_read_board!, only: [:index, :show]
before_action :authorize_read_board!, only: [:show]
def index
@boards = ::Boards::ListService.new(project, current_user).execute
respond_to do |format|
format.html
format.json do
render json: serialize_as_json(@boards)
end
end
end
def show
::Boards::CreateService.new(project, current_user).execute
@board = project.boards.find(params[:id])
respond_to do |format|
format.html
format.json do
render json: serialize_as_json(@board)
end
end
end
private
......@@ -14,4 +30,8 @@ class Projects::BoardsController < Projects::ApplicationController
def authorize_read_board!
return access_denied! unless can?(current_user, :read_board, project)
end
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
end
......@@ -38,12 +38,12 @@ class Projects::GraphsController < Projects::ApplicationController
@languages = @languages.map do |language|
name, share = language
color = Digest::SHA256.hexdigest(name)[0...6]
color = Linguist::Language[name].color || "##{Digest::SHA256.hexdigest(name)[0...6]}"
{
value: (share.to_f * 100 / total).round(2),
label: name,
color: "##{color}",
highlight: "##{color}"
color: color,
highlight: color
}
end
......
......@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check,
:ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts
:ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines]
......@@ -31,6 +31,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# Allow modify merge_request
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
def index
......@@ -354,6 +356,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render layout: false
end
def assign_related_issues
result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute
respond_to do |format|
format.html do
case result[:count]
when 0
flash[:error] = "Failed to assign you issues related to the merge request"
when 1
flash[:notice] = "1 issue has been assigned to you"
else
flash[:notice] = "#{result[:count]} issues have been assigned to you"
end
redirect_to(merge_request_path(@merge_request))
end
end
end
def ci_status
pipeline = @merge_request.pipeline
if pipeline
......@@ -382,6 +403,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
def ci_environments_status
environments =
begin
@merge_request.environments.map do |environment|
next unless can?(current_user, :read_environment, environment)
project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
{
id: environment.id,
name: environment.name,
url: namespace_project_environment_path(project.namespace, project, environment),
external_url: environment.external_url,
external_url_formatted: environment.formatted_external_url,
deployed_at: deployment.try(:created_at),
deployed_at_formatted: deployment.try(:formatted_deployment_time)
}
end.compact
end
render json: environments
end
protected
def selected_target_project
......
......@@ -20,6 +20,8 @@ class Projects::TagsController < Projects::ApplicationController
def show
@tag = @repository.find_tag(params[:id])
return render_404 unless @tag
@release = @project.releases.find_or_initialize_by(tag: @tag.name)
@commit = @repository.commit(@tag.target)
end
......
class SnippetsController < ApplicationController
include ToggleAwardEmoji
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
# Allow read snippet
before_action :authorize_read_snippet!, only: [:show, :raw]
before_action :authorize_read_snippet!, only: [:show, :raw, :download]
# Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update]
......@@ -12,7 +12,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy]
skip_before_action :authenticate_user!, only: [:index, :show, :raw]
skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download]
layout 'snippets'
respond_to :html
......@@ -75,6 +75,14 @@ class SnippetsController < ApplicationController
)
end
def download
send_data(
@snippet.content,
type: 'text/plain; charset=utf-8',
filename: @snippet.sanitized_file_name
)
end
protected
def snippet
......
# Finder for retrieving public trending projects in a given time range.
class TrendingProjectsFinder
# current_user - The currently logged in User, if any.
# last_months - The number of months to limit the trending data to.
def execute(months_limit = 1)
Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do
Project.public_only.trending(months_limit.months.ago)
end
end
private
def cache_key_for(months)
"trending_projects/#{months}"
end
end
module BoardsHelper
def board_data
board = @board || @boards.first
{
endpoint: namespace_project_boards_path(@project.namespace, @project),
board_id: board.id,
disabled: !can?(current_user, :admin_list, @project),
issue_link_base: namespace_project_issues_path(@project.namespace, @project)
}
end
end
......@@ -15,13 +15,14 @@ module ButtonHelper
#
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
class: "btn btn-clipboard",
class: "btn #{css_class}",
data: data,
type: :button,
title: "Copy to Clipboard"
title: 'Copy to Clipboard'
end
def http_clone_button(project, placement = 'right', append_link: true)
......
......@@ -72,6 +72,19 @@ module MergeRequestsHelper
)
end
def mr_assign_issues_link
issues = MergeRequests::AssignIssuesService.new(@project,
current_user,
merge_request: @merge_request,
closes_issues: mr_closes_issues
).assignable_issues
path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
if issues.present?
pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue"
link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post
end
end
def source_branch_with_namespace(merge_request)
branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
......@@ -110,4 +123,8 @@ module MergeRequestsHelper
def version_index(merge_request_diff)
@merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
end
def different_base?(version1, version2)
version1 && version2 && version1.base_commit_sha != version2.base_commit_sha
end
end
......@@ -3,6 +3,7 @@ module Ci
extend Ci::Model
include HasStatus
include Importable
include AfterCommitQueue
self.table_name = 'ci_commits'
......@@ -56,6 +57,10 @@ module Ci
pipeline.finished_at = Time.now
end
before_transition do |pipeline|
pipeline.update_duration
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)
......@@ -66,8 +71,8 @@ module Ci
update_all(latest_build_finished_at: pipeline.finished_at)
end
before_transition do |pipeline|
pipeline.update_duration
after_transition [:created, :pending, :running] => :success do |pipeline|
pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
end
after_transition do |pipeline, transition|
......@@ -292,9 +297,9 @@ module Ci
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
def merge_requests
@merge_requests ||=
project.merge_requests.where(source_branch: ref).
select { |merge_request| merge_request.pipeline.try(:id) == id }
@merge_requests ||= project.merge_requests
.where(source_branch: self.ref)
.select { |merge_request| merge_request.pipeline.try(:id) == self.id }
end
def merge_requests_with_active_first
......
class CommitStatus < ActiveRecord::Base
include HasStatus
include Importable
include AfterCommitQueue
self.table_name = 'ci_builds'
......@@ -85,25 +86,24 @@ class CommitStatus < ActiveRecord::Base
end
after_transition do |commit_status, transition|
commit_status.pipeline.try do |pipeline|
break if transition.loopback?
next if transition.loopback?
if commit_status.complete?
ProcessPipelineWorker.perform_async(pipeline.id)
commit_status.run_after_commit do
pipeline.try do |pipeline|
if complete?
PipelineProcessWorker.perform_async(pipeline.id)
else
PipelineUpdateWorker.perform_async(pipeline.id)
end
UpdatePipelineWorker.perform_async(pipeline.id)
end
true
end
after_transition [:created, :pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
end
after_transition any => :failed do |commit_status|
MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
commit_status.run_after_commit do
MergeRequests::AddTodoWhenBuildFailsService
.new(pipeline.project, nil).execute(self)
end
end
end
......
......@@ -11,9 +11,10 @@ class Compare
end
end
def initialize(compare, project)
def initialize(compare, project, straight: false)
@compare = compare
@project = project
@straight = straight
end
def commits
......@@ -45,6 +46,18 @@ class Compare
end
end
def start_commit_sha
start_commit.try(:sha)
end
def base_commit_sha
base_commit.try(:sha)
end
def head_commit_sha
commit.try(:sha)
end
def raw_diffs(*args)
@compare.diffs(*args)
end
......@@ -58,9 +71,9 @@ class Compare
def diff_refs
Gitlab::Diff::DiffRefs.new(
base_sha: base_commit.try(:sha),
start_sha: start_commit.try(:sha),
head_sha: commit.try(:sha)
base_sha: @straight ? start_commit_sha : base_commit_sha,
start_sha: start_commit_sha,
head_sha: head_commit_sha
)
end
end
......@@ -2,6 +2,8 @@ class CycleAnalytics
include Gitlab::Database::Median
include Gitlab::Database::DateTime
DEPLOYMENT_METRIC_STAGES = %i[production staging]
def initialize(project, from:)
@project = project
@from = from
......@@ -66,7 +68,7 @@ class CycleAnalytics
# cycle analytics stage.
interval_query = Arel::Nodes::As.new(
cte_table,
subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s))
subtract_datetimes(base_query_for(name), end_time_attrs, start_time_attrs, name.to_s))
median_datetime(cte_table, interval_query, name)
end
......@@ -75,7 +77,7 @@ class CycleAnalytics
# 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
def base_query_for(name)
arel_table = MergeRequestsClosingIssues.arel_table
# Load issues
......@@ -91,7 +93,11 @@ class CycleAnalytics
join(MergeRequest::Metrics.arel_table).
on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
if DEPLOYMENT_METRIC_STAGES.include?(name)
# 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
query
end
end
......@@ -40,7 +40,14 @@ class Deployment < ActiveRecord::Base
def includes_commit?(commit)
return false unless commit
# Before 8.10, deployments didn't have keep-around refs. Any deployment
# created before then could have a `sha` referring to a commit that no
# longer exists in the repository, so just ignore those.
begin
project.repository.is_ancestor?(commit.id, sha)
rescue Rugged::OdbError
false
end
end
def update_merge_request_metrics!
......@@ -77,6 +84,10 @@ class Deployment < ActiveRecord::Base
take
end
def formatted_deployment_time
created_at.to_time.in_time_zone.to_s(:medium)
end
private
def ref_path
......
......@@ -48,7 +48,22 @@ class Environment < ActiveRecord::Base
self.name == "production"
end
def first_deployment_for(commit)
ref = project.repository.ref_name_for_sha(ref_path, commit.sha)
return nil unless ref
deployment_id = ref.split('/').last
deployments.find(deployment_id)
end
def ref_path
"refs/environments/#{Shellwords.shellescape(name)}"
end
def formatted_external_url
return nil unless external_url
external_url.gsub(/\A.*?:\/\//, '')
end
end
......@@ -68,8 +68,10 @@ class Event < ActiveRecord::Base
true
elsif issue? || issue_note?
Ability.allowed?(user, :read_issue, note? ? note_target : target)
elsif merge_request? || merge_request_note?
Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
else
((merge_request? || note?) && target.present?) || milestone?
milestone?
end
end
......@@ -280,6 +282,10 @@ class Event < ActiveRecord::Base
note? && target && target.for_issue?
end
def merge_request_note?
note? && target && target.for_merge_request?
end
def project_snippet_note?
target.for_snippet?
end
......
......@@ -692,6 +692,8 @@ class MergeRequest < ActiveRecord::Base
def environments
return [] unless diff_head_commit
@environments ||=
begin
environments = source_project.environments_for(
source_branch, diff_head_commit)
environments += target_project.environments_for(
......@@ -699,6 +701,7 @@ class MergeRequest < ActiveRecord::Base
environments.uniq
end
end
def state_human_name
if merged?
......
......@@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
# Valid types of serialized diffs allowed by Gitlab::Git::Diff
VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta]
belongs_to :merge_request
state_machine :state, initial: :empty do
......@@ -164,12 +167,24 @@ class MergeRequestDiff < ActiveRecord::Base
self == merge_request.merge_request_diff
end
def compare_with(sha)
CompareService.new.execute(project, head_commit_sha, project, sha)
def compare_with(sha, straight: true)
# When compare merge request versions we want diff A..B instead of A...B
# so we handle cases when user does squash and rebase of the commits between versions.
# For this reason we set straight to true by default.
CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight)
end
private
# Old GitLab implementations may have generated diffs as ["--broken-diff"].
# Avoid an error 500 by ignoring bad elements. See:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
def valid_raw_diff?(raw)
return false unless raw.respond_to?(:each)
raw.any? { |element| VALID_CLASSES.include?(element.class) }
end
def dump_commits(commits)
commits.map(&:to_hash)
end
......@@ -200,7 +215,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def load_diffs(raw, options)
if raw.respond_to?(:each)
if valid_raw_diff?(raw)
if paths = options[:paths]
raw = raw.select do |diff|
paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
......
......@@ -62,14 +62,12 @@ class Namespace < ActiveRecord::Base
path = path.dup
# Get the email username by removing everything after an `@` sign.
path.gsub!(/@.*\z/, "")
# Usernames can't end in .git, so remove it.
path.gsub!(/\.git\z/, "")
# Remove dashes at the start of the username.
path.gsub!(/\A-+/, "")
# Remove periods at the end of the username.
path.gsub!(/\.+\z/, "")
# Remove everything that's not in the list of allowed characters.
path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
# Remove trailing violations ('.atom', '.git', or '.')
path.gsub!(/(\.atom|\.git|\.)*\z/, "")
# Remove leading violations ('-')
path.gsub!(/\A\-+/, "")
# Users with the great usernames of "." or ".." would end up with a blank username.
# Work around that by setting their username to "blank", followed by a counter.
......
......@@ -16,6 +16,9 @@ class Project < ActiveRecord::Base
extend Gitlab::ConfigHelper
class BoardLimitExceeded < StandardError; end
NUMBER_OF_PERMITTED_BOARDS = 1
UNKNOWN_IMPORT_URL = 'http://unknown.git'
cache_markdown_field :description, pipeline: :description
......@@ -65,8 +68,7 @@ class Project < ActiveRecord::Base
belongs_to :namespace
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
has_one :board, dependent: :destroy
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
# Project services
has_many :services
......@@ -376,19 +378,9 @@ class Project < ActiveRecord::Base
%r{(?<project>#{name_pattern}/#{name_pattern})}
end
def trending(since = 1.month.ago)
# By counting in the JOIN we don't expose the GROUP BY to the outer query.
# This means that calls such as "any?" and "count" just return a number of
# the total count, instead of the counts grouped per project as a Hash.
join_body = "INNER JOIN (
SELECT project_id, COUNT(*) AS amount
FROM notes
WHERE created_at >= #{sanitize(since)}
AND system IS FALSE
GROUP BY project_id
) join_note_counts ON projects.id = join_note_counts.project_id"
joins(join_body).reorder('join_note_counts.amount DESC')
def trending
joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id').
reorder('trending_projects.id ASC')
end
def cached_count
......@@ -838,11 +830,6 @@ class Project < ActiveRecord::Base
end
end
def update_merge_requests(oldrev, newrev, ref, user)
MergeRequests::RefreshService.new(self, user).
execute(oldrev, newrev, ref)
end
def valid_repo?
repository.exists?
rescue
......@@ -1350,4 +1337,8 @@ class Project < ActiveRecord::Base
shared_projects.any?
end
def validate_board_limit(board)
raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
end
end
......@@ -10,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base
belongs_to :group
validates :project_id, presence: true
validates :group_id, presence: true
validates :group, presence: true
validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
validates :group_access, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
......
......@@ -111,8 +111,10 @@ class Repository
def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
ref ||= root_ref
# Limited to 1000 commits for now, could be parameterized?
args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query})
args = %W(
#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset}
--max-count #{limit} --grep=#{query} --regexp-ignore-case
)
args = args.concat(%W(-- #{path})) if path.present?
git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp)
......@@ -717,6 +719,14 @@ class Repository
end
end
def ref_name_for_sha(ref_path, sha)
args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
# Not found -> ["", 0]
# Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
Gitlab::Popen.popen(args, path_to_repo).first.split.last
end
def refs_contains_sha(ref_type, sha)
args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
names = Gitlab::Popen.popen(args, path_to_repo).first
......@@ -1014,7 +1024,8 @@ class Repository
root_ref_commit = commit(root_ref)
if branch_commit
is_ancestor?(branch_commit.id, root_ref_commit.id)
same_head = branch_commit.id == root_ref_commit.id
!same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
else
nil
end
......
class TrendingProject < ActiveRecord::Base
belongs_to :project
# The number of months to include in the trending calculation.
MONTHS_TO_INCLUDE = 1
# The maximum number of projects to include in the trending set.
PROJECTS_LIMIT = 100
# Populates the trending projects table with the current list of trending
# projects.
def self.refresh!
# The calculation **must** run in a transaction. If the removal of data and
# insertion of new data were to run separately a user might end up with an
# empty list of trending projects for a short period of time.
transaction do
delete_all
timestamp = connection.quote(MONTHS_TO_INCLUDE.months.ago)
connection.execute <<-EOF.strip_heredoc
INSERT INTO #{table_name} (project_id)
SELECT project_id
FROM notes
INNER JOIN projects ON projects.id = notes.project_id
WHERE notes.created_at >= #{timestamp}
AND notes.system IS FALSE
AND projects.visibility_level = #{Gitlab::VisibilityLevel::PUBLIC}
GROUP BY project_id
ORDER BY count(*) DESC
LIMIT #{PROJECTS_LIMIT};
EOF
end
end
end
......@@ -589,6 +589,11 @@ class User < ActiveRecord::Base
end
def set_projects_limit
# `User.select(:id)` raises
# `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
# without this safeguard!
return unless self.has_attribute?(:projects_limit)
connection_default_value_defined = new_record? && !projects_limit_changed?
return unless self.projects_limit.nil? || connection_default_value_defined
......
......@@ -40,7 +40,6 @@ class ProjectPolicy < BasePolicy
can! :read_milestone
can! :read_project_snippet
can! :read_project_member
can! :read_merge_request
can! :read_note
can! :create_project
can! :create_issue
......@@ -63,6 +62,7 @@ class ProjectPolicy < BasePolicy
can! :read_pipeline
can! :read_environment
can! :read_deployment
can! :read_merge_request
end
# Permissions given when an user is team member of a project
......@@ -98,7 +98,6 @@ class ProjectPolicy < BasePolicy
can! :admin_milestone
can! :admin_project_snippet
can! :admin_project_member
can! :admin_merge_request
can! :admin_note
can! :admin_wiki
can! :admin_project
......@@ -118,6 +117,7 @@ class ProjectPolicy < BasePolicy
can! :read_container_image
can! :build_download_code
can! :build_read_container_image
can! :read_merge_request
end
def owner_access!
......@@ -139,11 +139,18 @@ class ProjectPolicy < BasePolicy
def team_access!(user)
access = project.team.max_member_access(user.id)
guest_access! if access >= Gitlab::Access::GUEST
reporter_access! if access >= Gitlab::Access::REPORTER
team_member_reporter_access! if access >= Gitlab::Access::REPORTER
developer_access! if access >= Gitlab::Access::DEVELOPER
master_access! if access >= Gitlab::Access::MASTER
return if access < Gitlab::Access::GUEST
guest_access!
return if access < Gitlab::Access::REPORTER
reporter_access!
team_member_reporter_access!
return if access < Gitlab::Access::DEVELOPER
developer_access!
return if access < Gitlab::Access::MASTER
master_access!
end
def archived_access!
......
module Boards
class BaseService < ::BaseService
delegate :board, to: :project
end
end
module Boards
class CreateService < Boards::BaseService
class CreateService < BaseService
def execute
create_board! unless project.board.present?
project.board
if project.boards.empty?
create_board!
else
project.boards.first
end
end
private
def create_board!
project.create_board
project.board.lists.create(list_type: :backlog)
project.board.lists.create(list_type: :done)
board = project.boards.create
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
board
end
end
end
module Boards
module Issues
class CreateService < Boards::BaseService
def execute(list)
params.merge!(label_ids: [list.label_id])
create_issue
class CreateService < BaseService
def execute
create_issue(params.merge(label_ids: [list.label_id]))
end
private
def create_issue
def board
@board ||= project.boards.find(params.delete(:board_id))
end
def list
@list ||= board.lists.find(params.delete(:list_id))
end
def create_issue(params)
::Issues::CreateService.new(project, current_user, params).execute
end
end
......
module Boards
module Issues
class ListService < Boards::BaseService
class ListService < BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless list.movable?
......@@ -10,6 +10,10 @@ module Boards
private
def board
@board ||= project.boards.find(params[:board_id])
end
def list
@list ||= board.lists.find(params[:id])
end
......
module Boards
module Issues
class MoveService < Boards::BaseService
class MoveService < BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
return false unless valid_move?
......@@ -10,6 +10,10 @@ module Boards
private
def board
@board ||= project.boards.find(params[:board_id])
end
def valid_move?
moving_from_list.present? && moving_to_list.present? &&
moving_from_list != moving_to_list
......@@ -49,7 +53,7 @@ module Boards
if moving_to_list.movable?
moving_from_list.label_id
else
board.lists.movable.pluck(:label_id)
project.boards.joins(:lists).merge(List.movable).pluck(:label_id)
end
Array(label_ids).compact
......
module Boards
class ListService < BaseService
def execute
create_board! if project.boards.empty?
project.boards
end
private
def create_board!
Boards::CreateService.new(project, current_user).execute
end
end
end
module Boards
module Lists
class CreateService < Boards::BaseService
def execute
class CreateService < BaseService
def execute(board)
List.transaction do
label = project.labels.find(params[:label_id])
position = next_position
position = next_position(board)
create_list(label, position)
create_list(board, label, position)
end
end
private
def next_position
def next_position(board)
max_position = board.lists.movable.maximum(:position)
max_position.nil? ? 0 : max_position.succ
end
def create_list(label, position)
def create_list(board, label, position)
board.lists.create(label: label, list_type: :label, position: position)
end
end
......
module Boards
module Lists
class DestroyService < Boards::BaseService
class DestroyService < BaseService
def execute(list)
return false unless list.destroyable?
@board = list.board
list.with_lock do
decrement_higher_lists(list)
remove_list(list)
......@@ -12,6 +14,8 @@ module Boards
private
attr_reader :board
def decrement_higher_lists(list)
board.lists.movable.where('position > ?', list.position)
.update_all('position = position - 1')
......
module Boards
module Lists
class GenerateService < Boards::BaseService
def execute
class GenerateService < BaseService
def execute(board)
return false unless board.lists.movable.empty?
List.transaction do
label_params.each { |params| create_list(params) }
label_params.each { |params| create_list(board, params) }
end
true
......@@ -13,9 +13,9 @@ module Boards
private
def create_list(params)
def create_list(board, params)
label = find_or_create_label(params)
Lists::CreateService.new(project, current_user, label_id: label.id).execute
Lists::CreateService.new(project, current_user, label_id: label.id).execute(board)
end
def find_or_create_label(params)
......
module Boards
module Lists
class ListService < BaseService
def execute(board)
board.lists
end
end
end
end
module Boards
module Lists
class MoveService < Boards::BaseService
class MoveService < BaseService
def execute(list)
@board = list.board
@old_position = list.position
@new_position = params[:position]
......@@ -16,7 +17,7 @@ module Boards
private
attr_reader :old_position, :new_position
attr_reader :board, :old_position, :new_position
def valid_move?
new_position.present? && new_position != old_position &&
......
......@@ -16,6 +16,8 @@ module Ci
process_stage(index)
end
@pipeline.update_status
# Return a flag if a when builds got enqueued
new_builds.flatten.any?
end
......
......@@ -3,7 +3,7 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
def execute(source_project, source_branch, target_project, target_branch)
def execute(source_project, source_branch, target_project, target_branch, straight: false)
source_commit = source_project.commit(source_branch)
return unless source_commit
......@@ -23,9 +23,10 @@ class CompareService
raw_compare = Gitlab::Git::Compare.new(
target_project.repository.raw_repository,
target_branch,
source_sha
source_sha,
straight
)
Compare.new(raw_compare, target_project)
Compare.new(raw_compare, target_project, straight: straight)
end
end
......@@ -63,13 +63,12 @@ class GitPushService < BaseService
protected
def update_merge_requests
@project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user)
UpdateMergeRequestsWorker.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
EventCreateService.new.push(@project, current_user, build_push_data)
SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
Ci::CreatePipelineService.new(project, current_user, build_push_data).execute
Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute
ProjectCacheWorker.perform_async(@project.id)
end
......@@ -148,16 +147,6 @@ class GitPushService < BaseService
push_commits)
end
def build_push_data_system_hook
@push_data_system ||= Gitlab::DataBuilder::Push.build(
@project,
current_user,
params[:oldrev],
params[:newrev],
params[:ref],
[])
end
def push_to_existing_branch?
# Return if this is not a push to a branch (e.g. new commits)
Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev])
......
......@@ -2,14 +2,14 @@ module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails
def execute(commit_status)
each_merge_request(commit_status) do |merge_request|
commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
end
end
# Closes any pending build failed todos for the parent MRs when a build is retried
def close(commit_status)
each_merge_request(commit_status) do |merge_request|
commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
end
end
......
module MergeRequests
class AssignIssuesService < BaseService
def assignable_issues
@assignable_issues ||= begin
if current_user == merge_request.author
closes_issues.select do |issue|
!issue.assignee_id? && can?(current_user, :admin_issue, issue)
end
else
[]
end
end
end
def execute
assignable_issues.each do |issue|
Issues::UpdateService.new(issue.project, current_user, assignee_id: current_user.id).execute(issue)
end
{
count: assignable_issues.count
}
end
private
def merge_request
params[:merge_request]
end
def closes_issues
@closes_issues ||= params[:closes_issues] || merge_request.closes_issues(current_user)
end
end
end
......@@ -42,28 +42,33 @@ module MergeRequests
super(:merge_request)
end
def merge_request_from(commit_status)
branches = commit_status.ref
def merge_requests_for(branch)
origin_merge_requests = @project.origin_merge_requests
.opened.where(source_branch: branch).to_a
# This is for ref-less builds
branches ||= @project.repository.branch_names_contains(commit_status.sha)
fork_merge_requests = @project.fork_merge_requests
.opened.where(source_branch: branch).to_a
return [] if branches.blank?
(origin_merge_requests + fork_merge_requests)
.uniq.select(&:source_project)
end
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
def pipeline_merge_requests(pipeline)
merge_requests_for(pipeline.ref).each do |merge_request|
next unless pipeline == merge_request.pipeline
merge_requests.uniq.select(&:source_project)
yield merge_request
end
end
def each_merge_request(commit_status)
merge_request_from(commit_status).each do |merge_request|
def commit_status_merge_requests(commit_status)
merge_requests_for(commit_status.ref).each do |merge_request|
pipeline = merge_request.pipeline
next unless pipeline
next unless pipeline.sha == commit_status.sha
yield merge_request, pipeline
yield merge_request
end
end
end
......
......@@ -4,7 +4,7 @@ module MergeRequests
merge_request = MergeRequest.new(params)
# Set MR attributes
merge_request.can_be_created = false
merge_request.can_be_created = true
merge_request.compare_commits = []
merge_request.source_project = project unless merge_request.source_project
......@@ -22,6 +22,12 @@ module MergeRequests
return build_failed(merge_request, message)
end
if merge_request.source_project == merge_request.target_project &&
merge_request.target_branch == merge_request.source_branch
return build_failed(merge_request, 'You must select different branches')
end
compare = CompareService.new.execute(
merge_request.source_project,
merge_request.source_branch,
......@@ -29,17 +35,8 @@ module MergeRequests
merge_request.target_branch,
)
commits = compare.commits
# At this point we decide if merge request can be created
# If we have at least one commit to merge -> creation allowed
if commits.present?
merge_request.compare_commits = commits
merge_request.can_be_created = true
merge_request.compare_commits = compare.commits
merge_request.compare = compare
else
merge_request.can_be_created = false
end
set_title_and_description(merge_request)
end
......@@ -89,6 +86,8 @@ module MergeRequests
end
end
merge_request.title = merge_request.wip_title if commits.empty?
merge_request
end
......
......@@ -18,12 +18,13 @@ module MergeRequests
merge_request.save
end
# Triggers the automatic merge of merge_request once the build succeeds
def trigger(commit_status)
each_merge_request(commit_status) do |merge_request, pipeline|
# Triggers the automatic merge of merge_request once the pipeline succeeds
def trigger(pipeline)
return unless pipeline.success?
pipeline_merge_requests(pipeline) do |merge_request|
next unless merge_request.merge_when_build_succeeds?
next unless merge_request.mergeable?
next unless pipeline.success?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
......
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.
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