diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c477721f9da..d21785f7af2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,16 +12,18 @@ cache: variables: MYSQL_ALLOW_EMPTY_PASSWORD: "1" + # retry tests only in CI environment + RSPEC_RETRY_RETRY_COUNT: "3" before_script: - source ./scripts/prepare_build.sh - ruby -v - which ruby - - gem install bundler --no-ri --no-rdoc + - retry gem install bundler --no-ri --no-rdoc - cp config/gitlab.yml.example config/gitlab.yml - touch log/application.log - touch log/test.log - - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" + - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate stages: diff --git a/CHANGELOG b/CHANGELOG index e939b632d0d..9474ca50b24 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,16 +3,31 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.6.0 (unreleased) - Contributions to forked projects are included in calendar - Improve the formatting for the user page bio (Connor Shea) + - Removed the default password from the initial admin account created during + setup. A password can be provided during setup (see installation docs), or + GitLab will ask the user to create a new one upon first visit. - Fix issue when pushing to projects ending in .wiki - Fix avatar stretching by providing a cropping feature (Johann Pardanaud) - Don't load all of GitLab in mail_room + - Memoize @group in Admin::GroupsController (Yatish Mehta) + - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Strip leading and trailing spaces in URL validator (evuez) + - Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez) - Return empty array instead of 404 when commit has no statuses in commit status API + - Add support for cross-project label references - Update documentation to reflect Guest role not being enforced on internal projects - Allow search for logged out users + - Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio) - Don't show Issues/MRs from archived projects in Groups view - Increase the notes polling timeout over time (Roberto Dip) - Add shortcut to toggle markdown preview (Florent Baldino) + - Show labels in dashboard and group milestone views + - Add main language of a project in the list of projects (Tiago Botelho) + - Add ability to show archived projects on dashboard, explore and group pages + +v 8.5.5 + - Ensure removing a project removes associated Todo entries. + - Prevent a 500 error in Todos when author was removed. v 8.5.4 - Do not cache requests for badges (including builds badge) diff --git a/Gemfile b/Gemfile index c66ef3cffad..7e70761a77a 100644 --- a/Gemfile +++ b/Gemfile @@ -263,7 +263,9 @@ group :development, :test do gem 'database_cleaner', '~> 1.4.0' gem 'factory_girl_rails', '~> 4.6.0' gem 'rspec-rails', '~> 3.3.0' + gem 'rspec-retry' gem 'spinach-rails', '~> 0.2.1' + gem 'spinach-rerun-reporter', '~> 0.0.2' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) gem 'minitest', '~> 5.7.0' @@ -273,7 +275,7 @@ group :development, :test do gem 'capybara', '~> 2.4.0' gem 'capybara-screenshot', '~> 1.0.0' - gem 'poltergeist', '~> 1.8.1' + gem 'poltergeist', '~> 1.9.0' gem 'teaspoon', '~> 1.0.0' gem 'teaspoon-jasmine', '~> 2.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 22c86e4ae8f..432bdc344ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -552,7 +552,7 @@ GEM parser (2.2.3.0) ast (>= 1.1, < 3.0) pg (0.18.4) - poltergeist (1.8.1) + poltergeist (1.9.0) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) @@ -679,6 +679,8 @@ GEM rspec-expectations (~> 3.3.0) rspec-mocks (~> 3.3.0) rspec-support (~> 3.3.0) + rspec-retry (0.4.5) + rspec-core rspec-support (3.3.0) rubocop (0.35.1) astrolabe (~> 1.3) @@ -764,6 +766,8 @@ GEM capybara (>= 2.0.0) railties (>= 3) spinach (>= 0.4) + spinach-rerun-reporter (0.0.2) + spinach (~> 0.8) spring (1.6.4) spring-commands-rspec (1.0.4) spring (>= 0.9.1) @@ -978,7 +982,7 @@ DEPENDENCIES org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) - poltergeist (~> 1.8.1) + poltergeist (~> 1.9.0) pry-rails quiet_assets (~> 1.0.2) rack-attack (~> 4.3.1) @@ -999,6 +1003,7 @@ DEPENDENCIES rouge (~> 1.10.1) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.3.0) + rspec-retry rubocop (~> 0.35.0) ruby-fogbugz (~> 0.2.1) sanitize (~> 2.0) @@ -1017,6 +1022,7 @@ DEPENDENCIES six (~> 0.2.0) slack-notifier (~> 1.2.0) spinach-rails (~> 0.2.1) + spinach-rerun-reporter (~> 0.0.2) spring (~> 1.6.4) spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.0.0) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index d7feb5d5c87..54b28f2dd8d 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -23,7 +23,7 @@ class Dispatcher new Issue() shortcut_handler = new ShortcutsIssuable() new ZenMode() - when 'projects:milestones:show' + when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' new Milestone() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index e6d8518bec8..0037a3a21c2 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -69,7 +69,7 @@ class @Milestone @bindIssuesSorting() @bindMergeRequestSorting() - @bindTabsSwitching + @bindTabsSwitching() bindIssuesSorting: -> $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable( @@ -104,7 +104,7 @@ class @Milestone ).disableSelection() - bindMergeRequestSorting: -> + bindTabsSwitching: -> $('a[data-toggle="tab"]').on 'show.bs.tab', (e) -> currentTabClass = $(e.target).data('show') previousTabClass = $(e.relatedTarget).data('show') @@ -112,7 +112,8 @@ class @Milestone $(previousTabClass).hide() $(currentTabClass).removeClass('hidden') $(currentTabClass).show() - + + bindMergeRequestSorting: -> $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable( connectWith: ".merge_requests-sortable-list", dropOnEmpty: true, diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d4878b333f9..3dc524ccca4 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -81,8 +81,9 @@ &::before { content: "\f00c"; position: absolute; - left: 4px; - top: 8px; + left: 5px; + top: 50%; + margin-top: -7px; font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; @@ -94,8 +95,8 @@ } .dropdown-header { - padding-left: 10px; - padding-right: 10px; + padding-left: 5px; + padding-right: 5px; color: $dropdown-header-color; font-size: 13px; line-height: 22px; diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index d24adbf67e6..d0e72a4422c 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -19,10 +19,11 @@ li.milestone { width: 105px; } - .issue-row { + .issuable-row { .color-label { border-radius: 2px; padding: 3px !important; + margin-right: 7px; } // Issue title @@ -44,20 +45,15 @@ li.milestone { } } -.issues-sortable-list { - .issue-detail { +.issues-sortable-list, .merge_requests-sortable-list { + .issuable-detail { display: block; + margin-top: 7px; - .issue-number{ + .issuable-number { color: rgba(0,0,0,0.44); margin-right: 5px; } - .color-label { - padding: 6px 10px; - margin-right: 7px; - margin-top: 10px; - } - .avatar { float: none; } diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 0161642d871..7d414ae003d 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -26,5 +26,5 @@ margin-right: 10px; font-size: $gl-font-size; border: 1px solid; - line-height: 40px; + line-height: 32px; } diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 4d3e48f7f81..668396a0f20 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController private def group - @group = Group.find_by(path: params[:id]) + @group ||= Group.find_by(path: params[:id]) end def group_params diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb new file mode 100644 index 00000000000..f63b703d101 --- /dev/null +++ b/app/controllers/concerns/filter_projects.rb @@ -0,0 +1,15 @@ +# == FilterProjects +# +# Controller concern to handle projects filtering +# * by name +# * by archived state +# +module FilterProjects + extend ActiveSupport::Concern + + def filter_projects(projects) + projects = projects.search(params[:filter_projects]) if params[:filter_projects].present? + projects = projects.non_archived if params[:archived].blank? + projects + end +end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index dc880b634e5..fc51c3241af 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -1,18 +1,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController + include FilterProjects + before_action :event_filter def index - @projects = current_user.authorized_projects.sorted_by_activity.non_archived - @projects = @projects.sort(@sort = params[:sort]) + @projects = current_user.authorized_projects.sorted_by_activity + @projects = filter_projects(@projects) @projects = @projects.includes(:namespace) + @projects = @projects.sort(@sort = params[:sort]) + @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? - terms = params[:filter_projects] - - if terms.present? - @projects = @projects.search(terms) - end - - @projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank? @last_push = current_user.recent_push respond_to do |format| @@ -32,16 +29,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController def starred @projects = current_user.starred_projects.sorted_by_activity + @projects = filter_projects(@projects) @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.sort(@sort = params[:sort]) + @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? - terms = params[:filter_projects] - - if terms.present? - @projects = @projects.search(terms) - end - - @projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank? @last_push = current_user.recent_push @groups = [] diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index a384f3004db..5b811db3068 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -1,12 +1,12 @@ class Explore::ProjectsController < Explore::ApplicationController + include FilterProjects + def index @projects = ProjectsFinder.new.execute(current_user) @tags = @projects.tags_on(:tags) @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? - @projects = @projects.non_archived - @projects = @projects.search(params[:search]) if params[:search].present? - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @@ -22,8 +22,7 @@ class Explore::ProjectsController < Explore::ApplicationController def trending @projects = TrendingProjectsFinder.new.execute(current_user) - @projects = @projects.non_archived - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? respond_to do |format| @@ -38,7 +37,7 @@ class Explore::ProjectsController < Explore::ApplicationController def starred @projects = ProjectsFinder.new.execute(current_user) - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) @projects = @projects.reorder('star_count DESC') @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index ca5ce1e2046..f05c29e9974 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,4 +1,5 @@ class GroupsController < Groups::ApplicationController + include FilterProjects include IssuesAction include MergeRequestsAction @@ -41,7 +42,8 @@ class GroupsController < Groups::ApplicationController def show @last_push = current_user.recent_push if current_user @projects = @projects.includes(:namespace) - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) + @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? respond_to do |format| @@ -98,7 +100,7 @@ class GroupsController < Groups::ApplicationController end def load_projects - @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity.non_archived + @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity end # Dont allow unauthorized access to group diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index b64dbbd89ce..a6bebc46b06 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -7,6 +7,9 @@ class Projects::AvatarsController < Projects::ApplicationController @blob = @repository.blob_at_branch('master', @project.avatar_in_git) if @blob headers['X-Content-Type-Options'] = 'nosniff' + + return if cached_blob? + headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) headers['Content-Disposition'] = 'inline' headers['Content-Type'] = safe_content_type(@blob) diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 21f30f278c8..da46731d945 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -32,10 +32,6 @@ class Projects::MilestonesController < Projects::ApplicationController end def show - @issues = @milestone.issues - @users = @milestone.participants.uniq - @merge_requests = @milestone.merge_requests - @labels = @milestone.labels end def create diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index d9723acb1d9..10de0e60530 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -13,6 +13,8 @@ class Projects::RawController < Projects::ApplicationController if @blob headers['X-Content-Type-Options'] = 'nosniff' + return if cached_blob? + if @blob.lfs_pointer? send_lfs_object else diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f7240edd618..c88a420b412 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -263,11 +263,9 @@ class IssuableFinder def by_label(items) if labels? if filter_by_no_label? - items = items. - joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id"). - where(label_links: { id: nil }) + items = items.without_label else - items = items.joins(:labels).where(labels: { title: label_names }) + items = items.with_label(label_names) if projects items = items.where(labels: { project_id: projects }) diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 07b5759443b..a41172816b8 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -4,7 +4,7 @@ class SnippetsFinder case filter when :all then - snippets(current_user).fresh.non_expired + snippets(current_user).fresh when :by_user then by_user(current_user, params[:user], params[:scope]) when :by_project @@ -27,7 +27,7 @@ class SnippetsFinder end def by_user(current_user, user, scope) - snippets = user.snippets.fresh.non_expired + snippets = user.snippets.fresh return snippets.are_public unless current_user @@ -48,7 +48,7 @@ class SnippetsFinder end def by_project(current_user, project) - snippets = project.snippets.fresh.non_expired + snippets = project.snippets.fresh if current_user if project.team.member?(current_user.id) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f0aa2b57121..368969c6472 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -72,7 +72,7 @@ module ApplicationHelper if user_or_email.is_a?(User) user = user_or_email else - user = User.find_by(email: user_or_email.downcase) + user = User.find_by(email: user_or_email.try(:downcase)) end if user diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 7f63a2e2cb4..0f77b3b299a 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -152,4 +152,25 @@ module BlobHelper 'application/octet-stream' end end + + def cached_blob? + stale = stale?(etag: @blob.id) # The #stale? method sets cache headers. + + # Because we are opionated we set the cache headers ourselves. + response.cache_control[:public] = @project.public? + + if @ref && @commit && @ref == @commit.id + # This is a link to a commit by its commit SHA. That means that the blob + # is immutable. The only reason to invalidate the cache is if the commit + # was deleted or if the user lost access to the repository. + response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE + else + # A branch or tag points at this blob. That means that the expected blob + # value may change over time. + response.cache_control[:max_age] = Blob::CACHE_TIME + end + + response.etag = @blob.id + !stale + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index a09e91578b6..f994c9e6170 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -211,4 +211,15 @@ module CommitsHelper def clean(string) Sanitize.clean(string, remove_contents: true) end + + def limited_commits(commits) + if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + [ + commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), + commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE + ] + else + [commits, 0] + end + end end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index 3648757428b..337b0aacbb5 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -1,5 +1,5 @@ module ExploreHelper - def explore_projects_filter_path(options={}) + def filter_projects_path(options={}) exist_opts = { sort: params[:sort], scope: params[:scope], @@ -9,15 +9,7 @@ module ExploreHelper } options = exist_opts.merge(options) - - path = if explore_controller? - explore_projects_path - elsif current_action?(:starred) - starred_dashboard_projects_path - else - dashboard_projects_path - end - + path = request.path path << "?#{options.to_param}" path end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 1c7fcc13b42..89a054289e8 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -50,19 +50,25 @@ module LabelsHelper @project.labels.pluck(:title) end - def render_colored_label(label) + def render_colored_label(label, label_suffix = '') label_color = label.color || Label::DEFAULT_COLOR text_color = text_color_for_bg(label_color) # Intentionally not using content_tag here so that this method can be called # by LabelReferenceFilter span = %() + - escape_once(label.name) + '' + %(style="background-color: #{label_color}; color: #{text_color}">) + + %(#{escape_once(label.name)}#{label_suffix}) span.html_safe end + def render_colored_cross_project_label(label) + label_suffix = label.project.name_with_namespace + label_suffix = " in #{escape_once(label_suffix)}" + render_colored_label(label, label_suffix) + end + def suggested_colors [ '#0033CC', @@ -119,5 +125,6 @@ module LabelsHelper end # Required for Banzai::Filter::LabelReferenceFilter - module_function :render_colored_label, :text_color_for_bg, :escape_once + module_function :render_colored_label, :render_colored_cross_project_label, + :text_color_for_bg, :escape_once end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 7de81d8dfdb..e3e7daa49c5 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -9,6 +9,32 @@ module MilestonesHelper end end + def milestones_label_path(opts = {}) + if @project + namespace_project_issues_path(@project.namespace, @project, opts) + elsif @group + issues_group_path(@group, opts) + else + issues_dashboard_path(opts) + end + end + + def milestones_browse_issuables_path(milestone, type:) + opts = { milestone_title: milestone.title } + + if @project + polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts) + elsif @group + polymorphic_url([type, @group], opts) + else + polymorphic_url([type, :dashboard], opts) + end + end + + def milestone_issues_by_label_count(milestone, label, state:) + milestone.issues.with_label(label.title).send(state).size + end + def milestone_progress_bar(milestone) options = { class: 'progress-bar progress-bar-success', diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 41ae4048992..0a5a8eb5aee 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -1,14 +1,4 @@ module SnippetsHelper - def lifetime_select_options - options = [ - ['forever', nil], - ['1 day', "#{Date.current + 1.day}"], - ['1 week', "#{Date.current + 1.week}"], - ['1 month', "#{Date.current + 1.month}"] - ] - options_for_select(options) - end - def reliable_snippet_path(snippet) if snippet.project_id? namespace_project_snippet_path(snippet.project.namespace, diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index f9026b887da..2f2d2721d6d 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -16,6 +16,16 @@ module SortingHelper } end + def projects_sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_created => sort_title_recently_created, + sort_value_oldest_created => sort_title_oldest_created, + } + end + def sort_title_oldest_updated 'Oldest updated' end diff --git a/app/models/ability.rb b/app/models/ability.rb index f34554d557c..fe9e0aab717 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -9,6 +9,7 @@ class Ability when CommitStatus then commit_status_abilities(user, subject) when Project then project_abilities(user, subject) when Issue then issue_abilities(user, subject) + when ExternalIssue then external_issue_abilities(user, subject) when Note then note_abilities(user, subject) when ProjectSnippet then project_snippet_abilities(user, subject) when PersonalSnippet then personal_snippet_abilities(user, subject) @@ -424,6 +425,10 @@ class Ability end end + def external_issue_abilities(user, subject) + project_abilities(user, subject.project) + end + private def named_abilities(name) diff --git a/app/models/blob.rb b/app/models/blob.rb index 8ee9f3006b2..72e6c5fa3fd 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -1,5 +1,8 @@ # Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects class Blob < SimpleDelegator + CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute + CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour + # Wrap a Gitlab::Git::Blob object, or return nil when given nil # # This method prevents the decorated object from evaluating to "truthy" when diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 286d6655861..27b97944e38 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -29,12 +29,15 @@ module Issuable scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } + scope :of_milestones, ->(ids) { where(milestone_id: ids) } scope :opened, -> { with_state(:opened, :reopened) } scope :only_opened, -> { with_state(:opened) } scope :only_reopened, -> { with_state(:reopened) } scope :closed, -> { with_state(:closed) } scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } + scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) } + scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } scope :references_project, -> { references(:project) } diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb new file mode 100644 index 00000000000..d67df7c1d9c --- /dev/null +++ b/app/models/concerns/milestoneish.rb @@ -0,0 +1,25 @@ +module Milestoneish + def closed_items_count + issues.closed.size + merge_requests.closed_and_merged.size + end + + def total_items_count + issues.size + merge_requests.size + end + + def complete? + total_items_count == closed_items_count + end + + def percent_complete + ((closed_items_count * 100) / total_items_count).abs + rescue ZeroDivisionError + 0 + end + + def remaining_days + return 0 if !due_date || expired? + + (due_date - Date.today).to_i + end +end diff --git a/app/models/global_label.rb b/app/models/global_label.rb index 0171f7d54b7..ddd4bad5c21 100644 --- a/app/models/global_label.rb +++ b/app/models/global_label.rb @@ -2,16 +2,19 @@ class GlobalLabel attr_accessor :title, :labels alias_attribute :name, :title + delegate :color, :description, to: :@first_label + def self.build_collection(labels) labels = labels.group_by(&:title) - labels.map do |title, label| - new(title, label) + labels.map do |title, labels| + new(title, labels) end end def initialize(title, labels) @title = title @labels = labels + @first_label = labels.find { |lbl| lbl.description.present? } || labels.first end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 7ee276255a0..97bd79af083 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -1,4 +1,6 @@ class GlobalMilestone + include Milestoneish + attr_accessor :title, :milestones alias_attribute :name, :title @@ -28,33 +30,7 @@ class GlobalMilestone end def projects - milestones.map { |milestone| milestone.project } - end - - def issue_count - milestones.map { |milestone| milestone.issues.count }.sum - end - - def merge_requests_count - milestones.map { |milestone| milestone.merge_requests.count }.sum - end - - def open_items_count - milestones.map { |milestone| milestone.open_items_count }.sum - end - - def closed_items_count - milestones.map { |milestone| milestone.closed_items_count }.sum - end - - def total_items_count - milestones.map { |milestone| milestone.total_items_count }.sum - end - - def percent_complete - ((closed_items_count * 100) / total_items_count).abs - rescue ZeroDivisionError - 0 + @projects ||= Project.for_milestones(milestones.map(&:id)) end def state @@ -76,35 +52,20 @@ class GlobalMilestone end def issues - @issues ||= milestones.map(&:issues).flatten.group_by(&:state) + @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project) end def merge_requests - @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state) + @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project) end def participants @participants ||= milestones.map(&:participants).flatten.compact.uniq end - def opened_issues - issues.values_at("opened", "reopened").compact.flatten - end - - def closed_issues - issues['closed'] - end - - def opened_merge_requests - merge_requests.values_at("opened", "reopened").compact.flatten - end - - def closed_merge_requests - merge_requests.values_at("closed", "merged", "locked").compact.flatten - end - - def complete? - total_items_count == closed_items_count + def labels + @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten) + .sort_by!(&:title) end def due_date diff --git a/app/models/label.rb b/app/models/label.rb index c34f4e4ba60..5ff644b8426 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -48,10 +48,15 @@ class Label < ActiveRecord::Base '~' end + ## # Pattern used to extract label references from text + # + # This pattern supports cross-project references. + # def self.reference_pattern %r{ - #{reference_prefix} + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)} (?: (?\d+) | # Integer-based label ID, or (? @@ -62,24 +67,31 @@ class Label < ActiveRecord::Base }x end + def self.link_reference_pattern + nil + end + + ## # Returns the String necessary to reference this Label in Markdown # # format - Symbol format to use (default: :id, optional: :name) # - # Note that its argument differs from other objects implementing Referable. If - # a non-Symbol argument is given (such as a Project), it will default to :id. - # # Examples: # - # Label.first.to_reference # => "~1" - # Label.first.to_reference(:name) # => "~\"bug\"" + # Label.first.to_reference # => "~1" + # Label.first.to_reference(format: :name) # => "~\"bug\"" + # Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1" # # Returns a String - def to_reference(format = :id) - if format == :name && !name.include?('"') - %(#{self.class.reference_prefix}"#{name}") + # + def to_reference(from_project = nil, format: :id) + format_reference = label_format_reference(format) + reference = "#{self.class.reference_prefix}#{format_reference}" + + if cross_project_reference?(from_project) + project.to_reference + reference else - "#{self.class.reference_prefix}#{id}" + reference end end @@ -98,4 +110,16 @@ class Label < ActiveRecord::Base def template? template end + + private + + def label_format_reference(format = :id) + raise StandardError, 'Unknown format' unless [:id, :name].include?(format) + + if format == :name && !name.include?('"') + %("#{name}") + else + id + end + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index cf26183c254..c1e18bb3cc5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -137,9 +137,7 @@ class MergeRequest < ActiveRecord::Base scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) } - scope :opened, -> { with_states(:opened, :reopened) } scope :merged, -> { with_state(:merged) } - scope :closed, -> { with_state(:closed) } scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :join_project, -> { joins(:target_project) } @@ -537,6 +535,29 @@ class MergeRequest < ActiveRecord::Base end end + def diverged_commits_count + cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits") + + if cache.blank? || cache[:source_sha] != source_sha || cache[:target_sha] != target_sha + cache = { + source_sha: source_sha, + target_sha: target_sha, + diverged_commits_count: compute_diverged_commits_count + } + Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache) + end + + cache[:diverged_commits_count] + end + + def compute_diverged_commits_count + Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size + end + + def diverged_from_target_branch? + diverged_commits_count > 0 + end + def ci_commit @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index df08d3a6dfb..33884118595 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base include Sortable # Prevent store of diff if commits amount more then 500 - COMMITS_SAFE_SIZE = 500 + COMMITS_SAFE_SIZE = 100 belongs_to :merge_request diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 7dc2f909b2f..e3969f32dd6 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -24,12 +24,13 @@ class Milestone < ActiveRecord::Base include Sortable include Referable include StripAttribute + include Milestoneish belongs_to :project has_many :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :merge_requests - has_many :participants, through: :issues, source: :assignee + has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee scope :active, -> { with_state(:active) } scope :closed, -> { with_state(:closed) } @@ -92,30 +93,6 @@ class Milestone < ActiveRecord::Base end end - def open_items_count - self.issues.opened.count + self.merge_requests.opened.count - end - - def closed_items_count - self.issues.closed.count + self.merge_requests.closed_and_merged.count - end - - def total_items_count - self.issues.count + self.merge_requests.count - end - - def percent_complete - ((closed_items_count * 100) / total_items_count).abs - rescue ZeroDivisionError - 0 - end - - def remaining_days - return 0 if !due_date || expired? - - (due_date - Date.today).to_i - end - def expires_at if due_date if due_date.past? diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index 9cee3b70cb3..452f3913eef 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # diff --git a/app/models/project.rb b/app/models/project.rb index 148eab692ff..426464dee81 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -151,6 +151,7 @@ class Project < ActiveRecord::Base has_many :releases, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects, through: :lfs_objects_projects + has_many :todos, dependent: :destroy has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" @@ -215,6 +216,7 @@ class Project < ActiveRecord::Base scope :public_only, -> { where(visibility_level: Project::PUBLIC) } scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :non_archived, -> { where(archived: false) } + scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } state_machine :import_status, initial: :none do event :import_start do diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 9e2c1b0e18e..1f7d85a5f3d 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # @@ -23,6 +22,4 @@ class ProjectSnippet < Snippet # Scopes scope :fresh, -> { order("created_at DESC") } - scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } - scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } end diff --git a/app/models/repository.rb b/app/models/repository.rb index c135ab61f6a..6441cd87e87 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -133,18 +133,18 @@ class Repository rugged.branches.create(branch_name, target) end - expire_branches_cache + after_create_branch find_branch(branch_name) end def add_tag(tag_name, ref, message = nil) - expire_tags_cache + before_push_tag gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) end def rm_branch(user, branch_name) - expire_branches_cache + before_remove_branch branch = find_branch(branch_name) oldrev = branch.try(:target) @@ -155,12 +155,12 @@ class Repository rugged.branches.delete(branch_name) end - expire_branches_cache + after_remove_branch true end def rm_tag(tag_name) - expire_tags_cache + before_remove_tag gitlab_shell.rm_tag(path_with_namespace, tag_name) end @@ -183,6 +183,14 @@ class Repository end end + def branch_count + @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count } + end + + def tag_count + @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count } + end + # Return repo size in megabytes # Cached in redis def size @@ -278,6 +286,16 @@ class Repository @has_visible_content = nil end + def expire_branch_count_cache + cache.expire(:branch_count) + @branch_count = nil + end + + def expire_tag_count_cache + cache.expire(:tag_count) + @tag_count = nil + end + def rebuild_cache cache_keys.each do |key| cache.expire(key) @@ -313,9 +331,17 @@ class Repository expire_root_ref_cache end - # Runs code before creating a new tag. - def before_create_tag + # Runs code before pushing (= creating or removing) a tag. + def before_push_tag expire_cache + expire_tags_cache + expire_tag_count_cache + end + + # Runs code before removing a tag. + def before_remove_tag + expire_tags_cache + expire_tag_count_cache end # Runs code after a repository has been forked/imported. @@ -330,12 +356,21 @@ class Repository # Runs code after a new branch has been created. def after_create_branch + expire_branches_cache expire_has_visible_content_cache + expire_branch_count_cache + end + + # Runs code before removing an existing branch. + def before_remove_branch + expire_branches_cache end # Runs code after an existing branch has been removed. def after_remove_branch expire_has_visible_content_cache + expire_branch_count_cache + expire_branches_cache end def method_missing(m, *args, &block) @@ -812,6 +847,12 @@ class Repository raw_repository.ls_files(actual_ref) end + def main_language + unless empty? + Linguist::Repository.new(rugged, rugged.head.target_id).language + end + end + private def cache diff --git a/app/models/snippet.rb b/app/models/snippet.rb index f876be7a4c8..dd3925c7a7d 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # @@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } - scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } - scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } participant :author, :notes @@ -111,10 +108,6 @@ class Snippet < ActiveRecord::Base nil end - def expired? - expires_at && expires_at < Time.current - end - def visibility_level_field visibility_level end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 9ba200f7bde..93a16e88967 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -14,6 +14,7 @@ class GitPushService < BaseService # 3. Recognizes cross-references from commit messages # 4. Executes the project's web hooks # 5. Executes the project's services + # 6. Checks if the project's main language has changed # def execute @project.repository.after_push_commit(branch_name) @@ -42,11 +43,24 @@ class GitPushService < BaseService @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) process_commit_messages end + # Checks if the main language has changed in the project and if so + # it updates it accordingly + update_main_language # Update merge requests that may be affected by this push. A new branch # could cause the last commit of a merge request to change. update_merge_requests end + def update_main_language + current_language = @project.repository.main_language + + unless current_language == @project.main_language + return @project.update_attributes(main_language: current_language) + end + + true + end + protected def update_merge_requests @@ -96,7 +110,9 @@ class GitPushService < BaseService # a different branch. closed_issues = commit.closes_issues(current_user) closed_issues.each do |issue| - Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) + if can?(current_user, :update_issue, issue) + Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) + end end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index a62c5fc4fc4..c88c7672805 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -2,7 +2,7 @@ class GitTagPushService attr_accessor :project, :user, :push_data def execute(project, user, oldrev, newrev, ref) - project.repository.before_create_tag + project.repository.before_push_tag @project, @user = project, user @push_data = build_push_data(oldrev, newrev, ref) diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index 8f25c5e2496..ebb67c7db65 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -21,7 +21,9 @@ module MergeRequests closed_issues = merge_request.closes_issues(current_user) closed_issues.each do |issue| - Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) + if can?(current_user, :update_issue, issue) + Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index edced010811..58a861ee08e 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -66,7 +66,7 @@ class SystemNoteService def self.change_label(noteable, project, author, added_labels, removed_labels) labels_count = added_labels.count + removed_labels.count - references = ->(label) { label.to_reference(:id) } + references = ->(label) { label.to_reference(format: :id) } added_labels = added_labels.map(&references).join(' ') removed_labels = removed_labels.map(&references).join(' ') diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 40f88261c10..9da3fcbd986 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -15,7 +15,7 @@ .nav-controls = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2" - = render 'explore/projects/dropdown' + = render 'shared/projects/dropdown' - if current_user.can_create_project? = link_to new_project_path, class: 'btn btn-new' do = icon('plus') diff --git a/app/views/dashboard/milestones/_issue.html.haml b/app/views/dashboard/milestones/_issue.html.haml deleted file mode 100644 index 1408ebdd5dc..00000000000 --- a/app/views/dashboard/milestones/_issue.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid } - %span.milestone-row - - project = issue.project - %strong #{project.name_with_namespace} · - = link_to [project.namespace.becomes(Namespace), project, issue] do - %span.cgray ##{issue.iid} - = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title - .pull-right.assignee-icon - - if issue.assignee - = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_issues.html.haml b/app/views/dashboard/milestones/_issues.html.haml deleted file mode 100644 index 9f350b772bd..00000000000 --- a/app/views/dashboard/milestones/_issues.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.panel.panel-default - .panel-heading= title - %ul{ class: "well-list issues-sortable-list" } - - if issues - - issues.each do |issue| - = render 'issue', issue: issue diff --git a/app/views/dashboard/milestones/_merge_request.html.haml b/app/views/dashboard/milestones/_merge_request.html.haml deleted file mode 100644 index 77c46de030b..00000000000 --- a/app/views/dashboard/milestones/_merge_request.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid } - %span.milestone-row - - project = merge_request.project - %strong #{project.name_with_namespace} · - = link_to [project.namespace.becomes(Namespace), project, merge_request] do - %span.cgray ##{merge_request.iid} - = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title - .pull-right.assignee-icon - - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_merge_requests.html.haml b/app/views/dashboard/milestones/_merge_requests.html.haml deleted file mode 100644 index 50057e2c636..00000000000 --- a/app/views/dashboard/milestones/_merge_requests.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.panel.panel-default - .panel-heading= title - %ul{ class: "well-list merge_requests-sortable-list" } - - if merge_requests - - merge_requests.each do |merge_request| - = render 'merge_request', merge_request: merge_request diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml index 7c882a32702..6173ca6ab9b 100644 --- a/app/views/dashboard/milestones/_milestone.html.haml +++ b/app/views/dashboard/milestones/_milestone.html.haml @@ -1,25 +1,6 @@ -%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } - .row - .col-sm-6 - %strong - = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) - .col-sm-6 - .pull-right.light #{milestone.percent_complete}% complete - .row - .col-sm-6 - = link_to issues_dashboard_path(milestone_title: milestone.title) do - = pluralize milestone.issue_count, 'Issue' - · - = link_to merge_requests_dashboard_path(milestone_title: milestone.title) do - = pluralize milestone.merge_requests_count, 'Merge Request' - .col-sm-6 - = milestone_progress_bar(milestone) - .row - .col-sm-6 - .expiration - = render 'shared/milestone_expired', milestone: milestone - .projects - - milestone.milestones.each do |milestone| - = link_to milestone_path(milestone) do - %span.label.label-gray - = milestone.project.name_with_namespace += render 'shared/milestones/milestone', + milestone_path: dashboard_milestone_path(milestone.safe_title, title: milestone.title), + issues_path: issues_dashboard_path(milestone_title: milestone.title), + merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title), + milestone: milestone, + dashboard: true diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 3810267577c..60c84a26420 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -1,105 +1,5 @@ -- page_title @milestone.title, "Milestones" - header_title "Milestones", dashboard_milestones_path -.detail-page-header - .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } - - if @milestone.closed? - Closed - - else - Open - %span.identifier - Milestone #{@milestone.title} - -.detail-page-description.gray-content-block.second-block - %h2.title - = markdown escape_once(@milestone.title), pipeline: :single_line - -- if @milestone.complete? && @milestone.active? - .alert.alert-success.prepend-top-default - %span All issues for this milestone are closed. Navigate to the project to close the milestone. - -.table-holder - %table.table - %thead - %tr - %th Project - %th Open issues - %th State - %th Due date - - @milestone.milestones.each do |milestone| - %tr - %td - = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - %td - = milestone.issues.opened.count - %td - - if milestone.closed? - Closed - - else - Open - %td - = milestone.expires_at - -.context - %p.lead - Progress: - #{@milestone.closed_items_count} closed - – - #{@milestone.open_items_count} open - = milestone_progress_bar(@milestone) - -%ul.nav-links.no-top.no-bottom - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab' do - Issues - %span.badge= @milestone.issue_count - %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do - Merge Requests - %span.badge= @milestone.merge_requests_count - %li - = link_to '#tab-participants', 'data-toggle' => 'tab' do - Participants - %span.badge= @milestone.participants.count - -.tab-content - .tab-pane.active#tab-issues - .gray-content-block.middle-block - .pull-right - = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All issues in this milestone - - .row.prepend-top-default - .col-md-6 - = render 'issues', title: "Open", issues: @milestone.opened_issues - .col-md-6 - = render 'issues', title: "Closed", issues: @milestone.closed_issues - - .tab-pane#tab-merge-requests - .gray-content-block.middle-block - .pull-right - = link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All merge requests in this milestone - - .row.prepend-top-default - .col-md-6 - = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests - .col-md-6 - = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests - - .tab-pane#tab-participants - .gray-content-block.middle-block - .oneline - All participants to this milestone - %ul.bordered-list - - @milestone.participants.each do |user| - %li - = link_to user, title: user.name, class: "darken" do - = image_tag avatar_icon(user, 32), class: "avatar s32" - %strong= truncate(user.name, lenght: 40) - %br - %small.cgray= user.username += render 'shared/milestones/top', milestone: @milestone += render 'shared/milestones/summary', milestone: @milestone += render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index f878d36e739..45cfe3da188 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -4,7 +4,10 @@ .todo-title %span.author-name - = link_to_author todo + - if todo.author + = link_to_author(todo) + - else + (removed) %span.todo-label = todo_action_name(todo) = todo_target_link(todo) diff --git a/app/views/explore/projects/_dropdown.html.haml b/app/views/explore/projects/_dropdown.html.haml deleted file mode 100644 index a4b4cd8d6c7..00000000000 --- a/app/views/explore/projects/_dropdown.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_updated - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to explore_projects_filter_path(sort: sort_value_name) do - = sort_title_name - = link_to explore_projects_filter_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to explore_projects_filter_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 39e3e8e2738..cd485da5104 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -10,11 +10,11 @@ %b.caret %ul.dropdown-menu %li - = link_to explore_projects_filter_path(visibility_level: nil) do + = link_to filter_projects_path(visibility_level: nil) do Any - Gitlab::VisibilityLevel.values.each do |level| %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } - = link_to explore_projects_filter_path(visibility_level: level) do + = link_to filter_projects_path(visibility_level: level) do = visibility_level_icon(level) = visibility_level_label(level) @@ -30,11 +30,11 @@ %b.caret %ul.dropdown-menu %li - = link_to explore_projects_filter_path(tag: nil) do + = link_to filter_projects_path(tag: nil) do Any - @tags.each do |tag| %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } - = link_to explore_projects_filter_path(tag: tag.name) do - %i.fa.fa-tag + = link_to filter_projects_path(tag: tag.name) do + = icon('tag') = tag.name diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 794aa57b55a..7cd8e9bea46 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -3,9 +3,10 @@ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| - if @projects.present? = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - - if can? current_user, :create_projects, @group - = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do - = icon('plus') - New Project + = render 'shared/projects/dropdown' + - if can? current_user, :create_projects, @group + = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do + = icon('plus') + New Project = render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml deleted file mode 100644 index 9b85d83d6d8..00000000000 --- a/app/views/groups/milestones/_issue.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid } - %span.milestone-row - - project = issue.project - %strong #{project.name} · - = link_to [project.namespace.becomes(Namespace), project, issue] do - %span.cgray ##{issue.iid} - = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title - .pull-right.assignee-icon - - if issue.assignee - = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/groups/milestones/_issues.html.haml b/app/views/groups/milestones/_issues.html.haml deleted file mode 100644 index 9f350b772bd..00000000000 --- a/app/views/groups/milestones/_issues.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.panel.panel-default - .panel-heading= title - %ul{ class: "well-list issues-sortable-list" } - - if issues - - issues.each do |issue| - = render 'issue', issue: issue diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml deleted file mode 100644 index e3aa4aad198..00000000000 --- a/app/views/groups/milestones/_merge_request.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid } - %span.milestone-row - - project = merge_request.project - %strong #{project.name} · - = link_to [project.namespace.becomes(Namespace), project, merge_request] do - %span.cgray ##{merge_request.iid} - = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title - .pull-right.assignee-icon - - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/groups/milestones/_merge_requests.html.haml b/app/views/groups/milestones/_merge_requests.html.haml deleted file mode 100644 index 50057e2c636..00000000000 --- a/app/views/groups/milestones/_merge_requests.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.panel.panel-default - .panel-heading= title - %ul{ class: "well-list merge_requests-sortable-list" } - - if merge_requests - - merge_requests.each do |merge_request| - = render 'merge_request', merge_request: merge_request diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml index a20bf75bc39..4c4e0a26728 100644 --- a/app/views/groups/milestones/_milestone.html.haml +++ b/app/views/groups/milestones/_milestone.html.haml @@ -1,29 +1,5 @@ -%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } - .row - .col-sm-6 - %strong - = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) - .col-sm-6 - .pull-right.light #{milestone.percent_complete}% complete - .row - .col-sm-6 - = link_to issues_group_path(@group, milestone_title: milestone.title) do - = pluralize milestone.issue_count, 'Issue' - · - = link_to merge_requests_group_path(@group, milestone_title: milestone.title) do - = pluralize milestone.merge_requests_count, 'Merge Request' - .col-sm-6 - = milestone_progress_bar(milestone) - .row - .col-sm-6 - %div - - milestone.milestones.each do |milestone| - = link_to milestone_path(milestone) do - %span.label.label-gray - = milestone.project.name - .col-sm-6 - - 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 - = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close" += render 'shared/milestones/milestone', + milestone_path: group_milestone_path(@group, milestone.safe_title, title: milestone.title), + issues_path: issues_group_path(@group, milestone_title: milestone.title), + merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title), + milestone: milestone diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 1233da85524..fb6f0da28f8 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,112 +1,4 @@ -- page_title @milestone.title, "Milestones" = render "header_title" - -.detail-page-header - .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } - - if @milestone.closed? - Closed - - else - Open - %span.identifier - Milestone #{@milestone.title} - .pull-right - - 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-grouped btn-close" - - else - = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" - -.detail-page-description.gray-content-block.second-block - %h2.title - = markdown escape_once(@milestone.title), pipeline: :single_line - -- if @milestone.complete? && @milestone.active? - .alert.alert-success.prepend-top-default - %span All issues for this milestone are closed. You may close the milestone now. - -.table-holder - %table.table - %thead - %tr - %th Project - %th Open issues - %th State - %th Due date - - @milestone.milestones.each do |milestone| - %tr - %td - = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - %td - = milestone.issues.opened.count - %td - - if milestone.closed? - Closed - - else - Open - %td - = milestone.expires_at - -.context - %p.lead - Progress: - #{@milestone.closed_items_count} closed - – - #{@milestone.open_items_count} open - = milestone_progress_bar(@milestone) - -%ul.nav-links.no-top.no-bottom - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab' do - Issues - %span.badge= @milestone.issue_count - %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do - Merge Requests - %span.badge= @milestone.merge_requests_count - %li - = link_to '#tab-participants', 'data-toggle' => 'tab' do - Participants - %span.badge= @milestone.participants.count - -.tab-content - .tab-pane.active#tab-issues - .gray-content-block.middle-block - .pull-right - = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All issues in this milestone - - .row.prepend-top-default - .col-md-6 - = render 'issues', title: "Open", issues: @milestone.opened_issues - .col-md-6 - = render 'issues', title: "Closed", issues: @milestone.closed_issues - - .tab-pane#tab-merge-requests - .gray-content-block.middle-block - .pull-right - = link_to 'Browse Merge Requests', merge_requests_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All merge requests in this milestone - - .row.prepend-top-default - .col-md-6 - = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests - .col-md-6 - = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests - - .tab-pane#tab-participants - .gray-content-block.middle-block - .oneline - All participants to this milestone - - %ul.bordered-list - - @milestone.participants.each do |user| - %li - = link_to user, title: user.name, class: "darken" do - = image_tag avatar_icon(user, 32), class: "avatar s32" - %strong= truncate(user.name, lenght: 40) - %br - %small.cgray= user.username += render 'shared/milestones/top', milestone: @milestone, group: @group += render 'shared/milestones/summary', milestone: @milestone += render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index 3c11b97921f..18caddabd39 100644 --- a/app/views/projects/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -6,4 +6,4 @@ - blob = sanitize_svg(blob) %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} - else - %img{src: namespace_project_raw_path(@project.namespace, @project, @id)} + %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))} diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml index 882a4d0c5e2..a21ddaf4930 100644 --- a/app/views/projects/branches/destroy.js.haml +++ b/app/views/projects/branches/destroy.js.haml @@ -1 +1 @@ -$('.js-totalbranch-count').html("#{@repository.branches.size}") +$('.js-totalbranch-count').html("#{@repository.branch_count}") diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index ce60fbdf032..bac9e244d36 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -1,11 +1,14 @@ +- commits, hidden = limited_commits(@commits) +- commits = Commit.decorate(commits, @project) + %div.panel.panel-default .panel-heading Commits (#{@commits.count}) - - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + - if hidden > 0 %ul.well-list - - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), @project).each do |commit| + - commits.each do |commit| = render "projects/commits/inline_commit", commit: commit, project: @project %li.warning-row.unstyled - other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues. + #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. - else - %ul.well-list= render Commit.decorate(@commits, @project), project: @project + %ul.well-list= render commits, project: @project diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 6c631228002..a7e3c2478c2 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -1,7 +1,9 @@ - unless defined?(project) - project = @project -- @commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| +- commits, hidden = limited_commits(@commits) + +- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| .row.commits-row .col-md-2.hidden-xs.hidden-sm %h5.commits-row-date @@ -13,3 +15,7 @@ %ul.bordered-list = render commits, project: project %hr.lists-separator + +- if hidden > 0 + .alert.alert-warning + #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 498c5e05b32..7a5b0d993db 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -15,9 +15,9 @@ = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do Branches - %span.badge.js-totalbranch-count= @repository.branches.size + %span.badge.js-totalbranch-count= @repository.branch_count = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags - %span.badge.js-totaltags-count= @repository.tags.length + %span.badge.js-totaltags-count= @repository.tag_count diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 7b5e2991c09..b262892ac65 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -34,6 +34,8 @@ %span into = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do = @merge_request.target_branch + - if @merge_request.open? && @merge_request.diverged_from_target_branch? + %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/widget/show.html.haml" diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml deleted file mode 100644 index ca51b8c745d..00000000000 --- a/app/views/projects/milestones/_issue.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } - %span - = link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title - .issue-detail - = link_to [@project.namespace.becomes(Namespace), @project, issue] do - %span.issue-number ##{issue.iid} - - issue.labels.each do |label| - = render_colored_label(label) - - if issue.assignee - = image_tag avatar_icon(issue.assignee, 16), class: "avatar s24", alt: '' diff --git a/app/views/projects/milestones/_issues.html.haml b/app/views/projects/milestones/_issues.html.haml deleted file mode 100644 index 6f8a341e478..00000000000 --- a/app/views/projects/milestones/_issues.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -.panel.panel-default - .panel-heading - = title - .pull-right= issues.size - %ul{ class: "well-list issues-sortable-list", id: "issues-list-#{id}", "data-state" => id } - - issues.sort_by(&:position).each do |issue| - = render 'issue', issue: issue diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml deleted file mode 100644 index a1033607c5d..00000000000 --- a/app/views/projects/milestones/_merge_request.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) } - %span.str-truncated - = link_to [@project.namespace.becomes(Namespace), @project, merge_request] do - %span.cgray ##{merge_request.iid} - = link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title - .pull-right.assignee-icon - - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/projects/milestones/_merge_requests.html.haml b/app/views/projects/milestones/_merge_requests.html.haml deleted file mode 100644 index 9a5a02af215..00000000000 --- a/app/views/projects/milestones/_merge_requests.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -.panel.panel-default - .panel-heading= title - %ul{ class: "well-list merge_requests-sortable-list", id: "merge_requests-list-#{id}", "data-state" => id } - - merge_requests.sort_by(&:position).each do |merge_request| - = render 'merge_request', merge_request: merge_request diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 67d95ab0364..77b566db6b6 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -1,31 +1,5 @@ -%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) } - .row - .col-sm-6 - %strong - = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - - .col-sm-6 - .pull-right.light #{milestone.percent_complete}% complete - .row - .col-sm-6 - = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do - = pluralize milestone.issues.count, 'Issue' - · - = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do - = pluralize milestone.merge_requests.count, 'Merge Request' - .col-sm-6 - = milestone_progress_bar(milestone) - - .row - .col-sm-6 - = render 'shared/milestone_expired', milestone: milestone - .col-sm-6 - - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do - = icon('pencil-square-o') - Edit - \ - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close" - = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do - = icon('trash-o') - Delete += render 'shared/milestones/milestone', + milestone_path: namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), + issues_path: namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title), + merge_requests_path: namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title), + milestone: milestone diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 2cae1ac4e2c..b4597043a27 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -42,102 +42,9 @@ = preserve do = markdown @milestone.description -- if @milestone.issues.any? && @milestone.can_be_closed? +- if @milestone.complete? && @milestone.active? .alert.alert-success.prepend-top-default %span All issues for this milestone are closed. You may close milestone now. -.context.prepend-top-default - .milestone-summary - %h4 Progress - %strong= @milestone.issues.count - issues: - %span.milestone-stat - %strong= @milestone.open_items_count - open and - %strong= @milestone.closed_items_count - closed - %span.milestone-stat - %strong== #{@milestone.percent_complete}% - complete - %span.milestone-stat - %span.remaining-days= milestone_remaining_days(@milestone) - %span.pull-right.tab-issues-buttons - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do - %i.fa.fa-plus - New Issue - - if can?(current_user, :read_issue, @project) - = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" - %span.pull-right.tab-merge-requests-buttons.hidden - - if can?(current_user, :read_merge_request, @project) - = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" - - = milestone_progress_bar(@milestone) - -%ul.nav-links.no-top.no-bottom - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do - Issues - %span.badge= @issues.count - %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do - Merge Requests - %span.badge= @merge_requests.count - %li - = link_to '#tab-participants', 'data-toggle' => 'tab' do - Participants - %span.badge= @users.count - %li - = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do - Labels - %span.badge= @labels.count - -.tab-content.milestone-content - .tab-pane.active#tab-issues - .row.prepend-top-default - .col-md-4 - = render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned') - .col-md-4 - = render('issues', title: 'Ongoing Issues (open and assigned)', issues: @issues.opened.assigned, id: 'ongoing') - .col-md-4 - = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed') - - .tab-pane#tab-merge-requests - .row.prepend-top-default - .col-md-3 - = render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned') - .col-md-3 - = render('merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: @merge_requests.opened.assigned, id: 'ongoing') - .col-md-3 - = render('merge_requests', title: 'Rejected (closed)', merge_requests: @merge_requests.closed, id: 'closed') - .col-md-3 - .panel.panel-primary - .panel-heading Merged - %ul.well-list - - @merge_requests.merged.each do |merge_request| - = render 'merge_request', merge_request: merge_request - - .tab-pane#tab-participants - %ul.bordered-list - - @users.each do |user| - %li - = link_to user, title: user.name, class: "darken" do - = image_tag avatar_icon(user, 32), class: "avatar s32" - %strong= truncate(user.name, lenght: 40) - %br - %small.cgray= user.username - - .tab-pane#tab-labels - %ul.bordered-list.manage-labels-list - - @labels.each do |label| - %li - = render_colored_label(label) - - args = [@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title, label_name: label.title] - - options = args.extract_options! - - %span.issues-count - = link_to namespace_project_issues_path(*args, options.merge(state: 'opened')) do - = pluralize label.open_issues_count, 'open issue' - %span.issues-count - = link_to namespace_project_issues_path(*args, options.merge(state: 'closed')) do - = pluralize label.closed_issues_count, 'closed issue' += render 'shared/milestones/summary', milestone: @milestone, project: @project += render 'shared/milestones/tabs', milestone: @milestone diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml new file mode 100644 index 00000000000..f7c6fc14adf --- /dev/null +++ b/app/views/shared/milestones/_issuable.html.haml @@ -0,0 +1,25 @@ +-# @project is present when viewing Project's milestone +- project = @project || issuable.project +- assignee = issuable.assignee +- issuable_type = issuable.class.table_name +- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type] + +%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) } + %span + - if show_project_name + %strong #{project.name} · + - elsif show_full_project_name + %strong #{project.name_with_namespace} · + = link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title + %div{class: 'issuable-detail'} + = link_to [project.namespace.becomes(Namespace), project, issuable] do + %span{ class: 'issuable-number' }>= issuable.to_reference + + - issuable.labels.each do |label| + = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do + - render_colored_label(label) + + - if assignee + = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), + class: 'has_tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do + - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '') diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml new file mode 100644 index 00000000000..8619939dde7 --- /dev/null +++ b/app/views/shared/milestones/_issuables.html.haml @@ -0,0 +1,16 @@ +- show_counter = local_assigns.fetch(:show_counter, false) +- primary = local_assigns.fetch(:primary, false) +- panel_class = primary ? 'panel-primary' : 'panel-default' + +.panel{ class: panel_class } + .panel-heading + = title + - if show_counter + .pull-right= issuables.size + + - class_prefix = dom_class(issuables).pluralize + %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } + = render partial: 'shared/milestones/issuable', + collection: issuables.sort_by(&:position), + as: :issuable, + locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name } diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml new file mode 100644 index 00000000000..a8db7f8a556 --- /dev/null +++ b/app/views/shared/milestones/_issues_tab.html.haml @@ -0,0 +1,10 @@ +- args = { show_project_name: local_assigns.fetch(:show_project_name, false), + show_full_project_name: local_assigns.fetch(:show_full_project_name, false) } + +.row.prepend-top-default + .col-md-4 + = render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true) + .col-md-4 + = render 'shared/milestones/issuables', args.merge(title: 'Ongoing Issues (open and assigned)', issuables: issues.opened.assigned, id: 'ongoing', show_counter: true) + .col-md-4 + = render 'shared/milestones/issuables', args.merge(title: 'Completed Issues (closed)', issuables: issues.closed, id: 'closed', show_counter: true) diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml new file mode 100644 index 00000000000..ba27bafd1bc --- /dev/null +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -0,0 +1,18 @@ +%ul.bordered-list.manage-labels-list + - labels.each do |label| + - options = { milestone_title: @milestone.title, label_name: label.title } + + %li + %span.label-row + = link_to milestones_label_path(options) do + - render_colored_label(label) + %span.prepend-left-10 + = markdown(label.description, pipeline: :single_line) + + .pull-right + %strong.issues-count + = link_to milestones_label_path(options.merge(state: 'opened')) do + - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' + %strong.issues-count + = link_to milestones_label_path(options.merge(state: 'closed')) do + - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml new file mode 100644 index 00000000000..c29d8ee6737 --- /dev/null +++ b/app/views/shared/milestones/_merge_requests_tab.haml @@ -0,0 +1,12 @@ +- args = { show_project_name: local_assigns.fetch(:show_project_name, false), + show_full_project_name: local_assigns.fetch(:show_full_project_name, false) } + +.row.prepend-top-default + .col-md-3 + = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned') + .col-md-3 + = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing') + .col-md-3 + = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed') + .col-md-3 + = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true) diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml new file mode 100644 index 00000000000..f01138af3f0 --- /dev/null +++ b/app/views/shared/milestones/_milestone.html.haml @@ -0,0 +1,45 @@ +- dashboard = local_assigns[:dashboard] +- custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first) + +%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id } + .row + .col-sm-6 + %strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path + .col-sm-6 + .pull-right.light #{milestone.percent_complete}% complete + .row + .col-sm-6 + = link_to pluralize(milestone.issues.size, 'Issue'), issues_path + · + = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path + .col-sm-6= milestone_progress_bar(milestone) + - if milestone.is_a?(GlobalMilestone) + .row + .col-sm-6 + .expiration= render('shared/milestone_expired', milestone: milestone) + .projects + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = dashboard ? milestone.project.name_with_namespace : milestone.project.name + - if @group + .col-sm-6 + - 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 + = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close" + + - if @project + .row + .col-sm-6= render('shared/milestone_expired', milestone: milestone) + .col-sm-6 + - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? + = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do + = icon('pencil-square-o') + Edit + \ + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close" + = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do + = icon('trash-o') + Delete diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml new file mode 100644 index 00000000000..67ae85ac276 --- /dev/null +++ b/app/views/shared/milestones/_participants_tab.html.haml @@ -0,0 +1,8 @@ +%ul.bordered-list + - users.each do |user| + %li + = link_to user, title: user.name, class: "darken" do + = image_tag avatar_icon(user, 32), class: "avatar s32" + %strong= truncate(user.name, lenght: 40) + %br + %small.cgray= user.username diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml new file mode 100644 index 00000000000..59d4ae29f79 --- /dev/null +++ b/app/views/shared/milestones/_summary.html.haml @@ -0,0 +1,28 @@ +- project = local_assigns[:project] + +.context.prepend-top-default + .milestone-summary + %h4 Progress + %strong= milestone.issues.size + issues: + %span.milestone-stat + %strong= milestone.issues.opened.size + open and + %strong= milestone.issues.closed.size + closed + %span.milestone-stat + %strong== #{milestone.percent_complete}% + complete + + %span.milestone-stat + %span.remaining-days= milestone_remaining_days(milestone) + %span.pull-right.tab-issues-buttons + - if project && can?(current_user, :create_issue, project) + = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do + %i.fa.fa-plus + New Issue + = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped" + %span.pull-right.tab-merge-requests-buttons.hidden + = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped" + + = milestone_progress_bar(milestone) diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml new file mode 100644 index 00000000000..57d7ee85a3b --- /dev/null +++ b/app/views/shared/milestones/_tabs.html.haml @@ -0,0 +1,30 @@ +%ul.nav-links.no-top.no-bottom + %li.active + = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + Issues + %span.badge= milestone.issues.size + %li + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + Merge Requests + %span.badge= milestone.merge_requests.size + %li + = link_to '#tab-participants', 'data-toggle' => 'tab' do + Participants + %span.badge= milestone.participants.count + %li + = link_to '#tab-labels', 'data-toggle' => 'tab' do + Labels + %span.badge= milestone.labels.count + +- show_project_name = local_assigns.fetch(:show_project_name, false) +- show_full_project_name = local_assigns.fetch(:show_full_project_name, false) + +.tab-content.milestone-content + .tab-pane.active#tab-issues + = render 'shared/milestones/issues_tab', issues: milestone.issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane#tab-merge-requests + = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane#tab-participants + = render 'shared/milestones/participants_tab', users: milestone.participants + .tab-pane#tab-labels + = render 'shared/milestones/labels_tab', labels: milestone.labels diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml new file mode 100644 index 00000000000..4cf1d948b5b --- /dev/null +++ b/app/views/shared/milestones/_top.html.haml @@ -0,0 +1,58 @@ +- page_title milestone.title, "Milestones" + +- group = local_assigns[:group] + +.detail-page-header + .status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" } + - if milestone.closed? + Closed + - elsif milestone.expired? + Expired + - else + Open + %span.identifier + Milestone #{milestone.title} + - if milestone.expires_at + %span.creator + · + = milestone.expires_at + - if group + .pull-right + - 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-grouped btn-close" + - else + = link_to 'Reopen Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" + +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(milestone.title), pipeline: :single_line + +- if milestone.complete? && milestone.active? + .alert.alert-success.prepend-top-default + - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' + %span All issues for this milestone are closed. #{close_msg} + +.table-holder + %table.table + %thead + %tr + %th Project + %th Open issues + %th State + %th Due date + - milestone.milestones.each do |ms| + %tr + %td + - project_name = group ? ms.project.name : ms.project.name_with_namespace + = link_to project_name, namespace_project_milestone_path(ms.project.namespace, ms.project, ms) + %td + = ms.issues.opened.count + %td + - if ms.closed? + Closed + - else + Open + %td + = ms.expires_at + diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml new file mode 100644 index 00000000000..e7e04621ff4 --- /dev/null +++ b/app/views/shared/projects/_dropdown.html.haml @@ -0,0 +1,22 @@ +- @sort ||= sort_value_recently_updated +- archived = params[:archived] +.dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + = projects_sort_options_hash[@sort] + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %li.dropdown-header + Sort by + - projects_sort_options_hash.each do |value, title| + %li + = link_to filter_projects_path(sort: value, archived: archived), class: ("is-active" if @sort == value) do + = title + + %li.divider + %li + = link_to filter_projects_path(sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do + Hide archived projects + %li + = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do + Show archived projects diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 99e48e86e38..97cfb76cdb0 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -28,6 +28,9 @@ = project.name .controls + - if project.main_language + %span + = project.main_language - if ci_commit %span = render_ci_status(ci_commit) diff --git a/config/environments/test.rb b/config/environments/test.rb index d6842affa6c..f96ac6f9753 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -7,8 +7,6 @@ Rails.application.configure do # and recreated between test runs. Don't rely on the data there! config.cache_classes = false - config.cache_store = :null_store - # Configure static asset server for tests with Cache-Control for performance config.serve_static_files = true config.static_cache_control = "public, max-age=3600" diff --git a/db/migrate/20160229193553_add_main_language_to_repository.rb b/db/migrate/20160229193553_add_main_language_to_repository.rb new file mode 100644 index 00000000000..b5446c6a447 --- /dev/null +++ b/db/migrate/20160229193553_add_main_language_to_repository.rb @@ -0,0 +1,5 @@ +class AddMainLanguageToRepository < ActiveRecord::Migration + def change + add_column :projects, :main_language, :string + end +end diff --git a/db/migrate/20160305220806_remove_expires_at_from_snippets.rb b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb new file mode 100644 index 00000000000..fc12b5b09e6 --- /dev/null +++ b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb @@ -0,0 +1,5 @@ +class RemoveExpiresAtFromSnippets < ActiveRecord::Migration + def change + remove_column :snippets, :expires_at, :datetime + end +end diff --git a/db/migrate/20160309140734_fix_todos.rb b/db/migrate/20160309140734_fix_todos.rb new file mode 100644 index 00000000000..ebe0fc82305 --- /dev/null +++ b/db/migrate/20160309140734_fix_todos.rb @@ -0,0 +1,16 @@ +class FixTodos < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM todos + WHERE todos.target_type IN ('Commit', 'ProjectSnippet') + OR NOT EXISTS ( + SELECT * + FROM projects + WHERE projects.id = todos.project_id + ) + SQL + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 71d9257a31e..a74b86d8e2f 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: 20160222153918) do +ActiveRecord::Schema.define(version: 20160309140734) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -697,6 +697,7 @@ ActiveRecord::Schema.define(version: 20160222153918) do t.integer "build_timeout", default: 3600, null: false t.boolean "pending_delete", default: false t.boolean "public_builds", default: true, null: false + t.string "main_language" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree @@ -777,7 +778,6 @@ ActiveRecord::Schema.define(version: 20160222153918) do t.datetime "created_at" t.datetime "updated_at" t.string "file_name" - t.datetime "expires_at" t.string "type" t.integer "visibility_level", default: 0, null: false end @@ -785,7 +785,6 @@ ActiveRecord::Schema.define(version: 20160222153918) do add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree - add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree diff --git a/doc/api/commits.md b/doc/api/commits.md index e4d436b8e52..6341440c58b 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -213,8 +213,7 @@ Example response: ## Commit status -Since GitLab 8.1, this is the new commit status API. The documentation in -[ci/api/commits](../ci/api/commits.md) is deprecated. +Since GitLab 8.1, this is the new commit status API. ### Get the status of a commit diff --git a/doc/api/notes.md b/doc/api/notes.md index d4d63e825ab..85d4f0bafa2 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -145,7 +145,6 @@ Parameters: "state": "active", "created_at": "2013-09-30T13:46:01Z" }, - "expires_at": null, "updated_at": "2013-10-02T07:34:20Z", "created_at": "2013-10-02T07:34:20Z" } diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index a7acf37b5bc..fb802102e3a 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -51,7 +51,6 @@ Parameters: "state": "active", "created_at": "2012-05-23T08:00:58Z" }, - "expires_at": null, "updated_at": "2012-06-28T10:52:04Z", "created_at": "2012-06-28T10:52:04Z" } diff --git a/doc/api/users.md b/doc/api/users.md index b7fc903825e..82c57a2fd43 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -151,6 +151,8 @@ Parameters: "name": "John Smith", "state": "active", "created_at": "2012-05-23T08:00:58Z", + "confirmed_at": "2012-05-23T08:00:58Z", + "last_sign_in_at": "2015-03-23T08:00:58Z", "bio": null, "skype": "", "linkedin": "", diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md index cf9710ede57..aea808007fc 100644 --- a/doc/ci/api/README.md +++ b/doc/ci/api/README.md @@ -1,86 +1,22 @@ # GitLab CI API +## Purpose + +Main purpose of GitLab CI API is to provide necessary data and context for +GitLab CI Runners. + +For consumer API take a look at this [documentation](../../api/README.md) where +you will find all relevant information. + +## API Prefix + +Current CI API prefix is `/ci/api/v1`. + +You need to prepend this prefix to all examples in this documentation, like: + + GET /ci/api/v1/builds/:id/artifacts + ## Resources -- [Projects](projects.md) -- [Runners](runners.md) -- [Commits](commits.md) - [Builds](builds.md) - - -## Authentication - -GitLab CI API uses different types of authentication depends on what API you use. -Each API document has section with information about authentication you need to use. - -GitLab CI API has 4 authentication methods: - -* GitLab user token & GitLab url -* GitLab CI project token -* GitLab CI runners registration token -* GitLab CI runner token - - -### Authentication #1: GitLab user token & GitLab url - -Authentication is done by -sending the `private-token` of a valid user and the `url` of an -authorized GitLab instance via a query string along with the API -request: - - GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/ - -If preferred, you may instead send the `private-token` as a header in -your request: - - curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://gitlab.example.com/ci/api/v1/projects?url=http://demo.gitlab.com/" - - -### Authentication #2: GitLab CI project token - -Each project in GitLab CI has it own token. -It can be used to get project commits and builds information. -You can use project token only for certain project. - -### Authentication #3: GitLab CI runners registration token - -This token is not persisted and is generated on each application start. -It can be used only for registering new runners in system. You can find it on -GitLab CI Runners web page https://gitlab-ci.example.com/admin/runners - -### Authentication #4: GitLab CI runner token - -Every GitLab CI runner has it own token that allow it to receive and update -GitLab CI builds. This token exists of internal purposes and should be used only -by runners - -## JSON - -All API requests are serialized using JSON. You don't need to specify -`.json` at the end of API URL. - -## Status codes - -The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave. - -API request types: - -- `GET` requests access one or more resources and return the result as JSON -- `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON -- `GET`, `PUT` and `DELETE` return `200 OK` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON -- `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not. - -The following list shows the possible return codes for API requests. - -Return values: - -- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON -- `201 Created` - The `POST` request was successful and the resource is returned as JSON -- `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given -- `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above -- `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project -- `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found -- `405 Method Not Allowed` - The request is not supported -- `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists -- `422 Unprocessable` - The entity could not be processed -- `500 Server Error` - While handling the request something went wrong on the server side +- [Runners](runners.md) diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md index 018ca22dbbd..d100e261178 100644 --- a/doc/ci/api/builds.md +++ b/doc/ci/api/builds.md @@ -1,85 +1,73 @@ # Builds API -This API used by runners to receive and update builds. +API used by runners to receive and update builds. -__Authentication is done by runner token__ +_**Note:** This API is intended to be used only by Runners as their own +communication channel. For the consumer API see the +[Builds API](../../api/builds.md)._ + +## Authentication + +This API uses two types of authentication: + +1. Unique runner's token + + Token assigned to runner after it has been registered. + +2. Using build authorization token + + This is project's CI token that can be found in Continuous Integration + project settings. + + Build authorization token can be passed as a parameter or a value of + `BUILD-TOKEN` header. This method are interchangeable. ## Builds ### Runs oldest pending build by runner - POST /ci/builds/register + POST /ci/api/v1/builds/register Parameters: - * `token` (required) - The unique token of runner + * `token` (required) - Unique runner token -Returns: - -```json -{ - "id": 48584, - "ref": "0.1.1", - "tag": true, - "sha": "d63117656af6ff57d99e50cc270f854691f335ad", - "status": "success", - "name": "pages", - "token": "9dd60b4f1a439d1765357446c1084c", - "stage": "test", - "project_id": 479, - "project_name": "test", - "commands": "echo commands", - "repo_url": "http://gitlab-ci-token:token@gitlab.example/group/test.git", - "before_sha": "0000000000000000000000000000000000000000", - "allow_git_fetch": false, - "options": { - "image": "docker:image", - "artifacts": { - "paths": [ - "public" - ] - }, - "cache": { - "paths": [ - "vendor" - ] - } - }, - "timeout": 3600, - "variables": [ - { - "key": "CI_BUILD_TAG", - "value": "0.1.1", - "public": true - } - ], - "depends_on_builds": [ - { - "id": 48584, - "ref": "0.1.1", - "tag": true, - "sha": "d63117656af6ff57d99e50cc270f854691f335ad", - "status": "success", - "name": "build", - "token": "9dd60b4f1a439d1765357446c1084c", - "stage": "build", - "project_id": 479, - "project_name": "test", - "artifacts_file": { - "filename": "artifacts.zip", - "size": 0 - } - } - ] -} -``` ### Update details of an existing build - PUT /ci/builds/:id + PUT /ci/api/v1/builds/:id Parameters: * `id` (required) - The ID of a project + * `token` (required) - Unique runner token * `state` (optional) - The state of a build * `trace` (optional) - The trace of a build + +### Upload artifacts to build + + POST /ci/api/v1/builds/:id/artifacts + +Parameters: + + * `id` (required) - The ID of a build + * `token` (required) - The build authorization token + * `file` (required) - Artifacts file + +### Download the artifacts file from build + + GET /ci/api/v1/builds/:id/artifacts + +Parameters: + + * `id` (required) - The ID of a build + * `token` (required) - The build authorization token + +### Remove the artifacts file from build + + DELETE /ci/api/v1/builds/:id/artifacts + +Parameters: + + * ` id` (required) - The ID of a build + * `token` (required) - The build authorization token diff --git a/doc/ci/api/commits.md b/doc/ci/api/commits.md deleted file mode 100644 index 871de7abcce..00000000000 --- a/doc/ci/api/commits.md +++ /dev/null @@ -1,108 +0,0 @@ -# Commits API - -**DEPRECATED** - -Since GitLab 8.1, there is a new commit status API. Please see the [revised -documentation](../../api/commits.md#commit-status). - ---- - -__Authentication is done by GitLab CI project token__ - -## Commits - -### Retrieve all commits per project - -Get list of commits per project - - GET /ci/commits - -Parameters: - - * `project_id` (required) - The ID of a project - * `project_token` (requires) - Project token - * `page` (optional) - * `per_page` (optional) - items per request (default is 20) - -Returns: - -```json -[{ - "id": 3, - "ref": "master", - "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", - "project_id": 2, - "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", - "created_at": "2014-11-05T09:46:35.247Z", - "status": "success", - "finished_at": "2014-11-05T09:46:44.254Z", - "duration": 5.062692165374756, - "git_commit_message": "wow\n", - "git_author_name": "Administrator", - "git_author_email": "admin@example.com", - "builds": [{ - "id": 7, - "project_id": 2, - "ref": "master", - "status": "success", - "finished_at": "2014-11-05T09:46:44.254Z", - "created_at": "2014-11-05T09:46:35.259Z", - "updated_at": "2014-11-05T09:46:44.255Z", - "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", - "started_at": "2014-11-05T09:46:39.192Z", - "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", - "runner_id": 1, - "coverage": null, - "commit_id": 3 - }] -}] -``` - -### Create commit - -Inform GitLab CI about new commit you want it to build. - -__If commit already exists in GitLab CI it will not be created__ - - - POST /ci/commits - -Parameters: - - * `project_id` (required) - The ID of a project - * `project_token` (requires) - Project token - * `data` (required) - Push data. For example see comment in `lib/api/commits.rb` - -Returns: - -```json -{ - "id": 3, - "ref": "master", - "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", - "project_id": 2, - "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", - "created_at": "2014-11-05T09:46:35.247Z", - "status": "success", - "finished_at": "2014-11-05T09:46:44.254Z", - "duration": 5.062692165374756, - "git_commit_message": "wow\n", - "git_author_name": "Administrator", - "git_author_email": "admin@example.com", - "builds": [{ - "id": 7, - "project_id": 2, - "ref": "master", - "status": "success", - "finished_at": "2014-11-05T09:46:44.254Z", - "created_at": "2014-11-05T09:46:35.259Z", - "updated_at": "2014-11-05T09:46:44.255Z", - "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", - "started_at": "2014-11-05T09:46:39.192Z", - "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", - "runner_id": 1, - "coverage": null, - "commit_id": 3 - }] -} -``` diff --git a/doc/ci/api/projects.md b/doc/ci/api/projects.md deleted file mode 100644 index fe6b1c01352..00000000000 --- a/doc/ci/api/projects.md +++ /dev/null @@ -1,149 +0,0 @@ -# Projects API - -This API is intended to aid in the setup and configuration of -projects on GitLab CI. - -__Authentication is done by GitLab user token & GitLab url__ - -## Projects - -### List Authorized Projects - -Lists all projects that the authenticated user has access to. - -``` -GET /ci/projects -``` - -Returns: - -```json -[ - { - "id" : 271, - "name" : "gitlabhq", - "timeout" : 1800, - "token" : "iPWx6WM4lhHNedGfBpPJNP", - "default_ref" : "master", - "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell", - "path" : "gitlab/gitlab-shell", - "always_build" : false, - "polling_interval" : null, - "public" : false, - "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", - "gitlab_id" : 3 - }, - { - "id" : 272, - "name" : "gitlab-ci", - "timeout" : 1800, - "token" : "iPWx6WM4lhHNedGfBpPJNP", - "default_ref" : "master", - "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell", - "path" : "gitlab/gitlab-shell", - "always_build" : false, - "polling_interval" : null, - "public" : false, - "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", - "gitlab_id" : 4 - } -] -``` - -### List Owned Projects - -Lists all projects that the authenticated user owns. - -``` -GET /ci/projects/owned -``` - -Returns: - -```json -[ - { - "id" : 272, - "name" : "gitlab-ci", - "timeout" : 1800, - "token" : "iPWx6WM4lhHNedGfBpPJNP", - "default_ref" : "master", - "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell", - "path" : "gitlab/gitlab-shell", - "always_build" : false, - "polling_interval" : null, - "public" : false, - "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", - "gitlab_id" : 4 - } -] -``` - -### Single Project - -Returns information about a single project for which the user is -authorized. - - GET /ci/projects/:id - -Parameters: - - * `id` (required) - The ID of the GitLab CI project - -### Create Project - -Creates a GitLab CI project using GitLab project details. - - POST /ci/projects - -Parameters: - - * `name` (required) - The name of the project - * `gitlab_id` (required) - The ID of the project on the GitLab instance - * `default_ref` (optional) - The branch to run on (default to `master`) - -### Update Project - -Updates a GitLab CI project using GitLab project details that the -authenticated user has access to. - - PUT /ci/projects/:id - -Parameters: - - * `name` - The name of the project - * `default_ref` - The branch to run on (default to `master`) - -### Remove Project - -Removes a GitLab CI project that the authenticated user has access to. - - DELETE /ci/projects/:id - -Parameters: - - * `id` (required) - The ID of the GitLab CI project - -### Link Project to Runner - -Links a runner to a project so that it can make builds (only via -authorized user). - - POST /ci/projects/:id/runners/:runner_id - -Parameters: - - * `id` (required) - The ID of the GitLab CI project - * `runner_id` (required) - The ID of the GitLab CI runner - -### Remove Project from Runner - -Removes a runner from a project so that it can not make builds (only -via authorized user). - - DELETE /ci/projects/:id/runners/:runner_id - -Parameters: - - * `id` (required) - The ID of the GitLab CI project - * `runner_id` (required) - The ID of the GitLab CI runner \ No newline at end of file diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md index e9033aeacd5..2f01da4bd76 100644 --- a/doc/ci/api/runners.md +++ b/doc/ci/api/runners.md @@ -1,81 +1,46 @@ # Runners API +API used by runners to register and delete themselves. + _**Note:** This API is intended to be used only by Runners as their own communication channel. For the consumer API see the [new Runners API](../../api/runners.md)._ +## Authentication + +This API uses two types of authentication: + +1. Unique runner's token + + Token assigned to runner after it has been registered. + +2. Using runners' registration token + + This is a token that can be found in project's settings. + It can be also found in Admin area » Runners settings. + + There are two types of tokens you can pass - shared runner registration + token or project specific registration token. + ## Runners -### Retrieve all runners - -__Authentication is done by GitLab user token & GitLab url__ - -Used to get information about all runners registered on the GitLab CI -instance. - - GET /ci/runners - -Returns: - -```json -[ - { - "id" : 85, - "token" : "12b68e90394084703135" - }, - { - "id" : 86, - "token" : "76bf894e969364709864" - }, -] -``` - ### Register a new runner - -__Authentication is done with a Shared runner registration token or a project Specific runner registration token__ - Used to make GitLab CI aware of available runners. - POST /ci/runners/register + POST /ci/api/v1/runners/register Parameters: - * `token` (required) - The registration token. It is 2 types of token you can pass here. + * `token` (required) - Registration token -1. Shared runner registration token -2. Project specific registration token - -Returns: - -```json -{ - "id" : 85, - "token" : "12b68e90394084703135" -} -``` ### Delete a runner +Used to remove runner. -__Authentication is done by runner token__ - -Used to removing runners. - - DELETE /ci/runners/delete + DELETE /ci/api/v1/runners/delete Parameters: - * `token` (required) - The runner token. - -Returns: - -```json -{ - "id" : 1, - "token" : "d14963981a428f70121777e50643d1", - "created_at" : "2015-02-26T11:39:39.232Z", - "updated_at" : "2015-02-26T11:39:39.232Z", - "description" : "awesome runner" -} -``` + * `token` (required) - Unique runner token diff --git a/doc/development/README.md b/doc/development/README.md index b9a0d81e5ba..f5c3107ff44 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -9,4 +9,5 @@ - [Rake tasks](rake_tasks.md) for development - [Shell commands](shell_commands.md) in the GitLab codebase - [Sidekiq debugging](sidekiq_debugging.md) +- [SQL guidelines](sql.md) for SQL guidelines - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements diff --git a/doc/development/sql.md b/doc/development/sql.md new file mode 100644 index 00000000000..23fd7604957 --- /dev/null +++ b/doc/development/sql.md @@ -0,0 +1,219 @@ +# SQL Query Guidelines + +This document describes various guidelines to follow when writing SQL queries, +either using ActiveRecord/Arel or raw SQL queries. + +## Using LIKE Statements + +The most common way to search for data is using the `LIKE` statement. For +example, to get all issues with a title starting with "WIP:" you'd write the +following query: + +```sql +SELECT * +FROM issues +WHERE title LIKE 'WIP:%'; +``` + +On PostgreSQL the `LIKE` statement is case-sensitive. On MySQL this depends on +the case-sensitivity of the collation, which is usually case-insensitive. To +perform a case-insensitive `LIKE` on PostgreSQL you have to use `ILIKE` instead. +This statement in turn isn't supported on MySQL. + +To work around this problem you should write `LIKE` queries using Arel instead +of raw SQL fragments as Arel automatically uses `ILIKE` on PostgreSQL and `LIKE` +on MySQL. This means that instead of this: + +```ruby +Issue.where('title LIKE ?', 'WIP:%') +``` + +You'd write this instead: + +```ruby +Issue.where(Issue.arel_table[:title].matches('WIP:%')) +``` + +Here `matches` generates the correct `LIKE` / `ILIKE` statement depending on the +database being used. + +If you need to chain multiple `OR` conditions you can also do this using Arel: + +```ruby +table = Issue.arel_table + +Issue.where(table[:title].matches('WIP:%').or(table[:foo].matches('WIP:%'))) +``` + +For PostgreSQL this produces: + +```sql +SELECT * +FROM issues +WHERE (title ILIKE 'WIP:%' OR foo ILIKE 'WIP:%') +``` + +In turn for MySQL this produces: + +```sql +SELECT * +FROM issues +WHERE (title LIKE 'WIP:%' OR foo LIKE 'WIP:%') +``` + +## LIKE & Indexes + +Neither PostgreSQL nor MySQL use any indexes when using `LIKE` / `ILIKE` with a +wildcard at the start. For example, this will not use any indexes: + +```sql +SELECT * +FROM issues +WHERE title ILIKE '%WIP:%'; +``` + +Because the value for `ILIKE` starts with a wildcard the database is not able to +use an index as it doesn't know where to start scanning the indexes. + +MySQL provides no known solution to this problem. Luckily PostgreSQL _does_ +provide a solution: trigram GIN indexes. These indexes can be created as +follows: + +```sql +CREATE INDEX [CONCURRENTLY] index_name_here +ON table_name +USING GIN(column_name gin_trgm_ops); +``` + +The key here is the `GIN(column_name gin_trgm_ops)` part. This creates a [GIN +index][gin-index] with the operator class set to `gin_trgm_ops`. These indexes +_can_ be used by `ILIKE` / `LIKE` and can lead to greatly improved performance. +One downside of these indexes is that they can easily get quite large (depending +on the amount of data indexed). + +To keep naming of these indexes consistent please use the following naming +pattern: + + index_TABLE_on_COLUMN_trigram + +For example, a GIN/trigram index for `issues.title` would be called +`index_issues_on_title_trigram`. + +Due to these indexes taking quite some time to be built they should be built +concurrently. This can be done by using `CREATE INDEX CONCURRENTLY` instead of +just `CREATE INDEX`. Concurrent indexes can _not_ be created inside a +transaction. Transactions for migrations can be disabled using the following +pattern: + +```ruby +class MigrationName < ActiveRecord::Migration + disable_ddl_transaction! +end +``` + +For example: + +```ruby +class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_username ON users (LOWER(username));' + execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_email ON users (LOWER(email));' + end + + def down + return unless Gitlab::Database.postgresql? + + remove_index :users, :index_on_users_lower_username + remove_index :users, :index_on_users_lower_email + end +end +``` + +## Plucking IDs + +This can't be stressed enough: **never** use ActiveRecord's `pluck` to pluck a +set of values into memory only to use them as an argument for another query. For +example, this will make the database **very** sad: + +```ruby +projects = Project.all.pluck(:id) + +MergeRequest.where(source_project_id: projects) +``` + +Instead you can just use sub-queries which perform far better: + +```ruby +MergeRequest.where(source_project_id: Project.all.select(:id)) +``` + +The _only_ time you should use `pluck` is when you actually need to operate on +the values in Ruby itself (e.g. write them to a file). In almost all other cases +you should ask yourself "Can I not just use a sub-query?". + +## Use UNIONs + +UNIONs aren't very commonly used in most Rails applications but they're very +powerful and useful. In most applications queries tend to use a lot of JOINs to +get related data or data based on certain criteria, but JOIN performance can +quickly deteriorate as the data involved grows. + +For example, if you want to get a list of projects where the name contains a +value _or_ the name of the namespace contains a value most people would write +the following query: + +```sql +SELECT * +FROM projects +JOIN namespaces ON namespaces.id = projects.namespace_id +WHERE projects.name ILIKE '%gitlab%' +OR namespaces.name ILIKE '%gitlab%'; +``` + +Using a large database this query can easily take around 800 milliseconds to +run. Using a UNION we'd write the following instead: + +```sql +SELECT projects.* +FROM projects +WHERE projects.name ILIKE '%gitlab%' + +UNION + +SELECT projects.* +FROM projects +JOIN namespaces ON namespaces.id = projects.namespace_id +WHERE namespaces.name ILIKE '%gitlab%'; +``` + +This query in turn only takes around 15 milliseconds to complete while returning +the exact same records. + +This doesn't mean you should start using UNIONs everywhere, but it's something +to keep in mind when using lots of JOINs in a query and filtering out records +based on the joined data. + +GitLab comes with a `Gitlab::SQL::Union` class that can be used to build a UNION +of multiple `ActiveRecord::Relation` objects. You can use this class as +follows: + +```ruby +union = Gitlab::SQL::Union.new([projects, more_projects, ...]) + +Project.from("(#{union.to_sql}) projects") +``` + +## Ordering by Creation Date + +When ordering records based on the time they were created you can simply order +by the `id` column instead of ordering by `created_at`. Because IDs are always +unique and incremented in the order that rows are created this will produce the +exact same results. This also means there's no need to add an index on +`created_at` to ensure consistent performance as `id` is already indexed by +default. + +[gin-index]: http://www.postgresql.org/docs/current/static/gin.html diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index 0f2665a3bf7..5a4b2f43ba7 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore webhooks as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** +Please explore [web hooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. diff --git a/doc/install/installation.md b/doc/install/installation.md index c1787a7c6a8..0fd54be58b0 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -467,12 +467,15 @@ NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit proj ### Initial Login -Visit YOUR_SERVER in your web browser for your first GitLab login. The setup has created a default admin account for you. You can use it to log in: +Visit YOUR_SERVER in your web browser for your first GitLab login. - root - 5iveL!fe +If you didn't [provide a root password during setup](#initialize-database-and-activate-advanced-features), +you'll be redirected to a password reset screen to provide the password for the +initial administrator account. Enter your desired password and you'll be +redirected back to the login screen. -**Important Note:** On login you'll be prompted to change the password. +The default account's username is **root**. Provide the password you created +earlier and login. After login you can change the username if you wish. **Enjoy!** diff --git a/doc/integration/README.md b/doc/integration/README.md index 281eea8363d..7c8f785a61f 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -39,3 +39,34 @@ please see the [project_services directory][projects-code]. [jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html [Project Service]: ../project_services/project_services.md [projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services + +## SSL certificate errors + +When trying to integrate GitLab with services that are using self-signed certificates, +it is very likely that SSL certificate errors will occur on different parts of the +application, most likely Sidekiq. There are 2 approaches you can take to solve this: + +1. Add the root certificate to the trusted chain of the OS. +1. If using Omnibus, you can add the certificate to GitLab's trusted certificates. + +**OS main trusted chain** + +This [resource](http://kb.kerio.com/product/kerio-connect/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html) +has all the information you need to add a certificate to the main trusted chain. + +This [answer](http://superuser.com/questions/437330/how-do-you-add-a-certificate-authority-ca-to-ubuntu) +at SuperUser also has relevant information. + +**Omnibus Trusted Chain** + +It is enough to concatenate the certificate to the main trusted certificate: + +```bash +cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem +``` + +After that restart GitLab with: + +```bash +sudo gitlab-ctl restart +``` diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index f256477196b..cf1f98492ea 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -204,3 +204,25 @@ When setting `method: ssl`, the underlying authentication method used by `omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with the LDAP server before any LDAP-protocol data is exchanged but no validation of the LDAP server's SSL certificate is performed. + +## Troubleshooting + +### Invalid credentials when logging in + +Make sure the user you are binding with has enough permissions to read the user's +tree and traverse it. + +Also make sure that the `user_filter` is not blocking otherwise valid users. + +To make sure that the LDAP settings are correct and GitLab can see your users, +execute the following command: + + +```bash +# For Omnibus installations +sudo gitlab-rake gitlab:ldap:check + +# For installations from source +sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production +``` + diff --git a/doc/integration/saml.md b/doc/integration/saml.md index c84113556cd..148c4ac1886 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -133,12 +133,16 @@ will be returned to GitLab and will be signed in. ## Troubleshooting +### 500 error after login + If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, this likely indicates that GitLab could not get the email address for the SAML user. Make sure the IdP provides a claim containing the user's email address, using claim name `email` or `mail`. +### Redirect back to login screen with no evident error + If after signing in into your SAML server you are redirected back to the sign in page and no error is displayed, check your `production.log` file. It will most likely contain the message `Can't verify CSRF token authenticity`. This means that there is an error during @@ -147,4 +151,36 @@ the SAML request, but this error never reaches GitLab due to the CSRF check. To bypass this you can add `skip_before_action :verify_authenticity_token` to the `omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab, where it can then be seen in the usual logs, or as a flash message in the login -screen. \ No newline at end of file +screen. + +### Invalid audience + +This error means that the IdP doesn't recognize GitLab as a valid sender and +receiver of SAML requests. Make sure to add the GitLab callback URL to the approved +audiences of the IdP server. + +### Missing claims + +The IdP server needs to pass certain information in order for GitLab to either +create an account, or match the login information to an existing account. `email` +is the minimum amount of information that needs to be passed. If the IdP server +is not providing this information, all SAML requests will fail. + +Make sure this information is provided. + +### Key validation error, Digest mismatch or Fingerprint mismatch + +These errors all come from a similar place, the SAML certificate. SAML requests +need to be validated using a fingerprint, a certificate or a validator. + +For this you need take the following into account: + +- If no certificate is provided in the settings, a fingerprint or fingerprint + validator needs to be provided and the response from the server must contain + a certificate (``) +- If a certificate is provided in the settings, it is no longer necessary for + the request to contain one. In this case the fingerprint or fingerprint + validators are optional + +Make sure that one of the above described scenarios is valid, or the requests will +fail with one of the mentioned errors. \ No newline at end of file diff --git a/doc/integration/slack.md b/doc/integration/slack.md index ecbe0d3e887..f6ba80f46d5 100644 --- a/doc/integration/slack.md +++ b/doc/integration/slack.md @@ -2,19 +2,11 @@ ## On Slack -To enable Slack integration you must create an Incoming WebHooks integration on Slack; +To enable Slack integration you must create an Incoming WebHooks integration on Slack: 1. [Sign in to Slack](https://slack.com/signin) -1. Select **Apps & Custom Integrations** from the dropdown next to your team name. - -1. Click the **Configure** link (right-upper corner). - -1. Select the **Custom integrations** tab. - -1. Click the **Incoming WebHooks** row. - -1. Click the **Add configuration** button. +1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/) 1. Choose the channel name you want to send notifications to. diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index c400cdac64d..cbf57db5684 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -207,6 +207,7 @@ GFM also recognizes certain cross-project references: | `namespace/project$123` | snippet | | `namespace/project@9ba12248` | specific commit | | `namespace/project@9ba12248...b19a04f5` | commit range comparison | +| `namespace/project~"Some label"` | issues with given label | ## Task Lists diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 7c12557a321..27170c1eb19 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -219,3 +219,16 @@ You can see from the above image that there are four references to GitLab: [JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" [jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service" [8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post" + +## Troubleshooting + +### GitLab is unable to comment on a ticket + +Make sure that the user you set up for GitLab to communicate with JIRA has the +correct access permission to post comments on a ticket and to also transition the +ticket, if you'd like GitLab to also take care of closing them. + +### GitLab is unable to close a ticket + +Make sure the the `Transition ID` you set within the JIRA settings matches the +one your project needs to close a ticket. diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md index 408a17ac348..0a9cb5683e7 100644 --- a/doc/update/8.4-to-8.5.md +++ b/doc/update/8.4-to-8.5.md @@ -64,6 +64,9 @@ sudo -u git -H bundle install --without postgres development test --deployment # PostgreSQL installations (note: the line below states '--without mysql') sudo -u git -H bundle install --without mysql development test --deployment +# Optional: clean up old gems +sudo -u git -H bundle clean + # Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index a10e62877ba..f446ed0a35b 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -62,7 +62,13 @@ sudo -u git -H bundle install --without development test mysql --deployment # MySQL sudo -u git -H bundle install --without development test postgres --deployment +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production ``` diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index fd0327686b1..5fa39ef1b0a 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -4,7 +4,7 @@ Although deprecated, if someone wants to make this script into a gem or otherwise improve it merge requests are welcome. -*Make sure you view this [upgrade guide from the 'master' branch](../../../master/doc/update/upgrader.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the 'master' branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/upgrader.md) for the most up to date instructions.* GitLab Upgrader - a ruby script that allows you easily upgrade GitLab to latest minor version. diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index b82306bd1da..e2b53c45ab1 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -582,7 +582,6 @@ X-Gitlab-Event: Note Hook "created_at": "2015-04-09 02:40:38 UTC", "updated_at": "2015-04-09 02:40:38 UTC", "file_name": "test.rb", - "expires_at": null, "type": "ProjectSnippet", "visibility_level": 0 } diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md index 5076b2697a3..36cb9da2380 100644 --- a/doc/workflow/lfs/lfs_administration.md +++ b/doc/workflow/lfs/lfs_administration.md @@ -9,7 +9,8 @@ Documentation on how to use Git LFS are under [Managing large binary files with ## Configuration -Git LFS objects can be large in size. By default, they are stored on the server GitLab is installed on. +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: @@ -37,5 +38,8 @@ In `config/gitlab.yml`: ## Known limitations -* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported +* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) + is not supported * Currently, removing LFS objects from GitLab Git LFS storage is not supported +* LFS authentications via SSH is not supported for the time being +* Only compatible with the GitLFS client versions 1.1.0 or 1.0.2. diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index b59e92cb317..ba91685a20b 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -1,17 +1,21 @@ # 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. +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. +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. +Git LFS makes this simpler for the end user by removing the requirement to +learn new commands. ## 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. +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. ## GitLab server configuration @@ -24,15 +28,19 @@ Documentation for GitLab instance administrators is under [LFS administration do ## Known limitations -* Git LFS v1 original API is not supported since it was deprecated early in LFS development +* Git LFS v1 original API is not supported since it was deprecated early in LFS + development * 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 -* 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) +* Any Git LFS request will ask for HTTPS credentials to be provided so good Git + credentials store is recommended +* 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) ## 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: +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 @@ -40,7 +48,8 @@ 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: +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 @@ -49,13 +58,17 @@ 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 ``` -Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP authentication. +Cloning the repository works the same as before. Git automatically detects the +LFS-tracked files and clones them via HTTP. If you performed the git clone +command with a SSH URL, you have to enter your GitLab credentials for HTTP +authentication. ```bash git clone git@gitlab.example.com:group/project.git ``` -If you already cloned the repository and you want to get the latest LFS object that are on the remote repository, eg. from branch `master`: +If you already cloned the repository and you want to get the latest LFS object +that are on the remote repository, eg. from branch `master`: ```bash git lfs fetch master @@ -73,8 +86,8 @@ Check if you have permissions to push to the project or fetch from the project. * Project is not allowed to access the LFS object -LFS object you are trying to push to the project or fetch from the project is not available to the project anymore. -Probably the object was removed from the server. +LFS object you are trying to push to the project or fetch from the project is not +available to the project anymore. Probably the object was removed from the server. * Local git repository is using deprecated LFS API @@ -89,16 +102,26 @@ git lfs logs last If the status `error 501` is shown, it is because: -* Git LFS support is not enabled on the GitLab server. Check with your GitLab administrator why Git LFS is not enabled on the server. See [LFS administration documentation](lfs_administration.md) for instructions on how to enable LFS support. +* Git LFS support is not enabled on the GitLab server. Check with your GitLab + administrator why Git LFS is not enabled on the server. See + [LFS administration documentation](lfs_administration.md) for instructions + on how to enable LFS support. -* Git LFS client version is not supported by GitLab server. Check your Git LFS version with `git lfs version`. 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 to update your Git LFS client. Only version 1.0.1 and newer are supported. +* Git LFS client version is not supported by GitLab server. Check your Git LFS + version with `git lfs version`. 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 to update your Git LFS client. Only version 1.0.1 and + newer are supported. ### getsockopt: connection refused -If you push a LFS object to a project and you receive an error similar to: `Post /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. +If you push a LFS object to a project and you receive an error similar to: +`Post /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. +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: @@ -109,18 +132,24 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/o ### 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. +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). +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: +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. +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, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). +If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. +For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). -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 +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/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature index 69b3a776441..bed9282f1c6 100644 --- a/features/dashboard/archived_projects.feature +++ b/features/dashboard/archived_projects.feature @@ -10,3 +10,8 @@ Feature: Dashboard Archived Projects Scenario: I should see non-archived projects on dashboard Then I should see "Shop" project link And I should not see "Forum" project link + + Scenario: I toggle show of archived projects on dashboard + When I click "Show archived projects" link + Then I should see "Shop" project link + And I should see "Forum" project link diff --git a/features/explore/projects.feature b/features/explore/projects.feature index 7df6b6f09ba..092e18d1b86 100644 --- a/features/explore/projects.feature +++ b/features/explore/projects.feature @@ -140,4 +140,4 @@ Feature: Explore Projects When I visit the explore starred projects Then I should see project "Community" And I should see project "Internal" - And I should see project "Archive" + And I should not see project "Archive" diff --git a/features/group/milestones.feature b/features/group/milestones.feature index 62ea66a783c..d6c05df9840 100644 --- a/features/group/milestones.feature +++ b/features/group/milestones.feature @@ -28,3 +28,20 @@ Feature: Group Milestones And I fill milestone name When I press create mileston button Then milestone in each project should be created + + Scenario: I should see Issues listed with labels + Given Group has projects with milestones + When I visit group "Owned" page + And I click on group milestones + And I click on one group milestone + Then I should see the "bug" label + And I should see the "feature" label + And I should see the project name in the Issue row + + Scenario: I should see the Labels tab + Given Group has projects with milestones + When I visit group "Owned" page + And I click on group milestones + And I click on one group milestone + And I click on the "Labels" tab + Then I should see the list of labels diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 89af58dcef3..ff21c7d1b83 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -58,14 +58,6 @@ Feature: Project Issues Then I should see comment "XML attached" And I should see an error alert section within the comment form - @javascript - Scenario: Visiting Issues after leaving a comment - Given I visit issue page "Release 0.4" - And I leave a comment like "XML attached" - And I visit project "Shop" issues page - And I sort the list by "Last updated" - Then I should see "Release 0.4" at the top - @javascript Scenario: Visiting Issues after being sorted the list Given I visit project "Shop" issues page diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 495f25f28e7..f8d9fe1854d 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -26,6 +26,16 @@ Feature: Project Merge Requests When I visit project "Shop" merge requests page Then I should see "other_branch" branch + Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target + Given project "Shop" have "Bug NS-07" open merge request with rebased branch + When I visit merge request page "Bug NS-07" + Then I should not see the diverged commits count + + Scenario: I should see the numbers of diverged commits if the branch diverged from the target + Given project "Shop" have "Bug NS-08" open merge request with diverged branch + When I visit merge request page "Bug NS-08" + Then I should see the diverged commits count + Scenario: I should see rejected merge requests Given I click link "Closed" Then I should see "Feature NS-03" in merge requests @@ -76,15 +86,6 @@ Feature: Project Merge Requests And I leave a comment like "XML attached" Then I should see comment "XML attached" - @javascript - Scenario: Visiting Merge Requests after leaving a comment - Given project "Shop" have "Bug NS-05" open merge request with diffs inside - And I visit merge request page "Bug NS-04" - And I leave a comment like "XML attached" - And I visit project "Shop" merge requests page - And I sort the list by "Last updated" - Then I should see "Bug NS-04" at the top - @javascript Scenario: Visiting Merge Requests after being sorted the list Given I visit project "Shop" merge requests page @@ -118,16 +119,6 @@ Feature: Project Merge Requests And I sort the list by "Least popular" Then The list should be sorted by "Least popular" - @javascript - Scenario: Visiting Merge Requests after commenting on diffs - Given project "Shop" have "Bug NS-05" open merge request with diffs inside - And I visit merge request page "Bug NS-05" - And I click on the Changes tab - And I leave a comment like "Line is wrong" on diff - And I visit project "Shop" merge requests page - And I sort the list by "Last updated" - Then I should see "Bug NS-05" at the top - @javascript Scenario: I comment on a merge request diff Given project "Shop" have "Bug NS-05" open merge request with diffs inside diff --git a/features/steps/dashboard/archived_projects.rb b/features/steps/dashboard/archived_projects.rb index 36e092f50c6..6510f8d9b32 100644 --- a/features/steps/dashboard/archived_projects.rb +++ b/features/steps/dashboard/archived_projects.rb @@ -19,4 +19,8 @@ class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps step 'I should see "Forum" project link' do expect(page).to have_link "Forum" end + + step 'I click "Show archived projects" link' do + click_link "Show archived projects" + end end diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index 2363ad797fa..a167d259837 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -24,6 +24,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps end step 'I click on one group milestone' do + milestones = Milestone.where(title: 'GL-113') + @global_milestone = GlobalMilestone.new('GL-113', milestones) + click_link 'GL-113' end @@ -33,7 +36,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps step 'I should see group milestone with all issues and MRs assigned to that milestone' do expect(page).to have_content('Milestone GL-113') - expect(page).to have_content('Progress: 0 closed – 3 open') + expect(page).to have_content('3 issues: 3 open and 0 closed') issue = Milestone.find_by(name: 'GL-113').issues.first expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue)) end @@ -60,6 +63,39 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps end end + step 'I should see the "bug" label' do + page.within('#tab-issues') do + expect(page).to have_content 'bug' + end + end + + step 'I should see the "feature" label' do + page.within('#tab-issues') do + expect(page).to have_content 'bug' + end + end + + step 'I should see the project name in the Issue row' do + page.within('#tab-issues') do + @global_milestone.projects.each do |project| + expect(page).to have_content project.name + end + end + end + + step 'I click on the "Labels" tab' do + page.within('.nav-links') do + page.find(:xpath, "//a[@href='#tab-labels']").click + end + end + + step 'I should see the list of labels' do + page.within('#tab-labels') do + expect(page).to have_content 'bug' + expect(page).to have_content 'feature' + end + end + private def group_milestone @@ -68,6 +104,10 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path| project = create :project, path: path, group: group milestone = create :milestone, title: "Version 7.2", project: project + + create(:label, project: project, title: 'bug') + create(:label, project: project, title: 'feature') + create :issue, project: project, assignee: current_user, @@ -80,11 +120,14 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps due_date: '2114-08-20', description: 'Lorem Ipsum is simply dummy text' - create :issue, + issue = create :issue, project: project, assignee: current_user, author: current_user, milestone: milestone + + issue.labels << project.labels.find_by(title: 'bug') + issue.labels << project.labels.find_by(title: 'feature') end end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 565bf088b41..d9842ccf95e 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -355,10 +355,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end end - step 'I should see "Release 0.4" at the top' do - expect(page.find('ul.content-list.issues-list li.issue:first-child')).to have_content("Release 0.4") - end - def filter_issue(text) fill_in 'issue_search', with: text end diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index e2eda511497..4faa0f4707c 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -59,7 +59,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps end step 'I should see 3 issues' do - expect(page).to have_selector('#tab-issues li.issue-row', count: 4) + expect(page).to have_selector('#tab-issues li.issuable-row', count: 4) end step 'I click link to remove milestone' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index dde864f5180..c19b15bc9ed 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -60,7 +60,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps expect(page).not_to have_content "Feature NS-03" end - step 'I should not see "Bug NS-04" in merge requests' do expect(page).not_to have_content "Bug NS-04" end @@ -121,6 +120,22 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps author: project.users.first) end + step 'project "Shop" have "Bug NS-07" open merge request with rebased branch' do + create(:merge_request, :rebased, + title: "Bug NS-07", + source_project: project, + target_project: project, + author: project.users.first) + end + + step 'project "Shop" have "Bug NS-08" open merge request with diverged branch' do + create(:merge_request, :diverged, + title: "Bug NS-08", + source_project: project, + target_project: project, + author: project.users.first) + end + step 'project "Shop" have "Feature NS-03" closed merge request' do create(:closed_merge_request, title: "Feature NS-03", @@ -490,12 +505,16 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end - step 'I should see "Bug NS-05" at the top' do - expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-05") + step 'I should see the diverged commits count' do + page.within ".mr-source-target" do + expect(page).to have_content /([0-9]+ commits behind)/ + end end - step 'I should see "Bug NS-04" at the top' do - expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-04") + step 'I should not see the diverged commits count' do + page.within ".mr-source-target" do + expect(page).not_to have_content /([0-9]+ commit[s]? behind)/ + end end def merge_request diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb index 2685f5fd6b4..4fda0731e2f 100644 --- a/features/steps/project/merge_requests/acceptance.rb +++ b/features/steps/project/merge_requests/acceptance.rb @@ -29,7 +29,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps step 'There is an open Merge Request' do @user = create(:user) @project = create(:project, :public) - @project_member = create(:project_member, user: @user, project: @project, access_level: ProjectMember::DEVELOPER) + @project_member = create(:project_member, :developer, user: @user, project: @project) @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project) end diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb index c5a4cfce6f0..efbc4831ce1 100644 --- a/features/steps/project/merge_requests/revert.rb +++ b/features/steps/project/merge_requests/revert.rb @@ -36,7 +36,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps step 'There is an open Merge Request' do @user = create(:user) @project = create(:project, :public) - @project_member = create(:project_member, user: @user, project: @project, access_level: ProjectMember::DEVELOPER) + @project_member = create(:project_member, :developer, user: @user, project: @project) @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project) end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index eb6df61b8e6..444d6726f99 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -144,11 +144,4 @@ module SharedNote expect(page).to have_content("+1 Awesome!") end end - - step 'I sort the list by "Last updated"' do - find('button.dropdown-toggle.btn').click - page.within('ul.dropdown-menu.dropdown-menu-align-right li') do - click_link "Last updated" - end - end end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 6432a786bfc..da9d1503ebc 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -388,13 +388,19 @@ module SharedPaths end step 'I visit merge request page "Bug NS-04"' do - mr = MergeRequest.find_by(title: "Bug NS-04") - visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) + visit merge_request_path("Bug NS-04") end step 'I visit merge request page "Bug NS-05"' do - mr = MergeRequest.find_by(title: "Bug NS-05") - visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) + visit merge_request_path("Bug NS-05") + end + + step 'I visit merge request page "Bug NS-07"' do + visit merge_request_path("Bug NS-07") + end + + step 'I visit merge request page "Bug NS-08"' do + visit merge_request_path("Bug NS-08") end step 'I visit merge request page "Bug CO-01"' do @@ -503,6 +509,11 @@ module SharedPaths Project.find_by!(name: 'Shop') end + def merge_request_path(title) + mr = MergeRequest.find_by(title: title) + namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) + end + # ---------------------------------------- # Errors # ---------------------------------------- diff --git a/features/support/env.rb b/features/support/env.rb index 62c80b9c948..357d164d87f 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -14,6 +14,7 @@ require 'sidekiq/testing/inline' require_relative 'capybara' require_relative 'db_cleaner' +require_relative 'rerun' %w(select2_helper test_env repo_helpers).each do |f| require Rails.root.join('spec', 'support', f) diff --git a/features/support/rerun.rb b/features/support/rerun.rb new file mode 100644 index 00000000000..8b176c5be89 --- /dev/null +++ b/features/support/rerun.rb @@ -0,0 +1,14 @@ +# The spinach-rerun-reporter doesn't define the on_undefined_step +# See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb +module Spinach + class Reporter + class Rerun + def on_undefined_step(step_data, failure, step_definitions = nil) + super step_data, failure, step_definitions + + # save feature file and scenario line + @rerun << "#{current_feature.filename}:#{current_scenario.line}" + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b021db8fa5b..5b5b8bd044b 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -23,6 +23,8 @@ module API end class UserFull < User + expose :last_sign_in_at + expose :confirmed_at expose :email expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at expose :identities, using: Entities::Identity @@ -141,7 +143,7 @@ module API class ProjectSnippet < Grape::Entity expose :id, :title, :file_name expose :author, using: Entities::UserBasic - expose :expires_at, :updated_at, :created_at + expose :updated_at, :created_at end class ProjectEntity < Grape::Entity diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index cdbaecf8d90..34c38913474 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -94,6 +94,8 @@ module Banzai object_link_filter(link, object_class.link_reference_pattern, link_text: text) end end + + doc end # Replace references (like `!123` for merge requests) in text with links diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 95e7d209119..8147e5ed3c7 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -1,78 +1,47 @@ module Banzai module Filter # HTML filter that replaces label references with links. - class LabelReferenceFilter < ReferenceFilter - # Public: Find label references in text - # - # LabelReferenceFilter.references_in(text) do |match, id, name| - # "#{Label.find(id)}" - # end - # - # text - String text to search. - # - # Yields the String match, an optional Integer label ID, and an optional - # String label name. - # - # Returns a String replaced with the return of the block. - def self.references_in(text) - text.gsub(Label.reference_pattern) do |match| - yield match, $~[:label_id].to_i, $~[:label_name] + class LabelReferenceFilter < AbstractReferenceFilter + def self.object_class + Label + end + + def find_object(project, id) + project.labels.find(id) + end + + def self.references_in(text, pattern = Label.reference_pattern) + text.gsub(pattern) do |match| + yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~ end end - def self.referenced_by(node) - { label: LazyReference.new(Label, node.attr("data-label")) } - end + def references_in(text, pattern = Label.reference_pattern) + text.gsub(pattern) do |match| + project = project_from_ref($~[:project]) + params = label_params($~[:label_id].to_i, $~[:label_name]) + label = project.labels.find_by(params) - def call - replace_text_nodes_matching(Label.reference_pattern) do |content| - label_link_filter(content) - end - - replace_link_nodes_with_href(Label.reference_pattern) do |link, text| - label_link_filter(link, link_text: text) - end - end - - # Replace label references in text with links to the label specified. - # - # text - String text to replace references in. - # - # Returns a String with label references replaced with links. All links - # have `gfm` and `gfm-label` class names attached for styling. - def label_link_filter(text, link_text: nil) - project = context[:project] - - self.class.references_in(text) do |match, id, name| - params = label_params(id, name) - - if label = project.labels.find_by(params) - url = url_for_label(project, label) - klass = reference_class(:label) - data = data_attribute( - original: link_text || match, - project: project.id, - label: label.id - ) - - text = link_text || render_colored_label(label) - - %(#{escape_once(text)}) + if label + yield match, label.id, $~[:project], $~ else match end end end - def url_for_label(project, label) + def url_for_object(label, project) h = Gitlab::Application.routes.url_helpers - h.namespace_project_issues_url( project.namespace, project, label_name: label.name, - only_path: context[:only_path]) + h.namespace_project_issues_url(project.namespace, project, label_name: label.name, + only_path: context[:only_path]) end - def render_colored_label(label) - LabelsHelper.render_colored_label(label) + def object_link_text(object, matches) + if context[:project] == object.project + LabelsHelper.render_colored_label(object) + else + LabelsHelper.render_colored_cross_project_label(object) + end end # Parameters to pass to `Label.find_by` based on the given arguments diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 3f483847efa..46e51a4bf6d 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -76,7 +76,7 @@ module Gitlab project.issues.create!( description: body, title: issue["title"], - state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened', + state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened', author_id: gl_user_id(project, reporter) ) end diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake new file mode 100644 index 00000000000..ebe301c1fc7 --- /dev/null +++ b/lib/tasks/gemojione.rake @@ -0,0 +1,121 @@ +# This task will generate a standard and Retina sprite of all of the current +# Gemojione Emojis, with the accompanying SCSS map. +# +# It will not appear in `rake -T` output, and the dependent gems are not +# included in the Gemfile by default, because this task will only be needed +# occasionally, such as when new Emojis are added to Gemojione. + +begin + require 'sprite_factory' + require 'rmagick' +rescue LoadError + # noop +end + +namespace :gemojione do + task sprite: :environment do + check_requirements! + + SIZE = 20 + RETINA = SIZE * 2 + + Dir.mktmpdir do |tmpdir| + # Copy the Gemojione assets to the temporary folder for resizing + FileUtils.cp_r(Gemojione.index.images_path, tmpdir) + + Dir.chdir(tmpdir) do + Dir["**/*.png"].each do |png| + resize!(File.join(tmpdir, png), SIZE) + end + end + + style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss)) + + # Combine the resized assets into a packed sprite and re-generate the SCSS + SpriteFactory.cssurl = "image-url('$IMAGE')" + SpriteFactory.run!(File.join(tmpdir, 'images'), { + output_style: style_path, + output_image: "app/assets/images/emoji.png", + selector: '.emoji-', + style: :scss, + nocomments: true, + pngcrush: true, + layout: :packed + }) + + # SpriteFactory's SCSS is a bit too verbose for our purposes here, so + # let's simplify it + system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path})) + system(%Q(sed -i '' "s/ no-repeat//" #{style_path})) + + # Append a generic rule that applies to all Emojis + File.open(style_path, 'a') do |f| + f.puts + f.puts <<-CSS.strip_heredoc + .emoji-icon { + background-image: image-url('emoji.png'); + background-repeat: no-repeat; + height: #{SIZE}px; + width: #{SIZE}px; + + @media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (min--moz-device-pixel-ratio: 2), + only screen and (-o-min-device-pixel-ratio: 2/1), + only screen and (min-device-pixel-ratio: 2), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 2dppx) { + background-image: image-url('emoji@2x.png'); + background-size: 840px 820px; + } + } + CSS + end + end + + # Now do it again but for Retina + Dir.mktmpdir do |tmpdir| + # Copy the Gemojione assets to the temporary folder for resizing + FileUtils.cp_r(Gemojione.index.images_path, tmpdir) + + Dir.chdir(tmpdir) do + Dir["**/*.png"].each do |png| + resize!(File.join(tmpdir, png), RETINA) + end + end + + # Combine the resized assets into a packed sprite and re-generate the SCSS + SpriteFactory.run!(File.join(tmpdir, 'images'), { + output_image: "app/assets/images/emoji@2x.png", + style: false, + nocomments: true, + pngcrush: true, + layout: :packed + }) + end + end + + def check_requirements! + return if defined?(SpriteFactory) && defined?(Magick) + + puts <<-MSG.strip_heredoc + This task is disabled by default and should only be run when the Gemojione + gem is updated with new Emojis. + + To enable this task, *temporarily* add the following lines to Gemfile and + re-bundle: + + gem 'sprite-factory' + gem 'rmagick' + MSG + + exit 1 + end + + def resize!(image_path, size) + # Resize the image in-place, save it, and free the object + image = Magick::Image.read(image_path).first + image.resize!(size, size) + image.write(image_path) { self.quality = 100 } + image.destroy! + end +end diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index 3acfc6e2075..01d23b89bb7 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -4,53 +4,59 @@ namespace :spinach do namespace :project do desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features" task :half do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags @project_commits,@project_issues,@project_merge_requests), - ] - run_commands(cmds) + run_spinach_tests('@project_commits,@project_issues,@project_merge_requests') end desc "GitLab | Spinach | Run remaining project spinach features" task :rest do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests), - ] - run_commands(cmds) + run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests') end end desc "GitLab | Spinach | Run project spinach features" task :project do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets), - ] - run_commands(cmds) + run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets') end desc "GitLab | Spinach | Run other spinach features" task :other do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags @admin,@dashboard,@profile,@public,@snippets), - ] - run_commands(cmds) + run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets') + end + + desc "GitLab | Spinach | Run other spinach features" + task :builds do + run_spinach_tests('@builds') end end desc "GitLab | Run spinach" task :spinach do - cmds = [ - %W(rake gitlab:setup), - %W(spinach), - ] - run_commands(cmds) + run_spinach_tests(nil) end -def run_commands(cmds) - cmds.each do |cmd| - system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!") - end +def run_command(cmd) + system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) +end + +def run_spinach_command(args) + run_command(%w(spinach -r rerun) + args) +end + +def run_spinach_tests(tags) + #run_command(%w(rake gitlab:setup)) or raise('gitlab:setup failed!') + + success = run_spinach_command(%W(--tags #{tags})) + 3.times do |_| + break if success + break unless File.exists?('tmp/spinach-rerun.txt') + + tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) + puts '' + puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red + puts '' + sleep(3) + success = run_spinach_command(tests) + end + + raise("spinach tests for #{tags} failed!") unless success end diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index b6f076a90c3..4a7ee7dbb64 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,16 +1,30 @@ #!/bin/bash +retry() { + for i in $(seq 1 3); do + if eval "$@"; then + return 0 + fi + sleep 3s + echo "Retrying..." + done + return 1 +} + if [ -f /.dockerinit ]; then mkdir -p vendor - if [ ! -e vendor/phantomjs_1.9.8-0jessie_amd64.deb ]; then - wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb - mv phantomjs_1.9.8-0jessie_amd64.deb vendor/ - fi - dpkg -i vendor/phantomjs_1.9.8-0jessie_amd64.deb - apt-get update -qq - apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ - libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip + # Install phantomjs package + pushd vendor + if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then + wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb + fi + dpkg -i phantomjs_1.9.8-0jessie_amd64.deb + popd + + # Try to install packages + retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ + libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip' cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml @@ -20,7 +34,7 @@ if [ -f /.dockerinit ]; then cp config/resque.yml.example config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml - export FLAGS=(--path vendor) + export FLAGS=(--path vendor --retry 3) else export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin cp config/database.yml.mysql config/database.yml diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb index 883bbaedd4e..70ed8f3a62e 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::ForksController do let(:user) { create(:user) } - let(:project) { create(:project, visibility_level: Project::PUBLIC) } + let(:project) { create(:project, :public) } let(:forked_project) { Projects::ForkService.new(project, user).execute } let(:group) { create(:group, owner: forked_project.creator) } diff --git a/spec/factories/emails.rb b/spec/factories/emails.rb index ed072213153..9794772ac7d 100644 --- a/spec/factories/emails.rb +++ b/spec/factories/emails.rb @@ -1,14 +1,6 @@ FactoryGirl.define do factory :email do user - email do - FFaker::Internet.email('alias') - end - - factory :another_email do - email do - FFaker::Internet.email('another.alias') - end - end + email { FFaker::Internet.email('alias') } end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 00de7bb5294..ca1c636fce4 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -69,6 +69,16 @@ FactoryGirl.define do target_branch "master" end + trait :rebased do + source_branch "markdown" + target_branch "improve/awesome" + end + + trait :diverged do + source_branch "feature" + target_branch "master" + end + trait :merge_when_build_succeeds do merge_when_build_succeeds true merge_user author diff --git a/spec/factories/personal_snippets.rb b/spec/factories/personal_snippets.rb index b493a6968ff..0f13b2c1020 100644 --- a/spec/factories/personal_snippets.rb +++ b/spec/factories/personal_snippets.rb @@ -1,15 +1,4 @@ FactoryGirl.define do factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do - trait :public do - visibility_level PersonalSnippet::PUBLIC - end - - trait :internal do - visibility_level PersonalSnippet::INTERNAL - end - - trait :private do - visibility_level PersonalSnippet::PRIVATE - end end end diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb index 70fb7595c74..cf3659ba275 100644 --- a/spec/factories/project_members.rb +++ b/spec/factories/project_members.rb @@ -2,6 +2,26 @@ FactoryGirl.define do factory :project_member do user project - access_level { ProjectMember::MASTER } + master + + trait :guest do + access_level ProjectMember::GUEST + end + + trait :reporter do + access_level ProjectMember::REPORTER + end + + trait :developer do + access_level ProjectMember::DEVELOPER + end + + trait :master do + access_level ProjectMember::MASTER + end + + trait :owner do + access_level ProjectMember::OWNER + end end end diff --git a/spec/factories/project_snippets.rb b/spec/factories/project_snippets.rb index 154442bd3db..d681a2c8483 100644 --- a/spec/factories/project_snippets.rb +++ b/spec/factories/project_snippets.rb @@ -1,9 +1,5 @@ FactoryGirl.define do - factory :project_snippet do + factory :project_snippet, parent: :snippet, class: :ProjectSnippet do project - author - title - content - file_name end end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index f7c285cff3a..9de78d68280 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -1,7 +1,5 @@ FactoryGirl.define do factory :service do - type "" - title "GitLab CI" project end end diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb index b9127b3d75e..365f12a0c95 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -12,5 +12,17 @@ FactoryGirl.define do title content file_name + + trait :public do + visibility_level Snippet::PUBLIC + end + + trait :internal do + visibility_level Snippet::INTERNAL + end + + trait :private do + visibility_level Snippet::PRIVATE + end end end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 1b4ffc2d717..7fdc5e5d7aa 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -5,15 +5,14 @@ describe SnippetsFinder do let(:user1) { create :user } let(:group) { create :group } - let(:project1) { create(:empty_project, :public, group: group) } - let(:project2) { create(:empty_project, :private, group: group) } - + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :private, group: group) } context ':all filter' do before do - @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE) - @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL) - @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC) + @snippet1 = create(:personal_snippet, :private) + @snippet2 = create(:personal_snippet, :internal) + @snippet3 = create(:personal_snippet, :public) end it "returns all private and internal snippets" do @@ -31,9 +30,9 @@ describe SnippetsFinder do context ':by_user filter' do before do - @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE, author: user) - @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL, author: user) - @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC, author: user) + @snippet1 = create(:personal_snippet, :private, author: user) + @snippet2 = create(:personal_snippet, :internal, author: user) + @snippet3 = create(:personal_snippet, :public, author: user) end it "returns all public and internal snippets" do @@ -75,9 +74,9 @@ describe SnippetsFinder do context 'by_project filter' do before do - @snippet1 = create(:project_snippet, visibility_level: Snippet::PRIVATE, project: project1) - @snippet2 = create(:project_snippet, visibility_level: Snippet::INTERNAL, project: project1) - @snippet3 = create(:project_snippet, visibility_level: Snippet::PUBLIC, project: project1) + @snippet1 = create(:project_snippet, :private, project: project1) + @snippet2 = create(:project_snippet, :internal, project: project1) + @snippet3 = create(:project_snippet, :public, project: project1) end it "returns public snippets for unauthorized user" do @@ -93,7 +92,7 @@ describe SnippetsFinder do end it "returns all snippets for project members" do - project1.team << [user, :developer] + project1.team << [user, :developer] snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet1, @snippet2, @snippet3) end diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index fe6d42acee2..1772cc3f6a4 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -209,7 +209,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Label by ID: <%= simple_label.to_reference %> - Label by name: <%= Label.reference_prefix %><%= simple_label.name %> -- Label by name in quotes: <%= label.to_reference(:name) %> +- Label by name in quotes: <%= label.to_reference(format: :name) %> - Ignored in code: `<%= simple_label.to_reference %>` - Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link) - Link to label by reference: [Label](<%= label.to_reference %>) diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb index aafc24397a9..cd7596a763d 100644 --- a/spec/helpers/visibility_level_helper_spec.rb +++ b/spec/helpers/visibility_level_helper_spec.rb @@ -58,7 +58,7 @@ describe VisibilityLevelHelper do describe "skip_level?" do describe "forks" do - let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:project) { create(:project, :internal) } let(:fork_project) { create(:forked_project_with_submodules) } before do @@ -74,7 +74,7 @@ describe VisibilityLevelHelper do end describe "non-forked project" do - let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:project) { create(:project, :internal) } it "skips levels" do expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey @@ -84,7 +84,7 @@ describe VisibilityLevelHelper do end describe "Snippet" do - let(:snippet) { create(:snippet, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:snippet) { create(:snippet, :internal) } it "skips levels" do expect(skip_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index b46ccc47605..e2d21f53b7e 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -111,7 +111,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do context 'String-based multi-word references in quotes' do let(:label) { create(:label, name: 'gfm references', project: project) } - let(:reference) { label.to_reference(:name) } + let(:reference) { label.to_reference(format: :name) } it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -176,4 +176,29 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do expect(result[:references][:label]).to eq [label] end end + + describe 'cross project label references' do + let(:another_project) { create(:empty_project, :public) } + let(:project_name) { another_project.name_with_namespace } + let(:label) { create(:label, project: another_project, color: '#00ff00') } + let(:reference) { label.to_reference(project) } + + let!(:result) { reference_filter("See #{reference}") } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ + end + + it 'contains cross project content' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}" + end + end end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb new file mode 100644 index 00000000000..c413132abe5 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Gitlab::BitbucketImport::Importer, lib: true do + before do + Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") + end + + let(:statuses) do + [ + "open", + "resolved", + "on hold", + "invalid", + "duplicate", + "wontfix", + "closed" # undocumented status + ] + end + let(:sample_issues_statuses) do + issues = [] + + statuses.map.with_index do |status, index| + issues << { + local_id: index, + status: status, + title: "Issue #{index}", + content: "Some content to issue #{index}" + } + end + + issues + end + + let(:project_identifier) { 'namespace/repo' } + let(:data) do + { + bb_session: { + bitbucket_access_token: "123456", + bitbucket_access_token_secret: "secret" + } + } + end + let(:project) do + create( + :project, + import_source: project_identifier, + import_data: ProjectImportData.new(data: data) + ) + end + let(:importer) { Gitlab::BitbucketImport::Importer.new(project) } + let(:issues_statuses_sample_data) do + { + count: sample_issues_statuses.count, + issues: sample_issues_statuses + } + end + + context 'issues statuses' do + before do + stub_request( + :get, + "https://bitbucket.org/api/1.0/repositories/#{project_identifier}" + ).to_return(status: 200, body: { has_issues: true }.to_json) + + stub_request( + :get, + "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0" + ).to_return(status: 200, body: issues_statuses_sample_data.to_json) + + sample_issues_statuses.each_with_index do |issue, index| + stub_request( + :get, + "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments" + ).to_return( + status: 200, + body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json + ) + end + end + + it 'map statuses to open or closed' do + importer.execute + + expect(project.issues.where(state: "closed").size).to eq(5) + expect(project.issues.where(state: "opened").size).to eq(2) + end + end +end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 696fbf7e0aa..0614ca1e7c9 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -59,18 +59,42 @@ describe Label, models: true do context 'using id' do it 'returns a String reference to the object' do expect(label.to_reference).to eq "~#{label.id}" - expect(label.to_reference(double('project'))).to eq "~#{label.id}" end end context 'using name' do it 'returns a String reference to the object' do - expect(label.to_reference(:name)).to eq %(~"#{label.name}") + expect(label.to_reference(format: :name)).to eq %(~"#{label.name}") end it 'uses id when name contains double quote' do label = create(:label, name: %q{"irony"}) - expect(label.to_reference(:name)).to eq "~#{label.id}" + expect(label.to_reference(format: :name)).to eq "~#{label.id}" + end + end + + context 'using invalid format' do + it 'raises error' do + expect { label.to_reference(format: :invalid) } + .to raise_error StandardError, /Unknown format/ + end + end + + context 'cross project reference' do + let(:project) { create(:project) } + + context 'using name' do + it 'returns cross reference with label name' do + expect(label.to_reference(project, format: :name)) + .to eq %Q(#{label.project.to_reference}~"#{label.name}") + end + end + + context 'using id' do + it 'returns cross reference with label id' do + expect(label.to_reference(project, format: :id)) + .to eq %Q(#{label.project.to_reference}~#{label.id}) + end end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c51f34034d7..59c40922abb 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -274,6 +274,70 @@ describe MergeRequest, models: true do end end + describe '#diverged_commits_count' do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + + context 'diverged on same repository' do + subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) } + + it 'counts commits that are on target branch but not on source branch' do + expect(subject.diverged_commits_count).to eq(5) + end + end + + context 'diverged on fork' do + subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) } + + it 'counts commits that are on target branch but not on source branch' do + expect(subject.diverged_commits_count).to eq(5) + end + end + + context 'rebased on fork' do + subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: fork_project, target_project: project) } + + it 'counts commits that are on target branch but not on source branch' do + expect(subject.diverged_commits_count).to eq(0) + end + end + + describe 'caching' do + before(:example) do + allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + end + + it 'caches the output' do + expect(subject).to receive(:compute_diverged_commits_count). + once. + and_return(2) + + subject.diverged_commits_count + subject.diverged_commits_count + end + + it 'invalidates the cache when the source sha changes' do + expect(subject).to receive(:compute_diverged_commits_count). + twice. + and_return(2) + + subject.diverged_commits_count + allow(subject).to receive(:source_sha).and_return('123abc') + subject.diverged_commits_count + end + + it 'invalidates the cache when the target sha changes' do + expect(subject).to receive(:compute_diverged_commits_count). + twice. + and_return(2) + + subject.diverged_commits_count + allow(subject).to receive(:target_sha).and_return('123abc') + subject.diverged_commits_count + end + end + end + it_behaves_like 'an editable mentionable' do subject { create(:merge_request) } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 1b1380ce4e2..28f13100d15 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -60,7 +60,7 @@ describe Milestone, models: true do end it "should recover from dividing by zero" do - expect(milestone.issues).to receive(:count).and_return(0) + expect(milestone.issues).to receive(:size).and_return(0) expect(milestone.percent_complete).to eq(0) end end @@ -114,7 +114,6 @@ describe Milestone, models: true do end it { expect(milestone.closed_items_count).to eq(1) } - it { expect(milestone.open_items_count).to eq(2) } it { expect(milestone.total_items_count).to eq(3) } it { expect(milestone.is_empty?).to be_falsey } end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 583937ca748..33085dac4ea 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -24,7 +24,7 @@ require 'spec_helper' describe Note, models: true do describe 'associations' do it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:noteable) } + it { is_expected.to belong_to(:noteable).touch(true) } it { is_expected.to belong_to(:author).class_name('User') } it { is_expected.to have_many(:todos).dependent(:destroy) } diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index cc92eb0bd9f..e0feb606f78 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f9842d23afa..2fa38a5d3d3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -68,6 +68,7 @@ describe Project, models: true do it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe 'modules' do @@ -561,7 +562,7 @@ describe Project, models: true do end describe '#visibility_level_allowed?' do - let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + let(:project) { create(:project, :internal) } context 'when checking on non-forked project' do it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 1c7d66398cb..34866be3395 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -148,6 +148,12 @@ describe Repository, models: true do expect(branch.name).to eq('new_feature') end + + it 'calls the after_create_branch hook' do + expect(repository).to receive(:after_create_branch) + + repository.add_branch(user, 'new_feature', 'master') + end end context 'when pre hooks failed' do @@ -405,7 +411,7 @@ describe Repository, models: true do end end - describe '#expire_branch_ache' do + describe '#expire_branch_cache' do # This method is private but we need it for testing purposes. Sadly there's # no other proper way of testing caching operations. let(:cache) { repository.send(:cache) } @@ -556,11 +562,12 @@ describe Repository, models: true do end end - describe '#before_create_tag' do + describe '#before_push_tag' do it 'flushes the cache' do expect(repository).to receive(:expire_cache) + expect(repository).to receive(:expire_tag_count_cache) - repository.before_create_tag + repository.before_push_tag end end @@ -595,4 +602,89 @@ describe Repository, models: true do repository.after_remove_branch end end + + describe "#main_language" do + it 'shows the main language of the project' do + expect(repository.main_language).to eq("Ruby") + end + + it 'returns nil when the repository is empty' do + allow(repository).to receive(:empty?).and_return(true) + + expect(repository.main_language).to be_nil + end + end + + describe '#before_remove_tag' do + it 'flushes the tag cache' do + expect(repository).to receive(:expire_tag_count_cache) + + repository.before_remove_tag + end + end + + describe '#branch_count' do + it 'returns the number of branches' do + expect(repository.branch_count).to be_an_instance_of(Fixnum) + end + end + + describe '#tag_count' do + it 'returns the number of tags' do + expect(repository.tag_count).to be_an_instance_of(Fixnum) + end + end + + describe '#expire_branch_count_cache' do + let(:cache) { repository.send(:cache) } + + it 'expires the cache' do + expect(cache).to receive(:expire).with(:branch_count) + + repository.expire_branch_count_cache + end + end + + describe '#expire_tag_count_cache' do + let(:cache) { repository.send(:cache) } + + it 'expires the cache' do + expect(cache).to receive(:expire).with(:tag_count) + + repository.expire_tag_count_cache + end + end + + describe '#add_tag' do + it 'adds a tag' do + expect(repository).to receive(:before_push_tag) + + expect_any_instance_of(Gitlab::Shell).to receive(:add_tag). + with(repository.path_with_namespace, '8.5', 'master', 'foo') + + repository.add_tag('8.5', 'master', 'foo') + end + end + + describe '#rm_branch' do + let(:user) { create(:user) } + + it 'removes a branch' do + expect(repository).to receive(:before_remove_branch) + expect(repository).to receive(:after_remove_branch) + + repository.rm_branch(user, 'feature') + end + end + + describe '#rm_tag' do + it 'removes a tag' do + expect(repository).to receive(:before_remove_tag) + + expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag). + with(repository.path_with_namespace, '8.5') + + repository.rm_tag('8.5') + end + end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index eb2dbbdc5a4..7e5b5499aea 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 36461e84c3a..55582aa53d2 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -7,8 +7,8 @@ describe API::API, api: true do 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) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:branch_name) { 'feature' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 175ee861a71..967c34800d0 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -7,8 +7,8 @@ describe API::API, api: true do let(:api_user) { user } let(:user2) { create(:user) } let!(:project) { create(:project, creator_id: user.id) } - let!(:developer) { create(:project_member, user: user, project: project, access_level: ProjectMember::DEVELOPER) } - let!(:reporter) { create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) } + let!(:developer) { create(:project_member, :developer, user: user, project: project) } + let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } let(:commit) { create(:ci_commit, project: project)} let(:build) { create(:ci_build, commit: commit) } diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb index 8017ed97d88..429a24109fd 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_status_spec.rb @@ -6,9 +6,9 @@ describe API::CommitStatus, api: true do let!(:project) { create(:project) } let(:commit) { project.repository.commit } let(:commit_status) { create(:commit_status, commit: ci_commit) } - let(:guest) { create_user(ProjectMember::GUEST) } - let(:reporter) { create_user(ProjectMember::REPORTER) } - let(:developer) { create_user(ProjectMember::DEVELOPER) } + let(:guest) { create_user(:guest) } + let(:reporter) { create_user(:reporter) } + let(:developer) { create_user(:developer) } let(:sha) { commit.id } @@ -201,9 +201,9 @@ describe API::CommitStatus, api: true do end end - def create_user(access_level) + def create_user(access_level_trait) user = create(:user) - create(:project_member, user: user, project: project, access_level: access_level) + create(:project_member, access_level_trait, user: user, project: project) user end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 49acc3368f4..7ff21175c1b 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -6,8 +6,8 @@ describe API::API, api: true do 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) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb index 3fe7efff5ba..fa94e03ec32 100644 --- a/spec/requests/api/fork_spec.rb +++ b/spec/requests/api/fork_spec.rb @@ -12,7 +12,7 @@ describe API::API, api: true do end let(:project_user2) do - create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) + create(:project_member, :guest, user: user2, project: project) end describe 'POST /projects/fork/:id' do diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb index 6358f6a2a4a..4301588b16a 100644 --- a/spec/requests/api/project_members_spec.rb +++ b/spec/requests/api/project_members_spec.rb @@ -6,8 +6,8 @@ describe API::API, api: true do let(:user2) { create(:user) } let(:user3) { create(:user) } let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } - let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) } + let(:project_member) { create(:project_member, :master, user: user, project: project) } + let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } describe "GET /projects/:id/members" do before { project_member } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 2a310f3834d..9f2365a4832 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -12,8 +12,8 @@ describe API::API, api: true do let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) } let(:project3) { create(:project, path: 'project3', creator_id: user.id, namespace: user.namespace) } let(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') } - let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } - let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) } + let(:project_member) { create(:project_member, :master, user: user, project: project) } + let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } let(:project3) do create(:project, diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 0ae63b0afec..7cf4a01d76b 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -9,8 +9,8 @@ describe API::API, api: true do 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) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } describe "GET /projects/:id/repository/tree" do context "authorized user" do diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 78484747d6a..3af61d4b335 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -28,9 +28,9 @@ describe API::Runners, api: true do before do # Set project access for users - create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) - create(:project_member, user: user, project: project2, access_level: ProjectMember::MASTER) - create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) + create(:project_member, :master, user: user, project: project) + create(:project_member, :master, user: user, project: project2) + create(:project_member, :reporter, user: user2, project: project) end describe 'GET /runners' do diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index f966e38cd3e..a15be07ed57 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -8,8 +8,8 @@ describe API::API, api: true do 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) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } describe "GET /projects/:id/repository/tags" do let(:tag_name) { project.repository.tag_names.sort.reverse.first } diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 2a86b60bc4d..0510b77a39b 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -8,8 +8,8 @@ describe API::API do let!(:trigger_token) { 'secure_token' } let!(:trigger_token_2) { 'secure_token_2' } let!(:project) { create(:project, creator_id: user.id) } - let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } - let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:developer) { create(:project_member, :developer, user: user2, project: project) } let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) } let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) } let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') } diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index b82c5c7685f..96e8c8c51f8 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -47,6 +47,8 @@ describe API::API, api: true do expect(json_response.first.keys).to include 'identities' expect(json_response.first.keys).to include 'can_create_project' expect(json_response.first.keys).to include 'two_factor_enabled' + expect(json_response.first.keys).to include 'last_sign_in_at' + expect(json_response.first.keys).to include 'confirmed_at' end end end diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index 9744729ba0c..b1e1053d037 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -6,8 +6,8 @@ describe API::API, api: true do 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!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:developer) { create(:project_member, :developer, user: user2, project: project) } let!(:variable) { create(:ci_variable, project: project) } describe 'GET /projects/:id/variables' do diff --git a/spec/services/delete_tag_service_spec.rb b/spec/services/delete_tag_service_spec.rb new file mode 100644 index 00000000000..5b7ba521812 --- /dev/null +++ b/spec/services/delete_tag_service_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe DeleteTagService, services: true do + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:user) { create(:user) } + let(:service) { described_class.new(project, user) } + + let(:tag) { double(:tag, name: '8.5', target: 'abc123') } + + describe '#execute' do + before do + allow(repository).to receive(:find_tag).and_return(tag) + end + + it 'removes the tag' do + expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag). + and_return(true) + + expect(repository).to receive(:before_remove_tag) + expect(service).to receive(:success) + + service.execute('8.5') + end + end +end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 994585fb32c..82813cee227 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -155,6 +155,23 @@ describe GitPushService, services: true do end end + describe "Updates main language" do + + context "before push" do + it { expect(project.main_language).to eq(nil) } + end + + context "after push" do + before do + @service = execute_service(project, user, @oldrev, @newrev, @ref) + end + + it { expect(@service.update_main_language).to eq(true) } + it { expect(project.main_language).to eq("Ruby") } + end + end + + describe "Web Hooks" do context "execute web hooks" do it "when pushing a branch for the first time" do @@ -254,22 +271,24 @@ describe GitPushService, services: true do allow(project.repository).to receive(:commits_between). and_return([closing_commit]) + + project.team << [commit_author, :master] end context "to default branches" do it "closes issues" do - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(Issue.find(issue.id)).to be_closed end it "adds a note indicating that the issue is now closed" do expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit) - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) end it "doesn't create additional cross-reference notes" do expect(SystemNoteService).not_to receive(:cross_reference) - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) end it "doesn't close issues when external issue tracker is in use" do @@ -277,7 +296,7 @@ describe GitPushService, services: true do # The push still shouldn't create cross-reference notes. expect do - execute_service(project, user, @oldrev, @newrev, 'refs/heads/hurf' ) + execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' ) end.not_to change { Note.where(project_id: project.id, system: true).count } end end @@ -299,7 +318,6 @@ describe GitPushService, services: true do end end - # EE-only tests context "for jira issue tracker" do include JiraServiceHelper @@ -349,7 +367,7 @@ describe GitPushService, services: true do } }.to_json - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_transition_url).with( body: transition_body ).once @@ -360,7 +378,7 @@ describe GitPushService, services: true do body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_comment_url).with( body: comment_body ).once diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 3c06a890163..e8b9e6b9238 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -102,8 +102,8 @@ describe Projects::UpdateService, services: true do describe :visibility_level do let(:user) { create :user, admin: true } - let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } - let(:forked_project) { create :forked_project_with_submodules, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + let(:project) { create(:project, :internal) } + let(:forked_project) { create(:forked_project_with_submodules, :internal) } let(:opts) { {} } before do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8f381f46e57..159fb964171 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,6 +15,7 @@ require 'rspec/rails' require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'benchmark/ips' +require 'rspec/retry' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. @@ -25,6 +26,9 @@ RSpec.configure do |config| config.use_instantiated_fixtures = false config.mock_with :rspec + config.verbose_retry = true + config.display_try_failure_messages = true + config.include Devise::TestHelpers, type: :controller config.include LoginHelpers, type: :feature config.include LoginHelpers, type: :request