diff options
77 files changed, 874 insertions, 668 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c477721f9da..ffefeb6dfd8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,6 +12,8 @@ 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 @@ -21,7 +23,7 @@ before_script: - cp config/gitlab.yml.example config/gitlab.yml - touch log/application.log - touch log/test.log - - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" + - 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 a9742d22015..10984ebd190 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,15 +9,20 @@ v 8.6.0 (unreleased) - 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) - 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.4 - Do not cache requests for badges (including builds badge) @@ -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/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/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/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/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/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/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/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_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/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/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/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/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/schema.rb b/db/schema.rb index 71d9257a31e..4f56f3970db 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: 20160305220806) 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 -## 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/ +## Purpose -If preferred, you may instead send the `private-token` as a header in -your request: +Main purpose of GitLab CI API is to provide necessary data and context for +GitLab CI Runners. - curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://gitlab.example.com/ci/api/v1/projects?url=http://demo.gitlab.com/" +For consumer API take a look at this [documentation](../../api/README.md) where +you will find all relevant information. +## API Prefix -### Authentication #2: GitLab CI project token +Current CI API prefix is `/ci/api/v1`. -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. +You need to prepend this prefix to all examples in this documentation, like: -### Authentication #3: GitLab CI runners registration token + GET /ci/api/v1/builds/:id/artifacts -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: +## Resources -- `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 +- [Builds](builds.md) +- [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 - -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 - } - } - ] -} -``` + * `token` (required) - Unique runner token + ### 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)._ -## Runners - -### Retrieve all runners +## Authentication -__Authentication is done by GitLab user token & GitLab url__ +This API uses two types of authentication: -Used to get information about all runners registered on the GitLab CI -instance. +1. Unique runner's token - GET /ci/runners + Token assigned to runner after it has been registered. -Returns: +2. Using runners' registration token -```json -[ - { - "id" : 85, - "token" : "12b68e90394084703135" - }, - { - "id" : 86, - "token" : "76bf894e969364709864" - }, -] -``` + This is a token that can be found in project's settings. + It can be also found in Admin area » Runners settings. -### Register a new runner + There are two types of tokens you can pass - shared runner registration + token or project specific registration token. +## Runners -__Authentication is done with a Shared runner registration token or a project Specific runner registration token__ +### Register a new runner 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/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/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/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/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/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/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/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_command(cmd) + system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) end -def run_commands(cmds) - cmds.each do |cmd| - system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!") +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..82de51a9a2e 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -2,15 +2,27 @@ if [ -f /.dockerinit ]; then mkdir -p vendor - if [ ! -e vendor/phantomjs_1.9.8-0jessie_amd64.deb ]; then + + # 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 - mv phantomjs_1.9.8-0jessie_amd64.deb vendor/ fi - dpkg -i vendor/phantomjs_1.9.8-0jessie_amd64.deb + dpkg -i phantomjs_1.9.8-0jessie_amd64.deb + popd + + # Try to install packages + for i in $(seq 1 3); do + apt-get update -yqqq || true + + if apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ + libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip; then + break + fi - 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 + sleep 3s + echo "Retrying package installation..." + done cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml @@ -20,7 +32,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/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_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/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/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/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/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..c458d9c9b1b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -561,7 +561,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/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/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..f5c51e46e8b 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 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 |