summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzesiek.bizon@gmail.com>2016-12-09 20:30:39 +0100
committerGrzegorz Bizon <grzesiek.bizon@gmail.com>2016-12-09 20:30:39 +0100
commit0f40ae5f18bc67291dcf9bdd38ef7dcbaf9781ef (patch)
tree04a7f1f0513548d2148c6a5985e96ac638cb333d
parent2ecb65b8c4a3fc277925938e691621e34c449664 (diff)
parent6eae378d82634b3305b2e9e031eecf30834f1700 (diff)
downloadgitlab-ce-0f40ae5f18bc67291dcf9bdd38ef7dcbaf9781ef.tar.gz
Merge branch 'master' into auto-pipelines-vue
* master: (76 commits) Update "Installation from source" guide for 8.15.0 Group links spec update Updates the font weight of button styles because of the change to system fonts Refactor SSH keys docs Improvements to setting up ssh Do not reload diff for merge request made from fork when target branch in fork is updated Add 8.12.10, 8.12.11, and 8.12.12 CHANGELOG.md items Changes after review Fix broken test Adds CHANGELOG entry Adds tests Uniformize props name format Replace commit icon svg logic Replace play icon svg logic Updated JS based on review Fixed group links dropdown to match Update docs to reflect new defaults on omnibus Merge branch 'jej-23867-use-mr-finder-instead-of-access-check' into 'security' Merge branch 'html-safe-diff-line-content' into 'security' Merge branch 'rs-filter-authentication_token' into 'security' Merge branch 'destroy-session' into 'security' ... Conflicts: app/models/ci/pipeline.rb app/models/commit_status.rb app/views/projects/ci/pipelines/_pipeline.html.haml app/views/projects/commit/_pipeline.html.haml app/views/projects/pipelines/_with_tabs.html.haml app/views/projects/pipelines/index.html.haml lib/api/helpers.rb
-rw-r--r--.eslintrc3
-rw-r--r--.gitlab-ci.yml108
-rw-r--r--CHANGELOG.md33
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock8
-rw-r--r--app/assets/javascripts/blob/template_selector.js.es62
-rw-r--r--app/assets/javascripts/dispatcher.js.es61
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es610
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js.es634
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js.es64
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es630
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js.es69
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js.es64
-rw-r--r--app/assets/javascripts/gl_dropdown.js5
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js4
-rw-r--r--app/assets/javascripts/members.js.es665
-rw-r--r--app/assets/javascripts/signin_tabs_memoizer.js.es649
-rw-r--r--app/assets/javascripts/vue_common_component/commit.js.es645
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/asciidoctor.scss27
-rw-r--r--app/assets/stylesheets/framework/awards.scss5
-rw-r--r--app/assets/stylesheets/framework/buttons.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss5
-rw-r--r--app/assets/stylesheets/framework/nav.scss4
-rw-r--r--app/assets/stylesheets/pages/members.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss3
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/concerns/creates_commit.rb6
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/projects/commit_controller.rb16
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects/compare_controller.rb2
-rw-r--r--app/controllers/projects/discussions_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb29
-rw-r--r--app/controllers/projects/todos_controller.rb2
-rw-r--r--app/controllers/registrations_controller.rb5
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/helpers/commits_helper.rb4
-rw-r--r--app/helpers/diff_helper.rb4
-rw-r--r--app/helpers/events_helper.rb6
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/models/ci/pipeline.rb27
-rw-r--r--app/models/ci/stage.rb37
-rw-r--r--app/models/commit.rb41
-rw-r--r--app/models/commit_status.rb16
-rw-r--r--app/models/concerns/has_status.rb5
-rw-r--r--app/models/concerns/milestoneish.rb10
-rw-r--r--app/models/concerns/routable.rb70
-rw-r--r--app/models/merge_request.rb6
-rw-r--r--app/models/namespace.rb18
-rw-r--r--app/models/project.rb97
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/route.rb22
-rw-r--r--app/services/ci/process_pipeline_service.rb4
-rw-r--r--app/services/commits/change_service.rb2
-rw-r--r--app/services/destroy_group_service.rb4
-rw-r--r--app/services/merge_requests/refresh_service.rb10
-rw-r--r--app/uploaders/uploader_helper.rb2
-rw-r--r--app/views/devise/sessions/new.html.haml1
-rw-r--r--app/views/groups/group_members/update.js.haml1
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/notify/pipeline_success_email.html.haml2
-rw-r--r--app/views/notify/pipeline_success_email.text.erb2
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml15
-rw-r--r--app/views/projects/_merge_request_settings.html.haml26
-rw-r--r--app/views/projects/builds/_sidebar.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml8
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml2
-rw-r--r--app/views/projects/commit/_change.html.haml2
-rw-r--r--app/views/projects/commit/_ci_stage.html.haml15
-rw-r--r--app/views/projects/commit/_pipeline.html.haml19
-rw-r--r--app/views/projects/commit/_pipeline_stage.html.haml14
-rw-r--r--app/views/projects/edit.html.haml3
-rw-r--r--app/views/projects/environments/index.html.haml4
-rw-r--r--app/views/projects/forks/error.html.haml6
-rw-r--r--app/views/projects/group_links/update.js.haml1
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/pipelines/_graph.html.haml4
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml18
-rw-r--r--app/views/projects/project_members/update.js.haml1
-rw-r--r--app/views/projects/stage/_graph.html.haml22
-rw-r--r--app/views/projects/stage/_in_stage_group.html.haml (renamed from app/views/projects/commit/_pipeline_status_group.html.haml)0
-rw-r--r--app/views/projects/stage/_stage.html.haml13
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/members/_group.html.haml21
-rw-r--r--app/views/shared/members/_member.html.haml26
-rw-r--r--app/views/shared/milestones/_summary.html.haml6
-rw-r--r--changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml4
-rw-r--r--changelogs/unreleased/23696-fix-diff-view-highlighting.yml4
-rw-r--r--changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml4
-rw-r--r--changelogs/unreleased/24814-pipeline-tabs.yml4
-rw-r--r--changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml5
-rw-r--r--changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml4
-rw-r--r--changelogs/unreleased/25272_fix_comments_tab_disappearing.yml4
-rw-r--r--changelogs/unreleased/25374-svg-as-prop.yml4
-rw-r--r--changelogs/unreleased/api-remove-source-branch.yml4
-rw-r--r--changelogs/unreleased/destroy-session.yml4
-rw-r--r--changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups.yml4
-rw-r--r--changelogs/unreleased/enable-asciidoctor-admonition-icons.yml4
-rw-r--r--changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml4
-rw-r--r--changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml4
-rw-r--r--changelogs/unreleased/fix-milestone-summary.yml4
-rw-r--r--changelogs/unreleased/fix-slack-pipeline-event.yml4
-rw-r--r--changelogs/unreleased/group-members-in-project-members-view.yml4
-rw-r--r--changelogs/unreleased/html-safe-diff-line-content.yml4
-rw-r--r--changelogs/unreleased/issue-events-filter.yml4
-rw-r--r--changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml4
-rw-r--r--changelogs/unreleased/members-dropdowns.yml4
-rw-r--r--changelogs/unreleased/public-tags-api.yml4
-rw-r--r--changelogs/unreleased/remove-has-visible-content-caching.yml4
-rw-r--r--changelogs/unreleased/render-svg-in-diffs-and-notes.yml4
-rw-r--r--changelogs/unreleased/small-emoji-adjustments.yml4
-rw-r--r--changelogs/unreleased/timeago-perf-fix.yml4
-rw-r--r--changelogs/unreleased/update-button-font-weight.yml4
-rw-r--r--config/application.rb3
-rw-r--r--config/initializers/sentry.rb2
-rw-r--r--db/migrate/20161124111390_add_parent_id_to_namespace.rb12
-rw-r--r--db/migrate/20161124111395_add_index_to_parent_id.rb14
-rw-r--r--db/migrate/20161124111402_add_routes_table.rb18
-rw-r--r--db/migrate/20161130095245_fill_routes_table.rb21
-rw-r--r--db/migrate/20161130101252_fill_projects_routes_table.rb22
-rw-r--r--db/migrate/20161202152031_remove_duplicates_from_routes.rb28
-rw-r--r--db/migrate/20161202152035_add_index_to_routes.rb16
-rw-r--r--db/schema.rb25
-rw-r--r--doc/administration/high_availability/database.md4
-rw-r--r--doc/api/build_triggers.md8
-rw-r--r--doc/api/build_variables.md10
-rw-r--r--doc/api/merge_requests.md49
-rw-r--r--doc/api/tags.md7
-rw-r--r--doc/api/users.md51
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/project_services/img/mattermost_console_integrations.pngbin41186 -> 314642 bytes
-rw-r--r--doc/project_services/jira.md1
-rw-r--r--doc/project_services/mattermost_slash_commands.md8
-rw-r--r--doc/ssh/README.md197
-rw-r--r--doc/university/README.md12
-rw-r--r--doc/update/8.14-to-8.15.md8
-rw-r--r--features/steps/group/members.rb7
-rw-r--r--features/steps/project/team_management.rb8
-rw-r--r--lib/api/entities.rb6
-rw-r--r--lib/api/helpers.rb31
-rw-r--r--lib/api/issues.rb263
-rw-r--r--lib/api/merge_requests.rb8
-rw-r--r--lib/api/session.rb4
-rw-r--r--lib/api/tags.rb1
-rw-r--r--lib/api/users.rb16
-rw-r--r--lib/backup/manager.rb19
-rw-r--r--lib/constraints/group_url_constrainer.rb2
-rw-r--r--lib/event_filter.rb38
-rw-r--r--lib/gitlab/asciidoc.rb2
-rw-r--r--lib/gitlab/ci/status/factory.rb43
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb30
-rw-r--r--lib/gitlab/ci/status/stage/common.rb24
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb15
-rw-r--r--lib/gitlab/data_builder/pipeline.rb2
-rw-r--r--lib/gitlab/search_results.rb2
-rw-r--r--spec/controllers/projects/todo_controller_spec.rb15
-rw-r--r--spec/controllers/sessions_controller_spec.rb1
-rw-r--r--spec/factories/ci/stages.rb13
-rw-r--r--spec/features/environments_spec.rb4
-rw-r--r--spec/features/merge_requests/target_branch_spec.rb41
-rw-r--r--spec/features/projects/features_visibility_spec.rb38
-rw-r--r--spec/features/projects/members/group_links_spec.rb9
-rw-r--r--spec/features/projects/members/group_members_spec.rb90
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb70
-rw-r--r--spec/features/u2f_spec.rb9
-rw-r--r--spec/fixtures/api/schemas/user/login.json37
-rw-r--r--spec/fixtures/api/schemas/user/public.json79
-rw-r--r--spec/helpers/diff_helper_spec.rb61
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js.es632
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js.es64
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js.es618
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js.es62
-rw-r--r--spec/javascripts/fixtures/event_filter.html.haml4
-rw-r--r--spec/javascripts/fixtures/signin_tabs.html.haml5
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js.es653
-rw-r--r--spec/javascripts/vue_common_components/commit_spec.js.es637
-rw-r--r--spec/lib/constraints/group_url_constrainer_spec.rb7
-rw-r--r--spec/lib/event_filter_spec.rb15
-rw-r--r--spec/lib/gitlab/backup/manager_spec.rb127
-rw-r--r--spec/lib/gitlab/ci/status/factory_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/status/stage/common_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb37
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/search_results_spec.rb16
-rw-r--r--spec/models/ci/pipeline_spec.rb55
-rw-r--r--spec/models/ci/stage_spec.rb133
-rw-r--r--spec/models/commit_range_spec.rb15
-rw-r--r--spec/models/commit_spec.rb11
-rw-r--r--spec/models/commit_status_spec.rb45
-rw-r--r--spec/models/concerns/has_status_spec.rb2
-rw-r--r--spec/models/concerns/routable_spec.rb67
-rw-r--r--spec/models/namespace_spec.rb8
-rw-r--r--spec/models/project_spec.rb62
-rw-r--r--spec/models/repository_spec.rb4
-rw-r--r--spec/models/route_spec.rb29
-rw-r--r--spec/requests/api/api_helpers_spec.rb199
-rw-r--r--spec/requests/api/issues_spec.rb26
-rw-r--r--spec/requests/api/merge_requests_spec.rb12
-rw-r--r--spec/requests/api/tags_spec.rb55
-rw-r--r--spec/requests/api/users_spec.rb79
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb28
207 files changed, 2914 insertions, 1120 deletions
diff --git a/.eslintrc b/.eslintrc
index b80dcec9d1d..e13f76b213c 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -8,7 +8,8 @@
"globals": {
"_": false,
"gl": false,
- "gon": false
+ "gon": false,
+ "localStorage": false
},
"plugins": [
"filenames"
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8de7ca897ad..e522d47d19d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -30,7 +30,12 @@ stages:
- post-test
- pages
-# Prepare and merge knapsack tests
+# Predefined scopes
+.dedicated-runner: &dedicated-runner
+ tags:
+ - gitlab-org
+ - 2gb
+
.knapsack-state: &knapsack-state
services: []
variables:
@@ -45,47 +50,14 @@ stages:
paths:
- knapsack/
-knapsack:
- <<: *knapsack-state
- stage: prepare
- script:
- - mkdir -p knapsack/
- - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
- - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
-
-update-knapsack:
- <<: *knapsack-state
- stage: post-test
- script:
- - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
- - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- - rm -f knapsack/*_node_*.json
- only:
- - master@gitlab-org/gitlab-ce
- - master@gitlab-org/gitlab-ee
- - master@gitlab/gitlabhq
- - master@gitlab/gitlab-ee
-
.use-db: &use-db
services:
- mysql:latest
- redis:alpine
-setup-test-env:
- <<: *use-db
- stage: prepare
- script:
- - bundle exec rake assets:precompile 2>/dev/null
- - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
- artifacts:
- expire_in: 7d
- paths:
- - public/assets
- - tmp/tests
-
-
.rspec-knapsack: &rspec-knapsack
stage: test
+ <<: *dedicated-runner
<<: *use-db
script:
- JOB_NAME=( $CI_BUILD_NAME )
@@ -103,6 +75,7 @@ setup-test-env:
.spinach-knapsack: &spinach-knapsack
stage: test
+ <<: *dedicated-runner
<<: *use-db
script:
- JOB_NAME=( $CI_BUILD_NAME )
@@ -118,6 +91,44 @@ setup-test-env:
- knapsack/
- coverage/
+# Prepare and merge knapsack tests
+
+knapsack:
+ <<: *knapsack-state
+ <<: *dedicated-runner
+ stage: prepare
+ script:
+ - mkdir -p knapsack/
+ - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
+ - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
+
+setup-test-env:
+ <<: *use-db
+ <<: *dedicated-runner
+ stage: prepare
+ script:
+ - bundle exec rake assets:precompile 2>/dev/null
+ - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
+ artifacts:
+ expire_in: 7d
+ paths:
+ - public/assets
+ - tmp/tests
+
+update-knapsack:
+ <<: *knapsack-state
+ <<: *dedicated-runner
+ stage: post-test
+ script:
+ - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
+ - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
+ - rm -f knapsack/*_node_*.json
+ only:
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
+
rspec 0 20: *rspec-knapsack
rspec 1 20: *rspec-knapsack
rspec 2 20: *rspec-knapsack
@@ -166,10 +177,12 @@ spinach 9 10: *spinach-knapsack
.rspec-knapsack-ruby21: &rspec-knapsack-ruby21
<<: *rspec-knapsack
+ <<: *dedicated-runner
<<: *ruby-21
.spinach-knapsack-ruby21: &spinach-knapsack-ruby21
<<: *spinach-knapsack
+ <<: *dedicated-runner
<<: *ruby-21
rspec 0 20 ruby21: *rspec-knapsack-ruby21
@@ -214,6 +227,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
.exec: &exec
<<: *ruby-static-analysis
+ <<: *dedicated-runner
stage: test
script:
- bundle exec $CI_BUILD_NAME
@@ -249,12 +263,14 @@ rake ee_compat_check:
rake db:migrate:reset:
stage: test
<<: *use-db
+ <<: *dedicated-runner
script:
- rake db:migrate:reset
rake db:seed_fu:
stage: test
<<: *use-db
+ <<: *dedicated-runner
variables:
SIZE: "1"
SETUP_DB: "false"
@@ -276,6 +292,7 @@ teaspoon:
- node_modules/
stage: test
<<: *use-db
+ <<: *dedicated-runner
script:
- npm install
- npm link istanbul
@@ -288,20 +305,23 @@ teaspoon:
lint-doc:
stage: test
+ <<: *dedicated-runner
image: "phusion/baseimage:latest"
before_script: []
script:
- scripts/lint-doc.sh
bundler:check:
- stage: test
- <<: *ruby-static-analysis
- script:
+ stage: test
+ <<: *dedicated-runner
+ <<: *ruby-static-analysis
+ script:
- bundle check
bundler:audit:
stage: test
<<: *ruby-static-analysis
+ <<: *dedicated-runner
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
@@ -313,6 +333,7 @@ bundler:audit:
migration paths:
stage: test
<<: *use-db
+ <<: *dedicated-runner
variables:
SETUP_DB: "false"
only:
@@ -334,6 +355,7 @@ migration paths:
coverage:
stage: post-test
services: []
+ <<: *dedicated-runner
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
@@ -347,6 +369,7 @@ coverage:
- coverage/assets/
lint:javascript:
+ <<: *dedicated-runner
cache:
paths:
- node_modules/
@@ -358,6 +381,7 @@ lint:javascript:
- npm --silent run eslint
lint:javascript:report:
+ <<: *dedicated-runner
cache:
paths:
- node_modules/
@@ -379,6 +403,7 @@ lint:javascript:report:
trigger_docs:
stage: post-test
image: "alpine"
+ <<: *dedicated-runner
before_script:
- apk update && apk add curl
variables:
@@ -394,6 +419,7 @@ trigger_docs:
notify:slack:
stage: post-test
+ <<: *dedicated-runner
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
@@ -409,6 +435,7 @@ notify:slack:
pages:
before_script: []
stage: pages
+ <<: *dedicated-runner
dependencies:
- coverage
- teaspoon
@@ -423,11 +450,12 @@ pages:
paths:
- public
only:
- - master
+ - master@gitlab-org/gitlab-ce
# Insurance in case a gem needed by one of our releases gets yanked from
# rubygems.org in the future.
cache gems:
+ <<: *dedicated-runner
only:
- tags
variables:
@@ -437,3 +465,5 @@ cache gems:
artifacts:
paths:
- vendor/cache
+ only:
+ - master@gitlab-org/gitlab-ce
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e03123111c3..fb13db4dd1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,19 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.14.4 (2016-12-08)
+
+- Fix diff view permalink highlighting. !7090
+- Fix pipeline author for Slack and use pipeline id for pipeline link. !7506
+- Fix compatibility with Internet Explorer 11 for merge requests. !7525 (Steffen Rauh)
+- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615
+- Fix Cicking on tabs on pipeline page should set URL. !7709
+- Authorize users into imported GitLab project.
+- Destroy a user's session when they delete their own account.
+- Don't accidentally mark unsafe diff lines as HTML safe.
+- Replace MR access checks with use of MergeRequestsFinder.
+- Remove visible content caching.
+
## 8.14.3 (2016-12-02)
- Pass commit data to ProcessCommitWorker to reduce Git overhead. !7744
@@ -251,6 +264,11 @@ entry.
- Fix "Without projects" filter. !6611 (Ben Bodenmiller)
- Fix 404 when visit /projects page
+## 8.13.9 (2016-12-08)
+
+- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615
+- Replace MR access checks with use of MergeRequestsFinder.
+
## 8.13.8 (2016-12-02)
- Pass tag SHA to post-receive hook when tag is created via UI. !7700
@@ -495,6 +513,21 @@ entry.
- Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master)
+## 8.12.12 (2016-12-08)
+
+- Replace MR access checks with use of MergeRequestsFinder
+- Reenables /user API request to return private-token if user is admin and request is made with sudo
+
+## 8.12.11 (2016-12-02)
+
+- No changes
+
+## 8.12.10 (2016-11-28)
+
+- Fix information disclosure in `Projects::BlobController#update`
+- Fix missing access checks on issue lookup using IssuableFinder
+- Replace issue access checks with use of IssuableFinder
+
## 8.12.9 (2016-11-07)
- Fix XSS issue in Markdown autolinker
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 9084fa2f716..524cb55242b 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.1.0
+1.1.1
diff --git a/Gemfile b/Gemfile
index f49ef0386e7..f27d6363e3d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -271,7 +271,7 @@ group :development, :test do
gem 'fuubar', '~> 2.0.0'
gem 'database_cleaner', '~> 1.5.0'
- gem 'factory_girl_rails', '~> 4.6.0'
+ gem 'factory_girl_rails', '~> 4.7.0'
gem 'rspec-rails', '~> 3.5.0'
gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7a024e81ad2..c464ff70587 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -177,10 +177,10 @@ GEM
excon (0.52.0)
execjs (2.6.0)
expression_parser (0.9.0)
- factory_girl (4.5.0)
+ factory_girl (4.7.0)
activesupport (>= 3.0.0)
- factory_girl_rails (4.6.0)
- factory_girl (~> 4.5.0)
+ factory_girl_rails (4.7.0)
+ factory_girl (~> 4.7.0)
railties (>= 3.0.0)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
@@ -819,7 +819,7 @@ DEPENDENCIES
dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8)
email_spec (~> 1.6.0)
- factory_girl_rails (~> 4.6.0)
+ factory_girl_rails (~> 4.7.0)
ffaker (~> 2.0.0)
flay (~> 2.6.1)
fog-aws (~> 0.9)
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
index 5434a19bcec..0ff5c0fab05 100644
--- a/app/assets/javascripts/blob/template_selector.js.es6
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -70,6 +70,8 @@
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess(file, { skipFocus } = {}) {
+ if (!file) return;
+
const oldValue = this.editor.getValue();
let newValue = file.content;
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 3a7c5ff3681..413117c2226 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -24,6 +24,7 @@
switch (page) {
case 'sessions:new':
new UsernameValidator();
+ new ActiveTabMemoizer();
break;
case 'projects:boards:show':
case 'projects:boards:index':
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index 84faabf938a..1db29dd47fb 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -74,6 +74,8 @@
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
+ commitIconSvg: environmentsData.commitIconSvg,
+ playIconSvg: environmentsData.playIconSvg,
};
},
@@ -227,7 +229,9 @@
:model="model"
:toggleRow="toggleRow.bind(model)"
:can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"></tr>
+ :can-read-environment="canReadEnvironmentParsed"
+ :play-icon-svg="playIconSvg"
+ :commit-icon-svg="commitIconSvg"></tr>
<tr v-if="model.isOpen && model.children && model.children.length > 0"
is="environment-item"
@@ -235,7 +239,9 @@
:model="children"
:toggleRow="toggleRow.bind(children)"
:can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed">
+ :can-read-environment="canReadEnvironmentParsed"
+ :play-icon-svg="playIconSvg"
+ :commit-icon-svg="commitIconSvg">
</tr>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6
index d149a446e0b..7c743705d51 100644
--- a/app/assets/javascripts/environments/components/environment_actions.js.es6
+++ b/app/assets/javascripts/environments/components/environment_actions.js.es6
@@ -12,38 +12,18 @@
required: false,
default: () => [],
},
- },
-
- /**
- * Appends the svg icon that were render in the index page.
- * In order to reuse the svg instead of copy and paste in this template
- * we need to render it outside this component using =custom_icon partial.
- *
- * TODO: Remove this when webpack is merged.
- *
- */
- mounted() {
- const playIcon = document.querySelector('.play-icon-svg.hidden svg');
-
- const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container');
- const actionContainers = this.$el.querySelectorAll('.action-play-icon-container');
- // Phantomjs does not have support to iterate a nodelist.
- const actionsArray = [].slice.call(actionContainers);
-
- if (playIcon && actionsArray && dropdownContainer) {
- dropdownContainer.appendChild(playIcon.cloneNode(true));
- actionsArray.forEach((element) => {
- element.appendChild(playIcon.cloneNode(true));
- });
- }
+ playIconSvg: {
+ type: String,
+ required: false,
+ },
},
template: `
<div class="inline">
<div class="dropdown">
<a class="dropdown-new btn btn-default" data-toggle="dropdown">
- <span class="dropdown-play-icon-container"></span>
+ <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
<i class="fa fa-caret-down"></i>
</a>
@@ -53,7 +33,9 @@
data-method="post"
rel="nofollow"
class="js-manual-action-link">
- <span class="action-play-icon-container"></span>
+
+ <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
+
<span>
{{action.name}}
</span>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6
index 79cd5ded5bd..aed65b33c04 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.js.es6
+++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6
@@ -7,14 +7,14 @@
window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
props: {
- external_url: {
+ externalUrl: {
type: String,
default: '',
},
},
template: `
- <a class="btn external_url" :href="external_url" target="_blank">
+ <a class="btn external_url" :href="externalUrl" target="_blank">
<i class="fa fa-external-link"></i>
</a>
`,
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
index 6ed14261fc3..2e046a60146 100644
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -58,6 +58,16 @@
required: false,
default: false,
},
+
+ commitIconSvg: {
+ type: String,
+ required: false,
+ },
+
+ playIconSvg: {
+ type: String,
+ required: false,
+ },
},
data() {
@@ -451,11 +461,12 @@
<div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component">
<commit-component
:tag="commitTag"
- :commit_ref="commitRef"
- :commit_url="commitUrl"
- :short_sha="commitShortSha"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
:title="commitTitle"
- :author="commitAuthor">
+ :author="commitAuthor"
+ :commit-icon-svg="commitIconSvg">
</commit-component>
</div>
<p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title">
@@ -476,6 +487,7 @@
<div v-if="hasManualActions && canCreateDeployment"
class="inline js-manual-actions-container">
<actions-component
+ :play-icon-svg="playIconSvg"
:actions="manualActions">
</actions-component>
</div>
@@ -483,22 +495,22 @@
<div v-if="model.external_url && canReadEnvironment"
class="inline js-external-url-container">
<external-url-component
- :external_url="model.external_url">
- </external_url-component>
+ :external-url="model.external_url">
+ </external-url-component>
</div>
<div v-if="isStoppable && canCreateDeployment"
class="inline js-stop-component-container">
<stop-component
- :stop_url="model.stop_path">
+ :stop-url="model.stop_path">
</stop-component>
</div>
<div v-if="canRetry && canCreateDeployment"
class="inline js-rollback-component-container">
<rollback-component
- :is_last_deployment="isLastDeployment"
- :retry_url="retryUrl">
+ :is-last-deployment="isLastDeployment"
+ :retry-url="retryUrl">
</rollback-component>
</div>
</div>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6
index 55e5c826e07..6d4e8fad604 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.js.es6
+++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6
@@ -7,19 +7,20 @@
window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
props: {
- retry_url: {
+ retryUrl: {
type: String,
default: '',
},
- is_last_deployment: {
+
+ isLastDeployment: {
type: Boolean,
default: true,
},
},
template: `
- <a class="btn" :href="retry_url" data-method="post" rel="nofollow">
- <span v-if="is_last_deployment">
+ <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
+ <span v-if="isLastDeployment">
Re-deploy
</span>
<span v-else>
diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6
index e6d66a0148c..7292f924e5c 100644
--- a/app/assets/javascripts/environments/components/environment_stop.js.es6
+++ b/app/assets/javascripts/environments/components/environment_stop.js.es6
@@ -7,7 +7,7 @@
window.gl.environmentsList.StopComponent = Vue.component('stop-component', {
props: {
- stop_url: {
+ stopUrl: {
type: String,
default: '',
},
@@ -15,7 +15,7 @@
template: `
<a class="btn stop-env-link"
- :href="stop_url"
+ :href="stopUrl"
data-confirm="Are you sure you want to stop this environment?"
data-method="post"
rel="nofollow">
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 969778dded7..9a91018a8e4 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -650,6 +650,11 @@
} else if(value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
+
+ if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
+ return;
+ }
+
if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 963d2851e5f..e8e502694d6 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -29,7 +29,7 @@
setTimeago = true;
}
- $timeagoEls.each(function() {
+ $timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
var $el = $(this);
$el.attr('title', gl.utils.formatDate($el.attr('datetime')));
@@ -39,6 +39,8 @@
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
}
+
+ $el.attr('data-timeago-rendered', true);
gl.utils.renderTimeago($el);
});
};
diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6
index 895bc10784f..e3f367a11eb 100644
--- a/app/assets/javascripts/members.js.es6
+++ b/app/assets/javascripts/members.js.es6
@@ -1,38 +1,81 @@
-/* eslint-disable */
-((w) => {
- w.gl = w.gl || {};
+/* eslint-disable class-methods-use-this */
+(() => {
+ window.gl = window.gl || {};
class Members {
constructor() {
this.addListeners();
+ this.initGLDropdown();
}
addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
- $('.js-member-update-control').off('change').on('change', this.formSubmit);
- $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess);
+ $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
+ $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
+ initGLDropdown() {
+ $('.js-member-permissions-dropdown').each((i, btn) => {
+ const $btn = $(btn);
+
+ $btn.glDropdown({
+ selectable: true,
+ isSelectable(selected, $el) {
+ return !$el.hasClass('is-active');
+ },
+ fieldName: $btn.data('field-name'),
+ id(selected, $el) {
+ return $el.data('id');
+ },
+ toggleLabel(selected, $el) {
+ return $el.text();
+ },
+ clicked: (selected, $link) => {
+ this.formSubmit(null, $link);
+ },
+ });
+ });
+ }
+
removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
- .fadeOut(function () {
+ .fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
- formSubmit() {
- $(this).closest('form').trigger("submit.rails").end().disable();
+ formSubmit(e, $el = null) {
+ const $this = e ? $(e.currentTarget) : $el;
+ const { $toggle, $dateInput } = this.getMemberListItems($this);
+
+ $this.closest('form').trigger('submit.rails');
+
+ $toggle.disable();
+ $dateInput.disable();
}
- formSuccess() {
- $(this).find('.js-member-update-control').enable();
+ formSuccess(e) {
+ const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
+
+ $toggle.enable();
+ $dateInput.enable();
+ }
+
+ getMemberListItems($el) {
+ const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
+
+ return {
+ $memberListItem,
+ $toggle: $memberListItem.find('.dropdown-menu-toggle'),
+ $dateInput: $memberListItem.find('.js-access-expiration-date'),
+ };
}
}
gl.Members = Members;
-})(window);
+})();
diff --git a/app/assets/javascripts/signin_tabs_memoizer.js.es6 b/app/assets/javascripts/signin_tabs_memoizer.js.es6
new file mode 100644
index 00000000000..d811d1cd53a
--- /dev/null
+++ b/app/assets/javascripts/signin_tabs_memoizer.js.es6
@@ -0,0 +1,49 @@
+/* eslint no-param-reassign: ["error", { "props": false }]*/
+/* eslint no-new: "off" */
+((global) => {
+ /**
+ * Memorize the last selected tab after reloading a page.
+ * Does that setting the current selected tab in the localStorage
+ */
+ class ActiveTabMemoizer {
+ constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
+ this.currentTabKey = currentTabKey;
+ this.tabSelector = tabSelector;
+ this.bootstrap();
+ }
+
+ bootstrap() {
+ const tabs = document.querySelectorAll(this.tabSelector);
+ if (tabs.length > 0) {
+ tabs[0].addEventListener('click', (e) => {
+ if (e.target && e.target.nodeName === 'A') {
+ const anchorName = e.target.getAttribute('href');
+ this.saveData(anchorName);
+ }
+ });
+ }
+
+ this.showTab();
+ }
+
+ showTab() {
+ const anchorName = this.readData();
+ if (anchorName) {
+ const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`);
+ if (tab) {
+ tab.click();
+ }
+ }
+ }
+
+ saveData(val) {
+ localStorage.setItem(this.currentTabKey, val);
+ }
+
+ readData() {
+ return localStorage.getItem(this.currentTabKey);
+ }
+ }
+
+ global.ActiveTabMemoizer = ActiveTabMemoizer;
+})(window);
diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6
index 2ef2959cbf4..62a22e39a3b 100644
--- a/app/assets/javascripts/vue_common_component/commit.js.es6
+++ b/app/assets/javascripts/vue_common_component/commit.js.es6
@@ -23,7 +23,7 @@
* name
* ref_url
*/
- commit_ref: {
+ commitRef: {
type: Object,
required: false,
default: () => ({}),
@@ -32,16 +32,16 @@
/**
* Used to link to the commit sha.
*/
- commit_url: {
+ commitUrl: {
type: String,
required: false,
default: '',
},
/**
- * Used to show the commit short_sha that links to the commit url.
+ * Used to show the commit short sha that links to the commit url.
*/
- short_sha: {
+ shortSha: {
type: String,
required: false,
default: '',
@@ -68,6 +68,11 @@
required: false,
default: () => ({}),
},
+
+ commitIconSvg: {
+ type: String,
+ required: false,
+ },
},
computed: {
@@ -80,7 +85,7 @@
* @returns {Boolean}
*/
hasCommitRef() {
- return this.commit_ref && this.commit_ref.name && this.commit_ref.ref_url;
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
@@ -110,24 +115,6 @@
},
},
- /**
- * In order to reuse the svg instead of copy and paste in this template
- * we need to render it outside this component using =custom_icon partial.
- * Make sure it has this structure:
- * .commit-icon-svg.hidden
- * svg
- *
- * TODO: Find a better way to include SVG
- */
- mounted() {
- const commitIconContainer = this.$el.querySelector('.commit-icon-container');
- const commitIcon = document.querySelector('.commit-icon-svg.hidden svg');
-
- if (commitIconContainer && commitIcon) {
- commitIconContainer.appendChild(commitIcon.cloneNode(true));
- }
- },
-
template: `
<div class="branch-commit">
@@ -138,15 +125,15 @@
<a v-if="hasCommitRef"
class="monospace branch-name"
- :href="commit_ref.ref_url">
- {{commit_ref.name}}
+ :href="commitRef.ref_url">
+ {{commitRef.name}}
</a>
- <div class="icon-container commit-icon commit-icon-container"></div>
+ <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
<a class="commit-id monospace"
- :href="commit_url">
- {{short_sha}}
+ :href="commitUrl">
+ {{shortSha}}
</a>
<p class="commit-title">
@@ -162,7 +149,7 @@
</a>
<a class="commit-row-message"
- :href="commit_url">
+ :href="commitUrl">
{{title}}
</a>
</span>
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 4d4835568ed..c82a9a2b9e3 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -6,6 +6,7 @@
@import "framework/animations.scss";
@import "framework/avatar.scss";
+@import "framework/asciidoctor.scss";
@import "framework/blocks.scss";
@import "framework/buttons.scss";
@import "framework/calendar.scss";
diff --git a/app/assets/stylesheets/framework/asciidoctor.scss b/app/assets/stylesheets/framework/asciidoctor.scss
new file mode 100644
index 00000000000..62493c32833
--- /dev/null
+++ b/app/assets/stylesheets/framework/asciidoctor.scss
@@ -0,0 +1,27 @@
+.admonitionblock td.icon {
+ width: 1%;
+
+ [class^="fa icon-"] {
+ @extend .fa-2x;
+ }
+
+ .icon-note {
+ @extend .fa-thumb-tack;
+ }
+
+ .icon-tip {
+ @extend .fa-lightbulb-o;
+ }
+
+ .icon-warning {
+ @extend .fa-exclamation-triangle;
+ }
+
+ .icon-caution {
+ @extend .fa-fire;
+ }
+
+ .icon-important {
+ @extend .fa-exclamation-circle;
+ }
+}
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index c13cb4a02b2..dece5c3202b 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -1,7 +1,7 @@
.awards {
.emoji-icon {
- width: 19px;
- height: 19px;
+ width: 20px;
+ height: 20px;
}
}
@@ -136,5 +136,6 @@
.award-control-icon {
color: $award-emoji-new-btn-icon-color;
+ margin-top: 1px;
}
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 8da3da2ad08..1c7b2f4df7c 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,7 +1,7 @@
@mixin btn-default {
border-radius: 3px;
font-size: $gl-font-size;
- font-weight: 500;
+ font-weight: 400;
padding: $gl-vert-padding $gl-btn-padding;
&:focus,
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 600bf17259b..251e43d2edd 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -255,6 +255,7 @@ img.emoji {
height: 20px;
vertical-align: top;
width: 20px;
+ margin-top: 1px;
}
.chart {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 33de652c06f..d5914b900e2 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -42,6 +42,11 @@
border-radius: $border-radius-base;
white-space: nowrap;
+ &[disabled] {
+ background-color: $input-bg-disabled;
+ cursor: not-allowed;
+ }
+
&.no-outline {
outline: 0;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 69da520f21f..ea77348633d 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -71,6 +71,10 @@
border-bottom: 2px solid $link-underline-blue;
color: $black;
font-weight: 600;
+
+ .badge {
+ color: $black;
+ }
}
.badge {
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 756efa9c7fa..5f3bbb40ba0 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -54,6 +54,10 @@
@media (min-width: $screen-sm-min) {
width: 50%;
}
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
}
.member-access-text {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 16b099c09eb..10eb3d4203e 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -124,7 +124,7 @@ ul.notes {
position: absolute;
left: 0;
bottom: 0;
- background: linear-gradient(rgba($gray-light, 0.1) -100px, $white-light 100%);
+ background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);
}
&.hide-shade {
@@ -413,7 +413,6 @@ ul.notes {
.fa {
color: $notes-action-color;
position: relative;
- top: 1px;
font-size: 17px;
}
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index aa7570cd896..1e3d194e9f9 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -56,7 +56,7 @@ class Admin::GroupsController < Admin::ApplicationController
private
def group
- @group ||= Group.find_by(path: params[:id])
+ @group ||= Group.find_by_full_path(params[:id])
end
def group_params
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index dacb5679dd3..936d9bab57e 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -81,10 +81,8 @@ module CreatesCommit
def merge_request_exists?
return @merge_request if defined?(@merge_request)
- @merge_request = @mr_target_project.merge_requests.opened.find_by(
- source_branch: @mr_source_branch,
- target_branch: @mr_target_branch
- )
+ @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened.
+ find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch)
end
def different_project?
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 949b4a6c25a..c411c21bb80 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -9,7 +9,7 @@ class Groups::ApplicationController < ApplicationController
def group
unless @group
id = params[:group_id] || params[:id]
- @group = Group.find_by(path: id)
+ @group = Group.find_by_full_path(id)
unless @group && can?(current_user, :read_group, @group)
@group = nil
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index cdfc1ba7b92..8197d9e4c99 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -65,7 +65,7 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @target_branch.blank?
- create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.",
+ create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
success_path: successful_change_path, failure_path: failed_change_path)
end
@@ -74,26 +74,24 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @target_branch.blank?
- create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.",
+ create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
success_path: successful_change_path, failure_path: failed_change_path)
end
private
def successful_change_path
- return referenced_merge_request_url if @commit.merged_merge_request
-
- namespace_project_commits_url(@project.namespace, @project, @target_branch)
+ referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch)
end
def failed_change_path
- return referenced_merge_request_url if @commit.merged_merge_request
-
- namespace_project_commit_url(@project.namespace, @project, params[:id])
+ referenced_merge_request_url || namespace_project_commit_url(@project.namespace, @project, params[:id])
end
def referenced_merge_request_url
- namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request)
+ if merge_request = @commit.merged_merge_request(current_user)
+ namespace_project_merge_request_url(@project.namespace, @project, merge_request)
+ end
end
def commit
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index aba87b6144b..ad92f05a42d 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -21,7 +21,7 @@ class Projects::CommitsController < Projects::ApplicationController
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
- @merge_request = @project.merge_requests.opened.
+ @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
respond_to do |format|
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index bee3d56076c..ec02fc15d35 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -53,7 +53,7 @@ class Projects::CompareController < Projects::ApplicationController
end
def merge_request
- @merge_request ||= @project.merge_requests.opened.
+ @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
end
end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 148e39630e3..1349b015a63 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -24,7 +24,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
private
def merge_request
- @merge_request ||= @project.merge_requests.find_by!(iid: params[:merge_request_id])
+ @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id])
end
def discussion
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 699a56ae2f8..53308948f62 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -10,14 +10,37 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
+ group = @project.group
+
+ if group
+ # We need `.where.not(user_id: nil)` here otherwise when a group has an
+ # invitee, it would make the following query return 0 rows since a NULL
+ # user_id would be present in the subquery
+ # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+ # FIXME: This whole logic should be moved to a finder!
+ non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
+ group_members = group.group_members.where.not(user_id: non_null_user_ids)
+ group_members = group_members.non_invite unless can?(current_user, :admin_group, @group)
+ end
+
if params[:search].present?
- users = @project.users.search(params[:search]).to_a
- @project_members = @project_members.where(user_id: users)
+ user_ids = @project.users.search(params[:search]).select(:id)
+ @project_members = @project_members.where(user_id: user_ids)
+
+ if group_members
+ user_ids = group.users.search(params[:search]).select(:id)
+ group_members = group_members.where(user_id: user_ids)
+ end
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
- @project_members = @project_members.order(access_level: :desc).page(params[:page])
+ wheres = ["id IN (#{@project_members.select(:id).to_sql})"]
+ wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members
+
+ @project_members = Member.
+ where(wheres.join(' OR ')).
+ order(access_level: :desc).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index 52517381c65..a41fcb85c40 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -18,7 +18,7 @@ class Projects::TodosController < Projects::ApplicationController
when "issue"
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
when "merge_request"
- @project.merge_requests.find(params[:issuable_id])
+ MergeRequestsFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
end
end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 3327f4f2b87..c45196cc3e9 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -27,7 +27,10 @@ class RegistrationsController < Devise::RegistrationsController
DeleteUserService.new(current_user).execute(current_user)
respond_to do |format|
- format.html { redirect_to new_user_session_path, notice: "Account successfully removed." }
+ format.html do
+ session.try(:destroy)
+ redirect_to new_user_session_path, notice: "Account successfully removed."
+ end
end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 5d7ecfeacf4..38e7c6f4a48 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -31,6 +31,8 @@ class SessionsController < Devise::SessionsController
resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil)
end
+ # hide the signed-in notification
+ flash[:notice] = nil
log_audit_event(current_user, with: authentication_method)
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index c9bee01b9ad..b4c14d05eaf 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -77,6 +77,10 @@ class IssuableFinder
counts
end
+ def find_by!(*params)
+ execute.find_by!(*params)
+ end
+
def group
return @group if defined?(@group)
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index a653a6d59c6..2484339e3a4 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -14,7 +14,7 @@ class NotesFinder
when "issue"
IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author
when "merge_request"
- project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
+ MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet"
project.snippets.find(target_id).notes
else
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index ed402b698fb..66a720a9426 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -130,7 +130,7 @@ module CommitsHelper
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user
- tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip
+ tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
if can_collaborate_with_project?
btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
@@ -154,7 +154,7 @@ module CommitsHelper
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user
- tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
+ tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request"
if can_collaborate_with_project?
btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index f489f9aa0d6..c35d6611ab0 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -55,7 +55,9 @@ module DiffHelper
if line.blank?
"&nbsp;".html_safe
else
- line.sub(/^[\-+ ]/, '').html_safe
+ # We can't use `sub` because the HTML-safeness of `line` will not survive.
+ line[0] = '' if line.start_with?('+', '-', ' ')
+ line
end
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index f1a0b929d82..362046c0270 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -45,6 +45,12 @@ module EventsHelper
@project.feature_available?(feature_key, current_user)
end
+ def comments_visible?
+ event_filter_visible(:repository) ||
+ event_filter_visible(:merge_requests) ||
+ event_filter_visible(:issues)
+ end
+
def event_preposition(event)
if event.push? || event.commented? || event.target
"at"
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 19ab059aea6..f6d4ea4659a 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -5,7 +5,7 @@ module GroupsHelper
def group_icon(group)
if group.is_a?(String)
- group = Group.find_by(path: group)
+ group = Group.find_by_full_path(group)
end
group.try(:avatar_url) || image_path('no_group_avatar.png')
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 05303007625..fda8228a1e9 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -100,34 +100,29 @@ module Ci
where.not(duration: nil).sum(:duration)
end
- def stages_query
- statuses.group('stage').select(:stage)
- .order('max(stage_idx)')
+ def stages_count
+ statuses.select(:stage).distinct.count
end
- def stages
- self.stages_query.pluck(:stage)
+ def stages_name
+ statuses.order(:stage_idx).distinct.
+ pluck(:stage, :stage_idx).map(&:first)
end
- def stages_with_statuses
+ def stages
status_sql = statuses.latest.where('stage=sg.stage').status_sql
- stages_with_statuses = CommitStatus.from(self.stages_query, :sg).
+ stages_query = statuses.group('stage').select(:stage)
+ .order('max(stage_idx)')
+
+ stages_with_statuses = CommitStatus.from(stages_query, :sg).
pluck('sg.stage', status_sql)
stages_with_statuses.map do |stage|
- OpenStruct.new(
- name: stage.first,
- status: stage.last,
- pipeline: self
- )
+ Ci::Stage.new(self, name: stage.first, status: stage.last)
end
end
- def stages_with_latest_statuses
- statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
- end
-
def artifacts
builds.latest.with_artifacts_not_expired
end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
new file mode 100644
index 00000000000..d2a37c0a827
--- /dev/null
+++ b/app/models/ci/stage.rb
@@ -0,0 +1,37 @@
+module Ci
+ # Currently this is artificial object, constructed dynamically
+ # We should migrate this object to actual database record in the future
+ class Stage
+ include StaticModel
+
+ attr_reader :pipeline, :name
+
+ delegate :project, to: :pipeline
+
+ def initialize(pipeline, name:, status: nil)
+ @pipeline = pipeline
+ @name = name
+ @status = status
+ end
+
+ def to_param
+ name
+ end
+
+ def status
+ @status ||= statuses.latest.status
+ end
+
+ def detailed_status
+ Gitlab::Ci::Status::Stage::Factory.new(self).fabricate!
+ end
+
+ def statuses
+ @statuses ||= pipeline.statuses.where(stage: name)
+ end
+
+ def builds
+ @builds ||= pipeline.builds.where(stage: name)
+ end
+ end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 248140f421b..1831cc7e175 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -245,44 +245,47 @@ class Commit
project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
end
- def revert_description
- if merged_merge_request
- "This reverts merge request #{merged_merge_request.to_reference}"
+ def revert_description(user)
+ if merged_merge_request?(user)
+ "This reverts merge request #{merged_merge_request(user).to_reference}"
else
"This reverts commit #{sha}"
end
end
- def revert_message
- %Q{Revert "#{title.strip}"\n\n#{revert_description}}
+ def revert_message(user)
+ %Q{Revert "#{title.strip}"\n\n#{revert_description(user)}}
end
- def reverts_commit?(commit)
- description? && description.include?(commit.revert_description)
+ def reverts_commit?(commit, user)
+ description? && description.include?(commit.revert_description(user))
end
def merge_commit?
parents.size > 1
end
- def merged_merge_request
- return @merged_merge_request if defined?(@merged_merge_request)
-
- @merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit?
+ def merged_merge_request(current_user)
+ # Memoize with per-user access check
+ @merged_merge_request_hash ||= Hash.new do |hash, user|
+ hash[user] = merged_merge_request_no_cache(user)
+ end
+
+ @merged_merge_request_hash[current_user]
end
- def has_been_reverted?(current_user = nil, noteable = self)
+ def has_been_reverted?(current_user, noteable = self)
ext = all_references(current_user)
noteable.notes_with_associations.system.each do |note|
note.all_references(current_user, extractor: ext)
end
- ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) }
+ ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) }
end
- def change_type_title
- merged_merge_request ? 'merge request' : 'commit'
+ def change_type_title(user)
+ merged_merge_request?(user) ? 'merge request' : 'commit'
end
# Get the URI type of the given path
@@ -350,4 +353,12 @@ class Commit
changes
end
+
+ def merged_merge_request?(user)
+ !!merged_merge_request(user)
+ end
+
+ def merged_merge_request_no_cache(user)
+ MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
+ end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index d9021a38ce3..cf90475f4d4 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -31,18 +31,13 @@ class CommitStatus < ActiveRecord::Base
end
scope :exclude_ignored, -> do
- quoted_when = connection.quote_column_name('when')
# We want to ignore failed_but_allowed jobs
where("allow_failure = ? OR status IN (?)",
- false, all_state_names - [:failed, :canceled]).
- # We want to ignore skipped manual jobs
- where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped').
- # We want to ignore skipped on_failure
- where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
+ false, all_state_names - [:failed, :canceled])
end
- scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
- scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
+ scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
+ scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
state_machine :status do
event :enqueue do
@@ -117,11 +112,6 @@ class CommitStatus < ActiveRecord::Base
name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
end
- def self.stages
- # We group by stage name, but order stages by theirs' index
- unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').select('sg.stage')
- end
-
def failed_but_allowed?
allow_failure? && (failed? || canceled?)
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 2f5aa91a964..90432fc4050 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -4,7 +4,7 @@ module HasStatus
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running]
- COMPLETED_STATUSES = %w[success failed canceled]
+ COMPLETED_STATUSES = %w[success failed canceled skipped]
ORDERED_STATUSES = %w[failed pending running canceled success skipped]
class_methods do
@@ -23,9 +23,10 @@ module HasStatus
canceled = scope.canceled.select('count(*)').to_sql
"(CASE
+ WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created'
- WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped'
+ WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index e65fc9eaa09..875e9834487 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,17 +1,17 @@
module Milestoneish
- def closed_items_count(user = nil)
+ def closed_items_count(user)
issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
end
- def total_items_count(user = nil)
+ def total_items_count(user)
issues_visible_to_user(user).size + merge_requests.size
end
- def complete?(user = nil)
+ def complete?(user)
total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
end
- def percent_complete(user = nil)
+ def percent_complete(user)
((closed_items_count(user) * 100) / total_items_count(user)).abs
rescue ZeroDivisionError
0
@@ -29,7 +29,7 @@ module Milestoneish
(Date.today - start_date).to_i
end
- def issues_visible_to_user(user = nil)
+ def issues_visible_to_user(user)
issues.visible_to_user(user)
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
new file mode 100644
index 00000000000..d36bb9da296
--- /dev/null
+++ b/app/models/concerns/routable.rb
@@ -0,0 +1,70 @@
+# Store object full path in separate table for easy lookup and uniq validation
+# Object must have path db field and respond to full_path and full_path_changed? methods.
+module Routable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :route, as: :source, autosave: true, dependent: :destroy
+
+ validates_associated :route
+
+ before_validation :update_route_path, if: :full_path_changed?
+ end
+
+ class_methods do
+ # Finds a single object by full path match in routes table.
+ #
+ # Usage:
+ #
+ # Klass.find_by_full_path('gitlab-org/gitlab-ce')
+ #
+ # Returns a single object, or nil.
+ def find_by_full_path(path)
+ # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
+ # any literal matches come first, for this we have to use "BINARY".
+ # Without this there's still no guarantee in what order MySQL will return
+ # rows.
+ binary = Gitlab::Database.mysql? ? 'BINARY' : ''
+
+ order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
+
+ where_paths_in([path]).reorder(order_sql).take
+ end
+
+ # Builds a relation to find multiple objects by their full paths.
+ #
+ # Usage:
+ #
+ # Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
+ #
+ # Returns an ActiveRecord::Relation.
+ def where_paths_in(paths)
+ wheres = []
+ cast_lower = Gitlab::Database.postgresql?
+
+ paths.each do |path|
+ path = connection.quote(path)
+ where = "(routes.path = #{path})"
+
+ if cast_lower
+ where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))"
+ end
+
+ wheres << where
+ end
+
+ if wheres.empty?
+ none
+ else
+ joins(:route).where(wheres.join(' OR '))
+ end
+ end
+ end
+
+ private
+
+ def update_route_path
+ route || build_route(source: self)
+ route.path = full_path
+ end
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 33b578e12c1..ea3cf1cdaac 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -101,7 +101,9 @@ class MergeRequest < ActiveRecord::Base
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
- scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
+ scope :by_source_or_target_branch, ->(branch_name) do
+ where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
+ end
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
@@ -805,7 +807,7 @@ class MergeRequest < ActiveRecord::Base
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end
- def can_be_reverted?(current_user = nil)
+ def can_be_reverted?(current_user)
merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 7a545f752b6..37374044551 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -4,12 +4,16 @@ class Namespace < ActiveRecord::Base
include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
+ include Routable
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User"
+ belongs_to :parent, class_name: "Namespace"
+ has_many :children, class_name: "Namespace", foreign_key: :parent_id
+
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
presence: true,
@@ -86,7 +90,7 @@ class Namespace < ActiveRecord::Base
end
def to_param
- path
+ full_path
end
def human_name
@@ -150,6 +154,14 @@ class Namespace < ActiveRecord::Base
Gitlab.config.lfs.enabled
end
+ def full_path
+ if parent
+ parent.full_path + '/' + path
+ else
+ path
+ end
+ end
+
private
def repository_storage_paths
@@ -185,4 +197,8 @@ class Namespace < ActiveRecord::Base
where(projects: { namespace_id: id }).
find_each(&:refresh_members_authorized_projects)
end
+
+ def full_path_changed?
+ path_changed? || parent_id_changed?
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 590885c0177..77d740081c6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -14,6 +14,7 @@ class Project < ActiveRecord::Base
include TokenAuthenticatable
include ProjectFeaturesCompatibility
include SelectForProjectAuthorization
+ include Routable
extend Gitlab::ConfigHelper
@@ -324,87 +325,6 @@ class Project < ActiveRecord::Base
non_archived.where(table[:name].matches(pattern))
end
- # Finds a single project for the given path.
- #
- # path - The full project path (including namespace path).
- #
- # Returns a Project, or nil if no project could be found.
- def find_with_namespace(path)
- namespace_path, project_path = path.split('/', 2)
-
- return unless namespace_path && project_path
-
- namespace_path = connection.quote(namespace_path)
- project_path = connection.quote(project_path)
-
- # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
- # any literal matches come first, for this we have to use "BINARY".
- # Without this there's still no guarantee in what order MySQL will return
- # rows.
- binary = Gitlab::Database.mysql? ? 'BINARY' : ''
-
- order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \
- "AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)"
-
- where_paths_in([path]).reorder(order_sql).take
- end
-
- # Builds a relation to find multiple projects by their full paths.
- #
- # Each path must be in the following format:
- #
- # namespace_path/project_path
- #
- # For example:
- #
- # gitlab-org/gitlab-ce
- #
- # Usage:
- #
- # Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
- #
- # This would return the projects with the full paths matching the values
- # given.
- #
- # paths - An Array of full paths (namespace path + project path) for which
- # to find the projects.
- #
- # Returns an ActiveRecord::Relation.
- def where_paths_in(paths)
- wheres = []
- cast_lower = Gitlab::Database.postgresql?
-
- paths.each do |path|
- namespace_path, project_path = path.split('/', 2)
-
- next unless namespace_path && project_path
-
- namespace_path = connection.quote(namespace_path)
- project_path = connection.quote(project_path)
-
- where = "(namespaces.path = #{namespace_path}
- AND projects.path = #{project_path})"
-
- if cast_lower
- where = "(
- #{where}
- OR (
- LOWER(namespaces.path) = LOWER(#{namespace_path})
- AND LOWER(projects.path) = LOWER(#{project_path})
- )
- )"
- end
-
- wheres << where
- end
-
- if wheres.empty?
- none
- else
- joins(:namespace).where(wheres.join(' OR '))
- end
- end
-
def visibility_levels
Gitlab::VisibilityLevel.options
end
@@ -440,6 +360,10 @@ class Project < ActiveRecord::Base
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end
+
+ # Add alias for Routable method for compatibility with old code.
+ # In future all calls `find_with_namespace` should be replaced with `find_by_full_path`
+ alias_method :find_with_namespace, :find_by_full_path
end
def lfs_enabled?
@@ -879,13 +803,14 @@ class Project < ActiveRecord::Base
end
alias_method :human_name, :name_with_namespace
- def path_with_namespace
- if namespace
- namespace.path + '/' + path
+ def full_path
+ if namespace && path
+ namespace.full_path + '/' + path
else
path
end
end
+ alias_method :path_with_namespace, :full_path
def execute_hooks(data, hooks_scope = :push_hooks)
hooks.send(hooks_scope).each do |hook|
@@ -1373,4 +1298,8 @@ class Project < ActiveRecord::Base
def validate_board_limit(board)
raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
end
+
+ def full_path_changed?
+ path_changed? || namespace_id_changed?
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 3c4b0212af7..1ccabdb7c1f 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -950,7 +950,7 @@ class Repository
update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
- message: commit.revert_message,
+ message: commit.revert_message(user),
author: committer,
committer: committer,
tree: revert_tree_id,
diff --git a/app/models/route.rb b/app/models/route.rb
new file mode 100644
index 00000000000..d40214b9da6
--- /dev/null
+++ b/app/models/route.rb
@@ -0,0 +1,22 @@
+class Route < ActiveRecord::Base
+ belongs_to :source, polymorphic: true
+
+ validates :source, presence: true
+
+ validates :path,
+ length: { within: 1..255 },
+ presence: true,
+ uniqueness: { case_sensitive: false }
+
+ after_update :rename_children, if: :path_changed?
+
+ def rename_children
+ # We update each row separately because MySQL does not have regexp_replace.
+ # rubocop:disable Rails/FindEach
+ Route.where('path LIKE ?', "#{path_was}%").each do |route|
+ # Note that update column skips validation and callbacks.
+ # We need this to avoid recursive call of rename_children method
+ route.update_column(:path, route.path.sub(path_was, path))
+ end
+ end
+end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 2e028c44d8b..79eb97b7b55 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -44,11 +44,11 @@ module Ci
def valid_statuses_for_when(value)
case value
when 'on_success'
- %w[success]
+ %w[success skipped]
when 'on_failure'
%w[failed]
when 'always'
- %w[success failed]
+ %w[success failed skipped]
else
[]
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 1c82599c579..db5f2bf9b2e 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -34,7 +34,7 @@ module Commits
repository.public_send(action, current_user, @commit, into, tree_id)
success
else
- error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically.
+ error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg
end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index a880952e274..2316c57bf1e 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -20,6 +20,10 @@ class DestroyGroupService
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end
+ group.children.each do |group|
+ DestroyGroupService.new(group, current_user).async_execute
+ end
+
group.really_destroy!
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index e4056306bc4..0a9563ed7e7 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -55,8 +55,9 @@ module MergeRequests
# Refresh merge request diff if we push to source or target branch of merge request
# Note: we should update merge requests from forks too
def reload_merge_requests
- merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a
- merge_requests += fork_merge_requests.by_branch(@branch_name).to_a
+ merge_requests = @project.merge_requests.opened.
+ by_source_or_target_branch(@branch_name).to_a
+ merge_requests += fork_merge_requests
merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request|
@@ -157,13 +158,14 @@ module MergeRequests
def merge_requests_for_source_branch
@source_merge_requests ||= begin
merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
- merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a
+ merge_requests += fork_merge_requests
filter_merge_requests(merge_requests)
end
end
def fork_merge_requests
- @fork_merge_requests ||= @project.fork_merge_requests.opened
+ @fork_merge_requests ||= @project.fork_merge_requests.opened.
+ where(source_branch: @branch_name).to_a
end
def branch_added?
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index b10ad71d052..fbaea2744a3 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,6 +1,6 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff]
+ IMAGE_EXT = %w[png jpg jpeg gif bmp tiff svg]
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index fa8e7979461..af87129e49e 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,4 +1,5 @@
- page_title "Sign in"
+
%div
- if form_based_providers.any?
= render 'devise/shared/tabs_ldap'
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index de8f53b6b52..9d05bff6c4e 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
$("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}"));
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 701bcd3ab71..7bd11f5727a 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -77,7 +77,7 @@
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
Merge Requests
- %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
+ %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 697c8d19257..56c1949ab2b 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -133,7 +133,7 @@
%tr.success-message
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"}
- build_count = @pipeline.statuses.latest.size
- - stage_count = @pipeline.stages.size
+ - stage_count = @pipeline.stages_count
Pipeline
%a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
= "\##{@pipeline.id}"
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index ae22d474f2c..40e5e306426 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -16,7 +16,7 @@ Commit Author: <%= commit.author_name %>
<% end -%>
<% build_count = @pipeline.statuses.latest.size -%>
-<% stage_count = @pipeline.stages.size -%>
+<% stage_count = @pipeline.stages_count -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
new file mode 100644
index 00000000000..afe2fd7fd7b
--- /dev/null
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -0,0 +1,15 @@
+- form = local_assigns.fetch(:form)
+
+.form-group
+ .checkbox.builds-feature
+ = form.label :only_allow_merge_if_build_succeeds do
+ = form.check_box :only_allow_merge_if_build_succeeds
+ %strong Only allow merge requests to be merged if the build succeeds
+ %br
+ %span.descr
+ Builds need to be configured to enable this feature.
+ = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
+ .checkbox
+ = form.label :only_allow_merge_if_all_discussions_are_resolved do
+ = form.check_box :only_allow_merge_if_all_discussions_are_resolved
+ %strong Only allow merge requests to be merged if all discussions are resolved
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index 6e143c4b570..818010bc7d3 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -1,18 +1,8 @@
-.merge-requests-feature
- %fieldset.builds-feature
- %hr
- %h5.prepend-top-0
- Merge Requests
- .form-group
- .checkbox
- = f.label :only_allow_merge_if_build_succeeds do
- = f.check_box :only_allow_merge_if_build_succeeds
- %strong Only allow merge requests to be merged if the build succeeds
- %br
- %span.descr
- Builds need to be configured to enable this feature.
- = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
- .checkbox
- = f.label :only_allow_merge_if_all_discussions_are_resolved do
- = f.check_box :only_allow_merge_if_all_discussions_are_resolved
- %strong Only allow merge requests to be merged if all discussions are resolved
+- form = local_assigns.fetch(:form)
+
+%fieldset.features.merge-requests-feature.append-bottom-default
+ %hr
+ %h5.prepend-top-0
+ Merge Requests
+
+ = render 'projects/merge_request_merge_settings', form: form
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index d5004f6a066..ce8b66b1945 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -111,7 +111,7 @@
%span.label.label-primary
= tag
- - if @build.pipeline.stages.many?
+ - if @build.pipeline.stages_count > 1
.dropdown.build-dropdown
.title Stage
%button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
@@ -120,7 +120,7 @@
%ul.dropdown-menu
- @build.pipeline.stages.each do |stage|
%li
- %a.stage-item= stage
+ %a.stage-item= stage.name
.builds-container
- HasStatus::ORDERED_STATUSES.each do |build_status|
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index e75547c815f..18b3b04154f 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -104,9 +104,9 @@
= link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred')
- elsif allow_retry
- - if build.retryable?
- = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
- = icon('repeat')
- - elsif build.playable? && !admin
+ - if build.playable? && !admin
= link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= custom_icon('icon_play')
+ - elsif build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
+ = icon('repeat')
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index e4a963a278c..b58dceb58c9 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -44,7 +44,7 @@
Cant find HEAD commit for this branch
%td.stage-cell
- - pipeline.stages_with_statuses.each do |stage|
+ - pipeline.stages.each do |stage|
- if stage.status
- tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}"
.stage-container
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index e4cd55b9f7a..f6e3d5e76f5 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -11,7 +11,7 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3.page-title== #{label} this #{commit.change_type_title}
+ %h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body
= form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do
.form-group.branch
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
deleted file mode 100644
index 3a3d750439f..00000000000
--- a/app/views/projects/commit/_ci_stage.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-%tr
- %th{colspan: 10}
- %strong
- %a{name: stage}
- - status = statuses.latest.status
- %span{class: "ci-status-link ci-status-icon-#{status}"}
- = ci_icon_for_status(status)
- - if stage
- &nbsp;
- = stage.titleize
- = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true
- = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true
-%tr
- %td{colspan: 10}
- &nbsp;
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index cd9ed46d2c1..c7b5c1124b3 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -24,20 +24,8 @@
in
= time_interval_in_words pipeline.duration
- .row-content-block.build-content.middle-block.pipeline-graph.hidden
- .pipeline-visualization
- %ul.stage-column-list
- - stages = pipeline.stages_with_latest_statuses
- - stages.each do |stage, statuses|
- %li.stage-column
- .stage-name
- %a{name: stage}
- - if stage
- = stage.titleize
- .builds-container
- %ul
- = render "projects/commit/pipeline_stage", statuses: statuses
-
+ .row-content-block.build-content.middle-block.hidden
+ = render "projects/pipelines/graph", pipeline: pipeline
- if pipeline.yaml_errors.present?
.bs-callout.bs-callout-danger
@@ -62,5 +50,4 @@
- if pipeline.project.build_coverage_enabled?
%th Coverage
%th
- - pipeline.stages.each do |stage|
- = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
+ = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
deleted file mode 100644
index f9a9c8707f5..00000000000
--- a/app/views/projects/commit/_pipeline_stage.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-- status_groups = statuses.sort_by(&:name).group_by(&:group_name)
-- status_groups.each do |group_name, grouped_statuses|
- - if grouped_statuses.one?
- - status = grouped_statuses.first
- - is_playable = status.playable? && can?(current_user, :update_build, @project)
- %li.build{ class: ("playable" if is_playable) }
- .curve
- .build-content
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
- - else
- %li.build
- .curve
- .dropdown.inline.build-content
- = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 3a5af2723c6..01cd8fa0938 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -112,7 +112,8 @@
%span.descr Enable Container Registry for this project
= link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
- = render 'merge_request_settings', f: f
+ = render 'merge_request_settings', form: f
+
%hr
%fieldset.features.append-bottom-default
%h5.prepend-top-0
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index a9235d6af35..a65a630f2d0 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -17,4 +17,6 @@
"project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
"new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project),
"help-page-path" => help_page_path("ci/environments"),
- "css-class" => container_class}}
+ "css-class" => container_class,
+ "commit-icon-svg" => custom_icon("icon_commit"),
+ "play-icon-svg" => custom_icon("icon_play")}}
diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml
index 3d0ab5b85d6..98d81308407 100644
--- a/app/views/projects/forks/error.html.haml
+++ b/app/views/projects/forks/error.html.haml
@@ -13,7 +13,11 @@
- if @forked_project && @forked_project.errors.any?
%p
&ndash;
- = @forked_project.errors.full_messages.first
+ - error = @forked_project.errors.full_messages.first
+ - if error.include?("already been taken")
+ Name has already been taken
+ - else
+ = error
%p
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork", class: "btn" do
diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml
index af9a5b19060..55520fda494 100644
--- a/app/views/projects/group_links/update.js.haml
+++ b/app/views/projects/group_links/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}');
$("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("#group_member_#{@group_link.id}"));
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 0e2975bd551..896f10104fa 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -31,7 +31,7 @@
%span.label-branch= source_branch_with_namespace(@merge_request)
%span into
%span.label-branch
- = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
+ = link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
- if @merge_request.open? && @merge_request.diverged_from_target_branch?
%span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
diff --git a/app/views/projects/pipelines/_graph.html.haml b/app/views/projects/pipelines/_graph.html.haml
new file mode 100644
index 00000000000..0202833c0bf
--- /dev/null
+++ b/app/views/projects/pipelines/_graph.html.haml
@@ -0,0 +1,4 @@
+- pipeline = local_assigns.fetch(:pipeline)
+.pipeline-visualization.pipeline-graph
+ %ul.stage-column-list
+ = render partial: "projects/stage/graph", collection: pipeline.stages, as: :stage
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index ac139797a89..739e5930822 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -12,19 +12,8 @@
.tab-content
#js-tab-pipeline.tab-pane
- .build-content.middle-block.pipeline-graph
- .pipeline-visualization
- %ul.stage-column-list
- - stages = pipeline.stages_with_latest_statuses
- - stages.each do |stage, statuses|
- %li.stage-column
- .stage-name
- %a{name: stage}
- - if stage
- = stage.titleize
- .builds-container
- %ul
- = render "projects/commit/pipeline_stage", statuses: statuses
+ .build-content.middle-block
+ = render "projects/pipelines/graph", pipeline: pipeline
#js-tab-builds.tab-pane
- if pipeline.yaml_errors.present?
@@ -50,5 +39,4 @@
- if pipeline.project.build_coverage_enabled?
%th Coverage
%th
- - pipeline.stages.each do |stage|
- = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
+ = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 91927181efb..d15f4310ff5 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
$("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml
new file mode 100644
index 00000000000..1d8fa10db0c
--- /dev/null
+++ b/app/views/projects/stage/_graph.html.haml
@@ -0,0 +1,22 @@
+- stage = local_assigns.fetch(:stage)
+- statuses = stage.statuses.latest
+- status_groups = statuses.sort_by(&:name).group_by(&:group_name)
+%li.stage-column
+ .stage-name
+ %a{ name: stage.name }
+ = stage.name.titleize
+ .builds-container
+ %ul
+ - status_groups.each do |group_name, grouped_statuses|
+ - if grouped_statuses.one?
+ - status = grouped_statuses.first
+ - is_playable = status.playable? && can?(current_user, :update_build, @project)
+ %li.build{ class: ("playable" if is_playable) }
+ .curve
+ .build-content
+ = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ - else
+ %li.build
+ .curve
+ .dropdown.inline.build-content
+ = render "projects/stage/in_stage_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml
index 2b26ad9d6fa..2b26ad9d6fa 100644
--- a/app/views/projects/commit/_pipeline_status_group.html.haml
+++ b/app/views/projects/stage/_in_stage_group.html.haml
diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml
new file mode 100644
index 00000000000..1684e02fbad
--- /dev/null
+++ b/app/views/projects/stage/_stage.html.haml
@@ -0,0 +1,13 @@
+%tr
+ %th{colspan: 10}
+ %strong
+ %a{ name: stage.name }
+ %span{class: "ci-status-link ci-status-icon-#{stage.status}"}
+ = ci_icon_for_status(stage.status)
+ &nbsp;
+ = stage.name.titleize
+= render stage.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true
+= render stage.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true
+%tr
+ %td{colspan: 10}
+ &nbsp;
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index c367ae336db..e50ab5fea09 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -5,5 +5,7 @@
- if event_filter_visible(:merge_requests)
= event_filter_link EventFilter.merged, 'Merge events'
- if event_filter_visible(:issues)
+ = event_filter_link EventFilter.issue, 'Issue events'
+ - if comments_visible?
= event_filter_link EventFilter.comments, 'Comments'
= event_filter_link EventFilter.team, 'Team'
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 1c0346bbc78..8928de9097b 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -1,7 +1,8 @@
- group_link = local_assigns[:group_link]
- group = group_link.group
- can_admin_member = can?(current_user, :admin_project_member, @project)
-%li.member.group_member{ id: "group_member_#{group_link.id}" }
+- dom_id = "group_member_#{group_link.id}"
+%li.member.group_member{ id: dom_id }
%span{ class: "list-item-name" }
= image_tag group_icon(group), class: "avatar s40", alt: ''
%strong
@@ -14,7 +15,23 @@
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls
= form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
- = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member
+ = hidden_field_tag "group_link[group_access]", group_link.group_access
+ .member-form-control.dropdown.append-right-5
+ %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: !can_admin_member,
+ data: { toggle: "dropdown", field_name: "group_link[group_access]" } }
+ %span.dropdown-toggle-text
+ = group_link.human_access
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ = dropdown_title("Change permissions")
+ .dropdown-content
+ %ul
+ - Gitlab::Access.options.each do |role, role_id|
+ %li
+ = link_to role, "javascript:void(0)",
+ class: ("is-active" if group_link.group_access == role_id),
+ data: { id: role_id, el_id: dom_id }
.prepend-left-5.clearable-input.member-form-control
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 432047a1c4e..659d4c905fc 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -20,8 +20,8 @@
%strong Blocked
- if source.instance_of?(Group) && !@group
- = link_to source, class: "member-group-link prepend-left-5" do
- = "· #{source.name}"
+ &middot;
+ = link_to source.name, source, class: "member-group-link"
.hidden-xs.cgray
- if member.request?
@@ -45,12 +45,28 @@
= time_ago_with_tooltip(member.created_at)
- if show_roles
.controls.member-controls
- - if show_controls
+ - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project)
- if user != current_user
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
- = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member
+ = f.hidden_field :access_level
+ .member-form-control.dropdown.append-right-5
+ %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: !can_admin_member,
+ data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
+ %span.dropdown-toggle-text
+ = member.human_access
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ = dropdown_title("Change permissions")
+ .dropdown-content
+ %ul
+ - Gitlab::Access.options.each do |role, role_id|
+ %li
+ = link_to role, "javascript:void(0)",
+ class: ("is-active" if member.access_level == role_id),
+ data: { id: role_id, el_id: dom_id(member) }
.prepend-left-5.clearable-input.member-form-control
- = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member
+ = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) }
%i.clear-icon.js-clear-input
- else
%span.member-access-text= member.human_access
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index 0a237136959..d27fba805a3 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -25,8 +25,10 @@
%span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}%
complete
- %span.milestone-stat
- %span.remaining-days= milestone_remaining_days(milestone)
+ - remaining_days = milestone_remaining_days(milestone)
+ - if remaining_days.present?
+ %span.milestone-stat
+ %span.remaining-days= remaining_days
.milestone-progress-buttons
%span.tab-issues-buttons
diff --git a/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml
new file mode 100644
index 00000000000..19c76b5b437
--- /dev/null
+++ b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml
@@ -0,0 +1,4 @@
+---
+title: Fix wrong tab selected when loggin fails and multiple login tabs exists
+merge_request: 7314
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/23696-fix-diff-view-highlighting.yml b/changelogs/unreleased/23696-fix-diff-view-highlighting.yml
deleted file mode 100644
index db523caffed..00000000000
--- a/changelogs/unreleased/23696-fix-diff-view-highlighting.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix diff view permalink highlighting
-merge_request: 7090
-author:
diff --git a/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml
new file mode 100644
index 00000000000..34999480d4a
--- /dev/null
+++ b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml
@@ -0,0 +1,4 @@
+---
+title: 'Remove unnecessary target branch link from MR page in case of deleted target branch'
+merge_request: 7916
+author: Rydkin Maxim
diff --git a/changelogs/unreleased/24814-pipeline-tabs.yml b/changelogs/unreleased/24814-pipeline-tabs.yml
deleted file mode 100644
index f85e7576905..00000000000
--- a/changelogs/unreleased/24814-pipeline-tabs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Cicking on tabs on pipeline page should set URL
-merge_request: 7709
-author:
diff --git a/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml
new file mode 100644
index 00000000000..12ea08e3815
--- /dev/null
+++ b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml
@@ -0,0 +1,5 @@
+---
+title: 'fix: 24982- Remove''Signed in successfully'' message After this change the
+ sign-in-success flash message will not be shown'
+merge_request: 7837
+author: jnoortheen
diff --git a/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml b/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml
new file mode 100644
index 00000000000..a7576e2cbdb
--- /dev/null
+++ b/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml
@@ -0,0 +1,4 @@
+---
+title: Remove wrong '.builds-feature' class from the MR settings fieldset
+merge_request: 7930
+author:
diff --git a/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml
new file mode 100644
index 00000000000..79cb2c6d843
--- /dev/null
+++ b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml
@@ -0,0 +1,4 @@
+---
+title: 'Fix comments activity tab visibility condition'
+merge_request: 7913
+author: Rydkin Maxim
diff --git a/changelogs/unreleased/25374-svg-as-prop.yml b/changelogs/unreleased/25374-svg-as-prop.yml
new file mode 100644
index 00000000000..45a71b55b3b
--- /dev/null
+++ b/changelogs/unreleased/25374-svg-as-prop.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve "Provide SVG as a prop instead of hiding and copy them in environments table"
+merge_request: 7992
+author:
diff --git a/changelogs/unreleased/api-remove-source-branch.yml b/changelogs/unreleased/api-remove-source-branch.yml
new file mode 100644
index 00000000000..d1b6507aedb
--- /dev/null
+++ b/changelogs/unreleased/api-remove-source-branch.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Ability to set ''should_remove_source_branch'' on merge requests'
+merge_request:
+author: Robert Schilling
diff --git a/changelogs/unreleased/destroy-session.yml b/changelogs/unreleased/destroy-session.yml
new file mode 100644
index 00000000000..e713e2dc424
--- /dev/null
+++ b/changelogs/unreleased/destroy-session.yml
@@ -0,0 +1,4 @@
+---
+title: Destroy a user's session when they delete their own account
+merge_request:
+author:
diff --git a/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml
new file mode 100644
index 00000000000..12b1460f388
--- /dev/null
+++ b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml
@@ -0,0 +1,4 @@
+---
+title: Do not reload diff for merge request made from fork when target branch in fork is updated
+merge_request: 7973
+author:
diff --git a/changelogs/unreleased/dz-nested-groups.yml b/changelogs/unreleased/dz-nested-groups.yml
new file mode 100644
index 00000000000..c227c5a8ea5
--- /dev/null
+++ b/changelogs/unreleased/dz-nested-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Add nested groups support on data level
+merge_request:
+author:
diff --git a/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml
new file mode 100644
index 00000000000..9c52e53c3b4
--- /dev/null
+++ b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml
@@ -0,0 +1,4 @@
+---
+title: Enable AsciiDoctor admonition icons
+merge_request: 7812
+author: Horacio Sanson
diff --git a/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml b/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml
deleted file mode 100644
index 9f14463fdd1..00000000000
--- a/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Authorize users into imported GitLab project
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml b/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml
deleted file mode 100644
index db92e45d8f1..00000000000
--- a/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix compatibility with Internet Explorer 11 for merge requests
-merge_request: 7525
-author: Steffen Rauh
diff --git a/changelogs/unreleased/fix-milestone-summary.yml b/changelogs/unreleased/fix-milestone-summary.yml
new file mode 100644
index 00000000000..3045a15054c
--- /dev/null
+++ b/changelogs/unreleased/fix-milestone-summary.yml
@@ -0,0 +1,4 @@
+---
+title: Displays milestone remaining days only when it's present
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-slack-pipeline-event.yml b/changelogs/unreleased/fix-slack-pipeline-event.yml
deleted file mode 100644
index fec864eeb3d..00000000000
--- a/changelogs/unreleased/fix-slack-pipeline-event.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix pipeline author for Slack and use pipeline id for pipeline link
-merge_request: 7506
-author:
diff --git a/changelogs/unreleased/group-members-in-project-members-view.yml b/changelogs/unreleased/group-members-in-project-members-view.yml
new file mode 100644
index 00000000000..415e2b6b1e2
--- /dev/null
+++ b/changelogs/unreleased/group-members-in-project-members-view.yml
@@ -0,0 +1,4 @@
+---
+title: Shows group members in project members list
+merge_request:
+author:
diff --git a/changelogs/unreleased/html-safe-diff-line-content.yml b/changelogs/unreleased/html-safe-diff-line-content.yml
new file mode 100644
index 00000000000..8f8bbc51963
--- /dev/null
+++ b/changelogs/unreleased/html-safe-diff-line-content.yml
@@ -0,0 +1,4 @@
+---
+title: Don't accidentally mark unsafe diff lines as HTML safe
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue-events-filter.yml b/changelogs/unreleased/issue-events-filter.yml
new file mode 100644
index 00000000000..a3b08bde6e7
--- /dev/null
+++ b/changelogs/unreleased/issue-events-filter.yml
@@ -0,0 +1,4 @@
+---
+title: Add issue events filter and make all really show all events
+merge_request: 7673
+author: Oxan van Leeuwen
diff --git a/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml
new file mode 100644
index 00000000000..5a4a44b9562
--- /dev/null
+++ b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml
@@ -0,0 +1,4 @@
+---
+title: Replace MR access checks with use of MergeRequestsFinder
+merge_request:
+author:
diff --git a/changelogs/unreleased/members-dropdowns.yml b/changelogs/unreleased/members-dropdowns.yml
new file mode 100644
index 00000000000..b15403d6d62
--- /dev/null
+++ b/changelogs/unreleased/members-dropdowns.yml
@@ -0,0 +1,4 @@
+---
+title: Updated members dropdowns
+merge_request:
+author:
diff --git a/changelogs/unreleased/public-tags-api.yml b/changelogs/unreleased/public-tags-api.yml
new file mode 100644
index 00000000000..f5e844470b2
--- /dev/null
+++ b/changelogs/unreleased/public-tags-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow public access to some Tag API endpoints
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-has-visible-content-caching.yml b/changelogs/unreleased/remove-has-visible-content-caching.yml
deleted file mode 100644
index e2940c60443..00000000000
--- a/changelogs/unreleased/remove-has-visible-content-caching.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove visible content caching
-merge_request:
-author:
diff --git a/changelogs/unreleased/render-svg-in-diffs-and-notes.yml b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml
new file mode 100644
index 00000000000..827b0dbb1d3
--- /dev/null
+++ b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml
@@ -0,0 +1,4 @@
+---
+title: Render SVG images in diffs and notes
+merge_request: 7747
+author: andrebsguedes
diff --git a/changelogs/unreleased/small-emoji-adjustments.yml b/changelogs/unreleased/small-emoji-adjustments.yml
new file mode 100644
index 00000000000..804bd05b613
--- /dev/null
+++ b/changelogs/unreleased/small-emoji-adjustments.yml
@@ -0,0 +1,4 @@
+---
+title: Various small emoji positioning adjustments
+merge_request:
+author:
diff --git a/changelogs/unreleased/timeago-perf-fix.yml b/changelogs/unreleased/timeago-perf-fix.yml
new file mode 100644
index 00000000000..265e7db29a9
--- /dev/null
+++ b/changelogs/unreleased/timeago-perf-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed timeago re-rendering every timeago
+merge_request:
+author:
diff --git a/changelogs/unreleased/update-button-font-weight.yml b/changelogs/unreleased/update-button-font-weight.yml
new file mode 100644
index 00000000000..ddb3c1c8da4
--- /dev/null
+++ b/changelogs/unreleased/update-button-font-weight.yml
@@ -0,0 +1,4 @@
+---
+title: Updates the font weight of button styles because of the change to system fonts
+merge_request:
+author:
diff --git a/config/application.rb b/config/application.rb
index e2babbe6707..310d9e80156 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -45,7 +45,7 @@ module Gitlab
#
# Parameters filtered:
# - Password (:password, :password_confirmation)
- # - Private tokens (:private_token)
+ # - Private tokens (:private_token, :authentication_token)
# - Two-factor tokens (:otp_attempt)
# - Repo/Project Import URLs (:import_url)
# - Build variables (:variables)
@@ -55,6 +55,7 @@ module Gitlab
# - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key)
config.filter_parameters += %i(
+ authentication_token
certificate
encrypted_key
hook
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 4f30d1265c8..6b0cff75653 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -15,7 +15,7 @@ if Rails.env.production?
Raven.configure do |config|
config.dsn = current_application_settings.sentry_dsn
config.release = Gitlab::REVISION
-
+
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
# Sanitize authentication headers
diff --git a/db/migrate/20161124111390_add_parent_id_to_namespace.rb b/db/migrate/20161124111390_add_parent_id_to_namespace.rb
new file mode 100644
index 00000000000..a6fa1b70a9d
--- /dev/null
+++ b/db/migrate/20161124111390_add_parent_id_to_namespace.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddParentIdToNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:namespaces, :parent_id, :integer)
+ end
+end
diff --git a/db/migrate/20161124111395_add_index_to_parent_id.rb b/db/migrate/20161124111395_add_index_to_parent_id.rb
new file mode 100644
index 00000000000..eab74c01dfd
--- /dev/null
+++ b/db/migrate/20161124111395_add_index_to_parent_id.rb
@@ -0,0 +1,14 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToParentId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index(:namespaces, [:parent_id, :id], unique: true)
+ end
+end
diff --git a/db/migrate/20161124111402_add_routes_table.rb b/db/migrate/20161124111402_add_routes_table.rb
new file mode 100644
index 00000000000..a02e046a18e
--- /dev/null
+++ b/db/migrate/20161124111402_add_routes_table.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddRoutesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :routes do |t|
+ t.integer :source_id, null: false
+ t.string :source_type, null: false
+ t.string :path, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20161130095245_fill_routes_table.rb b/db/migrate/20161130095245_fill_routes_table.rb
new file mode 100644
index 00000000000..6754e583000
--- /dev/null
+++ b/db/migrate/20161130095245_fill_routes_table.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class FillRoutesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'No new namespaces should be created during data copy'
+
+ def up
+ execute <<-EOF
+ INSERT INTO routes
+ (source_id, source_type, path)
+ (SELECT id, 'Namespace', path FROM namespaces)
+ EOF
+ end
+
+ def down
+ Route.delete_all(source_type: 'Namespace')
+ end
+end
diff --git a/db/migrate/20161130101252_fill_projects_routes_table.rb b/db/migrate/20161130101252_fill_projects_routes_table.rb
new file mode 100644
index 00000000000..14700583be5
--- /dev/null
+++ b/db/migrate/20161130101252_fill_projects_routes_table.rb
@@ -0,0 +1,22 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class FillProjectsRoutesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'No new projects should be created during data copy'
+
+ def up
+ execute <<-EOF
+ INSERT INTO routes
+ (source_id, source_type, path)
+ (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) FROM projects
+ INNER JOIN namespaces ON projects.namespace_id = namespaces.id)
+ EOF
+ end
+
+ def down
+ Route.delete_all(source_type: 'Project')
+ end
+end
diff --git a/db/migrate/20161202152031_remove_duplicates_from_routes.rb b/db/migrate/20161202152031_remove_duplicates_from_routes.rb
new file mode 100644
index 00000000000..510796e05f2
--- /dev/null
+++ b/db/migrate/20161202152031_remove_duplicates_from_routes.rb
@@ -0,0 +1,28 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveDuplicatesFromRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row|
+ path = connection.quote(row['path'])
+ execute(%Q{
+ DELETE FROM #{quote_table_name(:routes)}
+ WHERE path = #{path}
+ AND id != (
+ SELECT id FROM (
+ SELECT max(id) AS id
+ FROM #{quote_table_name(:routes)}
+ WHERE path = #{path}
+ ) max_ids
+ )
+ })
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20161202152035_add_index_to_routes.rb b/db/migrate/20161202152035_add_index_to_routes.rb
new file mode 100644
index 00000000000..4a51337bda6
--- /dev/null
+++ b/db/migrate/20161202152035_add_index_to_routes.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index(:routes, :path, unique: true)
+ add_concurrent_index(:routes, [:source_type, :source_id], unique: true)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index da7c8c735a4..8c9c4663c61 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: 20161128161412) do
+ActiveRecord::Schema.define(version: 20161202152035) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161128161412) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
- t.boolean "sidekiq_throttling_enabled", default: false
- t.string "sidekiq_throttling_queues"
- t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false
+ t.boolean "sidekiq_throttling_enabled", default: false
+ t.string "sidekiq_throttling_queues"
+ t.decimal "sidekiq_throttling_factor"
t.boolean "html_emails_enabled", default: true
end
@@ -737,8 +737,9 @@ ActiveRecord::Schema.define(version: 20161128161412) do
t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at"
- t.boolean "lfs_enabled"
t.text "description_html"
+ t.boolean "lfs_enabled"
+ t.integer "parent_id"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -746,6 +747,7 @@ ActiveRecord::Schema.define(version: 20161128161412) do
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
+ add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
@@ -991,6 +993,17 @@ ActiveRecord::Schema.define(version: 20161128161412) do
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree
+ create_table "routes", force: :cascade do |t|
+ t.integer "source_id", null: false
+ t.string "source_type", null: false
+ t.string "path", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree
+ add_index "routes", ["source_type", "source_id"], name: "index_routes_on_source_type_and_source_id", unique: true, using: :btree
+
create_table "sent_notifications", force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
@@ -1206,8 +1219,8 @@ ActiveRecord::Schema.define(version: 20161128161412) do
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
- t.string "organization"
t.string "incoming_email_token"
+ t.string "organization"
t.boolean "authorized_projects_populated"
end
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index 76f3a0fb387..b36cf18d459 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -41,7 +41,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
mailroom['enable'] = false
# PostgreSQL configuration
- postgresql['sql_password'] = 'DB password'
+ gitlab_rails['db_password'] = 'DB password'
postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0']
postgresql['listen_address'] = '0.0.0.0'
```
@@ -80,7 +80,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
1. Similarly, set the password for the `gitlab` database user. Use the same
password that you specified in the `/etc/gitlab/gitlab.rb` file for
- `postgresql['sql_password']`.
+ `gitlab_rails['db_password']`.
```
\password gitlab
diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md
index 1b7a1840138..b6459971420 100644
--- a/doc/api/build_triggers.md
+++ b/doc/api/build_triggers.md
@@ -15,7 +15,7 @@ GET /projects/:id/triggers
| `id` | integer | yes | The ID of a project |
```
-curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
```
```json
@@ -51,7 +51,7 @@ GET /projects/:id/triggers/:token
| `token` | string | yes | The `token` of a trigger |
```
-curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
```
```json
@@ -77,7 +77,7 @@ POST /projects/:id/triggers
| `id` | integer | yes | The ID of a project |
```
-curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
```
```json
@@ -104,7 +104,7 @@ DELETE /projects/:id/triggers/:token
| `token` | string | yes | The `token` of a trigger |
```
-curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
```
```json
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
index a21751a49ea..917e9773913 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/build_variables.md
@@ -13,7 +13,7 @@ GET /projects/:id/variables
| `id` | integer | yes | The ID of a project |
```
-curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
```
```json
@@ -43,7 +43,7 @@ GET /projects/:id/variables/:key
| `key` | string | yes | The `key` of a variable |
```
-curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
```
```json
@@ -68,7 +68,7 @@ POST /projects/:id/variables
| `value` | string | yes | The `value` of a variable |
```
-curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
```
```json
@@ -93,7 +93,7 @@ PUT /projects/:id/variables/:key
| `value` | string | yes | The `value` of a variable |
```
-curl --request PUT --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
```
```json
@@ -117,7 +117,7 @@ DELETE /projects/:id/variables/:key
| `key` | string | yes | The `key` of a variable |
```
-curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
```
```json
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 9460b3f73b1..81df55ab4ab 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -271,17 +271,18 @@ Creates a new merge request.
POST /projects/:id/merge_requests
```
-Parameters:
-
-- `id` (required) - The ID of a project
-- `source_branch` (required) - The source branch
-- `target_branch` (required) - The target branch
-- `assignee_id` (optional) - Assignee user ID
-- `title` (required) - Title of MR
-- `description` (optional) - Description of MR
-- `target_project_id` (optional) - The target project (numeric id)
-- `labels` (optional) - Labels for MR as a comma-separated list
-- `milestone_id` (optional) - Milestone ID
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | string | yes | The ID of a project |
+| `source_branch` | string | yes | The source branch |
+| `target_branch` | string | yes | The target branch |
+| `title` | string | yes | Title of MR |
+| `assignee_id` | integer | no | Assignee user ID |
+| `description` | string | no | Description of MR |
+| `target_project_id` | integer | no | The target project (numeric id) |
+| `labels` | string | no | Labels for MR as a comma-separated list |
+| `milestone_id` | integer | no | The ID of a milestone |
+| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
```json
{
@@ -346,17 +347,19 @@ Updates an existing merge request. You can change the target branch, title, or e
PUT /projects/:id/merge_requests/:merge_request_id
```
-Parameters:
-
-- `id` (required) - The ID of a project
-- `merge_request_id` (required) - ID of MR
-- `target_branch` - The target branch
-- `assignee_id` - Assignee user ID
-- `title` - Title of MR
-- `description` - Description of MR
-- `state_event` - New state (close|reopen|merge)
-- `labels` (optional) - Labels for MR as a comma-separated list
-- `milestone_id` (optional) - Milestone ID
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | string | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a merge request |
+| `source_branch` | string | yes | The source branch |
+| `target_branch` | string | yes | The target branch |
+| `title` | string | yes | Title of MR |
+| `assignee_id` | integer | no | Assignee user ID |
+| `description` | string | no | Description of MR |
+| `target_project_id` | integer | no | The target project (numeric id) |
+| `labels` | string | no | Labels for MR as a comma-separated list |
+| `milestone_id` | integer | no | The ID of a milestone |
+| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
```json
{
@@ -807,7 +810,7 @@ Example response:
## Create a todo
-Manually creates a todo for the current user on a merge request.
+Manually creates a todo for the current user on a merge request.
If there already exists a todo for the user on that merge request,
status code `304` is returned.
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 14573d48fe4..7f78ffc2390 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -2,7 +2,9 @@
## List project repository tags
-Get a list of repository tags from a project, sorted by name in reverse alphabetical order.
+Get a list of repository tags from a project, sorted by name in reverse
+alphabetical order. This endpoint can be accessed without authentication if the
+repository is publicly accessible.
```
GET /projects/:id/repository/tags
@@ -40,7 +42,8 @@ Parameters:
## Get a single repository tag
-Get a specific repository tag determined by its name.
+Get a specific repository tag determined by its name. This endpoint can be
+accessed without authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/tags/:tag_name
diff --git a/doc/api/users.md b/doc/api/users.md
index 52a6b691610..28b6c7bd491 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -291,7 +291,9 @@ Parameters:
- `id` (required) - The ID of the user
-## Current user
+## User
+
+### For normal users
Gets currently authenticated user.
@@ -335,6 +337,53 @@ GET /user
}
```
+### For admins
+
+Parameters:
+
+- `sudo` (required) - the ID of a user
+
+```
+GET /user
+```
+
+```json
+{
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
+ "web_url": "http://localhost:3000/john_smith",
+ "created_at": "2012-05-23T08:00:58Z",
+ "is_admin": false,
+ "bio": null,
+ "location": null,
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": "",
+ "organization": "",
+ "last_sign_in_at": "2012-06-01T11:41:01Z",
+ "confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
+ "color_scheme_id": 2,
+ "projects_limit": 100,
+ "current_sign_in_at": "2012-06-02T06:36:55Z",
+ "identities": [
+ {"provider": "github", "extern_uid": "2435223452345"},
+ {"provider": "bitbucket", "extern_uid": "john_smith"},
+ {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"}
+ ],
+ "can_create_group": true,
+ "can_create_project": true,
+ "two_factor_enabled": true,
+ "external": false,
+ "private_token": "dd34asd13as"
+}
+```
+
## List SSH keys
Get a list of currently authenticated user's SSH keys.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 9c6a9656557..2740b2982b9 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-14-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab
-**Note:** You can change `8-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/project_services/img/mattermost_console_integrations.png b/doc/project_services/img/mattermost_console_integrations.png
index b3b8c20d7bf..92a30da5be0 100644
--- a/doc/project_services/img/mattermost_console_integrations.png
+++ b/doc/project_services/img/mattermost_console_integrations.png
Binary files differ
diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md
index 366e4b2d306..390066c9989 100644
--- a/doc/project_services/jira.md
+++ b/doc/project_services/jira.md
@@ -197,6 +197,7 @@ incorrectly the JIRA-GitLab integration.
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.
+JIRA issue references and update comments will not work if the GitLab issue tracker is disabled.
### GitLab is unable to close a ticket
diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md
index 1507dfa3abd..6fcbf6f1642 100644
--- a/doc/project_services/mattermost_slash_commands.md
+++ b/doc/project_services/mattermost_slash_commands.md
@@ -22,6 +22,9 @@ commands in Mattermost and then enable the service in GitLab.
### Step 1. Enable custom slash commands in Mattermost
+This step is only required when using a source install, omnibus installs will be
+preconfigured with the right settings.
+
The first thing to do in Mattermost is to enable custom slash commands from
the administrator console.
@@ -32,8 +35,9 @@ the administrator console.
---
-1. Click **Custom integrations** and set **Enable Custom Slash Commands** to
- true.
+1. Click **Custom integrations** and set **Enable Custom Slash Commands**,
+ **Enable custom integrations to override usernames**, and **Override
+ custom integrations to override profile picture icons** to true
![Mattermost console](img/mattermost_console_integrations.png)
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index d6a0979f6ec..9803937fcf9 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -1,75 +1,141 @@
# SSH
-## SSH keys
+Git is a distributed version control system, which means you can work locally
+but you can also share or "push" your changes to other servers.
+Before you can push your changes to a GitLab server
+you need a secure communication channel for sharing information.
+GitLab uses Public-key or asymmetric cryptography
+which encrypts a communication channel by locking it with your "private key"
+and allows trusted parties to unlock it with your "public key".
+If someone does not have your public key they cannot access the unencrypted message.
-An SSH key allows you to establish a secure connection between your
-computer and GitLab. Before generating an SSH key in your shell, check if your system
-already has one by running the following command:
+## Locating an existing SSH key pair
+
+Before generating a new SSH key check if your system already has one
+at the default location by opening a shell, or Command Prompt on Windows,
+and running the following command:
+
+**Windows Command Prompt:**
-**Windows Command Line:**
```bash
type %userprofile%\.ssh\id_rsa.pub
```
-**GNU/Linux/Mac/PowerShell:**
+
+**GNU/Linux / macOS / PowerShell:**
+
```bash
cat ~/.ssh/id_rsa.pub
```
-If you see a long string starting with `ssh-rsa`, you can skip the `ssh-keygen` step.
+If you see a string starting with `ssh-rsa` you already have an SSH key pair
+and you can skip the next step **Generating a new SSH key pair**
+and continue onto **Copying your public SSH key to the clipboard**.
+If you don't see the string or would like to generate a SSH key pair with a
+custom name continue onto the next step.
-To generate a new SSH key, use the following command:
-```bash
-ssh-keygen -t rsa -C "$your_email"
-```
-This command will prompt you for a location and filename to store the key
-pair and for a password. When prompted for the location and filename, just
-press enter to use the default. If you use a different name, the key will not
-be used automatically.
+## Generating a new SSH key pair
-Note: It is a best practice to use a password for an SSH key, but it is not
-required and you can skip creating a password by pressing enter.
+1. To generate a new SSH key, use the following command:
-If you want to change the password of your key later, you can use the following
-command: `ssh-keygen -p <keyname>`
+ **GNU/Linux / macOS:**
-Use the command below to show your public key:
+ ```bash
+ ssh-keygen -t rsa -C "GitLab" -b 4096
+ ```
-**Windows Command Line:**
-```bash
-type %userprofile%\.ssh\id_rsa.pub
-```
-**GNU/Linux/Mac/PowerShell:**
-```bash
-cat ~/.ssh/id_rsa.pub
-```
+ **Windows:**
-Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your
-user profile. Please copy the complete key starting with `ssh-rsa` and ending
-with your username and host.
+ On Windows you will need to download
+ [PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)
+ and follow this [documentation article][winputty] to generate a SSH key pair.
-To copy your public key to the clipboard, use the code below. Depending on your
-OS you'll need to use a different command:
+1. Next, you will be prompted to input a file path to save your key pair to.
-**Windows Command Line:**
-```bash
-type %userprofile%\.ssh\id_rsa.pub | clip
-```
+ If you don't already have an SSH key pair use the suggested path by pressing
+ enter. Using the suggested path will allow your SSH client
+ to automatically use the key pair with no additional configuration.
-**Windows PowerShell:**
-```bash
-cat ~/.ssh/id_rsa.pub | clip
-```
+ If you already have a key pair with the suggested file path, you will need
+ to input a new file path and declare what host this key pair will be used
+ for in your `.ssh/config` file, see **Working with non-default SSH key pair paths**
+ for more information.
+
+1. Once you have input a file path you will be prompted to input a password to
+ secure your SSH key pair. It is a best practice to use a password for an SSH
+ key pair, but it is not required and you can skip creating a password by
+ pressing enter.
+
+ >**Note:**
+ If you want to change the password of your key, you can use `ssh-keygen -p <keyname>`.
+
+1. The next step is to copy the public key as we will need it afterwards.
+
+ To copy your public key to the clipboard, use the appropriate code for your
+ operating system below:
+
+ **macOS:**
+
+ ```bash
+ pbcopy < ~/.ssh/id_rsa.pub
+ ```
+
+ **GNU/Linux (requires the xclip package):**
+
+ ```bash
+ xclip -sel clip < ~/.ssh/id_rsa.pub
+ ```
+
+ **Windows Command Line:**
+
+ ```bash
+ type %userprofile%\.ssh\id_rsa.pub | clip
+ ```
+
+ **Windows PowerShell:**
+
+ ```bash
+ cat ~/.ssh/id_rsa.pub | clip
+ ```
+
+1. The final step is to add your public SSH key to GitLab.
+
+ Navigate to the 'SSH Keys' tab in you 'Profile Settings'.
+ Paste your key in the 'Key' section and give it a relevant 'Title'.
+ Use an identifiable title like 'Work Laptop - Windows 7' or
+ 'Home MacBook Pro 15'.
+
+ If you manually copied your public SSH key make sure you copied the entire
+ key starting with `ssh-rsa` and ending with your email.
+
+## Working with non-default SSH key pair paths
+
+If you used a non-default file path for your GitLab SSH key pair,
+you must configure your SSH client to find your GitLab SSH private key
+for connections to your GitLab server (perhaps gitlab.com).
+
+For OpenSSH clients this is configured in the `~/.ssh/config` file.
+Below are two example host configurations using their own key:
-**Mac:**
-```bash
-pbcopy < ~/.ssh/id_rsa.pub
```
+# GitLab.com server
+Host gitlab.com
+RSAAuthentication yes
+IdentityFile ~/.ssh/config/private-key-filename-01
-**GNU/Linux (requires xclip):**
-```bash
-xclip -sel clip < ~/.ssh/id_rsa.pub
+# Private GitLab server
+Host gitlab.company.com
+RSAAuthentication yes
+IdentityFile ~/.ssh/config/private-key-filename
```
+Due to the wide variety of SSH clients and their very large number of
+configuration options, further explanation of these topics is beyond the scope
+of this document.
+
+Public SSH keys need to be unique, as they will bind to your account.
+Your SSH key is the only identifier you'll have when pushing code via SSH.
+That's why it needs to uniquely map to a single user.
+
## Deploy keys
Deploy keys allow read-only access to multiple projects with a single SSH
@@ -89,10 +155,10 @@ If you want to add the same key to another project, please enable it in the
list that says 'Deploy keys from projects available to you'. All the deploy
keys of all the projects you have access to are available. This project
access can happen through being a direct member of the project, or through
-a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more
-information.
+a group.
-Deploy keys can be shared between projects, you just need to add them to each project.
+Deploy keys can be shared between projects, you just need to add them to each
+project.
## Applications
@@ -100,33 +166,4 @@ Deploy keys can be shared between projects, you just need to add them to each pr
How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
-## Tip: Non-default OpenSSH key file names or locations
-
-If, for whatever reason, you decide to specify a non-default location and filename for your GitLab SSH key pair, you must configure your SSH client to find your GitLab SSH private key for connections to your GitLab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following:
-
-```
-#
-# Main gitlab.com server
-#
-Host gitlab.com
-RSAAuthentication yes
-IdentityFile ~/my-ssh-key-directory/my-gitlab-private-key-filename
-User mygitlabusername
-```
-
-Another example
-```
-#
-# Our company's internal GitLab server
-#
-Host my-gitlab.company.com
-RSAAuthentication yes
-IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename
-```
-
-Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ.
-
-Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document.
-
-Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll
-have when pushing code via SSH. That's why it needs to uniquely map to a single user. \ No newline at end of file
+[winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen
diff --git a/doc/university/README.md b/doc/university/README.md
index 8917636c59b..12727e9d56f 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -5,7 +5,7 @@ GitLab University is the best place to learn about **Version Control with Git an
It doesn't replace, but accompanies our great [Documentation](https://docs.gitlab.com)
and [Blog Articles](https://about.gitlab.com/blog/).
-Would you like to contribute to GitLab University? Then please take a look at our contribution [process](/process) for more information.
+Would you like to contribute to GitLab University? Then please take a look at our contribution [process](process) for more information.
## Gitlab University Curriculum
@@ -97,7 +97,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Markdown in GitLab](../user/markdown.md)
1. [Issues and Merge Requests - Video](https://www.youtube.com/watch?v=raXvuwet78M)
-1. [Due Dates and Milestones fro GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
+1. [Due Dates and Milestones for GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/)
1. [Applying GitLab Labels Automatically](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/)
1. [GitLab Issue Board - Product Page](https://about.gitlab.com/solutions/issueboard/)
@@ -147,7 +147,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Xebia Labs: Dev Ops Terminology](https://xebialabs.com/glossary/)
1. [Xebia Labs: Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/)
-1. [Puppet Labs: State of Dev Ops 2015 - Book](https://puppetlabs.com/sites/default/files/2015-state-of-devops-report.pdf)
+1. [Puppet Labs: State of Dev Ops 2016 - Book](https://puppet.com/resources/white-paper/2016-state-of-devops-report)
#### 3.2. Installing GitLab with Omnibus
@@ -173,7 +173,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 3.6 Custom Languages
-1. [How to add Syntax Highlighting Support for Custom Langauges to GitLab - Video](how to add support for your favorite language to GitLab)
+1. [How to add Syntax Highlighting Support for Custom Languages to GitLab - Video](https://youtu.be/6WxTMqatrrA)
#### 3.7. Scalability and High Availability
@@ -198,7 +198,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
---
-## 4. External Articles
+### 4. External Articles
1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
@@ -206,7 +206,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
---
-## 5. Resources for GitLab Team Members
+### 5. Resources for GitLab Team Members
*Some content can only be accessed by GitLab team members*
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index 576b943b98c..5556dae2551 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.0.0
+sudo -u git -H git checkout v4.0.3
```
### 6. Update gitlab-workhorse
@@ -113,7 +113,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
-git diff origin/8-13-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example
+git diff origin/8-14-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example
```
#### Git configuration
@@ -131,10 +131,10 @@ Ensure you're still up-to-date with the latest NGINX configuration changes:
```sh
# For HTTPS configurations
-git diff origin/8-13-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl
+git diff origin/8-14-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl
# For HTTP configurations
-git diff origin/8-13-stable:lib/support/nginx/gitlab origin/8-15-stable:lib/support/nginx/gitlab
+git diff origin/8-14-stable:lib/support/nginx/gitlab origin/8-15-stable:lib/support/nginx/gitlab
```
If you are using Apache instead of NGINX please see the updated [Apache templates].
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index cefc55d07ab..adaf375453c 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -117,7 +117,12 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- select 'Developer', from: "member_access_level_#{member.id}"
+ click_button member.human_access
+
+ page.within '.dropdown-menu' do
+ click_link 'Developer'
+ end
+
wait_for_ajax
end
end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index b21d0849ad1..22d971fadfb 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -65,7 +65,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
- select "Reporter", from: "member_access_level_#{project_member.id}"
+ click_button project_member.human_access
+
+ page.within '.dropdown-menu' do
+ click_link 'Reporter'
+ end
end
end
@@ -144,7 +148,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
step 'I should see "Opensource" group user listing' do
page.within '.project-members-groups' do
expect(page).to have_content('OpenSource')
- expect(find('select').value).to eq('40')
+ expect(first('.group_member')).to have_content('Master')
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 899d68bc6c7..006d5f9f44e 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -22,7 +22,7 @@ module API
expose :provider, :extern_uid
end
- class UserFull < User
+ class UserPublic < User
expose :last_sign_in_at
expose :confirmed_at
expose :email
@@ -34,7 +34,7 @@ module API
expose :external
end
- class UserLogin < UserFull
+ class UserWithPrivateToken < UserPublic
expose :private_token
end
@@ -289,7 +289,7 @@ module API
end
class SSHKeyWithUser < SSHKey
- expose :user, using: Entities::UserFull
+ expose :user, using: Entities::UserPublic
end
class Note < Grape::Entity
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 164aea613e4..40096f367db 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -45,11 +45,14 @@ module API
return nil
end
- identifier = sudo_identifier()
+ identifier = sudo_identifier
- # If the sudo is the current user do nothing
- if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
+ if identifier
+ # We check for private_token because we cannot allow PAT to be used
forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
+ forbidden!('Private token must be specified in order to use sudo') unless private_token_used?
+
+ @impersonator = @current_user
@current_user = User.by_username_or_id(identifier)
not_found!("No user id or username for: #{identifier}") if @current_user.nil?
end
@@ -109,7 +112,7 @@ module API
if id =~ /^\d+$/
Group.find_by(id: id)
else
- Group.find_by(path: id)
+ Group.find_by_full_path(id)
end
end
@@ -212,22 +215,6 @@ module API
end
end
- def issuable_order_by
- if params["order_by"] == 'updated_at'
- 'updated_at'
- else
- 'created_at'
- end
- end
-
- def issuable_sort
- if params["sort"] == 'asc'
- :asc
- else
- :desc
- end
- end
-
def filter_by_iid(items, iid)
items.where(iid: iid)
end
@@ -362,6 +349,10 @@ module API
private
+ def private_token_used?
+ private_token == @current_user.private_token
+ end
+
def secret_token
Gitlab::Shell.secret_token
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 26c8f2fecd0..c9124649cbb 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -1,6 +1,7 @@
module API
- # Issues API
class Issues < Grape::API
+ include PaginationParams
+
before { authenticate! }
helpers do
@@ -20,77 +21,68 @@ module API
issues.includes(:milestone).where('milestones.title' => milestone)
end
- def issue_params
- new_params = declared(params, include_parent_namespace: false, include_missing: false).to_h
- new_params = new_params.with_indifferent_access
- new_params.delete(:id)
- new_params.delete(:issue_id)
+ params :issues_params do
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return issues sorted in `asc` or `desc` order.'
+ use :pagination
+ end
- new_params
+ params :issue_params do
+ optional :description, type: String, desc: 'The description of an issue'
+ optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
+ optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
+ optional :state_event, type: String, values: %w[open close],
+ desc: 'State of the issue'
end
end
resource :issues do
- # Get currently authenticated user's issues
- #
- # Parameters:
- # state (optional) - Return "opened" or "closed" issues
- # labels (optional) - Comma-separated list of label names
- # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- #
- # Example Requests:
- # GET /issues
- # GET /issues?state=opened
- # GET /issues?state=closed
- # GET /issues?labels=foo
- # GET /issues?labels=foo,bar
- # GET /issues?labels=foo,bar&state=opened
+ desc "Get currently authenticated user's issues" do
+ success Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ use :issues_params
+ end
get do
issues = current_user.issues.inc_notes_with_associations
- issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
+ issues = filter_issues_state(issues, params[:state])
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
- issues = issues.reorder(issuable_order_by => issuable_sort)
+ issues = issues.reorder(params[:order_by] => params[:sort])
present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
resource :groups do
- # Get a list of group issues
- #
- # Parameters:
- # id (required) - The ID of a group
- # state (optional) - Return "opened" or "closed" issues
- # labels (optional) - Comma-separated list of label names
- # milestone (optional) - Milestone title
- # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- #
- # Example Requests:
- # GET /groups/:id/issues
- # GET /groups/:id/issues?state=opened
- # GET /groups/:id/issues?state=closed
- # GET /groups/:id/issues?labels=foo
- # GET /groups/:id/issues?labels=foo,bar
- # GET /groups/:id/issues?labels=foo,bar&state=opened
- # GET /groups/:id/issues?milestone=1.0.0
- # GET /groups/:id/issues?milestone=1.0.0&state=closed
+ desc 'Get a list of group issues' do
+ success Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'opened',
+ desc: 'Return opened, closed, or all issues'
+ use :issues_params
+ end
get ":id/issues" do
- group = find_group!(params[:id])
+ group = find_group!(params.delete(:id))
- params[:state] ||= 'opened'
params[:group_id] = group.id
params[:milestone_title] = params.delete(:milestone)
params[:label_name] = params.delete(:labels)
- if params[:order_by] || params[:sort]
- # The Sortable concern takes 'created_desc', not 'created_at_desc' (for example)
- params[:sort] = "#{issuable_order_by.sub('_at', '')}_#{issuable_sort}"
- end
-
issues = IssuesFinder.new(current_user, params).execute
+ issues = issues.reorder(params[:order_by] => params[:sort])
present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
@@ -98,32 +90,19 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
-
resource :projects do
- # Get a list of project issues
- #
- # Parameters:
- # id (required) - The ID of a project
- # iid (optional) - Return the project issue having the given `iid`
- # state (optional) - Return "opened" or "closed" issues
- # labels (optional) - Comma-separated list of label names
- # milestone (optional) - Milestone title
- # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- #
- # Example Requests:
- # GET /projects/:id/issues
- # GET /projects/:id/issues?state=opened
- # GET /projects/:id/issues?state=closed
- # GET /projects/:id/issues?labels=foo
- # GET /projects/:id/issues?labels=foo,bar
- # GET /projects/:id/issues?labels=foo,bar&state=opened
- # GET /projects/:id/issues?milestone=1.0.0
- # GET /projects/:id/issues?milestone=1.0.0&state=closed
- # GET /issues?iid=42
+ desc 'Get a list of project issues' do
+ success Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ optional :iid, type: Integer, desc: 'The IID of the issue'
+ use :issues_params
+ end
get ":id/issues" do
issues = IssuesFinder.new(current_user, project_id: user_project.id).execute.inc_notes_with_associations
- issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
+ issues = filter_issues_state(issues, params[:state])
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
@@ -131,59 +110,49 @@ module API
issues = filter_issues_milestone(issues, params[:milestone])
end
- issues = issues.reorder(issuable_order_by => issuable_sort)
-
+ issues = issues.reorder(params[:order_by] => params[:sort])
present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
end
- # Get a single project issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # issue_id (required) - The ID of a project issue
- # Example Request:
- # GET /projects/:id/issues/:issue_id
+ desc 'Get a single project issue' do
+ success Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ end
get ":id/issues/:issue_id" do
- @issue = find_project_issue(params[:issue_id])
- present @issue, with: Entities::Issue, current_user: current_user, project: user_project
+ issue = find_project_issue(params[:issue_id])
+ present issue, with: Entities::Issue, current_user: current_user, project: user_project
end
- # Create a new project issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # title (required) - The title of an issue
- # description (optional) - The description of an issue
- # assignee_id (optional) - The ID of a user to assign issue
- # milestone_id (optional) - The ID of a milestone to assign issue
- # labels (optional) - The labels of an issue
- # created_at (optional) - Date time string, ISO 8601 formatted
- # due_date (optional) - Date time string in the format YEAR-MONTH-DAY
- # confidential (optional) - Boolean parameter if the issue should be confidential
- # merge_request_for_resolving_discussions (optional) - The IID of a merge request for which to resolve discussions
- # Example Request:
- # POST /projects/:id/issues
+ desc 'Create a new project issue' do
+ success Entities::Issue
+ end
+ params do
+ requires :title, type: String, desc: 'The title of an issue'
+ optional :created_at, type: DateTime,
+ desc: 'Date time when the issue was created. Available only for admins and project owners.'
+ optional :merge_request_for_resolving_discussions, type: Integer,
+ desc: 'The IID of a merge request for which to resolve discussions'
+ use :issue_params
+ end
post ':id/issues' do
- required_attributes! [:title]
-
- keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential, :labels, :merge_request_for_resolving_discussions]
- keys << :created_at if current_user.admin? || user_project.owner == current_user
- attrs = attributes_for_keys(keys)
+ # Setting created_at time only allowed for admins and project owners
+ unless current_user.admin? || user_project.owner == current_user
+ params.delete(:created_at)
+ end
- attrs[:labels] = params[:labels] if params[:labels]
+ issue_params = declared_params(include_missing: false)
if merge_request_iid = params[:merge_request_for_resolving_discussions]
- attrs[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
+ issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
execute.
find_by(iid: merge_request_iid)
end
- # Convert and filter out invalid confidential flags
- attrs['confidential'] = to_boolean(attrs['confidential'])
- attrs.delete('confidential') if attrs['confidential'].nil?
-
- issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute
-
+ issue = ::Issues::CreateService.new(user_project,
+ current_user,
+ issue_params.merge(request: request, api: true)).execute
if issue.spam?
render_api_error!({ error: 'Spam detected' }, 400)
end
@@ -199,31 +168,26 @@ module API
success Entities::Issue
end
params do
- requires :id, type: String, desc: 'The ID of a project'
- requires :issue_id, type: Integer, desc: "The ID of a project issue"
- optional :title, type: String, desc: 'The new title of the issue'
- optional :description, type: String, desc: 'The description of an issue'
- optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue'
- optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
- optional :labels, type: String, desc: 'The labels of an issue'
- optional :state_event, type: String, values: ['close', 'reopen'], desc: 'The state event of an issue'
- # TODO 9.0, use the Grape DateTime type here
- optional :updated_at, type: String, desc: 'Date time string, ISO 8601 formatted'
- optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
- # TODO 9.0, use the Grape boolean type here
- optional :confidential, type: String, desc: 'Boolean parameter if the issue should be confidential'
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ optional :title, type: String, desc: 'The title of an issue'
+ optional :updated_at, type: DateTime,
+ desc: 'Date time when the issue was updated. Available only for admins and project owners.'
+ use :issue_params
+ at_least_one_of :title, :description, :assignee_id, :milestone_id,
+ :labels, :created_at, :due_date, :confidential, :state_event
end
put ':id/issues/:issue_id' do
- issue = user_project.issues.find(params[:issue_id])
+ issue = user_project.issues.find(params.delete(:issue_id))
authorize! :update_issue, issue
- # Convert and filter out invalid confidential flags
- params[:confidential] = to_boolean(params[:confidential])
- params.delete(:confidential) if params[:confidential].nil?
-
- params.delete(:updated_at) unless current_user.admin? || user_project.owner == current_user
+ # Setting created_at time only allowed for admins and project owners
+ unless current_user.admin? || user_project.owner == current_user
+ params.delete(:updated_at)
+ end
- issue = ::Issues::UpdateService.new(user_project, current_user, issue_params).execute(issue)
+ issue = ::Issues::UpdateService.new(user_project,
+ current_user,
+ declared_params(include_missing: false)).execute(issue)
if issue.valid?
present issue, with: Entities::Issue, current_user: current_user, project: user_project
@@ -232,19 +196,19 @@ module API
end
end
- # Move an existing issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # issue_id (required) - The ID of a project issue
- # to_project_id (required) - The ID of the new project
- # Example Request:
- # POST /projects/:id/issues/:issue_id/move
+ desc 'Move an existing issue' do
+ success Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :to_project_id, type: Integer, desc: 'The ID of the new project'
+ end
post ':id/issues/:issue_id/move' do
- required_attributes! [:to_project_id]
+ issue = user_project.issues.find_by(id: params[:issue_id])
+ not_found!('Issue') unless issue
- issue = user_project.issues.find(params[:issue_id])
- new_project = Project.find(params[:to_project_id])
+ new_project = Project.find_by(id: params[:to_project_id])
+ not_found!('Project') unless new_project
begin
issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
@@ -254,16 +218,13 @@ module API
end
end
- #
- # Delete a project issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # issue_id (required) - The ID of a project issue
- # Example Request:
- # DELETE /projects/:id/issues/:issue_id
+ desc 'Delete a project issue'
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ end
delete ":id/issues/:issue_id" do
issue = user_project.issues.find_by(id: params[:issue_id])
+ not_found!('Issue') unless issue
authorize!(:destroy_issue, issue)
issue.destroy
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 253460830ff..55bdbc6a47c 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -30,6 +30,7 @@ module API
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
end
end
@@ -78,7 +79,8 @@ module API
post ":id/merge_requests" do
authorize! :create_merge_request, user_project
- mr_params = declared_params
+ mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
@@ -147,13 +149,15 @@ module API
desc: 'Status of the merge request'
use :optional_params
at_least_one_of :title, :target_branch, :description, :assignee_id,
- :milestone_id, :labels, :state_event
+ :milestone_id, :labels, :state_event,
+ :remove_source_branch
end
put path do
merge_request = user_project.merge_requests.find(params.delete(:merge_request_id))
authorize! :update_merge_request, merge_request
mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
diff --git a/lib/api/session.rb b/lib/api/session.rb
index d09400b81f5..002ffd1d154 100644
--- a/lib/api/session.rb
+++ b/lib/api/session.rb
@@ -1,7 +1,7 @@
module API
class Session < Grape::API
desc 'Login to get token' do
- success Entities::UserLogin
+ success Entities::UserWithPrivateToken
end
params do
optional :login, type: String, desc: 'The username'
@@ -14,7 +14,7 @@ module API
return unauthorized! unless user
return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
- present user, with: Entities::UserLogin
+ present user, with: Entities::UserWithPrivateToken
end
end
end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index cd33f9a9903..5b345db3a41 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -1,7 +1,6 @@
module API
# Git Tags API
class Tags < Grape::API
- before { authenticate! }
before { authorize! :download_code, user_project }
params do
diff --git a/lib/api/users.rb b/lib/api/users.rb
index bc2362aa72e..1dab799dd61 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -51,7 +51,7 @@ module API
users = users.external if params[:external] && current_user.is_admin?
end
- entity = current_user.is_admin? ? Entities::UserFull : Entities::UserBasic
+ entity = current_user.is_admin? ? Entities::UserPublic : Entities::UserBasic
present paginate(users), with: entity
end
@@ -66,7 +66,7 @@ module API
not_found!('User') unless user
if current_user && current_user.is_admin?
- present user, with: Entities::UserFull
+ present user, with: Entities::UserPublic
elsif can?(current_user, :read_user, user)
present user, with: Entities::User
else
@@ -75,7 +75,7 @@ module API
end
desc 'Create a user. Available only for admins.' do
- success Entities::UserFull
+ success Entities::UserPublic
end
params do
requires :email, type: String, desc: 'The email of the user'
@@ -99,7 +99,7 @@ module API
end
if user.save
- present user, with: Entities::UserFull
+ present user, with: Entities::UserPublic
else
conflict!('Email has already been taken') if User.
where(email: user.email).
@@ -114,7 +114,7 @@ module API
end
desc 'Update a user. Available only for admins.' do
- success Entities::UserFull
+ success Entities::UserPublic
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
@@ -161,7 +161,7 @@ module API
user_params.delete(:provider)
if user.update_attributes(user_params)
- present user, with: Entities::UserFull
+ present user, with: Entities::UserPublic
else
render_validation_error!(user)
end
@@ -350,10 +350,10 @@ module API
resource :user do
desc 'Get the currently authenticated user' do
- success Entities::UserFull
+ success Entities::UserPublic
end
get do
- present current_user, with: Entities::UserFull
+ present current_user, with: @impersonator ? Entities::UserWithPrivateToken : Entities::UserPublic
end
desc "Get the currently authenticated user's SSH keys" do
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 96c20100541..7e6537e3d9e 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -82,16 +82,17 @@ module Backup
removed = 0
Dir.chdir(Gitlab.config.backup.path) do
- file_list = Dir.glob('*_gitlab_backup.tar')
- file_list.map! do |path_string|
- if path_string =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/
- { timestamp: $1.to_i, path: path_string }
- end
- end
- file_list.sort.each do |file|
- if Time.at(file[:timestamp]) < (Time.now - keep_time)
- if Kernel.system(*%W(rm #{file[:path]}))
+ Dir.glob('*_gitlab_backup.tar').each do |file|
+ next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/
+
+ timestamp = $1.to_i
+
+ if Time.at(timestamp) < (Time.now - keep_time)
+ begin
+ FileUtils.rm(file)
removed += 1
+ rescue => e
+ $progress.puts "Deleting #{file} failed: #{e.message}".color(:red)
end
end
end
diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
index 5711d96a586..bae4db1ca4d 100644
--- a/lib/constraints/group_url_constrainer.rb
+++ b/lib/constraints/group_url_constrainer.rb
@@ -4,7 +4,7 @@ class GroupUrlConstrainer
return false unless valid?(id)
- Group.find_by(path: id).present?
+ Group.find_by_full_path(id).present?
end
private
diff --git a/lib/event_filter.rb b/lib/event_filter.rb
index 21f6a9a762b..515095af1c2 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -14,6 +14,10 @@ class EventFilter
'merged'
end
+ def issue
+ 'issue'
+ end
+
def comments
'comments'
end
@@ -32,32 +36,20 @@ class EventFilter
end
def apply_filter(events)
- return events unless params.present?
-
- filter = params.dup
- actions = []
+ return events if params.blank? || params == EventFilter.all
- case filter
+ case params
when EventFilter.push
- actions = [Event::PUSHED]
+ events.where(action: Event::PUSHED)
when EventFilter.merged
- actions = [Event::MERGED]
+ events.where(action: Event::MERGED)
when EventFilter.comments
- actions = [Event::COMMENTED]
+ events.where(action: Event::COMMENTED)
when EventFilter.team
- actions = [Event::JOINED, Event::LEFT, Event::EXPIRED]
- when EventFilter.all
- actions = [
- Event::PUSHED,
- Event::MERGED,
- Event::COMMENTED,
- Event::JOINED,
- Event::LEFT,
- Event::EXPIRED
- ]
+ events.where(action: [Event::JOINED, Event::LEFT, Event::EXPIRED])
+ when EventFilter.issue
+ events.where(action: [Event::CREATED, Event::UPDATED, Event::CLOSED, Event::REOPENED])
end
-
- events.where(action: actions)
end
def options(key)
@@ -73,6 +65,10 @@ class EventFilter
end
def active?(key)
- params.include? key
+ if params.present?
+ params.include? key
+ else
+ key == EventFilter.all
+ end
end
end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 1a22ad9acf5..9667df4ffb8 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -6,7 +6,7 @@ module Gitlab
module Asciidoc
DEFAULT_ADOC_ATTRS = [
'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab',
- 'env-gitlab', 'source-highlighter=html-pipeline'
+ 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font'
].freeze
# Public: Converts the provided Asciidoc markup into HTML.
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
new file mode 100644
index 00000000000..b2f896f2211
--- /dev/null
+++ b/lib/gitlab/ci/status/factory.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Ci
+ module Status
+ class Factory
+ attr_reader :subject
+
+ def initialize(subject)
+ @subject = subject
+ end
+
+ def fabricate!
+ if extended_status
+ extended_status.new(core_status)
+ else
+ core_status
+ end
+ end
+
+ private
+
+ def subject_status
+ @subject_status ||= subject.status
+ end
+
+ def core_status
+ Gitlab::Ci::Status
+ .const_get(subject_status.capitalize)
+ .new(subject)
+ end
+
+ def extended_status
+ @extended ||= extended_statuses.find do |status|
+ status.matches?(subject)
+ end
+ end
+
+ def extended_statuses
+ []
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 71d27bf7cf5..4ac4ec671d0 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -2,35 +2,15 @@ module Gitlab
module Ci
module Status
module Pipeline
- class Factory
- EXTENDED_STATUSES = [Pipeline::SuccessWithWarnings]
-
- def initialize(pipeline)
- @pipeline = pipeline
- @status = pipeline.status || :created
- end
-
- def fabricate!
- if extended_status
- extended_status.new(core_status)
- else
- core_status
- end
- end
-
+ class Factory < Status::Factory
private
- def core_status
- Gitlab::Ci::Status
- .const_get(@status.capitalize)
- .new(@pipeline)
- .extend(Status::Pipeline::Common)
+ def extended_statuses
+ [Pipeline::SuccessWithWarnings]
end
- def extended_status
- @extended ||= EXTENDED_STATUSES.find do |status|
- status.matches?(@pipeline)
- end
+ def core_status
+ super.extend(Status::Pipeline::Common)
end
end
end
diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb
new file mode 100644
index 00000000000..14c437d2b98
--- /dev/null
+++ b/lib/gitlab/ci/status/stage/common.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module Ci
+ module Status
+ module Stage
+ module Common
+ def has_details?
+ true
+ end
+
+ def details_path
+ namespace_project_pipeline_path(@subject.project.namespace,
+ @subject.project,
+ @subject.pipeline,
+ anchor: @subject.name)
+ end
+
+ def has_action?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
new file mode 100644
index 00000000000..c6522d5ada1
--- /dev/null
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Ci
+ module Status
+ module Stage
+ class Factory < Status::Factory
+ private
+
+ def core_status
+ super.extend(Status::Stage::Common)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index 06a783ebc1c..e50e54b6e99 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -22,7 +22,7 @@ module Gitlab
sha: pipeline.sha,
before_sha: pipeline.before_sha,
status: pipeline.status,
- stages: pipeline.stages,
+ stages: pipeline.stages_name,
created_at: pipeline.created_at,
finished_at: pipeline.finished_at,
duration: pipeline.duration
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 47d8599e298..35212992698 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -68,7 +68,7 @@ module Gitlab
end
def merge_requests
- merge_requests = MergeRequest.in_projects(project_ids_relation)
+ merge_requests = MergeRequestsFinder.new(current_user).execute.in_projects(project_ids_relation)
if query =~ /[#!](\d+)\z/
merge_requests = merge_requests.where(iid: $1)
else
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb
index 193a3f6b5a3..415c264e0dd 100644
--- a/spec/controllers/projects/todo_controller_spec.rb
+++ b/spec/controllers/projects/todo_controller_spec.rb
@@ -110,7 +110,7 @@ describe Projects::TodosController do
end
end
- context 'when not authorized' do
+ context 'when not authorized for project' do
it 'does not create todo for merge request user has no access to' do
sign_in(user)
expect do
@@ -128,6 +128,19 @@ describe Projects::TodosController do
expect(response).to have_http_status(302)
end
end
+
+ context 'when not authorized for merge_request' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ sign_in(user)
+ end
+
+ it "doesn't create todo" do
+ expect{ go }.not_to change { user.todos.count }
+ expect(response).to have_http_status(404)
+ end
+ end
end
end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 48d69377461..b56c7880b64 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -22,7 +22,6 @@ describe SessionsController do
it 'authenticates user correctly' do
post(:create, user: { login: user.username, password: user.password })
- expect(response).to set_flash.to /Signed in successfully/
expect(subject.current_user). to eq user
end
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
new file mode 100644
index 00000000000..ee3b17b8bf1
--- /dev/null
+++ b/spec/factories/ci/stages.rb
@@ -0,0 +1,13 @@
+FactoryGirl.define do
+ factory :ci_stage, class: Ci::Stage do
+ transient do
+ name 'test'
+ status nil
+ pipeline factory: :ci_empty_pipeline
+ end
+
+ initialize_with do
+ Ci::Stage.new(pipeline, name: name, status: status)
+ end
+ end
+end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index c7fe622c477..e1b97b31e5d 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -85,14 +85,14 @@ feature 'Environments page', :feature, :js do
end
scenario 'does show a play button' do
- find('.dropdown-play-icon-container').click
+ find('.js-dropdown-play-icon-container').click
expect(page).to have_content(manual.name.humanize)
end
scenario 'does allow to play manual action', js: true do
expect(manual).to be_skipped
- find('.dropdown-play-icon-container').click
+ find('.js-dropdown-play-icon-container').click
expect(page).to have_content(manual.name.humanize)
expect { click_link(manual.name.humanize) }
diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb
new file mode 100644
index 00000000000..b6134540273
--- /dev/null
+++ b/spec/features/merge_requests/target_branch_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe 'Target branch', feature: true do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+
+ def path_to_merge_request
+ namespace_project_merge_request_path(
+ project.namespace,
+ project, merge_request
+ )
+ end
+
+ before do
+ login_as user
+ project.team << [user, :master]
+ end
+
+ it 'shows link to target branch' do
+ visit path_to_merge_request
+ expect(page).to have_link('feature', href: namespace_project_commits_path(project.namespace, project, merge_request.target_branch))
+ end
+
+ context 'when branch was deleted' do
+ before do
+ DeleteBranchService.new(project, user).execute('feature')
+ visit path_to_merge_request
+ end
+
+ it 'shows a message about missing target branch' do
+ expect(page).to have_content(
+ 'Target branch feature does not exist'
+ )
+ end
+
+ it 'does not show link to target branch' do
+ expect(page).not_to have_link('feature')
+ end
+ end
+end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 09aa6758b5c..3bb33394be7 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -182,6 +182,44 @@ describe 'Edit Project Settings', feature: true do
expect(page).not_to have_content("Comments")
end
end
+
+ # Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/25272
+ it "hides comments activity tab only on disabled issues, merge requests and repository" do
+ select "Disabled", from: "project_project_feature_attributes_issues_access_level"
+
+ save_changes_and_check_activity_tab do
+ expect(page).to have_content("Comments")
+ end
+
+ visit edit_namespace_project_path(project.namespace, project)
+
+ select "Disabled", from: "project_project_feature_attributes_merge_requests_access_level"
+
+ save_changes_and_check_activity_tab do
+ expect(page).to have_content("Comments")
+ end
+
+ visit edit_namespace_project_path(project.namespace, project)
+
+ select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+
+ save_changes_and_check_activity_tab do
+ expect(page).not_to have_content("Comments")
+ end
+
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ def save_changes_and_check_activity_tab
+ click_button "Save changes"
+ wait_for_ajax
+
+ visit activity_namespace_project_path(project.namespace, project)
+
+ page.within(".event-filter") do
+ yield
+ end
+ end
end
# Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/24056
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
index cc2f695211c..94995f7cf95 100644
--- a/spec/features/projects/members/group_links_spec.rb
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -16,12 +16,17 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
end
it 'updates group access level' do
- select 'Guest', from: "member_access_level_#{group.id}"
+ click_button @group_link.human_access
+
+ page.within '.dropdown-menu' do
+ click_link 'Guest'
+ end
+
wait_for_ajax
visit namespace_project_project_members_path(project.namespace, project)
- expect(page).to have_select("member_access_level_#{group.id}", selected: 'Guest')
+ expect(first('.group_member')).to have_content('Guest')
end
it 'updates expiry date' do
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
new file mode 100644
index 00000000000..7d0065ee2c4
--- /dev/null
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+feature 'Projects members', feature: true do
+ let(:user) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:group) { create(:group, :public, :access_requestable) }
+ let(:project) { create(:empty_project, :public, :access_requestable, creator: user, group: group) }
+ let(:project_invitee) { create(:project_member, project: project, invite_token: '123', invite_email: 'test1@abc.com', user: nil) }
+ let(:group_invitee) { create(:group_member, group: group, invite_token: '123', invite_email: 'test2@abc.com', user: nil) }
+ let(:project_requester) { create(:user) }
+ let(:group_requester) { create(:user) }
+
+ background do
+ project.team << [developer, :developer]
+ group.add_owner(user)
+ login_as(user)
+ end
+
+ context 'with a group invitee' do
+ before do
+ group_invitee
+ visit namespace_project_project_members_path(project.namespace, project)
+ end
+
+ scenario 'does not appear in the project members page' do
+ page.within first('.content-list') do
+ expect(page).not_to have_content('test2@abc.com')
+ end
+ end
+ end
+
+ context 'with a group and a project invitee' do
+ before do
+ group_invitee
+ project_invitee
+ visit namespace_project_project_members_path(project.namespace, project)
+ end
+
+ scenario 'shows the project invitee, the project developer, and the group owner' do
+ page.within first('.content-list') do
+ expect(page).to have_content('test1@abc.com')
+ expect(page).not_to have_content('test2@abc.com')
+
+ # Project developer
+ expect(page).to have_content(developer.name)
+
+ # Group owner
+ expect(page).to have_content(user.name)
+ expect(page).to have_content(group.name)
+ end
+ end
+ end
+
+ context 'with a group requester' do
+ before do
+ group.request_access(group_requester)
+ visit namespace_project_project_members_path(project.namespace, project)
+ end
+
+ scenario 'does not appear in the project members page' do
+ page.within first('.content-list') do
+ expect(page).not_to have_content(group_requester.name)
+ end
+ end
+ end
+
+ context 'with a group and a project requesters' do
+ before do
+ group.request_access(group_requester)
+ project.request_access(project_requester)
+ visit namespace_project_project_members_path(project.namespace, project)
+ end
+
+ scenario 'shows the project requester, the project developer, and the group owner' do
+ page.within first('.content-list') do
+ expect(page).to have_content(project_requester.name)
+ expect(page).not_to have_content(group_requester.name)
+ end
+
+ page.within all('.content-list').last do
+ # Project developer
+ expect(page).to have_content(developer.name)
+
+ # Group owner
+ expect(page).to have_content(user.name)
+ expect(page).to have_content(group.name)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
new file mode 100644
index 00000000000..4bfaa499272
--- /dev/null
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+feature 'Project settings > Merge Requests', feature: true, js: true do
+ include GitlabRoutingHelper
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ background do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'when Merge Request and Builds are initially enabled' do
+ before do
+ project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED)
+ end
+
+ context 'when Builds are initially enabled' do
+ before do
+ project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED)
+ visit edit_project_path(project)
+ end
+
+ scenario 'shows the Merge Requests settings' do
+ expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+
+ select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level"
+
+ expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ end
+ end
+
+ context 'when Builds are initially disabled' do
+ before do
+ project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED)
+ visit edit_project_path(project)
+ end
+
+ scenario 'shows the Merge Requests settings that do not depend on Builds feature' do
+ expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+
+ select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level"
+
+ expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ end
+ end
+ end
+
+ context 'when Merge Request are initially disabled' do
+ before do
+ project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED)
+ visit edit_project_path(project)
+ end
+
+ scenario 'does not show the Merge Requests settings' do
+ expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
+
+ select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level"
+
+ expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ end
+ end
+end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index b750f27ea72..be21b403084 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -163,8 +163,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
-
- expect(page.body).to match('Signed in successfully')
+ expect(page.body).to match('href="/users/sign_out"')
end
end
@@ -178,7 +177,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
- expect(page.body).to match('Signed in successfully')
+ expect(page.body).to match('href="/users/sign_out"')
end
end
@@ -234,7 +233,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
- expect(page.body).to match('Signed in successfully')
+ expect(page.body).to match('href="/users/sign_out"')
end
end
end
@@ -275,7 +274,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
- expect(page.body).to match('Signed in successfully')
+ expect(page.body).to match('href="/users/sign_out"')
logout
end
diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/user/login.json
new file mode 100644
index 00000000000..e6c1d9c9d84
--- /dev/null
+++ b/spec/fixtures/api/schemas/user/login.json
@@ -0,0 +1,37 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "username",
+ "email",
+ "name",
+ "state",
+ "avatar_url",
+ "web_url",
+ "created_at",
+ "is_admin",
+ "bio",
+ "location",
+ "skype",
+ "linkedin",
+ "twitter",
+ "website_url",
+ "organization",
+ "last_sign_in_at",
+ "confirmed_at",
+ "theme_id",
+ "color_scheme_id",
+ "projects_limit",
+ "current_sign_in_at",
+ "identities",
+ "can_create_group",
+ "can_create_project",
+ "two_factor_enabled",
+ "external",
+ "private_token"
+ ],
+ "properties": {
+ "$ref": "full.json",
+ "private_token": { "type": "string" }
+ }
+}
diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/user/public.json
new file mode 100644
index 00000000000..dbd5d32e89c
--- /dev/null
+++ b/spec/fixtures/api/schemas/user/public.json
@@ -0,0 +1,79 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "username",
+ "email",
+ "name",
+ "state",
+ "avatar_url",
+ "web_url",
+ "created_at",
+ "is_admin",
+ "bio",
+ "location",
+ "skype",
+ "linkedin",
+ "twitter",
+ "website_url",
+ "organization",
+ "last_sign_in_at",
+ "confirmed_at",
+ "theme_id",
+ "color_scheme_id",
+ "projects_limit",
+ "current_sign_in_at",
+ "identities",
+ "can_create_group",
+ "can_create_project",
+ "two_factor_enabled",
+ "external"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "username": { "type": "string" },
+ "email": {
+ "type": "string",
+ "pattern": "^[^@]+@[^@]+$"
+ },
+ "name": { "type": "string" },
+ "state": {
+ "type": "string",
+ "enum": ["active", "blocked"]
+ },
+ "avatar_url": { "type": "string" },
+ "web_url": { "type": "string" },
+ "created_at": { "type": "date" },
+ "is_admin": { "type": "boolean" },
+ "bio": { "type": ["string", "null"] },
+ "location": { "type": ["string", "null"] },
+ "skype": { "type": "string" },
+ "linkedin": { "type": "string" },
+ "twitter": { "type": "string "},
+ "website_url": { "type": "string" },
+ "organization": { "type": ["string", "null"] },
+ "last_sign_in_at": { "type": "date" },
+ "confirmed_at": { "type": ["date", "null"] },
+ "theme_id": { "type": "integer" },
+ "color_scheme_id": { "type": "integer" },
+ "projects_limit": { "type": "integer" },
+ "current_sign_in_at": { "type": "date" },
+ "identities": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "provider": {
+ "type": "string",
+ "enum": ["github", "bitbucket", "google_oauth2"]
+ },
+ "extern_uid": { "type": ["number", "string"] }
+ }
+ }
+ },
+ "can_create_group": { "type": "boolean" },
+ "can_create_project": { "type": "boolean" },
+ "two_factor_enabled": { "type": "boolean" },
+ "external": { "type": "boolean" }
+ }
+}
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 837e7afa7e8..468bcc7badc 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -60,15 +60,58 @@ describe DiffHelper do
end
describe '#diff_line_content' do
- it 'returns non breaking space when line is empty' do
- expect(diff_line_content(nil)).to eq('&nbsp;')
- end
-
- it 'returns the line itself' do
- expect(diff_line_content(diff_file.diff_lines.first.text)).
- to eq('@@ -6,12 +6,18 @@ module Popen')
- expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match')
- expect(diff_file.diff_lines.first.new_pos).to eq(6)
+ context 'when the line is empty' do
+ it 'returns a non breaking space' do
+ expect(diff_line_content(nil)).to eq('&nbsp;')
+ end
+
+ it 'returns an HTML-safe string' do
+ expect(diff_line_content(nil)).to be_html_safe
+ end
+ end
+
+ context 'when the line is not empty' do
+ context 'when the line starts with +, -, or a space' do
+ it 'strips the first character' do
+ expect(diff_line_content('+new line')).to eq('new line')
+ expect(diff_line_content('-new line')).to eq('new line')
+ expect(diff_line_content(' new line')).to eq('new line')
+ end
+
+ context 'when the line is HTML-safe' do
+ it 'returns an HTML-safe string' do
+ expect(diff_line_content('+new line'.html_safe)).to be_html_safe
+ expect(diff_line_content('-new line'.html_safe)).to be_html_safe
+ expect(diff_line_content(' new line'.html_safe)).to be_html_safe
+ end
+ end
+
+ context 'when the line is not HTML-safe' do
+ it 'returns a non-HTML-safe string' do
+ expect(diff_line_content('+new line')).not_to be_html_safe
+ expect(diff_line_content('-new line')).not_to be_html_safe
+ expect(diff_line_content(' new line')).not_to be_html_safe
+ end
+ end
+ end
+
+ context 'when the line does not start with a +, -, or a space' do
+ it 'returns the string' do
+ expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen')).to eq('@@ -6,12 +6,18 @@ module Popen')
+ end
+
+ context 'when the line is HTML-safe' do
+ it 'returns an HTML-safe string' do
+ expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen'.html_safe)).to be_html_safe
+ end
+ end
+
+ context 'when the line is not HTML-safe' do
+ it 'returns a non-HTML-safe string' do
+ expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen')).not_to be_html_safe
+ end
+ end
+ end
end
end
diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6
index 76e81233e89..4bae3f30bb5 100644
--- a/spec/javascripts/environments/environment_actions_spec.js.es6
+++ b/spec/javascripts/environments/environment_actions_spec.js.es6
@@ -8,7 +8,7 @@ describe('Actions Component', () => {
fixture.load('environments/element.html');
});
- it('Should render a dropdown with the provided actions', () => {
+ it('should render a dropdown with the provided actions', () => {
const actionsMock = [
{
name: 'bar',
@@ -24,6 +24,7 @@ describe('Actions Component', () => {
el: document.querySelector('.test-dom-element'),
propsData: {
actions: actionsMock,
+ playIconSvg: '<svg></svg>',
},
});
@@ -34,4 +35,33 @@ describe('Actions Component', () => {
component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
).toEqual(actionsMock[0].play_path);
});
+
+ it('should render a dropdown with the provided svg', () => {
+ const actionsMock = [
+ {
+ name: 'bar',
+ play_path: 'https://gitlab.com/play',
+ },
+ {
+ name: 'foo',
+ play_path: '#',
+ },
+ ];
+
+ const component = new window.gl.environmentsList.ActionsComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ actions: actionsMock,
+ playIconSvg: '<svg></svg>',
+ },
+ });
+
+ expect(
+ component.$el.querySelector('.js-dropdown-play-icon-container').children,
+ ).toContain('svg');
+
+ expect(
+ component.$el.querySelector('.js-action-play-icon-container').children,
+ ).toContain('svg');
+ });
});
diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6
index 156506ef28f..9f82567c35b 100644
--- a/spec/javascripts/environments/environment_external_url_spec.js.es6
+++ b/spec/javascripts/environments/environment_external_url_spec.js.es6
@@ -7,12 +7,12 @@ describe('External URL Component', () => {
fixture.load('environments/element.html');
});
- it('should link to the provided external_url', () => {
+ it('should link to the provided externalUrl prop', () => {
const externalURL = 'https://gitlab.com';
const component = new window.gl.environmentsList.ExternalUrlComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
- external_url: externalURL,
+ externalUrl: externalURL,
},
});
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6
index 29449bbbd9e..77ba0ab38ec 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js.es6
+++ b/spec/javascripts/environments/environment_rollback_spec.js.es6
@@ -9,24 +9,24 @@ describe('Rollback Component', () => {
fixture.load('environments/element.html');
});
- it('Should link to the provided retry_url', () => {
+ it('Should link to the provided retryUrl', () => {
const component = new window.gl.environmentsList.RollbackComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
- retry_url: retryURL,
- is_last_deployment: true,
+ retryUrl: retryURL,
+ isLastDeployment: true,
},
});
expect(component.$el.getAttribute('href')).toEqual(retryURL);
});
- it('Should render Re-deploy label when is_last_deployment is true', () => {
+ it('Should render Re-deploy label when isLastDeployment is true', () => {
const component = new window.gl.environmentsList.RollbackComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
- retry_url: retryURL,
- is_last_deployment: true,
+ retryUrl: retryURL,
+ isLastDeployment: true,
},
});
@@ -34,12 +34,12 @@ describe('Rollback Component', () => {
});
- it('Should render Rollback label when is_last_deployment is false', () => {
+ it('Should render Rollback label when isLastDeployment is false', () => {
const component = new window.gl.environmentsList.RollbackComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
- retry_url: retryURL,
- is_last_deployment: false,
+ retryUrl: retryURL,
+ isLastDeployment: false,
},
});
diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6
index b842be4da61..84a41b2bf46 100644
--- a/spec/javascripts/environments/environment_stop_spec.js.es6
+++ b/spec/javascripts/environments/environment_stop_spec.js.es6
@@ -13,7 +13,7 @@ describe('Stop Component', () => {
component = new window.gl.environmentsList.StopComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
- stop_url: stopURL,
+ stopUrl: stopURL,
},
});
});
diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml
index 95e248cadf8..5477c6075f0 100644
--- a/spec/javascripts/fixtures/event_filter.html.haml
+++ b/spec/javascripts/fixtures/event_filter.html.haml
@@ -12,6 +12,10 @@
%span
Merge events
%li
+ %a.event-filter-link{ id: "issue_event_filter", title: "Filter by issue events", href: "/dashboard/activity"}
+ %span
+ Issue events
+ %li
%a.event-filter-link{ id: "comments_event_filter", title: "Filter by comments", href: "/dashboard/activity"}
%span
Comments
diff --git a/spec/javascripts/fixtures/signin_tabs.html.haml b/spec/javascripts/fixtures/signin_tabs.html.haml
new file mode 100644
index 00000000000..12b8d423cbe
--- /dev/null
+++ b/spec/javascripts/fixtures/signin_tabs.html.haml
@@ -0,0 +1,5 @@
+%ul.nav-tabs
+ %li
+ %a.active{ id: 'standard', href: '#standard'} Standard
+ %li
+ %a{ id: 'ldap', href: '#ldap'} Ldap
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6
new file mode 100644
index 00000000000..9a9fb22255b
--- /dev/null
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6
@@ -0,0 +1,53 @@
+/*= require signin_tabs_memoizer */
+
+((global) => {
+ describe('SigninTabsMemoizer', () => {
+ const fixtureTemplate = 'signin_tabs.html';
+ const tabSelector = 'ul.nav-tabs';
+ const currentTabKey = 'current_signin_tab';
+ let memo;
+
+ function createMemoizer() {
+ memo = new global.ActiveTabMemoizer({
+ currentTabKey,
+ tabSelector,
+ });
+ return memo;
+ }
+
+ fixture.preload(fixtureTemplate);
+
+ beforeEach(() => {
+ fixture.load(fixtureTemplate);
+ });
+
+ it('does nothing if no tab was previously selected', () => {
+ createMemoizer();
+
+ expect(document.querySelector('li a.active').getAttribute('id')).toEqual('standard');
+ });
+
+ it('shows last selected tab on boot', () => {
+ createMemoizer().saveData('#ldap');
+ const fakeTab = {
+ click: () => {},
+ };
+ spyOn(document, 'querySelector').and.returnValue(fakeTab);
+ spyOn(fakeTab, 'click');
+
+ memo.bootstrap();
+
+ // verify that triggers click on the last selected tab
+ expect(document.querySelector).toHaveBeenCalledWith(`${tabSelector} a[href="#ldap"]`);
+ expect(fakeTab.click).toHaveBeenCalled();
+ });
+
+ it('saves last selected tab on change', () => {
+ createMemoizer();
+
+ document.getElementById('standard').click();
+
+ expect(memo.readData()).toEqual('#standard');
+ });
+ });
+})(window);
diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6
index d170517dd9b..26dfdb94aae 100644
--- a/spec/javascripts/vue_common_components/commit_spec.js.es6
+++ b/spec/javascripts/vue_common_components/commit_spec.js.es6
@@ -10,12 +10,12 @@ describe('Commit component', () => {
el: document.querySelector('.test-commit-container'),
propsData: {
tag: false,
- commit_ref: {
+ commitRef: {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
- commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- short_sha: 'b7836edd',
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
title: 'Commit message',
author: {
avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
@@ -34,18 +34,19 @@ describe('Commit component', () => {
props = {
tag: true,
- commit_ref: {
+ commitRef: {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
- commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- short_sha: 'b7836edd',
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
title: 'Commit message',
author: {
avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
web_url: 'https://gitlab.com/jschatz1',
username: 'jschatz1',
},
+ commitIconSvg: '<svg></svg>',
};
component = new window.gl.CommitComponent({
@@ -59,20 +60,24 @@ describe('Commit component', () => {
});
it('should render a link to the ref url', () => {
- expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commit_ref.ref_url);
+ expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
});
it('should render the ref name', () => {
- expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commit_ref.name);
+ expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name);
});
it('should render the commit short sha with a link to the commit url', () => {
- expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commit_url);
- expect(component.$el.querySelector('.commit-id').textContent).toContain(props.short_sha);
+ expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl);
+ expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha);
+ });
+
+ it('should render the given commitIconSvg', () => {
+ expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg');
});
describe('Given commit title and author props', () => {
- it('Should render a link to the author profile', () => {
+ it('should render a link to the author profile', () => {
expect(
component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'),
).toEqual(props.author.web_url);
@@ -91,7 +96,7 @@ describe('Commit component', () => {
it('should render the commit title', () => {
expect(
component.$el.querySelector('a.commit-row-message').getAttribute('href'),
- ).toEqual(props.commit_url);
+ ).toEqual(props.commitUrl);
expect(
component.$el.querySelector('a.commit-row-message').textContent,
).toContain(props.title);
@@ -99,16 +104,16 @@ describe('Commit component', () => {
});
describe('When commit title is not provided', () => {
- it('Should render default message', () => {
+ it('should render default message', () => {
fixture.set('<div class="test-commit-container"></div>');
props = {
tag: false,
- commit_ref: {
+ commitRef: {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
- commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- short_sha: 'b7836edd',
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
title: null,
author: {},
};
diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb
index 892554f2870..96dacdc5cd2 100644
--- a/spec/lib/constraints/group_url_constrainer_spec.rb
+++ b/spec/lib/constraints/group_url_constrainer_spec.rb
@@ -10,6 +10,13 @@ describe GroupUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_truthy }
end
+ context 'valid request for nested group' do
+ let!(:nested_group) { create(:group, path: 'nested', parent: group) }
+ let!(:request) { build_request('gitlab/nested') }
+
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+
context 'invalid request' do
let(:request) { build_request('foo') }
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index a6d8e6927e0..ec2f66b1136 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -7,6 +7,10 @@ describe EventFilter, lib: true do
let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) }
let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) }
+ let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) }
+ let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) }
+ let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) }
+ let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) }
let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) }
let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) }
let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) }
@@ -21,6 +25,11 @@ describe EventFilter, lib: true do
expect(events).to contain_exactly(merged_event)
end
+ it 'applies issue filter' do
+ events = EventFilter.new(EventFilter.issue).apply_filter(Event.all)
+ expect(events).to contain_exactly(created_event, updated_event, closed_event, reopened_event)
+ end
+
it 'applies comments filter' do
events = EventFilter.new(EventFilter.comments).apply_filter(Event.all)
expect(events).to contain_exactly(comments_event)
@@ -33,17 +42,17 @@ describe EventFilter, lib: true do
it 'applies all filter' do
events = EventFilter.new(EventFilter.all).apply_filter(Event.all)
- expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event)
end
it 'applies no filter' do
events = EventFilter.new(nil).apply_filter(Event.all)
- expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event)
end
it 'applies unknown filter' do
events = EventFilter.new('').apply_filter(Event.all)
- expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event)
end
end
end
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb
new file mode 100644
index 00000000000..1b749d1bd39
--- /dev/null
+++ b/spec/lib/gitlab/backup/manager_spec.rb
@@ -0,0 +1,127 @@
+require 'spec_helper'
+
+describe Backup::Manager, lib: true do
+ describe '#remove_old' do
+ let(:progress) { StringIO.new }
+
+ let(:files) do
+ [
+ '1451606400_2016_01_01_gitlab_backup.tar',
+ '1451520000_2015_12_31_gitlab_backup.tar',
+ '1450742400_2015_12_22_gitlab_backup.tar',
+ '1449878400_gitlab_backup.tar',
+ '1449014400_gitlab_backup.tar',
+ 'manual_gitlab_backup.tar'
+ ]
+ end
+
+ before do
+ allow(Dir).to receive(:chdir).and_yield
+ allow(Dir).to receive(:glob).and_return(files)
+ allow(FileUtils).to receive(:rm)
+ allow(Time).to receive(:now).and_return(Time.utc(2016))
+
+ allow(progress).to receive(:puts)
+ allow(progress).to receive(:print)
+
+ allow_any_instance_of(String).to receive(:color) do |string, _color|
+ string
+ end
+
+ @old_progress = $progress # rubocop:disable Style/GlobalVars
+ $progress = progress # rubocop:disable Style/GlobalVars
+ end
+
+ after do
+ $progress = @old_progress # rubocop:disable Style/GlobalVars
+ end
+
+ context 'when keep_time is zero' do
+ before do
+ allow(Gitlab.config.backup).to receive(:keep_time).and_return(0)
+
+ subject.remove_old
+ end
+
+ it 'removes no files' do
+ expect(FileUtils).not_to have_received(:rm)
+ end
+
+ it 'prints a skipped message' do
+ expect(progress).to have_received(:puts).with('skipping')
+ end
+ end
+
+ context 'when there are no files older than keep_time' do
+ before do
+ allow(Gitlab.config.backup).to receive(:keep_time).and_return(2592000)
+
+ subject.remove_old
+ end
+
+ it 'removes no files' do
+ expect(FileUtils).not_to have_received(:rm)
+ end
+
+ it 'prints a done message' do
+ expect(progress).to have_received(:puts).with('done. (0 removed)')
+ end
+ end
+
+ context 'when keep_time is set to remove files' do
+ before do
+ allow(Gitlab.config.backup).to receive(:keep_time).and_return(1)
+
+ subject.remove_old
+ end
+
+ it 'removes matching files with a human-readable timestamp' do
+ expect(FileUtils).to have_received(:rm).with(files[1])
+ expect(FileUtils).to have_received(:rm).with(files[2])
+ end
+
+ it 'removes matching files without a human-readable timestamp' do
+ expect(FileUtils).to have_received(:rm).with(files[3])
+ expect(FileUtils).to have_received(:rm).with(files[4])
+ end
+
+ it 'does not remove files that are not old enough' do
+ expect(FileUtils).not_to have_received(:rm).with(files[0])
+ end
+
+ it 'does not remove non-matching files' do
+ expect(FileUtils).not_to have_received(:rm).with(files[5])
+ end
+
+ it 'prints a done message' do
+ expect(progress).to have_received(:puts).with('done. (4 removed)')
+ end
+ end
+
+ context 'when removing a file fails' do
+ let(:file) { files[1] }
+ let(:message) { "Permission denied @ unlink_internal - #{file}" }
+
+ before do
+ allow(Gitlab.config.backup).to receive(:keep_time).and_return(1)
+ allow(FileUtils).to receive(:rm).with(file).and_raise(Errno::EACCES, message)
+
+ subject.remove_old
+ end
+
+ it 'removes the remaining expected files' do
+ expect(FileUtils).to have_received(:rm).with(files[2])
+ expect(FileUtils).to have_received(:rm).with(files[3])
+ expect(FileUtils).to have_received(:rm).with(files[4])
+ end
+
+ it 'sets the correct removed count' do
+ expect(progress).to have_received(:puts).with('done. (3 removed)')
+ end
+
+ it 'prints the error from file that could not be removed' do
+ expect(progress).to have_received(:puts).with(a_string_matching(message))
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb
new file mode 100644
index 00000000000..d5bd7f7102b
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/factory_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Factory do
+ subject do
+ described_class.new(object)
+ end
+
+ let(:status) { subject.fabricate! }
+
+ context 'when object has a core status' do
+ HasStatus::AVAILABLE_STATUSES.each do |core_status|
+ context "when core status is #{core_status}" do
+ let(:object) { double(status: core_status) }
+
+ it "fabricates a core status #{core_status}" do
+ expect(status).to be_a(
+ Gitlab::Ci::Status.const_get(core_status.capitalize))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb
new file mode 100644
index 00000000000..f3259c6f23e
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Stage::Common do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+ let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') }
+
+ subject do
+ Class.new(Gitlab::Ci::Status::Core)
+ .new(stage).extend(described_class)
+ end
+
+ it 'does not have action' do
+ expect(subject).not_to have_action
+ end
+
+ it 'has details' do
+ expect(subject).to have_details
+ end
+
+ it 'links to the pipeline details page' do
+ expect(subject.details_path)
+ .to include "pipelines/#{pipeline.id}"
+ expect(subject.details_path)
+ .to include "##{stage.name}"
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
new file mode 100644
index 00000000000..17929665c83
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Stage::Factory do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+ let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') }
+
+ subject do
+ described_class.new(stage)
+ end
+
+ let(:status) do
+ subject.fabricate!
+ end
+
+ context 'when stage has a core status' do
+ HasStatus::AVAILABLE_STATUSES.each do |core_status|
+ context "when core status is #{core_status}" do
+ before do
+ create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status)
+ create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status)
+ create(:ci_build, pipeline: pipeline, stage: 'build', status: :failed)
+ end
+
+ it "fabricates a core status #{core_status}" do
+ expect(status).to be_a(
+ Gitlab::Ci::Status.const_get(core_status.capitalize))
+ end
+
+ it 'extends core status with common stage methods' do
+ expect(status).to have_details
+ expect(status.details_path).to include "pipelines/#{pipeline.id}"
+ expect(status.details_path).to include "##{stage.name}"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 7e00e214c6e..8e1a28f2723 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -188,6 +188,7 @@ project:
- project_feature
- authorized_users
- project_authorizations
+- route
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index f23e3522625..9614aad3e73 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -40,6 +40,15 @@ describe Gitlab::SearchResults do
expect(results.milestones_count).to eq(1)
end
end
+
+ it 'includes merge requests from source and target projects' do
+ forked_project = create(:empty_project, forked_from_project: project)
+ merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo')
+
+ results = described_class.new(user, Project.where(id: forked_project.id), 'foo')
+
+ expect(results.objects('merge_requests')).to include merge_request_2
+ end
end
it 'does not list issues on private projects' do
@@ -152,4 +161,11 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 5
end
end
+
+ it 'does not list merge requests on projects with limited access' do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+
+ expect(results.objects('merge_requests')).not_to include merge_request
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 3f93d9ddf19..8158e71dd55 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -20,8 +20,6 @@ describe Ci::Pipeline, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
- it { is_expected.to delegate_method(:stages).to(:statuses) }
-
describe '#valid_commit_sha' do
context 'commit.sha can not start with 00000000' do
before do
@@ -125,16 +123,55 @@ describe Ci::Pipeline, models: true do
end
describe '#stages' do
- let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project }
- subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages }
-
before do
- FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1
- FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0
+ create(:commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success')
+ create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed')
+ create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running')
+ create(:commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success')
+ end
+
+ subject { pipeline.stages }
+
+ context 'stages list' do
+ it 'returns ordered list of stages' do
+ expect(subject.map(&:name)).to eq(%w[build test deploy])
+ end
+ end
+
+ it 'returns a valid number of stages' do
+ expect(pipeline.stages_count).to eq(3)
+ end
+
+ it 'returns a valid names of stages' do
+ expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
end
- it 'return all stages' do
- is_expected.to eq(%w(build test))
+ context 'stages with statuses' do
+ let(:statuses) do
+ subject.map do |stage|
+ [stage.name, stage.status]
+ end
+ end
+
+ it 'returns list of stages with statuses' do
+ expect(statuses).to eq([['build', 'failed'],
+ ['test', 'success'],
+ ['deploy', 'running']
+ ])
+ end
+
+ context 'when build is retried' do
+ before do
+ create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success')
+ end
+
+ it 'ignores the previous state' do
+ expect(statuses).to eq([['build', 'success'],
+ ['test', 'success'],
+ ['deploy', 'running']
+ ])
+ end
+ end
end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
new file mode 100644
index 00000000000..f232761dba2
--- /dev/null
+++ b/spec/models/ci/stage_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe Ci::Stage, models: true do
+ let(:stage) { build(:ci_stage) }
+ let(:pipeline) { stage.pipeline }
+ let(:stage_name) { stage.name }
+
+ describe '#expectations' do
+ subject { stage }
+
+ it { is_expected.to include_module(StaticModel) }
+
+ it { is_expected.to respond_to(:pipeline) }
+ it { is_expected.to respond_to(:name) }
+
+ it { is_expected.to delegate_method(:project).to(:pipeline) }
+ end
+
+ describe '#statuses' do
+ let!(:stage_build) { create_job(:ci_build) }
+ let!(:commit_status) { create_job(:commit_status) }
+ let!(:other_build) { create_job(:ci_build, stage: 'other stage') }
+
+ subject { stage.statuses }
+
+ it "returns only matching statuses" do
+ is_expected.to contain_exactly(stage_build, commit_status)
+ end
+ end
+
+ describe '#builds' do
+ let!(:stage_build) { create_job(:ci_build) }
+ let!(:commit_status) { create_job(:commit_status) }
+
+ subject { stage.builds }
+
+ it "returns only builds" do
+ is_expected.to contain_exactly(stage_build)
+ end
+ end
+
+ describe '#status' do
+ subject { stage.status }
+
+ context 'if status is already defined' do
+ let(:stage) { build(:ci_stage, status: 'success') }
+
+ it "returns defined status" do
+ is_expected.to eq('success')
+ end
+ end
+
+ context 'if status has to be calculated' do
+ let!(:stage_build) { create_job(:ci_build, status: :failed) }
+
+ it "returns status of a build" do
+ is_expected.to eq('failed')
+ end
+
+ context 'and builds are retried' do
+ let!(:new_build) { create_job(:ci_build, status: :success) }
+
+ it "returns status of latest build" do
+ is_expected.to eq('success')
+ end
+ end
+ end
+ end
+
+ describe '#detailed_status' do
+ subject { stage.detailed_status }
+
+ context 'when build is created' do
+ let!(:stage_build) { create_job(:ci_build, status: :created) }
+
+ it 'returns detailed status for created stage' do
+ expect(subject.text).to eq 'created'
+ end
+ end
+
+ context 'when build is pending' do
+ let!(:stage_build) { create_job(:ci_build, status: :pending) }
+
+ it 'returns detailed status for pending stage' do
+ expect(subject.text).to eq 'pending'
+ end
+ end
+
+ context 'when build is running' do
+ let!(:stage_build) { create_job(:ci_build, status: :running) }
+
+ it 'returns detailed status for running stage' do
+ expect(subject.text).to eq 'running'
+ end
+ end
+
+ context 'when build is successful' do
+ let!(:stage_build) { create_job(:ci_build, status: :success) }
+
+ it 'returns detailed status for successful stage' do
+ expect(subject.text).to eq 'passed'
+ end
+ end
+
+ context 'when build is failed' do
+ let!(:stage_build) { create_job(:ci_build, status: :failed) }
+
+ it 'returns detailed status for failed stage' do
+ expect(subject.text).to eq 'failed'
+ end
+ end
+
+ context 'when build is canceled' do
+ let!(:stage_build) { create_job(:ci_build, status: :canceled) }
+
+ it 'returns detailed status for canceled stage' do
+ expect(subject.text).to eq 'canceled'
+ end
+ end
+
+ context 'when build is skipped' do
+ let!(:stage_build) { create_job(:ci_build, status: :skipped) }
+
+ it 'returns detailed status for skipped stage' do
+ expect(subject.text).to eq 'skipped'
+ end
+ end
+ end
+
+ def create_job(type, status: 'success', stage: stage_name)
+ create(type, pipeline: pipeline, stage: stage, status: status)
+ end
+end
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index d89d4342dea..30782ca75a0 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -137,26 +137,25 @@ describe CommitRange, models: true do
end
describe '#has_been_reverted?' do
- it 'returns true if the commit has been reverted' do
- issue = create(:issue)
+ let(:issue) { create(:issue) }
+ let(:user) { issue.author }
+ it 'returns true if the commit has been reverted' do
create(:note_on_issue,
noteable: issue,
system: true,
- note: commit1.revert_description,
+ note: commit1.revert_description(user),
project: issue.project)
expect_any_instance_of(Commit).to receive(:reverts_commit?).
- with(commit1).
+ with(commit1, user).
and_return(true)
- expect(commit1.has_been_reverted?(nil, issue)).to eq(true)
+ expect(commit1.has_been_reverted?(user, issue)).to eq(true)
end
it 'returns false a commit has not been reverted' do
- issue = create(:issue)
-
- expect(commit1.has_been_reverted?(nil, issue)).to eq(false)
+ expect(commit1.has_been_reverted?(user, issue)).to eq(false)
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index eb482c7f913..0935fc0561c 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -179,25 +179,26 @@ eos
describe '#reverts_commit?' do
let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") }
+ let(:user) { commit.author }
- it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+ it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
context 'commit has no description' do
before { allow(commit).to receive(:description?).and_return(false) }
- it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+ it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
end
context "another_commit's description does not revert commit" do
before { allow(commit).to receive(:description).and_return("Foo Bar") }
- it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+ it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
end
context "another_commit's description reverts commit" do
before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") }
- it { expect(commit.reverts_commit?(another_commit)).to be_truthy }
+ it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy }
end
context "another_commit's description reverts merged merge request" do
@@ -207,7 +208,7 @@ eos
allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar")
end
- it { expect(commit.reverts_commit?(another_commit)).to be_truthy }
+ it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy }
end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 80c2a1bc7a9..1ec08c2a9d0 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -175,7 +175,7 @@ describe CommitStatus, models: true do
end
it 'returns statuses without what we want to ignore' do
- is_expected.to eq(statuses.values_at(1, 2, 4, 5, 6, 8, 9))
+ is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9))
end
end
@@ -200,49 +200,6 @@ describe CommitStatus, models: true do
end
end
- describe '#stages' do
- before do
- create :commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success'
- create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed'
- create :commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running'
- create :commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success'
- end
-
- context 'stages list' do
- subject { CommitStatus.where(pipeline: pipeline).stages }
-
- it 'returns ordered list of stages' do
- is_expected.to eq(%w[build test deploy])
- end
- end
-
- context 'stages with statuses' do
- subject { CommitStatus.where(pipeline: pipeline).latest.stages_status }
-
- it 'returns list of stages with statuses' do
- is_expected.to eq({
- 'build' => 'failed',
- 'test' => 'success',
- 'deploy' => 'running'
- })
- end
-
- context 'when build is retried' do
- before do
- create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success'
- end
-
- it 'ignores a previous state' do
- is_expected.to eq({
- 'build' => 'success',
- 'test' => 'success',
- 'deploy' => 'running'
- })
- end
- end
- end
- end
-
describe '#commit' do
it 'returns commit pipeline has been created for' do
expect(commit_status.commit).to eq project.commit
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index 9defb17dc92..4d0f51fe82a 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -48,7 +48,7 @@ describe HasStatus do
[create(type, status: :failed, allow_failure: true)]
end
- it { is_expected.to eq 'success' }
+ it { is_expected.to eq 'skipped' }
end
context 'success and canceled' do
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
new file mode 100644
index 00000000000..0acefc0c1d5
--- /dev/null
+++ b/spec/models/concerns/routable_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Group, 'Routable' do
+ let!(:group) { create(:group) }
+
+ describe 'Associations' do
+ it { is_expected.to have_one(:route).dependent(:destroy) }
+ end
+
+ describe 'Callbacks' do
+ it 'creates route record on create' do
+ expect(group.route.path).to eq(group.path)
+ end
+
+ it 'updates route record on path change' do
+ group.update_attributes(path: 'wow')
+
+ expect(group.route.path).to eq('wow')
+ end
+
+ it 'ensure route path uniqueness across different objects' do
+ create(:group, parent: group, path: 'xyz')
+ duplicate = build(:project, namespace: group, path: 'xyz')
+
+ expect { duplicate.save! }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Route path has already been taken, Route is invalid')
+ end
+ end
+
+ describe '.find_by_full_path' do
+ let!(:nested_group) { create(:group, parent: group) }
+
+ it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
+ it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
+ it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
+ it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
+ end
+
+ describe '.where_paths_in' do
+ context 'without any paths' do
+ it 'returns an empty relation' do
+ expect(described_class.where_paths_in([])).to eq([])
+ end
+ end
+
+ context 'without any valid paths' do
+ it 'returns an empty relation' do
+ expect(described_class.where_paths_in(%w[unknown])).to eq([])
+ end
+ end
+
+ context 'with valid paths' do
+ let!(:nested_group) { create(:group, parent: group) }
+
+ it 'returns the projects matching the paths' do
+ result = described_class.where_paths_in([group.to_param, nested_group.to_param])
+
+ expect(result).to contain_exactly(group, nested_group)
+ end
+
+ it 'returns projects regardless of the casing of paths' do
+ result = described_class.where_paths_in([group.to_param.upcase, nested_group.to_param.upcase])
+
+ expect(result).to contain_exactly(group, nested_group)
+ end
+ end
+ end
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index ba0ed4a3603..7f82e85563b 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -124,4 +124,12 @@ describe Namespace, models: true do
expect(Namespace.clean_path("--%+--valid_*&%name=.git.%.atom.atom.@email.com")).to eq("valid_name")
end
end
+
+ describe '#full_path' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+
+ it { expect(group.full_path).to eq(group.path) }
+ it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4d57330ed1c..21ff238841e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -478,35 +478,6 @@ describe Project, models: true do
end
end
- describe '.find_with_namespace' do
- context 'with namespace' do
- before do
- @group = create :group, name: 'gitlab'
- @project = create(:project, name: 'gitlabhq', namespace: @group)
- end
-
- it { expect(Project.find_with_namespace('gitlab/gitlabhq')).to eq(@project) }
- it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) }
- it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil }
- end
-
- context 'when multiple projects using a similar name exist' do
- let(:group) { create(:group, name: 'gitlab') }
-
- let!(:project1) do
- create(:empty_project, name: 'gitlab1', path: 'gitlab', namespace: group)
- end
-
- let!(:project2) do
- create(:empty_project, name: 'gitlab2', path: 'GITLAB', namespace: group)
- end
-
- it 'returns the row where the path matches literally' do
- expect(Project.find_with_namespace('gitlab/GITLAB')).to eq(project2)
- end
- end
- end
-
describe '#to_param' do
context 'with namespace' do
before do
@@ -1548,39 +1519,6 @@ describe Project, models: true do
end
end
- describe '.where_paths_in' do
- context 'without any paths' do
- it 'returns an empty relation' do
- expect(Project.where_paths_in([])).to eq([])
- end
- end
-
- context 'without any valid paths' do
- it 'returns an empty relation' do
- expect(Project.where_paths_in(%w[foo])).to eq([])
- end
- end
-
- context 'with valid paths' do
- let!(:project1) { create(:project) }
- let!(:project2) { create(:project) }
-
- it 'returns the projects matching the paths' do
- projects = Project.where_paths_in([project1.path_with_namespace,
- project2.path_with_namespace])
-
- expect(projects).to contain_exactly(project1, project2)
- end
-
- it 'returns projects regardless of the casing of paths' do
- projects = Project.where_paths_in([project1.path_with_namespace.upcase,
- project2.path_with_namespace.upcase])
-
- expect(projects).to contain_exactly(project1, project2)
- end
- end
- end
-
describe 'change_head' do
let(:project) { create(:project) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index d9b0e63eeb6..b5a42edd192 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1176,7 +1176,7 @@ describe Repository, models: true do
end
describe '#after_create_branch' do
- it 'flushes the visible content cache' do
+ it 'expires the branch caches' do
expect(repository).to receive(:expire_branches_cache)
repository.after_create_branch
@@ -1184,7 +1184,7 @@ describe Repository, models: true do
end
describe '#after_remove_branch' do
- it 'flushes the visible content cache' do
+ it 'expires the branch caches' do
expect(repository).to receive(:expire_branches_cache)
repository.after_remove_branch
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
new file mode 100644
index 00000000000..6f491fdf9a0
--- /dev/null
+++ b/spec/models/route_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Route, models: true do
+ let!(:group) { create(:group) }
+ let!(:route) { group.route }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:source) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:source) }
+ it { is_expected.to validate_presence_of(:path) }
+ it { is_expected.to validate_uniqueness_of(:path) }
+ end
+
+ describe '#rename_children' do
+ let!(:nested_group) { create(:group, path: "test", parent: group) }
+ let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) }
+
+ it "updates children routes with new path" do
+ route.update_attributes(path: 'bar')
+
+ expect(described_class.exists?(path: 'bar')).to be_truthy
+ expect(described_class.exists?(path: 'bar/test')).to be_truthy
+ expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy
+ end
+ end
+end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 36517ad0f8c..3f34309f419 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -153,85 +153,144 @@ describe API::Helpers, api: true do
end
end
- it "changes current user to sudo when admin" do
- set_env(admin, user.id)
- expect(current_user).to eq(user)
- set_param(admin, user.id)
- expect(current_user).to eq(user)
- set_env(admin, user.username)
- expect(current_user).to eq(user)
- set_param(admin, user.username)
- expect(current_user).to eq(user)
- end
+ context 'sudo usage' do
+ context 'with admin' do
+ context 'with header' do
+ context 'with id' do
+ it 'changes current_user to sudo' do
+ set_env(admin, user.id)
- it "throws an error when the current user is not an admin and attempting to sudo" do
- set_env(user, admin.id)
- expect { current_user }.to raise_error(Exception)
- set_param(user, admin.id)
- expect { current_user }.to raise_error(Exception)
- set_env(user, admin.username)
- expect { current_user }.to raise_error(Exception)
- set_param(user, admin.username)
- expect { current_user }.to raise_error(Exception)
- end
+ expect(current_user).to eq(user)
+ end
- it "throws an error when the user cannot be found for a given id" do
- id = user.id + admin.id
- expect(user.id).not_to eq(id)
- expect(admin.id).not_to eq(id)
- set_env(admin, id)
- expect { current_user }.to raise_error(Exception)
+ it 'handles sudo to oneself' do
+ set_env(admin, admin.id)
- set_param(admin, id)
- expect { current_user }.to raise_error(Exception)
- end
+ expect(current_user).to eq(admin)
+ end
- it "throws an error when the user cannot be found for a given username" do
- username = "#{user.username}#{admin.username}"
- expect(user.username).not_to eq(username)
- expect(admin.username).not_to eq(username)
- set_env(admin, username)
- expect { current_user }.to raise_error(Exception)
+ it 'throws an error when user cannot be found' do
+ id = user.id + admin.id
+ expect(user.id).not_to eq(id)
+ expect(admin.id).not_to eq(id)
- set_param(admin, username)
- expect { current_user }.to raise_error(Exception)
- end
+ set_env(admin, id)
- it "handles sudo's to oneself" do
- set_env(admin, admin.id)
- expect(current_user).to eq(admin)
- set_param(admin, admin.id)
- expect(current_user).to eq(admin)
- set_env(admin, admin.username)
- expect(current_user).to eq(admin)
- set_param(admin, admin.username)
- expect(current_user).to eq(admin)
- end
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
- it "handles multiple sudo's to oneself" do
- set_env(admin, user.id)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
- set_env(admin, user.username)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
-
- set_param(admin, user.id)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
- set_param(admin, user.username)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
- end
+ context 'with username' do
+ it 'changes current_user to sudo' do
+ set_env(admin, user.username)
+
+ expect(current_user).to eq(user)
+ end
+
+ it 'handles sudo to oneself' do
+ set_env(admin, admin.username)
+
+ expect(current_user).to eq(admin)
+ end
+
+ it "throws an error when the user cannot be found for a given username" do
+ username = "#{user.username}#{admin.username}"
+ expect(user.username).not_to eq(username)
+ expect(admin.username).not_to eq(username)
+
+ set_env(admin, username)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+ end
+
+ context 'with param' do
+ context 'with id' do
+ it 'changes current_user to sudo' do
+ set_param(admin, user.id)
+
+ expect(current_user).to eq(user)
+ end
+
+ it 'handles sudo to oneself' do
+ set_param(admin, admin.id)
+
+ expect(current_user).to eq(admin)
+ end
+
+ it 'handles sudo to oneself using string' do
+ set_env(admin, user.id.to_s)
+
+ expect(current_user).to eq(user)
+ end
+
+ it 'throws an error when user cannot be found' do
+ id = user.id + admin.id
+ expect(user.id).not_to eq(id)
+ expect(admin.id).not_to eq(id)
- it "handles multiple sudo's to oneself using string ids" do
- set_env(admin, user.id.to_s)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
+ set_param(admin, id)
- set_param(admin, user.id.to_s)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+
+ context 'with username' do
+ it 'changes current_user to sudo' do
+ set_param(admin, user.username)
+
+ expect(current_user).to eq(user)
+ end
+
+ it 'handles sudo to oneself' do
+ set_param(admin, admin.username)
+
+ expect(current_user).to eq(admin)
+ end
+
+ it "throws an error when the user cannot be found for a given username" do
+ username = "#{user.username}#{admin.username}"
+ expect(user.username).not_to eq(username)
+ expect(admin.username).not_to eq(username)
+
+ set_param(admin, username)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+ end
+ end
+
+ context 'with regular user' do
+ context 'with env' do
+ it 'changes current_user to sudo when admin and user id' do
+ set_env(user, admin.id)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+
+ it 'changes current_user to sudo when admin and user username' do
+ set_env(user, admin.username)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+
+ context 'with params' do
+ it 'changes current_user to sudo when admin and user id' do
+ set_param(user, admin.id)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+
+ it 'changes current_user to sudo when admin and user username' do
+ set_param(user, admin.username)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 553983575c4..5c80dd98dc7 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -72,13 +72,6 @@ describe API::Issues, api: true do
expect(json_response.last).to have_key('web_url')
end
- it "adds pagination headers and keep query params" do
- get api("/issues?state=closed&per_page=3", user)
- expect(response.headers['Link']).to eq(
- '<http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="first", <http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="last"' % [user.private_token, user.private_token]
- )
- end
-
it 'returns an array of closed issues' do
get api('/issues?state=closed', user)
expect(response).to have_http_status(200)
@@ -649,9 +642,8 @@ describe API::Issues, api: true do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: 'foo'
- expect(response).to have_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_falsy
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
end
it "sends notifications for subscribers of newly added labels" do
@@ -862,8 +854,8 @@ describe API::Issues, api: true do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
confidential: 'foo'
- expect(response).to have_http_status(200)
- expect(json_response['confidential']).to be_truthy
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
end
end
end
@@ -985,6 +977,14 @@ describe API::Issues, api: true do
expect(json_response['state']).to eq 'opened'
end
end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ delete api("/projects/#{project.id}/issues/123", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
end
describe '/projects/:id/issues/:issue_id/move' do
@@ -1033,6 +1033,7 @@ describe API::Issues, api: true do
to_project_id: target_project.id
expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Issue Not Found')
end
end
@@ -1042,6 +1043,7 @@ describe API::Issues, api: true do
to_project_id: target_project.id
expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 0b89ac7960e..75b270aa93c 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -234,11 +234,14 @@ describe API::MergeRequests, api: true do
target_branch: 'master',
author: user,
labels: 'label, label2',
- milestone_id: milestone.id
+ milestone_id: milestone.id,
+ remove_source_branch: true
+
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['labels']).to eq(['label', 'label2'])
expect(json_response['milestone']['id']).to eq(milestone.id)
+ expect(json_response['force_remove_source_branch']).to be_truthy
end
it "returns 422 when source_branch equals target_branch" do
@@ -511,6 +514,13 @@ describe API::MergeRequests, api: true do
expect(json_response['target_branch']).to eq('wiki')
end
+ it "returns merge_request that removes the source branch" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['force_remove_source_branch']).to be_truthy
+ end
+
it 'allows special label names' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
title: 'new issue',
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 06fa94fae87..a1c32ae65ba 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -15,6 +15,31 @@ describe API::Tags, api: true do
let(:tag_name) { project.repository.tag_names.sort.reverse.first }
let(:description) { 'Awesome release!' }
+ shared_examples_for 'repository tags' do
+ it 'returns the repository tags' do
+ get api("/projects/#{project.id}/repository/tags", current_user)
+
+ expect(response).to have_http_status(200)
+
+ first_tag = json_response.first
+
+ expect(first_tag['name']).to eq(tag_name)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'repository tags' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'repository tags' do
+ let(:current_user) { user }
+ end
+ end
+
context 'without releases' do
it "returns an array of project tags" do
get api("/projects/#{project.id}/repository/tags", user)
@@ -45,17 +70,33 @@ describe API::Tags, api: true do
describe 'GET /projects/:id/repository/tags/:tag_name' do
let(:tag_name) { project.repository.tag_names.sort.reverse.first }
- it 'returns a specific tag' do
- get api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
+ shared_examples_for 'repository tag' do
+ it 'returns the repository tag' do
+ get api("/projects/#{project.id}/repository/tags/#{tag_name}", current_user)
+
+ expect(response).to have_http_status(200)
+
+ expect(json_response['name']).to eq(tag_name)
+ end
+
+ it 'returns 404 for an invalid tag name' do
+ get api("/projects/#{project.id}/repository/tags/foobar", current_user)
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(tag_name)
+ expect(response).to have_http_status(404)
+ end
end
- it 'returns 404 for an invalid tag name' do
- get api("/projects/#{project.id}/repository/tags/foobar", user)
+ context 'when unauthenticated' do
+ it_behaves_like 'repository tag' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
- expect(response).to have_http_status(404)
+ context 'when authenticated' do
+ it_behaves_like 'repository tag' do
+ let(:current_user) { user }
+ end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index f82f52e7399..c37dbfa0a33 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -651,20 +651,75 @@ describe API::Users, api: true do
end
describe "GET /user" do
- it "returns current user" do
- get api("/user", user)
- expect(response).to have_http_status(200)
- expect(json_response['email']).to eq(user.email)
- expect(json_response['is_admin']).to eq(user.is_admin?)
- expect(json_response['can_create_project']).to eq(user.can_create_project?)
- expect(json_response['can_create_group']).to eq(user.can_create_group?)
- expect(json_response['projects_limit']).to eq(user.projects_limit)
- expect(json_response['private_token']).to be_blank
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+ let(:private_token) { user.private_token }
+
+ context 'with regular user' do
+ context 'with personal access token' do
+ it 'returns 403 without private token when sudo is defined' do
+ get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}")
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'with private token' do
+ it 'returns 403 without private token when sudo defined' do
+ get api("/user?private_token=#{private_token}&sudo=#{user.id}")
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ it 'returns current user without private token when sudo not defined' do
+ get api("/user", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('user/public')
+ end
end
- it "returns 401 error if user is unauthenticated" do
- get api("/user")
- expect(response).to have_http_status(401)
+ context 'with admin' do
+ let(:user) { create(:admin) }
+
+ context 'with personal access token' do
+ it 'returns 403 without private token when sudo defined' do
+ get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}")
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns current user without private token when sudo not defined' do
+ get api("/user?private_token=#{personal_access_token.token}")
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('user/public')
+ end
+ end
+
+ context 'with private token' do
+ it 'returns current user with private token when sudo defined' do
+ get api("/user?private_token=#{private_token}&sudo=#{user.id}")
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('user/login')
+ end
+
+ it 'returns current user without private token when sudo not defined' do
+ get api("/user?private_token=#{private_token}")
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('user/public')
+ end
+ end
+ end
+
+ context 'with unauthenticated user' do
+ it "returns 401 error if user is unauthenticated" do
+ get api("/user")
+
+ expect(response).to have_http_status(401)
+ end
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index bc340ff9d3c..7e3705983fb 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -126,17 +126,27 @@ describe MergeRequests::RefreshService, services: true do
end
context 'push to fork repo target branch' do
- before do
- service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
- reload_mrs
+ describe 'changes to merge requests' do
+ before do
+ service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
+ reload_mrs
+ end
+
+ it { expect(@merge_request.notes).to be_empty }
+ it { expect(@merge_request).to be_open }
+ it { expect(@fork_merge_request.notes).to be_empty }
+ it { expect(@fork_merge_request).to be_open }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
- it { expect(@merge_request.notes).to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ describe 'merge request diff' do
+ it 'does not reload the diff of the merge request made from fork' do
+ expect do
+ service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
+ end.not_to change { @fork_merge_request.reload.merge_request_diff }
+ end
+ end
end
context 'push to origin repo target branch after fork project was removed' do