diff options
338 files changed, 5674 insertions, 2081 deletions
diff --git a/.flayignore b/.flayignore new file mode 100644 index 00000000000..9c9875d4f9e --- /dev/null +++ b/.flayignore @@ -0,0 +1 @@ +*.erb diff --git a/.gitignore b/.gitignore index 73bde4cc761..f5b6427ca03 100644 --- a/.gitignore +++ b/.gitignore @@ -37,8 +37,10 @@ nohup.out public/assets/ public/uploads.* public/uploads/ +shared/artifacts/ rails_best_practices_output.html /tags tmp/ vendor/bundle/* builds/* +shared/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cf6d28b01af..94753093540 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -73,3 +73,17 @@ brakeman: tags: - ruby - mysql + +flog: + script: + - bundle exec rake flog + tags: + - ruby + - mysql + +flay: + script: + - bundle exec rake flay + tags: + - ruby + - mysql diff --git a/CHANGELOG b/CHANGELOG index d82cab6f36c..ea8d5d6000f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,10 +1,21 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.2.0 (unreleased) +v 8.3.0 (unreleased) + +v 8.2.0 + - Improved performance of finding projects and groups in various places + - Improved performance of rendering user profile pages and Atom feeds + - Fix grouping of contributors by email in graph. - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) + - Fix Drone CI service template not saving properly (Stan Hu) + - Fix avatars not showing in Atom feeds and project issues when Gravatar disabled (Stan Hu) - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749) - Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu) - Improved performance of finding users by one of their Email addresses + - Add allow_failure field to commit status API (Stan Hu) + - Commits without .gitlab-ci.yml are marked as skipped + - Save detailed error when YAML syntax is invalid + - Added build artifacts - Improved performance of replacing references in comments - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL @@ -15,25 +26,41 @@ v 8.2.0 (unreleased) - Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork - Use git follow flag for commits page when retrieve history for file or directory - Show merge request CI status on merge requests index page + - Send build name and stage in CI notification e-mail - Extend yml syntax for only and except to support specifying repository path - Enable shared runners to all new projects + - Bump GitLab-Workhorse to 0.4.1 + - Allow to define cache in `.gitlab-ci.yml` - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Remove deprecated CI events from project settings page - [API] Add ability to fetch the commit ID of the last commit that actually touched a file + - Fix omniauth documentation setting for omnibus configuration (Jon Cairns) - Add "New file" link to dropdown on project page - Include commit logs in project search - Add "added", "modified" and "removed" properties to commit object in webhook - Rename "Back to" links to "Go to" because its not always a case it point to place user come from - Allow groups to appear in the search results if the group owner allows it + - Add email notification to former assignee upon unassignment (Adam Lieskovský) - New design for project graphs page + - Remove deprecated dumped yaml file generated from previous job definitions - Fix incoming email config defaults + - Show specific runners from projects where user is master or owner - MR target branch is now visible on a list view when it is different from project's default one - Improve Continuous Integration graphs page + - Make color of "Accept Merge Request" button consistent with current build status + - Add ignore white space option in merge request diff and commit and compare view + - Ability to add release notes (markdown text and attachments) to git tags (aka Releases) + - Relative links from a repositories README.md now link to the default branch + - Fix trailing whitespace issue in merge request/issue title + - Fix bug when milestone/label filter was empty for dashboard issues page + - Add ability to create milestone in group projects from single form + - Prevent the last owner of a group from being able to delete themselves by 'adding' themselves as a master (James Lopez) v 8.1.4 - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) - Prevent redirect loop when home_page_url is set to the root URL - - Ability to add release notes (markdown text and attachments) to git tags + - Fix incoming email config defaults + - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) v 8.1.3 - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) @@ -41,7 +68,7 @@ v 8.1.3 - Use issue editor as cross reference comment author when issue is edited with a new mention - Add Facebook authentication -v 8.1.1 +v 8.1.2 - Fix cloning Wiki repositories via HTTP (Stan Hu) - Add migration to remove satellites directory - Fix specific runners visibility @@ -51,14 +78,15 @@ v 8.1.1 - Fix CI badge - Allow developer to manage builds +v 8.1.1 + - Removed, see 8.1.2 + v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) - Fix duplicate repositories in GitHub import page (Stan Hu) - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) - Adds ability to create directories using the web editor (Ben Ford) - Cleanup stuck CI builds - -v 8.1.0 (unreleased) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f79ff413a0..5d85c9f3fca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ By submitting code as an individual you agree to the [individual contributor lic ## Security vulnerability disclosure -Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. +Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. ## Closing policy for issues and merge requests @@ -35,7 +35,7 @@ The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab Do not use the issue tracker for feature requests. We have a specific [feature request forum](http://feedback.gitlab.com) for this purpose. Please keep feature requests as small and simple as possible, complex ones might be edited to make them small and simple. -Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](http://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there. +Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](https://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there. ### Issue tracker guidelines @@ -72,7 +72,7 @@ If you can, please submit a merge request with the fix or improvements including 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Add your changes to the [CHANGELOG](CHANGELOG) 1. If you are changing the README, some documentation or other things which have no effect on the tests, add `[ci skip]` somewhere in the commit message -1. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) +1. If you have multiple commits please combine them into one commit by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) 1. Push the commit to your fork 1. Submit a merge request (MR) to the master branch 1. The MR title should describe the change you want to make @@ -181,4 +181,4 @@ This code of conduct applies both within project spaces and in public spaces whe Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com -This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 338a5b5d8fe..e261122d5c4 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.6 +2.6.7 diff --git a/GITLAB_WORKHORSE b/GITLAB_WORKHORSE new file mode 100644 index 00000000000..267577d47e4 --- /dev/null +++ b/GITLAB_WORKHORSE @@ -0,0 +1 @@ +0.4.1 @@ -54,7 +54,7 @@ gem 'gollum-lib', '~> 4.0.2' gem "github-linguist", "~> 4.7.0", require: "linguist" # API -gem 'grape', '~> 0.6.1' +gem 'grape', '~> 0.13.0' gem 'grape-entity', '~> 0.4.2' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' @@ -259,6 +259,8 @@ group :development, :test do gem 'rubocop', '~> 0.28.0', require: false gem 'coveralls', '~> 0.8.2', require: false gem 'simplecov', '~> 0.10.0', require: false + gem 'flog', require: false + gem 'flay', require: false gem 'benchmark-ips', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index dca8606806a..99cdc2a50ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -194,6 +194,12 @@ GEM ffi (1.9.10) fission (0.5.0) CFPropertyList (~> 2.2) + flay (2.6.1) + ruby_parser (~> 3.0) + sexp_processor (~> 4.0) + flog (4.3.2) + ruby_parser (~> 3.1, > 3.1.0) + sexp_processor (~> 4.4) flowdock (0.7.0) httparty (~> 0.7) multi_json @@ -306,10 +312,10 @@ GEM gon (5.0.4) actionpack (>= 2.3.0) json - grape (0.6.1) + grape (0.13.0) activesupport builder - hashie (>= 1.2.0) + hashie (>= 2.1.0) multi_json (>= 1.3.2) multi_xml (>= 0.5.2) rack (>= 1.3.0) @@ -821,6 +827,8 @@ DEPENDENCIES enumerize (~> 0.7.0) factory_girl_rails (~> 4.3.0) ffaker (~> 2.0.0) + flay + flog fog (~> 1.25.0) font-awesome-rails (~> 4.2) foreman @@ -835,7 +843,7 @@ DEPENDENCIES gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.0.2) gon (~> 5.0.0) - grape (~> 0.6.1) + grape (~> 0.13.0) grape-entity (~> 0.4.2) haml-rails (~> 0.9.0) hipchat (~> 1.5.0) diff --git a/PROCESS.md b/PROCESS.md index a4b0c83644b..482ad5fe9e1 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -34,13 +34,18 @@ The most important thing is making sure valid issues receive feedback from the d ## Workflow labels -Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to re-evaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue. +Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to re-evaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue. - *Awaiting feedback*: Feedback pending from the reporter - *Awaiting confirmation of fix*: The issue should already be solved in **master** (generally you can avoid this workflow item and just close the issue right away) - *Attached MR*: There is a MR attached and the discussion should happen there - We need to let issues stay in sync with the MR's. We can do this with a "Closing #XXXX" or "Fixes #XXXX" comment in the MR. We can't close the issue when there is a merge request because sometimes a MR is not good and we just close the MR, then the issue must stay. -- *Awaiting developer action/feedback*: Issue needs to be fixed or clarified by a developer +- *Developer*: needs help from a developer +- *UX* needs needs help from a UX designer +- *Frontend* needs help from a Front-end engineer +- *Graphics* needs help from a Graphics designer + +Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label. ## Functional labels @@ -1 +1 @@ -8.1.0.pre +8.3.0.pre diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 951173af5d5..4059fc39c67 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -28,6 +28,8 @@ class Dispatcher when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() new DropzoneInput($('.milestone-form')) + when 'groups:milestones:new' + new ZenMode() when 'projects:compare:show' new Diff() when 'projects:issues:new','projects:issues:edit' diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee index cfe5508290f..f5584bcfe4b 100644 --- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee @@ -6,7 +6,7 @@ window.ContributorsStatGraphUtil = for entry in log @add_date(entry.date, total) unless total[entry.date]? - data = by_author[entry.author_name] #|| by_email[entry.author_email] + data = by_author[entry.author_name] || by_email[entry.author_email] data ?= @add_author(entry, by_author, by_email) @add_date(entry.date, data) unless data[entry.date] @@ -95,5 +95,4 @@ window.ContributorsStatGraphUtil = if date_range is null || date_range[0] <= new Date(date) <= date_range[1] true else - false - + false
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index ddbacd7fd41..40f4beb1968 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -328,6 +328,10 @@ table { } } +.well { + margin-bottom: 0; +} + .search_box { @extend .well; text-align: center; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 91e6975e269..02ea91602e8 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -118,6 +118,10 @@ header { } } } + + .impersonation i { + color: $red-normal; + } } @mixin collapsed-header { diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 99d028d1228..50c0cf61f4e 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -172,7 +172,7 @@ } .panel-body { - form { + form, pre { margin: 0; } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index fbd7c363de1..a0e5f7554ed 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -56,6 +56,7 @@ li { padding: 3px 0px; + line-height: 20px; } } .new-file { diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 56e24386463..9083bfb41cf 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -8,4 +8,10 @@ class Admin::ApplicationController < ApplicationController def authenticate_admin! return render_404 unless current_user.is_admin? end + + def authorize_impersonator! + if session[:impersonator_id] + User.find_by!(username: session[:impersonator_id]).admin? + end + end end diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 3d9c59050ff..a9bcfc7456a 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -58,6 +58,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :admin_notification_email, :user_oauth_applications, :shared_runners_enabled, + :max_artifacts_size, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/impersonation_controller.rb b/app/controllers/admin/impersonation_controller.rb new file mode 100644 index 00000000000..0382402afa6 --- /dev/null +++ b/app/controllers/admin/impersonation_controller.rb @@ -0,0 +1,32 @@ +class Admin::ImpersonationController < Admin::ApplicationController + skip_before_action :authenticate_admin!, only: :destroy + + before_action :user + before_action :authorize_impersonator! + + def create + session[:impersonator_id] = current_user.username + session[:impersonator_return_to] = request.env['HTTP_REFERER'] + + warden.set_user(user, scope: 'user') + + flash[:alert] = "You are impersonating #{user.username}." + + redirect_to root_path + end + + def destroy + redirect = session[:impersonator_return_to] + + warden.set_user(user, scope: 'user') + + session[:impersonator_return_to] = nil + session[:impersonator_id] = nil + + redirect_to redirect || root_path + end + + def user + @user ||= User.find_by!(username: params[:id] || session[:impersonator_id]) + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index c63d0793e31..d7c927d444c 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -63,12 +63,6 @@ class Admin::UsersController < Admin::ApplicationController end end - def login_as - sign_in(user) - flash[:alert] = "Logged in as #{user.username}" - redirect_to root_path - end - def disable_two_factor user.disable_two_factor! redirect_to admin_user_path(user), diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 809b44387ba..8406399fb60 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -26,10 +26,6 @@ module Ci redirect_to namespace_project_runners_path(project.gl_project.namespace, project.gl_project) end - def dumped_yaml - send_data @project.generated_yaml_config, filename: '.gitlab-ci.yml' - end - protected def project diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb new file mode 100644 index 00000000000..b428249acd3 --- /dev/null +++ b/app/controllers/concerns/global_milestones.rb @@ -0,0 +1,19 @@ +module GlobalMilestones + extend ActiveSupport::Concern + + def milestones + @milestones = MilestonesFinder.new.execute(@projects, params) + @milestones = GlobalMilestone.build_collection(@milestones) + @milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE) + end + + def milestone + milestones = Milestone.of_projects(@projects).where(title: params[:title]) + + if milestones.present? + @milestone = GlobalMilestone.new(params[:title], milestones) + else + render_404 + end + end +end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb new file mode 100644 index 00000000000..effd4721949 --- /dev/null +++ b/app/controllers/concerns/issues_action.rb @@ -0,0 +1,14 @@ +module IssuesAction + extend ActiveSupport::Concern + + def issues + @issues = get_issues_collection + @issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE) + @issues = @issues.preload(:author, :project) + + respond_to do |format| + format.html + format.atom { render layout: false } + end + end +end diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb new file mode 100644 index 00000000000..f7a25111db9 --- /dev/null +++ b/app/controllers/concerns/merge_requests_action.rb @@ -0,0 +1,9 @@ +module MergeRequestsAction + extend ActiveSupport::Concern + + def merge_requests + @merge_requests = get_merge_requests_collection + @merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE) + @merge_requests = @merge_requests.preload(:author, :target_project) + end +end diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 53896d4f2c7..2bdce0f8a00 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -1,34 +1,19 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController - before_action :load_projects + include GlobalMilestones + + before_action :projects + before_action :milestones, only: [:index] + before_action :milestone, only: [:show] def index - project_milestones = case params[:state] - when 'all'; state - when 'closed'; state('closed') - else state('active') - end - @dashboard_milestones = Milestones::GroupService.new(project_milestones).execute - @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(PER_PAGE) end def show - project_milestones = Milestone.where(project_id: @projects).order("due_date ASC") - @dashboard_milestone = Milestones::GroupService.new(project_milestones).milestone(title) end private - def load_projects - @projects = current_user.authorized_projects.sorted_by_activity.non_archived - end - - def title - params[:title] - end - - def state(state = nil) - conditions = { project_id: @projects } - conditions.reverse_merge!(state: state) if state - Milestone.where(conditions).order("title ASC") + def projects + @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived end end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 4ebb3d7276e..087da935087 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,25 +1,12 @@ class DashboardController < Dashboard::ApplicationController + include IssuesAction + include MergeRequestsAction + before_action :event_filter, only: :activity + before_action :projects, only: [:issues, :merge_requests] respond_to :html - def merge_requests - @merge_requests = get_merge_requests_collection - @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) - @merge_requests = @merge_requests.preload(:author, :target_project) - end - - def issues - @issues = get_issues_collection - @issues = @issues.page(params[:page]).per(PER_PAGE) - @issues = @issues.preload(:author, :project) - - respond_to do |format| - format.html - format.atom { render layout: false } - end - end - def activity @last_push = current_user.recent_push @@ -47,4 +34,8 @@ class DashboardController < Dashboard::ApplicationController @events = @event_filter.apply_filter(@events).with_associations @events = @events.limit(20).offset(params[:offset] || 0) end + + def projects + @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived + end end diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 6878d4bc07e..be801858eaf 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -1,8 +1,13 @@ class Groups::ApplicationController < ApplicationController layout 'group' + before_action :group private - + + def group + @group ||= Group.find_by(path: params[:group_id]) + end + def authorize_read_group! unless @group and can?(current_user, :read_group, @group) if current_user.nil? @@ -12,13 +17,13 @@ class Groups::ApplicationController < ApplicationController end end end - + def authorize_admin_group! unless can?(current_user, :admin_group, group) return render_404 end end - + def authorize_admin_group_member! unless can?(current_user, :admin_group_member, group) return render_403 diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb index 6aa64222f77..76c87366baa 100644 --- a/app/controllers/groups/avatars_controller.rb +++ b/app/controllers/groups/avatars_controller.rb @@ -1,8 +1,6 @@ -class Groups::AvatarsController < ApplicationController +class Groups::AvatarsController < Groups::ApplicationController def destroy - @group = Group.find_by(path: params[:group_id]) @group.remove_avatar! - @group.save redirect_to edit_group_path(@group) diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 91518c44a98..0e902c4bb43 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,11 +1,9 @@ class Groups::GroupMembersController < Groups::ApplicationController skip_before_action :authenticate_user!, only: [:index] - before_action :group # Authorize before_action :authorize_read_group! - before_action :authorize_admin_group!, except: [:index, :leave] - before_action :authorize_admin_group_member!, only: [:create, :resend_invite] + before_action :authorize_admin_group_member!, except: [:index, :leave] def index @project = @group.projects.find(params[:project_id]) if params[:project_id] @@ -18,7 +16,8 @@ class Groups::GroupMembersController < Groups::ApplicationController end @members = @members.order('access_level DESC').page(params[:page]).per(50) - @group_member = GroupMember.new + + @group_member = @group.group_members.new end def create @@ -28,24 +27,23 @@ class Groups::GroupMembersController < Groups::ApplicationController end def update - @member = @group.group_members.find(params[:id]) + @group_member = @group.group_members.find(params[:id]) - return render_403 unless can?(current_user, :update_group_member, @member) + return render_403 unless can?(current_user, :update_group_member, @group_member) - @member.update_attributes(member_params) + @group_member.update_attributes(member_params) end def destroy @group_member = @group.group_members.find(params[:id]) - if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner. - @group_member.destroy - respond_to do |format| - format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } - format.js { render nothing: true } - end - else - return render_403 + return render_403 unless can?(current_user, :destroy_group_member, @group_member) + + @group_member.destroy + + respond_to do |format| + format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } + format.js { render nothing: true } end end @@ -64,10 +62,11 @@ class Groups::GroupMembersController < Groups::ApplicationController end def leave - @group_member = @group.group_members.where(user_id: current_user.id).first + @group_member = @group.group_members.find_by(user_id: current_user) if can?(current_user, :destroy_group_member, @group_member) @group_member.destroy + redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.") else if @group.last_owner?(current_user) @@ -80,10 +79,6 @@ class Groups::GroupMembersController < Groups::ApplicationController protected - def group - @group ||= Group.find_by(path: params[:group_id]) - end - def member_params params.require(:group_member).permit(:access_level, :user_id) end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 669f7f3126d..10233222ee1 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -1,54 +1,55 @@ class Groups::MilestonesController < Groups::ApplicationController - before_action :authorize_group_milestone!, only: :update + include GlobalMilestones + + before_action :projects + before_action :milestones, only: [:index] + before_action :milestone, only: [:show, :update] + before_action :authorize_group_milestone!, only: [:create, :update] def index - project_milestones = case params[:state] - when 'all'; state - when 'closed'; state('closed') - else state('active') - end - @group_milestones = Milestones::GroupService.new(project_milestones).execute - @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(PER_PAGE) end - def show - project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC") - @group_milestone = Milestones::GroupService.new(project_milestones).milestone(title) + def new + @milestone = Milestone.new end - def update - project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC") - @group_milestones = Milestones::GroupService.new(project_milestones).milestone(title) + def create + project_ids = params[:milestone][:project_ids] + title = milestone_params[:title] - @group_milestones.milestones.each do |milestone| - Milestones::UpdateService.new(milestone.project, current_user, params[:milestone]).execute(milestone) + @group.projects.where(id: project_ids).each do |project| + Milestones::CreateService.new(project, current_user, milestone_params).execute end - respond_to do |format| - format.js - format.html do - redirect_to group_milestones_path(group) - end + redirect_to milestone_path(title) + end + + def show + end + + def update + @milestone.milestones.each do |milestone| + Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone) end + + redirect_back_or_default(default: milestone_path(@milestone.title)) end private - def group - @group ||= Group.find_by(path: params[:group_id]) + def authorize_group_milestone! + return render_404 unless can?(current_user, :admin_milestones, group) end - def title - params[:title] + def milestone_params + params.require(:milestone).permit(:title, :description, :due_date, :state_event) end - def state(state = nil) - conditions = { project_id: group.projects } - conditions.reverse_merge!(state: state) if state - Milestone.where(conditions).order("title ASC") + def milestone_path(title) + group_milestone_path(@group, title.parameterize, title: title) end - def authorize_group_milestone! - return render_404 unless can?(current_user, :admin_group, group) + def projects + @projects ||= @group.projects end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index fb4eb094f27..fb26a4e6fc3 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,4 +1,7 @@ class GroupsController < Groups::ApplicationController + include IssuesAction + include MergeRequestsAction + skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests] respond_to :html before_action :group, except: [:new, :create] @@ -53,23 +56,6 @@ class GroupsController < Groups::ApplicationController end end - def merge_requests - @merge_requests = get_merge_requests_collection - @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) - @merge_requests = @merge_requests.preload(:author, :target_project) - end - - def issues - @issues = get_issues_collection - @issues = @issues.page(params[:page]).per(PER_PAGE) - @issues = @issues.preload(:author, :project) - - respond_to do |format| - format.html - format.atom { render layout: false } - end - end - def edit end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 519d6d6127e..d3f926b62bc 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -29,7 +29,7 @@ class Projects::ApplicationController < ApplicationController private def ci_enabled - return render_404 unless @project.gitlab_ci? + return render_404 unless @project.builds_enabled? end def ci_project diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 953f30e7c03..4638f77b887 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -3,6 +3,7 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] before_action :authorize_manage_builds!, except: [:index, :show, :status] + before_action :authorize_download_build_artifacts!, only: [:download] layout "project" @@ -51,6 +52,18 @@ class Projects::BuildsController < Projects::ApplicationController redirect_to build_path(build) end + def download + unless artifacts_file.file_storage? + return redirect_to artifacts_file.url + end + + unless artifacts_file.exists? + return not_found! + end + + send_file artifacts_file.path, disposition: 'attachment' + end + def status render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) end @@ -67,6 +80,10 @@ class Projects::BuildsController < Projects::ApplicationController @build ||= ci_project.builds.unscoped.find_by!(id: params[:id]) end + def artifacts_file + build.artifacts_file + end + def build_path(build) namespace_project_build_path(build.gl_project.namespace, build.gl_project, build) end @@ -76,4 +93,14 @@ class Projects::BuildsController < Projects::ApplicationController return page_404 end end + + def authorize_download_build_artifacts! + unless can?(current_user, :download_build_artifacts, @project) + if current_user.nil? + return authenticate_user! + else + return render_404 + end + end + end end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 71aaad1fad6..5200d609cc9 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -12,15 +12,16 @@ class Projects::CompareController < Projects::ApplicationController def show base_ref = Addressable::URI.unescape(params[:from]) @ref = head_ref = Addressable::URI.unescape(params[:to]) + diff_options = { ignore_whitespace_change: true } if params[:w] == '1' compare_result = CompareService.new. - execute(@project, head_ref, @project, base_ref) + execute(@project, head_ref, @project, base_ref, diff_options) if compare_result @commits = Commit.decorate(compare_result.commits, @project) @diffs = compare_result.diffs - @commit = @commits.last - @first_commit = @commits.first + @commit = @project.commit(head_ref) + @first_commit = @project.commit(base_ref) @line_notes = [] end end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 066b66014f8..fb8788f0818 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -28,8 +28,8 @@ class Projects::ImportsController < Projects::ApplicationController if @project.import_finished? redirect_to(project_path(@project)) and return else - redirect_to new_namespace_project_import_path(@project.namespace, - @project) && return + redirect_to(new_namespace_project_import_path(@project.namespace, + @project)) and return end end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e767efbdc0c..e74c2905e48 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -158,10 +158,12 @@ class Projects::IssuesController < Projects::ApplicationController end def issue_params - params.require(:issue).permit( + permitted = params.require(:issue).permit( :title, :assignee_id, :position, :description, :milestone_id, :state_event, :task_num, label_ids: [] ) + params[:issue][:title].strip! if params[:issue][:title] + permitted end def bulk_update_params diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index b0788a2d073..188f0cc4cea 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -276,11 +276,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_request_params - params.require(:merge_request).permit( + permitted = params.require(:merge_request).permit( :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, :state_event, :description, :task_num, label_ids: [] ) + params[:merge_request][:title].strip! if params[:merge_request][:title] + permitted end # Make sure merge requests created before 8.0 diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 9de5269cd25..07eb94e4f48 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,6 +1,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController # Authorize - before_action :authorize_admin_project!, except: :leave + before_action :authorize_admin_project_member!, except: :leave def index @project_members = @project.project_members @@ -29,10 +29,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController @project_member = @project.project_members.new end - def new - @project_member = @project.project_members.new - end - def create @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user) @@ -41,11 +37,17 @@ class Projects::ProjectMembersController < Projects::ApplicationController def update @project_member = @project.project_members.find(params[:id]) + + return render_403 unless can?(current_user, :update_project_member, @project_member) + @project_member.update_attributes(member_params) end def destroy @project_member = @project.project_members.find(params[:id]) + + return render_403 unless can?(current_user, :destroy_project_member, @project_member) + @project_member.destroy respond_to do |format| @@ -71,16 +73,22 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def leave - if @project.namespace == current_user.namespace - message = 'You can not leave your own project. Transfer or delete the project.' - return redirect_back_or_default(default: { action: 'index' }, options: { alert: message }) - end + @project_member = @project.project_members.find_by(user_id: current_user) - @project.project_members.find_by(user_id: current_user).destroy + if can?(current_user, :destroy_project_member, @project_member) + @project_member.destroy - respond_to do |format| - format.html { redirect_to dashboard_projects_path } - format.js { render nothing: true } + respond_to do |format| + format.html { redirect_to dashboard_projects_path, notice: "You left the project." } + format.js { render nothing: true } + end + else + if current_user == @project.owner + message = 'You can not leave your own project. Transfer or delete the project.' + redirect_back_or_default(default: { action: 'index' }, options: { alert: message }) + else + render_403 + end end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 00d13a83ce8..23453195e85 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -72,8 +72,7 @@ class ProjectsController < ApplicationController def remove_fork return access_denied! unless can?(current_user, :remove_fork_project, @project) - if @project.forked? - @project.forked_project_link.destroy + if @project.unlink_fork flash[:notice] = 'The fork relationship has been removed.' end end @@ -213,7 +212,8 @@ class ProjectsController < ApplicationController params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, - :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar + :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, + :builds_enabled ) end @@ -242,7 +242,7 @@ class ProjectsController < ApplicationController project.repository_exists? && !project.empty_repo? end - # Override get_id from ExtractsPath, which returns the branch and file path + # Override get_id from ExtractsPath, which returns the branch and file path # for the blob/tree, which in this case is just the root of the default branch. def get_id project.repository.root_ref diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 1484356a7f4..30cb869eb2a 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,14 +3,11 @@ class UsersController < ApplicationController before_action :set_user def show - @contributed_projects = contributed_projects.joined(@user). - reject(&:forked?) + @contributed_projects = contributed_projects.joined(@user).reject(&:forked?) - @projects = @user.personal_projects. - where(id: authorized_projects_ids).includes(:namespace) + @projects = PersonalProjectsFinder.new(@user).execute(current_user) - # Collect only groups common for both users - @groups = @user.groups & GroupsFinder.new.execute(current_user) + @groups = JoinedGroupsFinder.new(@user).execute(current_user) respond_to do |format| format.html @@ -53,16 +50,8 @@ class UsersController < ApplicationController @user = User.find_by_username!(params[:username]) end - def authorized_projects_ids - # Projects user can view - @authorized_projects_ids ||= - ProjectsFinder.new.execute(current_user).pluck(:id) - end - def contributed_projects - @contributed_projects = Project. - where(id: authorized_projects_ids & @user.contributed_projects_ids). - includes(:namespace) + ContributedProjectsFinder.new(@user).execute(current_user) end def contributions_calendar @@ -73,9 +62,13 @@ class UsersController < ApplicationController def load_events # Get user activity feed for projects common for both users @events = @user.recent_events. - where(project_id: authorized_projects_ids). - with_associations + merge(projects_for_current_user). + references(:project). + with_associations. + limit_recent(20, params[:offset]) + end - @events = @events.limit(20).offset(params[:offset] || 0) + def projects_for_current_user + ProjectsFinder.new.execute(current_user) end end diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb new file mode 100644 index 00000000000..0209649b017 --- /dev/null +++ b/app/finders/contributed_projects_finder.rb @@ -0,0 +1,37 @@ +class ContributedProjectsFinder + def initialize(user) + @user = user + end + + # Finds the projects "@user" contributed to, limited to either public projects + # or projects visible to the given user. + # + # current_user - When given the list of the projects is limited to those only + # visible by this user. + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil) + if current_user + relation = projects_visible_to_user(current_user) + else + relation = public_projects + end + + relation.includes(:namespace).order_id_desc + end + + private + + def projects_visible_to_user(current_user) + authorized = @user.contributed_projects.visible_to_user(current_user) + + union = Gitlab::SQL::Union. + new([authorized.select(:id), public_projects.select(:id)]) + + Project.where("projects.id IN (#{union.to_sql})") + end + + def public_projects + @user.contributed_projects.public_only + end +end diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index b5f3176461c..91cb0f228f0 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -1,39 +1,44 @@ class GroupsFinder - def execute(current_user, options = {}) - all_groups(current_user) + # Finds the groups available to the given user. + # + # current_user - The user to find the groups for. + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil) + if current_user + relation = groups_visible_to_user(current_user) + else + relation = public_groups + end + + relation.order_id_desc end private - def all_groups(current_user) - group_ids = if current_user - if current_user.authorized_groups.any? - # User has access to groups - # - # Return only: - # groups with public projects - # groups with internal projects - # groups with joined projects - # - Project.public_and_internal_only.pluck(:namespace_id) + - current_user.authorized_groups.pluck(:id) - else - # User has no group membership - # - # Return only: - # groups with public projects - # groups with internal projects - # - Project.public_and_internal_only.pluck(:namespace_id) - end - else - # Not authenticated - # - # Return only: - # groups with public projects - Project.public_only.pluck(:namespace_id) - end - - Group.where("public IS TRUE OR id IN(?)", group_ids) + # This method returns the groups "current_user" can see. + def groups_visible_to_user(current_user) + base = groups_for_projects(public_and_internal_projects) + + union = Gitlab::SQL::Union. + new([base.select(:id), current_user.authorized_groups.select(:id)]) + + Group.where("namespaces.id IN (#{union.to_sql})") + end + + def public_groups + groups_for_projects(public_projects) + end + + def groups_for_projects(projects) + Group.public_and_given_groups(projects.select(:namespace_id)) + end + + def public_projects + Project.unscoped.public_only + end + + def public_and_internal_projects + Project.unscoped.public_and_internal_only end end diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb new file mode 100644 index 00000000000..e7523136fea --- /dev/null +++ b/app/finders/joined_groups_finder.rb @@ -0,0 +1,49 @@ +# Class for finding the groups a user is a member of. +class JoinedGroupsFinder + def initialize(user = nil) + @user = user + end + + # Finds the groups of the source user, optionally limited to those visible to + # the current user. + # + # current_user - If given the groups of "@user" will only include the groups + # "current_user" can also see. + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil) + if current_user + relation = groups_visible_to_user(current_user) + else + relation = public_groups + end + + relation.order_id_desc + end + + private + + # Returns the groups the user in "current_user" can see. + # + # This list includes all public/internal projects as well as the projects of + # "@user" that "current_user" also has access to. + def groups_visible_to_user(current_user) + base = @user.authorized_groups.visible_to_user(current_user) + extra = public_and_internal_groups + union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)]) + + Group.where("namespaces.id IN (#{union.to_sql})") + end + + def public_groups + groups_for_projects(@user.authorized_projects.public_only) + end + + def public_and_internal_groups + groups_for_projects(@user.authorized_projects.public_and_internal_only) + end + + def groups_for_projects(projects) + @user.groups.public_and_given_groups(projects.select(:namespace_id)) + end +end diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb new file mode 100644 index 00000000000..b704e878903 --- /dev/null +++ b/app/finders/milestones_finder.rb @@ -0,0 +1,12 @@ +class MilestonesFinder + def execute(projects, params) + milestones = Milestone.of_projects(projects) + milestones = milestones.order("due_date ASC") + + case params[:state] + when 'closed' then milestones.closed + when 'all' then milestones + else milestones.active + end + end +end diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb new file mode 100644 index 00000000000..a61ffa22990 --- /dev/null +++ b/app/finders/personal_projects_finder.rb @@ -0,0 +1,41 @@ +class PersonalProjectsFinder + def initialize(user) + @user = user + end + + # Finds the projects belonging to the user in "@user", limited to either + # public projects or projects visible to the given user. + # + # current_user - When given the list of projects is limited to those only + # visible by this user. + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil) + if current_user + relation = projects_visible_to_user(current_user) + else + relation = public_projects + end + + relation.includes(:namespace).order_id_desc + end + + private + + def projects_visible_to_user(current_user) + authorized = @user.personal_projects.visible_to_user(current_user) + + union = Gitlab::SQL::Union. + new([authorized.select(:id), public_and_internal_projects.select(:id)]) + + Project.where("projects.id IN (#{union.to_sql})") + end + + def public_projects + @user.personal_projects.public_only + end + + def public_and_internal_projects + @user.personal_projects.public_and_internal_only + end +end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index c81bb51583a..dd35c215c50 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -1,11 +1,39 @@ class ProjectsFinder - def execute(current_user, options = {}) + # Returns all projects, optionally including group projects a user has access + # to. + # + # ## Examples + # + # Retrieving all public projects: + # + # ProjectsFinder.new.execute + # + # Retrieving all public/internal projects and those the given user has access + # to: + # + # ProjectsFinder.new.execute(some_user) + # + # Retrieving all public/internal projects as well as the group's projects the + # user has access to: + # + # ProjectsFinder.new.execute(some_user, group: some_group) + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil, options = {}) group = options[:group] if group - group_projects(current_user, group) + base, extra = group_projects(current_user, group) else - all_projects(current_user) + base, extra = all_projects(current_user) + end + + if base and extra + union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)]) + + Project.where("projects.id IN (#{union.to_sql})") + else + base end end @@ -13,77 +41,36 @@ class ProjectsFinder def group_projects(current_user, group) if current_user - if group.users.include?(current_user) - # User is group member - # - # Return ALL group projects - group.projects - else - projects_members = ProjectMember.in_projects(group.projects). - with_user(current_user) - - if projects_members.any? - # User is a project member - # - # Return only: - # public projects - # internal projects - # joined projects - # - group.projects.where( - "projects.id IN (?) OR projects.visibility_level IN (?)", - projects_members.pluck(:source_id), - Project.public_and_internal_levels - ) - else - # User has no access to group or group projects - # - # Return only: - # public projects - # internal projects - # - group.projects.public_and_internal_only - end - end + [ + group_projects_for_user(current_user, group), + group.projects.public_and_internal_only + ] else - # Not authenticated - # - # Return only: - # public projects - group.projects.public_only + [group.projects.public_only] end end def all_projects(current_user) if current_user - if current_user.authorized_projects.any? - # User has access to private projects - # - # Return only: - # public projects - # internal projects - # joined projects - # - Project.where( - "projects.id IN (?) OR projects.visibility_level IN (?)", - current_user.authorized_projects.pluck(:id), - Project.public_and_internal_levels - ) - else - # User has no access to private projects - # - # Return only: - # public projects - # internal projects - # - Project.public_and_internal_only - end + [current_user.authorized_projects, public_and_internal_projects] else - # Not authenticated - # - # Return only: - # public projects - Project.public_only + [Project.public_only] end end + + def group_projects_for_user(current_user, group) + if group.users.include?(current_user) + group.projects + else + group.projects.visible_to_user(current_user) + end + end + + def public_projects + Project.unscoped.public_only + end + + def public_and_internal_projects + Project.unscoped.public_and_internal_only + end end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index b889fb28973..bfd3622a6a9 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -1,4 +1,8 @@ module DiffHelper + def diff_view + params[:view] == 'parallel' ? 'parallel' : 'inline' + end + def allowed_diff_size if diff_hard_limit_enabled? Commit::DIFF_HARD_LIMIT_FILES @@ -132,25 +136,11 @@ module DiffHelper end def inline_diff_btn - params_copy = params.dup - params_copy[:view] = 'inline' - # Always use HTML to handle case where JSON diff rendered this button - params_copy.delete(:format) - - link_to url_for(params_copy), id: "inline-diff-btn", class: (params[:view] != 'parallel' ? 'btn active' : 'btn') do - 'Inline' - end + diff_btn('Inline', 'inline', diff_view == 'inline') end def parallel_diff_btn - params_copy = params.dup - params_copy[:view] = 'parallel' - # Always use HTML to handle case where JSON diff rendered this button - params_copy.delete(:format) - - link_to url_for(params_copy), id: "parallel-diff-btn", class: (params[:view] == 'parallel' ? 'btn active' : 'btn') do - 'Side-by-side' - end + diff_btn('Side-by-side', 'parallel', diff_view == 'parallel') end def submodule_link(blob, ref, repository = @repository) @@ -171,7 +161,7 @@ module DiffHelper def commit_for_diff(diff) if diff.deleted_file first_commit = @first_commit || @commit - first_commit.parent + first_commit.parent || @first_commit else @commit end @@ -187,4 +177,18 @@ module DiffHelper def editable_diff?(diff) !diff.deleted_file && @merge_request && @merge_request.source_project end + + private + + def diff_btn(title, name, selected) + params_copy = params.dup + params_copy[:view] = name + + # Always use HTML to handle case where JSON diff rendered this button + params_copy.delete(:format) + + link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do + title + end + end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 6f69c2a9f32..dde83ff36b5 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -108,19 +108,23 @@ module EventsHelper end end elsif event.push? - if event.push_with_commits? && event.md_ref? - if event.commits_count > 1 - namespace_project_compare_url(event.project.namespace, event.project, - from: event.commit_from, to: - event.commit_to) - else - namespace_project_commit_url(event.project.namespace, event.project, - id: event.commit_to) - end + push_event_feed_url(event) + end + end + + def push_event_feed_url(event) + if event.push_with_commits? && event.md_ref? + if event.commits_count > 1 + namespace_project_compare_url(event.project.namespace, event.project, + from: event.commit_from, to: + event.commit_to) else - namespace_project_commits_url(event.project.namespace, event.project, - event.ref_name) + namespace_project_commit_url(event.project.namespace, event.project, + id: event.commit_to) end + else + namespace_project_commits_url(event.project.namespace, event.project, + event.ref_name) end end @@ -198,7 +202,7 @@ module EventsHelper xml.link href: event_link xml.title truncate(event_title, length: 80) xml.updated event.created_at.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email) + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) xml.author do |author| xml.name event.author_name xml.email event.author_email diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 65813482120..98c6d9d5d2e 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -46,39 +46,13 @@ module GitlabMarkdownHelper end def markdown(text, context = {}) - return "" unless text.present? - - context.reverse_merge!( - path: @path, - pipeline: :default, - project: @project, - project_wiki: @project_wiki, - ref: @ref - ) - - user = current_user if defined?(current_user) - - html = Gitlab::Markdown.render(text, context) - Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], project: @project, user: user) + process_markdown(text, context) end # TODO (rspeicher): Remove all usages of this helper and just call `markdown` # with a custom pipeline depending on the content being rendered def gfm(text, options = {}) - return "" unless text.present? - - options.reverse_merge!( - path: @path, - pipeline: :default, - project: @project, - project_wiki: @project_wiki, - ref: @ref - ) - - user = current_user if defined?(current_user) - - html = Gitlab::Markdown.gfm(text, options) - Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user) + process_markdown(text, options, :gfm) end def asciidoc(text) @@ -204,4 +178,26 @@ module GitlabMarkdownHelper '' end end + + def process_markdown(text, options, method = :markdown) + return "" unless text.present? + + options.reverse_merge!( + path: @path, + pipeline: :default, + project: @project, + project_wiki: @project_wiki, + ref: @ref + ) + + user = current_user if defined?(current_user) + + html = if method == :gfm + Gitlab::Markdown.gfm(text, options) + else + Gitlab::Markdown.render(text, options) + end + + Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user) + end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index fda18e7b316..beb083d82dc 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -74,7 +74,7 @@ module IssuesHelper issue.project, issue) xml.title truncate(issue.title, length: 80) xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(issue.author_email) + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) xml.author do |author| xml.name issue.author_name xml.email issue.author_email diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index ee04ace35d0..795fb439f25 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -100,7 +100,7 @@ module LabelsHelper Label.where(project_id: @projects) end - grouped_labels = Labels::GroupService.new(labels).execute + grouped_labels = GlobalLabel.build_collection(labels) grouped_labels.unshift(Label::None) grouped_labels.unshift(Label::Any) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 37a5b58cce8..ad43892b639 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -28,7 +28,7 @@ module MilestonesHelper Milestone.where(project_id: @projects) end.active - grouped_milestones = Milestones::GroupService.new(milestones).execute + grouped_milestones = GlobalMilestone.build_collection(milestones) grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::Any) diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index b3132a1f3ba..e7f3cb21038 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -17,15 +17,6 @@ module NamespacesHelper grouped_options_for_select(options, selected) end - def namespace_select_tag(id, opts = {}) - css_class = "ajax-namespace-select " - css_class << "multiselect " if opts[:multiple] - css_class << (opts[:class] || '') - value = opts[:selected] || '' - - hidden_field_tag(id, value, class: css_class) - end - def namespace_icon(namespace, size = 40) if namespace.kind_of?(Group) group_icon(namespace) diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index cf11f8e5320..499c655d2bf 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -16,40 +16,28 @@ module NotificationsHelper def notification_list_item(notification_level, user_membership) case notification_level when Notification::N_DISABLED - content_tag(:li, class: active_level_for(user_membership, Notification::N_DISABLED)) do - link_to '#', class: 'update-notification', data: { notification_level: Notification::N_DISABLED } do - icon('microphone-slash fw', text: 'Disabled') - end - end + update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash') when Notification::N_PARTICIPATING - content_tag(:li, class: active_level_for(user_membership, Notification::N_PARTICIPATING)) do - link_to '#', class: 'update-notification', data: { notification_level: Notification::N_PARTICIPATING } do - icon('volume-up fw', text: 'Participate') - end - end + update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up') when Notification::N_WATCH - content_tag(:li, class: active_level_for(user_membership, Notification::N_WATCH)) do - link_to '#', class: 'update-notification', data: { notification_level: Notification::N_WATCH } do - icon('eye fw', text: 'Watch') - end - end + update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye') when Notification::N_MENTION - content_tag(:li, class: active_level_for(user_membership, Notification::N_MENTION)) do - link_to '#', class: 'update-notification', data: { notification_level: Notification::N_MENTION } do - icon('at fw', text: 'On mention') - end - end + update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at') when Notification::N_GLOBAL - content_tag(:li, class: active_level_for(user_membership, Notification::N_GLOBAL)) do - link_to '#', class: 'update-notification', data: { notification_level: Notification::N_GLOBAL } do - icon('globe fw', text: 'Global') - end - end + update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe') else # do nothing end end + def update_notification_link(notification_level, user_membership, title, icon) + content_tag(:li, class: active_level_for(user_membership, notification_level)) do + link_to '#', class: 'update-notification', data: { notification_level: notification_level } do + icon("#{icon} fw", text: title) + end + end + end + def notification_label(user_membership) Notification.new(user_membership).to_s end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5301c2ccf76..c9cd4a0d54c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -117,7 +117,7 @@ module ProjectsHelper nav_tabs << :merge_requests end - if project.gitlab_ci? && can?(current_user, :read_build, project) + if project.builds_enabled? && can?(current_user, :read_build, project) nav_tabs << :builds end @@ -253,14 +253,6 @@ module ProjectsHelper filename_path(project, :version) end - def hidden_pass_url(original_url) - result = URI(original_url) - result.password = '*****' unless result.password.nil? - result - rescue - original_url - end - def project_wiki_path_with_version(proj, page, version, is_newest) url_params = is_newest ? {} : { version_id: version } namespace_project_wiki_path(proj.namespace, proj, page, url_params) diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 12fce8db701..7e54d4d1b5b 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -35,8 +35,20 @@ module SelectsHelper end def groups_select_tag(id, opts = {}) - css_class = "ajax-groups-select " - css_class << "multiselect " if opts[:multiple] + opts[:class] ||= '' + opts[:class] << ' ajax-groups-select' + select2_tag(id, opts) + end + + def namespace_select_tag(id, opts = {}) + opts[:class] ||= '' + opts[:class] << ' ajax-namespace-select' + select2_tag(id, opts) + end + + def select2_tag(id, opts = {}) + css_class = '' + css_class << 'multiselect ' if opts[:multiple] css_class << (opts[:class] || '') value = opts[:selected] || '' diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 2c035fbb70b..abdeefed5ef 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -1,53 +1,49 @@ module Emails module Issues def new_issue_email(recipient_id, issue_id) - @issue = Issue.find(issue_id) - @project = @issue.project - @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - mail_new_thread(@issue, - from: sender(@issue.author_id), - to: recipient(recipient_id), - subject: subject("#{@issue.title} (##{@issue.iid})")) - - SentNotification.record(@issue, recipient_id, reply_key) + issue_mail_with_notification(issue_id, recipient_id) do + mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id)) + end end def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id) - @issue = Issue.find(issue_id) - @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id - @project = @issue.project - @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - mail_answer_thread(@issue, - from: sender(updated_by_user_id), - to: recipient(recipient_id), - subject: subject("#{@issue.title} (##{@issue.iid})")) - - SentNotification.record(@issue, recipient_id, reply_key) + issue_mail_with_notification(issue_id, recipient_id) do + @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) + end end def closed_issue_email(recipient_id, issue_id, updated_by_user_id) - @issue = Issue.find issue_id - @project = @issue.project - @updated_by = User.find updated_by_user_id - @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - mail_answer_thread(@issue, - from: sender(updated_by_user_id), - to: recipient(recipient_id), - subject: subject("#{@issue.title} (##{@issue.iid})")) - - SentNotification.record(@issue, recipient_id, reply_key) + issue_mail_with_notification(issue_id, recipient_id) do + @updated_by = User.find updated_by_user_id + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) + end end def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) - @issue = Issue.find issue_id - @issue_status = status + issue_mail_with_notification(issue_id, recipient_id) do + @issue_status = status + @updated_by = User.find updated_by_user_id + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) + end + end + + private + + def issue_thread_options(sender_id, recipient_id) + { + from: sender(sender_id), + to: recipient(recipient_id), + subject: subject("#{@issue.title} (##{@issue.iid})") + } + end + + def issue_mail_with_notification(issue_id, recipient_id) + @issue = Issue.find(issue_id) @project = @issue.project - @updated_by = User.find updated_by_user_id @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - mail_answer_thread(@issue, - from: sender(updated_by_user_id), - to: recipient(recipient_id), - subject: subject("#{@issue.title} (##{@issue.iid})")) + + yield SentNotification.record(@issue, recipient_id, reply_key) end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 87ba94a583d..65f37e92677 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -1,49 +1,54 @@ module Emails module Notes def note_commit_email(recipient_id, note_id) - @note = Note.find(note_id) - @commit = @note.noteable - @project = @note.project - @target_url = namespace_project_commit_url(@project.namespace, @project, - @commit, anchor: - "note_#{@note.id}") - mail_answer_thread(@commit, - from: sender(@note.author_id), - to: recipient(recipient_id), - subject: subject("#{@commit.title} (#{@commit.short_id})")) - - SentNotification.record_note(@note, recipient_id, reply_key) + note_mail_with_notification(note_id, recipient_id) do + @commit = @note.noteable + @target_url = namespace_project_commit_url(*note_target_url_options) + + mail_answer_thread(@commit, + from: sender(@note.author_id), + to: recipient(recipient_id), + subject: subject("#{@commit.title} (#{@commit.short_id})")) + end end def note_issue_email(recipient_id, note_id) - @note = Note.find(note_id) - @issue = @note.noteable - @project = @note.project - @target_url = namespace_project_issue_url(@project.namespace, @project, - @issue, anchor: - "note_#{@note.id}") - mail_answer_thread(@issue, - from: sender(@note.author_id), - to: recipient(recipient_id), - subject: subject("#{@issue.title} (##{@issue.iid})")) - - SentNotification.record_note(@note, recipient_id, reply_key) + note_mail_with_notification(note_id, recipient_id) do + @issue = @note.noteable + @target_url = namespace_project_issue_url(*note_target_url_options) + mail_answer_thread(@issue, note_thread_options(recipient_id)) + end end def note_merge_request_email(recipient_id, note_id) + note_mail_with_notification(note_id, recipient_id) do + @merge_request = @note.noteable + @target_url = namespace_project_merge_request_url(*note_target_url_options) + mail_answer_thread(@merge_request, note_thread_options(recipient_id)) + end + end + + private + + def note_target_url_options + [@project.namespace, @project, @note.noteable, anchor: "note_#{@note.id}"] + end + + def note_thread_options(recipient_id) + { + from: sender(@note.author_id), + to: recipient(recipient_id), + subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})") + } + end + + def note_mail_with_notification(note_id, recipient_id) @note = Note.find(note_id) - @merge_request = @note.noteable @project = @note.project - @target_url = namespace_project_merge_request_url(@project.namespace, - @project, - @merge_request, anchor: - "note_#{@note.id}") - mail_answer_thread(@merge_request, - from: sender(@note.author_id), - to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) - - SentNotification.record_note(@note, recipient_id, reply_key) + + yield + + SentNotification.record(@note, recipient_id, reply_key) end end end diff --git a/app/models/ability.rb b/app/models/ability.rb index b72178fa126..500af08d209 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -15,6 +15,7 @@ class Ability when "Group" then group_abilities(user, subject) when "Namespace" then namespace_abilities(user, subject) when "GroupMember" then group_member_abilities(user, subject) + when "ProjectMember" then project_member_abilities(user, subject) else [] end.concat(global_abilities(user)) end @@ -154,6 +155,7 @@ class Ability :create_merge_request, :create_wiki, :manage_builds, + :download_build_artifacts, :push_code ] end @@ -230,18 +232,19 @@ class Ability # Only group masters and group owners can create new projects in group if group.has_master?(user) || group.has_owner?(user) || user.admin? - rules.push(*[ + rules += [ :create_projects, - ]) + :admin_milestones + ] end # Only group owner and administrators can admin group if group.has_owner?(user) || user.admin? - rules.push(*[ + rules += [ :admin_group, :admin_namespace, :admin_group_member - ]) + ] end rules.flatten @@ -252,16 +255,15 @@ class Ability # Only namespace owner and administrators can admin it if namespace.owner == user || user.admin? - rules.push(*[ + rules += [ :create_projects, :admin_namespace - ]) + ] end rules.flatten end - [:issue, :merge_request].each do |name| define_method "#{name}_abilities" do |user, subject| rules = [] @@ -302,15 +304,39 @@ class Ability rules = [] target_user = subject.user group = subject.group - can_manage = group_abilities(user, group).include?(:admin_group_member) - if can_manage && (user != target_user) - rules << :update_group_member - rules << :destroy_group_member + unless group.last_owner?(target_user) + can_manage = group_abilities(user, group).include?(:admin_group_member) + + if can_manage && user != target_user + rules << :update_group_member + rules << :destroy_group_member + end + + if user == target_user + rules << :destroy_group_member + end end - if !group.last_owner?(user) && (can_manage || (user == target_user)) - rules << :destroy_group_member + rules + end + + def project_member_abilities(user, subject) + rules = [] + target_user = subject.user + project = subject.project + + unless target_user == project.owner + can_manage = project_abilities(user, project).include?(:admin_project_member) + + if can_manage && user != target_user + rules << :update_project_member + rules << :destroy_project_member + end + + if user == target_user + rules << :destroy_project_member + end end rules @@ -318,10 +344,10 @@ class Ability def abilities @abilities ||= begin - abilities = Six.new - abilities << self - abilities - end + abilities = Six.new + abilities << self + abilities + end end private diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 266045f7afa..9e70247ef51 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -23,6 +23,10 @@ # after_sign_out_path :string(255) # session_expire_delay :integer default(10080), not null # import_sources :text +# help_page_text :text +# admin_notification_email :string(255) +# shared_runners_enabled :boolean default(TRUE), not null +# max_artifacts_size :integer default(100), not null # class ApplicationSetting < ActiveRecord::Base @@ -68,8 +72,14 @@ class ApplicationSetting < ActiveRecord::Base end end + after_commit do + Rails.cache.write('application_setting.last', self) + end + def self.current - ApplicationSetting.last + Rails.cache.fetch('application_setting.last') do + ApplicationSetting.last + end end def self.create_from_defaults @@ -89,6 +99,7 @@ class ApplicationSetting < ActiveRecord::Base restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], + max_artifacts_size: Settings.gitlab_ci['max_artifacts_size'], ) end diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb index 0cf496f7d81..1307fa0b472 100644 --- a/app/models/ci/application_setting.rb +++ b/app/models/ci/application_setting.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: application_settings +# Table name: ci_application_settings # # id :integer not null, primary key # all_broken_builds :boolean @@ -12,9 +12,15 @@ module Ci class ApplicationSetting < ActiveRecord::Base extend Ci::Model - + + after_commit do + Rails.cache.write('ci_application_setting.last', self) + end + def self.current - Ci::ApplicationSetting.last + Rails.cache.fetch('ci_application_setting.last') do + Ci::ApplicationSetting.last + end end def self.create_from_defaults diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 7f185ae7cc3..e78b154084b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: builds +# Table name: ci_builds # # id :integer not null, primary key # project_id :integer @@ -11,16 +11,24 @@ # updated_at :datetime # started_at :datetime # runner_id :integer -# commit_id :integer # coverage :float +# commit_id :integer # commands :text # job_id :integer # name :string(255) +# deploy :boolean default(FALSE) # options :text # allow_failure :boolean default(FALSE), not null # stage :string(255) -# deploy :boolean default(FALSE) # trigger_request_id :integer +# stage_idx :integer +# tag :boolean +# ref :string(255) +# user_id :integer +# type :string(255) +# target_url :string(255) +# description :string(255) +# artifacts_file :text # module Ci @@ -39,6 +47,8 @@ module Ci scope :ignore_failures, ->() { where(allow_failure: false) } scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } + mount_uploader :artifacts_file, ArtifactUploader + acts_as_taggable # To prevent db load megabytes of data from trace @@ -217,6 +227,14 @@ module Ci "#{dir_to_trace}/#{id}.log" end + def token + project.token + end + + def valid_token? token + project.valid_token? token + end + def target_url Gitlab::Application.routes.url_helpers. namespace_project_build_url(gl_project.namespace, gl_project, self) @@ -248,6 +266,13 @@ module Ci pending? && !any_runners_online? end + def download_url + if artifacts_file.exists? + Gitlab::Application.routes.url_helpers. + download_namespace_project_build_path(gl_project.namespace, gl_project, self) + end + end + private def yaml_variables diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index e58420d82d4..33b57173928 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -1,18 +1,19 @@ # == Schema Information # -# Table name: commits +# Table name: ci_commits # -# id :integer not null, primary key -# project_id :integer -# ref :string(255) -# sha :string(255) -# before_sha :string(255) -# push_data :text -# created_at :datetime -# updated_at :datetime -# tag :boolean default(FALSE) -# yaml_errors :text -# committed_at :datetime +# id :integer not null, primary key +# project_id :integer +# ref :string(255) +# sha :string(255) +# before_sha :string(255) +# push_data :text +# created_at :datetime +# updated_at :datetime +# tag :boolean default(FALSE) +# yaml_errors :text +# committed_at :datetime +# gl_project_id :integer # module Ci diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb index cac3a7a49c1..8c39be42677 100644 --- a/app/models/ci/event.rb +++ b/app/models/ci/event.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: events +# Table name: ci_events # # id :integer not null, primary key # project_id :integer diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index 4e806ca1a68..669ee1cc0d2 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -1,9 +1,9 @@ # == Schema Information # -# Table name: projects +# Table name: ci_projects # # id :integer not null, primary key -# name :string(255) not null +# name :string(255) # timeout :integer default(3600), not null # created_at :datetime # updated_at :datetime @@ -66,30 +66,6 @@ module Ci class << self include Ci::CurrentSettings - def base_build_script - <<-eos - git submodule update --init - ls -la - eos - end - - def parse(project) - params = { - gitlab_id: project.id, - default_ref: project.default_branch || 'master', - email_add_pusher: current_application_settings.add_pusher, - email_only_broken_builds: current_application_settings.all_broken_builds, - } - - project = Ci::Project.new(params) - project.build_missing_services - project - end - - def already_added?(project) - where(gitlab_id: project.id).any? - end - def unassigned(runner) joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \ "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}"). diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index b719ad3c87e..89710485811 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: runners +# Table name: ci_runners # # id :integer not null, primary key # token :string(255) diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 44453ee4b41..3f4fc43873e 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: runner_projects +# Table name: ci_runner_projects # # id :integer not null, primary key # runner_id :integer not null diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb index ed5e3f940b6..8063c51e82b 100644 --- a/app/models/ci/service.rb +++ b/app/models/ci/service.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: services +# Table name: ci_services # # id :integer not null, primary key # type :string(255) diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index fe224b7dc70..b73c35d5ae5 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: triggers +# Table name: ci_triggers # # id :integer not null, primary key # token :string(255) diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index 29cd9553394..9973d2e5ade 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: trigger_requests +# Table name: ci_trigger_requests # # id :integer not null, primary key # trigger_id :integer not null diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 7a542802fa6..b3d2b809e03 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: variables +# Table name: ci_variables # # id :integer not null, primary key # project_id :integer not null diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb index 8f03b0625da..7ca16a1bde8 100644 --- a/app/models/ci/web_hook.rb +++ b/app/models/ci/web_hook.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: web_hooks +# Table name: ci_web_hooks # # id :integer not null, primary key # url :string(255) not null diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 7d54d83974a..e70f4d37184 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,3 +1,36 @@ +# == Schema Information +# +# Table name: ci_builds +# +# id :integer not null, primary key +# project_id :integer +# status :string(255) +# finished_at :datetime +# trace :text +# created_at :datetime +# updated_at :datetime +# started_at :datetime +# runner_id :integer +# coverage :float +# commit_id :integer +# commands :text +# job_id :integer +# name :string(255) +# deploy :boolean default(FALSE) +# options :text +# allow_failure :boolean default(FALSE), not null +# stage :string(255) +# trigger_request_id :integer +# stage_idx :integer +# tag :boolean +# ref :string(255) +# user_id :integer +# type :string(255) +# target_url :string(255) +# description :string(255) +# artifacts_file :text +# + class CommitStatus < ActiveRecord::Base self.table_name = 'ci_builds' @@ -92,4 +125,8 @@ class CommitStatus < ActiveRecord::Base def show_warning? false end + + def download_url + nil + end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5e964f04ef5..492a026add9 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -24,7 +24,7 @@ module Issuable scope :authored, ->(user) { where(author_id: user) } scope :assigned_to, ->(u) { where(assignee_id: u.id)} - scope :recent, -> { order("created_at DESC") } + scope :recent, -> { reorder(id: :desc) } scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 913c747a1c3..7391a77383c 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -8,8 +8,9 @@ module Sortable included do # By default all models should be ordered # by created_at field starting from newest - default_scope { order(id: :desc) } + default_scope { order_id_desc } + scope :order_id_desc, -> { reorder(id: :desc) } scope :order_created_desc, -> { reorder(created_at: :desc) } scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) } diff --git a/app/models/event.rb b/app/models/event.rb index 47600c57e35..9afd223bce5 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -45,7 +45,7 @@ class Event < ActiveRecord::Base after_create :reset_project_activity # Scopes - scope :recent, -> { order(created_at: :desc) } + scope :recent, -> { reorder(id: :desc) } scope :code_push, -> { where(action: PUSHED) } scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent } scope :with_associations, -> { includes(project: :namespace) } @@ -63,6 +63,16 @@ class Event < ActiveRecord::Base Event::PUSHED, ["MergeRequest", "Issue"], [Event::CREATED, Event::CLOSED, Event::MERGED]) end + + def latest_update_time + row = select(:updated_at, :project_id).reorder(id: :desc).take + + row ? row.updated_at : nil + end + + def limit_recent(limit = 20, offset = nil) + recent.limit(limit).offset(offset) + end end def proper? diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index fa54e3540d0..12c934e2494 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -1,3 +1,36 @@ +# == Schema Information +# +# Table name: ci_builds +# +# id :integer not null, primary key +# project_id :integer +# status :string(255) +# finished_at :datetime +# trace :text +# created_at :datetime +# updated_at :datetime +# started_at :datetime +# runner_id :integer +# coverage :float +# commit_id :integer +# commands :text +# job_id :integer +# name :string(255) +# deploy :boolean default(FALSE) +# options :text +# allow_failure :boolean default(FALSE), not null +# stage :string(255) +# trigger_request_id :integer +# stage_idx :integer +# tag :boolean +# ref :string(255) +# user_id :integer +# type :string(255) +# target_url :string(255) +# description :string(255) +# artifacts_file :text +# + class GenericCommitStatus < CommitStatus before_validation :set_default_values diff --git a/app/models/global_label.rb b/app/models/global_label.rb new file mode 100644 index 00000000000..0171f7d54b7 --- /dev/null +++ b/app/models/global_label.rb @@ -0,0 +1,17 @@ +class GlobalLabel + attr_accessor :title, :labels + alias_attribute :name, :title + + def self.build_collection(labels) + labels = labels.group_by(&:title) + + labels.map do |title, label| + new(title, label) + end + end + + def initialize(title, labels) + @title = title + @labels = labels + end +end diff --git a/app/models/group_milestone.rb b/app/models/global_milestone.rb index 91844da62e2..1321ccd963f 100644 --- a/app/models/group_milestone.rb +++ b/app/models/global_milestone.rb @@ -1,7 +1,15 @@ -class GroupMilestone +class GlobalMilestone attr_accessor :title, :milestones alias_attribute :name, :title + def self.build_collection(milestones) + milestones = milestones.group_by(&:title) + + milestones.map do |title, milestones| + new(title, milestones) + end + end + def initialize(title, milestones) @title = title @milestones = milestones @@ -10,7 +18,7 @@ class GroupMilestone def safe_title @title.parameterize end - + def projects milestones.map { |milestone| milestone.project } end @@ -60,15 +68,15 @@ class GroupMilestone end def issues - @group_issues ||= milestones.map(&:issues).flatten.group_by(&:state) + @issues ||= milestones.map(&:issues).flatten.group_by(&:state) end def merge_requests - @group_merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state) + @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state) end def participants - @group_participants ||= milestones.map(&:participants).flatten.compact.uniq + @participants ||= milestones.map(&:participants).flatten.compact.uniq end def opened_issues @@ -86,4 +94,8 @@ class GroupMilestone def closed_merge_requests merge_requests.values_at("closed", "merged", "locked").compact.flatten end + + def complete? + total_items_count == closed_items_count + end end diff --git a/app/models/group.rb b/app/models/group.rb index 34904af3b5b..1b5b875a19e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -11,6 +11,7 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) +# public :boolean default(FALSE) # require 'carrierwave/orm/activerecord' @@ -19,8 +20,9 @@ require 'file_size_validator' class Group < Namespace include Gitlab::ConfigHelper include Referable - + has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' + alias_method :members, :group_members has_many :users, through: :group_members validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } @@ -47,6 +49,14 @@ class Group < Namespace def reference_pattern User.reference_pattern end + + def public_and_given_groups(ids) + where('public IS TRUE OR namespaces.id IN (?)', ids) + end + + def visible_to_user(user) + where(id: user.authorized_groups.select(:id).reorder(nil)) + end end def to_reference(_from_project = nil) @@ -109,10 +119,6 @@ class Group < Namespace has_owner?(user) && owners.size == 1 end - def members - group_members - end - def avatar_type unless self.avatar.image? self.errors.add :avatar, "only images allowed" diff --git a/app/models/group_label.rb b/app/models/group_label.rb deleted file mode 100644 index 0fc39cb8771..00000000000 --- a/app/models/group_label.rb +++ /dev/null @@ -1,9 +0,0 @@ -class GroupLabel - attr_accessor :title, :labels - alias_attribute :name, :title - - def initialize(title, labels) - @title = title - @labels = labels - end -end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index ca7066b959a..337b3097126 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -2,18 +2,19 @@ # # Table name: web_hooks # -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) default("ProjectHook") +# service_id :integer +# push_events :boolean default(TRUE), not null +# issues_events :boolean default(FALSE), not null +# merge_requests_events :boolean default(FALSE), not null +# tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null +# enable_ssl_verification :boolean default(TRUE) # class ProjectHook < WebHook diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index b55e217975f..09bb3ee52a2 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -2,18 +2,19 @@ # # Table name: web_hooks # -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) default("ProjectHook") +# service_id :integer +# push_events :boolean default(TRUE), not null +# issues_events :boolean default(FALSE), not null +# merge_requests_events :boolean default(FALSE), not null +# tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null +# enable_ssl_verification :boolean default(TRUE) # class ServiceHook < WebHook diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index 6fb2d421026..2f63c59b07e 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -2,18 +2,19 @@ # # Table name: web_hooks # -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) default("ProjectHook") +# service_id :integer +# push_events :boolean default(TRUE), not null +# issues_events :boolean default(FALSE), not null +# merge_requests_events :boolean default(FALSE), not null +# tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null +# enable_ssl_verification :boolean default(TRUE) # class SystemHook < WebHook diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index a078accbdbd..d6c6f415c4a 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -2,18 +2,19 @@ # # Table name: web_hooks # -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) default("ProjectHook") +# service_id :integer +# push_events :boolean default(TRUE), not null +# issues_events :boolean default(FALSE), not null +# merge_requests_events :boolean default(FALSE), not null +# tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null +# enable_ssl_verification :boolean default(TRUE) # class WebHook < ActiveRecord::Base diff --git a/app/models/label.rb b/app/models/label.rb index 1bb4b5f55cf..b306aecbac1 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -8,6 +8,7 @@ # project_id :integer # created_at :datetime # updated_at :datetime +# template :boolean default(FALSE) # class Label < ActiveRecord::Base diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb new file mode 100644 index 00000000000..3c1426f59d0 --- /dev/null +++ b/app/models/lfs_object.rb @@ -0,0 +1,8 @@ +class LfsObject < ActiveRecord::Base + has_many :lfs_objects_projects, dependent: :destroy + has_many :projects, through: :lfs_objects_projects + + validates :oid, presence: true, uniqueness: true + + mount_uploader :file, LfsObjectUploader +end diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb new file mode 100644 index 00000000000..0fd5f089db9 --- /dev/null +++ b/app/models/lfs_objects_project.rb @@ -0,0 +1,8 @@ +class LfsObjectsProject < ActiveRecord::Base + belongs_to :project + belongs_to :lfs_object + + validates :lfs_object_id, presence: true + validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" } + validates :project_id, presence: true +end diff --git a/app/models/member.rb b/app/models/member.rb index cae8caa23fb..28aee2e3799 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -30,13 +30,22 @@ class Member < ActiveRecord::Base validates :user, presence: true, unless: :invite? validates :source, presence: true - validates :user_id, uniqueness: { scope: [:source_type, :source_id], + validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source", allow_nil: true } validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true - validates :invite_email, presence: { if: :invite? }, - email: { strict_mode: true, allow_nil: true }, - uniqueness: { scope: [:source_type, :source_id], allow_nil: true } + validates :invite_email, + presence: { + if: :invite? + }, + email: { + strict_mode: true, + allow_nil: true + }, + uniqueness: { + scope: [:source_type, :source_id], + allow_nil: true + } scope :invite, -> { where(user_id: nil) } scope :non_invite, -> { where("user_id IS NOT NULL") } @@ -73,7 +82,7 @@ class Member < ActiveRecord::Base def add_user(members, user_id, access_level, current_user = nil) user = user_for_id(user_id) - + # `user` can be either a User object or an email to be invited if user.is_a?(User) member = members.find_or_initialize_by(user_id: user.id) @@ -82,10 +91,21 @@ class Member < ActiveRecord::Base member.invite_email = user end - member.created_by ||= current_user - member.access_level = access_level + if can_update_member?(current_user, member) + member.created_by ||= current_user + member.access_level = access_level + + member.save + end + end + + private - member.save + def can_update_member?(current_user, member) + # There is no current user for bulk actions, in which case anything is allowed + !current_user || + current_user.can?(:update_group_member, member) || + current_user.can?(:update_project_member, member) end end @@ -95,7 +115,7 @@ class Member < ActiveRecord::Base def accept_invite!(new_user) return false unless invite? - + self.invite_token = nil self.invite_accepted_at = Time.now.utc diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 85f37e49e62..2eb03b8ba5b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -20,6 +20,7 @@ # position :integer default(0) # locked_at :datetime # updated_by_id :integer +# merge_error :string(255) # require Rails.root.join("app/models/commit") @@ -40,7 +41,7 @@ class MergeRequest < ActiveRecord::Base after_create :create_merge_request_diff after_update :update_merge_request_diff - delegate :commits, :diffs, to: :merge_request_diff, prefix: nil + delegate :commits, :diffs, :diffs_no_whitespace, to: :merge_request_diff, prefix: nil # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 6575d0bc81f..c499a4b5b4c 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -19,7 +19,7 @@ class MergeRequestDiff < ActiveRecord::Base # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 500 - attr_reader :commits, :diffs + attr_reader :commits, :diffs, :diffs_no_whitespace belongs_to :merge_request @@ -47,6 +47,20 @@ class MergeRequestDiff < ActiveRecord::Base @diffs ||= (load_diffs(st_diffs) || []) end + def diffs_no_whitespace + # Get latest sha of branch from source project + source_sha = merge_request.source_project.commit(source_branch).sha + + compare_result = Gitlab::CompareResult.new( + Gitlab::Git::Compare.new( + merge_request.target_project.repository.raw_repository, + merge_request.target_branch, + source_sha, + ), { ignore_whitespace_change: true } + ) + @diffs_no_whitespace ||= load_diffs(dump_commits(compare_result.diffs)) + end + def commits @commits ||= load_commits(st_commits || []) end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 5782e649f8b..20b92e68d61 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -11,6 +11,7 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) +# public :boolean default(FALSE) # class Namespace < ActiveRecord::Base diff --git a/app/models/project.rb b/app/models/project.rb index bdb22e49bb5..f0a4b6aae7b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -42,7 +42,7 @@ class Project < ActiveRecord::Base include Sortable include AfterCommitQueue include CaseSensitivity - + extend Gitlab::ConfigHelper extend Enumerize @@ -52,6 +52,7 @@ class Project < ActiveRecord::Base default_value_for :visibility_level, gitlab_config_features.visibility_level default_value_for :issues_enabled, gitlab_config_features.issues default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests + default_value_for :builds_enabled, gitlab_config_features.builds default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets @@ -123,6 +124,8 @@ class Project < ActiveRecord::Base has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build' has_many :releases, dependent: :destroy + has_many :lfs_objects_projects, dependent: :destroy + has_many :lfs_objects, through: :lfs_objects_projects has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id @@ -283,6 +286,10 @@ class Project < ActiveRecord::Base joins(join_body).reorder('join_note_counts.amount DESC') end + + def visible_to_user(user) + where(id: user.authorized_projects.select(:id).reorder(nil)) + end end def team @@ -307,15 +314,17 @@ class Project < ActiveRecord::Base def add_import_job if forked? - unless RepositoryForkWorker.perform_async(id, forked_from_project.path_with_namespace, self.namespace.path) - import_fail - end + RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) else - RepositoryImportWorker.perform_async(id) + RepositoryImportWorker.perform_async(self.id) end end def clear_import_data + update(import_error: nil) + + ProjectCacheWorker.perform_async(self.id) + self.import_data.destroy if self.import_data end @@ -343,6 +352,14 @@ class Project < ActiveRecord::Base import_status == 'finished' end + def safe_import_url + result = URI.parse(self.import_url) + result.password = '*****' unless result.password.nil? + result.to_s + rescue + original_url + end + def check_limit unless creator.can_create_project? or namespace.kind == 'group' errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it") @@ -457,10 +474,6 @@ class Project < ActiveRecord::Base list.find { |service| service.to_param == name } end - def gitlab_ci? - gitlab_ci_service && gitlab_ci_service.active && gitlab_ci_project.present? - end - def ci_services services.select { |service| service.category == :ci } end @@ -782,9 +795,33 @@ class Project < ActiveRecord::Base ) end - def enable_ci + # TODO: this should be migrated to Project table, + # the same as issues_enabled + def builds_enabled + gitlab_ci_service && gitlab_ci_service.active + end + + def builds_enabled? + builds_enabled + end + + def builds_enabled=(value) service = gitlab_ci_service || create_gitlab_ci_service - service.active = true + service.active = value service.save end + + def enable_ci + self.builds_enabled = true + end + + def unlink_fork + if forked? + forked_from_project.lfs_objects.find_each do |lfs_object| + lfs_object.projects << self + end + + forked_project_link.destroy + end + end end diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb index f17993d9f3b..0df03890efb 100644 --- a/app/models/project_services/ci/hip_chat_service.rb +++ b/app/models/project_services/ci/hip_chat_service.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: services +# Table name: ci_services # # id :integer not null, primary key # type :string(255) diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb index fd193301001..d31dd6899c1 100644 --- a/app/models/project_services/ci/mail_service.rb +++ b/app/models/project_services/ci/mail_service.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: services +# Table name: ci_services # # id :integer not null, primary key # type :string(255) diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb index ee8e4988826..7064bfe78db 100644 --- a/app/models/project_services/ci/slack_service.rb +++ b/app/models/project_services/ci/slack_service.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: services +# Table name: ci_services # # id :integer not null, primary key # type :string(255) diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index c73c4b058a1..c240213200d 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -32,7 +32,6 @@ class DroneCiService < CiService def compose_service_hook hook = service_hook || build_service_hook - hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join hook.enable_ssl_verification = enable_ssl_verification hook.save end diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb index 074478b292d..b15d9a14677 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/slack_service/note_message.rb @@ -45,30 +45,27 @@ class SlackService def create_commit_note(commit) commit_sha = commit[:id] commit_sha = Commit.truncate_sha(commit_sha) - commit_link = "[commit #{commit_sha}](#{@note_url})" - title = format_title(commit[:message]) - @message = "#{@user_name} commented on #{commit_link} in #{project_link}: *#{title}*" + commented_on_message( + "[commit #{commit_sha}](#{@note_url})", + format_title(commit[:message])) end def create_issue_note(issue) - issue_iid = issue[:iid] - note_link = "[issue ##{issue_iid}](#{@note_url})" - title = format_title(issue[:title]) - @message = "#{@user_name} commented on #{note_link} in #{project_link}: *#{title}*" + commented_on_message( + "[issue ##{issue[:iid]}](#{@note_url})", + format_title(issue[:title])) end def create_merge_note(merge_request) - merge_request_id = merge_request[:iid] - merge_request_link = "[merge request ##{merge_request_id}](#{@note_url})" - title = format_title(merge_request[:title]) - @message = "#{@user_name} commented on #{merge_request_link} in #{project_link}: *#{title}*" + commented_on_message( + "[merge request ##{merge_request[:iid]}](#{@note_url})", + format_title(merge_request[:title])) end def create_snippet_note(snippet) - snippet_id = snippet[:id] - snippet_link = "[snippet ##{snippet_id}](#{@note_url})" - title = format_title(snippet[:title]) - @message = "#{@user_name} commented on #{snippet_link} in #{project_link}: *#{title}*" + commented_on_message( + "[snippet ##{snippet[:id]}](#{@note_url})", + format_title(snippet[:title])) end def description_message @@ -78,5 +75,9 @@ class SlackService def project_link "[#{@project_name}](#{@project_url})" end + + def commented_on_message(target_link, title) + @message = "#{@user_name} commented on #{target_link} in #{project_link}: *#{title}*" + end end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 231973fa543..b5fec38378b 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -86,6 +86,8 @@ class ProjectWiki commit = commit_details(:created, message, title) wiki.write_page(title, format, content, commit) + + update_project_activity rescue Gollum::DuplicatePageError => e @error_message = "Duplicate page: #{e.message}" return false @@ -95,10 +97,14 @@ class ProjectWiki commit = commit_details(:updated, message, page.title) wiki.update_page(page, page.name, format, content, commit) + + update_project_activity end def delete_page(page, message = nil) wiki.delete_page(page, commit_details(:deleted, message, page.title)) + + update_project_activity end def page_title_and_dir(title) @@ -146,4 +152,8 @@ class ProjectWiki def path_to_repo @path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git") end + + def update_project_activity + @project.touch(:last_activity_at) + end end diff --git a/app/models/release.rb b/app/models/release.rb index e196b84eb18..89f70278af5 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -1,3 +1,15 @@ +# == Schema Information +# +# Table name: releases +# +# id :integer not null, primary key +# tag :string(255) +# description :text +# project_id :integer +# created_at :datetime +# updated_at :datetime +# + class Release < ActiveRecord::Base belongs_to :project diff --git a/app/models/repository.rb b/app/models/repository.rb index f8c4cb1387b..c1836103463 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -6,7 +6,7 @@ class Repository include Gitlab::ShellAdapter - attr_accessor :raw_repository, :path_with_namespace, :project + attr_accessor :path_with_namespace, :project def self.clean_old_archives repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path @@ -19,14 +19,18 @@ class Repository def initialize(path_with_namespace, default_branch = nil, project = nil) @path_with_namespace = path_with_namespace @project = project + end - if path_with_namespace - @raw_repository = Gitlab::Git::Repository.new(path_to_repo) - @raw_repository.autocrlf = :input - end + def raw_repository + return nil unless path_with_namespace - rescue Gitlab::Git::Repository::NoRepository - nil + @raw_repository ||= begin + repo = Gitlab::Git::Repository.new(path_to_repo) + repo.autocrlf = :input + repo + rescue Gitlab::Git::Repository::NoRepository + nil + end end # Return absolute path to repository @@ -105,29 +109,25 @@ class Repository end def add_branch(branch_name, ref) - cache.expire(:branch_names) - @branches = nil + expire_branches_cache gitlab_shell.add_branch(path_with_namespace, branch_name, ref) end def add_tag(tag_name, ref, message = nil) - cache.expire(:tag_names) - @tags = nil + expire_tags_cache gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) end def rm_branch(branch_name) - cache.expire(:branch_names) - @branches = nil + expire_branches_cache gitlab_shell.rm_branch(path_with_namespace, branch_name) end def rm_tag(tag_name) - cache.expire(:tag_names) - @tags = nil + expire_tags_cache gitlab_shell.rm_tag(path_with_namespace, tag_name) end @@ -169,6 +169,16 @@ class Repository end end + def expire_tags_cache + cache.expire(:tag_names) + @tags = nil + end + + def expire_branches_cache + cache.expire(:branch_names) + @branches = nil + end + def expire_cache cache_keys.each do |key| cache.expire(key) @@ -346,8 +356,8 @@ class Repository end end - def branch_names_contains(sha) - args = %W(#{Gitlab.config.git.bin_path} branch --contains #{sha}) + 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 if names.respond_to?(:split) @@ -363,21 +373,12 @@ class Repository end end - def tag_names_contains(sha) - args = %W(#{Gitlab.config.git.bin_path} tag --contains #{sha}) - names = Gitlab::Popen.popen(args, path_to_repo).first - - if names.respond_to?(:split) - names = names.split("\n").map(&:strip) - - names.each do |name| - name.slice! '* ' - end + def branch_names_contains(sha) + refs_contains_sha('branch', sha) + end - names - else - [] - end + def tag_names_contains(sha) + refs_contains_sha('tag', sha) end def branches @@ -493,7 +494,7 @@ class Repository root_ref_commit = commit(root_ref) if branch_commit - rugged.merge_base(root_ref_commit.id, branch_commit.id) == branch_commit.id + is_ancestor?(branch_commit.id, root_ref_commit.id) else nil end @@ -503,6 +504,11 @@ class Repository rugged.merge_base(first_commit_id, second_commit_id) end + def is_ancestor?(ancestor_id, descendant_id) + merge_base(ancestor_id, descendant_id) == ancestor_id + end + + def search_files(query, ref) offset = 2 args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) diff --git a/app/models/user.rb b/app/models/user.rb index 67fef1c1e6a..9374f01f99f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -54,6 +54,7 @@ # public_email :string(255) default(""), not null # dashboard :integer default(0) # project_view :integer default(0) +# consumed_timestep :integer # layout :integer default(0) # @@ -388,33 +389,23 @@ class User < ActiveRecord::Base end end - # Groups user has access to + # Returns the groups a user has access to def authorized_groups - @authorized_groups ||= begin - group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id)) - Group.where(id: group_ids) - end - end + union = Gitlab::SQL::Union. + new([groups.select(:id), authorized_projects.select(:namespace_id)]) - def authorized_projects_id - @authorized_projects_id ||= begin - project_ids = personal_projects.pluck(:id) - project_ids.push(*groups_projects.pluck(:id)) - project_ids.push(*projects.pluck(:id).uniq) - end + Group.where("namespaces.id IN (#{union.to_sql})") end - # Projects user has access to + # Returns the groups a user is authorized to access. def authorized_projects - @authorized_projects ||= Project.where(id: authorized_projects_id) + Project.where("projects.id IN (#{projects_union.to_sql})") end def owned_projects @owned_projects ||= - begin - namespace_ids = owned_groups.pluck(:id).push(namespace.id) - Project.in_namespace(namespace_ids).joins(:namespace) - end + Project.where('namespace_id IN (?) OR namespace_id = ?', + owned_groups.select(:id), namespace.id).joins(:namespace) end # Team membership in authorized projects @@ -729,12 +720,25 @@ class User < ActiveRecord::Base Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) end - def contributed_projects_ids - Event.contributions.where(author_id: self). + # Returns the projects a user contributed to in the last year. + # + # This method relies on a subquery as this performs significantly better + # compared to a JOIN when coupled with, for example, + # `Project.visible_to_user`. That is, consider the following code: + # + # some_user.contributed_projects.visible_to_user(other_user) + # + # If this method were to use a JOIN the resulting query would take roughly 200 + # ms on a database with a similar size to GitLab.com's database. On the other + # hand, using a subquery means we can get the exact same data in about 40 ms. + def contributed_projects + events = Event.select(:project_id). + contributions.where(author_id: self). where("created_at > ?", Time.now - 1.year). - reorder(project_id: :desc). - select(:project_id). - uniq.map(&:project_id) + uniq. + reorder(nil) + + Project.where(id: events) end def restricted_signup_domains @@ -764,15 +768,30 @@ class User < ActiveRecord::Base !solo_owned_groups.present? end - def ci_authorized_projects - @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects_id) - end - def ci_authorized_runners @ci_authorized_runners ||= begin runner_ids = Ci::RunnerProject.joins(:project). - where(ci_projects: { gitlab_id: authorized_projects_id }).select(:runner_id) + where("ci_projects.gitlab_id IN (#{ci_projects_union.to_sql})"). + select(:runner_id) + Ci::Runner.specific.where(id: runner_ids) end end + + private + + def projects_union + Gitlab::SQL::Union.new([personal_projects.select(:id), + groups_projects.select(:id), + projects.select(:id)]) + end + + def ci_projects_union + scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] } + groups = groups_projects.where(members: scope) + other = projects.where(members: scope) + + Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id), + other.select(:id)]) + end end diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index bfe6a3dc4be..ec581658fc1 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,7 +3,7 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::CompareResult 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, diff_options = {}) source_commit = source_project.commit(source_branch) return unless source_commit @@ -25,7 +25,7 @@ class CompareService target_project.repository.raw_repository, target_branch, source_sha, - ) + ), diff_options ) end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 3de7bb9dcaa..ccb6b97858c 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -60,7 +60,7 @@ class GitPushService # If CI was disabled but .gitlab-ci.yml file was pushed # we enable CI automatically - if !project.gitlab_ci? && gitlab_ci_yaml?(newrev) + if !project.builds_enabled? && gitlab_ci_yaml?(newrev) project.enable_ci end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 15b3825f96a..11d2b08bba7 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -28,6 +28,9 @@ class IssuableBaseService < BaseService end def filter_params(issuable_ability_name = :issue) + params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE + params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE + ability = :"admin_#{issuable_ability_name}" unless can?(current_user, ability, project) @@ -36,4 +39,36 @@ class IssuableBaseService < BaseService params.delete(:assignee_id) end end + + def update(issuable) + change_state(issuable) + filter_params + old_labels = issuable.labels.to_a + + if params.present? && issuable.update_attributes(params.merge(updated_by: current_user)) + issuable.reset_events_cache + + if issuable.labels != old_labels + create_labels_note( + issuable, + issuable.labels - old_labels, + old_labels - issuable.labels) + end + + handle_changes(issuable) + issuable.create_new_cross_references!(current_user) + execute_hooks(issuable, 'update') + end + + issuable + end + + def change_state(issuable) + case params.delete(:state_event) + when 'reopen' + reopen_service.new(project, current_user, {}).execute(issuable) + when 'close' + close_service.new(project, current_user, {}).execute(issuable) + end + end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 551325e4cab..7c112f731a7 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -1,45 +1,30 @@ module Issues class UpdateService < Issues::BaseService def execute(issue) - case params.delete(:state_event) - when 'reopen' - Issues::ReopenService.new(project, current_user, {}).execute(issue) - when 'close' - Issues::CloseService.new(project, current_user, {}).execute(issue) - end - - params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE - params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE - - filter_params - old_labels = issue.labels.to_a - - if params.present? && issue.update_attributes(params.merge(updated_by: current_user)) - issue.reset_events_cache - - if issue.labels != old_labels - create_labels_note( - issue, issue.labels - old_labels, old_labels - issue.labels) - end - - if issue.previous_changes.include?('milestone_id') - create_milestone_note(issue) - end + update(issue) + end - if issue.previous_changes.include?('assignee_id') - create_assignee_note(issue) - notification_service.reassigned_issue(issue, current_user) - end + def handle_changes(issue) + if issue.previous_changes.include?('milestone_id') + create_milestone_note(issue) + end - if issue.previous_changes.include?('title') - create_title_change_note(issue, issue.previous_changes['title'].first) - end + if issue.previous_changes.include?('assignee_id') + create_assignee_note(issue) + notification_service.reassigned_issue(issue, current_user) + end - issue.create_new_cross_references!(current_user) - execute_hooks(issue, 'update') + if issue.previous_changes.include?('title') + create_title_change_note(issue, issue.previous_changes['title'].first) end + end + + def reopen_service + Issues::ReopenService + end - issue + def close_service + Issues::CloseService end end end diff --git a/app/services/labels/group_service.rb b/app/services/labels/group_service.rb deleted file mode 100644 index b26cee24d56..00000000000 --- a/app/services/labels/group_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Labels - class GroupService < ::BaseService - def initialize(project_labels) - @project_labels = project_labels.group_by(&:title) - end - - def execute - build(@project_labels) - end - - def label(title) - if title - group_label = @project_labels[title].group_by(&:title) - build(group_label).first - else - nil - end - end - - private - - def build(label) - label.map { |title, labels| GroupLabel.new(title, labels) } - end - end -end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 61f7d2bbe89..a5db3776eb6 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,59 +11,41 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) - case params.delete(:state_event) - when 'reopen' - MergeRequests::ReopenService.new(project, current_user, {}).execute(merge_request) - when 'close' - MergeRequests::CloseService.new(project, current_user, {}).execute(merge_request) - end - - params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE - params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE - - filter_params - old_labels = merge_request.labels.to_a - - if params.present? && merge_request.update_attributes(params.merge(updated_by: current_user)) - merge_request.reset_events_cache - - if merge_request.labels != old_labels - create_labels_note( - merge_request, - merge_request.labels - old_labels, - old_labels - merge_request.labels - ) - end - - if merge_request.previous_changes.include?('target_branch') - create_branch_change_note(merge_request, 'target', - merge_request.previous_changes['target_branch'].first, - merge_request.target_branch) - end + update(merge_request) + end - if merge_request.previous_changes.include?('milestone_id') - create_milestone_note(merge_request) - end + def handle_changes(merge_request) + if merge_request.previous_changes.include?('target_branch') + create_branch_change_note(merge_request, 'target', + merge_request.previous_changes['target_branch'].first, + merge_request.target_branch) + end - if merge_request.previous_changes.include?('assignee_id') - create_assignee_note(merge_request) - notification_service.reassigned_merge_request(merge_request, current_user) - end + if merge_request.previous_changes.include?('milestone_id') + create_milestone_note(merge_request) + end - if merge_request.previous_changes.include?('title') - create_title_change_note(merge_request, merge_request.previous_changes['title'].first) - end + if merge_request.previous_changes.include?('assignee_id') + create_assignee_note(merge_request) + notification_service.reassigned_merge_request(merge_request, current_user) + end - if merge_request.previous_changes.include?('target_branch') || - merge_request.previous_changes.include?('source_branch') - merge_request.mark_as_unchecked - end + if merge_request.previous_changes.include?('title') + create_title_change_note(merge_request, merge_request.previous_changes['title'].first) + end - merge_request.create_new_cross_references!(current_user) - execute_hooks(merge_request, 'update') + if merge_request.previous_changes.include?('target_branch') || + merge_request.previous_changes.include?('source_branch') + merge_request.mark_as_unchecked end + end + + def reopen_service + MergeRequests::ReopenService + end - merge_request + def close_service + MergeRequests::CloseService end end end diff --git a/app/services/milestones/group_service.rb b/app/services/milestones/group_service.rb deleted file mode 100644 index 11d702f1e7b..00000000000 --- a/app/services/milestones/group_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Milestones - class GroupService < Milestones::BaseService - def initialize(project_milestones) - @project_milestones = project_milestones.group_by(&:title) - end - - def execute - build(@project_milestones) - end - - def milestone(title) - if title - group_milestone = @project_milestones[title].group_by(&:title) - build(group_milestone).first - else - nil - end - end - - private - - def build(milestone) - milestone.map{ |title, milestones| GroupMilestone.new(title, milestones) } - end - end -end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index a6b22348650..bbfe755f44a 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -113,7 +113,7 @@ class NotificationService end # Add all users participating in the thread (author, assignee, comment authors) - participants = + participants = if target.respond_to?(:participants) target.participants(note.author) else @@ -276,35 +276,25 @@ class NotificationService # Remove users with disabled notifications from array # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) - users = users.to_a.compact.uniq - users = users.reject(&:blocked?) - - users.reject do |user| - next user.notification.disabled? unless project - - member = project.project_members.find_by(user_id: user.id) - - if !member && project.group - member = project.group.group_members.find_by(user_id: user.id) - end - - # reject users who globally disabled notification and has no membership - next user.notification.disabled? unless member - - # reject users who disabled notification in project - next true if member.notification.disabled? - - # reject users who have N_GLOBAL in project and disabled in global settings - member.notification.global? && user.notification.disabled? - end + reject_users(users, :disabled?, project) end # Remove users with notification level 'Mentioned' def reject_mention_users(users, project = nil) + reject_users(users, :mention?, project) + end + + # Reject users which method_name from notification object returns true. + # + # Example: + # reject_users(users, :watch?, project) + # + def reject_users(users, method_name, project = nil) users = users.to_a.compact.uniq + users = users.reject(&:blocked?) users.reject do |user| - next user.notification.mention? unless project + next user.notification.send(method_name) unless project member = project.project_members.find_by(user_id: user.id) @@ -313,19 +303,19 @@ class NotificationService end # reject users who globally set mention notification and has no membership - next user.notification.mention? unless member + next user.notification.send(method_name) unless member # reject users who set mention notification in project - next true if member.notification.mention? + next true if member.notification.send(method_name) # reject users who have N_MENTION in project and disabled in global settings - member.notification.global? && user.notification.mention? + member.notification.global? && user.notification.send(method_name) end end def reject_unsubscribed_users(recipients, target) return recipients unless target.respond_to? :subscriptions - + recipients.reject do |user| subscription = target.subscriptions.find_by_user_id(user.id) subscription && !subscription.subscribed @@ -343,7 +333,7 @@ class NotificationService recipients end end - + def new_resource_email(target, project, method) recipients = build_recipients(target, project, target.author) @@ -361,11 +351,13 @@ class NotificationService end def reassign_resource_email(target, project, current_user, method) - assignee_id_was = previous_record(target, "assignee_id") - recipients = build_recipients(target, project, current_user) + previous_assignee_id = previous_record(target, "assignee_id") + previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id + + recipients = build_recipients(target, project, current_user, [previous_assignee]) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, assignee_id_was, current_user.id) + mailer.send(method, recipient.id, target.id, previous_assignee_id, current_user.id) end end @@ -377,9 +369,11 @@ class NotificationService end end - def build_recipients(target, project, current_user) + def build_recipients(target, project, current_user, extra_recipients = nil) recipients = target.participants(current_user) + recipients = recipients.concat(extra_recipients).compact.uniq if extra_recipients + recipients = add_project_watchers(recipients, project) recipients = reject_mention_users(recipients, project) recipients = reject_muted_users(recipients, project) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 5b84527eccf..700a1db04d8 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -55,7 +55,9 @@ module Projects @project.save if @project.persisted? && !@project.import? - raise 'Failed to create repository' unless @project.create_repository + unless @project.create_repository + raise 'Failed to create repository' + end end end @@ -94,9 +96,7 @@ module Projects @project.team << [current_user, :master, current_user] end - if @project.import? - @project.import_start - end + @project.import_start if @project.import? end end end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 46374a3909a..5da1c7afd92 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -17,7 +17,7 @@ module Projects new_project = CreateService.new(current_user, new_params).execute if new_project.persisted? - if @project.gitlab_ci? + if @project.builds_enabled? new_project.enable_ci settings = @project.gitlab_ci_project.attributes.select do |attr_name, value| diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 9a5fe4af9dd..8b5143e1eb7 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -33,17 +33,7 @@ class SystemHooksService ) end when Project - owner = model.owner - - data.merge!({ - name: model.name, - path: model.path, - path_with_namespace: model.path_with_namespace, - project_id: model.id, - owner_name: owner.name, - owner_email: owner.respond_to?(:email) ? owner.email : "", - project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase - }) + data.merge!(project_data(model)) when User data.merge!({ name: model.name, @@ -51,16 +41,7 @@ class SystemHooksService user_id: model.id }) when ProjectMember - data.merge!({ - project_name: model.project.name, - project_path: model.project.path, - project_path_with_namespace: model.project.path_with_namespace, - project_id: model.project.id, - user_name: model.user.name, - user_email: model.user.email, - access_level: model.human_access, - project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase - }) + data.merge!(project_member_data(model)) when Group owner = model.owner @@ -72,15 +53,7 @@ class SystemHooksService owner_email: owner.respond_to?(:email) ? owner.email : nil, ) when GroupMember - data.merge!( - group_name: model.group.name, - group_path: model.group.path, - group_id: model.group.id, - user_name: model.user.name, - user_email: model.user.email, - user_id: model.user.id, - group_access: model.human_access, - ) + data.merge!(group_member_data(model)) end end @@ -96,4 +69,43 @@ class SystemHooksService "#{model.class.name.downcase}_#{event.to_s}" end end + + def project_data(model) + owner = model.owner + + { + name: model.name, + path: model.path, + path_with_namespace: model.path_with_namespace, + project_id: model.id, + owner_name: owner.name, + owner_email: owner.respond_to?(:email) ? owner.email : "", + project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase + } + end + + def project_member_data(model) + { + project_name: model.project.name, + project_path: model.project.path, + project_path_with_namespace: model.project.path_with_namespace, + project_id: model.project.id, + user_name: model.user.name, + user_email: model.user.email, + access_level: model.human_access, + project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase + } + end + + def group_member_data(model) + { + group_name: model.group.name, + group_path: model.group.path, + group_id: model.group.id, + user_name: model.user.name, + user_email: model.user.email, + user_id: model.user.id, + group_access: model.human_access, + } + end end diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb new file mode 100644 index 00000000000..b4e0fc5772d --- /dev/null +++ b/app/uploaders/artifact_uploader.rb @@ -0,0 +1,50 @@ +# encoding: utf-8 +class ArtifactUploader < CarrierWave::Uploader::Base + storage :file + + attr_accessor :build, :field + + def self.artifacts_path + File.expand_path('shared/artifacts/', Rails.root) + end + + def self.artifacts_upload_path + File.expand_path('shared/artifacts/tmp/uploads/', Rails.root) + end + + def self.artifacts_cache_path + File.expand_path('shared/artifacts/tmp/cache/', Rails.root) + end + + def initialize(build, field) + @build, @field = build, field + end + + def artifacts_path + File.join(build.created_at.utc.strftime('%Y_%m'), build.project.id.to_s, build.id.to_s) + end + + def store_dir + File.join(ArtifactUploader.artifacts_path, artifacts_path) + end + + def cache_dir + File.join(ArtifactUploader.artifacts_cache_path, artifacts_path) + end + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end + + def exists? + file.try(:exists?) + end + + def move_to_cache + true + end + + def move_to_store + true + end +end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index a9691bee46e..a65a896e41e 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -1,26 +1,11 @@ # encoding: utf-8 class AttachmentUploader < CarrierWave::Uploader::Base + include UploaderHelper + storage :file def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end - - def image? - img_ext = %w(png jpg jpeg gif bmp tiff) - if file.respond_to?(:extension) - img_ext.include?(file.extension.downcase) - else - # Not all CarrierWave storages respond to :extension - ext = file.path.split('.').last.downcase - img_ext.include?(ext) - end - rescue - false - end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 7cad044555b..6135c3ad96f 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -1,6 +1,8 @@ # encoding: utf-8 class AvatarUploader < CarrierWave::Uploader::Base + include UploaderHelper + storage :file after :store, :reset_events_cache @@ -9,23 +11,6 @@ class AvatarUploader < CarrierWave::Uploader::Base "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end - def image? - img_ext = %w(png jpg jpeg gif bmp tiff) - if file.respond_to?(:extension) - img_ext.include?(file.extension.downcase) - else - # Not all CarrierWave storages respond to :extension - ext = file.path.split('.').last.downcase - img_ext.include?(ext) - end - rescue - false - end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end - def reset_events_cache(file) model.reset_events_cache if model.is_a?(User) end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index e8211585834..ac920119a85 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,5 +1,7 @@ # encoding: utf-8 class FileUploader < CarrierWave::Uploader::Base + include UploaderHelper + storage :file attr_accessor :project, :secret @@ -28,21 +30,4 @@ class FileUploader < CarrierWave::Uploader::Base def secure_url File.join("/uploads", @secret, file.filename) end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end - - def image? - img_ext = %w(png jpg jpeg gif bmp tiff) - if file.respond_to?(:extension) - img_ext.include?(file.extension.downcase) - else - # Not all CarrierWave storages respond to :extension - ext = file.path.split('.').last.downcase - img_ext.include?(ext) - end - rescue - false - end end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb new file mode 100644 index 00000000000..28085b31083 --- /dev/null +++ b/app/uploaders/lfs_object_uploader.rb @@ -0,0 +1,29 @@ +# encoding: utf-8 + +class LfsObjectUploader < CarrierWave::Uploader::Base + storage :file + + def store_dir + "#{Gitlab.config.lfs.storage_path}/#{model.oid[0,2]}/#{model.oid[2,2]}" + end + + def cache_dir + "#{Gitlab.config.lfs.storage_path}/tmp/cache" + end + + def move_to_cache + true + end + + def move_to_store + true + end + + def exists? + file.try(:exists?) + end + + def filename + model.oid[4..-1] + end +end diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb new file mode 100644 index 00000000000..5ef440f3367 --- /dev/null +++ b/app/uploaders/uploader_helper.rb @@ -0,0 +1,19 @@ +# Extra methods for uploader +module UploaderHelper + def image? + img_ext = %w(png jpg jpeg gif bmp tiff) + if file.respond_to?(:extension) + img_ext.include?(file.extension.downcase) + else + # Not all CarrierWave storages respond to :extension + ext = file.path.split('.').last.downcase + img_ext.include?(ext) + end + rescue + false + end + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 7253218c2e9..ddaf0e0e8ff 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -139,5 +139,10 @@ = f.check_box :shared_runners_enabled Enable shared runners for a new projects + .form-group + = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :max_artifacts_size, class: 'form-control' + .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index 4245d0f1eda..8d1cab4137c 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -7,7 +7,7 @@ .pull-right - unless @user == current_user - = link_to 'Log in as this user', login_as_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info" + = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info" = link_to edit_admin_user_path(@user), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml index 90d9980c85c..7d11edc79e2 100644 --- a/app/views/admin/users/_profile.html.haml +++ b/app/views/admin/users/_profile.html.haml @@ -16,11 +16,11 @@ - unless user.linkedin.blank? %li %span.light LinkedIn: - %strong= link_to user.linkedin, "http://www.linkedin.com/in/#{user.linkedin}" + %strong= link_to user.linkedin, "https://www.linkedin.com/in/#{user.linkedin}" - unless user.twitter.blank? %li %span.light Twitter: - %strong= link_to user.twitter, "http://www.twitter.com/#{user.twitter}" + %strong= link_to user.twitter, "https://twitter.com/#{user.twitter}" - unless user.website_url.blank? %li %span.light Website: diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml index cefb75040e9..b0aaea89075 100644 --- a/app/views/ci/notify/build_fail_email.html.haml +++ b/app/views/ci/notify/build_fail_email.html.haml @@ -13,6 +13,10 @@ %p Branch: #{@build.ref} %p + Stage: #{@build.stage} +%p + Job: #{@build.name} +%p Message: #{@build.commit.git_commit_message} %p diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/ci/notify/build_fail_email.text.erb index 6de5dc10f17..17a3b9b1d33 100644 --- a/app/views/ci/notify/build_fail_email.text.erb +++ b/app/views/ci/notify/build_fail_email.text.erb @@ -4,6 +4,8 @@ Status: <%= @build.status %> Commit: <%= @build.commit.short_sha %> Author: <%= @build.commit.git_author_name %> Branch: <%= @build.ref %> +Stage: <%= @build.stage %> +Job: <%= @build.name %> Message: <%= @build.commit.git_commit_message %> Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %> diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/ci/notify/build_success_email.html.haml index 617b88f7345..24c439e50eb 100644 --- a/app/views/ci/notify/build_success_email.html.haml +++ b/app/views/ci/notify/build_success_email.html.haml @@ -14,6 +14,10 @@ %p Branch: #{@build.ref} %p + Stage: #{@build.stage} +%p + Job: #{@build.name} +%p Message: #{@build.commit.git_commit_message} %p diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/ci/notify/build_success_email.text.erb index d0a43ae1c12..bc8b978c3d7 100644 --- a/app/views/ci/notify/build_success_email.text.erb +++ b/app/views/ci/notify/build_success_email.text.erb @@ -4,6 +4,8 @@ Status: <%= @build.status %> Commit: <%= @build.commit.short_sha %> Author: <%= @build.commit.git_author_name %> Branch: <%= @build.ref %> +Stage: <%= @build.stage %> +Job: <%= @build.name %> Message: <%= @build.commit.git_commit_message %> Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %> diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 21b25c3986e..635251e2374 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -10,10 +10,10 @@ .milestones %ul.content-list - - if @dashboard_milestones.blank? + - if @milestones.blank? %li .nothing-here-block No milestones to show - else - - @dashboard_milestones.each do |milestone| + - @milestones.each do |milestone| = render 'milestone', milestone: milestone - = paginate @dashboard_milestones, theme: "gitlab" + = paginate @milestones, theme: "gitlab" diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 2fe14c6388c..83077a398bd 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -1,14 +1,14 @@ -- page_title @dashboard_milestone.title, "Milestones" +- page_title @milestone.title, "Milestones" %h4.page-title - .issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" } - - if @dashboard_milestone.closed? + .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" } + - if @milestone.closed? Closed - else Open - Milestone #{@dashboard_milestone.title} + Milestone #{@milestone.title} %hr -- if (@dashboard_milestone.total_items_count == @dashboard_milestone.closed_items_count) && @dashboard_milestone.active? +- if @milestone.complete? && @milestone.active? .alert.alert-success %span All issues for this milestone are closed. You may close the milestone now. @@ -22,7 +22,7 @@ %th Open issues %th State %th Due date - - @dashboard_milestone.milestones.each do |milestone| + - @milestone.milestones.each do |milestone| %tr %td = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) @@ -39,46 +39,46 @@ .context %p.lead Progress: - #{@dashboard_milestone.closed_items_count} closed + #{@milestone.closed_items_count} closed – - #{@dashboard_milestone.open_items_count} open - = milestone_progress_bar(@dashboard_milestone) + #{@milestone.open_items_count} open + = milestone_progress_bar(@milestone) %ul.nav.nav-tabs %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues - %span.badge= @dashboard_milestone.issue_count + %span.badge= @milestone.issue_count %li = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do Merge Requests - %span.badge= @dashboard_milestone.merge_requests_count + %span.badge= @milestone.merge_requests_count %li = link_to '#tab-participants', 'data-toggle' => 'tab' do Participants - %span.badge= @dashboard_milestone.participants.count + %span.badge= @milestone.participants.count .pull-right - = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @dashboard_milestone.title), class: "btn edit-milestone-link btn-grouped" + = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped" .tab-content .tab-pane.active#tab-issues .row .col-md-6 - = render 'issues', title: "Open", issues: @dashboard_milestone.opened_issues + = render 'issues', title: "Open", issues: @milestone.opened_issues .col-md-6 - = render 'issues', title: "Closed", issues: @dashboard_milestone.closed_issues + = render 'issues', title: "Closed", issues: @milestone.closed_issues .tab-pane#tab-merge-requests .row .col-md-6 - = render 'merge_requests', title: "Open", merge_requests: @dashboard_milestone.opened_merge_requests + = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests .col-md-6 - = render 'merge_requests', title: "Closed", merge_requests: @dashboard_milestone.closed_merge_requests + = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests .tab-pane#tab-participants %ul.bordered-list - - @dashboard_milestone.participants.each do |user| + - @milestone.participants.each do |user| %li = link_to user, title: user.name, class: "darken" do = image_tag avatar_icon(user, 32), class: "avatar s32" diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index d2c51486841..c8c219f4cca 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.id dashboard_projects_url - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 9a1331b2549..41ad2c231d4 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -7,34 +7,26 @@ %h3 Sign in .login-body - if form_based_providers.any? - - if form_based_providers.count >= 2 || signin_enabled? - %ul.nav.nav-tabs - - if crowd_enabled? - %li.active - = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' - - @ldap_servers.each_with_index do |server, i| - %li{class: (:active if i.zero? && !crowd_enabled?)} - = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' - - if signin_enabled? - %li - = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' - .tab-content - - if crowd_enabled? - %div.tab-pane.active{id: "tab-crowd"} - = render 'devise/sessions/new_crowd' - - @ldap_servers.each_with_index do |server, i| - %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)} - = render 'devise/sessions/new_ldap', server: server - - if signin_enabled? - %div#tab-signin.tab-pane - = render 'devise/sessions/new_base' - - else + %ul.nav.nav-tabs - if crowd_enabled? - = render 'devise/sessions/new_crowd' - - elsif @ldap_servers.any? - = render 'devise/sessions/new_ldap', server: @ldap_servers.first - - elsif signin_enabled? - = render 'devise/sessions/new_base' + %li.active + = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' + - @ldap_servers.each_with_index do |server, i| + %li{class: (:active if i.zero? && !crowd_enabled?)} + = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' + - if signin_enabled? + %li + = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' + .tab-content + - if crowd_enabled? + %div.tab-pane.active{id: "tab-crowd"} + = render 'devise/sessions/new_crowd' + - @ldap_servers.each_with_index do |server, i| + %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)} + = render 'devise/sessions/new_ldap', server: server + - if signin_enabled? + %div#tab-signin.tab-pane + = render 'devise/sessions/new_base' - elsif signin_enabled? = render 'devise/sessions/new_base' diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 3c19381321a..be94b1abc11 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -1,6 +1,5 @@ - user = member.user - return unless user || member.invite? -- show_roles = true if show_roles.nil? %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %span{class: ("list-item-name" if show_controls)} @@ -25,11 +24,11 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if show_controls && can?(current_user, :admin_group_member, member) + - if show_controls && can?(current_user, :admin_group_member, @group) = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do Resend invite - - if show_roles + - if should_user_see_group_roles?(current_user, @group) %span.pull-right %strong= member.human_access - if show_controls @@ -37,6 +36,7 @@ = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o + - if can?(current_user, :destroy_group_member, member) - if current_user == user diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 15d289471c9..d4ad33a8bf1 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,8 +1,6 @@ - page_title "Members" - header_title group_title(@group, "Members", group_group_members_path(@group)) -- show_roles = should_user_see_group_roles?(current_user, @group) - -- if show_roles +- if should_user_see_group_roles?(current_user, @group) %p.light Members of group have access to all group projects. Read more about permissions @@ -32,7 +30,7 @@ (#{@members.total_count}) %ul.well-list - @members.each do |member| - = render 'groups/group_members/group_member', member: member, show_roles: show_roles, show_controls: true + = render 'groups/group_members/group_member', member: member, show_controls: true = paginate @members, theme: 'gitlab' diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml index 41dffdd2fb8..a20bf75bc39 100644 --- a/app/views/groups/milestones/_milestone.html.haml +++ b/app/views/groups/milestones/_milestone.html.haml @@ -22,7 +22,7 @@ %span.label.label-gray = milestone.project.name .col-sm-6 - - if can?(current_user, :admin_group, @group) + - if can?(current_user, :admin_milestones, @group) - if milestone.closed? = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen" - else diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 2bbcad5fdfb..84ec77c6188 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -3,15 +3,22 @@ = render 'shared/milestones_filter' .gray-content-block - Only milestones from - %strong #{@group.name} - group are listed here. + - if can?(current_user, :admin_milestones, @group) + .pull-right + %span.pull-right.hidden-xs + = link_to new_group_milestone_path(@group), class: "btn btn-new" do + New Milestone + + .oneline + Only milestones from + %strong #{@group.name} + group are listed here. .milestones %ul.content-list - - if @group_milestones.blank? + - if @milestones.blank? %li .nothing-here-block No milestones to show - else - - @group_milestones.each do |milestone| + - @milestones.each do |milestone| = render 'milestone', milestone: milestone - = paginate @group_milestones, theme: "gitlab" + = paginate @milestones, theme: "gitlab" diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml new file mode 100644 index 00000000000..800bac4ef02 --- /dev/null +++ b/app/views/groups/milestones/new.html.haml @@ -0,0 +1,48 @@ +- page_title "Milestones" +- header_title group_title(@group, "Milestones", group_milestones_path(@group)) + +%h3.page-title + New Milestone + +%p.light + This will create milestone in every selected project +%hr + += form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-requires-input' } do |f| + .row + .col-md-6 + .form-group + = f.label :title, "Title", class: "control-label" + .col-sm-10 + = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true + %p.hint Required + .form-group.milestone-description + = f.label :description, "Description", class: "control-label" + .col-sm-10 + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do + = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit' + .clearfix + .error-alert + .form-group + = f.label :projects, "Projects", class: "control-label" + .col-sm-10 + = f.collection_select :project_ids, @group.projects, :id, :name, + { selected: @group.projects.map(&:id) }, multiple: true, class: 'select2' + + .col-md-6 + .form-group + = f.label :due_date, "Due Date", class: "control-label" + .col-sm-10= f.hidden_field :due_date + .col-sm-10 + .datepicker + + .form-actions + = f.submit 'Create Milestone', class: "btn-create btn" + = link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel" + + +:javascript + $(".datepicker").datepicker({ + dateFormat: "yy-mm-dd", + onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } + }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index a92ad5d751b..d161259e4aa 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,22 +1,22 @@ -- page_title @group_milestone.title, "Milestones" +- page_title @milestone.title, "Milestones" = render "header_title" %h4.page-title - .issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" } - - if @group_milestone.closed? + .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" } + - if @milestone.closed? Closed - else Open - Milestone #{@group_milestone.title} + Milestone #{@milestone.title} .pull-right - - if can?(current_user, :admin_group, @group) - - if @group_milestone.active? - = link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" + - if can?(current_user, :admin_milestones, @group) + - if @milestone.active? + = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" - else - = link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" + = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" %hr -- if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active? +- if @milestone.complete? && @milestone.active? .alert.alert-success %span All issues for this milestone are closed. You may close the milestone now. @@ -30,7 +30,7 @@ %th Open issues %th State %th Due date - - @group_milestone.milestones.each do |milestone| + - @milestone.milestones.each do |milestone| %tr %td = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) @@ -47,46 +47,46 @@ .context %p.lead Progress: - #{@group_milestone.closed_items_count} closed + #{@milestone.closed_items_count} closed – - #{@group_milestone.open_items_count} open - = milestone_progress_bar(@group_milestone) + #{@milestone.open_items_count} open + = milestone_progress_bar(@milestone) %ul.nav.nav-tabs %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues - %span.badge= @group_milestone.issue_count + %span.badge= @milestone.issue_count %li = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do Merge Requests - %span.badge= @group_milestone.merge_requests_count + %span.badge= @milestone.merge_requests_count %li = link_to '#tab-participants', 'data-toggle' => 'tab' do Participants - %span.badge= @group_milestone.participants.count + %span.badge= @milestone.participants.count .pull-right - = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @group_milestone.title), class: "btn edit-milestone-link btn-grouped" + = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped" .tab-content .tab-pane.active#tab-issues .row .col-md-6 - = render 'issues', title: "Open", issues: @group_milestone.opened_issues + = render 'issues', title: "Open", issues: @milestone.opened_issues .col-md-6 - = render 'issues', title: "Closed", issues: @group_milestone.closed_issues + = render 'issues', title: "Closed", issues: @milestone.closed_issues .tab-pane#tab-merge-requests .row .col-md-6 - = render 'merge_requests', title: "Open", merge_requests: @group_milestone.opened_merge_requests + = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests .col-md-6 - = render 'merge_requests', title: "Closed", merge_requests: @group_milestone.closed_merge_requests + = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests .tab-pane#tab-participants %ul.bordered-list - - @group_milestone.participants.each do |user| + - @milestone.participants.each do |user| %li = link_to user, title: user.name, class: "darken" do = image_tag avatar_icon(user, 32), class: "avatar s32" diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index a91d1a6e94b..7ea574434c3 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.id group_url(@group) - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index c08a7b80744..3ca30d3baab 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -13,6 +13,10 @@ %li.visible-sm.visible-xs = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do = icon('search') + - if session[:impersonator_id] + %li.impersonation + = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop impersonation', data: { toggle: 'tooltip', placement: 'bottom' } do + = icon('user-secret fw') - if current_user.is_admin? %li = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index a59939ccd31..377a99e719a 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -34,7 +34,7 @@ %span Protected Branches - - if @project.gitlab_ci? + - if @project.builds_enabled? = nav_link(controller: :runners) do = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners', data: {placement: 'right'} do = icon('cog fw') diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index f58b9bd6ba6..3ca4c340406 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -42,7 +42,8 @@ - else #{link_to "View it on GitLab", @target_url}. %br + -# Don't link the host is the line below, one link in the email is easier to quickly click than two. You're receiving this email because of your account on #{Gitlab.config.gitlab.host}. If you'd like to receive fewer emails, you can adjust your notification settings. - = email_action @target_url + = email_action @target_url
\ No newline at end of file diff --git a/app/views/notify/project_was_moved_email.text.erb b/app/views/notify/project_was_moved_email.text.erb index d8a23dabf49..b2c5f71e465 100644 --- a/app/views/notify/project_was_moved_email.text.erb +++ b/app/views/notify/project_was_moved_email.text.erb @@ -1,4 +1,4 @@ -Project #{@old_path_with_namespace} was moved to another location +Project <%= @old_path_with_namespace %> was moved to another location The project is now located under <%= namespace_project_url(@project.namespace, @project) %> diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 8c0980369fd..88d54bf6f21 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -18,17 +18,12 @@ .project-repo-buttons .split-one = render 'projects/buttons/star' + = render 'projects/buttons/fork' - - unless empty_repo - = render 'projects/buttons/fork' - = render "shared/clone_panel" - .split-repo-buttons - - unless empty_repo - - if can? current_user, :download_code, @project - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - = icon('download fw') - + + .split-repo-buttons + = render "projects/buttons/download" = render 'projects/buttons/dropdown' + = render 'projects/buttons/notifications' - diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 3374d5432a5..907e1ce10bd 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -87,6 +87,9 @@ Test coverage %h1 #{@build.coverage}% + - if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url + .build-widget.center + = link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary' .build-widget %h4.title @@ -168,7 +171,7 @@ %td = ci_icon_for_status(build.status) %td - = link_to namespace_project_build_path(@project.namespace, @project, @build) do + = link_to namespace_project_build_path(@project.namespace, @project, build) do - if build.name = build.name - else diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml new file mode 100644 index 00000000000..14ee2263b7d --- /dev/null +++ b/app/views/projects/buttons/_download.html.haml @@ -0,0 +1,4 @@ +- unless @project.empty_repo? + - if can? current_user, :download_code, @project + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do + = icon('download') diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index bed2b16249e..b277b765b6b 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -5,7 +5,7 @@ %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - if can?(current_user, :create_issue, @project) %li - = link_to url_for_new_issue do + = link_to url_for_new_issue(@project, only_path: true) do = icon('exclamation-circle fw') New issue - if can?(current_user, :create_merge_request, @project) @@ -32,5 +32,3 @@ = link_to new_namespace_project_tag_path(@project.namespace, @project) do = icon('tags fw') New tag - - diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 8f2f631eb7d..2d3abf09051 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -1,12 +1,13 @@ -- if current_user && can?(current_user, :fork_project, @project) - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn' do - = icon('code-fork fw') - Fork - %span.count - = @project.forks_count - - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do - = icon('code-fork fw') - %span.count - = @project.forks_count +- unless @project.empty_repo? + - if current_user && can?(current_user, :fork_project, @project) + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has_tooltip' do + = icon('code-fork fw') + Fork + %span.count + = @project.forks_count + - else + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do + = icon('code-fork fw') + %span.count + = @project.forks_count diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 06583902035..41a3ec6d90f 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,5 +1,5 @@ - if current_user - = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do + = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has_tooltip', method: :post, remote: true, title: "Star project" do = icon('star fw') %span.count = @project.star_count diff --git a/app/views/projects/ci_settings/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml index 665556f5c20..acc912d4596 100644 --- a/app/views/projects/ci_settings/edit.html.haml +++ b/app/views/projects/ci_settings/edit.html.haml @@ -1,25 +1,6 @@ - page_title "CI Settings" -- if @ci_project.generated_yaml_config - %p.alert.alert-danger - CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_ci_project_path(@ci_project)} - or - %a.preview-yml{:href => "#yaml-content", "data-toggle" => "modal"} preview - yaml file which is based on your old jobs. - Put this file to the root of your project and name it .gitlab-ci.yml - if no_runners_for_project?(@ci_project) = render 'no_runners' = render 'form' - -- if @ci_project.generated_yaml_config - #yaml-content.modal.fade{"aria-hidden" => "true", "aria-labelledby" => ".gitlab-ci.yml", :role => "dialog", :tabindex => "-1"} - .modal-dialog - .modal-content - .modal-header - %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} × - %h4.modal-title Content of .gitlab-ci.yml - .modal-body - = text_area_tag :yaml, @ci_project.generated_yaml_config, size: "70x25", class: "form-control" - .modal-footer - %button.btn.btn-default{"data-dismiss" => "modal", :type => "button"} Close diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 30a3973828f..85e203cbe57 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -3,4 +3,4 @@ = render "commit_box" = render "ci_menu" if @ci_commit = render "projects/diffs/diffs", diffs: @diffs, project: @project -= render "projects/notes/notes_with_form", view: params[:view] += render "projects/notes/notes_with_form" diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index c255559b88c..9a0e7bff3f1 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -61,6 +61,9 @@ %td .pull-right + - if current_user && can?(current_user, :download_build_artifacts, @project) && commit_status.download_url + = link_to commit_status.download_url, title: 'Download artifacts' do + %i.fa.fa-download - if current_user && can?(current_user, :manage_builds, commit_status.gl_project) - if commit_status.active? - if commit_status.cancel_url diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 3854ad5d611..268b9b815ee 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -12,7 +12,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id) xml.title truncate(commit.title, length: 80) xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(commit.author_email) + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email)) xml.author do |author| xml.name commit.author_name xml.email commit.author_email diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index e46bf1ab1e7..416fb4da071 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,4 +1,4 @@ -- if params[:view] == 'parallel' +- if diff_view == 'parallel' - fluid_layout true - diff_files = safe_diff_files(diffs) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 410ff6abb43..c745b4e69bf 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -33,7 +33,7 @@ -# Skipp all non non-supported blobs - return unless blob.respond_to?('text?') - if blob.text? - - if params[:view] == 'parallel' + - if diff_view == 'parallel' = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - else = render "projects/diffs/text_file", diff_file: diff_file, index: i @@ -42,4 +42,3 @@ = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i - else .nothing-here-block No preview for this file type - diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index afbf88b5507..3ebc175648e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -57,7 +57,16 @@ = f.check_box :merge_requests_enabled %strong Merge Requests %br - %span.descr Submit changes to be merged upstream. + %span.descr Submit changes to be merged upstream + + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :builds_enabled do + = f.check_box :builds_enabled + %strong Builds + %br + %span.descr Test and deploy your changes before merge .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index e0d06a14bf4..03d0733f913 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -3,7 +3,7 @@ = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do = link_to 'Commits', commits_namespace_project_graph_path - - if @project.gitlab_ci? + - if @project.builds_enabled? = nav_link(action: :ci) do = link_to ci_namespace_project_graph_path do Continuous Integration diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 92a87690c54..6027fb23360 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -1,22 +1,19 @@ - page_title "Import repository" %h3.page-title - - if @project.import_failed? - Import failed. Retry? - - else - Import repository + Import repository %hr +- if @project.import_failed? + .panel.panel-danger + .panel-heading The repository could not be imported. + .panel-body + %pre + :preserve + #{@project.import_error.try(:strip)} + = form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f| - .form-group.import-url-data - = f.label :import_url, class: 'control-label' do - %span Import existing git repo - .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .well.prepend-top-20 - This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. - %br - The import will time out after 4 minutes. For big repositories, use a clone/push combination. - For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"} + = render "shared/import_form", f: f + .form-actions = f.submit 'Start import', class: "btn btn-create", tabindex: 4 diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index 06886d215a3..c0d1ce0d120 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -8,7 +8,7 @@ - else Import in progress. - unless @project.forked? - %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)} + %p.monospace git clone --bare #{@project.safe_import_url} %p Please wait while we import the repository for you. Refresh at will. :javascript new ProjectImport(); diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 626970f39be..d9cfc3d7ae9 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,5 +1,5 @@ - if @merge_request_diff.collected? - = render "projects/diffs/diffs", diffs: @merge_request.diffs, project: @merge_request.project + = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, project: @merge_request.project - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 255ddab479f..24879b19d2b 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -23,9 +23,7 @@ .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit' - .hint - .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. - .pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + = render 'projects/notes/hints' .clearfix .error-alert .col-md-6 @@ -45,7 +43,7 @@ :javascript - $( ".datepicker" ).datepicker({ + $(".datepicker").datepicker({ dateFormat: "yy-mm-dd", onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index a02c12f06a8..c9d1fc3da21 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -23,7 +23,6 @@ = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2} - if import_sources_enabled? - .project-import.js-toggle-container .form-group %label.control-label Import project from @@ -82,19 +81,7 @@ %span Any repo by URL .js-toggle-content.hide - .form-group.import-url-data - = f.label :import_url, class: 'control-label' do - %span Git repository URL - .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' - .well.prepend-top-20 - %ul - %li - The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. - %li - The import will time out after 4 minutes. For big repositories, use a clone/push combination. - %li - To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. + = render "shared/import_form", f: f .prepend-botton-10 diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 13dfa0a1bb3..5dd84317e3b 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,5 +1,5 @@ = 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 common-note-form gfm-form" }, authenticity_token: true do |f| - = hidden_field_tag :view, params[:view] + = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) = f.hidden_field :commit_id diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 5d184730796..efa7dd01cc2 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -2,7 +2,7 @@ .timeline-entry-inner .timeline-icon %a{href: user_path(note.author)} - %img.avatar.s40{src: avatar_icon(note.author), alt: ''} + = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header - if note_editable?(note) @@ -59,8 +59,7 @@ .note-text = preserve do = markdown(note.note, {no_header_anchors: true}) - - unless note.system? - -# System notes can't be edited + - if note_editable?(note) = render 'projects/notes/edit_form', note: note - if note.attachment.url diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 04222b8f7c4..99c1b0fa43e 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -4,7 +4,7 @@ .js-main-target-form - if can? current_user, :create_note, @project - = render "projects/notes/form", view: params[:view] + = render "projects/notes/form", view: diff_view :javascript - new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{params[:view]}") + new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 76c46d1d806..f07cd97e63d 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -24,18 +24,19 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if current_user_can_admin_project + - if can?(current_user, :admin_project_member, @project) = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do Resend invite - - if current_user_can_admin_project - - unless @project.personal? && user == current_user - .pull-right - %strong= member.human_access + - if can?(current_user, :admin_project_member, @project) + .pull-right + %strong= member.human_access + - if can?(current_user, :update_project_member, member) = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o + - if can?(current_user, :destroy_project_member, member) - if current_user == user = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 615c425e59a..b807fb2cc9d 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,5 +1,3 @@ -- can_admin_project = can?(current_user, :admin_project, @project) - .panel.panel-default.prepend-top-20 .panel-heading %strong #{@project.name} @@ -8,4 +6,4 @@ (#{members.count}) %ul.well-list - members.each do |project_member| - = render 'project_member', member: project_member, current_user_can_admin_project: can_admin_project + = render 'project_member', member: project_member diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 811b1858821..2fb3a41d541 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,3 +1,2 @@ -- can_admin_project = can?(current_user, :admin_project, @project) :plain - $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member, current_user_can_admin_project: can_admin_project))}'); + $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member))}'); diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index 242684e5c7c..15c49767556 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_url(@project.namespace, @project) - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 2e4aab36301..8bcb24ae9df 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -21,7 +21,6 @@ = gitlab_config.protocol.upcase = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true - if project.kind_of?(Project) - .input-group-addon - .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" } + .input-group-addon.has_tooltip{title: "#{visibility_level_label(project.visibility_level)} project", data: { container: "body" } } + .visibility-level-label = visibility_level_icon(project.visibility_level) - diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml new file mode 100644 index 00000000000..285af56ad73 --- /dev/null +++ b/app/views/shared/_import_form.html.haml @@ -0,0 +1,16 @@ +.form-group.import-url-data + = f.label :import_url, class: 'control-label' do + %span Git repository URL + .col-sm-10 + = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' + + .well.prepend-top-20 + %ul + %li + The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>. + %li + If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. + %li + The import will time out after 4 minutes. For big repositories, use a clone/push combination. + %li + To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder index 50232dc7186..2fe5b7fac83 100644 --- a/app/views/users/show.atom.builder +++ b/app/views/users/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" xml.link href: user_url(@user), rel: "alternate", type: "text/html" xml.id user_url(@user) - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 30992412184..d5a92cb816a 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -32,11 +32,11 @@ = icon('skype') - unless @user.linkedin.blank? .profile-link-holder - = link_to "http://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do + = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do = icon('linkedin-square') - unless @user.twitter.blank? .profile-link-holder - = link_to "http://www.twitter.com/#{@user.twitter}", title: "Twitter" do + = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do = icon('twitter-square') - unless @user.website_url.blank? .profile-link-holder diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index acd1c43f06b..2f991c52339 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -13,22 +13,20 @@ class RepositoryForkWorker end result = gitlab_shell.fork_repository(source_path, target_path) - unless result logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") + project.update(import_error: "The project could not be forked.") project.import_fail - project.save return end - if project.valid_repo? - ProjectCacheWorker.perform_async(project.id) - project.import_finish - else - project.import_fail + unless project.valid_repo? logger.error("Project #{id} had an invalid repository after fork") + project.update(import_error: "The forked repository is invalid.") + project.import_fail + return end - project.save + project.import_finish end end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index ea2808045eb..1de49161997 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -7,37 +7,52 @@ class RepositoryImportWorker def perform(project_id) project = Project.find(project_id) - unless project.import_url == Project::UNKNOWN_IMPORT_URL - import_result = gitlab_shell.send(:import_repository, - project.path_with_namespace, - project.import_url) - return project.import_fail unless import_result - else + if project.import_url == Project::UNKNOWN_IMPORT_URL + # In this case, we only want to import issues, not a repository. unless project.create_repository - return project.import_fail + project.update(import_error: "The repository could not be created.") + project.import_fail + return + end + else + begin + gitlab_shell.import_repository(project.path_with_namespace, project.import_url) + rescue Gitlab::Shell::Error => e + project.update(import_error: e.message) + project.import_fail + return end end - data_import_result = case project.import_type - when 'github' - Gitlab::GithubImport::Importer.new(project).execute - when 'gitlab' - Gitlab::GitlabImport::Importer.new(project).execute - when 'bitbucket' - Gitlab::BitbucketImport::Importer.new(project).execute - when 'google_code' - Gitlab::GoogleCodeImport::Importer.new(project).execute - when 'fogbugz' - Gitlab::FogbugzImport::Importer.new(project).execute - else - true - end - return project.import_fail unless data_import_result - - Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' + data_import_result = + case project.import_type + when 'github' + Gitlab::GithubImport::Importer.new(project).execute + when 'gitlab' + Gitlab::GitlabImport::Importer.new(project).execute + when 'bitbucket' + Gitlab::BitbucketImport::Importer.new(project).execute + when 'google_code' + Gitlab::GoogleCodeImport::Importer.new(project).execute + when 'fogbugz' + Gitlab::FogbugzImport::Importer.new(project).execute + else + true + end + + unless data_import_result + project.update(import_error: "The remote issue data could not be imported.") + project.import_fail + return + end + + if project.import_type == 'bitbucket' + Gitlab::BitbucketImport::KeyDeleter.new(project).execute + end project.import_finish - project.save - ProjectCacheWorker.perform_async(project.id) + + # Explicitly update mirror so that upstream remote is created and fetched + project.update_mirror end end diff --git a/config/environments/test.rb b/config/environments/test.rb index 2d5e7addcd3..955540837d3 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -30,4 +30,6 @@ Gitlab::Application.configure do config.active_support.deprecation = :stderr config.eager_load = false + + config.cache_store = :null_store end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 20894ebcdc9..8fdb2603ce8 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -84,6 +84,7 @@ production: &base merge_requests: true wiki: true snippets: false + builds: true ## Webhook settings # Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10) @@ -123,6 +124,12 @@ production: &base # The mailbox where incoming mail will end up. Usually "inbox". mailbox: "inbox" + ## Git LFS + lfs: + enabled: false + # The location where LFS objects are stored (default: shared/lfs-objects). + # storage_path: shared/lfs-objects + ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: @@ -316,8 +323,6 @@ production: &base # path: /mnt/gitlab # Default: shared - - # # 4. Advanced settings # ========================== @@ -418,6 +423,8 @@ test: <<: *base gravatar: enabled: true + lfs: + enabled: false gitlab: host: localhost port: 80 diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8192d727f2a..6b7990c0ab0 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -171,6 +171,7 @@ Settings.gitlab.default_projects_features['issues'] = true if Settings.g Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? +Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] @@ -186,6 +187,7 @@ Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_br Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root) +Settings.gitlab_ci['max_artifacts_size'] ||= 100 # in megabytes # # Reply by email @@ -198,6 +200,13 @@ Settings.incoming_email['start_tls'] = false if Settings.incoming_email['start_ Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil? # +# Git LFS +# +Settings['lfs'] ||= Settingslogic.new({}) +Settings.lfs['enabled'] = false if Settings.lfs['enabled'].nil? +Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root) + +# # Gravatar # Settings['gravatar'] ||= Settingslogic.new({}) diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 04ed9e90df5..d7c5432da76 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -9,12 +9,14 @@ begin rescue end -Gitlab::Application.config.session_store( - :redis_store, # Using the cookie_store would enable session replay attacks. - servers: Gitlab::Application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store - key: '_gitlab_session', - secure: Gitlab.config.gitlab.https, - httponly: true, - expire_after: Settings.gitlab['session_expire_delay'] * 60, - path: (Gitlab::Application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root -) +unless Rails.env.test? + Gitlab::Application.config.session_store( + :redis_store, # Using the cookie_store would enable session replay attacks. + servers: Gitlab::Application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store + key: '_gitlab_session', + secure: Gitlab.config.gitlab.https, + httponly: true, + expire_after: Settings.gitlab['session_expire_delay'] * 60, + path: (Gitlab::Application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root + ) +end diff --git a/config/routes.rb b/config/routes.rb index 2028ea938e4..0bc2c173453 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,7 +32,6 @@ Gitlab::Application.routes.draw do get :status, to: 'projects#badge' get :integration post :toggle_shared_runners - get :dumped_yaml end resources :runner_projects, only: [:create, :destroy] @@ -94,7 +93,7 @@ Gitlab::Application.routes.draw do end # Enable Grack support - mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post] + mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] # Help get 'help' => 'help#index' @@ -223,6 +222,8 @@ Gitlab::Application.routes.draw do resources :keys, only: [:show, :destroy] resources :identities, only: [:index, :edit, :update, :destroy] + delete 'stop_impersonation' => 'impersonation#destroy', on: :collection + member do get :projects get :keys @@ -232,7 +233,7 @@ Gitlab::Application.routes.draw do put :unblock put :unlock put :confirm - post :login_as + post 'impersonate' => 'impersonation#create' patch :disable_two_factor delete 'remove/:email_id', action: 'remove_email', as: 'remove_email' end @@ -367,7 +368,7 @@ Gitlab::Application.routes.draw do end resource :avatar, only: [:destroy] - resources :milestones, only: [:index, :show, :update] + resources :milestones, only: [:index, :show, :update, :new, :create] end end @@ -611,6 +612,7 @@ Gitlab::Application.routes.draw do member do get :status post :cancel + get :download post :retry end end diff --git a/db/migrate/20151013092124_add_artifacts_file_to_builds.rb b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb new file mode 100644 index 00000000000..5a299f7b26d --- /dev/null +++ b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb @@ -0,0 +1,5 @@ +class AddArtifactsFileToBuilds < ActiveRecord::Migration + def change + add_column :ci_builds, :artifacts_file, :text + end +end diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb new file mode 100644 index 00000000000..2d04c170a88 --- /dev/null +++ b/db/migrate/20151103134857_create_lfs_objects.rb @@ -0,0 +1,10 @@ +class CreateLfsObjects < ActiveRecord::Migration + def change + create_table :lfs_objects do |t| + t.string :oid, null: false, unique: true + t.integer :size, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb new file mode 100644 index 00000000000..f3f58b931ec --- /dev/null +++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb @@ -0,0 +1,12 @@ +class CreateLfsObjectsProjects < ActiveRecord::Migration + def change + create_table :lfs_objects_projects do |t| + t.integer :lfs_object_id, null: false + t.integer :project_id, null: false + + t.timestamps + end + + add_index :lfs_objects_projects, :project_id + end +end diff --git a/db/migrate/20151104105513_add_file_to_lfs_objects.rb b/db/migrate/20151104105513_add_file_to_lfs_objects.rb new file mode 100644 index 00000000000..7c57f3f0df6 --- /dev/null +++ b/db/migrate/20151104105513_add_file_to_lfs_objects.rb @@ -0,0 +1,5 @@ +class AddFileToLfsObjects < ActiveRecord::Migration + def change + add_column :lfs_objects, :file, :string + end +end diff --git a/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb new file mode 100644 index 00000000000..01d8c0f043e --- /dev/null +++ b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddMaxArtifactsSizeToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :max_artifacts_size, :integer, default: 100, null: false + end +end diff --git a/db/migrate/20151110125604_add_import_error_to_project.rb b/db/migrate/20151110125604_add_import_error_to_project.rb new file mode 100644 index 00000000000..7fc990f8d0a --- /dev/null +++ b/db/migrate/20151110125604_add_import_error_to_project.rb @@ -0,0 +1,5 @@ +class AddImportErrorToProject < ActiveRecord::Migration + def change + add_column :projects, :import_error, :text + end +end diff --git a/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb new file mode 100644 index 00000000000..d10f1f6e605 --- /dev/null +++ b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb @@ -0,0 +1,6 @@ +class AddIndexForLfsOidAndSize < ActiveRecord::Migration + def change + add_index :lfs_objects, :oid + add_index :lfs_objects, [:oid, :size] + end +end diff --git a/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb new file mode 100644 index 00000000000..41b93da0a86 --- /dev/null +++ b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb @@ -0,0 +1,7 @@ +class AddUniqueForLfsOidIndex < ActiveRecord::Migration + def change + remove_index :lfs_objects, :oid + remove_index :lfs_objects, [:oid, :size] + add_index :lfs_objects, :oid, unique: true + end +end diff --git a/db/migrate/20151118162244_add_projects_public_index.rb b/db/migrate/20151118162244_add_projects_public_index.rb new file mode 100644 index 00000000000..fded70e3c0c --- /dev/null +++ b/db/migrate/20151118162244_add_projects_public_index.rb @@ -0,0 +1,5 @@ +class AddProjectsPublicIndex < ActiveRecord::Migration + def change + add_index :namespaces, :public + end +end diff --git a/db/schema.rb b/db/schema.rb index 116c0c8d97d..462d5ed3b29 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151105094515) do +ActiveRecord::Schema.define(version: 20151118162244) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -48,6 +48,7 @@ ActiveRecord::Schema.define(version: 20151105094515) do t.text "help_page_text" t.string "admin_notification_email" t.boolean "shared_runners_enabled", default: true, null: false + t.integer "max_artifacts_size", default: 100, null: false end create_table "audit_events", force: true do |t| @@ -108,6 +109,7 @@ ActiveRecord::Schema.define(version: 20151105094515) do t.string "type" t.string "target_url" t.string "description" + t.text "artifacts_file" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree @@ -420,6 +422,25 @@ ActiveRecord::Schema.define(version: 20151105094515) do add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree + create_table "lfs_objects", force: true do |t| + t.string "oid", null: false + t.integer "size", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.string "file" + end + + add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree + + create_table "lfs_objects_projects", force: true do |t| + t.integer "lfs_object_id", null: false + t.integer "project_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree + create_table "members", force: true do |t| t.integer "access_level", null: false t.integer "source_id", null: false @@ -517,6 +538,7 @@ ActiveRecord::Schema.define(version: 20151105094515) do add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree + add_index "namespaces", ["public"], name: "index_namespaces_on_public", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree create_table "notes", force: true do |t| @@ -619,7 +641,10 @@ ActiveRecord::Schema.define(version: 20151105094515) do t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 + t.integer "commit_count", default: 0 + t.boolean "merge_requests_ff_only_enabled", default: false + t.text "issues_template" + t.text "import_error" end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree diff --git a/doc/api/commits.md b/doc/api/commits.md index 9f72adc6ed9..93d62b751e6 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -22,7 +22,8 @@ Parameters: "author_name": "Dmitriy Zaporozhets", "author_email": "dzaporozhets@sphereconsultinginc.com", "created_at": "2012-09-20T11:50:22+03:00", - "message": "Replace sanitize with escape once" + "message": "Replace sanitize with escape once", + "allow_failure": false }, { "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6", @@ -31,7 +32,8 @@ Parameters: "author_name": "randx", "author_email": "dmitriy.zaporozhets@gmail.com", "created_at": "2012-09-20T09:06:12+03:00", - "message": "Sanitize for network graph" + "message": "Sanitize for network graph", + "allow_failure": false } ] ``` @@ -186,7 +188,7 @@ Parameters: "target_url": "http://jenkins/project/url", "description": "Jenkins success", "created_at": "2015-10-12T09:47:16.250Z", - "started_at": "2015-10-12T09:47:16.250Z"", + "started_at": "2015-10-12T09:47:16.250Z", "finished_at": "2015-10-12T09:47:16.262Z", "author": { "id": 1, @@ -226,7 +228,7 @@ POST /projects/:id/statuses/:sha "target_url": "http://jenkins/project/url", "description": "Jenkins success", "created_at": "2015-10-12T09:47:16.250Z", - "started_at": "2015-10-12T09:47:16.250Z"", + "started_at": "2015-10-12T09:47:16.250Z", "finished_at": "2015-10-12T09:47:16.262Z", "author": { "id": 1, diff --git a/doc/api/projects.md b/doc/api/projects.md index 96485857035..755cc6525c2 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -60,6 +60,7 @@ Parameters: "path_with_namespace": "diaspora/diaspora-client", "issues_enabled": true, "merge_requests_enabled": true, + "builds_enabled": true, "wiki_enabled": true, "snippets_enabled": false, "created_at": "2013-09-30T13: 46: 02Z", @@ -101,6 +102,7 @@ Parameters: "path_with_namespace": "brightbox/puppet", "issues_enabled": true, "merge_requests_enabled": true, + "builds_enabled": true, "wiki_enabled": true, "snippets_enabled": false, "created_at": "2013-09-30T13:46:02Z", @@ -191,6 +193,7 @@ Parameters: "path_with_namespace": "diaspora/diaspora-project-site", "issues_enabled": true, "merge_requests_enabled": true, + "builds_enabled": true, "wiki_enabled": true, "snippets_enabled": false, "created_at": "2013-09-30T13: 46: 02Z", @@ -312,6 +315,7 @@ Parameters: - `description` (optional) - short project description - `issues_enabled` (optional) - `merge_requests_enabled` (optional) +- `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 @@ -334,6 +338,7 @@ Parameters: - `default_branch` (optional) - 'master' by default - `issues_enabled` (optional) - `merge_requests_enabled` (optional) +- `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 @@ -357,6 +362,7 @@ Parameters: - `default_branch` (optional) - `issues_enabled` (optional) - `merge_requests_enabled` (optional) +- `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 33167453802..b6cca5d4e2a 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -1,79 +1,5 @@ # Repositories -## List project repository tags - -Get a list of repository tags from a project, sorted by name in reverse alphabetical order. - -``` -GET /projects/:id/repository/tags -``` - -Parameters: - -- `id` (required) - The ID of a project - -```json -[ - { - "commit": { - "author_name": "John Smith", - "author_email": "john@example.com", - "authored_date": "2012-05-28T04:42:42-07:00", - "committed_date": "2012-05-28T04:42:42-07:00", - "committer_name": "Jack Smith", - "committer_email": "jack@example.com", - "id": "2695effb5807a22ff3d138d593fd856244e155e7", - "message": "Initial commit", - "parents_ids": [ - "2a4b78934375d7f53875269ffd4f45fd83a84ebe" - ] - }, - "name": "v1.0.0", - "message": null - } -] -``` - -## Create a new tag - -Creates new tag in the repository that points to the supplied ref. - -``` -POST /projects/:id/repository/tags -``` - -Parameters: - -- `id` (required) - The ID of a project -- `tag_name` (required) - The name of a tag -- `ref` (required) - Create tag using commit SHA, another tag name, or branch name. -- `message` (optional) - Creates annotated tag. - -```json -{ - "commit": { - "author_name": "John Smith", - "author_email": "john@example.com", - "authored_date": "2012-05-28T04:42:42-07:00", - "committed_date": "2012-05-28T04:42:42-07:00", - "committer_name": "Jack Smith", - "committer_email": "jack@example.com", - "id": "2695effb5807a22ff3d138d593fd856244e155e7", - "message": "Initial commit", - "parents_ids": [ - "2a4b78934375d7f53875269ffd4f45fd83a84ebe" - ] - }, - "name": "v1.0.0", - "message": null -} -``` -The message will be `nil` when creating a lightweight tag otherwise -it will contain the annotation. - -It returns 200 if the operation succeed. In case of an error, -405 with an explaining error message is returned. - ## List repository tree Get a list of repository files and directories in a project. diff --git a/doc/api/tags.md b/doc/api/tags.md new file mode 100644 index 00000000000..b5b90cf6b82 --- /dev/null +++ b/doc/api/tags.md @@ -0,0 +1,106 @@ +# Tags + +## List project repository tags + +Get a list of repository tags from a project, sorted by name in reverse alphabetical order. + +``` +GET /projects/:id/repository/tags +``` + +Parameters: + +- `id` (required) - The ID of a project + +```json +[ + { + "commit": { + "author_name": "John Smith", + "author_email": "john@example.com", + "authored_date": "2012-05-28T04:42:42-07:00", + "committed_date": "2012-05-28T04:42:42-07:00", + "committer_name": "Jack Smith", + "committer_email": "jack@example.com", + "id": "2695effb5807a22ff3d138d593fd856244e155e7", + "message": "Initial commit", + "parents_ids": [ + "2a4b78934375d7f53875269ffd4f45fd83a84ebe" + ] + }, + "release": { + "tag": "1.0.0", + "description": "Amazing release. Wow" + }, + "name": "v1.0.0", + "message": null + } +] +``` + +## Create a new tag + +Creates a new tag in the repository that points to the supplied ref. + +``` +POST /projects/:id/repository/tags +``` + +Parameters: + +- `id` (required) - The ID of a project +- `tag_name` (required) - The name of a tag +- `ref` (required) - Create tag using commit SHA, another tag name, or branch name. +- `message` (optional) - Creates annotated tag. +- `release_description` (optional) - Add release notes to the git tag and store it in the GitLab database. + +```json +{ + "commit": { + "author_name": "John Smith", + "author_email": "john@example.com", + "authored_date": "2012-05-28T04:42:42-07:00", + "committed_date": "2012-05-28T04:42:42-07:00", + "committer_name": "Jack Smith", + "committer_email": "jack@example.com", + "id": "2695effb5807a22ff3d138d593fd856244e155e7", + "message": "Initial commit", + "parents_ids": [ + "2a4b78934375d7f53875269ffd4f45fd83a84ebe" + ] + }, + "release": { + "tag": "1.0.0", + "description": "Amazing release. Wow" + }, + "name": "v1.0.0", + "message": null +} +``` +The message will be `nil` when creating a lightweight tag otherwise +it will contain the annotation. + +It returns 200 if the operation succeed. In case of an error, +405 with an explaining error message is returned. + + +## New release + +Add release notes to the existing git tag + +``` +PUT /projects/:id/repository/:tag/release +``` + +Parameters: + +- `id` (required) - The ID of a project +- `tag` (required) - The name of a tag +- `description` (required) - Release notes with markdown support + +```json +{ + "tag": "1.0.0", + "description": "Amazing release. Wow" +} +``` diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 5af27470d2f..4b1788a9af0 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -35,7 +35,7 @@ GitLab Runner then executes build scripts as `gitlab-runner` user. ```bash $ sudo gitlab-runner register -n \ - --url http://gitlab.com/ci \ + --url https://gitlab.com/ci \ --token RUNNER_TOKEN \ --executor shell --description "My Runner" @@ -84,7 +84,7 @@ In order to do that follow the steps: ```bash $ sudo gitlab-runner register -n \ - --url http://gitlab.com/ci \ + --url https://gitlab.com/ci \ --token RUNNER_TOKEN \ --executor docker \ --description "My Docker Runner" \ diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index a87a1f806fc..d69064a91fd 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -6,7 +6,7 @@ To start building projects with GitLab CI a few steps needs to be done. First you need to have a working GitLab and GitLab CI instance. -You can omit this step if you use [GitLab.com](http://GitLab.com/). +You can omit this step if you use [GitLab.com](https://GitLab.com/). ## 2. Create repository on GitLab @@ -16,7 +16,7 @@ Push your application to that repository. ## 3. Add project to CI The next part is to login to GitLab CI. -Point your browser to the URL you have set GitLab or use [gitlab.com/ci](http://gitlab.com/ci/). +Point your browser to the URL you have set GitLab or use [gitlab.com/ci](https://gitlab.com/ci/). On the first screen you will see a list of GitLab's projects that you have access to: @@ -97,7 +97,7 @@ If you do it correctly your runner should be shown under **Runners activated for ### Shared runners -If you use [gitlab.com/ci](http://gitlab.com/ci/) you can use **Shared runners** provided by GitLab Inc. +If you use [gitlab.com/ci](https://gitlab.com/ci/) you can use **Shared runners** provided by GitLab Inc. These are special virtual machines that are run on GitLab's infrastructure that can build any project. To enable **Shared runners** you have to go to **Runners** and click **Enable shared runners** for this project. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d117a2969be..3e6071a2ee7 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -56,6 +56,7 @@ There are a few `keywords` that can't be used as job names: | types | optional | Alias for `stages` | | before_script | optional | Define commands prepended for each job's script | | variables | optional | Define build variables | +| cache | optional | Define list of files that should be cached between subsequent runs | ### image and services This allows to specify a custom Docker image and a list of services that can be used for time of the build. @@ -110,6 +111,19 @@ These variables can be later used in all executed commands and scripts. The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them. +### cache +`cache` is used to specify list of files and directories which should be cached between builds. + +**The global setting allows to specify default cached files for all jobs.** + +To cache all git untracked files and files in `binaries`: +``` +cache: + untracked: true + paths: + - binaries/ +``` + ## Jobs `.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job has to have a unique `job_name`, which is not one of the keywords mentioned above. @@ -141,6 +155,8 @@ job_name: | tags | optional | Defines a list of tags which are used to select runner | | allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status | | when | optional | Define when to run build. Can be `on_success`, `on_failure` or `always` | +| artifacts | optional | Define list build artifacts | +| cache | optional | Define list of files that should be cached between subsequent runs | ### script `script` is a shell script which is executed by runner. The shell script is prepended with `before_script`. @@ -258,6 +274,86 @@ The above script will: 1. Execute `cleanup_build` only when the `build` failed, 2. Always execute `cleanup` as the last step in pipeline. +### artifacts +`artifacts` is used to specify list of files and directories which should be attached to build after success. + +1. Send all files in `binaries` and `.config`: +``` +artifacts: + paths: + - binaries/ + - .config +``` + +2. Send all git untracked files: +``` +artifacts: + untracked: true +``` + +3. Send all git untracked files and files in `binaries`: +``` +artifacts: + untracked: true + paths: + - binaries/ +``` + +The artifacts will be send after the build success to GitLab and will be accessible in GitLab interface to download. + +This feature requires GitLab Runner v0.7.0 or higher. + +### cache +`cache` is used to specify list of files and directories which should be cached between builds. + +1. Cache all files in `binaries` and `.config`: +``` +rspec: + script: test + cache: + paths: + - binaries/ + - .config +``` + +2. Cache all git untracked files: +``` +rspec: + script: test + cache: + untracked: true +``` + +3. Cache all git untracked files and files in `binaries`: +``` +rspec: + script: test + cache: + untracked: true + paths: + - binaries/ +``` + +4. Locally defined cache overwrites globally defined options. This will cache only `binaries/`: + +``` +cache: + paths: + - my/files + +rspec: + script: test + cache: + paths: + - binaries/ +``` + +The cache is provided on best effort basis, so don't expect that cache will be present. +For implementation details please check GitLab Runner. + +This feature requires GitLab Runner v0.7.0 or higher. + + ## Validate the .gitlab-ci.yml Each instance of GitLab CI has an embedded debug tool called Lint. You can find the link to the Lint in the project's settings page or use short url `/lint`. diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md index 54c1780c3ab..bd2c242afc2 100644 --- a/doc/customization/libravatar.md +++ b/doc/customization/libravatar.md @@ -2,7 +2,7 @@ GitLab by default supports [Gravatar](https://gravatar.com) avatar service. Libravatar is a service which delivers your avatar (profile picture) to other websites and their API is -[heavily based on gravatar](http://wiki.libravatar.org/api/). +[heavily based on gravatar](https://wiki.libravatar.org/api/). This means that it is not complicated to switch to Libravatar avatar service or even self hosted Libravatar server. @@ -31,7 +31,7 @@ the configuration options as follows: ## Self-hosted -If you are [running your own libravatar service](http://wiki.libravatar.org/running_your_own/) the URL will be different in the configuration +If you are [running your own libravatar service](https://wiki.libravatar.org/running_your_own/) the URL will be different in the configuration but the important part is to provide the same placeholders so GitLab can parse the URL correctly. For example, you host a service on `http://libravatar.example.com` the `plain_url` you need to supply in `gitlab.yml` is @@ -63,7 +63,7 @@ Run `sudo gitlab-ctl reconfigure` for changes to take effect. ## Default URL for missing images -[Libravatar supports different sets](http://wiki.libravatar.org/api/) of `missing images` for emails not found on the Libravatar service. +[Libravatar supports different sets](https://wiki.libravatar.org/api/) of `missing images` for emails not found on the Libravatar service. In order to use a different set other than `identicon`, replace `&d=identicon` portion of the URL with another supported set. For example, you can use `retro` set in which case the URL would look like: `plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=retro"` diff --git a/doc/development/architecture.md b/doc/development/architecture.md index c00d290371e..6101a71a8de 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -146,7 +146,7 @@ nginx Apache httpd -- [Explanation of Apache logs](http://httpd.apache.org/docs/2.2/logs.html). +- [Explanation of Apache logs](https://httpd.apache.org/docs/2.2/logs.html). - `/var/log/apache2/` contains error and output logs (on Ubuntu). - `/var/log/httpd/` contains error and output logs (on RHEL). diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index 548c484bc08..0f2665a3bf7 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -7,7 +7,7 @@ Please explore webhooks as an option if you do not have filesystem access. For a Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. See -[Git SCM Server-Side Hooks](http://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks) +[Git SCM Server-Side Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks) for more information about each hook type. As of gitlab-shell version 2.2.0 (which requires GitLab 7.5+), GitLab diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md index c565e90da2f..513ad69ec26 100644 --- a/doc/install/database_mysql.md +++ b/doc/install/database_mysql.md @@ -2,7 +2,7 @@ ## Note -We do not recommend using MySQL due to various issues. For example, case [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) and [problems](http://bugs.mysql.com/bug.php?id=65830) that [suggested](http://bugs.mysql.com/bug.php?id=50909) [fixes](http://bugs.mysql.com/bug.php?id=65830) [have](http://bugs.mysql.com/bug.php?id=63164). +We do not recommend using MySQL due to various issues. For example, case [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) and [problems](https://bugs.mysql.com/bug.php?id=65830) that [suggested](https://bugs.mysql.com/bug.php?id=50909) [fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164). ## MySQL diff --git a/doc/install/installation.md b/doc/install/installation.md index f17477a3218..7ef46b04065 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -106,7 +106,7 @@ Then select 'Internet Site' and press enter to confirm the hostname. ## 2. Ruby -The use of Ruby version managers such as [RVM](http://rvm.io/), [rbenv](https://github.com/sstephenson/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab in production frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly advise everyone to follow the instructions below to use a system Ruby. +The use of Ruby version managers such as [RVM](https://rvm.io/), [rbenv](https://github.com/sstephenson/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab in production frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly advise everyone to follow the instructions below to use a system Ruby. Remove the old Ruby 1.8 if present @@ -128,11 +128,10 @@ Install the Bundler Gem: ## 3. Go -Since GitLab 8.0, Git HTTP requests are handled by gitlab-git-http-server. -This is a small daemon written in Go. -To install gitlab-git-http-server we need a Go compiler. -The instructions below assume you use 64-bit Linux. You can find -downloads for other platforms at the [Go download +Since GitLab 8.0, Git HTTP requests are handled by gitlab-workhorse (formerly +gitlab-git-http-server). This is a small daemon written in Go. To install +gitlab-workhorse we need a Go compiler. The instructions below assume you +use 64-bit Linux. You can find downloads for other platforms at the [Go download page](https://golang.org/dl). curl -O --progress https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz @@ -211,9 +210,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-1-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-2-stable gitlab -**Note:** You can change `8-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -246,6 +245,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Change the permissions of the directory where CI build traces are stored sudo chmod -R u+rwX builds/ + # Change the permissions of the directory where CI artifacts are stored + sudo chmod -R u+rwX shared/artifacts/ + # Copy the example Unicorn config sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb @@ -295,7 +297,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Install Gems -**Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](http://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2. +**Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](https://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2. # For PostgreSQL (note, the option says "without ... mysql") sudo -u git -H bundle install --deployment --without development test mysql aws kerberos @@ -320,16 +322,16 @@ GitLab Shell is an SSH access and repository management software developed speci **Note:** Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in /etc/hosts ("127.0.0.1 hostname"). This might be necessary for example if you set up gitlab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)". -### Install gitlab-git-http-server +### Install gitlab-workhorse cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git - cd gitlab-git-http-server - sudo -u git -H git checkout 0.3.0 + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git + cd gitlab-workhorse + sudo -u git -H git checkout 0.4.1 sudo -u git -H make ### Initialize Database and Activate Advanced Features - + # Go to Gitlab installation folder cd /home/git/gitlab diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 9b7d8fa3969..7e2920b8865 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -71,7 +71,7 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server # Filter LDAP users # - # Format: RFC 4515 http://tools.ietf.org/search/rfc4515 + # Format: RFC 4515 https://tools.ietf.org/search/rfc4515 # Ex. (employeeType=developer) # # Note: GitLab does not support omniauth-ldap's custom filter syntax. @@ -145,7 +145,7 @@ If multiple LDAP email attributes are present, e.g. `mail: foo@bar.com` and `ema ## Using an LDAP filter to limit access to your GitLab server If you want to limit all GitLab access to a subset of the LDAP users on your LDAP server you can set up an LDAP user filter. -The filter must comply with [RFC 4515](http://tools.ietf.org/search/rfc4515). +The filter must comply with [RFC 4515](https://tools.ietf.org/search/rfc4515). ```ruby # For omnibus packages; new LDAP server syntax diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index bd9550c6ddb..f2b1721fc03 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -36,7 +36,7 @@ If you want to change these settings: ``` gitlab_rails['omniauth_enabled'] = true gitlab_rails['omniauth_allow_single_sign_on'] = false - gitlab_rails['block_auto_created_users'] = true + gitlab_rails['omniauth_block_auto_created_users'] = true ``` * **For installations from source** diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md index 13bf15fcf45..7b94506c297 100644 --- a/doc/legal/corporate_contributor_license_agreement.md +++ b/doc/legal/corporate_contributor_license_agreement.md @@ -22,4 +22,4 @@ You accept and agree to the following terms and conditions for Your present and 8. It is your responsibility to notify GitLab B.V. when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V.. -This text is licensed under the [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office. +This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office. diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md index 72b01433dd0..f97c252fd7c 100644 --- a/doc/legal/individual_contributor_license_agreement.md +++ b/doc/legal/individual_contributor_license_agreement.md @@ -22,4 +22,4 @@ You accept and agree to the following terms and conditions for Your present and 8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. -This text is licensed under the [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office. +This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office. diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index ac3851f8c95..bc8e7d155e7 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -43,7 +43,7 @@ You can also use other rich text files in GitLab. You might have to install a de ## Newlines -GFM honors the markdown specification in how [paragraphs and line breaks are handled](http://daringfireball.net/projects/markdown/syntax#p). +GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p). A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. Line-breaks, or softreturns, are rendered if you end a line with two or more spaces @@ -72,14 +72,14 @@ do_this_and_do_that_and_another_thing GFM will autolink almost any URL you copy and paste into your text. - * http://www.google.com + * https://www.google.com * https://google.com/ * ftp://ftp.us.debian.org/debian/ * smb://foo/bar/baz * irc://irc.freenode.net/gitlab * http://localhost:3000 -* http://www.google.com +* https://www.google.com * https://google.com/ * ftp://ftp.us.debian.org/debian/ * smb://foo/bar/baz @@ -390,7 +390,7 @@ There are two ways to create links, inline-style and reference-style. [arbitrary case-insensitive reference text]: https://www.mozilla.org [1]: http://slashdot.org - [link text itself]: http://www.reddit.com + [link text itself]: https://www.reddit.com [I'm an inline-style link](https://www.google.com) @@ -406,7 +406,7 @@ Some text to show that the reference links can follow later. [arbitrary case-insensitive reference text]: https://www.mozilla.org [1]: http://slashdot.org -[link text itself]: http://www.reddit.com +[link text itself]: https://www.reddit.com **Note** @@ -583,5 +583,5 @@ By including colons in the header row, you can align the text within that column ## References - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). -- The [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown. +- The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown. - [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown. diff --git a/doc/operations/unicorn.md b/doc/operations/unicorn.md index 3998da01f01..bad61151bda 100644 --- a/doc/operations/unicorn.md +++ b/doc/operations/unicorn.md @@ -52,7 +52,7 @@ leak memory, probably because it does not handle user requests.) To make these memory leaks manageable, GitLab comes with the [unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This -gem [monkey-patches](http://en.wikipedia.org/wiki/Monkey_patch) the Unicorn +gem [monkey-patches](https://en.wikipedia.org/wiki/Monkey_patch) the Unicorn workers to do a memory self-check after every 16 requests. If the memory of the Unicorn worker exceeds a pre-set limit then the worker process exits. The Unicorn master then automatically replaces the worker process. @@ -83,4 +83,4 @@ is a normal value for our current GitLab.com setup and traffic. The high frequency of Unicorn memory restarts on some GitLab sites can be a source of confusion for administrators. Usually they are a [red -herring](http://en.wikipedia.org/wiki/Red_herring). +herring](https://en.wikipedia.org/wiki/Red_herring). diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 606532a6fbe..1a5442cdac7 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -29,7 +29,8 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` Also you can choose what should be backed up by adding environment variable SKIP. Available options: db, -uploads (attachments), repositories, builds(CI build output logs). Use a comma to specify several options at the same time. +uploads (attachments), repositories, builds(CI build output logs), artifacts (CI build artifacts). +Use a comma to specify several options at the same time. ``` sudo gitlab-rake gitlab:backup:create SKIP=db,uploads diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 4925816daaa..c9ab87671d2 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -37,9 +37,9 @@ template are explained below: ### Xth: (6 working days before the 22nd) -- [ ] Merge CE `master` into EE `master` via merge request (#LINK) - [ ] Determine QA person and notify this person - [ ] Check the tasks in [how to rc1 guide](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/release/howto_rc1.md) and delegate tasks if necessary +- [ ] Merge CE `master` into EE `master` via merge request (#LINK) - [ ] Create CE and EE RC1 versions (#LINK) - [ ] Build RC1 packages @@ -54,21 +54,25 @@ template are explained below: - [ ] Update GitLab.com with RC1 - [ ] Create the regression issue in the CE issue tracker: - > This is a meta issue to index possible regressions in this monthly release - > and any patch versions. - > - > Please do not raise or discuss issues directly in this issue but link to - > issues that might warrant a patch release. If there is a Merge Request - > that fixes the issue, please link to that as well. - > - > Please only post one regression issue and/or merge request per comment. - > Comments will be updated by the release manager as they are addressed. + ``` + This is a meta issue to index possible regressions in this monthly release + and any patch versions. + + Please do not raise or discuss issues directly in this issue but link to + issues that might warrant a patch release. If there is a Merge Request + that fixes the issue, please link to that as well. + + Please only post one regression issue and/or merge request per comment. + Comments will be updated by the release manager as they are addressed. + ``` - [ ] Tweet about RC1 release: - > GitLab x.y.0.rc1 is available: https://packages.gitlab.com/gitlab/unstable - > Use at your own risk. Please link regressions issues from - > LINK_TO_REGRESSION_ISSUE + ``` + GitLab x.y.0.rc1 is available: https://packages.gitlab.com/gitlab/unstable + Use at your own risk. Please link regressions issues from + LINK_TO_REGRESSION_ISSUE + ``` ### Xth: (3 working days before the 22nd) diff --git a/doc/release/security.md b/doc/release/security.md index 60bcfbb6da5..b1a62b333e6 100644 --- a/doc/release/security.md +++ b/doc/release/security.md @@ -8,7 +8,7 @@ Do a security release when there is a critical issue that needs to be addresses ## Security vulnerability disclosure -Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. +Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. ## Release Procedure @@ -25,7 +25,7 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c 1. Send tweets about the release from `@gitlabhq` 1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq) 1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number. CVE is only needed for bugs that allow someone to own the server (Remote Code Execution) or access to code of projects they are not a member of. -1. Add the security researcher to the [Security Researcher Acknowledgments list](http://about.gitlab.com/vulnerability-acknowledgements/) +1. Add the security researcher to the [Security Researcher Acknowledgments list](https://about.gitlab.com/vulnerability-acknowledgements/) 1. Thank the security researcher in an email for their cooperation 1. Update the blog post and the CHANGELOG when we receive the CVE number diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 0bdb4070e74..9753504ac8b 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -77,7 +77,7 @@ Deploy keys can be shared between projects, you just need to add them to each pr ### Eclipse -How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration +How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration ## Tip: Non-default OpenSSH key file names or locations diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md index b34fb12da6f..4516a102084 100644 --- a/doc/update/6.x-or-7.x-to-7.14.md +++ b/doc/update/6.x-or-7.x-to-7.14.md @@ -47,7 +47,7 @@ Download and compile Ruby: ```bash mkdir /tmp/ruby && cd /tmp/ruby -curl --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz +curl --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz cd ruby-2.1.6 ./configure --disable-install-rdoc make diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md index 3772f624e98..73d899f9c2e 100644 --- a/doc/update/8.1-to-8.2.md +++ b/doc/update/8.1-to-8.2.md @@ -2,7 +2,8 @@ **NOTE:** GitLab 8.0 introduced several significant changes related to installation and configuration which *are not duplicated here*. Be sure you're -already running a working version of 8.0 before proceeding with this guide. +already running a working version of at least 8.0 before proceeding with this +guide. ### 0. Double-check your Git version @@ -67,7 +68,7 @@ sudo -u git -H git checkout 8-2-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.6.5 +sudo -u git -H git checkout v2.6.6 ``` ### 5. Replace gitlab-git-http-server with gitlab-workhorse @@ -80,7 +81,7 @@ from GitLab 8.1. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse -sudo -u git -H git checkout 0.3.1 +sudo -u git -H git checkout 0.4.1 sudo -u git -H make ``` @@ -165,12 +166,12 @@ To make sure you didn't miss anything run a more thorough check: If all items are green, then congratulations, the upgrade is complete! -## Things went south? Revert to previous version (8.0) +## Things went south? Revert to previous version (8.1) ### 1. Revert the code to the previous version -Follow the [upgrade guide from 7.14 to 8.0](7.14-to-8.0.md), except for the database migration -(The backup is already migrated to the previous version) +Follow the [upgrade guide from 8.0 to 8.1](8.0-to-8.1.md), except for the +database migration (the backup is already migrated to the previous version). ### 2. Restore from the backup diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 5b8d72dfd34..c1a4f64981f 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -13,5 +13,7 @@ - [Project users](add-user/add-user.md) - [Protected branches](protected_branches.md) - [Web Editor](web_editor.md) +- [Releases](releases.md) +- [Milestones](milestones.md) - [Merge Requests](merge_requests.md) - ["Work In Progress" Merge Requests](wip_merge_requests.md) diff --git a/doc/workflow/git_lfs.md b/doc/workflow/git_lfs.md new file mode 100644 index 00000000000..4990a9b5aac --- /dev/null +++ b/doc/workflow/git_lfs.md @@ -0,0 +1,136 @@ +# Git LFS + +Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. +The general recommendation is to not have Git repositories larger than 1GB to preserve performance. + +GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain +environments it is not always convenient to use different commands to differentiate between the large files and regular ones. + +Git LFS makes this simpler for the end user by removing the requirement to learn new commands. +<!-- more --> + +## How it works + +Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication to authorize client requests. +Once the request is authorized, Git LFS client receives instructions from where to fetch or where to push the large file. + +## Requirements + +* Git LFS is supported in GitLab starting with version 8.2 +* Git LFS client version 0.6.0 and up + +## GitLab and Git LFS + +### Configuration + +Git LFS objects can be large in size. By default, they are stored on the server GitLab is installed on. + +There are two configuration options to help GitLab server administrators: + +* Enabling/disabling Git LFS support +* Changing the location of LFS object storage + +#### Omnibus packages + +In `/etc/gitlab/gitlab.rb`: + +```ruby +gitlab_rails['lfs_enabled'] = false +gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects" +``` + +#### Installations from source + +In `config/gitlab.yml`: + +```yaml + lfs: + enabled: false + storage_path: /mnt/storage/lfs-objects +``` + +## Known limitations + +* Git LFS v1 original API is not supported since it was deprecated early in LFS development, starting with Git LFS version 0.6.0 +* When SSH is set as a remote, Git LFS objects still go through HTTPS +* Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended +* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported +* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the url to Git config manually (see #troubleshooting-tips) + +## Using Git LFS + +Lets take a look at the workflow when you need to check large files into your Git repository with Git LFS: +For example, if you want to upload a very large file and check it into your Git repository: + +```bash +git clone git@gitlab.example.com:group/project.git +git lfs init # initialize the Git LFS project project +git lfs track "*.iso" # select the file extensions that you want to treat as large files +``` + +Once a certain file extension is marked for tracking as a LFS object you can use Git as usual without having to redo the command to track a file with the same extension: + +```bash +cp ~/tmp/debian.iso ./ # copy a large file into the current directory +git add . # add the large file to the project +git commit -am "Added Debian iso" # commit the file meta data +git push origin master # sync the git repo and large file to the GitLab server +``` + +Downloading a single large file is also very simple: + +```bash +git clone git@gitlab.example.com:group/project.git +git lfs fetch debian.iso # download the large file +``` + + +## Troubleshooting + +### error: Repository or object not found + +There are a couple of reasons why this error can occur: + +* Wrong version of LFS client used: + +Check the version of Git LFS on the client machine with `git lfs version`. Only version 0.6.0 and newer are supported. + +* Project is using deprecated LFS API + +Check the Git config of the project for traces of deprecated API with `git lfs -l`. If `batch = false` is set in the config, remove the line and try using Git LFS client newer than 0.6.0. + +### Invalid status for <url> : 501 + +When attempting to push a LFS object to a GitLab server that doesn't have Git LFS support enabled, server will return status `error 501`. Check with your GitLab administrator why Git LFS is not enabled on the server. See [Configuration section](#configuration) for instructions on how to enable LFS support. + +### getsockopt: connection refused + +If you push a LFS object to a project and you receive an error similar to: `Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`, +the LFS client is trying to reach GitLab through HTTPS. However, your GitLab instance is being served on HTTP. + +This behaviour is caused by Git LFS using HTTPS connections by default when a `lfsurl` is not set in the Git config. + +To prevent this from happening, set the lfs url in project Git config: + +```bash + +git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/objects/batch" +``` + +### Credentials are always required when pushing an object + +Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing the LFS object on every push for every object, user HTTPS credentials are required. + +By default, Git has support for remembering the credentials for each repository you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials). + +For example, you can tell Git to remember the password for a period of time in which you expect to push the objects: + +```bash +git config --global credential.helper 'cache --timeout=3600' +``` + +This will remember the credentials for an hour after which Git operations will require re-authentication. + +If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, `wincred` is available. + +More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage)
\ No newline at end of file diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 9a24a1e252a..31495bce76e 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -7,7 +7,7 @@ This allows a wide variety of branching strategies and workflows. Almost all of these are an improvement over the methods used before git. But many organizations end up with a workflow that is not clearly defined, overly complex or not integrated with issue tracking systems. Therefore we propose the GitLab flow as clearly defined set of best practices. -It combines [feature driven development](http://en.wikipedia.org/wiki/Feature-driven_development) and [feature branches](http://martinfowler.com/bliki/FeatureBranch.html) with issue tracking. +It combines [feature driven development](https://en.wikipedia.org/wiki/Feature-driven_development) and [feature branches](http://martinfowler.com/bliki/FeatureBranch.html) with issue tracking. Organizations coming to git from other version control systems frequently find it hard to develop an effective workflow. This article describes the GitLab flow that integrates the git workflow with an issue tracking system. @@ -91,7 +91,7 @@ This workflow where commits only flow downstream ensures that everything has bee If you need to cherry-pick a commit with a hotfix it is common to develop it on a feature branch and merge it into master with a merge request, do not delete the feature branch. If master is good to go (it should be if you a practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches. If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches. -An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](http://teatro.io/). +An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](https://teatro.io/). ## Release branches with GitLab flow @@ -104,7 +104,7 @@ By branching as late as possible you minimize the time you have to apply bug fix After a release branch is announced, only serious bug fixes are included in the release branch. If possible these bug fixes are first merged into master and then cherry-picked into the release branch. This way you can't forget to cherry-pick them into master and encounter the same bug on subsequent releases. -This is called an 'upstream first' policy that is also practiced by [Google](http://www.chromium.org/chromium-os/chromiumos-design-docs/upstream-first) and [Red Hat](http://www.redhat.com/about/news/archive/2013/5/a-community-for-using-openstack-with-red-hat-rdo). +This is called an 'upstream first' policy that is also practiced by [Google](https://www.chromium.org/chromium-os/chromiumos-design-docs/upstream-first) and [Red Hat](https://www.redhat.com/about/news/archive/2013/5/a-community-for-using-openstack-with-red-hat-rdo). Every time a bug-fix is included in a release branch the patch version is raised (to comply with [Semantic Versioning](http://semver.org/)) by setting a new tag. Some projects also have a stable branch that points to the same commit as the latest released branch. In this flow it is not common to have a production branch (or git flow master branch). @@ -200,7 +200,7 @@ And to understand a change in context one can always look at the merge commit th After you merge multiple commits from a feature branch into the master branch this is harder to undo. If you would have squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed. -Fortunately [reverting a merge made some time ago](http://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git. +Fortunately [reverting a merge made some time ago](https://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git. This however, requires having specific merge commits for the commits your want to revert. If you revert a merge and you change your mind, revert the revert instead of merging again since git will not allow you to merge the code again otherwise. @@ -215,7 +215,7 @@ With git you can also rebase your feature branch commits to order them after the This prevents creating a merge commit when merging master into your feature branch and creates a nice linear history. However, just like with squashing you should never rebase commits you have pushed to a remote server. This makes it impossible to rebase work in progress that you already shared with your team which is something we recommend. -When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](http://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/). +When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](https://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/). You can reuse recorded resolutions (rerere) sometimes, but without rebasing you only have to solve the conflicts one time and you’re set. There has to be a better way to avoid many merge commits. @@ -307,7 +307,7 @@ When initiating a feature branch, always start with an up to date master to bran If you know beforehand that your work absolutely depends on another branch you can also branch from there. If you need to merge in another branch after starting explain the reason in the merge commit. If you have not pushed your commits to a shared location yet you can also rebase on master or another feature branch. -Do not merge in upstream if your code will work and merge cleanly without doing so, Linus even says that [you should never merge in upstream at random points, only at major releases](http://lwn.net/Articles/328438/). +Do not merge in upstream if your code will work and merge cleanly without doing so, Linus even says that [you should never merge in upstream at random points, only at major releases](https://lwn.net/Articles/328438/). Merging only when needed prevents creating merge commits in your feature branch that later end up littering the master history. ### References diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md index 485db4834e9..1938ccd0c26 100644 --- a/doc/workflow/importing/migrating_from_svn.md +++ b/doc/workflow/importing/migrating_from_svn.md @@ -6,9 +6,9 @@ Git is a distributed version control system. There are some major differences between the two, for more information consult your favorite search engine. Git has tools for migrating SVN repositories to git, namely `git svn`. You can read more about this at -[git documentation pages](http://git-scm.com/book/en/Git-and-Other-Systems-Git-and-Subversion). +[git documentation pages](https://git-scm.com/book/en/Git-and-Other-Systems-Git-and-Subversion). -Apart from the [official git documentation](http://git-scm.com/book/en/Git-and-Other-Systems-Migrating-to-Git) there is also +Apart from the [official git documentation](https://git-scm.com/book/en/Git-and-Other-Systems-Migrating-to-Git) there is also user created step by step guide for migrating from SVN to GitLab. [Benjamin New](https://github.com/leftclickben) wrote [a guide that shows how to do a migration](https://gist.github.com/leftclickben/322b7a3042cbe97ed2af). Mirrors can be found [here](https://gitlab.com/snippets/2168) and [here](https://gist.github.com/maxlazio/f1b593b0d00aa966e9ca). diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md index 751e19da7f1..6d57b5d98cd 100644 --- a/doc/workflow/merge_requests.md +++ b/doc/workflow/merge_requests.md @@ -38,3 +38,15 @@ To check out a particular merge request: ``` $ git checkout origin/merge-requests/1 ``` + +## Ignore whitespace changes in Merge Request diff view + +![MR diff](merge_requests/merge_request_diff.png) + +It you add `w=1` option to URL, you can see diff without whitespace changes. + +![MR diff without whitespace](merge_requests/merge_request_diff_without_whitespace.png) + +It is also working on commits compare view. + +![Commit Compare](merge_requests/commit_compare.png) diff --git a/doc/workflow/merge_requests/commit_compare.png b/doc/workflow/merge_requests/commit_compare.png Binary files differnew file mode 100644 index 00000000000..46b3a56a59b --- /dev/null +++ b/doc/workflow/merge_requests/commit_compare.png diff --git a/doc/workflow/merge_requests/merge_request_diff.png b/doc/workflow/merge_requests/merge_request_diff.png Binary files differnew file mode 100644 index 00000000000..ed08ae91bec --- /dev/null +++ b/doc/workflow/merge_requests/merge_request_diff.png diff --git a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png Binary files differnew file mode 100644 index 00000000000..67d67a64d12 --- /dev/null +++ b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png diff --git a/doc/workflow/milestones.md b/doc/workflow/milestones.md new file mode 100644 index 00000000000..dff36899aec --- /dev/null +++ b/doc/workflow/milestones.md @@ -0,0 +1,13 @@ +# Milestones + +Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date. +A common use is keeping track of an upcoming software version. Milestones are created per-project. + +![milestone form](milestones/form.png) + +## Groups and milestones + +You can create a milestone for several projects in the same group simultaneously. +On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects. + +![group milestone form](milestones/group_form.png) diff --git a/doc/workflow/milestones/form.png b/doc/workflow/milestones/form.png Binary files differnew file mode 100644 index 00000000000..de44c1ffc1a --- /dev/null +++ b/doc/workflow/milestones/form.png diff --git a/doc/workflow/milestones/group_form.png b/doc/workflow/milestones/group_form.png Binary files differnew file mode 100644 index 00000000000..38862dcca68 --- /dev/null +++ b/doc/workflow/milestones/group_form.png diff --git a/doc/workflow/releases.md b/doc/workflow/releases.md new file mode 100644 index 00000000000..6176784fc57 --- /dev/null +++ b/doc/workflow/releases.md @@ -0,0 +1,20 @@ +# Releases + +You can turn any git tag into a release, by adding a note to it. +Release notes behave like any other markdown form in GitLab so you can write text and drag-n-drop files to it. +Release notes are stored in the database of GitLab. + +There are several ways to add release notes: + +* In the interface, when you create a new git tag with GitLab +* In the interface, by adding a note to an existing git tag +* with the GitLab API + +## New tag page with release notes text area + +![new_tag](releases/new_tag.png) + +## Tags page with button to add or edit release notes for existing git tag + +![tags](releases/tags.png) + diff --git a/doc/workflow/releases/new_tag.png b/doc/workflow/releases/new_tag.png Binary files differnew file mode 100644 index 00000000000..e2b64bfe17f --- /dev/null +++ b/doc/workflow/releases/new_tag.png diff --git a/doc/workflow/releases/tags.png b/doc/workflow/releases/tags.png Binary files differnew file mode 100644 index 00000000000..aca91906c68 --- /dev/null +++ b/doc/workflow/releases/tags.png diff --git a/features/groups.feature b/features/groups.feature index db37fa3b375..abf3769a844 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -60,6 +60,14 @@ Feature: Groups Then I should see "Mike" in team list as "Reporter" @javascript + Scenario: Ignore add user to group when is already Owner + Given gitlab user "Mike" + When I visit group "Owned" members page + And I click link "Add members" + When I select "Mike" as "Reporter" + Then I should see "Mike" in team list as "Owner" + + @javascript Scenario: Invite user to group When I visit group "Owned" members page And I click link "Add members" @@ -153,6 +161,13 @@ Feature: Groups Then I should see group milestone with descriptions and expiry date And I should see group milestone with all issues and MRs assigned to that milestone + Scenario: Create multiple milestones with one form + Given I visit group "Owned" milestones page + And I click new milestone button + And I fill milestone name + When I press create mileston button + Then milestone in each project should be created + # Group projects in settings Scenario: I should see all projects in the project list in settings Given Group "Owned" has archived project @@ -169,4 +184,4 @@ Feature: Groups When I visit group "Owned" page Then I should see group "Owned" Then I should see project "Public-project" - + diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index 44a4aa9844a..a0aad66184d 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -44,7 +44,6 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps git_import_instructions = first('.js-toggle-content') expect(git_import_instructions).to be_visible expect(git_import_instructions).to have_content "Git repository URL" - expect(git_import_instructions).to have_content "The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL:" end step 'I click on "Google Code"' do diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 70388c18fcf..9c0313537b1 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -48,6 +48,17 @@ class Spinach::Features::Groups < Spinach::FeatureSteps click_button "Add users to group" end + step 'I select "Mike" as "Master"' do + user = User.find_by(name: "Mike") + + page.within ".users-group-form" do + select2(user.id, from: "#user_ids", multiple: true) + select "Master", from: "access_level" + end + + click_button "Add users to group" + end + step 'I should see "Mike" in team list as "Reporter"' do page.within '.well-list' do expect(page).to have_content('Mike') @@ -55,6 +66,13 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end end + step 'I should see "Mike" in team list as "Owner"' do + page.within '.well-list' do + expect(page).to have_content('Mike') + expect(page).to have_content('Owner') + end + end + step 'I select "sjobs@apple.com" as "Reporter"' do page.within ".users-group-form" do select2("sjobs@apple.com", from: "#user_ids", multiple: true) @@ -255,6 +273,28 @@ class Spinach::Features::Groups < Spinach::FeatureSteps expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') end + step 'I fill milestone name' do + fill_in 'milestone_title', with: 'v2.9.0' + end + + step 'I click new milestone button' do + click_link "New Milestone" + end + + step 'I press create mileston button' do + click_button "Create Milestone" + end + + step 'milestone in each project should be created' do + group = Group.find_by(name: 'Owned') + expect(page).to have_content "Milestone v2.9.0" + expect(group.projects).to be_present + + group.projects.each do |project| + expect(page).to have_content project.name + end + end + protected def assigned_to_me(key) diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index eb978620da6..c74a5fd3bc7 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -31,6 +31,10 @@ module SharedPaths visit merge_requests_group_path(Group.find_by(name: "Owned")) end + step 'I visit group "Owned" milestones page' do + visit group_milestones_path(Group.find_by(name: "Owned")) + end + step 'I visit group "Owned" members page' do visit group_group_members_path(Group.find_by(name: "Owned")) end diff --git a/lib/api/api.rb b/lib/api/api.rb index 40671e2517c..fe1bf8a4816 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -52,5 +52,6 @@ module API mount Labels mount Settings mount Keys + mount Tags end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 883a5e14b17..d6aec03d7f5 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -62,7 +62,7 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at + expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at expose :creator_id expose :namespace expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } @@ -95,25 +95,6 @@ module API end end - class RepoTag < Grape::Entity - expose :name - expose :message do |repo_obj, _options| - if repo_obj.respond_to?(:message) - repo_obj.message - else - nil - end - end - - expose :commit do |repo_obj, options| - if repo_obj.respond_to?(:commit) - repo_obj.commit - elsif options[:project] - options[:project].repository.commit(repo_obj.target) - end - end - end - class RepoObject < Grape::Entity expose :name @@ -231,7 +212,7 @@ module API class CommitStatus < Grape::Entity expose :id, :sha, :ref, :status, :name, :target_url, :description, - :created_at, :started_at, :finished_at + :created_at, :started_at, :finished_at, :allow_failure expose :author, using: Entities::UserBasic end @@ -341,5 +322,34 @@ module API expose :user_oauth_applications expose :after_sign_out_path end + + class Release < Grape::Entity + expose :tag, :description + end + + class RepoTag < Grape::Entity + expose :name + expose :message do |repo_obj, _options| + if repo_obj.respond_to?(:message) + repo_obj.message + else + nil + end + end + + expose :commit do |repo_obj, options| + if repo_obj.respond_to?(:commit) + repo_obj.commit + elsif options[:project] + options[:project].repository.commit(repo_obj.target) + end + end + + expose :release, using: Entities::Release do |repo_obj, options| + if options[:project] + options[:project].releases.find_by(tag: repo_obj.name) + end + end + end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 652bdf9b278..92540ccf2b1 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -133,6 +133,12 @@ module API authorize! :admin_project, user_project end + def require_gitlab_workhorse! + unless env['HTTP_GITLAB_WORKHORSE'].present? + forbidden!('Request should be executed via GitLab Workhorse') + end + end + def can?(object, action, subject) abilities.allowed?(object, action, subject) end @@ -234,6 +240,10 @@ module API render_api_error!(message || '409 Conflict', 409) end + def file_to_large! + render_api_error!('413 Request Entity Too Large', 413) + end + def render_validation_error!(model) if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) @@ -282,6 +292,44 @@ module API end end + # file helpers + + def uploaded_file!(field, uploads_path) + if params[field] + bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename) + return params[field] + end + + # sanitize file paths + # this requires all paths to exist + required_attributes! %W(#{field}.path) + uploads_path = File.realpath(uploads_path) + file_path = File.realpath(params["#{field}.path"]) + bad_request!('Bad file path') unless file_path.start_with?(uploads_path) + + UploadedFile.new( + file_path, + params["#{field}.name"], + params["#{field}.type"] || 'application/octet-stream', + ) + end + + def present_file!(path, filename, content_type = 'application/octet-stream') + filename ||= File.basename(path) + header['Content-Disposition'] = "attachment; filename=#{filename}" + header['Content-Transfer-Encoding'] = 'binary' + content_type content_type + + # Support download acceleration + case headers['X-Sendfile-Type'] + when 'X-Sendfile' + header['X-Sendfile'] = path + body + else + file FileStreamer.new(path) + end + end + private def add_pagination_headers(paginated, per_page) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 67ee66a2058..2b4ada6e2eb 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -75,6 +75,7 @@ module API # description (optional) - short project description # issues_enabled (optional) # merge_requests_enabled (optional) + # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) # namespace_id (optional) - defaults to user namespace @@ -90,6 +91,7 @@ module API :description, :issues_enabled, :merge_requests_enabled, + :builds_enabled, :wiki_enabled, :snippets_enabled, :namespace_id, @@ -117,6 +119,7 @@ module API # default_branch (optional) - 'master' by default # issues_enabled (optional) # merge_requests_enabled (optional) + # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 @@ -132,6 +135,7 @@ module API :default_branch, :issues_enabled, :merge_requests_enabled, + :builds_enabled, :wiki_enabled, :snippets_enabled, :public, @@ -172,6 +176,7 @@ module API # description (optional) - short project description # issues_enabled (optional) # merge_requests_enabled (optional) + # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 @@ -185,6 +190,7 @@ module API :default_branch, :issues_enabled, :merge_requests_enabled, + :builds_enabled, :wiki_enabled, :snippets_enabled, :public, diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 20d568cf462..d7c48639eba 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -16,41 +16,6 @@ module API end end - # Get a project repository tags - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/repository/tags - get ":id/repository/tags" do - present user_project.repo.tags.sort_by(&:name).reverse, - with: Entities::RepoTag, project: user_project - end - - # Create tag - # - # Parameters: - # id (required) - The ID of a project - # tag_name (required) - The name of the tag - # ref (required) - Create tag from commit sha or branch - # message (optional) - Specifying a message creates an annotated tag. - # Example Request: - # POST /projects/:id/repository/tags - post ':id/repository/tags' do - authorize_push_project - message = params[:message] || nil - result = CreateTagService.new(user_project, current_user). - execute(params[:tag_name], params[:ref], message) - - if result[:status] == :success - present result[:tag], - with: Entities::RepoTag, - project: user_project - else - render_api_error!(result[:message], 400) - end - end - # Get a project repository tree # # Parameters: diff --git a/lib/api/tags.rb b/lib/api/tags.rb new file mode 100644 index 00000000000..673342dd447 --- /dev/null +++ b/lib/api/tags.rb @@ -0,0 +1,61 @@ +module API + # Git Tags API + class Tags < Grape::API + before { authenticate! } + before { authorize! :download_code, user_project } + + resource :projects do + # Get a project repository tags + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/repository/tags + get ":id/repository/tags" do + present user_project.repo.tags.sort_by(&:name).reverse, + with: Entities::RepoTag, project: user_project + end + + # Create tag + # + # Parameters: + # id (required) - The ID of a project + # tag_name (required) - The name of the tag + # ref (required) - Create tag from commit sha or branch + # message (optional) - Specifying a message creates an annotated tag. + # Example Request: + # POST /projects/:id/repository/tags + post ':id/repository/tags' do + authorize_push_project + message = params[:message] || nil + result = CreateTagService.new(user_project, current_user). + execute(params[:tag_name], params[:ref], message, params[:release_description]) + + if result[:status] == :success + present result[:tag], + with: Entities::RepoTag, + project: user_project + else + render_api_error!(result[:message], 400) + end + end + + # Add release notes to tag + # + # Parameters: + # id (required) - The ID of a project + # tag (required) - The name of the tag + # description (required) - Release notes with markdown support + # Example Request: + # PUT /projects/:id/repository/tags + put ':id/repository/:tag/release', requirements: { tag: /.*/ } do + authorize_push_project + required_attributes! [:description] + release = user_project.releases.find_or_initialize_by(tag: params[:tag]) + release.update_attributes(description: params[:description]) + + present release, with: Entities::Release + end + end + end +end diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb new file mode 100644 index 00000000000..51fa3867e67 --- /dev/null +++ b/lib/backup/artifacts.rb @@ -0,0 +1,13 @@ +require 'backup/files' + +module Backup + class Artifacts < Files + def initialize + super('artifacts', ArtifactUploader.artifacts_path) + end + + def create_files_dir + Dir.mkdir(app_files_dir, 0700) + end + end +end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index f011fd03de0..e7eda7c6f45 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -150,17 +150,15 @@ module Backup private def backup_contents - folders_to_backup + ["uploads.tar.gz", "builds.tar.gz", "backup_information.yml"] + folders_to_backup + archives_to_backup + ["backup_information.yml"] end - def folders_to_backup - folders = %w{repositories db} - - if ENV["SKIP"] - return folders.reject{ |folder| ENV["SKIP"].include?(folder) } - end + def archives_to_backup + %w{uploads builds artifacts}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact + end - folders + def folders_to_backup + %w{repositories db}.reject{ |name| skipped?(name) } end def settings diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 0a4cbf69b63..07e68216d7f 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -27,6 +27,7 @@ module Ci helpers Helpers helpers ::API::Helpers + helpers Gitlab::CurrentSettings mount Builds mount Commits diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 83ca1e6481c..0a586672807 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -47,6 +47,106 @@ module Ci build.drop end end + + # Authorize artifacts uploading for build - Runners only + # + # Parameters: + # id (required) - The ID of a build + # token (required) - The build authorization token + # filesize (optional) - the size of uploaded file + # Example Request: + # POST /builds/:id/artifacts/authorize + post ":id/artifacts/authorize" do + require_gitlab_workhorse! + build = Ci::Build.find_by_id(params[:id]) + not_found! unless build + authenticate_build_token!(build) + forbidden!('build is not running') unless build.running? + + if params[:filesize] + file_size = params[:filesize].to_i + file_to_large! unless file_size < max_artifacts_size + end + + status 200 + { TempPath: ArtifactUploader.artifacts_upload_path } + end + + # Upload artifacts to build - Runners only + # + # Parameters: + # id (required) - The ID of a build + # token (required) - The build authorization token + # file (required) - The uploaded file + # Parameters (accelerated by GitLab Workhorse): + # file.path - path to locally stored body (generated by Workhorse) + # file.name - real filename as send in Content-Disposition + # file.type - real content type as send in Content-Type + # Headers: + # BUILD-TOKEN (required) - The build authorization token, the same as token + # Body: + # The file content + # + # Example Request: + # POST /builds/:id/artifacts + post ":id/artifacts" do + require_gitlab_workhorse! + build = Ci::Build.find_by_id(params[:id]) + not_found! unless build + authenticate_build_token!(build) + forbidden!('build is not running') unless build.running? + + file = uploaded_file!(:file, ArtifactUploader.artifacts_upload_path) + file_to_large! unless file.size < max_artifacts_size + + if build.update_attributes(artifacts_file: file) + present build, with: Entities::Build + else + render_validation_error!(build) + end + end + + # Download the artifacts file from build - Runners only + # + # Parameters: + # id (required) - The ID of a build + # token (required) - The build authorization token + # Headers: + # BUILD-TOKEN (required) - The build authorization token, the same as token + # Example Request: + # GET /builds/:id/artifacts + get ":id/artifacts" do + build = Ci::Build.find_by_id(params[:id]) + not_found! unless build + authenticate_build_token!(build) + artifacts_file = build.artifacts_file + + unless artifacts_file.file_storage? + return redirect_to build.artifacts_file.url + end + + unless artifacts_file.exists? + not_found! + end + + present_file!(artifacts_file.path, artifacts_file.filename) + end + + # Remove the artifacts file from build + # + # Parameters: + # id (required) - The ID of a build + # token (required) - The build authorization token + # Headers: + # BUILD-TOKEN (required) - The build authorization token, the same as token + # Example Request: + # DELETE /builds/:id/artifacts + delete ":id/artifacts" do + build = Ci::Build.find_by_id(params[:id]) + not_found! unless build + authenticate_build_token!(build) + build.remove_artifacts_file! + end end end end diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index b80c0b8b273..750f421872d 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -11,10 +11,16 @@ module Ci expose :builds end + class ArtifactFile < Grape::Entity + expose :filename, :size + end + class Build < Grape::Entity expose :id, :commands, :ref, :sha, :status, :project_id, :repo_url, :before_sha, :allow_git_fetch, :project_name + expose :name, :token, :stage + expose :options do |model| model.options end @@ -24,6 +30,7 @@ module Ci end expose :variables + expose :artifacts_file, using: ArtifactFile end class Runner < Grape::Entity diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 7e4986b6af3..02502333756 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -1,6 +1,8 @@ module Ci module API module Helpers + BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN" + BUILD_TOKEN_PARAM = :token UPDATE_RUNNER_EVERY = 60 def authenticate_runners! @@ -15,6 +17,11 @@ module Ci forbidden! unless project.valid_token?(params[:project_token]) end + def authenticate_build_token!(build) + token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s + forbidden! unless token && build.valid_token?(token) + end + def update_runner_last_contact # Use a random threshold to prevent beating DB updates contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY) @@ -32,6 +39,10 @@ module Ci info = attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"]) current_runner.update(info) end + + def max_artifacts_size + current_application_settings.max_artifacts_size.megabytes.to_i + end end end end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 0f57a4f53ab..3beafcad117 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -4,10 +4,10 @@ module Ci DEFAULT_STAGES = %w(build test deploy) DEFAULT_STAGE = 'test' - ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables] - ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when] + ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache] + ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when, :artifacts, :cache] - attr_reader :before_script, :image, :services, :variables, :path + attr_reader :before_script, :image, :services, :variables, :path, :cache def initialize(config, path = nil) @config = YAML.load(config) @@ -46,6 +46,7 @@ module Ci @services = @config[:services] @stages = @config[:stages] || @config[:types] @variables = @config[:variables] || {} + @cache = @config[:cache] @config.except!(*ALLOWED_YAML_KEYS) # anything that doesn't have script is considered as unknown @@ -77,7 +78,9 @@ module Ci when: job[:when] || 'on_success', options: { image: job[:image] || @image, - services: job[:services] || @services + services: job[:services] || @services, + artifacts: job[:artifacts], + cache: job[:cache] || @cache, }.compact } end @@ -111,6 +114,16 @@ module Ci raise ValidationError, "variables should be a map of key-valued strings" end + if @cache + if @cache[:untracked] && !validate_boolean(@cache[:untracked]) + raise ValidationError, "cache:untracked parameter should be an boolean" + end + + if @cache[:paths] && !validate_array_of_strings(@cache[:paths]) + raise ValidationError, "cache:paths parameter should be an array of strings" + end + end + @jobs.each do |name, job| validate_job!(name, job) end @@ -159,7 +172,27 @@ module Ci raise ValidationError, "#{name} job: except parameter should be an array of strings" end - if job[:allow_failure] && !job[:allow_failure].in?([true, false]) + if job[:cache] + if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked]) + raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean" + end + + if job[:cache][:paths] && !validate_array_of_strings(job[:cache][:paths]) + raise ValidationError, "#{name} job: cache:paths parameter should be an array of strings" + end + end + + if job[:artifacts] + if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked]) + raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean" + end + + if job[:artifacts][:paths] && !validate_array_of_strings(job[:artifacts][:paths]) + raise ValidationError, "#{name} job: artifacts:paths parameter should be an array of strings" + end + end + + if job[:allow_failure] && !validate_boolean(job[:allow_failure]) raise ValidationError, "#{name} job: allow_failure parameter should be an boolean" end @@ -182,6 +215,10 @@ module Ci value.is_a?(String) || value.is_a?(Symbol) end + def validate_boolean(value) + value.in?([true, false]) + end + def process?(only_params, except_params, ref, tag) if only_params.present? return false unless matching?(only_params, ref, tag) diff --git a/lib/file_streamer.rb b/lib/file_streamer.rb new file mode 100644 index 00000000000..4e3c6d3c773 --- /dev/null +++ b/lib/file_streamer.rb @@ -0,0 +1,16 @@ +class FileStreamer #:nodoc: + attr_reader :to_path + + def initialize(path) + @to_path = path + end + + # Stream the file's contents if Rack::Sendfile isn't present. + def each + File.open(to_path, 'rb') do |file| + while chunk = file.read(16384) + yield chunk + end + end + end +end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 440ef5a3cb3..0d156047ff0 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -33,6 +33,9 @@ module Grack auth! + lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call + return lfs_response unless lfs_response.nil? + if project && authorized_request? # Tell gitlab-workhorse the request is OK, and what the GL_ID is render_grack_auth_ok @@ -72,7 +75,7 @@ module Grack matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login) if project && matched_login.present? && git_cmd == 'git-upload-pack' - underscored_service = matched_login['s'].underscore + underscored_service = matched_login['s'].underscore if Service.available_services_names.include?(underscored_service) service_method = "#{underscored_service}_service" diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 01b8bda05c6..87ac30b5ffe 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -1,6 +1,6 @@ module Gitlab class Shell - class AccessDenied < StandardError; end + class Error < StandardError; end class KeyAdder < Struct.new(:io) def add_key(id, key) @@ -36,8 +36,9 @@ module Gitlab # import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git") # def import_repository(name, url) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'import-project', - "#{name}.git", url, '240']) + output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '240']) + raise Error, output unless status.zero? + true end # Move repository diff --git a/lib/gitlab/compare_result.rb b/lib/gitlab/compare_result.rb index d72391dade5..0d696a1ee28 100644 --- a/lib/gitlab/compare_result.rb +++ b/lib/gitlab/compare_result.rb @@ -2,8 +2,8 @@ module Gitlab class CompareResult attr_reader :commits, :diffs - def initialize(compare) - @commits, @diffs = compare.commits, compare.diffs + def initialize(compare, diff_options = {}) + @commits, @diffs = compare.commits, compare.diffs(nil, diff_options) end end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index cd84afa31d5..2d3e32d9539 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -25,6 +25,7 @@ module Gitlab session_expire_delay: Settings.gitlab['session_expire_delay'], import_sources: Settings.gitlab['import_sources'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], + max_artifacts_size: Ci::Settings.gitlab_ci['max_artifacts_size'], ) end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index c90184d31cf..3ed1eec517c 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -13,7 +13,7 @@ module Gitlab def user return @user if defined?(@user) - @user = + @user = case actor when User actor @@ -125,7 +125,7 @@ module Gitlab def change_access_check(change) oldrev, newrev, ref = change.split(' ') - action = + action = if project.protected_branch?(branch_name(ref)) protected_branch_action(oldrev, newrev, branch_name(ref)) elsif protected_tag?(tag_name(ref)) @@ -148,7 +148,7 @@ module Gitlab build_status_object(false, "You are not allowed to change existing tags on this project.") else # :push_code build_status_object(false, "You are not allowed to push code to this project.") - end + end return status end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index bd7340a80f1..b5720f6e2cb 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -19,7 +19,7 @@ module Gitlab if issue.pull_request.nil? body = @formatter.author_line(issue.user.login) - body += issue.body + body += issue.body || "" if issue.comments > 0 body += @formatter.comments_header diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb index 03c410726a5..87fee28dc01 100644 --- a/lib/gitlab/google_code_import/importer.rb +++ b/lib/gitlab/google_code_import/importer.rb @@ -30,7 +30,7 @@ module Gitlab def user_map @user_map ||= begin - user_map = Hash.new do |hash, user| + user_map = Hash.new do |hash, user| # Replace ... by \.\.\., so `johnsm...@gmail.com` isn't autolinked. Client.mask_email(user).sub("...", "\\.\\.\\.") end @@ -76,18 +76,7 @@ module Gitlab attachments = format_attachments(raw_issue["id"], 0, issue_comment["attachments"]) body = format_issue_body(author, date, content, attachments) - - labels = [] - raw_issue["labels"].each do |label| - name = nice_label_name(label) - labels << name - - unless @known_labels.include?(name) - create_label(name) - @known_labels << name - end - end - labels << nice_status_name(raw_issue["status"]) + labels = import_issue_labels(raw_issue) assignee_id = nil if raw_issue.has_key?("owner") @@ -110,6 +99,7 @@ module Gitlab assignee_id: assignee_id, state: raw_issue["state"] == "closed" ? "closed" : "opened" ) + issue.add_labels_by_names(labels) if issue.iid != raw_issue["id"] @@ -120,6 +110,23 @@ module Gitlab end end + def import_issue_labels(raw_issue) + labels = [] + + raw_issue["labels"].each do |label| + name = nice_label_name(label) + labels << name + + unless @known_labels.include?(name) + create_label(name) + @known_labels << name + end + end + + labels << nice_status_name(raw_issue["status"]) + labels + end + def import_issue_comments(issue, comments) Note.transaction do while raw_comment = comments.shift @@ -172,7 +179,7 @@ module Gitlab "#5cb85c" when "Status: Started" "#8e44ad" - + when "Priority: Critical" "#ffcfcf" when "Priority: High" @@ -181,7 +188,7 @@ module Gitlab "#fff5cc" when "Priority: Low" "#cfe9ff" - + when "Type: Defect" "#d9534f" when "Type: Enhancement" @@ -249,8 +256,8 @@ module Gitlab end if raw_updates.has_key?("cc") - cc = raw_updates["cc"].map do |l| - deleted = l.start_with?("-") + cc = raw_updates["cc"].map do |l| + deleted = l.start_with?("-") l = l[1..-1] if deleted l = user_map[l] l = "~~#{l}~~" if deleted @@ -261,8 +268,8 @@ module Gitlab end if raw_updates.has_key?("labels") - labels = raw_updates["labels"].map do |l| - deleted = l.start_with?("-") + labels = raw_updates["labels"].map do |l| + deleted = l.start_with?("-") l = l[1..-1] if deleted l = nice_label_name(l) l = "~~#{l}~~" if deleted @@ -278,45 +285,39 @@ module Gitlab if raw_updates.has_key?("blockedOn") blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on| - name, id = raw_blocked_on.split(":", 2) - - deleted = name.start_with?("-") - name = name[1..-1] if deleted - - text = - if name == project.import_source - "##{id}" - else - "#{project.namespace.path}/#{name}##{id}" - end - text = "~~#{text}~~" if deleted - text + format_blocking_updates(raw_blocked_on) end + updates << "*Blocked on: #{blocked_ons.join(", ")}*" end if raw_updates.has_key?("blocking") blockings = raw_updates["blocking"].map do |raw_blocked_on| - name, id = raw_blocked_on.split(":", 2) - - deleted = name.start_with?("-") - name = name[1..-1] if deleted - - text = - if name == project.import_source - "##{id}" - else - "#{project.namespace.path}/#{name}##{id}" - end - text = "~~#{text}~~" if deleted - text + format_blocking_updates(raw_blocked_on) end + updates << "*Blocking: #{blockings.join(", ")}*" end updates end + def format_blocking_updates(raw_blocked_on) + name, id = raw_blocked_on.split(":", 2) + + deleted = name.start_with?("-") + name = name[1..-1] if deleted + + text = + if name == project.import_source + "##{id}" + else + "#{project.namespace.path}/#{name}##{id}" + end + text = "~~#{text}~~" if deleted + text + end + def format_attachments(issue_id, comment_id, raw_attachments) return [] unless raw_attachments @@ -325,7 +326,7 @@ module Gitlab filename = attachment["fileName"] link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}" - + text = "[#{filename}](#{link})" text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/i text diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb index 99e7b529ba9..44507bde25d 100644 --- a/lib/gitlab/inline_diff.rb +++ b/lib/gitlab/inline_diff.rb @@ -11,48 +11,71 @@ module Gitlab indexes.each do |index| first_line = diff_arr[index+1] second_line = diff_arr[index+2] - max_length = [first_line.size, second_line.size].max # Skip inline diff if empty line was replaced with content next if first_line == "-\n" - first_the_same_symbols = 0 - (0..max_length + 1).each do |i| - first_the_same_symbols = i - 1 - if first_line[i] != second_line[i] && i > 0 - break - end - end + first_token = find_first_token(first_line, second_line) + apply_first_token(diff_arr, index, first_token) + + last_token = find_last_token(first_line, second_line, first_token) + apply_last_token(diff_arr, index, last_token) + end + + diff_arr + end + + def apply_first_token(diff_arr, index, first_token) + start = first_token + START + + if first_token.empty? + # In case if we remove string of spaces in commit + diff_arr[index+1].sub!("-", "-" => "-#{START}") + diff_arr[index+2].sub!("+", "+" => "+#{START}") + else + diff_arr[index+1].sub!(first_token, first_token => start) + diff_arr[index+2].sub!(first_token, first_token => start) + end + end - first_token = first_line[0..first_the_same_symbols][1..-1] - start = first_token + START + def apply_last_token(diff_arr, index, last_token) + # This is tricky: escape backslashes so that `sub` doesn't interpret them + # as backreferences. Regexp.escape does NOT do the right thing. + replace_token = FINISH + last_token.gsub(/\\/, '\&\&') + diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token) + diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token) + end + + def find_first_token(first_line, second_line) + max_length = [first_line.size, second_line.size].max + first_the_same_symbols = 0 + + (0..max_length + 1).each do |i| + first_the_same_symbols = i - 1 - if first_token.empty? - # In case if we remove string of spaces in commit - diff_arr[index+1].sub!("-", "-" => "-#{START}") - diff_arr[index+2].sub!("+", "+" => "+#{START}") - else - diff_arr[index+1].sub!(first_token, first_token => start) - diff_arr[index+2].sub!(first_token, first_token => start) + if first_line[i] != second_line[i] && i > 0 + break end + end + + first_line[0..first_the_same_symbols][1..-1] + end + + def find_last_token(first_line, second_line, first_token) + max_length = [first_line.size, second_line.size].max + last_the_same_symbols = 0 + + (1..max_length + 1).each do |i| + last_the_same_symbols = -i + shortest_line = second_line.size > first_line.size ? first_line : second_line - last_the_same_symbols = 0 - (1..max_length + 1).each do |i| - last_the_same_symbols = -i - shortest_line = second_line.size > first_line.size ? first_line : second_line - if ( first_line[-i] != second_line[-i] ) || "#{first_token}#{START}".size == shortest_line[1..-i].size - break - end + if (first_line[-i] != second_line[-i]) || "#{first_token}#{START}".size == shortest_line[1..-i].size + break end - last_the_same_symbols += 1 - last_token = first_line[last_the_same_symbols..-1] - # This is tricky: escape backslashes so that `sub` doesn't interpret them - # as backreferences. Regexp.escape does NOT do the right thing. - replace_token = FINISH + last_token.gsub(/\\/, '\&\&') - diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token) - diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token) end - diff_arr + + last_the_same_symbols += 1 + first_line[last_the_same_symbols..-1] end def _indexes_of_changed_lines(diff_arr) diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb new file mode 100644 index 00000000000..4202c786466 --- /dev/null +++ b/lib/gitlab/lfs/response.rb @@ -0,0 +1,308 @@ +module Gitlab + module Lfs + class Response + + def initialize(project, user, request) + @origin_project = project + @project = storage_project(project) + @user = user + @env = request.env + @request = request + end + + # Return a response for a download request + # Can be a response to: + # Request from a user to get the file + # Request from gitlab-workhorse which file to serve to the user + def render_download_hypermedia_response(oid) + render_response_to_download do + if check_download_accept_header? + render_lfs_download_hypermedia(oid) + else + render_not_found + end + end + end + + def render_download_object_response(oid) + render_response_to_download do + if check_download_sendfile_header? && check_download_accept_header? + render_lfs_sendfile(oid) + else + render_not_found + end + end + end + + def render_lfs_api_auth + render_response_to_push do + request_body = JSON.parse(@request.body.read) + return render_not_found if request_body.empty? || request_body['objects'].empty? + + response = build_response(request_body['objects']) + [ + 200, + { + "Content-Type" => "application/json; charset=utf-8", + "Cache-Control" => "private", + }, + [JSON.dump(response)] + ] + end + end + + def render_storage_upload_authorize_response(oid, size) + render_response_to_push do + [ + 200, + { "Content-Type" => "application/json; charset=utf-8" }, + [JSON.dump({ + 'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload", + 'LfsOid' => oid, + 'LfsSize' => size + })] + ] + end + end + + def render_storage_upload_store_response(oid, size, tmp_file_name) + render_response_to_push do + render_lfs_upload_ok(oid, size, tmp_file_name) + end + end + + private + + def render_not_enabled + [ + 501, + { + "Content-Type" => "application/vnd.git-lfs+json", + }, + [JSON.dump({ + 'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.', + 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", + })] + ] + end + + def render_unauthorized + [ + 401, + { + 'Content-Type' => 'text/plain' + }, + ['Unauthorized'] + ] + end + + def render_not_found + [ + 404, + { + "Content-Type" => "application/vnd.git-lfs+json" + }, + [JSON.dump({ + 'message' => 'Not found.', + 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", + })] + ] + end + + def render_forbidden + [ + 403, + { + "Content-Type" => "application/vnd.git-lfs+json" + }, + [JSON.dump({ + 'message' => 'Access forbidden. Check your access level.', + 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", + })] + ] + end + + def render_lfs_sendfile(oid) + return render_not_found unless oid.present? + + lfs_object = object_for_download(oid) + + if lfs_object && lfs_object.file.exists? + [ + 200, + { + # GitLab-workhorse will forward Content-Type header + "Content-Type" => "application/octet-stream", + "X-Sendfile" => lfs_object.file.path + }, + [] + ] + else + render_not_found + end + end + + def render_lfs_download_hypermedia(oid) + return render_not_found unless oid.present? + + lfs_object = object_for_download(oid) + if lfs_object + [ + 200, + { "Content-Type" => "application/vnd.git-lfs+json" }, + [JSON.dump(download_hypermedia(oid))] + ] + else + render_not_found + end + end + + def render_lfs_upload_ok(oid, size, tmp_file) + if store_file(oid, size, tmp_file) + [ + 200, + { + 'Content-Type' => 'text/plain', + 'Content-Length' => 0 + }, + [] + ] + else + [ + 422, + { 'Content-Type' => 'text/plain' }, + ["Unprocessable entity"] + ] + end + end + + def render_response_to_download + return render_not_enabled unless Gitlab.config.lfs.enabled + + unless @project.public? + return render_unauthorized unless @user + return render_forbidden unless user_can_fetch? + end + + yield + end + + def render_response_to_push + return render_not_enabled unless Gitlab.config.lfs.enabled + return render_unauthorized unless @user + return render_forbidden unless user_can_push? + + yield + end + + def check_download_sendfile_header? + @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile" + end + + def check_download_accept_header? + @env['HTTP_ACCEPT'].to_s == "application/vnd.git-lfs+json; charset=utf-8" + end + + def user_can_fetch? + # Check user access against the project they used to initiate the pull + @user.can?(:download_code, @origin_project) + end + + def user_can_push? + # Check user access against the project they used to initiate the push + @user.can?(:push_code, @origin_project) + end + + def storage_project(project) + if project.forked? + project.forked_from_project + else + project + end + end + + def store_file(oid, size, tmp_file) + tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file) + + object = LfsObject.find_or_create_by(oid: oid, size: size) + if object.file.exists? + success = true + else + success = move_tmp_file_to_storage(object, tmp_file_path) + end + + if success + success = link_to_project(object) + end + + success + ensure + # Ensure that the tmp file is removed + FileUtils.rm_f(tmp_file_path) + end + + def object_for_download(oid) + @project.lfs_objects.find_by(oid: oid) + end + + def move_tmp_file_to_storage(object, path) + File.open(path) do |f| + object.file = f + end + + object.file.store! + object.save + end + + def link_to_project(object) + if object && !object.projects.exists?(@project) + object.projects << @project + object.save + end + end + + def select_existing_objects(objects) + objects_oids = objects.map { |o| o['oid'] } + @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set + end + + def build_response(objects) + selected_objects = select_existing_objects(objects) + + upload_hypermedia(objects, selected_objects) + end + + def download_hypermedia(oid) + { + '_links' => { + 'download' => + { + 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{oid}", + 'header' => { + 'Accept' => "application/vnd.git-lfs+json; charset=utf-8", + 'Authorization' => @env['HTTP_AUTHORIZATION'] + }.compact + } + } + } + end + + def upload_hypermedia(all_objects, existing_objects) + all_objects.each do |object| + object['_links'] = hypermedia_links(object) unless existing_objects.include?(object['oid']) + end + + { 'objects' => all_objects } + end + + def hypermedia_links(object) + { + "upload" => { + 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}", + 'header' => { 'Authorization' => @env['HTTP_AUTHORIZATION'] } + }.compact + } + end + end + end +end diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb new file mode 100644 index 00000000000..4809e834984 --- /dev/null +++ b/lib/gitlab/lfs/router.rb @@ -0,0 +1,95 @@ +module Gitlab + module Lfs + class Router + def initialize(project, user, request) + @project = project + @user = user + @env = request.env + @request = request + end + + def try_call + return unless @request && @request.path.present? + + case @request.request_method + when 'GET' + get_response + when 'POST' + post_response + when 'PUT' + put_response + else + nil + end + end + + private + + def get_response + path_match = @request.path.match(/\/(info\/lfs|gitlab-lfs)\/objects\/([0-9a-f]{64})$/) + return nil unless path_match + + oid = path_match[2] + return nil unless oid + + case path_match[1] + when "info/lfs" + lfs.render_download_hypermedia_response(oid) + when "gitlab-lfs" + lfs.render_download_object_response(oid) + else + nil + end + end + + def post_response + post_path = @request.path.match(/\/info\/lfs\/objects(\/batch)?$/) + return nil unless post_path + + # Check for Batch API + if post_path[0].ends_with?("/info/lfs/objects/batch") + lfs.render_lfs_api_auth + else + nil + end + end + + def put_response + object_match = @request.path.match(/\/gitlab-lfs\/objects\/([0-9a-f]{64})\/([0-9]+)(|\/authorize){1}$/) + return nil if object_match.nil? + + oid = object_match[1] + size = object_match[2].try(:to_i) + return nil if oid.nil? || size.nil? + + # GitLab-workhorse requests + # 1. Try to authorize the request + # 2. send a request with a header containing the name of the temporary file + if object_match[3] && object_match[3] == '/authorize' + lfs.render_storage_upload_authorize_response(oid, size) + else + tmp_file_name = sanitize_tmp_filename(@request.env['HTTP_X_GITLAB_LFS_TMP']) + return nil unless tmp_file_name + + lfs.render_storage_upload_store_response(oid, size, tmp_file_name) + end + end + + def lfs + return unless @project + + Gitlab::Lfs::Response.new(@project, @user, @request) + end + + def sanitize_tmp_filename(name) + if name.present? + name.gsub!(/^.*(\\|\/)/, '') + name = name.match(/[0-9a-f]{73}/) + name[0] if name + else + nil + end + end + end + end +end diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb new file mode 100644 index 00000000000..fd5b7eb9332 --- /dev/null +++ b/lib/gitlab/markdown/abstract_reference_filter.rb @@ -0,0 +1,100 @@ +require 'gitlab/markdown' + +module Gitlab + module Markdown + # Issues, Snippets and Merge Requests shares similar functionality in refernce filtering. + # All this functionality moved to this class + class AbstractReferenceFilter < ReferenceFilter + include CrossProjectReference + + def self.object_class + # Implement in child class + # Example: MergeRequest + end + + def self.object_name + object_class.name.underscore + end + + def self.object_sym + object_name.to_sym + end + + def self.data_reference + "data-#{object_name.dasherize}" + end + + # Public: Find references in text (like `!123` for merge requests) + # + # AnyReferenceFilter.references_in(text) do |match, object| + # "<a href=...>PREFIX#{object}</a>" + # end + # + # PREFIX - symbol that detects reference (like ! for merge requests) + # object - reference object (snippet, merget request etc) + # text - String text to search. + # + # Yields the String match, the Integer referenced object ID, and an optional String + # of the external project reference. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(object_class.reference_pattern) do |match| + yield match, $~[object_sym].to_i, $~[:project] + end + end + + def self.referenced_by(node) + { object_sym => LazyReference.new(object_class, node.attr(data_reference)) } + end + + delegate :object_class, :object_sym, :references_in, to: :class + + def find_object(project, id) + # Implement in child class + # Example: project.merge_requests.find + end + + def url_for_object(object, project) + # Implement in child class + # Example: project_merge_request_url + end + + def call + replace_text_nodes_matching(object_class.reference_pattern) do |content| + object_link_filter(content) + end + end + + # Replace references (like `!123` for merge requests) in text with links + # to the referenced object's details page. + # + # text - String text to replace references in. + # + # Returns a String with references replaced with links. All links + # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. + def object_link_filter(text) + references_in(text) do |match, id, project_ref| + project = project_from_ref(project_ref) + + if project && object = find_object(project, id) + title = escape_once("#{object_title}: #{object.title}") + klass = reference_class(object_sym) + data = data_attribute(project: project.id, object_sym => object.id) + url = url_for_object(object, project) + + %(<a href="#{url}" #{data} + title="#{title}" + class="#{klass}">#{match}</a>) + else + match + end + end + end + + def object_title + object_class.name.titleize + end + end + end +end diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb index 481d282f7b1..1ed69e2f431 100644 --- a/lib/gitlab/markdown/issue_reference_filter.rb +++ b/lib/gitlab/markdown/issue_reference_filter.rb @@ -6,66 +6,17 @@ module Gitlab # issues that do not exist are ignored. # # This filter supports cross-project references. - class IssueReferenceFilter < ReferenceFilter - include CrossProjectReference - - # Public: Find `#123` issue references in text - # - # IssueReferenceFilter.references_in(text) do |match, issue, project_ref| - # "<a href=...>##{issue}</a>" - # end - # - # text - String text to search. - # - # Yields the String match, the Integer issue ID, and an optional String of - # the external project reference. - # - # Returns a String replaced with the return of the block. - def self.references_in(text) - text.gsub(Issue.reference_pattern) do |match| - yield match, $~[:issue].to_i, $~[:project] - end - end - - def self.referenced_by(node) - { issue: LazyReference.new(Issue, node.attr("data-issue")) } - end - - def call - replace_text_nodes_matching(Issue.reference_pattern) do |content| - issue_link_filter(content) - end + class IssueReferenceFilter < AbstractReferenceFilter + def self.object_class + Issue end - # Replace `#123` issue references in text with links to the referenced - # issue's details page. - # - # text - String text to replace references in. - # - # Returns a String with `#123` references replaced with links. All links - # have `gfm` and `gfm-issue` class names attached for styling. - def issue_link_filter(text) - self.class.references_in(text) do |match, id, project_ref| - project = self.project_from_ref(project_ref) - - if project && issue = project.get_issue(id) - url = url_for_issue(id, project, only_path: context[:only_path]) - - title = escape_once("Issue: #{issue.title}") - klass = reference_class(:issue) - data = data_attribute(project: project.id, issue: issue.id) - - %(<a href="#{url}" #{data} - title="#{title}" - class="#{klass}">#{match}</a>) - else - match - end - end + def find_object(project, id) + project.get_issue(id) end - def url_for_issue(*args) - IssuesHelper.url_for_issue(*args) + def url_for_object(issue, project) + IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path]) end end end diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 5bc63269808..1f47f03c94e 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -6,65 +6,16 @@ module Gitlab # to merge requests that do not exist are ignored. # # This filter supports cross-project references. - class MergeRequestReferenceFilter < ReferenceFilter - include CrossProjectReference - - # Public: Find `!123` merge request references in text - # - # MergeRequestReferenceFilter.references_in(text) do |match, merge_request, project_ref| - # "<a href=...>##{merge_request}</a>" - # end - # - # text - String text to search. - # - # Yields the String match, the Integer merge request ID, and an optional - # String of the external project reference. - # - # Returns a String replaced with the return of the block. - def self.references_in(text) - text.gsub(MergeRequest.reference_pattern) do |match| - yield match, $~[:merge_request].to_i, $~[:project] - end - end - - def self.referenced_by(node) - { merge_request: LazyReference.new(MergeRequest, node.attr("data-merge-request")) } - end - - def call - replace_text_nodes_matching(MergeRequest.reference_pattern) do |content| - merge_request_link_filter(content) - end + class MergeRequestReferenceFilter < AbstractReferenceFilter + def self.object_class + MergeRequest end - # Replace `!123` merge request references in text with links to the - # referenced merge request's details page. - # - # text - String text to replace references in. - # - # Returns a String with `!123` references replaced with links. All links - # have `gfm` and `gfm-merge_request` class names attached for styling. - def merge_request_link_filter(text) - self.class.references_in(text) do |match, id, project_ref| - project = self.project_from_ref(project_ref) - - if project && merge_request = project.merge_requests.find_by(iid: id) - title = escape_once("Merge Request: #{merge_request.title}") - klass = reference_class(:merge_request) - data = data_attribute(project: project.id, merge_request: merge_request.id) - - url = url_for_merge_request(merge_request, project) - - %(<a href="#{url}" #{data} - title="#{title}" - class="#{klass}">#{match}</a>) - else - match - end - end + def find_object(project, id) + project.merge_requests.find_by(iid: id) end - def url_for_merge_request(mr, project) + def url_for_object(mr, project) h = Gitlab::Application.routes.url_helpers h.namespace_project_merge_request_url(project.namespace, project, mr, only_path: context[:only_path]) diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb index 6ee3d1ce039..632be4d7542 100644 --- a/lib/gitlab/markdown/relative_link_filter.rb +++ b/lib/gitlab/markdown/relative_link_filter.rb @@ -51,7 +51,7 @@ module Gitlab relative_url_root, context[:project].path_with_namespace, path_type(file_path), - ref || 'master', # assume that if no ref exists we can point to master + ref || context[:project].default_branch, # if no ref exists, point to the default branch file_path ].compact.join('/').squeeze('/').chomp('/') diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb index f783f951711..f7bd07c2a34 100644 --- a/lib/gitlab/markdown/snippet_reference_filter.rb +++ b/lib/gitlab/markdown/snippet_reference_filter.rb @@ -6,65 +6,16 @@ module Gitlab # snippets that do not exist are ignored. # # This filter supports cross-project references. - class SnippetReferenceFilter < ReferenceFilter - include CrossProjectReference - - # Public: Find `$123` snippet references in text - # - # SnippetReferenceFilter.references_in(text) do |match, snippet| - # "<a href=...>$#{snippet}</a>" - # end - # - # text - String text to search. - # - # Yields the String match, the Integer snippet ID, and an optional String - # of the external project reference. - # - # Returns a String replaced with the return of the block. - def self.references_in(text) - text.gsub(Snippet.reference_pattern) do |match| - yield match, $~[:snippet].to_i, $~[:project] - end - end - - def self.referenced_by(node) - { snippet: LazyReference.new(Snippet, node.attr("data-snippet")) } - end - - def call - replace_text_nodes_matching(Snippet.reference_pattern) do |content| - snippet_link_filter(content) - end + class SnippetReferenceFilter < AbstractReferenceFilter + def self.object_class + Snippet end - # Replace `$123` snippet references in text with links to the referenced - # snippets's details page. - # - # text - String text to replace references in. - # - # Returns a String with `$123` references replaced with links. All links - # have `gfm` and `gfm-snippet` class names attached for styling. - def snippet_link_filter(text) - self.class.references_in(text) do |match, id, project_ref| - project = self.project_from_ref(project_ref) - - if project && snippet = project.snippets.find_by(id: id) - title = escape_once("Snippet: #{snippet.title}") - klass = reference_class(:snippet) - data = data_attribute(project: project.id, snippet: snippet.id) - - url = url_for_snippet(snippet, project) - - %(<a href="#{url}" #{data} - title="#{title}" - class="#{klass}">#{match}</a>) - else - match - end - end + def find_object(project, id) + project.snippets.find_by(id: id) end - def url_for_snippet(snippet, project) + def url_for_object(snippet, project) h = Gitlab::Application.routes.url_helpers h.namespace_project_snippet_url(project.namespace, project, snippet, only_path: context[:only_path]) diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index 2a594e1662e..ab5e1f6fe9e 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -85,13 +85,12 @@ module Gitlab def link_to_all project = context[:project] - url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) data = data_attribute(project: project.id) - text = User.reference_prefix + 'all' - %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) + + link_tag(url, data, text) end def link_to_namespace(namespace) @@ -105,16 +104,20 @@ module Gitlab def link_to_group(group, namespace) url = urls.group_url(group, only_path: context[:only_path]) data = data_attribute(group: namespace.id) - text = Group.reference_prefix + group - %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) + + link_tag(url, data, text) end def link_to_user(user, namespace) url = urls.user_url(user, only_path: context[:only_path]) data = data_attribute(user: namespace.owner_id) - text = User.reference_prefix + user + + link_tag(url, data, text) + end + + def link_tag(url, data, text) %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) end end diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb new file mode 100644 index 00000000000..1cd89b3a9c4 --- /dev/null +++ b/lib/gitlab/sql/union.rb @@ -0,0 +1,34 @@ +module Gitlab + module SQL + # Class for building SQL UNION statements. + # + # ORDER BYs are dropped from the relations as the final sort order is not + # guaranteed any way. + # + # Example usage: + # + # union = Gitlab::SQL::Union.new(user.personal_projects, user.projects) + # sql = union.to_sql + # + # Project.where("id IN (#{sql})") + class Union + def initialize(relations) + @relations = relations + end + + def to_sql + # Some relations may include placeholders for prepared statements, these + # aren't incremented properly when joining relations together this way. + # By using "unprepared_statements" we remove the usage of placeholders + # (thus fixing this problem), at a slight performance cost. + fragments = ActiveRecord::Base.connection.unprepared_statement do + @relations.map do |rel| + rel.reorder(nil).to_sql + end + end + + fragments.join("\nUNION\n") + end + end + end +end diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index e767027dc29..acc547afcce 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -44,7 +44,7 @@ upstream gitlab-workhorse { ## Normal HTTP host server { - ## Either remove "default_server" from the listen line below, + ## Either remove "default_server" from the listen line below, ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab ## to be served if you visit any address that your server responds to, eg. ## the ip address of the server (http://x.x.x.x/)n 0.0.0.0:80 default_server; @@ -67,7 +67,7 @@ server { location / { ## Serve static files from defined root folder. ## @gitlab is a named location for the upstream fallback, see below. - try_files $uri $uri/index.html $uri.html @gitlab; + try_files $uri /index.html $uri.html @gitlab; } ## We route uploads through GitLab to prevent XSS and enforce access control. @@ -113,6 +113,13 @@ server { proxy_pass http://gitlab; } + location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects { + client_max_body_size 0; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; + } + location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; @@ -131,6 +138,22 @@ server { return 418; } + # Build artifacts should be submitted to this location + location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { + client_max_body_size 0; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; + } + + # Build artifacts should be submitted to this location + location ~ /ci/api/v1/builds/[0-9]+/artifacts { + client_max_body_size 0; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; + } + location @gitlab-workhorse { ## If you use HTTPS make sure you disable gzip compression ## to be safe against BREACH attack. diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 4d31e31f8d5..a8f8d48eb1f 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -48,7 +48,7 @@ upstream gitlab-workhorse { ## Redirects all HTTP traffic to the HTTPS host server { - ## Either remove "default_server" from the listen line below, + ## Either remove "default_server" from the listen line below, ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab ## to be served if you visit any address that your server responds to, eg. ## the ip address of the server (http://x.x.x.x/) @@ -112,7 +112,7 @@ server { location / { ## Serve static files from defined root folder. ## @gitlab is a named location for the upstream fallback, see below. - try_files $uri $uri/index.html $uri.html @gitlab; + try_files $uri /index.html $uri.html @gitlab; } ## We route uploads through GitLab to prevent XSS and enforce access control. @@ -160,6 +160,13 @@ server { proxy_pass http://gitlab; } + location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects { + client_max_body_size 0; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; + } + location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; @@ -178,6 +185,22 @@ server { return 418; } + # Build artifacts should be submitted to this location + location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { + client_max_body_size 0; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; + } + + # Build artifacts should be submitted to this location + location ~ /ci/api/v1/builds/[0-9]+/artifacts { + client_max_body_size 0; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; + } + location @gitlab-workhorse { ## If you use HTTPS make sure you disable gzip compression ## to be safe against BREACH attack. diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake new file mode 100644 index 00000000000..e9587595fef --- /dev/null +++ b/lib/tasks/flay.rake @@ -0,0 +1,9 @@ +desc 'Code duplication analyze via flay' +task :flay do + output = %x(bundle exec flay --mass 35 app/ lib/gitlab/) + + if output.include? "Similar code found" + puts output + exit 1 + end +end diff --git a/lib/tasks/flog.rake b/lib/tasks/flog.rake new file mode 100644 index 00000000000..3bfe999ae74 --- /dev/null +++ b/lib/tasks/flog.rake @@ -0,0 +1,25 @@ +desc 'Code complexity analyze via flog' +task :flog do + output = %x(bundle exec flog -m app/ lib/gitlab) + exit_code = 0 + minimum_score = 70 + output = output.lines + + # Skip total complexity score + output.shift + + # Skip some trash info + output.shift + + output.each do |line| + score, method = line.split(" ") + score = score.to_i + + if score > minimum_score + exit_code = 1 + puts "High complexity in #{method}. Score: #{score}" + end + end + + exit exit_code +end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index f20c7f71ba5..3c46bcea40e 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -12,6 +12,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:repo:create"].invoke Rake::Task["gitlab:backup:uploads:create"].invoke Rake::Task["gitlab:backup:builds:create"].invoke + Rake::Task["gitlab:backup:artifacts:create"].invoke backup = Backup::Manager.new backup.pack @@ -32,6 +33,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories") Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads") Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds") + Rake::Task["gitlab:backup:artifacts:restore"].invoke unless backup.skipped?("artifacts") Rake::Task["gitlab:shell:setup"].invoke backup.cleanup @@ -113,6 +115,25 @@ namespace :gitlab do end end + namespace :artifacts do + task create: :environment do + $progress.puts "Dumping artifacts ... ".blue + + if ENV["SKIP"] && ENV["SKIP"].include?("artifacts") + $progress.puts "[SKIPPED]".cyan + else + Backup::Artifacts.new.dump + $progress.puts "done".green + end + end + + task restore: :environment do + $progress.puts "Restoring artifacts ... ".blue + Backup::Artifacts.new.restore + $progress.puts "done".green + end + end + def configure_cron_mode if ENV['CRON'] # We need an object we can say 'puts' and 'print' to; let's use a diff --git a/lib/tasks/grape.rake b/lib/tasks/grape.rake new file mode 100644 index 00000000000..9980e0b7984 --- /dev/null +++ b/lib/tasks/grape.rake @@ -0,0 +1,8 @@ +namespace :grape do + desc 'Print compiled grape routes' + task routes: :environment do + API::API.routes.each do |route| + puts route + end + end +end diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb new file mode 100644 index 00000000000..d4291f012d3 --- /dev/null +++ b/lib/uploaded_file.rb @@ -0,0 +1,37 @@ +require "tempfile" +require "fileutils" + +# Taken from: Rack::Test::UploadedFile +class UploadedFile + + # The filename, *not* including the path, of the "uploaded" file + attr_reader :original_filename + + # The tempfile + attr_reader :tempfile + + # The content type of the "uploaded" file + attr_accessor :content_type + + def initialize(path, filename, content_type = "text/plain") + raise "#{path} file does not exist" unless ::File.exist?(path) + + @content_type = content_type + @original_filename = filename || ::File.basename(path) + @tempfile = File.new(path, 'rb') + end + + def path + @tempfile.path + end + + alias_method :local_path, :path + + def method_missing(method_name, *args, &block) #:nodoc: + @tempfile.__send__(method_name, *args, &block) + end + + def respond_to?(method_name, include_private = false) #:nodoc: + @tempfile.respond_to?(method_name, include_private) || super + end +end diff --git a/shared/artifacts/.gitkeep b/shared/artifacts/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/shared/artifacts/.gitkeep diff --git a/shared/artifacts/tmp/cache/.gitkeep b/shared/artifacts/tmp/cache/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/shared/artifacts/tmp/cache/.gitkeep diff --git a/shared/artifacts/tmp/uploads/.gitkeep b/shared/artifacts/tmp/uploads/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/shared/artifacts/tmp/uploads/.gitkeep diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index fcbe62cace8..8b7af4d3a0a 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -7,21 +7,6 @@ describe Admin::UsersController do sign_in(admin) end - describe 'POST login_as' do - let(:user) { create(:user) } - - it 'logs admin as another user' do - expect(warden.authenticate(scope: :user)).not_to eq(user) - post :login_as, id: user.username - expect(warden.authenticate(scope: :user)).to eq(user) - end - - it 'redirects user to homepage' do - post :login_as, id: user.username - expect(response).to redirect_to(root_path) - end - end - describe 'DELETE #user with projects' do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 2a447248b70..be19f1abc53 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -23,6 +23,22 @@ describe Projects::CompareController do expect(assigns(:commits).length).to be >= 1 end + it 'compare should show some diffs with ignore whitespace change option' do + get(:show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: '08f22f25', + to: '66eceea0', + w: 1) + + expect(response).to be_success + expect(assigns(:diffs).length).to be >= 1 + expect(assigns(:commits).length).to be >= 1 + # without whitespace option, there are more than 2 diff_splits + diff_splits = assigns(:diffs)[0].diff.split("\n") + expect(diff_splits.length).to be <= 2 + end + describe 'non-existent refs' do it 'invalid source ref' do get(:show, diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index b8db8591709..3e5e1fa87ae 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -147,6 +147,34 @@ describe Projects::MergeRequestsController do end end + describe 'GET diffs with ignore_whitespace_change' do + def go(format: 'html') + get :diffs, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: merge_request.iid, + format: format, + w: 1 + end + + context 'as html' do + it 'renders the diff template' do + go + + expect(response).to render_template('diffs') + end + end + + context 'as json' do + it 'renders the diffs template to a string' do + go format: 'json' + + expect(response).to render_template('projects/merge_requests/show/_diffs') + expect(JSON.parse(response.body)).to have_key('html') + end + end + end + describe 'GET commits' do def go(format: 'html') get :commits, diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 9f89101d7f7..104a5f50143 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -16,13 +16,26 @@ describe UsersController do context 'with rendered views' do render_views - it 'renders the show template' do - sign_in(user) + describe 'when logged in' do + before do + sign_in(user) + end - get :show, username: user.username + it 'renders the show template' do + get :show, username: user.username - expect(response).to be_success - expect(response).to render_template('show') + expect(response).to be_success + expect(response).to render_template('show') + end + end + + describe 'when logged out' do + it 'renders the show template' do + get :show, username: user.username + + expect(response).to be_success + expect(response).to render_template('show') + end end end end diff --git a/spec/factories/ci/projects.rb b/spec/factories/ci/projects.rb index 1183a190353..11cb8c9eeaa 100644 --- a/spec/factories/ci/projects.rb +++ b/spec/factories/ci/projects.rb @@ -31,16 +31,20 @@ FactoryGirl.define do factory :ci_project_without_token, class: Ci::Project do default_ref 'master' - gl_project factory: :empty_project - shared_runners_enabled false factory :ci_project do token 'iPWx6WM4lhHNedGfBpPJNP' end - factory :ci_public_project do - public true + initialize_with do + # TODO: + # this is required, because builds_enabled is initialized when Project is created + # and this create gitlab_ci_project if builds is set to true + # here we take created gitlab_ci_project and update it's attributes + ci_project = create(:empty_project).ensure_gitlab_ci_project + ci_project.update_attributes(attributes) + ci_project end end end diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb index 6829387c660..8b12ee11af5 100644 --- a/spec/factories/labels.rb +++ b/spec/factories/labels.rb @@ -8,6 +8,7 @@ # project_id :integer # created_at :datetime # updated_at :datetime +# template :boolean default(FALSE) # # Read about factories at https://github.com/thoughtbot/factory_girl diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb new file mode 100644 index 00000000000..7fb2d77ca32 --- /dev/null +++ b/spec/factories/lfs_objects.rb @@ -0,0 +1,12 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :lfs_object do + oid "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" + size 499013 + end + + trait :with_file do + file { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } + end +end diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb new file mode 100644 index 00000000000..93de6607df8 --- /dev/null +++ b/spec/factories/lfs_objects_projects.rb @@ -0,0 +1,8 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :lfs_objects_project do + lfs_object + project + end +end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 6080d0ccdef..729a49c9f72 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -20,6 +20,7 @@ # position :integer default(0) # locked_at :datetime # updated_by_id :integer +# merge_error :string(255) # FactoryGirl.define do diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb index 80d6bbee6c7..43d09b17534 100644 --- a/spec/factories/releases.rb +++ b/spec/factories/releases.rb @@ -1,3 +1,15 @@ +# == Schema Information +# +# Table name: releases +# +# id :integer not null, primary key +# tag :string(255) +# description :text +# project_id :integer +# created_at :datetime +# updated_at :datetime +# + # Read about factories at https://github.com/thoughtbot/factory_girl FactoryGirl.define do diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index c2c7364f6c5..4c756a8e732 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -111,24 +111,50 @@ describe "Admin::Users", feature: true do expect(page).to have_content(@user.name) end - describe 'Login as another user' do - it 'should show login button for other users and check that it works' do - another_user = create(:user) + describe 'Impersonation' do + let(:another_user) { create(:user) } + before { visit admin_user_path(another_user) } - visit admin_user_path(another_user) - - click_link 'Log in as this user' + context 'before impersonating' do + it 'shows impersonate button for other users' do + expect(page).to have_content('Impersonate') + end - expect(page).to have_content("Logged in as #{another_user.username}") + it 'should not show impersonate button for admin itself' do + visit admin_user_path(@user) - page.within '.sidebar-user .username' do - expect(page).to have_content(another_user.username) + expect(page).not_to have_content('Impersonate') end end - it 'should not show login button for admin itself' do - visit admin_user_path(@user) - expect(page).not_to have_content('Log in as this user') + context 'when impersonating' do + before { click_link 'Impersonate' } + + it 'logs in as the user when impersonate is clicked' do + page.within '.sidebar-user .username' do + expect(page).to have_content(another_user.username) + end + end + + it 'sees impersonation log out icon' do + icon = first('.fa.fa-user-secret') + + expect(icon).to_not eql nil + end + + it 'can log out of impersonated user back to original user' do + find(:css, 'li.impersonation a').click + + page.within '.sidebar-user .username' do + expect(page).to have_content(@user.username) + end + end + + it 'is redirected back to the impersonated users page in the admin after stopping' do + find(:css, 'li.impersonation a').click + + expect(current_path).to eql "/admin/users/#{another_user.username}" + end end end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 158e85e598f..5213ce1099f 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe "Builds" do + let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + before do login_as(:user) @commit = FactoryGirl.create :ci_commit @@ -66,6 +68,15 @@ describe "Builds" do it { expect(page).to have_content @commit.sha[0..7] } it { expect(page).to have_content @commit.git_commit_message } it { expect(page).to have_content @commit.git_author_name } + + context "Download artifacts" do + before do + @build.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + end + + it { expect(page).to have_content 'Download artifacts' } + end end describe "POST /:project/builds/:id/cancel" do @@ -90,4 +101,14 @@ describe "Builds" do it { expect(page).to have_content 'pending' } it { expect(page).to have_content 'Cancel' } end + + describe "GET /:project/builds/:id/download" do + before do + @build.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + click_link 'Download artifacts' + end + + it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } + end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 340924fafe7..90739cd6a28 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -19,7 +19,7 @@ describe "Commits" do stub_ci_commit_to_return_yaml_file end - describe "GET /:project/commits/:sha" do + describe "GET /:project/commits/:sha/ci" do before do visit ci_status_path(@commit) end @@ -29,6 +29,20 @@ describe "Commits" do it { expect(page).to have_content @commit.git_author_name } end + context "Download artifacts" do + let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + + before do + @build.update_attributes(artifacts_file: artifacts_file) + end + + it do + visit ci_status_path(@commit) + click_on "Download artifacts" + expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) + end + end + describe "Cancel all builds" do it "cancels commit" do visit ci_status_path(@commit) diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 06adb7633b2..b0259026630 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -14,15 +14,25 @@ describe "Runners" do @project2 = FactoryGirl.create :ci_project @project2.gl_project.team << [user, :master] + @project3 = FactoryGirl.create :ci_project + @project3.gl_project.team << [user, :developer] + @shared_runner = FactoryGirl.create :ci_shared_runner @specific_runner = FactoryGirl.create :ci_specific_runner @specific_runner2 = FactoryGirl.create :ci_specific_runner + @specific_runner3 = FactoryGirl.create :ci_specific_runner @project.runners << @specific_runner @project2.runners << @specific_runner2 + @project3.runners << @specific_runner3 visit runners_path(@project.gl_project) end + before do + expect(page).to_not have_content(@specific_runner3.display_name) + expect(page).to_not have_content(@specific_runner3.display_name) + end + it "places runners in right places" do expect(page.find(".available-specific-runners")).to have_content(@specific_runner2.display_name) expect(page.find(".activated-specific-runners")).to have_content(@specific_runner.display_name) @@ -76,10 +86,10 @@ describe "Runners" do @project.gl_project.team << [user, :master] @specific_runner = FactoryGirl.create :ci_specific_runner @project.runners << @specific_runner - visit runners_path(@project.gl_project) end it "shows runner information" do + visit runners_path(@project.gl_project) click_on @specific_runner.short_sha expect(page).to have_content(@specific_runner.platform) end diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb new file mode 100644 index 00000000000..65d7f14c721 --- /dev/null +++ b/spec/finders/contributed_projects_finder_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe ContributedProjectsFinder do + let(:source_user) { create(:user) } + let(:current_user) { create(:user) } + + let(:finder) { described_class.new(source_user) } + + let!(:public_project) { create(:project, :public) } + let!(:private_project) { create(:project, :private) } + + before do + private_project.team << [source_user, Gitlab::Access::MASTER] + private_project.team << [current_user, Gitlab::Access::DEVELOPER] + public_project.team << [source_user, Gitlab::Access::MASTER] + + create(:event, action: Event::PUSHED, project: public_project, + target: public_project, author: source_user) + + create(:event, action: Event::PUSHED, project: private_project, + target: private_project, author: source_user) + end + + describe 'without a current user' do + subject { finder.execute } + + it { is_expected.to eq([public_project]) } + end + + describe 'with a current user' do + subject { finder.execute(current_user) } + + it { is_expected.to eq([private_project, public_project]) } + end +end diff --git a/spec/finders/group_finder_spec.rb b/spec/finders/group_finder_spec.rb deleted file mode 100644 index 78dc027837c..00000000000 --- a/spec/finders/group_finder_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe GroupsFinder do - let(:user) { create :user } - let!(:group) { create :group } - let!(:public_group) { create :group, public: true } - - describe :execute do - it 'finds public group' do - groups = GroupsFinder.new.execute(user) - expect(groups.size).to eq(1) - expect(groups.first).to eq(public_group) - end - end -end diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb new file mode 100644 index 00000000000..4f6a000822e --- /dev/null +++ b/spec/finders/groups_finder_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe GroupsFinder do + describe '#execute' do + let(:user) { create(:user) } + + let(:group1) { create(:group) } + let(:group2) { create(:group) } + let(:group3) { create(:group) } + let(:group4) { create(:group, public: true) } + + let!(:public_project) { create(:project, :public, group: group1) } + let!(:internal_project) { create(:project, :internal, group: group2) } + let!(:private_project) { create(:project, :private, group: group3) } + + let(:finder) { described_class.new } + + describe 'with a user' do + subject { finder.execute(user) } + + describe 'when the user is not a member of any groups' do + it { is_expected.to eq([group4, group2, group1]) } + end + + describe 'when the user is a member of a group' do + before do + group3.add_user(user, Gitlab::Access::DEVELOPER) + end + + it { is_expected.to eq([group4, group3, group2, group1]) } + end + + describe 'when the user is a member of a private project' do + before do + private_project.team.add_user(user, Gitlab::Access::DEVELOPER) + end + + it { is_expected.to eq([group4, group3, group2, group1]) } + end + end + + describe 'without a user' do + subject { finder.execute } + + it { is_expected.to eq([group4, group1]) } + end + end +end diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb new file mode 100644 index 00000000000..2d9068cc720 --- /dev/null +++ b/spec/finders/joined_groups_finder_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe JoinedGroupsFinder do + describe '#execute' do + let(:source_user) { create(:user) } + let(:current_user) { create(:user) } + + let(:group1) { create(:group) } + let(:group2) { create(:group) } + let(:group3) { create(:group) } + let(:group4) { create(:group, public: true) } + + let!(:public_project) { create(:project, :public, group: group1) } + let!(:internal_project) { create(:project, :internal, group: group2) } + let!(:private_project) { create(:project, :private, group: group3) } + + let(:finder) { described_class.new(source_user) } + + before do + [group1, group2, group3, group4].each do |group| + group.add_user(source_user, Gitlab::Access::MASTER) + end + end + + describe 'with a current user' do + describe 'when the current user has access to the projects of the source user' do + before do + private_project.team.add_user(current_user, Gitlab::Access::DEVELOPER) + end + + subject { finder.execute(current_user) } + + it { is_expected.to eq([group4, group3, group2, group1]) } + end + + describe 'when the current user does not have access to the projects of the source user' do + subject { finder.execute(current_user) } + + it { is_expected.to eq([group4, group2, group1]) } + end + end + + describe 'without a current user' do + subject { finder.execute } + + it { is_expected.to eq([group4, group1]) } + end + end +end diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb new file mode 100644 index 00000000000..38817add456 --- /dev/null +++ b/spec/finders/personal_projects_finder_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe PersonalProjectsFinder do + let(:source_user) { create(:user) } + let(:current_user) { create(:user) } + + let(:finder) { described_class.new(source_user) } + + let!(:public_project) do + create(:project, :public, namespace: source_user.namespace, name: 'A', + path: 'A') + end + + let!(:private_project) do + create(:project, :private, namespace: source_user.namespace, name: 'B', + path: 'B') + end + + before do + private_project.team << [current_user, Gitlab::Access::DEVELOPER] + end + + describe 'without a current user' do + subject { finder.execute } + + it { is_expected.to eq([public_project]) } + end + + describe 'with a current user' do + subject { finder.execute(current_user) } + + it { is_expected.to eq([private_project, public_project]) } + end +end diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index de9d4cd6128..d1dede78f74 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -1,51 +1,56 @@ require 'spec_helper' describe ProjectsFinder do - let(:user) { create :user } - let(:group) { create :group } + describe '#execute' do + let(:user) { create(:user) } - let(:project1) { create(:empty_project, :public, group: group) } - let(:project2) { create(:empty_project, :internal, group: group) } - let(:project3) { create(:empty_project, :private, group: group) } - let(:project4) { create(:empty_project, :private, group: group) } + let!(:private_project) { create(:project, :private) } + let!(:internal_project) { create(:project, :internal) } + let!(:public_project) { create(:project, :public) } - context 'non authenticated' do - subject { ProjectsFinder.new.execute(nil, group: group) } + let(:finder) { described_class.new } - it { is_expected.to include(project1) } - it { is_expected.not_to include(project2) } - it { is_expected.not_to include(project3) } - it { is_expected.not_to include(project4) } - end + describe 'without a group' do + describe 'without a user' do + subject { finder.execute } - context 'authenticated' do - subject { ProjectsFinder.new.execute(user, group: group) } + it { is_expected.to eq([public_project]) } + end - it { is_expected.to include(project1) } - it { is_expected.to include(project2) } - it { is_expected.not_to include(project3) } - it { is_expected.not_to include(project4) } - end + describe 'with a user' do + subject { finder.execute(user) } - context 'authenticated, project member' do - before { project3.team << [user, :developer] } + describe 'without private projects' do + it { is_expected.to eq([public_project, internal_project]) } + end - subject { ProjectsFinder.new.execute(user, group: group) } + describe 'with private projects' do + before do + private_project.team.add_user(user, Gitlab::Access::MASTER) + end - it { is_expected.to include(project1) } - it { is_expected.to include(project2) } - it { is_expected.to include(project3) } - it { is_expected.not_to include(project4) } - end + it do + is_expected.to eq([public_project, internal_project, + private_project]) + end + end + end + end + + describe 'with a group' do + let(:group) { public_project.group } + + describe 'without a user' do + subject { finder.execute(nil, group: group) } - context 'authenticated, group member' do - before { group.add_developer(user) } + it { is_expected.to eq([public_project]) } + end - subject { ProjectsFinder.new.execute(user, group: group) } + describe 'with a user' do + subject { finder.execute(user, group: group) } - it { is_expected.to include(project1) } - it { is_expected.to include(project2) } - it { is_expected.to include(project3) } - it { is_expected.to include(project4) } + it { is_expected.to eq([public_project, internal_project]) } + end + end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 9963f76f993..7d90f9877c6 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -333,6 +333,97 @@ module Ci end end + describe "Caches" do + it "returns cache when defined globally" do + config = YAML.dump({ + cache: { paths: ["logs/", "binaries/"], untracked: true }, + rspec: { + script: "rspec" + } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + paths: ["logs/", "binaries/"], + untracked: true, + ) + end + + it "returns cache when defined in a job" do + config = YAML.dump({ + rspec: { + cache: { paths: ["logs/", "binaries/"], untracked: true }, + script: "rspec" + } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + paths: ["logs/", "binaries/"], + untracked: true, + ) + end + + it "overwrite cache when defined for a job and globally" do + config = YAML.dump({ + cache: { paths: ["logs/", "binaries/"], untracked: true }, + rspec: { + script: "rspec", + cache: { paths: ["test/"], untracked: false }, + } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + paths: ["test/"], + untracked: false, + ) + end + end + + describe "Artifacts" do + it "returns artifacts when defined" do + config = YAML.dump({ + image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { + artifacts: { paths: ["logs/", "binaries/"], untracked: true }, + script: "rspec" + } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + except: nil, + stage: "test", + stage_idx: 1, + name: :rspec, + only: nil, + commands: "pwd\nrspec", + tag_list: [], + options: { + image: "ruby:2.1", + services: ["mysql"], + artifacts: { + paths: ["logs/", "binaries/"], + untracked: true + } + }, + when: "on_success", + allow_failure: false + }) + end + end + describe "Error handling" do it "indicates that object is invalid" do expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.to raise_error(GitlabCiYamlProcessor::ValidationError) @@ -491,6 +582,48 @@ module Ci GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always") end + + it "returns errors if job artifacts:untracked is not an array of strings" do + config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:untracked parameter should be an boolean") + end + + it "returns errors if job artifacts:paths is not an array of strings" do + config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:paths parameter should be an array of strings") + end + + it "returns errors if cache:untracked is not an array of strings" do + config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked parameter should be an boolean") + end + + it "returns errors if cache:paths is not an array of strings" do + config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths parameter should be an array of strings") + end + + it "returns errors if job cache:untracked is not an array of strings" do + config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:untracked parameter should be an boolean") + end + + it "returns errors if job cache:paths is not an array of strings" do + config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings") + end end end end diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/inline_diff_spec.rb index 2e0a05088cc..2e0a05088cc 100644 --- a/spec/lib/gitlab/diff/inline_diff_spec.rb +++ b/spec/lib/gitlab/inline_diff_spec.rb diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb new file mode 100644 index 00000000000..cebcb5bc887 --- /dev/null +++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb @@ -0,0 +1,650 @@ +require 'spec_helper' + +describe Gitlab::Lfs::Router do + let(:project) { create(:project) } + let(:public_project) { create(:project, :public) } + let(:forked_project) { fork_project(public_project, user) } + + let(:user) { create(:user) } + let(:user_two) { create(:user) } + let!(:lfs_object) { create(:lfs_object, :with_file) } + + let(:request) { Rack::Request.new(env) } + let(:env) do + { + 'rack.input' => '', + 'REQUEST_METHOD' => 'GET', + } + end + + let(:lfs_router_auth) { new_lfs_router(project, user) } + let(:lfs_router_noauth) { new_lfs_router(project, nil) } + let(:lfs_router_public_auth) { new_lfs_router(public_project, user) } + let(:lfs_router_public_noauth) { new_lfs_router(public_project, nil) } + let(:lfs_router_forked_noauth) { new_lfs_router(forked_project, nil) } + let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user_two) } + + let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } + let(:sample_size) { 499013 } + + describe 'when lfs is disabled' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) + env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" + end + + it 'responds with 501' do + respond_with_disabled = [ 501, + { "Content-Type"=>"application/vnd.git-lfs+json" }, + ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"] + ] + expect(lfs_router_auth.try_call).to match_array(respond_with_disabled) + end + end + + describe 'when fetching lfs object' do + before do + enable_lfs + env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8" + env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" + end + + describe 'when user is authenticated' do + context 'and user has project download access' do + before do + @auth = authorize(user) + env["HTTP_AUTHORIZATION"] = @auth + project.lfs_objects << lfs_object + project.team << [user, :master] + end + + it "responds with status 200" do + expect(lfs_router_auth.try_call.first).to eq(200) + end + + it "responds with download hypermedia" do + json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first) + + expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}") + expect(json_response['_links']['download']['header']).to eq("Authorization" => @auth, "Accept" => "application/vnd.git-lfs+json; charset=utf-8") + end + end + + context 'and user does not have project access' do + it "responds with status 403" do + expect(lfs_router_auth.try_call.first).to eq(403) + end + end + end + + describe 'when user is unauthenticated' do + context 'and user does not have download access' do + it "responds with status 401" do + expect(lfs_router_noauth.try_call.first).to eq(401) + end + end + + context 'and user has download access' do + before do + project.team << [user, :master] + end + + it "responds with status 401" do + expect(lfs_router_noauth.try_call.first).to eq(401) + end + end + end + + describe 'and project is public' do + context 'and project has access to the lfs object' do + before do + public_project.lfs_objects << lfs_object + end + + context 'and user is authenticated' do + it "responds with status 200 and sends download hypermedia" do + expect(lfs_router_public_auth.try_call.first).to eq(200) + json_response = ActiveSupport::JSON.decode(lfs_router_public_auth.try_call.last.first) + + expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{public_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}") + expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8") + end + end + + context 'and user is unauthenticated' do + it "responds with status 200 and sends download hypermedia" do + expect(lfs_router_public_noauth.try_call.first).to eq(200) + json_response = ActiveSupport::JSON.decode(lfs_router_public_noauth.try_call.last.first) + + expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{public_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}") + expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8") + end + end + end + + context 'and project does not have access to the lfs object' do + it "responds with status 404" do + expect(lfs_router_public_auth.try_call.first).to eq(404) + end + end + end + + describe 'and request comes from gitlab-workhorse' do + before do + env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}" + end + context 'without user being authorized' do + it "responds with status 401" do + expect(lfs_router_noauth.try_call.first).to eq(401) + end + end + + context 'with required headers' do + before do + env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile" + end + + context 'when user does not have project access' do + it "responds with status 403" do + expect(lfs_router_auth.try_call.first).to eq(403) + end + end + + context 'when user has project access' do + before do + project.lfs_objects << lfs_object + project.team << [user, :master] + end + + it "responds with status 200" do + expect(lfs_router_auth.try_call.first).to eq(200) + end + + it "responds with the file location" do + expect(lfs_router_auth.try_call[1]['Content-Type']).to eq("application/octet-stream") + expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) + end + end + end + + context 'without required headers' do + it "responds with status 403" do + expect(lfs_router_auth.try_call.first).to eq(403) + end + end + end + + describe 'from a forked public project' do + before do + env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8" + env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" + end + + context "when fetching a lfs object" do + context "and user has project download access" do + before do + public_project.lfs_objects << lfs_object + end + + it "can download the lfs object" do + expect(lfs_router_forked_auth.try_call.first).to eq(200) + json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first) + + expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{forked_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}") + expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8") + end + end + + context "and user is not authenticated but project is public" do + before do + public_project.lfs_objects << lfs_object + end + + it "can download the lfs object" do + expect(lfs_router_forked_auth.try_call.first).to eq(200) + end + end + + context "and user has project download access" do + before do + env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897" + @auth = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) + env["HTTP_AUTHORIZATION"] = @auth + lfs_object_two = create(:lfs_object, :with_file, oid: "91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", size: 1575078) + public_project.lfs_objects << lfs_object_two + end + + it "can get a lfs object that is not in the forked project" do + expect(lfs_router_forked_auth.try_call.first).to eq(200) + + json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first) + expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{forked_project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") + expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8", "Authorization" => @auth) + end + end + + context "and user has project download access" do + before do + env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/267c8b1d876743971e3a9978405818ff5ca731c4c870b06507619cd9b1847b6b" + lfs_object_three = create(:lfs_object, :with_file, oid: "267c8b1d876743971e3a9978405818ff5ca731c4c870b06507619cd9b1847b6b", size: 127192524) + project.lfs_objects << lfs_object_three + end + + it "cannot get a lfs object that is not in the project" do + expect(lfs_router_forked_auth.try_call.first).to eq(404) + end + end + end + end + end + + describe 'when initiating pushing of the lfs object' do + before do + enable_lfs + env['REQUEST_METHOD'] = 'POST' + env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch" + end + + describe 'when user is authenticated' do + before do + body = { 'objects' => [{ + 'oid' => sample_oid, + 'size' => sample_size + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end + + describe 'when user has project push access' do + before do + @auth = authorize(user) + env["HTTP_AUTHORIZATION"] = @auth + project.team << [user, :master] + end + + context 'when pushing an lfs object that already exists' do + before do + public_project.lfs_objects << lfs_object + end + + it "responds with status 200 and links the object to the project" do + response_body = lfs_router_auth.try_call.last + response = ActiveSupport::JSON.decode(response_body.first) + + expect(response['objects']).to be_kind_of(Array) + expect(response['objects'].first['oid']).to eq(sample_oid) + expect(response['objects'].first['size']).to eq(sample_size) + expect(lfs_object.projects.pluck(:id)).to_not include(project.id) + expect(lfs_object.projects.pluck(:id)).to include(public_project.id) + expect(response['objects'].first).to have_key('_links') + end + end + + context 'when pushing a lfs object that does not exist' do + before do + body = { + 'objects' => [{ + 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end + + it "responds with status 200 and upload hypermedia link" do + response = lfs_router_auth.try_call + expect(response.first).to eq(200) + + response_body = ActiveSupport::JSON.decode(response.last.first) + expect(response_body['objects']).to be_kind_of(Array) + expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") + expect(response_body['objects'].first['size']).to eq(1575078) + expect(lfs_object.projects.pluck(:id)).not_to include(project.id) + expect(response_body['objects'].first['_links']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") + expect(response_body['objects'].first['_links']['upload']['header']).to eq("Authorization" => @auth) + end + end + + context 'when pushing one new and one existing lfs object' do + before do + body = { + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ] + }.to_json + env['rack.input'] = StringIO.new(body) + public_project.lfs_objects << lfs_object + end + + it "responds with status 200 with upload hypermedia link for the new object" do + response = lfs_router_auth.try_call + expect(response.first).to eq(200) + + response_body = ActiveSupport::JSON.decode(response.last.first) + expect(response_body['objects']).to be_kind_of(Array) + + + expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") + expect(response_body['objects'].first['size']).to eq(1575078) + expect(response_body['objects'].first['_links']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") + expect(response_body['objects'].first['_links']['upload']['header']).to eq("Authorization" => @auth) + + expect(response_body['objects'].last['oid']).to eq(sample_oid) + expect(response_body['objects'].last['size']).to eq(sample_size) + expect(lfs_object.projects.pluck(:id)).to_not include(project.id) + expect(lfs_object.projects.pluck(:id)).to include(public_project.id) + expect(response_body['objects'].last).to have_key('_links') + end + end + end + + context 'when user does not have push access' do + it 'responds with 403' do + expect(lfs_router_auth.try_call.first).to eq(403) + end + end + end + + context 'when user is not authenticated' do + context 'when user has push access' do + before do + project.team << [user, :master] + end + + it "responds with status 401" do + expect(lfs_router_public_noauth.try_call.first).to eq(401) + end + end + + context 'when user does not have push access' do + it "responds with status 401" do + expect(lfs_router_public_noauth.try_call.first).to eq(401) + end + end + end + end + + describe 'when pushing a lfs object' do + before do + enable_lfs + env['REQUEST_METHOD'] = 'PUT' + end + + describe 'to one project' do + describe 'when user has push access to the project' do + before do + project.team << [user, :master] + end + + describe 'when user is authenticated' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + header_for_upload_authorize(project) + end + + it 'responds with status 200, location of lfs store and object details' do + json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first) + + expect(lfs_router_auth.try_call.first).to eq(200) + expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") + expect(json_response['LfsOid']).to eq(sample_oid) + expect(json_response['LfsSize']).to eq(sample_size) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(project) + end + + it 'responds with status 200 and lfs object is linked to the project' do + expect(lfs_router_auth.try_call.first).to eq(200) + expect(lfs_object.projects.pluck(:id)).to include(project.id) + end + end + end + + describe 'when user is unauthenticated' do + let(:lfs_router_noauth) { new_lfs_router(project, nil) } + + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + header_for_upload_authorize(project) + end + + it 'responds with status 401' do + expect(lfs_router_noauth.try_call.first).to eq(401) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(project) + end + + it 'responds with status 401' do + expect(lfs_router_noauth.try_call.first).to eq(401) + end + end + + context 'and request is sent with a malformed headers' do + before do + env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" + env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd" + end + + it 'does not recognize it as a valid lfs command' do + expect(lfs_router_noauth.try_call).to eq(nil) + end + end + end + end + + describe 'and user does not have push access' do + describe 'when user is authenticated' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + header_for_upload_authorize(project) + end + + it 'responds with 403' do + expect(lfs_router_auth.try_call.first).to eq(403) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(project) + end + + it 'responds with 403' do + expect(lfs_router_auth.try_call.first).to eq(403) + end + end + end + + describe 'when user is unauthenticated' do + let(:lfs_router_noauth) { new_lfs_router(project, nil) } + + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + header_for_upload_authorize(project) + end + + it 'responds with 401' do + expect(lfs_router_noauth.try_call.first).to eq(401) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(project) + end + + it 'responds with 401' do + expect(lfs_router_noauth.try_call.first).to eq(401) + end + end + end + end + end + + describe "to a forked project" do + let(:forked_project) { fork_project(public_project, user) } + + describe 'when user has push access to the project' do + before do + forked_project.team << [user_two, :master] + end + + describe 'when user is authenticated' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + header_for_upload_authorize(forked_project) + end + + it 'responds with status 200, location of lfs store and object details' do + json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first) + + expect(lfs_router_forked_auth.try_call.first).to eq(200) + expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") + expect(json_response['LfsOid']).to eq(sample_oid) + expect(json_response['LfsSize']).to eq(sample_size) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(forked_project) + end + + it 'responds with status 200 and lfs object is linked to the source project' do + expect(lfs_router_forked_auth.try_call.first).to eq(200) + expect(lfs_object.projects.pluck(:id)).to include(public_project.id) + end + end + end + + describe 'when user is unauthenticated' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + header_for_upload_authorize(forked_project) + end + + it 'responds with status 401' do + expect(lfs_router_forked_noauth.try_call.first).to eq(401) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(forked_project) + end + + it 'responds with status 401' do + expect(lfs_router_forked_noauth.try_call.first).to eq(401) + end + end + end + end + + describe 'and user does not have push access' do + describe 'when user is authenticated' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + header_for_upload_authorize(forked_project) + end + + it 'responds with 403' do + expect(lfs_router_forked_auth.try_call.first).to eq(403) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(forked_project) + end + + it 'responds with 403' do + expect(lfs_router_forked_auth.try_call.first).to eq(403) + end + end + end + + describe 'when user is unauthenticated' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + header_for_upload_authorize(forked_project) + end + + it 'responds with 401' do + expect(lfs_router_forked_noauth.try_call.first).to eq(401) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(forked_project) + end + + it 'responds with 401' do + expect(lfs_router_forked_noauth.try_call.first).to eq(401) + end + end + end + end + + describe 'and second project not related to fork or a source project' do + let(:second_project) { create(:project) } + let(:lfs_router_second_project) { new_lfs_router(second_project, user) } + + before do + public_project.lfs_objects << lfs_object + headers_for_upload_finalize(second_project) + end + + context 'when pushing the same lfs object to the second project' do + before do + second_project.team << [user, :master] + end + + it 'responds with 200 and links the lfs object to the project' do + expect(lfs_router_second_project.try_call.first).to eq(200) + expect(lfs_object.projects.pluck(:id)).to include(second_project.id, public_project.id) + end + end + end + end + end + + def enable_lfs + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + end + + def authorize(user) + ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) + end + + def new_lfs_router(project, user) + Gitlab::Lfs::Router.new(project, user, request) + end + + def header_for_upload_authorize(project) + env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize" + end + + def headers_for_upload_finalize(project) + env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" + env["HTTP_X_GITLAB_LFS_TMP"] = "#{sample_oid}6e561c9d4" + end + + def fork_project(project, user, object = nil) + allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) + Projects::ForkService.new(project, user, {}).execute + end +end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb new file mode 100644 index 00000000000..9e1cd4419e0 --- /dev/null +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::SQL::Union do + describe '#to_sql' do + it 'returns a String joining relations together using a UNION' do + rel1 = User.where(email: 'alice@example.com') + rel2 = User.where(email: 'bob@example.com') + union = described_class.new([rel1, rel2]) + + sql1 = rel1.reorder(nil).to_sql + sql2 = rel2.reorder(nil).to_sql + + expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}") + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index f01fe8bd398..dfbac7b4004 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -23,6 +23,10 @@ # after_sign_out_path :string(255) # session_expire_delay :integer default(10080), not null # import_sources :text +# help_page_text :text +# admin_notification_email :string(255) +# shared_runners_enabled :boolean default(TRUE), not null +# max_artifacts_size :integer default(100), not null # require 'spec_helper' diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 7f5abb83ac2..839b4c6b16e 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -400,4 +400,19 @@ describe Ci::Build do end end end + + describe :download_url do + subject { build.download_url } + + it "should be nil if artifact doesn't exist" do + build.update_attributes(artifacts_file: nil) + is_expected.to be_nil + end + + it 'should be nil if artifact exist' do + gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') + build.update_attributes(artifacts_file: gif) + is_expected.to_not be_nil + end + end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 44dbd083f06..a13f6458cac 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -1,18 +1,19 @@ # == Schema Information # -# Table name: commits +# Table name: ci_commits # -# id :integer not null, primary key -# project_id :integer -# ref :string(255) -# sha :string(255) -# before_sha :string(255) -# push_data :text -# created_at :datetime -# updated_at :datetime -# tag :boolean default(FALSE) -# yaml_errors :text -# committed_at :datetime +# id :integer not null, primary key +# project_id :integer +# ref :string(255) +# sha :string(255) +# before_sha :string(255) +# push_data :text +# created_at :datetime +# updated_at :datetime +# tag :boolean default(FALSE) +# yaml_errors :text +# committed_at :datetime +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index 490c6a67982..ac7e38bbcb0 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -1,9 +1,9 @@ # == Schema Information # -# Table name: projects +# Table name: ci_projects # # id :integer not null, primary key -# name :string(255) not null +# name :string(255) # timeout :integer default(3600), not null # created_at :datetime # updated_at :datetime @@ -28,8 +28,8 @@ require 'spec_helper' describe Ci::Project do - let(:gl_project) { FactoryGirl.create :empty_project } - let(:project) { FactoryGirl.create :ci_project, gl_project: gl_project } + let(:project) { FactoryGirl.create :ci_project } + let(:gl_project) { project.gl_project } subject { project } it { is_expected.to have_many(:runner_projects) } @@ -194,18 +194,6 @@ describe Ci::Project do end end - describe 'Project.parse' do - let(:project) { FactoryGirl.create :project } - - subject { Ci::Project.parse(project) } - - it { is_expected.to be_valid } - it { is_expected.to be_kind_of(Ci::Project) } - it { expect(subject.name).to eq(project.name_with_namespace) } - it { expect(subject.gitlab_id).to eq(project.id) } - it { expect(subject.gitlab_url).to eq(project.web_url) } - end - describe :repo_url_with_auth do let(:project) { FactoryGirl.create :ci_project } subject { project.repo_url_with_auth } diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb index 0218d484130..37682c6ea0c 100644 --- a/spec/models/ci/runner_project_spec.rb +++ b/spec/models/ci/runner_project_spec.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: runner_projects +# Table name: ci_runner_projects # # id :integer not null, primary key # runner_id :integer not null diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index f8a51c29dc2..9a1233b9095 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: runners +# Table name: ci_runners # # id :integer not null, primary key # token :string(255) diff --git a/spec/models/ci/service_spec.rb b/spec/models/ci/service_spec.rb index 2df70e88888..36cda988eb4 100644 --- a/spec/models/ci/service_spec.rb +++ b/spec/models/ci/service_spec.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: services +# Table name: ci_services # # id :integer not null, primary key # type :string(255) diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index 19c14ef2da2..b8aa3c1e777 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -1,3 +1,15 @@ +# == Schema Information +# +# Table name: ci_triggers +# +# id :integer not null, primary key +# token :string(255) +# project_id :integer not null +# deleted_at :datetime +# created_at :datetime +# updated_at :datetime +# + require 'spec_helper' describe Ci::Trigger do diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index d034a6c7b9f..a515f5881ff 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: variables +# Table name: ci_variables # # id :integer not null, primary key # project_id :integer not null diff --git a/spec/models/ci/web_hook_spec.rb b/spec/models/ci/web_hook_spec.rb index bf9481ab81d..2865482a212 100644 --- a/spec/models/ci/web_hook_spec.rb +++ b/spec/models/ci/web_hook_spec.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: web_hooks +# Table name: ci_web_hooks # # id :integer not null, primary key # url :string(255) not null diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index c96a606fdaa..dca0715eed8 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -1,3 +1,36 @@ +# == Schema Information +# +# Table name: ci_builds +# +# id :integer not null, primary key +# project_id :integer +# status :string(255) +# finished_at :datetime +# trace :text +# created_at :datetime +# updated_at :datetime +# started_at :datetime +# runner_id :integer +# coverage :float +# commit_id :integer +# commands :text +# job_id :integer +# name :string(255) +# deploy :boolean default(FALSE) +# options :text +# allow_failure :boolean default(FALSE), not null +# stage :string(255) +# trigger_request_id :integer +# stage_idx :integer +# tag :boolean +# ref :string(255) +# user_id :integer +# type :string(255) +# target_url :string(255) +# description :string(255) +# artifacts_file :text +# + require 'spec_helper' describe CommitStatus do diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 0f32f162a10..ae53f7a536b 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -64,4 +64,42 @@ describe Event do it { expect(@event.branch_name).to eq("master") } it { expect(@event.author).to eq(@user) } end + + describe '.latest_update_time' do + describe 'when events are present' do + let(:time) { Time.utc(2015, 1, 1) } + + before do + create(:closed_issue_event, updated_at: time) + create(:closed_issue_event, updated_at: time + 5) + end + + it 'returns the latest update time' do + expect(Event.latest_update_time).to eq(time + 5) + end + end + + describe 'when no events exist' do + it 'returns nil' do + expect(Event.latest_update_time).to be_nil + end + end + end + + describe '.limit_recent' do + let!(:event1) { create(:closed_issue_event) } + let!(:event2) { create(:closed_issue_event) } + + describe 'without an explicit limit' do + subject { Event.limit_recent } + + it { is_expected.to eq([event2, event1]) } + end + + describe 'with an explicit limit' do + subject { Event.limit_recent(1) } + + it { is_expected.to eq([event2]) } + end + end end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index f442fa5fbe5..c86314c454c 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -1,3 +1,36 @@ +# == Schema Information +# +# Table name: ci_builds +# +# id :integer not null, primary key +# project_id :integer +# status :string(255) +# finished_at :datetime +# trace :text +# created_at :datetime +# updated_at :datetime +# started_at :datetime +# runner_id :integer +# coverage :float +# commit_id :integer +# commands :text +# job_id :integer +# name :string(255) +# deploy :boolean default(FALSE) +# options :text +# allow_failure :boolean default(FALSE), not null +# stage :string(255) +# trigger_request_id :integer +# stage_idx :integer +# tag :boolean +# ref :string(255) +# user_id :integer +# type :string(255) +# target_url :string(255) +# description :string(255) +# artifacts_file :text +# + require 'spec_helper' describe GenericCommitStatus do diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb new file mode 100644 index 00000000000..6eeff30b20e --- /dev/null +++ b/spec/models/global_milestone_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe GlobalMilestone do + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:group) { create(:group) } + let(:project1) { create(:project, group: group) } + let(:project2) { create(:project, path: 'gitlab-ci', group: group) } + let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) } + let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } + let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } + let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) } + let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) } + let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) } + let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) } + + describe :build_collection do + before do + milestones = + [ + milestone1_project1, + milestone1_project2, + milestone1_project3, + milestone2_project1, + milestone2_project2, + milestone2_project3 + ] + + @global_milestones = GlobalMilestone.build_collection(milestones) + end + + it 'should have all project milestones' do + expect(@global_milestones.count).to eq(2) + end + + it 'should have all project milestones titles' do + expect(@global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'VD-123']) + end + + it 'should have all project milestones' do + expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6) + end + end + + describe :initialize do + before do + milestones = + [ + milestone1_project1, + milestone1_project2, + milestone1_project3, + ] + + @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones) + end + + it 'should have exactly one group milestone' do + expect(@global_milestone.title).to eq('Milestone v1.2') + end + + it 'should have all project milestones with the same title' do + expect(@global_milestone.milestones.count).to eq(3) + end + end +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 0f23e81ace9..6f166b5ab75 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -11,6 +11,7 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) +# public :boolean default(FALSE) # require 'spec_helper' @@ -37,6 +38,33 @@ describe Group do it { is_expected.not_to validate_presence_of :owner } end + describe '.public_and_given_groups' do + let!(:public_group) { create(:group, public: true) } + + subject { described_class.public_and_given_groups([group.id]) } + + it { is_expected.to eq([public_group, group]) } + end + + describe '.visible_to_user' do + let!(:group) { create(:group) } + let!(:user) { create(:user) } + + subject { described_class.visible_to_user(user) } + + describe 'when the user has access to a group' do + before do + group.add_user(user, Gitlab::Access::MASTER) + end + + it { is_expected.to eq([group]) } + end + + describe 'when the user does not have access to any groups' do + it { is_expected.to eq([]) } + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(group.to_reference).to eq "@#{group.name}" diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 6518213d71c..511ee8cbd96 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -8,6 +8,7 @@ # project_id :integer # created_at :datetime # updated_at :datetime +# template :boolean default(FALSE) # require 'spec_helper' diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index eed2cbc5412..90af75ff0e3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -20,6 +20,7 @@ # position :integer default(0) # locked_at :datetime # updated_by_id :integer +# merge_error :string(255) # require 'spec_helper' diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 1d72a9503ae..a98b9cb7321 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -11,6 +11,7 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) +# public :boolean default(FALSE) # require 'spec_helper' diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f93935ebe3b..c42e8870f8c 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -415,12 +415,15 @@ describe Project do it { expect(project.ci_commit(commit.sha)).to eq(commit) } end - describe :enable_ci do + describe :builds_enabled do let(:project) { create :project } - before { project.enable_ci } + before { project.builds_enabled = true } - it { expect(project.gitlab_ci?).to be_truthy } + subject { project.builds_enabled } + + it { is_expected.to eq(project.gitlab_ci_service.active) } + it { expect(project.builds_enabled?).to be_truthy } it { expect(project.gitlab_ci_project).to be_a(Ci::Project) } end @@ -461,4 +464,23 @@ describe Project do end end end + + describe '.visible_to_user' do + let!(:project) { create(:project, :private) } + let!(:user) { create(:user) } + + subject { described_class.visible_to_user(user) } + + describe 'when a user has access to a project' do + before do + project.team.add_user(user, Gitlab::Access::MASTER) + end + + it { is_expected.to eq([project]) } + end + + describe 'when a user does not have access to any projects' do + it { is_expected.to eq([]) } + end + end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 9f6cdeeaa96..3b889144447 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -184,6 +184,12 @@ describe ProjectWiki do subject.create_page("test page", "some content", :markdown, "commit message") expect(subject.pages.first.page.version.message).to eq("commit message") end + + it 'updates project activity' do + expect(subject).to receive(:update_project_activity) + + subject.create_page('Test Page', 'This is content') + end end describe "#update_page" do @@ -205,6 +211,12 @@ describe ProjectWiki do it "sets the correct commit message" do expect(@page.version.message).to eq("updated page") end + + it 'updates project activity' do + expect(subject).to receive(:update_project_activity) + + subject.update_page(@gollum_page, 'Yet more content', :markdown, 'Updated page again') + end end describe "#delete_page" do @@ -217,6 +229,12 @@ describe ProjectWiki do subject.delete_page(@page) expect(subject.pages.count).to eq(0) end + + it 'updates project activity' do + expect(subject).to receive(:update_project_activity) + + subject.delete_page(@page) + end end private diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index 527005b2b69..72ecb442a36 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -1,3 +1,15 @@ +# == Schema Information +# +# Table name: releases +# +# id :integer not null, primary key +# tag :string(255) +# description :text +# project_id :integer +# created_at :datetime +# updated_at :datetime +# + require 'rails_helper' RSpec.describe Release, type: :model do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 49e0bfdd2ec..4631b12faf1 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -54,6 +54,8 @@ # public_email :string(255) default(""), not null # dashboard :integer default(0) # project_view :integer default(0) +# consumed_timestep :integer +# layout :integer default(0) # require 'spec_helper' @@ -684,7 +686,7 @@ describe User do end end - describe "#contributed_projects_ids" do + describe "#contributed_projects" do subject { create(:user) } let!(:project1) { create(:project) } let!(:project2) { create(:project, forked_from_project: project3) } @@ -699,15 +701,15 @@ describe User do end it "includes IDs for projects the user has pushed to" do - expect(subject.contributed_projects_ids).to include(project1.id) + expect(subject.contributed_projects).to include(project1) end it "includes IDs for projects the user has had merge requests merged into" do - expect(subject.contributed_projects_ids).to include(project3.id) + expect(subject.contributed_projects).to include(project3) end it "doesn't include IDs for unrelated projects" do - expect(subject.contributed_projects_ids).not_to include(project2.id) + expect(subject.contributed_projects).not_to include(project2) end end @@ -756,4 +758,30 @@ describe User do expect(subject.recent_push).to eq(nil) end end + + describe '#authorized_groups' do + let!(:user) { create(:user) } + let!(:private_group) { create(:group) } + + before do + private_group.add_user(user, Gitlab::Access::MASTER) + end + + subject { user.authorized_groups } + + it { is_expected.to eq([private_group]) } + end + + describe '#authorized_projects' do + let!(:user) { create(:user) } + let!(:private_project) { create(:project, :private) } + + before do + private_project.team << [user, Gitlab::Access::MASTER] + end + + subject { user.authorized_projects } + + it { is_expected.to eq([private_project]) } + end end diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb index b9e6dfc15a7..a28607bd240 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_status_spec.rb @@ -18,7 +18,7 @@ describe API::API, api: true do before do @status1 = create(:commit_status, commit: ci_commit, status: 'running') @status2 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'pending') - @status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running') + @status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running', allow_failure: true) @status4 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'success') @status5 = create(:commit_status, commit: ci_commit, ref: 'develop', status: 'success') @status6 = create(:commit_status, commit: ci_commit, status: 'success') @@ -30,6 +30,8 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(@status3.id, @status4.id, @status5.id, @status6.id) + json_response.sort_by!{ |status| status['id'] } + expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false]) end it "should return all commit statuses" do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index e9de9e0826d..9fc294118ae 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -88,8 +88,11 @@ describe API::API, api: true do end it 'returns projects in the correct order when ci_enabled_first parameter is passed' do - [project, project2, project3].each{ |project| project.build_missing_services } - project2.gitlab_ci_service.update(active: true) + [project, project2, project3].each do |project| + project.builds_enabled = false + project.build_missing_services + end + project2.builds_enabled = true get api('/projects', user), { ci_enabled_first: 'true' } expect(response.status).to eq(200) expect(json_response).to be_an Array diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index faf6b77a462..4911cdd9da6 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -11,81 +11,6 @@ describe API::API, api: true do let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } - describe "GET /projects/:id/repository/tags" do - it "should return an array of project tags" do - get api("/projects/#{project.id}/repository/tags", user) - expect(response.status).to eq(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project.repo.tags.sort_by(&:name).reverse.first.name) - end - end - - describe 'POST /projects/:id/repository/tags' do - context 'lightweight tags' do - it 'should create a new tag' do - post api("/projects/#{project.id}/repository/tags", user), - tag_name: 'v7.0.1', - ref: 'master' - - expect(response.status).to eq(201) - expect(json_response['name']).to eq('v7.0.1') - end - end - - context 'annotated tag' do - it 'should create a new annotated tag' do - # Identity must be set in .gitconfig to create annotated tag. - repo_path = project.repository.path_to_repo - system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name})) - system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email})) - - post api("/projects/#{project.id}/repository/tags", user), - tag_name: 'v7.1.0', - ref: 'master', - message: 'Release 7.1.0' - - expect(response.status).to eq(201) - expect(json_response['name']).to eq('v7.1.0') - expect(json_response['message']).to eq('Release 7.1.0') - end - end - - it 'should deny for user without push access' do - post api("/projects/#{project.id}/repository/tags", user2), - tag_name: 'v1.9.0', - ref: '621491c677087aa243f165eab467bfdfbee00be1' - expect(response.status).to eq(403) - end - - it 'should return 400 if tag name is invalid' do - post api("/projects/#{project.id}/repository/tags", user), - tag_name: 'v 1.0.0', - ref: 'master' - expect(response.status).to eq(400) - expect(json_response['message']).to eq('Tag name invalid') - end - - it 'should return 400 if tag already exists' do - post api("/projects/#{project.id}/repository/tags", user), - tag_name: 'v8.0.0', - ref: 'master' - expect(response.status).to eq(201) - post api("/projects/#{project.id}/repository/tags", user), - tag_name: 'v8.0.0', - ref: 'master' - expect(response.status).to eq(400) - expect(json_response['message']).to eq('Tag already exists') - end - - it 'should return 400 if ref name is invalid' do - post api("/projects/#{project.id}/repository/tags", user), - tag_name: 'mytag', - ref: 'foo' - expect(response.status).to eq(400) - expect(json_response['message']).to eq('Invalid reference name') - end - end - describe "GET /projects/:id/repository/tree" do context "authorized user" do before { project.team << [user2, :reporter] } diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index c0226605a23..b180d2fec77 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -46,6 +46,7 @@ describe API::API, api: true do delete api("/projects/#{project.id}/services/#{dashed_service}", user) expect(response.status).to eq(200) + project.send(service_method).reload expect(project.send(service_method).activated?).to be_falsey end end diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb new file mode 100644 index 00000000000..cc9a5f47582 --- /dev/null +++ b/spec/requests/api/tags_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' +require 'mime/types' + +describe API::API, api: true do + include ApiHelpers + include RepoHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, creator_id: user.id) } + let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } + let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + + describe "GET /projects/:id/repository/tags" do + let(:tag_name) { project.repository.tag_names.sort.reverse.first } + let(:description) { 'Awesome release!' } + + context 'without releases' do + it "should return an array of project tags" do + get api("/projects/#{project.id}/repository/tags", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + end + end + + context 'with releases' do + before do + release = project.releases.find_or_initialize_by(tag: tag_name) + release.update_attributes(description: description) + get api("/projects/#{project.id}/repository/tags", user) + end + + it "should return an array of project tags with release info" do + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + expect(json_response.first['release']['description']).to eq(description) + end + end + end + + describe 'POST /projects/:id/repository/tags' do + context 'lightweight tags' do + it 'should create a new tag' do + post api("/projects/#{project.id}/repository/tags", user), + tag_name: 'v7.0.1', + ref: 'master' + + expect(response.status).to eq(201) + expect(json_response['name']).to eq('v7.0.1') + end + end + + context 'lightweight tags with release notes' do + it 'should create a new tag' do + post api("/projects/#{project.id}/repository/tags", user), + tag_name: 'v7.0.1', + ref: 'master', + release_description: 'Wow' + + expect(response.status).to eq(201) + expect(json_response['name']).to eq('v7.0.1') + expect(json_response['release']['description']).to eq('Wow') + end + end + + context 'annotated tag' do + it 'should create a new annotated tag' do + # Identity must be set in .gitconfig to create annotated tag. + repo_path = project.repository.path_to_repo + system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name})) + system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email})) + + post api("/projects/#{project.id}/repository/tags", user), + tag_name: 'v7.1.0', + ref: 'master', + message: 'Release 7.1.0' + + expect(response.status).to eq(201) + expect(json_response['name']).to eq('v7.1.0') + expect(json_response['message']).to eq('Release 7.1.0') + end + end + + it 'should deny for user without push access' do + post api("/projects/#{project.id}/repository/tags", user2), + tag_name: 'v1.9.0', + ref: '621491c677087aa243f165eab467bfdfbee00be1' + expect(response.status).to eq(403) + end + + it 'should return 400 if tag name is invalid' do + post api("/projects/#{project.id}/repository/tags", user), + tag_name: 'v 1.0.0', + ref: 'master' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Tag name invalid') + end + + it 'should return 400 if tag already exists' do + post api("/projects/#{project.id}/repository/tags", user), + tag_name: 'v8.0.0', + ref: 'master' + expect(response.status).to eq(201) + post api("/projects/#{project.id}/repository/tags", user), + tag_name: 'v8.0.0', + ref: 'master' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Tag already exists') + end + + it 'should return 400 if ref name is invalid' do + post api("/projects/#{project.id}/repository/tags", user), + tag_name: 'mytag', + ref: 'foo' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Invalid reference name') + end + end + + describe 'PUT /projects/:id/repository/:tag/release' do + let(:tag_name) { project.repository.tag_names.first } + let(:description) { 'Awesome release!' } + + it 'should create description for existing git tag' do + put api("/projects/#{project.id}/repository/#{tag_name}/release", user), + description: description + + expect(response.status).to eq(200) + expect(json_response['tag']).to eq(tag_name) + expect(json_response['description']).to eq(description) + end + end +end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index d26a300ed82..a9ef2fe5885 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -343,8 +343,9 @@ describe API::API, api: true do end.to change{ user.keys.count }.by(1) end - it "should raise error for invalid ID" do - expect{post api("/users/ASDF/keys", admin) }.to raise_error(ActionController::RoutingError) + it "should return 405 for invalid ID" do + post api("/users/ASDF/keys", admin) + expect(response.status).to eq(405) end end @@ -374,9 +375,9 @@ describe API::API, api: true do expect(json_response.first['title']).to eq(key.title) end - it "should return 404 for invalid ID" do + it "should return 405 for invalid ID" do get api("/users/ASDF/keys", admin) - expect(response.status).to eq(404) + expect(response.status).to eq(405) end end end @@ -434,7 +435,8 @@ describe API::API, api: true do end it "should raise error for invalid ID" do - expect{post api("/users/ASDF/emails", admin) }.to raise_error(ActionController::RoutingError) + post api("/users/ASDF/emails", admin) + expect(response.status).to eq(405) end end @@ -465,7 +467,8 @@ describe API::API, api: true do end it "should raise error for invalid ID" do - expect{put api("/users/ASDF/emails", admin) }.to raise_error(ActionController::RoutingError) + put api("/users/ASDF/emails", admin) + expect(response.status).to eq(405) end end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 88218a93e1f..c2be045099d 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -5,7 +5,7 @@ describe Ci::API::API do let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) } let(:project) { FactoryGirl.create(:ci_project) } - let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:gl_project) { project.gl_project } before do stub_ci_commit_to_return_yaml_file @@ -14,7 +14,7 @@ describe Ci::API::API do describe "Builds API for runners" do let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") } let(:shared_project) { FactoryGirl.create(:ci_project, name: "SharedProject") } - let(:shared_gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: shared_project) } + let(:shared_gl_project) { shared_project.gl_project } before do FactoryGirl.create :ci_runner_project, project_id: project.id, runner_id: runner.id @@ -41,7 +41,7 @@ describe Ci::API::API do it "should return 404 error if no builds for specific runner" do commit = FactoryGirl.create(:ci_commit, gl_project: shared_gl_project) - FactoryGirl.create(:ci_build, commit: commit, status: 'pending' ) + FactoryGirl.create(:ci_build, commit: commit, status: 'pending') post ci_api("/builds/register"), token: runner.token @@ -50,7 +50,7 @@ describe Ci::API::API do it "should return 404 error if no builds for shared runner" do commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) - FactoryGirl.create(:ci_build, commit: commit, status: 'pending' ) + FactoryGirl.create(:ci_build, commit: commit, status: 'pending') post ci_api("/builds/register"), token: shared_runner.token @@ -79,7 +79,7 @@ describe Ci::API::API do { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, { "key" => "DB_NAME", "value" => "postgres", "public" => true }, - { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, + { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false } ]) end @@ -122,5 +122,191 @@ describe Ci::API::API do expect(build.reload.trace).to eq 'hello_world' end end + + context "Artifacts" do + let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } + let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) } + let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } + let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } + let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } + let(:get_url) { ci_api("/builds/#{build.id}/artifacts") } + let(:headers) { { "GitLab-Workhorse" => "1.0" } } + let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.project.token) } + + describe "POST /builds/:id/artifacts/authorize" do + context "should authorize posting artifact to running build" do + before do + build.run! + end + + it "using token as parameter" do + post authorize_url, { token: build.project.token }, headers + expect(response.status).to eq(200) + expect(json_response["TempPath"]).to_not be_nil + end + + it "using token as header" do + post authorize_url, {}, headers_with_token + expect(response.status).to eq(200) + expect(json_response["TempPath"]).to_not be_nil + end + end + + context "should fail to post too large artifact" do + before do + build.run! + end + + it "using token as parameter" do + stub_application_setting(max_artifacts_size: 0) + post authorize_url, { token: build.project.token, filesize: 100 }, headers + expect(response.status).to eq(413) + end + + it "using token as header" do + stub_application_setting(max_artifacts_size: 0) + post authorize_url, { filesize: 100 }, headers_with_token + expect(response.status).to eq(413) + end + end + + context "should get denied" do + it do + post authorize_url, { token: 'invalid', filesize: 100 } + expect(response.status).to eq(403) + end + end + end + + describe "POST /builds/:id/artifacts" do + context "Disable sanitizer" do + before do + # by configuring this path we allow to pass temp file from any path + allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/') + end + + context "should post artifact to running build" do + before do + build.run! + end + + it "uses regual file post" do + upload_artifacts(file_upload, headers_with_token, false) + expect(response.status).to eq(201) + expect(json_response["artifacts_file"]["filename"]).to eq(file_upload.original_filename) + end + + it "uses accelerated file post" do + upload_artifacts(file_upload, headers_with_token, true) + expect(response.status).to eq(201) + expect(json_response["artifacts_file"]["filename"]).to eq(file_upload.original_filename) + end + + it "updates artifact" do + upload_artifacts(file_upload, headers_with_token) + upload_artifacts(file_upload2, headers_with_token) + expect(response.status).to eq(201) + expect(json_response["artifacts_file"]["filename"]).to eq(file_upload2.original_filename) + end + end + + context "should fail to post too large artifact" do + before do + build.run! + end + + it do + stub_application_setting(max_artifacts_size: 0) + upload_artifacts(file_upload, headers_with_token) + expect(response.status).to eq(413) + end + end + + context "should fail to post artifacts without file" do + before do + build.run! + end + + it do + post post_url, {}, headers_with_token + expect(response.status).to eq(400) + end + end + + context "should fail to post artifacts without GitLab-Workhorse" do + before do + build.run! + end + + it do + post post_url, { token: build.project.token }, {} + expect(response.status).to eq(403) + end + end + end + + context "should fail to post artifacts for outside of tmp path" do + before do + # by configuring this path we allow to pass file from @tmpdir only + # but all temporary files are stored in system tmp directory + @tmpdir = Dir.mktmpdir + allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir) + build.run! + end + + after do + FileUtils.remove_entry @tmpdir + end + + it do + upload_artifacts(file_upload, headers_with_token) + expect(response.status).to eq(400) + end + end + + def upload_artifacts(file, headers = {}, accelerated = true) + if accelerated + post post_url, { + 'file.path' => file.path, + 'file.name' => file.original_filename + }, headers + else + post post_url, { file: file }, headers + end + end + end + + describe "DELETE /builds/:id/artifacts" do + before do + build.run! + post delete_url, token: build.project.token, file: file_upload + end + + it "should delete artifact build" do + build.success + delete delete_url, token: build.project.token + expect(response.status).to eq(200) + end + end + + describe "GET /builds/:id/artifacts" do + before do + build.run! + end + + it "should download artifact" do + build.update_attributes(artifacts_file: file_upload) + get get_url, token: build.project.token + expect(response.status).to eq(200) + end + + it "should fail to download if no artifact uploaded" do + get get_url, token: build.project.token + expect(response.status).to eq(404) + end + end + end end end diff --git a/spec/requests/ci/api/commits_spec.rb b/spec/requests/ci/api/commits_spec.rb index 6049135fd10..aa51ba95bca 100644 --- a/spec/requests/ci/api/commits_spec.rb +++ b/spec/requests/ci/api/commits_spec.rb @@ -4,7 +4,7 @@ describe Ci::API::API, 'Commits' do include ApiHelpers let(:project) { FactoryGirl.create(:ci_project) } - let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:gl_project) { project.gl_project } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } let(:options) do diff --git a/spec/requests/ci/api/projects_spec.rb b/spec/requests/ci/api/projects_spec.rb index 53f7f91cc1f..893fd168d3e 100644 --- a/spec/requests/ci/api/projects_spec.rb +++ b/spec/requests/ci/api/projects_spec.rb @@ -41,8 +41,8 @@ describe Ci::API::API do describe "GET /projects/owned" do let!(:gl_project1) {FactoryGirl.create(:empty_project, namespace: user.namespace)} let!(:gl_project2) {FactoryGirl.create(:empty_project, namespace: user.namespace)} - let!(:project1) { FactoryGirl.create(:ci_project, gl_project: gl_project1) } - let!(:project2) { FactoryGirl.create(:ci_project, gl_project: gl_project2) } + let!(:project1) { gl_project1.ensure_gitlab_ci_project } + let!(:project2) { gl_project2.ensure_gitlab_ci_project } before do project1.gl_project.team << [user, :developer] @@ -180,87 +180,53 @@ describe Ci::API::API do end end - describe "POST /projects" do - let(:gl_project) { FactoryGirl.create :empty_project } - let(:project_info) do - { - gitlab_id: gl_project.id - } - end - - let(:invalid_project_info) { {} } + describe "POST /projects/:id/runners/:id" do + let(:project) { FactoryGirl.create(:ci_project) } + let(:runner) { FactoryGirl.create(:ci_runner) } - context "with valid project info" do - before do - options.merge!(project_info) - end + it "should add the project to the runner" do + project.gl_project.team << [user, :master] + post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options + expect(response.status).to eq(201) - it "should create a project with valid data" do - post ci_api("/projects"), options - expect(response.status).to eq(201) - expect(json_response['name']).to eq(gl_project.name_with_namespace) - end + project.reload + expect(project.runners.first.id).to eq(runner.id) end - context "with invalid project info" do - before do - options.merge!(invalid_project_info) - end + it "should fail if it tries to link a non-existing project or runner" do + post ci_api("/projects/#{project.id}/runners/non-existing"), options + expect(response.status).to eq(404) - it "should error with invalid data" do - post ci_api("/projects"), options - expect(response.status).to eq(400) - end + post ci_api("/projects/non-existing/runners/#{runner.id}"), options + expect(response.status).to eq(404) end - describe "POST /projects/:id/runners/:id" do - let(:project) { FactoryGirl.create(:ci_project) } - let(:runner) { FactoryGirl.create(:ci_runner) } - - it "should add the project to the runner" do - project.gl_project.team << [user, :master] - post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options - expect(response.status).to eq(201) - - project.reload - expect(project.runners.first.id).to eq(runner.id) - end - - it "should fail if it tries to link a non-existing project or runner" do - post ci_api("/projects/#{project.id}/runners/non-existing"), options - expect(response.status).to eq(404) - - post ci_api("/projects/non-existing/runners/#{runner.id}"), options - expect(response.status).to eq(404) - end - - it "non-manager is not authorized" do - allow_any_instance_of(User).to receive(:can_manage_project?).and_return(false) - post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options - expect(response.status).to eq(401) - end + it "non-manager is not authorized" do + allow_any_instance_of(User).to receive(:can_manage_project?).and_return(false) + post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options + expect(response.status).to eq(401) end + end - describe "DELETE /projects/:id/runners/:id" do - let(:project) { FactoryGirl.create(:ci_project) } - let(:runner) { FactoryGirl.create(:ci_runner) } + describe "DELETE /projects/:id/runners/:id" do + let(:project) { FactoryGirl.create(:ci_project) } + let(:runner) { FactoryGirl.create(:ci_runner) } - it "should remove the project from the runner" do - project.gl_project.team << [user, :master] - post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options + it "should remove the project from the runner" do + project.gl_project.team << [user, :master] + post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options - expect(project.runners).to be_present - delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options - expect(response.status).to eq(200) + expect(project.runners).to be_present + delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options + expect(response.status).to eq(200) - project.reload - expect(project.runners).to be_empty - end + project.reload + expect(project.runners).to be_empty + end - it "non-manager is not authorized" do - delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options - expect(response.status).to eq(401) - end + it "non-manager is not authorized" do + delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options + expect(response.status).to eq(401) end end end diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index 93617fc4b3f..a2b436d5811 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -6,7 +6,7 @@ describe Ci::API::API do describe 'POST /projects/:project_id/refs/:ref/trigger' do let!(:trigger_token) { 'secure token' } let!(:gl_project) { FactoryGirl.create(:project) } - let!(:project) { FactoryGirl.create(:ci_project, gl_project: gl_project) } + let!(:project) { gl_project.ensure_gitlab_ci_project } let!(:project2) { FactoryGirl.create(:ci_project) } let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } let(:options) do diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index fcafae38644..2ef4bb50a57 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService do let(:service) { Ci::CreateTriggerRequestService.new } let(:gl_project) { create(:project) } - let(:project) { create(:ci_project, gl_project: gl_project) } + let(:project) { gl_project.ensure_gitlab_ci_project } let(:trigger) { create(:ci_trigger, project: project) } before do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index a91be3b4472..f55527ee9a3 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -3,13 +3,15 @@ require 'spec_helper' describe Issues::UpdateService do let(:user) { create(:user) } let(:user2) { create(:user) } - let(:issue) { create(:issue, title: 'Old title') } + let(:user3) { create(:user) } + let(:issue) { create(:issue, title: 'Old title', assignee_id: user3.id) } let(:label) { create(:label) } let(:project) { issue.project } before do project.team << [user, :master] project.team << [user2, :developer] + project.team << [user3, :developer] end describe 'execute' do @@ -34,9 +36,11 @@ describe Issues::UpdateService do it { expect(@issue.labels.count).to eq(1) } it { expect(@issue.labels.first.title).to eq('Bug') } - it 'should send email to user2 about assign of new issue' do - email = ActionMailer::Base.deliveries.last - expect(email.to.first).to eq(user2.email) + it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do + deliveries = ActionMailer::Base.deliveries + email = deliveries.last + recipients = deliveries.last(2).map(&:to).flatten + expect(recipients).to include(user2.email, user3.email) expect(email.subject).to include(issue.title) end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index c75173c1452..2ed51d223b7 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' describe MergeRequests::UpdateService do let(:user) { create(:user) } let(:user2) { create(:user) } - let(:merge_request) { create(:merge_request, :simple, title: 'Old title') } + let(:user3) { create(:user) } + let(:merge_request) { create(:merge_request, :simple, title: 'Old title', assignee_id: user3.id) } let(:project) { merge_request.project } let(:label) { create(:label) } @@ -47,9 +48,11 @@ describe MergeRequests::UpdateService do with(@merge_request, 'update') end - it 'should send email to user2 about assign of new merge_request' do - email = ActionMailer::Base.deliveries.last - expect(email.to.first).to eq(user2.email) + it 'should send email to user2 about assign of new merge request and email to user3 about merge request unassignment' do + deliveries = ActionMailer::Base.deliveries + email = deliveries.last + recipients = deliveries.last(2).map(&:to).flatten + expect(recipients).to include(user2.email, user3.email) expect(email.subject).to include(merge_request.title) end diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb new file mode 100644 index 00000000000..034c0f22e12 --- /dev/null +++ b/spec/services/milestones/close_service_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Milestones::CloseService do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } + + before do + project.team << [user, :master] + end + + describe :execute do + before do + Milestones::CloseService.new(project, user, {}).execute(milestone) + end + + it { expect(milestone).to be_valid } + it { expect(milestone).to be_closed } + + describe :event do + let(:event) { Event.first } + + it { expect(event.milestone).to be_truthy } + it { expect(event.target).to eq(milestone) } + it { expect(event.action_name).to eq('closed') } + end + end +end diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb new file mode 100644 index 00000000000..757c9a226d8 --- /dev/null +++ b/spec/services/milestones/create_service_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Milestones::CreateService do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + describe :execute do + context "valid params" do + before do + project.team << [user, :master] + + opts = { + title: 'v2.1.9', + description: 'Patch release to fix security issue' + } + + @milestone = Milestones::CreateService.new(project, user, opts).execute + end + + it { expect(@milestone).to be_valid } + it { expect(@milestone.title).to eq('v2.1.9') } + end + end +end diff --git a/spec/services/milestones/group_service_spec.rb b/spec/services/milestones/group_service_spec.rb deleted file mode 100644 index 74eb0f99e0f..00000000000 --- a/spec/services/milestones/group_service_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'spec_helper' - -describe Milestones::GroupService do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:group) { create(:group) } - let(:project1) { create(:project, group: group) } - let(:project2) { create(:project, path: 'gitlab-ci', group: group) } - let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) } - let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } - let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } - let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) } - let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) } - let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) } - let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) } - - describe 'execute' do - context 'with valid projects' do - before do - milestones = - [ - milestone1_project1, - milestone1_project2, - milestone1_project3, - milestone2_project1, - milestone2_project2, - milestone2_project3 - ] - @group_milestones = Milestones::GroupService.new(milestones).execute - end - - it 'should have all project milestones' do - expect(@group_milestones.count).to eq(2) - end - - it 'should have all project milestones titles' do - expect(@group_milestones.map { |group_milestone| group_milestone.title }).to match_array(['Milestone v1.2', 'VD-123']) - end - - it 'should have all project milestones' do - expect(@group_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6) - end - end - end - - describe 'milestone' do - context 'with valid title' do - before do - milestones = - [ - milestone1_project1, - milestone1_project2, - milestone1_project3, - milestone2_project1, - milestone2_project2, - milestone2_project3 - ] - @group_milestones = Milestones::GroupService.new(milestones).milestone('Milestone v1.2') - end - - it 'should have exactly one group milestone' do - expect(@group_milestones.title).to eq('Milestone v1.2') - end - - it 'should have all project milestones with the same title' do - expect(@group_milestones.milestones.count).to eq(3) - end - end - end -end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 25277f07482..e81c4edb7d8 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -70,6 +70,28 @@ describe Projects::CreateService do end end + context 'builds_enabled global setting' do + let(:project) { create_project(@user, @opts) } + + subject { project.builds_enabled? } + + context 'global builds_enabled false does not enable CI by default' do + before do + @opts.merge!(builds_enabled: false) + end + + it { is_expected.to be_falsey } + end + + context 'global builds_enabled true does enable CI by default' do + before do + @opts.merge!(builds_enabled: true) + end + + it { is_expected.to be_truthy } + end + end + context 'restricted visibility level' do before do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 65a8c81204d..1feba6ce048 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -25,13 +25,6 @@ describe Projects::ForkService do end end - context 'fork project failure' do - it "fails due to transaction failure" do - @to_project = fork_project(@from_project, @to_user, false) - expect(@to_project.import_failed?) - end - end - context 'project already exists' do it "should fail due to validation, not transaction failure" do @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @@ -46,7 +39,7 @@ describe Projects::ForkService do it "fork and enable CI for fork" do @from_project.enable_ci @to_project = fork_project(@from_project, @to_user) - expect(@to_project.gitlab_ci?).to be_truthy + expect(@to_project.builds_enabled?).to be_truthy end end end @@ -66,7 +59,7 @@ describe Projects::ForkService do context 'fork project for group' do it 'group owner successfully forks project into the group' do - to_project = fork_project(@project, @group_owner, true, @opts) + to_project = fork_project(@project, @group_owner, @opts) expect(to_project.owner).to eq(@group) expect(to_project.namespace).to eq(@group) expect(to_project.name).to eq(@project.name) @@ -78,7 +71,7 @@ describe Projects::ForkService do context 'fork project for group when user not owner' do it 'group developer should fail to fork project into the group' do - to_project = fork_project(@project, @developer, true, @opts) + to_project = fork_project(@project, @developer, @opts) expect(to_project.errors[:namespace]).to eq(['is not valid']) end end @@ -87,7 +80,7 @@ describe Projects::ForkService do it 'should fail due to validation, not transaction failure' do existing_project = create(:project, name: @project.name, namespace: @group) - to_project = fork_project(@project, @group_owner, true, @opts) + to_project = fork_project(@project, @group_owner, @opts) expect(existing_project.persisted?).to be_truthy expect(to_project.errors[:name]).to eq(['has already been taken']) expect(to_project.errors[:path]).to eq(['has already been taken']) @@ -95,8 +88,8 @@ describe Projects::ForkService do end end - def fork_project(from_project, user, fork_success = true, params = {}) - allow(RepositoryForkWorker).to receive(:perform_async).and_return(fork_success) + def fork_project(from_project, user, params = {}) + allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) Projects::ForkService.new(from_project, user, params).execute end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 386ac9c8372..fb5e74af648 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -16,7 +16,7 @@ describe 'gitlab:app namespace rake task' do end def reenable_backup_sub_tasks - %w{db repo uploads builds}.each do |subtask| + %w{db repo uploads builds artifacts}.each do |subtask| Rake::Task["gitlab:backup:#{subtask}:create"].reenable end end @@ -56,6 +56,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task["gitlab:backup:repo:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:uploads:restore"]).to receive(:invoke) + expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke) expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end @@ -113,19 +114,20 @@ describe 'gitlab:app namespace rake task' do it 'should set correct permissions on the tar contents' do tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz} ) expect(exit_status).to eq(0) expect(tar_contents).to match('db/') expect(tar_contents).to match('uploads.tar.gz') expect(tar_contents).to match('repositories/') expect(tar_contents).to match('builds.tar.gz') - expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz)\/$/) + expect(tar_contents).to match('artifacts.tar.gz') + expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz)\/$/) end it 'should delete temp directories' do temp_dirs = Dir.glob( - File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds}') + File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts}') ) expect(temp_dirs).to be_empty @@ -147,7 +149,7 @@ describe 'gitlab:app namespace rake task' do # Redirect STDOUT and run the rake task orig_stdout = $stdout $stdout = StringIO.new - ENV["SKIP"] = "repositories" + ENV["SKIP"] = "repositories,uploads" run_rake_task('gitlab:backup:create') $stdout = orig_stdout @@ -161,12 +163,13 @@ describe 'gitlab:app namespace rake task' do it "does not contain skipped item" do tar_contents, _exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz} ) expect(tar_contents).to match('db/') expect(tar_contents).to match('uploads.tar.gz') expect(tar_contents).to match('builds.tar.gz') + expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).not_to match('repositories/') end @@ -177,7 +180,9 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke + expect(Rake::Task["gitlab:backup:uploads:restore"]).not_to receive :invoke expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke + expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive :invoke expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index aa031106968..245f066df1f 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -12,7 +12,6 @@ describe RepositoryForkWorker do project.path_with_namespace, fork_project.namespace.path). and_return(true) - expect(ProjectCacheWorker).to receive(:perform_async) subject.perform(project.id, project.path_with_namespace, |